ultisuite-client/lib/mail-thread/index.ts
2026-05-25 13:52:40 +02:00

189 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ConversationMessage, Email } from "@/lib/email-data"
/** Id fil pour étoile, important, libellés, composition. */
export function threadStoreId(email: Pick<Email, "id" | "threadHeadId">): string {
return email.threadHeadId ?? email.id
}
/** Message affiché en tête de fil en mode conversation. */
export function isThreadHeadMessage(
email: Pick<Email, "id" | "isThreadHead" | "threadHeadId">
): 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
}
/** Chronological split of a thread around the opened message (Gmail-style). */
export function splitThreadAroundOpenMessage<T extends { id: string; date: string }>(
messages: T[],
openId: string
): { before: T[]; after: T[] } {
const sorted = [...messages].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
)
const idx = sorted.findIndex((m) => m.id === openId)
if (idx < 0) {
return { before: sorted.filter((m) => m.id !== openId), after: [] }
}
return {
before: sorted.slice(0, idx),
after: sorted.slice(idx + 1),
}
}
/** Reconstruit la vue fil (conversation[]) pour laperçu / réponse. */
export function buildThreadViewEmail(
message: Email,
byId: Map<string, Email>
): 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<Email, "threadMessageIds" | "conversation">
): 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<string, boolean>,
byId: Map<string, Email>,
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]
}