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.
This commit is contained in:
151
src/playground/liveClient.ts
Normal file
151
src/playground/liveClient.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user