import { useCallback, useEffect, useState } from 'react'; import { useInboxAdapter } from './provider'; import type { InboxItem, InboxState, MailAttachment, MailMessage, MailPerson } from './types'; export interface InboxData { items: InboxItem[]; loading: boolean; error: string | null; transition: (id: string, state: InboxState) => Promise; refetch: () => void; } /** The unified inbox for a given filter state. Refetches when the filter changes. */ export function useInbox(state?: InboxState): InboxData { const adapter = useInboxAdapter(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [nonce, setNonce] = useState(0); useEffect(() => { let alive = true; setLoading(true); adapter .listInbox(state) .then((list) => { if (!alive) return; setItems(list); setError(null); }) .catch((e: unknown) => { if (!alive) return; setError(e instanceof Error ? e.message : String(e)); setItems([]); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [adapter, state, nonce]); const refetch = useCallback(() => setNonce((n) => n + 1), []); const transition = useCallback( async (id: string, next: InboxState) => { await adapter.transition(id, next); setNonce((n) => n + 1); }, [adapter], ); return { items, loading, error, transition, refetch }; } export interface MailThreadState { messages: MailMessage[]; loading: boolean; error: string | null; reply: (content: string, attachment?: MailAttachment) => Promise; canAttach: boolean; upload: (file: File) => Promise; canDownload: boolean; download: (attachment: MailAttachment) => Promise; refetch: () => void; } /** One mail thread: history + reply. */ export function useMailThread(threadId: string | null): MailThreadState { const adapter = useInboxAdapter(); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [nonce, setNonce] = useState(0); useEffect(() => { if (!threadId) { setMessages([]); setLoading(false); return; } let alive = true; setLoading(true); adapter .mailHistory(threadId) .then((m) => { if (!alive) return; setMessages(m); setError(null); }) .catch((e: unknown) => { if (alive) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [adapter, threadId, nonce]); const refetch = useCallback(() => setNonce((n) => n + 1), []); const reply = useCallback( async (content: string, attachment?: MailAttachment) => { if (!threadId) return; await adapter.mailReply(threadId, content, attachment); setNonce((n) => n + 1); }, [adapter, threadId], ); const upload = useCallback( async (file: File) => { if (!adapter.uploadAttachment) throw new Error('attachments are not supported by this adapter'); return adapter.uploadAttachment(file); }, [adapter], ); const download = useCallback( async (attachment: MailAttachment) => { if (!adapter.downloadAttachment) throw new Error('attachment download is not supported by this adapter'); return adapter.downloadAttachment(attachment); }, [adapter], ); return { messages, loading, error, reply, canAttach: typeof adapter.uploadAttachment === 'function', upload, canDownload: typeof adapter.downloadAttachment === 'function', download, refetch, }; } export interface ComposeState { supported: boolean; directory: MailPerson[]; sendInternal: (recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]) => Promise; sendExternal: (target: string, subject: string, text: string, attachments?: MailAttachment[]) => Promise; canAttach: boolean; upload: (file: File) => Promise; } /** Compose a new message — in-app (to a person) or external (to an email). */ export function useCompose(): ComposeState { const adapter = useInboxAdapter(); const supported = typeof adapter.composeInternal === 'function'; const [directory, setDirectory] = useState([]); useEffect(() => { if (!adapter.directory) return; let alive = true; adapter .directory() .then((d) => { if (alive) setDirectory(d); }) .catch(() => { if (alive) setDirectory([]); }); return () => { alive = false; }; }, [adapter]); const sendInternal = useCallback( async (recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]) => { if (!adapter.composeInternal) throw new Error('compose is not supported by this adapter'); await adapter.composeInternal(recipientUserId, subject, text, attachments); }, [adapter], ); const sendExternal = useCallback( async (target: string, subject: string, text: string, attachments?: MailAttachment[]) => { if (!adapter.composeExternal) throw new Error('compose is not supported by this adapter'); await adapter.composeExternal(target, subject, text, attachments); }, [adapter], ); const upload = useCallback( async (file: File) => { if (!adapter.uploadAttachment) throw new Error('attachments are not supported by this adapter'); return adapter.uploadAttachment(file); }, [adapter], ); return { supported, directory, sendInternal, sendExternal, canAttach: typeof adapter.uploadAttachment === 'function', upload }; }