diff --git a/frontend/block/block.json b/frontend/block/block.json index b4b9053b..d4c765cf 100644 --- a/frontend/block/block.json +++ b/frontend/block/block.json @@ -8,6 +8,10 @@ "icon": "format-chat", "description": "Matrix client", "attributes": { + "instanceId": { + "type": "string", + "default": "" + }, "defaultHomeserver": { "type": "string", "default": "" diff --git a/frontend/block/edit.tsx b/frontend/block/edit.tsx index e169dc5e..3d883c1c 100644 --- a/frontend/block/edit.tsx +++ b/frontend/block/edit.tsx @@ -1,6 +1,6 @@ import { useBlockProps } from "@wordpress/block-editor"; import { ResizableBox } from "@wordpress/components"; -import { WPElement } from '@wordpress/element'; +import { useEffect, WPElement } from '@wordpress/element'; import { getIframeUrl } from "../app"; import { Block, BlockProps, parseAttributes } from "../components/block"; import './editor.scss'; @@ -11,13 +11,30 @@ interface Props { setAttributes: Function, } +let instanceIds: string[] = []; + export default function Edit(props: Props): WPElement { const { attributes, setAttributes } = props; - const parsedAttributes = parseAttributes(attributes); + // Set instanceId if it's not set, or if it already exists on the page (which happens when the block is duplicated). + useEffect(() => { + // @ts-ignore + let { instanceId } = attributes; + const hasInstanceId = instanceId && instanceId !== ""; + const isInstanceIdDuplicated = hasInstanceId && instanceIds.includes(instanceId); + + if (!hasInstanceId || isInstanceIdDuplicated) { + instanceId = (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); + setAttributes({ instanceId }); + } + + instanceIds.push(instanceId); + }, []); + + const parsedAttributes = parseAttributes(attributes); const blockProps: BlockProps = { focusable: true, - attributes, + attributes: parsedAttributes, iframeUrl: getIframeUrl(), }; diff --git a/frontend/block/view.ts b/frontend/block/view.ts index 07d26d52..a90b49f4 100644 --- a/frontend/block/view.ts +++ b/frontend/block/view.ts @@ -1,4 +1,5 @@ import { BlockProps, renderBlock } from "../app"; +import { parseAttributes } from "../components/block"; window.addEventListener('DOMContentLoaded', () => { renderAllBlocks().catch(error => { @@ -13,10 +14,16 @@ async function renderAllBlocks() { const containers = >document.getElementsByClassName('wp-block-automattic-chatrix'); for (const container of containers) { const config = getConfigFromDataAttribute(container); + const attributes = parseAttributes(config.attributes); const props: BlockProps = { - attributes: config.attributes, + attributes: attributes, }; + if (!attributes.instanceId) { + // In earlier versions of the block, there was no instanceId attribute, so it can be undefined. + console.warn('Chatrix block is missing the instanceId attribute. Re-save the page to fix this.'); + } + renderBlock(container, props); } } diff --git a/frontend/components/block/attributes.ts b/frontend/components/block/attributes.ts index eba59f7b..6d9156c3 100644 --- a/frontend/components/block/attributes.ts +++ b/frontend/components/block/attributes.ts @@ -1,6 +1,7 @@ import { Unit, ValueWithUnit } from "./unit"; -interface Attributes { +export interface BlockAttributes { + instanceId: string, defaultHomeserver?: string, roomId?: string, height?: Height, @@ -10,15 +11,27 @@ interface Attributes { borderColor?: string, } -export function parseAttributes(attributes): Attributes { +export function parseAttributes(attributes): BlockAttributes { + const { + instanceId, + defaultHomeserver, + roomId, + height, + borderWidth, + borderRadius, + borderStyle, + borderColor, + } = attributes; + return { - defaultHomeserver: attributes.defaultHomeserver ?? '', - roomId: attributes.roomId ?? '', - height: attributes.height ? new Height(attributes.height.value, attributes.height.unit) : undefined, - borderWidth: attributes.borderWidth ? new BorderWidth(attributes.borderWidth.value, attributes.borderWidth.unit) : undefined, - borderRadius: attributes.borderRadius ? new BorderRadius(attributes.borderRadius.value, attributes.borderRadius.unit) : undefined, - borderStyle: attributes.borderStyle, - borderColor: attributes.borderColor, + instanceId, + defaultHomeserver: defaultHomeserver ?? '', + roomId: roomId ?? '', + height: height ? new Height(height.value, height.unit) : undefined, + borderWidth: borderWidth ? new BorderWidth(borderWidth.value, borderWidth.unit) : undefined, + borderRadius: borderRadius ? new BorderRadius(borderRadius.value, borderRadius.unit) : undefined, + borderStyle, + borderColor, }; } diff --git a/frontend/components/block/block.tsx b/frontend/components/block/block.tsx index cb59db37..8b743f2d 100644 --- a/frontend/components/block/block.tsx +++ b/frontend/components/block/block.tsx @@ -1,28 +1,39 @@ import { Chat, ChatProps } from "../chat"; -import { parseAttributes } from "./attributes"; +import { BlockAttributes } from "./attributes"; export interface BlockProps { focusable?: boolean, - attributes: object, + attributes: BlockAttributes, iframeUrl: URL, } export function Block(props: BlockProps) { - const attributes = parseAttributes(props.attributes); + const { focusable, iframeUrl } = props; + const { + instanceId, + defaultHomeserver, + roomId, + height, + borderWidth, + borderRadius, + borderStyle, + borderColor, + } = props.attributes; const style = { - height: attributes.height ? attributes.height.toString() : '', - borderWidth: attributes.borderWidth ? attributes.borderWidth.toString() : 0, - borderRadius: attributes.borderRadius ? attributes.borderRadius.toString() : "", - borderStyle: attributes.borderStyle ?? '', - borderColor: attributes.borderColor ?? '', + height: height ? height.toString() : '', + borderWidth: borderWidth ? borderWidth.toString() : 0, + borderRadius: borderRadius ? borderRadius.toString() : "", + borderStyle: borderStyle ?? '', + borderColor: borderColor ?? '', }; const chatProps: ChatProps = { - focusable: props.focusable, - iframeUrl: props.iframeUrl, - defaultHomeserver: attributes.defaultHomeserver, - roomId: attributes.roomId, + focusable, + iframeUrl, + instanceId, + defaultHomeserver, + roomId, }; return ( diff --git a/frontend/components/chat/chat.tsx b/frontend/components/chat/chat.tsx index 7290a7fa..d7eeaf2f 100644 --- a/frontend/components/chat/chat.tsx +++ b/frontend/components/chat/chat.tsx @@ -4,16 +4,25 @@ import { IframeUrl } from "./url"; export interface ChatProps { focusable?: boolean, iframeUrl: URL, + instanceId: string, defaultHomeserver?: string, roomId?: string, } export function Chat(props: ChatProps) { - const ref = props.focusable ? useFocusableIframe() : undefined; + const { + focusable, + iframeUrl, + instanceId, + defaultHomeserver, + roomId + } = props; - const iframeUrl = new IframeUrl(props.iframeUrl, { - defaultHomeserver: props.defaultHomeserver, - roomId: props.roomId, + const ref = focusable ? useFocusableIframe() : undefined; + const url = new IframeUrl(iframeUrl, { + instanceId, + defaultHomeserver, + roomId, }); return ( @@ -21,7 +30,7 @@ export function Chat(props: ChatProps) { ); diff --git a/frontend/components/chat/url.ts b/frontend/components/chat/url.ts index f94c46a8..be01d1bd 100644 --- a/frontend/components/chat/url.ts +++ b/frontend/components/chat/url.ts @@ -1,6 +1,7 @@ const LOGIN_TOKEN_PARAM_NAME = "loginToken"; type IframeParams = { + instanceId: string, defaultHomeserver?: string roomId?: string, } diff --git a/frontend/iframe/config/ConfigFactory.ts b/frontend/iframe/config/ConfigFactory.ts index 91a43bae..6ecad4c9 100644 --- a/frontend/iframe/config/ConfigFactory.ts +++ b/frontend/iframe/config/ConfigFactory.ts @@ -12,6 +12,7 @@ export class ConfigFactory { }; return { + instanceId: getQueryParam("instanceId") ?? "", defaultHomeserver: getQueryParam("defaultHomeserver") ?? "", roomId: getQueryParam("roomId") ?? "", themeManifests: [ diff --git a/frontend/iframe/config/IConfig.ts b/frontend/iframe/config/IConfig.ts index 5c387846..e1782110 100644 --- a/frontend/iframe/config/IConfig.ts +++ b/frontend/iframe/config/IConfig.ts @@ -1,4 +1,10 @@ export interface IConfig { + /** + * Unique identifier for this instance of the client. + * When multiple clients are embedded on the same page, this is used to distinguish them. + */ + instanceId: string; + /** * The default homeserver used by Hydrogen; autofilled in the login UI. * eg: https://matrix.org diff --git a/frontend/iframe/platform/History.ts b/frontend/iframe/platform/History.ts index a5638d08..458c7582 100644 --- a/frontend/iframe/platform/History.ts +++ b/frontend/iframe/platform/History.ts @@ -1,10 +1,12 @@ import { History as BaseHistory } from "hydrogen-web/src/platform/web/dom/History"; export class History extends BaseHistory { + private readonly _instanceId: string; private _lastSessionHash: string | null | undefined; - constructor() { + constructor(instanceId: string) { super(); + this._instanceId = instanceId; } get() { @@ -21,16 +23,20 @@ export class History extends BaseHistory { } onSubscribeFirst() { - this._lastSessionHash = window.localStorage?.getItem("chatrix_last_url_hash"); + this._lastSessionHash = window.localStorage?.getItem(this.urlHashKey); // @ts-ignore window.addEventListener('hashchange', this); } _storeHash(hash) { - window.localStorage?.setItem("chatrix_last_url_hash", hash); + window.localStorage?.setItem(this.urlHashKey, hash); } replaceUrlSilently(url) { super.replaceUrlSilently(url); } + + private get urlHashKey(): string { + return `chatrix_${this._instanceId}_last_url_hash`; + } } diff --git a/frontend/iframe/platform/Platform.ts b/frontend/iframe/platform/Platform.ts index 2da1b3d3..ad074c9f 100644 --- a/frontend/iframe/platform/Platform.ts +++ b/frontend/iframe/platform/Platform.ts @@ -25,7 +25,7 @@ export class Platform extends BasePlatform { super._serviceWorkerHandler = serviceWorkerHandler; super.settingsStorage = new SettingsStorage("chatrix_setting_v1_"); super.sessionInfoStorage = new SessionInfoStorage("chatrix_sessions_v1"); - super.history = new History(); + super.history = new History(this.config.instanceId); } public get history(): History { diff --git a/frontend/index.html b/frontend/index.html index 30199f71..2fadfd8a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -36,6 +36,7 @@ renderBlock(document.getElementById("root-block"), { attributes: { + instanceId: "block", defaultHomeserver: env.VITE_HOMESERVER, roomId: env.VITE_ROOM_ID, } diff --git a/src/Popup/popup.js b/src/Popup/popup.js index e4e7f84c..a4cf7b7a 100644 --- a/src/Popup/popup.js +++ b/src/Popup/popup.js @@ -11,6 +11,7 @@ container.style.zIndex = 1; Chatrix.renderPopup(container, { + instanceId: config.instanceId, defaultHomeserver: config.defaultHomeserver, roomId: config.roomId, }); diff --git a/src/Popup/popup.php b/src/Popup/popup.php index 201ed880..047fb0e1 100644 --- a/src/Popup/popup.php +++ b/src/Popup/popup.php @@ -5,10 +5,6 @@ use function Automattic\Chatrix\Admin\Settings\get as get_chatrix_settings; use const Automattic\Chatrix\SCRIPT_HANDLE_APP; -function chatrix_config() { - return apply_filters( 'chatrix_config', array() ); -} - function register() { register_default_instance(); define_config(); @@ -35,10 +31,9 @@ function ( array $instances ) { return array( 'default' => array( - 'homeserver' => $settings['homeserver'], - 'room_id' => $settings['room'], - 'login_methods' => array( 'password', 'sso' ), // TODO: don't hardcode login methods. - 'pages' => 'all' === $settings['show_on'] ? 'all' : $settings['pages'], + 'homeserver' => $settings['homeserver'], + 'room_id' => $settings['room'], + 'pages' => 'all' === $settings['show_on'] ? 'all' : $settings['pages'], ), ); } @@ -67,7 +62,8 @@ function ( array $config ) { $instances = apply_filters( 'chatrix_instances', array() ); foreach ( $instances as $instance_id => $instance ) { - $instance_config = array( + $instance['instance_id'] = $instance_id; + $instance_config = array( 'url' => rest_url( "chatrix/config/$instance_id" ), 'config' => $instance, 'instance_id' => $instance_id, @@ -118,7 +114,7 @@ function init_javascript() { add_action( 'wp_enqueue_scripts', function () { - $config = chatrix_config(); + $config = apply_filters( 'chatrix_config', array() ); if ( empty( $config ) ) { return; } @@ -126,6 +122,7 @@ function () { $handle = 'chatrix-popup'; $json_data = wp_json_encode( array( + 'instanceId' => 'popup_' . $config['instance_id'], 'defaultHomeserver' => $config['config']['homeserver'], 'roomId' => empty( $config['config']['room_id'] ) ? null : $config['config']['room_id'], )