From fa90f14e66c4aeeb4fc100c863b21cdaaeb18586 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 16 Oct 2025 10:29:11 +0100 Subject: [PATCH] Draft of handling urls --- src/BasePlatform.ts | 6 ++++ src/Views.ts | 4 +++ src/components/structures/MatrixChat.tsx | 36 +++++++++++++++++-- .../tabs/user/PreferencesUserSettingsTab.tsx | 2 ++ src/settings/Settings.tsx | 7 ++++ .../controllers/ProtocolHandlerController.ts | 26 ++++++++++++++ src/vector/platform/WebPlatform.ts | 4 +++ 7 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/settings/controllers/ProtocolHandlerController.ts diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 7bd830f3f2..983af6fd39 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -524,4 +524,10 @@ export default abstract class BasePlatform { * @returns {Promise} True if the lock was acquired, false otherwise. */ public abstract getSessionLock(_onNewInstance: () => Promise): Promise; + + /** + * + * @param urlPattern + */ + public registerProtocolHandler?(urlPattern: `${string}%s`): Promise } diff --git a/src/Views.ts b/src/Views.ts index 6c0df53a66..f021dac830 100644 --- a/src/Views.ts +++ b/src/Views.ts @@ -43,6 +43,10 @@ enum Views { // Another instance of the application has started up. We just show an error page. LOCK_STOLEN, + + // Another instance of the application has opened and this session had a matrixuri, so we + // just show a warning and close. + SENT_URI, } export default Views; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a4df7a5fe9..ccf5fd9208 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP import Markdown from "../../Markdown"; import { sanitizeHtmlParams } from "../../Linkify"; import { isOnlyAdmin } from "../../utils/membership"; +import mxModuleApi from "../../modules/Api"; // legacy export export { default as Views } from "../../Views"; @@ -234,6 +235,7 @@ export default class MatrixChat extends React.PureComponent { private fontWatcher?: FontWatcher; private readonly stores: SdkContextClass; private loadSessionAbortController = new AbortController(); + private newUriBroadcastChannel = new BroadcastChannel("io.element.broadcast.new_uri"); private sessionLoadStarted = false; @@ -283,6 +285,12 @@ export default class MatrixChat extends React.PureComponent { // object field used for tracking the status info appended to the title tag. // we don't do it as react state as i'm scared about triggering needless react refreshes. this.subTitleStatus = ""; + + this.newUriBroadcastChannel.addEventListener("message", (ev) => { + const { screen, params } = ev.data; + console.log(ev, ev.data); + this.showScreen(screen, params); + }); } /** @@ -470,6 +478,7 @@ export default class MatrixChat extends React.PureComponent { initSentry(SdkConfig.get("sentry")); window.addEventListener("resize", this.onWindowResized); + console.log("MatrixCHAT IS LOADING") // Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does // in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously @@ -478,8 +487,18 @@ export default class MatrixChat extends React.PureComponent { this.sessionLoadStarted = true; const platform = PlatformPeg.get(); if (platform && !platform.checkSessionLockFree()) { - // another instance holds the lock; confirm its theft before proceeding - setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + console.log("screenAfterLogin", this.screenAfterLogin); + if (this.screenAfterLogin?.screen.startsWith("matrix")) { + console.log("Got URI req"); + // The user has clicked on a matrixuri link, so we need to forward it to them. + this.newUriBroadcastChannel.postMessage({screen: this.screenAfterLogin.screen, params: this.screenAfterLogin.params}); + // This will put the application in a final state so the other instance can handle the URI. + setTimeout(() => this.setState({ view: Views.SENT_URI }), 0); + } else { + // another instance holds the lock; confirm its theft before proceeding + setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + + } } else { this.startInitSession(); } @@ -1752,6 +1771,10 @@ export default class MatrixChat extends React.PureComponent { } public showScreen(screen: string, params?: { [key: string]: any }): void { + if (this.state.view === Views.SENT_URI) { + // Ignore any screens once we've reached this state. + return; + } const cli = MatrixClientPeg.get(); const isLoggedOutOrGuest = !cli || cli.isGuest(); if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) { @@ -1817,6 +1840,11 @@ export default class MatrixChat extends React.PureComponent { } } else if (screen === "settings") { dis.fire(Action.ViewUserSettings); + } else if (screen.startsWith("matrix")) { + console.log("matrix:", screen, params); + mxModuleApi.navigation.toMatrixToLink(screen, false).catch((ex) => { + console.log('Failed to handle matrix uri', ex); + }) } else if (screen === "welcome") { dis.dispatch({ action: "view_welcome_page", @@ -2098,6 +2126,10 @@ export default class MatrixChat extends React.PureComponent { }} /> ); + } else if (this.state.view === Views.SENT_URI) { + view = ( + PLACEHOLDER: The URI has been broadcast to the other app. + ); } else if (this.state.view === Views.COMPLETE_SECURITY) { view = ; } else if (this.state.view === Views.E2E_SETUP) { diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index d373aee60e..d0386fc522 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -363,6 +363,8 @@ export default class PreferencesUserSettingsTab extends React.Component {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} + + ; "inviteRules": IBaseSetting; "Developer.elementCallUrl": IBaseSetting; + protocolHandlerRegistered: IBaseSetting; } export type SettingKey = keyof Settings; @@ -1345,6 +1347,11 @@ export const SETTINGS: Settings = { // Contains room IDs shouldExportToRageshake: false, }, + "protocolHandlerRegistered": { + default: false, + controller: new ProtocolHandlerController(), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + }, /** * Enable or disable the release announcement feature */ diff --git a/src/settings/controllers/ProtocolHandlerController.ts b/src/settings/controllers/ProtocolHandlerController.ts new file mode 100644 index 0000000000..c3b9f1812b --- /dev/null +++ b/src/settings/controllers/ProtocolHandlerController.ts @@ -0,0 +1,26 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2020 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import SettingController from "./SettingController"; +import PlatformPeg from "../../PlatformPeg"; + +export default class ProtocolHandlerController extends SettingController { + public constructor() { + super(); + } + + public get settingDisabled() { + return PlatformPeg.get()?.registerProtocolHandler === undefined; + } + + public onChange(): void { + const platform = PlatformPeg.get()!; + const uri = platform.baseUrl; + platform.registerProtocolHandler!(`${uri}#/%s`); + } +} diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts index 87ed2389dc..2a1df38aae 100644 --- a/src/vector/platform/WebPlatform.ts +++ b/src/vector/platform/WebPlatform.ts @@ -277,4 +277,8 @@ export default class WebPlatform extends BasePlatform { public async getSessionLock(onNewInstance: () => Promise): Promise { return SessionLock.getSessionLock(onNewInstance); } + + public async registerProtocolHandler(urlPattern: `${string}%s`) { + navigator.registerProtocolHandler("matrix", urlPattern); + } }