Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-matrix-to-custom-base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Support `matrixToBaseUrl` in `config.json` to override the default `matrix.to` link base URL.
99 changes: 99 additions & 0 deletions src/app/components/ClientConfigLoader.test.tsx
Original file line number Diff line number Diff line change
@@ -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:
* <ClientConfigLoader>
* {(config) => { setMatrixToBase(config.matrixToBaseUrl); ... }}
* </ClientConfigLoader>
*
* 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(
<ClientConfigLoader>
{(config) => {
setMatrixToBase(config.matrixToBaseUrl);
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
}}
</ClientConfigLoader>
);

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(
<ClientConfigLoader>
{(config) => {
setMatrixToBase(config.matrixToBaseUrl);
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
}}
</ClientConfigLoader>
);

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(
<ClientConfigLoader>
{(config) => {
setMatrixToBase(config.matrixToBaseUrl);
return <span data-testid="user">{getMatrixToUser('@alice:example.com')}</span>;
}}
</ClientConfigLoader>
);

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(
<ClientConfigLoader>
{(config) => {
setMatrixToBase(config.matrixToBaseUrl);
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
}}
</ClientConfigLoader>
);

await waitFor(() =>
expect(screen.getByTestId('link')).toHaveTextContent(
'https://custom.example.org/#/!room:example.com'
)
);
});
});
2 changes: 2 additions & 0 deletions src/app/hooks/useClientConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export type ClientConfig = {
};

hashRouter?: HashRouterConfig;

matrixToBaseUrl?: string;
};

const ClientConfigContext = createContext<ClientConfig | null>(null);
Expand Down
24 changes: 14 additions & 10 deletions src/app/pages/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,16 +36,19 @@ function App() {
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
)}
>
{(clientConfig) => (
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<RouterProvider router={createRouter(clientConfig, screenSize)} />
</JotaiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</ClientConfigProvider>
)}
{(clientConfig) => {
setMatrixToBase(clientConfig.matrixToBaseUrl);
return (
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<RouterProvider router={createRouter(clientConfig, screenSize)} />
</JotaiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</ClientConfigProvider>
);
}}
</ClientConfigLoader>
</FeatureCheck>
</ScreenSizeProvider>
Expand Down
231 changes: 231 additions & 0 deletions src/app/plugins/matrix-to.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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,
});
});
});
Loading
Loading