feat: web UI chat render, panels, presence + analytics

This commit is contained in:
hh committed 2026-05-31 19:41:37 +02:00
1 parent 75425d1bee
commit ed469ba8dd
83 files changed
+6034 -136

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
import { accounts } from "$lib/stores/accounts.svelte";
import { auth } from "$lib/stores/auth.svelte";
const BASE = import.meta.env.VITE_API_BASE ?? "/api";
const RETRY_DELAY = 2500;
export interface CustomEmojiAsset {
mime: string;
url: string;
}
const ready = new Map<string, CustomEmojiAsset>();
const missing = new Set<string>();
const inflight = new Map<string, Promise<CustomEmojiAsset | null>>();
function authHeaders(): Record<string, string> {
return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function fetchEmoji(
account: number,
id: string,
key: string,
retry: boolean
): Promise<CustomEmojiAsset | null> {
const url = `${BASE}/custom-emoji/${id}?account_id=${account}`;
const response = await fetch(url, { headers: authHeaders() });
if (response.ok) {
const blob = await response.blob();
const asset = { url: URL.createObjectURL(blob), mime: blob.type };
ready.set(key, asset);
return asset;
}
if (response.status === 409 && retry) {
await delay(RETRY_DELAY);
return fetchEmoji(account, id, key, false);
}
missing.add(key);
return null;
}
export function loadCustomEmoji(id: string): Promise<CustomEmojiAsset | null> {
const account = accounts.selectedId;
if (account === null) {
return Promise.resolve(null);
}
const key = `${account}:${id}`;
const cached = ready.get(key);
if (cached) {
return Promise.resolve(cached);
}
if (missing.has(key)) {
return Promise.resolve(null);
}
const existing = inflight.get(key);
if (existing) {
return existing;
}
const promise = fetchEmoji(account, id, key, true).finally(() => {
inflight.delete(key);
});
inflight.set(key, promise);
return promise;
}