remnantchat/sdk/sdk.ts

177 lines
4.9 KiB
TypeScript
Raw Normal View History

feat(sdk): Implement Chitchatter SDK (#183) * feat(sdk): render iframe in chat-room component * fix(ci): install optional dependencies * feat(sdk): allow subset of attributes * feat(sdk): accept root-domain attribute * feat(sdk): accept custom room name or use sane default * feat(sdk): set allowed features * feat(sdk): add sdk instructions to embed code dialog * fix(sdk): use dynamic rootUrl * fix(sdk): use static defaultRoot * feat(sdk): send config from SDK to chat * fix(sdk): expire poller * fix(sdk): pass parent domain to iframe via query param * refactor(sdk): type message event data * feat(sdk): send user id to chat frame * feat(sdk): handle some attribute updates * chore(package): add build:sdk:watch script * refactor(sdk): move more code to updateIframeAttributes * feat(sdk): support changing rooms * feat(sdk): support more user settings * docs(sdk): add SDK section to README * feat(sdk): render root-url in embed code if necessary * refactor(sdk): use map for chat room attributes * fix(sdk): unbind event listener when chat-room is disconnected * fix(sdk): properly tear down receipt listener * fix(sdk): send config when frame reloads * feat(sdk): listen for config updates * feat(sdk): request config from sdk instead of sending it repeatedly * refactor(sdk): use type guard for config message * fix(sdk): use settings from SDK when there is no preexisting persisted data * fix(sdk): observe all iframe attributes * refactor(sdk): simplify bootup logic * feat(sdk): improve embed code display
2023-10-28 16:42:58 +00:00
import {
ChatEmbedAttributes,
PostMessageEvent,
PostMessageEventName,
isPostMessageEvent,
} from '../src/models/sdk'
import { QueryParamKeys } from '../src/models/shell'
import { isColorMode, UserSettings } from '../src/models/settings'
import { iframeFeatureAllowList } from '../src/config/iframeFeatureAllowList'
export const defaultRoot = 'https://remnant.chat/'
feat(sdk): Implement Chitchatter SDK (#183) * feat(sdk): render iframe in chat-room component * fix(ci): install optional dependencies * feat(sdk): allow subset of attributes * feat(sdk): accept root-domain attribute * feat(sdk): accept custom room name or use sane default * feat(sdk): set allowed features * feat(sdk): add sdk instructions to embed code dialog * fix(sdk): use dynamic rootUrl * fix(sdk): use static defaultRoot * feat(sdk): send config from SDK to chat * fix(sdk): expire poller * fix(sdk): pass parent domain to iframe via query param * refactor(sdk): type message event data * feat(sdk): send user id to chat frame * feat(sdk): handle some attribute updates * chore(package): add build:sdk:watch script * refactor(sdk): move more code to updateIframeAttributes * feat(sdk): support changing rooms * feat(sdk): support more user settings * docs(sdk): add SDK section to README * feat(sdk): render root-url in embed code if necessary * refactor(sdk): use map for chat room attributes * fix(sdk): unbind event listener when chat-room is disconnected * fix(sdk): properly tear down receipt listener * fix(sdk): send config when frame reloads * feat(sdk): listen for config updates * feat(sdk): request config from sdk instead of sending it repeatedly * refactor(sdk): use type guard for config message * fix(sdk): use settings from SDK when there is no preexisting persisted data * fix(sdk): observe all iframe attributes * refactor(sdk): simplify bootup logic * feat(sdk): improve embed code display
2023-10-28 16:42:58 +00:00
// NOTE: This is a subset of standard iframe attributes:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attributes
const iframeAttributes = [
'height',
'referrerpolicy',
'sandbox',
'style',
'width',
]
const configRequestTimeout = 10_000
class ChatEmbed extends HTMLElement {
private configRequestExpirationTimout: NodeJS.Timeout | null = null
feat(sdk): Implement Chitchatter SDK (#183) * feat(sdk): render iframe in chat-room component * fix(ci): install optional dependencies * feat(sdk): allow subset of attributes * feat(sdk): accept root-domain attribute * feat(sdk): accept custom room name or use sane default * feat(sdk): set allowed features * feat(sdk): add sdk instructions to embed code dialog * fix(sdk): use dynamic rootUrl * fix(sdk): use static defaultRoot * feat(sdk): send config from SDK to chat * fix(sdk): expire poller * fix(sdk): pass parent domain to iframe via query param * refactor(sdk): type message event data * feat(sdk): send user id to chat frame * feat(sdk): handle some attribute updates * chore(package): add build:sdk:watch script * refactor(sdk): move more code to updateIframeAttributes * feat(sdk): support changing rooms * feat(sdk): support more user settings * docs(sdk): add SDK section to README * feat(sdk): render root-url in embed code if necessary * refactor(sdk): use map for chat room attributes * fix(sdk): unbind event listener when chat-room is disconnected * fix(sdk): properly tear down receipt listener * fix(sdk): send config when frame reloads * feat(sdk): listen for config updates * feat(sdk): request config from sdk instead of sending it repeatedly * refactor(sdk): use type guard for config message * fix(sdk): use settings from SDK when there is no preexisting persisted data * fix(sdk): observe all iframe attributes * refactor(sdk): simplify bootup logic * feat(sdk): improve embed code display
2023-10-28 16:42:58 +00:00
private iframe = document.createElement('iframe')
static get observedAttributes() {
const chatAttributes = Object.values(ChatEmbedAttributes)
return [...chatAttributes, ...iframeAttributes]
}
get chatConfig() {
const chatConfig: Partial<UserSettings> = {}
if (this.hasAttribute(ChatEmbedAttributes.USER_ID)) {
chatConfig.userId = this.getAttribute(ChatEmbedAttributes.USER_ID) ?? ''
}
if (this.hasAttribute(ChatEmbedAttributes.USER_NAME)) {
chatConfig.customUsername = this.getAttribute(
ChatEmbedAttributes.USER_NAME
)!
}
chatConfig.playSoundOnNewMessage = Boolean(
this.hasAttribute(ChatEmbedAttributes.PLAY_MESSAGE_SOUND)
)
const colorMode = this.getAttribute(ChatEmbedAttributes.COLOR_MODE) ?? ''
if (isColorMode(colorMode)) {
chatConfig.colorMode = colorMode
}
return chatConfig
}
get rootUrl() {
return this.getAttribute(ChatEmbedAttributes.ROOT_URL) ?? defaultRoot
}
private sendConfigToChat = () => {
const { iframe, rootUrl } = this
const { origin: rootUrlOrigin } = new URL(rootUrl)
const postMessageEventData: PostMessageEvent['data'] = {
name: PostMessageEventName.CONFIG,
payload: this.chatConfig,
}
iframe.contentWindow?.postMessage(postMessageEventData, rootUrlOrigin)
}
private handleConfigRequestedMessage = (event: MessageEvent) => {
const { rootUrl } = this
const { origin: rootUrlOrigin } = new URL(rootUrl)
if (rootUrlOrigin !== event.origin) return
if (!isPostMessageEvent(event)) return
if (event.data.name !== PostMessageEventName.CONFIG_REQUESTED) return
this.sendConfigToChat()
this.stopListeningForConfigRequest()
}
private stopListeningForConfigRequest = () => {
window.removeEventListener('message', this.handleConfigRequestedMessage)
if (this.configRequestExpirationTimout !== null) {
clearInterval(this.configRequestExpirationTimout)
this.configRequestExpirationTimout = null
feat(sdk): Implement Chitchatter SDK (#183) * feat(sdk): render iframe in chat-room component * fix(ci): install optional dependencies * feat(sdk): allow subset of attributes * feat(sdk): accept root-domain attribute * feat(sdk): accept custom room name or use sane default * feat(sdk): set allowed features * feat(sdk): add sdk instructions to embed code dialog * fix(sdk): use dynamic rootUrl * fix(sdk): use static defaultRoot * feat(sdk): send config from SDK to chat * fix(sdk): expire poller * fix(sdk): pass parent domain to iframe via query param * refactor(sdk): type message event data * feat(sdk): send user id to chat frame * feat(sdk): handle some attribute updates * chore(package): add build:sdk:watch script * refactor(sdk): move more code to updateIframeAttributes * feat(sdk): support changing rooms * feat(sdk): support more user settings * docs(sdk): add SDK section to README * feat(sdk): render root-url in embed code if necessary * refactor(sdk): use map for chat room attributes * fix(sdk): unbind event listener when chat-room is disconnected * fix(sdk): properly tear down receipt listener * fix(sdk): send config when frame reloads * feat(sdk): listen for config updates * feat(sdk): request config from sdk instead of sending it repeatedly * refactor(sdk): use type guard for config message * fix(sdk): use settings from SDK when there is no preexisting persisted data * fix(sdk): observe all iframe attributes * refactor(sdk): simplify bootup logic * feat(sdk): improve embed code display
2023-10-28 16:42:58 +00:00
}
}
private async listenForConfigRequest() {
// NOTE: This cancels any pending config request listeners
this.stopListeningForConfigRequest()
window.addEventListener('message', this.handleConfigRequestedMessage)
this.configRequestExpirationTimout = setTimeout(() => {
console.error(`[remnantchat-sdk] configuration was not sent successfully`)
feat(sdk): Implement Chitchatter SDK (#183) * feat(sdk): render iframe in chat-room component * fix(ci): install optional dependencies * feat(sdk): allow subset of attributes * feat(sdk): accept root-domain attribute * feat(sdk): accept custom room name or use sane default * feat(sdk): set allowed features * feat(sdk): add sdk instructions to embed code dialog * fix(sdk): use dynamic rootUrl * fix(sdk): use static defaultRoot * feat(sdk): send config from SDK to chat * fix(sdk): expire poller * fix(sdk): pass parent domain to iframe via query param * refactor(sdk): type message event data * feat(sdk): send user id to chat frame * feat(sdk): handle some attribute updates * chore(package): add build:sdk:watch script * refactor(sdk): move more code to updateIframeAttributes * feat(sdk): support changing rooms * feat(sdk): support more user settings * docs(sdk): add SDK section to README * feat(sdk): render root-url in embed code if necessary * refactor(sdk): use map for chat room attributes * fix(sdk): unbind event listener when chat-room is disconnected * fix(sdk): properly tear down receipt listener * fix(sdk): send config when frame reloads * feat(sdk): listen for config updates * feat(sdk): request config from sdk instead of sending it repeatedly * refactor(sdk): use type guard for config message * fix(sdk): use settings from SDK when there is no preexisting persisted data * fix(sdk): observe all iframe attributes * refactor(sdk): simplify bootup logic * feat(sdk): improve embed code display
2023-10-28 16:42:58 +00:00
this.stopListeningForConfigRequest()
}, configRequestTimeout)
}
private updateIframeAttributes() {
const { iframe } = this
const roomName = encodeURIComponent(
this.getAttribute(ChatEmbedAttributes.ROOM_NAME) ?? window.location.href
)
const urlParams = new URLSearchParams({
[QueryParamKeys.IS_EMBEDDED]: '',
[QueryParamKeys.GET_SDK_CONFIG]: '',
[QueryParamKeys.PARENT_DOMAIN]: encodeURIComponent(
window.location.origin
),
})
const iframeSrc = new URL(this.rootUrl)
iframeSrc.pathname = `public/${roomName}`
iframeSrc.search = urlParams.toString()
const { href: src } = iframeSrc
// NOTE: Only update src if the value has changed to avoid reloading the
// iframe unnecessarily.
if (src !== iframe.getAttribute('src')) {
iframe.setAttribute('src', src)
}
for (let attributeName of iframeAttributes) {
const attributeValue = this.getAttribute(attributeName)
if (attributeValue !== null) {
iframe.setAttribute(attributeName, attributeValue)
}
}
}
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
const { iframe } = this
iframe.style.border = 'none'
iframe.setAttribute('allow', iframeFeatureAllowList.join(';'))
shadow.appendChild(iframe)
iframe.addEventListener('load', () => {
this.listenForConfigRequest()
})
}
disconnectedCallback() {
this.stopListeningForConfigRequest()
}
attributeChangedCallback(name: string) {
this.updateIframeAttributes()
const isChatEmbedAttribute = Object.values(ChatEmbedAttributes)
.map(String) // NOTE: Needed to avoid type warnings.
.includes(name)
if (isChatEmbedAttribute) {
this.sendConfigToChat()
}
}
}
window.customElements.define('chat-room', ChatEmbed)