|
|
|
@@ -0,0 +1,98 @@
|
|
|
|
|
import { useCallback, useEffect } from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
|
|
|
import { useRoomNavigate } from './useRoomNavigate';
|
|
|
|
|
import { isRoomId } from '../utils/matrix';
|
|
|
|
|
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
|
|
|
|
|
import { _RoomSearchParams } from '../pages/paths';
|
|
|
|
|
import { parseMatrixToRoom, parseMatrixToRoomEvent, testMatrixTo } from '../plugins/matrix-to';
|
|
|
|
|
|
|
|
|
|
/** Desktop (Tauri) injects deep links via a DOM CustomEvent — see lib.rs. */
|
|
|
|
|
const DEEP_LINK_EVENT = 'lotus-deeplink';
|
|
|
|
|
|
|
|
|
|
const inTauri = (): boolean => '__TAURI_INTERNALS__' in window;
|
|
|
|
|
|
|
|
|
|
const tryDecode = (value: string): string => {
|
|
|
|
|
try {
|
|
|
|
|
return decodeURIComponent(value);
|
|
|
|
|
} catch {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert a `matrix:` URI (MSC2312) to its `matrix.to` equivalent so we can
|
|
|
|
|
* reuse the existing matrix.to parsers. Returns undefined for forms we don't
|
|
|
|
|
* route (e.g. user links, which need an anchor/room context to open a profile).
|
|
|
|
|
*
|
|
|
|
|
* matrix:r/room:server -> https://matrix.to/#/#room:server
|
|
|
|
|
* matrix:roomid/id:server/e/$ev -> https://matrix.to/#/!id:server/$ev
|
|
|
|
|
*/
|
|
|
|
|
const matrixUriToMatrixTo = (uri: string): string | undefined => {
|
|
|
|
|
const body = uri.slice('matrix:'.length);
|
|
|
|
|
const [pathPart, queryPart] = body.split('?');
|
|
|
|
|
const segments = pathPart.split('/');
|
|
|
|
|
if (segments.length < 2) return undefined;
|
|
|
|
|
|
|
|
|
|
const [kind, rawId, evKind, rawEventId] = segments;
|
|
|
|
|
const sigil = kind === 'r' ? '#' : kind === 'roomid' ? '!' : undefined;
|
|
|
|
|
if (!sigil || !rawId) return undefined;
|
|
|
|
|
|
|
|
|
|
let fragment = `${sigil}${tryDecode(rawId)}`;
|
|
|
|
|
if (evKind === 'e' && rawEventId) {
|
|
|
|
|
fragment += `/$${tryDecode(rawEventId)}`;
|
|
|
|
|
}
|
|
|
|
|
const query = queryPart ? `?${queryPart}` : '';
|
|
|
|
|
return `https://matrix.to/#/${fragment}${query}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Routes deep links opened from outside the app (Windows `matrix:` links,
|
|
|
|
|
* forwarded from Tauri) into the client, mirroring the navigation in
|
|
|
|
|
* useMentionClickHandler. No-op outside Tauri.
|
|
|
|
|
*/
|
|
|
|
|
export const useDeepLinkNavigate = (): void => {
|
|
|
|
|
const mx = useMatrixClient();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
|
|
|
|
|
|
|
|
|
const navigateUri = useCallback(
|
|
|
|
|
(rawUrl: string) => {
|
|
|
|
|
const href = rawUrl.startsWith('matrix:') ? matrixUriToMatrixTo(rawUrl) : rawUrl;
|
|
|
|
|
if (!href || !testMatrixTo(href)) return;
|
|
|
|
|
|
|
|
|
|
const roomEvent = parseMatrixToRoomEvent(href);
|
|
|
|
|
const target = roomEvent ?? parseMatrixToRoom(href);
|
|
|
|
|
if (!target) return; // users / unsupported forms
|
|
|
|
|
|
|
|
|
|
const { roomIdOrAlias, viaServers } = target;
|
|
|
|
|
const eventId = roomEvent?.eventId;
|
|
|
|
|
|
|
|
|
|
// Already-joined room/space: navigate directly.
|
|
|
|
|
if (isRoomId(roomIdOrAlias) && mx.getRoom(roomIdOrAlias)) {
|
|
|
|
|
if (mx.getRoom(roomIdOrAlias)?.isSpaceRoom()) navigateSpace(roomIdOrAlias);
|
|
|
|
|
else navigateRoom(roomIdOrAlias, eventId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise route through the home path (triggers the join/preview flow).
|
|
|
|
|
const path = getHomeRoomPath(roomIdOrAlias, eventId);
|
|
|
|
|
navigate(
|
|
|
|
|
viaServers && viaServers.length > 0
|
|
|
|
|
? withSearchParam<_RoomSearchParams>(path, { viaServers: viaServers.join(',') })
|
|
|
|
|
: path,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[mx, navigate, navigateRoom, navigateSpace],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!inTauri()) return undefined;
|
|
|
|
|
const handler = (evt: Event) => {
|
|
|
|
|
const { detail } = evt as CustomEvent<unknown>;
|
|
|
|
|
if (typeof detail === 'string') navigateUri(detail);
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener(DEEP_LINK_EVENT, handler);
|
|
|
|
|
return () => window.removeEventListener(DEEP_LINK_EVENT, handler);
|
|
|
|
|
}, [navigateUri]);
|
|
|
|
|
};
|