feat(frontend): my message menu shows when it was read or listened, group readers open in a right panel

This commit is contained in:
hh committed 2026-10-19 02:40:49 +00:00
1 parent f03d7c2890
commit bb38e23f29
14 files changed
+543 -10

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
import { getJob, getReadInfo, refreshReadInfo } from "#lib/api/endpoints.js";
import type { ReadInfo } from "#lib/api/types.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
const POLL_INTERVAL = 1000;
const POLL_MAX = 20;
export interface ReadEntry {
fetching: boolean;
info: ReadInfo;
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function waitForJob(jobId: number) {
for (let attempt = 0; attempt < POLL_MAX; attempt += 1) {
// biome-ignore lint/performance/noAwaitInLoops: polling waits between tries on purpose
await delay(POLL_INTERVAL);
const job = await getJob(jobId);
if (job.status !== "pending" && job.status !== "running") {
return;
}
}
}
function createReads() {
let entries = $state<Record<string, ReadEntry>>({});
const inflight = new Set<string>();
function key(chatId: number, messageId: number): string {
return `${accounts.selectedId}:${chatId}:${messageId}`;
}
async function load(entryKey: string, chatId: number, messageId: number) {
const info = await getReadInfo(chatId, messageId);
entries[entryKey] = { fetching: info.can_fetch, info };
if (!info.can_fetch) {
return;
}
try {
const { job_id } = await refreshReadInfo(chatId, messageId);
if (job_id !== null) {
await waitForJob(job_id);
}
entries[entryKey] = {
fetching: false,
info: await getReadInfo(chatId, messageId),
};
} finally {
entries[entryKey].fetching = false;
}
}
return {
get(chatId: number, messageId: number): ReadEntry | undefined {
return entries[key(chatId, messageId)];
},
open(chatId: number, messageId: number) {
const entryKey = key(chatId, messageId);
if (accounts.selectedId === null || inflight.has(entryKey)) {
return;
}
inflight.add(entryKey);
load(entryKey, chatId, messageId)
.catch(() => undefined)
.finally(() => {
inflight.delete(entryKey);
});
},
};
}
export const reads = createReads();