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.
152 lines
4.1 KiB
TypeScript
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];
|
|
}
|