import type { ConversationMessage, Email } from "@/lib/email-data" /** Id fil pour étoile, important, libellés, composition. */ export function threadStoreId(email: Pick): string { return email.threadHeadId ?? email.id } /** Message affiché en tête de fil en mode conversation. */ export function isThreadHeadMessage( email: Pick ): boolean { if (email.isThreadHead === false) return false if (email.isThreadHead === true) return true return !email.threadHeadId || email.threadHeadId === email.id } function threadMetaFromHead(head: Email): Pick< Email, | "subject" | "labels" | "starred" | "important" | "spam" | "deleted" | "tag" | "hasInvitation" | "calendarInvitation" | "scheduledSendAt" | "scheduledToName" | "snoozeWakeAt" > { return { subject: head.subject, labels: head.labels, starred: head.starred, important: head.important, spam: head.spam, deleted: head.deleted, tag: head.tag, hasInvitation: head.hasInvitation, calendarInvitation: head.calendarInvitation, scheduledSendAt: head.scheduledSendAt, scheduledToName: head.scheduledToName, snoozeWakeAt: head.snoozeWakeAt, } } function priorToEmail( msg: ConversationMessage, head: Email, threadMessageIds: string[] ): Email { return { ...threadMetaFromHead(head), id: msg.id, sender: msg.sender, senderEmail: msg.senderEmail, date: msg.date, preview: msg.preview, body: msg.body, attachments: msg.attachments, hasAttachment: (msg.attachments?.length ?? 0) > 0, read: msg.read ?? true, threadHeadId: head.id, threadMessageIds, isThreadHead: false, } } /** Découpe les fixtures legacy (`conversation[]`) en messages autonomes. */ export function normalizeLegacyEmailCatalog(raw: Email[]): Email[] { const out: Email[] = [] for (const root of raw) { const conv = root.conversation ?? [] if (conv.length === 0) { out.push({ ...root, conversation: undefined, threadHeadId: root.id, threadMessageIds: [root.id], isThreadHead: true, }) continue } const threadMessageIds = [...conv.map((m) => m.id), root.id] for (const msg of conv) { out.push(priorToEmail(msg, root, threadMessageIds)) } out.push({ ...root, conversation: undefined, threadHeadId: root.id, threadMessageIds, isThreadHead: true, }) } return out } /** Reconstruit la vue fil (conversation[]) pour l’aperçu / réponse. */ export function buildThreadViewEmail( message: Email, byId: Map ): Email { const headId = message.threadHeadId ?? message.id const head = byId.get(headId) ?? message const ids = head.threadMessageIds ?? [headId] const priorIds = ids.slice(0, -1) const conversation: ConversationMessage[] = priorIds.map((id) => { const m = byId.get(id)! return { id: m.id, sender: m.sender, senderEmail: m.senderEmail ?? "", date: m.date, body: m.body ?? "", preview: m.preview, attachments: m.attachments, } }) return { ...head, conversation } } export function getThreadMessageCount( email: Pick ): number { if (email.threadMessageIds?.length) return email.threadMessageIds.length return 1 + (email.conversation?.length ?? 0) } /** Lu en liste : fil entier en mode conversation, message seul sinon. */ export function isListRowRead( email: Email, readOverrides: Record, byId: Map, conversationMode: boolean ): boolean { if ( conversationMode && email.threadMessageIds && email.threadMessageIds.length > 1 ) { return email.threadMessageIds.every((id) => { const m = byId.get(id) if (!m) return true return readOverrides[id] ?? m.read }) } return readOverrides[email.id] ?? email.read } /** Marque lu / non lu (un message ou tout le fil). */ export function readStateTargets( email: Email, conversationMode: boolean ): string[] { if ( conversationMode && email.threadMessageIds && email.threadMessageIds.length > 1 ) { return [...email.threadMessageIds] } return [email.id] }