feat: 1-to-1 message render + web data-lake backend

This commit is contained in:
hh committed 2026-05-31 01:45:05 +02:00
1 parent f0afb7ec5b
commit 75425d1bee
110 files changed
+10199 -54

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
import { requestMedia } from "$lib/api/client";
import { getMessageMedia } from "$lib/api/endpoints";
import type { MediaRef } from "$lib/api/types";
import { accounts } from "$lib/stores/accounts.svelte";
export type InlineMedia =
| {
state: "ready";
mediaId: number;
kind: string;
mime: string | null;
url: string;
transcript: string | null;
}
| { state: "not-downloaded"; mediaId: number; kind: string }
| { state: "missing" };
export interface ViewerItem {
downloaded: boolean;
kind: string;
mediaId: number | null;
messageId: number;
}
export function viewerItemsFrom(
messageId: number,
media: MediaRef[]
): ViewerItem[] {
if (media.length === 0) {
return [{ messageId, mediaId: null, kind: "", downloaded: false }];
}
return media.map((item) => ({
messageId: item.message_id,
mediaId: item.id,
kind: item.kind,
downloaded: item.downloaded,
}));
}
export type VisualKind = "image" | "video" | "other";
const VIDEO_KINDS = new Set(["video", "video_note", "animation", "gif"]);
export function visualKind(kind: string): VisualKind {
if (kind === "photo") {
return "image";
}
if (VIDEO_KINDS.has(kind)) {
return "video";
}
return "other";
}
const ready = new Map<string, InlineMedia>();
const inflight = new Map<string, Promise<InlineMedia>>();
function cacheKey(account: number, chatId: number, messageId: number): string {
return `${account}:${chatId}:${messageId}`;
}
async function resolve(
chatId: number,
messageId: number
): Promise<InlineMedia> {
let meta: Awaited<ReturnType<typeof getMessageMedia>>;
try {
meta = await getMessageMedia(chatId, messageId);
} catch {
return { state: "missing" };
}
if (!meta.downloaded) {
return { state: "not-downloaded", mediaId: meta.id, kind: meta.kind };
}
const blob = await requestMedia(meta.id);
if (blob.state === "ready") {
return {
state: "ready",
mediaId: meta.id,
kind: meta.kind,
mime: blob.mime,
url: blob.url,
transcript: meta.extracted_text,
};
}
if (blob.state === "not-downloaded") {
return { state: "not-downloaded", mediaId: meta.id, kind: meta.kind };
}
return { state: "missing" };
}
const byId = new Map<string, InlineMedia>();
const byIdInflight = new Map<string, Promise<InlineMedia>>();
async function resolveById(media: MediaRef): Promise<InlineMedia> {
if (media.id === null) {
return { state: "missing" };
}
if (!media.downloaded) {
return { state: "not-downloaded", mediaId: media.id, kind: media.kind };
}
const blob = await requestMedia(media.id);
if (blob.state === "ready") {
return {
state: "ready",
mediaId: media.id,
kind: media.kind,
mime: blob.mime,
url: blob.url,
transcript: null,
};
}
if (blob.state === "not-downloaded") {
return { state: "not-downloaded", mediaId: media.id, kind: media.kind };
}
return { state: "missing" };
}
export function loadMediaItem(media: MediaRef): Promise<InlineMedia> {
const account = accounts.selectedId;
if (account === null || media.id === null) {
return Promise.resolve<InlineMedia>({ state: "missing" });
}
const key = `${account}:${media.id}`;
const cached = byId.get(key);
if (cached) {
return Promise.resolve(cached);
}
const existing = byIdInflight.get(key);
if (existing) {
return existing;
}
const promise = resolveById(media)
.then((result) => {
if (result.state === "ready") {
byId.set(key, result);
}
return result;
})
.finally(() => {
byIdInflight.delete(key);
});
byIdInflight.set(key, promise);
return promise;
}
export function loadInlineMedia(
chatId: number,
messageId: number
): Promise<InlineMedia> {
const account = accounts.selectedId;
if (account === null) {
return Promise.resolve<InlineMedia>({ state: "missing" });
}
const key = cacheKey(account, chatId, messageId);
const cached = ready.get(key);
if (cached) {
return Promise.resolve(cached);
}
const existing = inflight.get(key);
if (existing) {
return existing;
}
const promise = resolve(chatId, messageId)
.then((result) => {
if (result.state === "ready") {
ready.set(key, result);
}
return result;
})
.finally(() => {
inflight.delete(key);
});
inflight.set(key, promise);
return promise;
}