From b584d1867b605473bffabf0eea43d028be05bea3 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 20:51:51 -0400 Subject: [PATCH 1/7] fix: support matrixToBaseUrl config to override matrix.to base (#67) --- .changeset/fix-matrix-to-custom-base.md | 7 +++++++ src/app/hooks/useClientConfig.ts | 2 ++ src/app/pages/App.tsx | 24 ++++++++++++++---------- src/app/plugins/matrix-to.ts | 10 +++++++++- 4 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 .changeset/fix-matrix-to-custom-base.md diff --git a/.changeset/fix-matrix-to-custom-base.md b/.changeset/fix-matrix-to-custom-base.md new file mode 100644 index 000000000..5d63f371f --- /dev/null +++ b/.changeset/fix-matrix-to-custom-base.md @@ -0,0 +1,7 @@ +--- +default: patch +--- + +Support `matrixToBaseUrl` in `config.json` to override the default `matrix.to` link base URL. + +Closes #67 diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index 4d6cec62a..87685337d 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -40,6 +40,8 @@ export type ClientConfig = { }; hashRouter?: HashRouterConfig; + + matrixToBaseUrl?: string; }; const ClientConfigContext = createContext(null); diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 0408f38ea..bf5ee6395 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -7,6 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { ClientConfigLoader } from '$components/ClientConfigLoader'; import { ClientConfigProvider } from '$hooks/useClientConfig'; +import { setMatrixToBase } from '$plugins/matrix-to'; import { ScreenSizeProvider, useScreenSize } from '$hooks/useScreenSize'; import { useCompositionEndTracking } from '$hooks/useComposingCheck'; import { ErrorPage } from '$components/DefaultErrorPage'; @@ -35,16 +36,19 @@ function App() { )} > - {(clientConfig) => ( - - - - - - - - - )} + {(clientConfig) => { + setMatrixToBase(clientConfig.matrixToBaseUrl); + return ( + + + + + + + + + ); + }} diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts index 03a7d2c17..35afe11d7 100644 --- a/src/app/plugins/matrix-to.ts +++ b/src/app/plugins/matrix-to.ts @@ -1,4 +1,12 @@ -const MATRIX_TO_BASE = 'https://matrix.to'; +let MATRIX_TO_BASE = 'https://matrix.to'; + +/** + * Override the default matrix.to base URL (configurable per deployment). + * Must be called before any getMatrixTo* functions are used. + */ +export const setMatrixToBase = (baseUrl?: string): void => { + if (baseUrl) MATRIX_TO_BASE = baseUrl.replace(/\/$/, ''); +}; export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`; From 2da742d7168d00d39f55966942ee86bcbe2f1b3e Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 21:17:19 -0400 Subject: [PATCH 2/7] fix: dynamic matrix.to regexes; route mentions through plugin generators (#67) - matrix-to.ts: replace static hardcoded regexes with a lazily-built cached set that matches both https://matrix.to (cross-client compat) and the configured custom base URL (if matrixToBaseUrl is set in config.json) - editor/output.ts: Mention block now calls getMatrixToRoom / getMatrixToRoomEvent instead of hardcoding the matrix.to base - room.ts: parseReplyFormattedBody uses getMatrixToRoomEvent / getMatrixToRoom for the same reason --- src/app/components/editor/output.ts | 16 +++------- src/app/plugins/matrix-to.ts | 47 ++++++++++++++++++++++------- src/app/utils/room.ts | 7 ++--- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 9e3901925..fa9a4f069 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -10,6 +10,7 @@ import { import { findAndReplace } from '$utils/findAndReplace'; import { sanitizeForRegex } from '$utils/regex'; import { isUserId } from '$utils/matrix'; +import { getMatrixToRoom, getMatrixToRoomEvent } from '$plugins/matrix-to'; import { CustomElement } from './slate'; import { BlockType } from './types'; @@ -63,17 +64,10 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => { return `
`; case BlockType.Mention: { - let fragment = node.id; - - if (node.eventId) { - fragment += `/${node.eventId}`; - } - if (node.viaServers && node.viaServers.length > 0) { - fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`; - } - - const matrixTo = `https://matrix.to/#/${fragment}`; - return `${sanitizeText(node.name)}`; + const href = node.eventId + ? getMatrixToRoomEvent(node.id, node.eventId, node.viaServers) + : getMatrixToRoom(node.id, node.viaServers); + return `${sanitizeText(node.name)}`; } case BlockType.Emoticon: return node.key.startsWith('mxc://') diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts index 35afe11d7..ae0a9f4f9 100644 --- a/src/app/plugins/matrix-to.ts +++ b/src/app/plugins/matrix-to.ts @@ -46,23 +46,48 @@ export type MatrixToRoomEvent = MatrixToRoom & { eventId: string; }; -const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/; -export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href); +const escapeForRegex = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/; -const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/?(\?[\S]*)?$/; -const MATRIX_TO_ROOM_EVENT = - /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; +// Lazily cached regex set; rebuilt if MATRIX_TO_BASE changes. +let cachedRegexBase = ''; +let cachedRegexes: { + any: RegExp; + user: RegExp; + room: RegExp; + event: RegExp; +} | null = null; + +/** + * Returns regexes that match BOTH https://matrix.to (for cross-client links + * received from standard clients) and the configured custom base URL (if any). + */ +const getMatchRegexes = () => { + if (cachedRegexBase === MATRIX_TO_BASE && cachedRegexes) return cachedRegexes; + cachedRegexBase = MATRIX_TO_BASE; + const standard = escapeForRegex('https://matrix.to'); + const b = + MATRIX_TO_BASE !== 'https://matrix.to' + ? `(?:${standard}|${escapeForRegex(MATRIX_TO_BASE)})` + : standard; + cachedRegexes = { + any: new RegExp(`^${b}\\S*$`), + user: new RegExp(`^${b}/#/(@[^:\\s]+:[^?/\\s]+)\\/?$`), + room: new RegExp(`^${b}/#/([#!][^?/\\s]+)\\/?([?\\S]*)?$`), + event: new RegExp(`^${b}/#/([#!][^?/\\s]+)/(\\$[^?/\\s]+)\\/?([?\\S]*)?$`), + }; + return cachedRegexes; +}; + +export const testMatrixTo = (href: string): boolean => getMatchRegexes().any.test(href); export const parseMatrixToUser = (href: string): string | undefined => { - const match = href.match(MATRIX_TO_USER); + const match = href.match(getMatchRegexes().user); if (!match) return undefined; - const userId = match[1]; - return userId; + return match[1]; }; export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => { - const match = href.match(MATRIX_TO_ROOM); + const match = href.match(getMatchRegexes().room); if (!match) return undefined; const roomIdOrAlias = match[1]; @@ -76,7 +101,7 @@ export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => { }; export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => { - const match = href.match(MATRIX_TO_ROOM_EVENT); + const match = href.match(getMatchRegexes().event); if (!match) return undefined; const roomIdOrAlias = match[1]; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 5003d53d5..2c30d1a79 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -1,4 +1,5 @@ import { IconName, IconSrc } from 'folds'; +import { getMatrixToRoom, getMatrixToRoomEvent } from '$plugins/matrix-to'; import { EventTimeline, @@ -511,10 +512,8 @@ export const parseReplyFormattedBody = ( eventId: string, formattedBody: string ): string => { - const replyToLink = `In reply to`; - const userLink = `${userId}`; + const replyToLink = `In reply to`; + const userLink = `${userId}`; return `
${replyToLink}${userLink}
${formattedBody}
`; }; From 121334b5e1f03f05d5a9189cafc2c8f4e8cdae5d Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 21:26:18 -0400 Subject: [PATCH 3/7] fix: keep formatted_body links as standard matrix.to for interop Outgoing links embedded in Matrix events (mention HTML, reply quotes) must use https://matrix.to so other clients (Element, FluffyChat, etc.) can parse them as Matrix permalinks. Custom base applies only to UI navigation links (copy-to-clipboard buttons). --- src/app/components/editor/output.ts | 16 +++++++++++----- src/app/utils/room.ts | 7 ++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index fa9a4f069..9e3901925 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -10,7 +10,6 @@ import { import { findAndReplace } from '$utils/findAndReplace'; import { sanitizeForRegex } from '$utils/regex'; import { isUserId } from '$utils/matrix'; -import { getMatrixToRoom, getMatrixToRoomEvent } from '$plugins/matrix-to'; import { CustomElement } from './slate'; import { BlockType } from './types'; @@ -64,10 +63,17 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => { return `
`; case BlockType.Mention: { - const href = node.eventId - ? getMatrixToRoomEvent(node.id, node.eventId, node.viaServers) - : getMatrixToRoom(node.id, node.viaServers); - return `${sanitizeText(node.name)}`; + let fragment = node.id; + + if (node.eventId) { + fragment += `/${node.eventId}`; + } + if (node.viaServers && node.viaServers.length > 0) { + fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`; + } + + const matrixTo = `https://matrix.to/#/${fragment}`; + return `${sanitizeText(node.name)}`; } case BlockType.Emoticon: return node.key.startsWith('mxc://') diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 2c30d1a79..5003d53d5 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -1,5 +1,4 @@ import { IconName, IconSrc } from 'folds'; -import { getMatrixToRoom, getMatrixToRoomEvent } from '$plugins/matrix-to'; import { EventTimeline, @@ -512,8 +511,10 @@ export const parseReplyFormattedBody = ( eventId: string, formattedBody: string ): string => { - const replyToLink = `In reply to`; - const userLink = `${userId}`; + const replyToLink = `In reply to`; + const userLink = `${userId}`; return `
${replyToLink}${userLink}
${formattedBody}
`; }; From edc1c67ecfa16cba492aa2aaa744fffe90193a71 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 21:43:51 -0400 Subject: [PATCH 4/7] test(matrix-to): add unit tests; fix setMatrixToBase reset, http support, room regex --- src/app/plugins/matrix-to.test.ts | 250 ++++++++++++++++++++++++++++++ src/app/plugins/matrix-to.ts | 7 +- 2 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 src/app/plugins/matrix-to.test.ts diff --git a/src/app/plugins/matrix-to.test.ts b/src/app/plugins/matrix-to.test.ts new file mode 100644 index 000000000..806dc03e7 --- /dev/null +++ b/src/app/plugins/matrix-to.test.ts @@ -0,0 +1,250 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { + getMatrixToRoom, + getMatrixToRoomEvent, + getMatrixToUser, + parseMatrixToRoom, + parseMatrixToRoomEvent, + parseMatrixToUser, + setMatrixToBase, + testMatrixTo, +} from './matrix-to'; + +// Reset to default after each test so state doesn't leak between tests. +afterEach(() => { + setMatrixToBase(undefined); +}); + +// --------------------------------------------------------------------------- +// Link generation +// --------------------------------------------------------------------------- + +describe('getMatrixToUser', () => { + it('generates a standard matrix.to user link', () => { + expect(getMatrixToUser('@alice:example.com')).toBe( + 'https://matrix.to/#/@alice:example.com' + ); + }); + + it('uses custom base when configured', () => { + setMatrixToBase('https://matrix.example.org'); + expect(getMatrixToUser('@alice:example.com')).toBe( + 'https://matrix.example.org/#/@alice:example.com' + ); + }); + + it('strips trailing slash from custom base', () => { + setMatrixToBase('https://matrix.example.org/'); + expect(getMatrixToUser('@alice:example.com')).toBe( + 'https://matrix.example.org/#/@alice:example.com' + ); + }); +}); + +describe('getMatrixToRoom', () => { + it('generates a standard matrix.to room link', () => { + expect(getMatrixToRoom('!room:example.com')).toBe( + 'https://matrix.to/#/!room:example.com' + ); + }); + + it('appends via servers', () => { + expect(getMatrixToRoom('!room:example.com', ['s1.org', 's2.org'])).toBe( + 'https://matrix.to/#/!room:example.com?via=s1.org&via=s2.org' + ); + }); + + it('uses custom base when configured', () => { + setMatrixToBase('https://matrix.example.org'); + expect(getMatrixToRoom('#general:example.com')).toBe( + 'https://matrix.example.org/#/#general:example.com' + ); + }); +}); + +describe('getMatrixToRoomEvent', () => { + it('generates a standard matrix.to event link', () => { + expect(getMatrixToRoomEvent('!room:example.com', '$event123')).toBe( + 'https://matrix.to/#/!room:example.com/$event123' + ); + }); + + it('appends via servers', () => { + expect( + getMatrixToRoomEvent('!room:example.com', '$event123', ['s1.org']) + ).toBe('https://matrix.to/#/!room:example.com/$event123?via=s1.org'); + }); + + it('uses custom base when configured', () => { + setMatrixToBase('https://matrix.example.org'); + expect(getMatrixToRoomEvent('!room:example.com', '$event123')).toBe( + 'https://matrix.example.org/#/!room:example.com/$event123' + ); + }); +}); + +// --------------------------------------------------------------------------- +// testMatrixTo +// --------------------------------------------------------------------------- + +describe('testMatrixTo', () => { + it('matches standard matrix.to URLs', () => { + expect(testMatrixTo('https://matrix.to/#/@alice:example.com')).toBe(true); + expect(testMatrixTo('https://matrix.to/#/!room:example.com')).toBe(true); + expect(testMatrixTo('https://matrix.to/#/!room:example.com/$event')).toBe(true); + expect(testMatrixTo('http://matrix.to/#/@alice:example.com')).toBe(true); + }); + + it('rejects non-matrix.to URLs', () => { + expect(testMatrixTo('https://example.com')).toBe(false); + expect(testMatrixTo('https://notmatrix.to/#/@alice:example.com')).toBe(false); + }); + + it('matches custom base URLs after setMatrixToBase', () => { + setMatrixToBase('https://matrix.example.org'); + expect(testMatrixTo('https://matrix.example.org/#/@alice:example.com')).toBe(true); + }); + + it('still matches standard matrix.to after setMatrixToBase (cross-client compat)', () => { + setMatrixToBase('https://matrix.example.org'); + expect(testMatrixTo('https://matrix.to/#/@alice:example.com')).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// parseMatrixToUser +// --------------------------------------------------------------------------- + +describe('parseMatrixToUser', () => { + it('parses a standard matrix.to user link', () => { + expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe( + '@alice:example.com' + ); + }); + + it('returns undefined for non-user links', () => { + expect(parseMatrixToUser('https://matrix.to/#/!room:example.com')).toBeUndefined(); + }); + + it('parses user links from custom base', () => { + setMatrixToBase('https://matrix.example.org'); + expect( + parseMatrixToUser('https://matrix.example.org/#/@alice:example.com') + ).toBe('@alice:example.com'); + }); + + it('parses standard matrix.to user links even after custom base is set', () => { + setMatrixToBase('https://matrix.example.org'); + expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe( + '@alice:example.com' + ); + }); +}); + +// --------------------------------------------------------------------------- +// parseMatrixToRoom +// --------------------------------------------------------------------------- + +describe('parseMatrixToRoom', () => { + it('parses a room ID link', () => { + expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com')).toEqual({ + roomIdOrAlias: '!room:example.com', + viaServers: undefined, + }); + }); + + it('parses a room alias link', () => { + expect(parseMatrixToRoom('https://matrix.to/#/#general:example.com')).toEqual({ + roomIdOrAlias: '#general:example.com', + viaServers: undefined, + }); + }); + + it('parses via servers', () => { + expect( + parseMatrixToRoom('https://matrix.to/#/!room:example.com?via=s1.org&via=s2.org') + ).toEqual({ + roomIdOrAlias: '!room:example.com', + viaServers: ['s1.org', 's2.org'], + }); + }); + + it('returns undefined for event links (too many segments)', () => { + expect( + parseMatrixToRoom('https://matrix.to/#/!room:example.com/$event123') + ).toBeUndefined(); + }); + + it('parses room links from custom base', () => { + setMatrixToBase('https://matrix.example.org'); + expect( + parseMatrixToRoom('https://matrix.example.org/#/!room:example.com') + ).toEqual({ roomIdOrAlias: '!room:example.com', viaServers: undefined }); + }); + + it('still parses standard matrix.to room links after custom base is set', () => { + setMatrixToBase('https://matrix.example.org'); + expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com')).toEqual({ + roomIdOrAlias: '!room:example.com', + viaServers: undefined, + }); + }); +}); + +// --------------------------------------------------------------------------- +// parseMatrixToRoomEvent +// --------------------------------------------------------------------------- + +describe('parseMatrixToRoomEvent', () => { + it('parses a room event link', () => { + expect( + parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123') + ).toEqual({ + roomIdOrAlias: '!room:example.com', + eventId: '$event123', + viaServers: undefined, + }); + }); + + it('parses via servers', () => { + expect( + parseMatrixToRoomEvent( + 'https://matrix.to/#/!room:example.com/$event123?via=s1.org' + ) + ).toEqual({ + roomIdOrAlias: '!room:example.com', + eventId: '$event123', + viaServers: ['s1.org'], + }); + }); + + it('returns undefined for room-only links', () => { + expect( + parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com') + ).toBeUndefined(); + }); + + it('parses event links from custom base', () => { + setMatrixToBase('https://matrix.example.org'); + expect( + parseMatrixToRoomEvent( + 'https://matrix.example.org/#/!room:example.com/$event123' + ) + ).toEqual({ + roomIdOrAlias: '!room:example.com', + eventId: '$event123', + viaServers: undefined, + }); + }); + + it('still parses standard matrix.to event links after custom base is set', () => { + setMatrixToBase('https://matrix.example.org'); + expect( + parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123') + ).toEqual({ + roomIdOrAlias: '!room:example.com', + eventId: '$event123', + viaServers: undefined, + }); + }); +}); diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts index ae0a9f4f9..0397a8010 100644 --- a/src/app/plugins/matrix-to.ts +++ b/src/app/plugins/matrix-to.ts @@ -5,7 +5,7 @@ let MATRIX_TO_BASE = 'https://matrix.to'; * Must be called before any getMatrixTo* functions are used. */ export const setMatrixToBase = (baseUrl?: string): void => { - if (baseUrl) MATRIX_TO_BASE = baseUrl.replace(/\/$/, ''); + MATRIX_TO_BASE = baseUrl ? baseUrl.replace(/\/$/, '') : 'https://matrix.to'; }; export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`; @@ -64,7 +64,8 @@ let cachedRegexes: { const getMatchRegexes = () => { if (cachedRegexBase === MATRIX_TO_BASE && cachedRegexes) return cachedRegexes; cachedRegexBase = MATRIX_TO_BASE; - const standard = escapeForRegex('https://matrix.to'); + // Use https? so both http:// and https://matrix.to are accepted (original behaviour). + const standard = `https?://${escapeForRegex('matrix.to')}`; const b = MATRIX_TO_BASE !== 'https://matrix.to' ? `(?:${standard}|${escapeForRegex(MATRIX_TO_BASE)})` @@ -72,7 +73,7 @@ const getMatchRegexes = () => { cachedRegexes = { any: new RegExp(`^${b}\\S*$`), user: new RegExp(`^${b}/#/(@[^:\\s]+:[^?/\\s]+)\\/?$`), - room: new RegExp(`^${b}/#/([#!][^?/\\s]+)\\/?([?\\S]*)?$`), + room: new RegExp(`^${b}/#/([#!][^?/\\s]+)\\/?(\\?[\\S]*)?$`), event: new RegExp(`^${b}/#/([#!][^?/\\s]+)/(\\$[^?/\\s]+)\\/?([?\\S]*)?$`), }; return cachedRegexes; From 6ab1fd4c3af4bffdf7b80df128156e3ca152a9ce Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 21:44:40 -0400 Subject: [PATCH 5/7] Update fix-matrix-to-custom-base.md --- .changeset/fix-matrix-to-custom-base.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/fix-matrix-to-custom-base.md b/.changeset/fix-matrix-to-custom-base.md index 5d63f371f..396d2a714 100644 --- a/.changeset/fix-matrix-to-custom-base.md +++ b/.changeset/fix-matrix-to-custom-base.md @@ -3,5 +3,3 @@ default: patch --- Support `matrixToBaseUrl` in `config.json` to override the default `matrix.to` link base URL. - -Closes #67 From 55fd9bbc7ebdced2d3e8e85aec4c11846c7de3a0 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 21:51:34 -0400 Subject: [PATCH 6/7] style(matrix-to): fix prettier formatting in test file --- src/app/plugins/matrix-to.test.ts | 59 +++++++++++-------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/src/app/plugins/matrix-to.test.ts b/src/app/plugins/matrix-to.test.ts index 806dc03e7..519ecd825 100644 --- a/src/app/plugins/matrix-to.test.ts +++ b/src/app/plugins/matrix-to.test.ts @@ -21,9 +21,7 @@ afterEach(() => { describe('getMatrixToUser', () => { it('generates a standard matrix.to user link', () => { - expect(getMatrixToUser('@alice:example.com')).toBe( - 'https://matrix.to/#/@alice:example.com' - ); + expect(getMatrixToUser('@alice:example.com')).toBe('https://matrix.to/#/@alice:example.com'); }); it('uses custom base when configured', () => { @@ -43,9 +41,7 @@ describe('getMatrixToUser', () => { describe('getMatrixToRoom', () => { it('generates a standard matrix.to room link', () => { - expect(getMatrixToRoom('!room:example.com')).toBe( - 'https://matrix.to/#/!room:example.com' - ); + expect(getMatrixToRoom('!room:example.com')).toBe('https://matrix.to/#/!room:example.com'); }); it('appends via servers', () => { @@ -70,9 +66,9 @@ describe('getMatrixToRoomEvent', () => { }); it('appends via servers', () => { - expect( - getMatrixToRoomEvent('!room:example.com', '$event123', ['s1.org']) - ).toBe('https://matrix.to/#/!room:example.com/$event123?via=s1.org'); + expect(getMatrixToRoomEvent('!room:example.com', '$event123', ['s1.org'])).toBe( + 'https://matrix.to/#/!room:example.com/$event123?via=s1.org' + ); }); it('uses custom base when configured', () => { @@ -117,9 +113,7 @@ describe('testMatrixTo', () => { describe('parseMatrixToUser', () => { it('parses a standard matrix.to user link', () => { - expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe( - '@alice:example.com' - ); + expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe('@alice:example.com'); }); it('returns undefined for non-user links', () => { @@ -128,16 +122,14 @@ describe('parseMatrixToUser', () => { it('parses user links from custom base', () => { setMatrixToBase('https://matrix.example.org'); - expect( - parseMatrixToUser('https://matrix.example.org/#/@alice:example.com') - ).toBe('@alice:example.com'); + expect(parseMatrixToUser('https://matrix.example.org/#/@alice:example.com')).toBe( + '@alice:example.com' + ); }); it('parses standard matrix.to user links even after custom base is set', () => { setMatrixToBase('https://matrix.example.org'); - expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe( - '@alice:example.com' - ); + expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe('@alice:example.com'); }); }); @@ -170,16 +162,15 @@ describe('parseMatrixToRoom', () => { }); it('returns undefined for event links (too many segments)', () => { - expect( - parseMatrixToRoom('https://matrix.to/#/!room:example.com/$event123') - ).toBeUndefined(); + expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com/$event123')).toBeUndefined(); }); it('parses room links from custom base', () => { setMatrixToBase('https://matrix.example.org'); - expect( - parseMatrixToRoom('https://matrix.example.org/#/!room:example.com') - ).toEqual({ roomIdOrAlias: '!room:example.com', viaServers: undefined }); + expect(parseMatrixToRoom('https://matrix.example.org/#/!room:example.com')).toEqual({ + roomIdOrAlias: '!room:example.com', + viaServers: undefined, + }); }); it('still parses standard matrix.to room links after custom base is set', () => { @@ -197,9 +188,7 @@ describe('parseMatrixToRoom', () => { describe('parseMatrixToRoomEvent', () => { it('parses a room event link', () => { - expect( - parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123') - ).toEqual({ + expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123')).toEqual({ roomIdOrAlias: '!room:example.com', eventId: '$event123', viaServers: undefined, @@ -208,9 +197,7 @@ describe('parseMatrixToRoomEvent', () => { it('parses via servers', () => { expect( - parseMatrixToRoomEvent( - 'https://matrix.to/#/!room:example.com/$event123?via=s1.org' - ) + parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123?via=s1.org') ).toEqual({ roomIdOrAlias: '!room:example.com', eventId: '$event123', @@ -219,17 +206,13 @@ describe('parseMatrixToRoomEvent', () => { }); it('returns undefined for room-only links', () => { - expect( - parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com') - ).toBeUndefined(); + expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com')).toBeUndefined(); }); it('parses event links from custom base', () => { setMatrixToBase('https://matrix.example.org'); expect( - parseMatrixToRoomEvent( - 'https://matrix.example.org/#/!room:example.com/$event123' - ) + parseMatrixToRoomEvent('https://matrix.example.org/#/!room:example.com/$event123') ).toEqual({ roomIdOrAlias: '!room:example.com', eventId: '$event123', @@ -239,9 +222,7 @@ describe('parseMatrixToRoomEvent', () => { it('still parses standard matrix.to event links after custom base is set', () => { setMatrixToBase('https://matrix.example.org'); - expect( - parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123') - ).toEqual({ + expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123')).toEqual({ roomIdOrAlias: '!room:example.com', eventId: '$event123', viaServers: undefined, From f65f12687ab28de3a8892ee0626fec4da7ac1b92 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Mon, 16 Mar 2026 22:19:23 -0400 Subject: [PATCH 7/7] =?UTF-8?q?test(matrix-to):=20add=20RTL=20integration?= =?UTF-8?q?=20tests=20for=20config=E2=86=92matrix-to=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ClientConfigLoader.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/app/components/ClientConfigLoader.test.tsx diff --git a/src/app/components/ClientConfigLoader.test.tsx b/src/app/components/ClientConfigLoader.test.tsx new file mode 100644 index 000000000..b80e40884 --- /dev/null +++ b/src/app/components/ClientConfigLoader.test.tsx @@ -0,0 +1,99 @@ +/** + * Integration tests: exercises the full config-load → setMatrixToBase → URL + * generation pipeline that App.tsx runs on startup. + * + * The pattern under test mirrors App.tsx: + * + * {(config) => { setMatrixToBase(config.matrixToBaseUrl); ... }} + * + * + * We mock fetch so we don't need a real config.json or a live matrix.to instance. + */ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import { setMatrixToBase, getMatrixToRoom, getMatrixToUser } from '$plugins/matrix-to'; +import { ClientConfigLoader } from './ClientConfigLoader'; + +afterEach(() => { + setMatrixToBase(); // reset module state to 'https://matrix.to' + vi.unstubAllGlobals(); +}); + +const mockFetch = (config: object) => + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve(config) })); + +describe('ClientConfigLoader + matrix-to wiring', () => { + it('generates a standard matrix.to URL when no custom base is configured', async () => { + mockFetch({}); + + render( + + {(config) => { + setMatrixToBase(config.matrixToBaseUrl); + return {getMatrixToRoom('!room:example.com')}; + }} + + ); + + await waitFor(() => + expect(screen.getByTestId('link')).toHaveTextContent('https://matrix.to/#/!room:example.com') + ); + }); + + it('generates a custom-base URL for rooms when matrixToBaseUrl is set', async () => { + mockFetch({ matrixToBaseUrl: 'https://custom.example.org' }); + + render( + + {(config) => { + setMatrixToBase(config.matrixToBaseUrl); + return {getMatrixToRoom('!room:example.com')}; + }} + + ); + + await waitFor(() => + expect(screen.getByTestId('link')).toHaveTextContent( + 'https://custom.example.org/#/!room:example.com' + ) + ); + }); + + it('generates a custom-base URL for users when matrixToBaseUrl is set', async () => { + mockFetch({ matrixToBaseUrl: 'https://custom.example.org' }); + + render( + + {(config) => { + setMatrixToBase(config.matrixToBaseUrl); + return {getMatrixToUser('@alice:example.com')}; + }} + + ); + + await waitFor(() => + expect(screen.getByTestId('user')).toHaveTextContent( + 'https://custom.example.org/#/@alice:example.com' + ) + ); + }); + + it('strips a trailing slash from matrixToBaseUrl', async () => { + mockFetch({ matrixToBaseUrl: 'https://custom.example.org/' }); + + render( + + {(config) => { + setMatrixToBase(config.matrixToBaseUrl); + return {getMatrixToRoom('!room:example.com')}; + }} + + ); + + await waitFor(() => + expect(screen.getByTestId('link')).toHaveTextContent( + 'https://custom.example.org/#/!room:example.com' + ) + ); + }); +});