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; if (typeof detail === 'string') navigateUri(detail); }; window.addEventListener(DEEP_LINK_EVENT, handler); return () => window.removeEventListener(DEEP_LINK_EVENT, handler); }, [navigateUri]); };