import type { InboxAdapter } from '../inbox/adapter'; import type { InboxItem, InboxState, MailAttachment, MailMessage, MailPerson } from '../inbox/types'; const PEOPLE: MailPerson[] = [ { id: 'pp_sofia', name: 'Sofia Ramirez', kind: 'staff' }, { id: 'pp_dan', name: 'Dan Whitaker', kind: 'staff' }, { id: 'cust_acme', name: 'Acme Roofing (Client)', kind: 'customer' }, ]; interface MockMail { subject: string; messages: MailMessage[]; } /** * In-memory inbox for demos/tests. State is PER INSTANCE. Seeds a few work items (mention, * needs-reply, alert) plus mail threads that fold into the Open view as MAIL rows. */ export class MockInboxAdapter implements InboxAdapter { private seq = 100; private readonly items: InboxItem[]; private readonly threads = new Map(); /** threadIds that surface as standalone MAIL rows (in addition to work items). */ private readonly mailFold = ['mt_welcome', 'mt_invoice']; constructor(private readonly now: () => string = () => new Date().toISOString()) { const at = this.now(); this.items = [ { id: 'in_1', kind: 'MENTION', state: 'OPEN', title: 'Sofia mentioned you', summary: '@you — can you confirm the Henderson scope?', priority: 'HIGH', threadId: 'mt_mention', createdAt: at }, { id: 'in_2', kind: 'NEEDS_REPLY', state: 'OPEN', title: 'Reply needed — Storm response', summary: 'Dan: crew rolling out at 7', priority: 'MEDIUM', threadId: 'mt_storm', createdAt: at }, { id: 'in_3', kind: 'SYSTEM_ALERT', state: 'OPEN', title: 'Ticket TK-204 updated', summary: 'Customer replied on the roof-leak case.', priority: 'LOW', createdAt: at }, ]; this.threads.set('mt_mention', { subject: 'Henderson scope', messages: [{ id: 'm1', actorId: 'pp_sofia', kind: 'EMAIL', at, html: '

Can you confirm the Henderson scope by EOD?

', text: 'Can you confirm the Henderson scope by EOD?', attachment: null }], }); this.threads.set('mt_storm', { subject: 'Storm response — East side', messages: [{ id: 'm2', actorId: 'pp_dan', kind: 'EMAIL', at, html: '

Crew is rolling out at 7. Confirm the Henderson job?

', text: 'Crew rolling out at 7. Confirm the Henderson job?', attachment: null }], }); this.threads.set('mt_welcome', { subject: 'Welcome to the Founders Club', messages: [{ id: 'm3', actorId: 'system', kind: 'EMAIL', at, html: '

Thanks for joining the Founders Club. Set up your account to get started.

', text: 'Thanks for joining the Founders Club.', attachment: null }], }); this.threads.set('mt_invoice', { subject: 'Invoice #1042 — Acme Roofing', messages: [{ id: 'm4', actorId: 'cust_acme', kind: 'EMAIL', at, html: '

Attached is invoice #1042 for the East-side job.

', text: 'Attached is invoice #1042 for the East-side job.', attachment: null }], }); } async listInbox(state?: InboxState): Promise { const showMail = !state || state === 'OPEN'; const work = this.items.filter((i) => (state ? i.state === state : true)); const mail: InboxItem[] = showMail ? this.mailFold.map((tid) => { const t = this.threads.get(tid)!; const last = t.messages[t.messages.length - 1]; return { id: `mail:${tid}`, kind: 'MAIL', state: 'OPEN' as InboxState, title: t.subject, ...(last?.text ? { summary: last.text } : {}), priority: 'LOW', threadId: tid, createdAt: last?.at ?? this.now(), }; }) : []; return [...mail, ...work]; } async transition(id: string, state: InboxState): Promise { const item = this.items.find((i) => i.id === id); if (item) item.state = state; } async mailHistory(threadId: string): Promise { return [...(this.threads.get(threadId)?.messages ?? [])]; } async mailReply(threadId: string, content: string, attachment?: MailAttachment): Promise { const t = this.threads.get(threadId); if (t) t.messages = [...t.messages, { id: `r_${this.seq++}`, actorId: 'me', kind: 'MESSAGE', at: this.now(), html: null, text: content || null, attachment: attachment ?? null }]; } async uploadAttachment(file: File): Promise { return { contentRef: `mock/${this.seq++}`, mimeType: file.type || 'application/octet-stream', sizeBytes: file.size, filename: file.name }; } async downloadAttachment(attachment: MailAttachment): Promise { // Demo: no real bytes — hand back a data URL so the click resolves without a network call. return `data:${attachment.mimeType};base64,`; } async directory(): Promise { return [...PEOPLE]; } async composeInternal(recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]): Promise { const threadId = `mt_${this.seq++}`; this.threads.set(threadId, { subject, messages: [{ id: `m_${this.seq++}`, actorId: 'me', kind: 'EMAIL', at: this.now(), html: `

${text}

`, text, attachment: attachments?.[0] ?? null }] }); this.mailFold.unshift(threadId); void recipientUserId; } async composeExternal(target: string, subject: string, text: string, attachments?: MailAttachment[]): Promise { // A mock external send has no in-app thread — no-op beyond acknowledging. void target; void subject; void text; void attachments; } }