From c2b11111dd433a16475f78c0f7bac282cc38d0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl?= Date: Tue, 30 Jun 2026 17:30:56 +0200 Subject: [PATCH] feat(permalinks): point Share links/QR at to.healthchat.ch Add a config-driven matrix_to_url option so Share dialog links and QR codes resolve against a matrix.to-compatible resolver instead of the hardcoded public matrix.to. MatrixToPermalinkConstructor now reads the base URL lazily from config (default https://matrix.to), keeping the matrix.to URL format. HealthChat configs point it at the in-house fork https://to.healthchat.ch and drop the conflicting permalink_prefix (which routes to the Element-format constructor a matrix.to fork can't parse). --- .../desktop/element.io/healthchat/config.json | 2 +- apps/web/config.json | 2 +- apps/web/src/IConfigOptions.ts | 6 ++++ .../MatrixToPermalinkConstructor.ts | 35 ++++++++++++++----- apps/web/src/utils/permalinks/Permalinks.ts | 9 ++--- .../MatrixToPermalinkConstructor-test.ts | 29 +++++++++++++++ docs/config.md | 4 +++ 7 files changed, 72 insertions(+), 15 deletions(-) diff --git a/apps/desktop/element.io/healthchat/config.json b/apps/desktop/element.io/healthchat/config.json index f8cbbefbb4..4b2fc642cd 100644 --- a/apps/desktop/element.io/healthchat/config.json +++ b/apps/desktop/element.io/healthchat/config.json @@ -33,7 +33,7 @@ "preferred_domain": "jitsi.healthchat.ch" }, "logout_redirect_url": "", - "permalink_prefix": "https://matrix.to", + "matrix_to_url": "https://to.healthchat.ch", "room_directory": { "servers": ["matrix.org"] }, diff --git a/apps/web/config.json b/apps/web/config.json index f0c81c6dd6..77ea15ae9e 100644 --- a/apps/web/config.json +++ b/apps/web/config.json @@ -35,7 +35,7 @@ "preferred_domain": "jitsi.healthchat.ch" }, "logout_redirect_url": "", - "permalink_prefix": "https://matrix.to", + "matrix_to_url": "https://to.healthchat.ch", "room_directory": { "servers": [ "matrix.org" diff --git a/apps/web/src/IConfigOptions.ts b/apps/web/src/IConfigOptions.ts index a3a6a32c91..4a620220e2 100644 --- a/apps/web/src/IConfigOptions.ts +++ b/apps/web/src/IConfigOptions.ts @@ -70,6 +70,12 @@ export interface IConfigOptions { }; permalink_prefix?: string; + /** + * Base URL of a matrix.to-compatible resolver to use for Share links and QR codes, + * instead of the public `https://matrix.to`. Must use the matrix.to URL format + * (`/#/`), e.g. a self-hosted matrix.to fork. + */ + matrix_to_url?: string; update_base_url?: string; desktop_builds: { diff --git a/apps/web/src/utils/permalinks/MatrixToPermalinkConstructor.ts b/apps/web/src/utils/permalinks/MatrixToPermalinkConstructor.ts index d7cbc5e927..08911c73e7 100644 --- a/apps/web/src/utils/permalinks/MatrixToPermalinkConstructor.ts +++ b/apps/web/src/utils/permalinks/MatrixToPermalinkConstructor.ts @@ -7,10 +7,27 @@ Please see LICENSE files in the repository root for full details. */ import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor"; +import SdkConfig from "../../SdkConfig"; -export const host = "matrix.to"; -export const baseUrl = `https://${host}`; -export const baseUrlPattern = `^(?:https?://)?${host.replace(".", "\\.")}/#/(.*)`; +const DEFAULT_BASE_URL = "https://matrix.to"; + +/** + * The base URL of the matrix.to-compatible resolver to generate permalinks against. + * Defaults to the public `https://matrix.to`, but can be pointed at a self-hosted + * matrix.to fork via the `matrix_to_url` config option. Read lazily so the value + * reflects the runtime config rather than module-load time. + */ +export function baseUrl(): string { + return SdkConfig.get("matrix_to_url") ?? DEFAULT_BASE_URL; +} + +export function host(): string { + return new URL(baseUrl()).host; +} + +export function baseUrlPattern(): string { + return `^(?:https?://)?${host().replaceAll(".", "\\.")}/#/(.*)`; +} /** * Generates matrix.to permalinks @@ -21,23 +38,23 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor { } public forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { - return `${baseUrl}/#/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`; + return `${baseUrl()}/#/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`; } public forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { - return `${baseUrl}/#/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`; + return `${baseUrl()}/#/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`; } public forUser(userId: string): string { - return `${baseUrl}/#/${userId}`; + return `${baseUrl()}/#/${userId}`; } public forEntity(entityId: string): string { - return `${baseUrl}/#/${entityId}`; + return `${baseUrl()}/#/${entityId}`; } public isPermalinkHost(testHost: string): boolean { - return testHost === host; + return testHost === host(); } public encodeServerCandidates(candidates: string[]): string { @@ -52,7 +69,7 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor { throw new Error("Does not appear to be a permalink"); } - const matches = [...fullUrl.matchAll(new RegExp(baseUrlPattern, "gi"))][0]; + const matches = [...fullUrl.matchAll(new RegExp(baseUrlPattern(), "gi"))][0]; if (!matches || matches.length < 2) { throw new Error("Does not appear to be a permalink"); diff --git a/apps/web/src/utils/permalinks/Permalinks.ts b/apps/web/src/utils/permalinks/Permalinks.ts index 8b8c850fc4..57aa2f0fa3 100644 --- a/apps/web/src/utils/permalinks/Permalinks.ts +++ b/apps/web/src/utils/permalinks/Permalinks.ts @@ -16,6 +16,7 @@ import MatrixToPermalinkConstructor, { baseUrl as matrixtoBaseUrl, baseUrlPattern as matrixToBaseUrlPattern, } from "./MatrixToPermalinkConstructor"; +// `matrixtoBaseUrl` and `matrixToBaseUrlPattern` are functions (config-driven); call them at use sites. import { type PermalinkParts } from "./PermalinkConstructor"; import type PermalinkConstructor from "./PermalinkConstructor"; import ElementPermalinkConstructor from "./ElementPermalinkConstructor"; @@ -342,13 +343,13 @@ export function tryTransformEntityToPermalink(matrixClient: MatrixClient, entity if (permalinkParts) { if (permalinkParts.roomIdOrAlias) { const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : ""; - let pl = matrixtoBaseUrl + `/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`; + let pl = matrixtoBaseUrl() + `/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`; if (permalinkParts.viaServers?.length) { pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers); } return pl; } else if (permalinkParts.userId) { - return matrixtoBaseUrl + `/#/${permalinkParts.userId}`; + return matrixtoBaseUrl() + `/#/${permalinkParts.userId}`; } } } catch {} @@ -437,7 +438,7 @@ export function getPrimaryPermalinkEntity(permalink: string): string | null { */ function getPermalinkConstructor(isPill = false): PermalinkConstructor { const elementPrefix = SdkConfig.get("permalink_prefix"); - if (elementPrefix && elementPrefix !== matrixtoBaseUrl && !isPill) { + if (elementPrefix && elementPrefix !== matrixtoBaseUrl() && !isPill) { return new ElementPermalinkConstructor(elementPrefix); } @@ -448,7 +449,7 @@ export function parsePermalink(fullUrl: string): PermalinkParts | null { try { const elementPrefix = SdkConfig.get("permalink_prefix"); const decodedUrl = decodeURIComponent(fullUrl); - if (new RegExp(matrixToBaseUrlPattern, "i").test(decodedUrl)) { + if (new RegExp(matrixToBaseUrlPattern(), "i").test(decodedUrl)) { return new MatrixToPermalinkConstructor().parsePermalink(decodedUrl); } else if (fullUrl.startsWith("matrix:")) { return new MatrixSchemePermalinkConstructor().parsePermalink(fullUrl); diff --git a/apps/web/test/unit-tests/utils/permalinks/MatrixToPermalinkConstructor-test.ts b/apps/web/test/unit-tests/utils/permalinks/MatrixToPermalinkConstructor-test.ts index 92f005d0d9..20e262640a 100644 --- a/apps/web/test/unit-tests/utils/permalinks/MatrixToPermalinkConstructor-test.ts +++ b/apps/web/test/unit-tests/utils/permalinks/MatrixToPermalinkConstructor-test.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import MatrixToPermalinkConstructor from "../../../../src/utils/permalinks/MatrixToPermalinkConstructor"; import { PermalinkParts } from "../../../../src/utils/permalinks/PermalinkConstructor"; +import SdkConfig from "../../../../src/SdkConfig"; describe("MatrixToPermalinkConstructor", () => { const peramlinkConstructor = new MatrixToPermalinkConstructor(); @@ -49,4 +50,32 @@ describe("MatrixToPermalinkConstructor", () => { ).toEqual("https://matrix.to/#/!myroom:example.com/$event4?via=one.example.com&via=two.example.com"); }); }); + + describe("with a custom matrix_to_url config", () => { + beforeEach(() => { + SdkConfig.add({ matrix_to_url: "https://to.healthchat.ch" }); + }); + + afterEach(() => { + SdkConfig.reset(); + }); + + it("generates links against the configured host", () => { + const constructor = new MatrixToPermalinkConstructor(); + expect(constructor.forUser("@user:example.com")).toEqual("https://to.healthchat.ch/#/@user:example.com"); + expect(constructor.forRoom("!myroom:example.com", [])).toEqual( + "https://to.healthchat.ch/#/!myroom:example.com", + ); + }); + + it("parses links from the configured host and rejects matrix.to links", () => { + const constructor = new MatrixToPermalinkConstructor(); + expect(constructor.parsePermalink("https://to.healthchat.ch/#/@user:example.com")).toEqual( + new PermalinkParts(null, null, "@user:example.com", null), + ); + expect(() => constructor.parsePermalink("https://matrix.to/#/@user:example.com")).toThrow( + new Error("Does not appear to be a permalink"), + ); + }); + }); }); diff --git a/docs/config.md b/docs/config.md index 046a737447..8f37dd21f6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -125,6 +125,10 @@ complete re-branding/private labeling, a more personalised experience can be ach 3. `brand`: Optional name for the app. Defaults to `Element`. This is used throughout the application in various strings/locations. 4. `permalink_prefix`: An optional URL pointing to an Element Web deployment. For example, `https://app.element.io`. This will change all permalinks (via the "Share" menus) to point at the Element Web deployment rather than `matrix.to`. + 4a. `matrix_to_url`: An optional base URL of a matrix.to-compatible resolver to use for Share links and QR codes instead of + the public `https://matrix.to`. Unlike `permalink_prefix` (which points at an Element Web deployment and uses Element's + `/#/room/…` URL format), this keeps the matrix.to URL format (`/#/`), so it must point at a matrix.to deployment or + fork. For example, `https://to.example.com`. 5. `desktop_builds`: Optional. Where the desktop builds for the application are, if available. This is explained in more detail down below. 6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.