Files
cinny/src/playground/liveClient.ts
litruv 0da4f0d6cb Add emoji confetti bursts and component playground.
Support app.relay.emoji_confetti on jumbo emoji clicks with canvas physics,
per-emoji particle profiles, and a fixed overlay that does not affect scroll.
Also add a Vite playground for live component previews and theme hover preview.
2026-07-29 20:40:43 +10:00

152 lines
4.1 KiB
TypeScript

import {
ClientEvent,
createClient,
MemoryStore,
SyncState,
type MatrixClient,
type Room,
} from 'matrix-js-sdk';
import { getCurrentSession, type Session } from '@cinny/app/state/sessions';
export type LiveConnectMode = 'mock' | 'live';
export type ManualSessionInput = {
baseUrl: string;
userId: string;
deviceId: string;
accessToken: string;
};
const MANUAL_SESSION_KEY = 'playgroundManualSession';
/** Session already used by Paarrot/Cinny on this origin (localStorage). */
export function peekPaarrotSession(): Session | undefined {
return getCurrentSession();
}
export function loadManualSession(): ManualSessionInput | null {
try {
const raw = sessionStorage.getItem(MANUAL_SESSION_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as ManualSessionInput;
if (parsed?.baseUrl && parsed?.userId && parsed?.deviceId && parsed?.accessToken) {
return parsed;
}
} catch {
/* ignore */
}
return null;
}
export function saveManualSession(input: ManualSessionInput | null) {
if (!input) {
sessionStorage.removeItem(MANUAL_SESSION_KEY);
return;
}
sessionStorage.setItem(MANUAL_SESSION_KEY, JSON.stringify(input));
}
function normalizeSession(input: Session | ManualSessionInput): Session {
return {
baseUrl: input.baseUrl.replace(/\/$/, ''),
userId: input.userId,
deviceId: input.deviceId,
accessToken: input.accessToken,
};
}
function resolveSession(explicit?: Session | ManualSessionInput): Session {
if (explicit) return normalizeSession(explicit);
const fromPaarrot = peekPaarrotSession();
if (fromPaarrot) return normalizeSession(fromPaarrot);
const manual = loadManualSession();
if (manual) return normalizeSession(manual);
throw new Error(
'No Paarrot session found. Log in at / (same origin), or paste homeserver + token in the playground.'
);
}
function waitForPrepared(mx: MatrixClient, timeoutMs = 90_000): Promise<void> {
return new Promise((resolve, reject) => {
const finish = (ok: boolean, err?: Error) => {
clearTimeout(timer);
mx.off(ClientEvent.Sync, onSync);
if (ok) resolve();
else reject(err ?? new Error('Sync failed'));
};
const timer = window.setTimeout(
() => finish(false, new Error('Timed out waiting for Matrix sync')),
timeoutMs
);
const onSync = (state: SyncState | null) => {
if (state === SyncState.Prepared || state === SyncState.Syncing) {
finish(true);
}
};
mx.on(ClientEvent.Sync, onSync);
const cur = mx.getSyncState();
if (cur === SyncState.Prepared || cur === SyncState.Syncing) {
finish(true);
}
});
}
/**
* Live Matrix client for the playground.
* Uses MemoryStore so it won't fight Paarrot's IndexedDB sync/crypto stores.
* Skips Rust crypto — encrypted timelines won't decrypt, but profiles/rooms/settings APIs work.
*/
export async function connectLiveMatrixClient(
session?: Session | ManualSessionInput
): Promise<MatrixClient> {
const finalSession = resolveSession(session);
const mx = createClient({
baseUrl: finalSession.baseUrl,
accessToken: finalSession.accessToken,
userId: finalSession.userId,
deviceId: finalSession.deviceId,
store: new MemoryStore({ localStorage: globalThis.localStorage }),
timelineSupport: true,
});
mx.setMaxListeners(50);
await mx.startClient({
lazyLoadMembers: true,
initialSyncLimit: 20,
});
await waitForPrepared(mx);
return mx;
}
export function stopLiveMatrixClient(mx: MatrixClient | null | undefined) {
if (!mx) return;
try {
mx.stopClient();
} catch {
/* ignore */
}
}
export function listJoinedRooms(mx: MatrixClient): Room[] {
return mx
.getRooms()
.filter((r) => r.getMyMembership() === 'join')
.sort((a, b) => (a.name || a.roomId).localeCompare(b.name || b.roomId));
}
export function pickDefaultRoom(mx: MatrixClient, preferredId?: string): Room | undefined {
const rooms = listJoinedRooms(mx);
if (preferredId) {
const hit = rooms.find((r) => r.roomId === preferredId);
if (hit) return hit;
}
return rooms[0];
}