/** * Inbox title display helpers. * * Mirrors packages/views/inbox/components/inbox-display.ts. Keeping behavior * identical is required by apps/mobile/CLAUDE.md "Behavioral parity": * the title a user sees in the mobile inbox MUST match what they see on * web for the same item. When the web version changes, sync this file. */ import type { InboxItem } from "@multica/core/types"; function singleLine(value: string | null | undefined): string { return (value ?? "").replace(/\s+/g, " ").trim(); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function stripQuickCreatePrefix( title: string, identifier?: string, ): string { const normalized = singleLine(title); if (!normalized) return ""; if (identifier) { const exactPrefix = new RegExp( `^Created\\s+${escapeRegExp(identifier)}:\\s*`, "i", ); const withoutExactPrefix = normalized.replace(exactPrefix, ""); if (withoutExactPrefix !== normalized) return withoutExactPrefix.trim(); } return normalized.replace(/^Created\s+[A-Z][A-Z0-9]*-\d+:\s*/i, "").trim(); } export function getInboxDisplayTitle(item: InboxItem): string { const details = item.details ?? {}; if (item.type === "quick_create_done") { const cleanedTitle = stripQuickCreatePrefix(item.title, details.identifier); if (cleanedTitle) return cleanedTitle; const prompt = singleLine(details.original_prompt); if (prompt) return prompt; } if (item.type === "quick_create_failed") { const prompt = singleLine(details.original_prompt); if (prompt) return prompt; } return item.title; } /** * Deduplicate inbox items by issue_id (Linear-style: one entry per issue). * * Mirrors packages/core/inbox/queries.ts deduplicateInboxItems. **MUST stay * aligned with that function** — see the inbox dedup incident in this file's * companion `apps/mobile/CLAUDE.md` "Behavioral parity" section. Skipping * this step makes the same workspace/user show different unread counts on * mobile vs web. * * Steps: * 1. Drop archived rows (these never appear in web's inbox view). * 2. Group by `issue_id` (fall back to `id` for items with no issue * attached — e.g. quick_create_failed). * 3. In each group, keep the newest by `created_at`. * 4. Preserve the newest grouped `comment_id` anchor when the newest row * is a later status/metadata event for the same issue. * 5. Sort the result newest-first. */ export function deduplicateInboxItems(items: InboxItem[]): InboxItem[] { const active = items.filter((i) => !i.archived); const groups = new Map(); for (const item of active) { const key = item.issue_id ?? item.id; const group = groups.get(key) ?? []; group.push(item); groups.set(key, group); } const merged: InboxItem[] = []; for (const group of groups.values()) { group.sort( (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), ); const newest = group[0]; if (!newest) continue; const commentId = newest.details?.comment_id ?? group.find((item) => item.details?.comment_id)?.details?.comment_id; if (commentId && newest.details?.comment_id !== commentId) { merged.push({ ...newest, details: { ...(newest.details ?? {}), comment_id: commentId }, }); continue; } merged.push(newest); } return merged.sort( (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), ); }