feat(links): Copy Lotus Link permalinks, in-app recognition, /home redirect for joined rooms, via= alias, OIDC deep-link redirect (#130)
CI / Build & Quality Checks (push) Successful in 2m4s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Successful in 2m11s
CI / Build & Quality Checks (push) Successful in 2m4s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Successful in 2m11s
matrix.to cannot target this deployment (Cinny adapter hard-codes app.cinny.in; web-instance[] is Element-only, allowlisted), so add a "Copy Lotus Link" next to every Copy Link (message menu, space header menu, sidebar space tab) producing https://<this origin>/home/<room>/<event> ?viaServers=… via plugins/lotus-permalink.ts. Lotus links in messages are rewritten to their matrix.to form inside the HTML parser so they render as room/event mentions and navigate in place. /home/<room> for a joined room that belongs to a space or Direct now redirects to its own route (was a preview card with a View button; also the form matrix.to → Cinny links use). ?via= is accepted as an alias of ?viaServers= (what the matrix.to Cinny adapter emits). A deep link visited while logged out is now honoured after an OIDC login: the OIDC callback reloads at the app root, which discarded the stored path — the index loader consumes it via the shared takeAfterLoginPath(). Verified end-to-end with Playwright on a local Synapse: logged-out cold link → login → lands on the event under the space route; menu copies the expected link; a pasted Lotus link renders as a mention and jumps in place; both space menus copy the space link. Closes #130 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { getHomeRoomPath, getSpacePath, withSearchParam } from '../pages/pathUtils';
|
||||
import { _RoomSearchParams } from '../pages/paths';
|
||||
import { MatrixToRoom, MatrixToRoomEvent } from './matrix-to';
|
||||
|
||||
/**
|
||||
* [Gitea #130] Direct Lotus permalinks.
|
||||
*
|
||||
* `matrix.to` cannot be pointed at this deployment (its Cinny adapter is
|
||||
* hard-coded to app.cinny.in and `web-instance[]` only works for Element), so
|
||||
* next to every "Copy Link" there is a "Copy Lotus Link" that yields
|
||||
* `https://<this deployment>/home/<room>/<event>?viaServers=…` — the same
|
||||
* routes the app already serves cold (logged-out → login → back to the room).
|
||||
*
|
||||
* `baseUrl` is `getOriginBaseUrl(hashRouter)` so the result is right for any
|
||||
* deployment and for the hash-router config.
|
||||
*/
|
||||
|
||||
const trimSlash = (s: string): string => s.replace(/\/+$/, '');
|
||||
|
||||
const withVia = (path: string, viaServers?: string[]): string =>
|
||||
viaServers && viaServers.length > 0
|
||||
? withSearchParam<_RoomSearchParams>(path, { viaServers: viaServers.join(',') })
|
||||
: path;
|
||||
|
||||
export const getLotusRoomPermalink = (
|
||||
baseUrl: string,
|
||||
roomIdOrAlias: string,
|
||||
eventId?: string,
|
||||
viaServers?: string[],
|
||||
): string => `${trimSlash(baseUrl)}${withVia(getHomeRoomPath(roomIdOrAlias, eventId), viaServers)}`;
|
||||
|
||||
export const getLotusSpacePermalink = (
|
||||
baseUrl: string,
|
||||
spaceIdOrAlias: string,
|
||||
viaServers?: string[],
|
||||
): string => `${trimSlash(baseUrl)}${withVia(getSpacePath(spaceIdOrAlias), viaServers)}`;
|
||||
|
||||
const tryDecode = (value: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const isRoomIdOrAlias = (s: string): boolean => /^[!#][^/\s]+:[^/\s]+$/.test(s);
|
||||
const isEventId = (s: string): boolean => /^\$[^/\s]+$/.test(s);
|
||||
|
||||
/**
|
||||
* Parse a link into the same shape the matrix.to parsers return, when it
|
||||
* points at THIS deployment's room/space routes:
|
||||
*
|
||||
* <base>/home/<room>/<event>?viaServers=a,b
|
||||
* <base>/direct/<room>/<event>
|
||||
* <base>/<space>/ (space lobby)
|
||||
* <base>/<space>/<room>/<event> (room inside a space → the room)
|
||||
*
|
||||
* Anything else — settings pages, explore, a different origin — is undefined.
|
||||
* `via` is accepted as an alias of `viaServers` because the matrix.to Cinny
|
||||
* adapter emits it.
|
||||
*/
|
||||
export const parseLotusPermalink = (
|
||||
baseUrl: string,
|
||||
href: string,
|
||||
): MatrixToRoom | MatrixToRoomEvent | undefined => {
|
||||
const base = trimSlash(baseUrl);
|
||||
if (!href.startsWith(`${base}/`)) return undefined;
|
||||
const rest = href.slice(base.length);
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(rest, 'http://lotus.invalid');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const segments = url.pathname
|
||||
.split('/')
|
||||
.filter((s) => s.length > 0)
|
||||
.map(tryDecode);
|
||||
const via = [...url.searchParams.getAll('viaServers'), ...url.searchParams.getAll('via')]
|
||||
.flatMap((v) => v.split(','))
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0);
|
||||
const viaServers = via.length > 0 ? via : undefined;
|
||||
|
||||
let roomIdOrAlias: string | undefined;
|
||||
let eventId: string | undefined;
|
||||
|
||||
if (segments[0] === 'home' || segments[0] === 'direct') {
|
||||
[, roomIdOrAlias, eventId] = segments;
|
||||
} else if (segments[0] && isRoomIdOrAlias(segments[0])) {
|
||||
// /<space>/ or /<space>/<room>/<event>
|
||||
roomIdOrAlias = segments[1] && isRoomIdOrAlias(segments[1]) ? segments[1] : segments[0];
|
||||
if (segments[1] && isRoomIdOrAlias(segments[1])) [, , eventId] = segments;
|
||||
}
|
||||
|
||||
if (!roomIdOrAlias || !isRoomIdOrAlias(roomIdOrAlias)) return undefined;
|
||||
if (eventId !== undefined && !isEventId(eventId)) return undefined;
|
||||
return eventId ? { roomIdOrAlias, eventId, viaServers } : { roomIdOrAlias, viaServers };
|
||||
};
|
||||
|
||||
/** Rewrite a Lotus permalink as the matrix.to link the rest of the app already understands. */
|
||||
export const lotusPermalinkToMatrixTo = (baseUrl: string, href: string): string | undefined => {
|
||||
const parsed = parseLotusPermalink(baseUrl, href);
|
||||
if (!parsed) return undefined;
|
||||
const fragment =
|
||||
'eventId' in parsed ? `${parsed.roomIdOrAlias}/${parsed.eventId}` : parsed.roomIdOrAlias;
|
||||
const query = parsed.viaServers ? `?${parsed.viaServers.map((s) => `via=${s}`).join('&')}` : '';
|
||||
return `https://matrix.to/#/${fragment}${query}`;
|
||||
};
|
||||
Reference in New Issue
Block a user