import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Archive, Building2, CheckCheck, Inbox, Layers3, MailOpen, Paperclip, Pencil, Plus, RefreshCw, Reply, Search, Send, Trash2, UserRoundCheck, X } from "lucide-react"; import { ActionBlockerHint, Button, ConfirmDialog, DataGridPaginationBar, Dialog, DismissibleAlert, DocumentationHelpLink, FormField, IconButton, SegmentedControl, SelectionList, SelectionListItem, StatusBadge, ToggleSwitch, hasScope, i18nMessage, isApiError, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { createPostboxGrouping, createPostboxMessage, deletePostboxGrouping, getPostboxMessage, listPostboxGroupings, listPostboxMessages, listPostboxes, markPostboxMessage, replyToPostboxMessage, updatePostboxGrouping, type PostboxDirectoryItem, type PostboxGrouping, type PostboxMessage } from "../../api/postbox"; import { POSTBOX_DOCUMENTATION, POSTBOX_FIELD_DOCUMENTATION, POSTBOX_INTERFACE_I18N, postboxBusyReason } from "./interfacePatterns"; type GroupingDraft = { id: string; name: string; is_default: boolean; postbox_ids: string[]; }; type MessageStateFilter = "all" | "unread" | "read" | "acknowledged"; type MessageDraft = { postbox_id: string; subject: string; body_text: string; classification: string; recipients: string; }; const emptyGrouping = (): GroupingDraft => ({ id: "", name: "", is_default: false, postbox_ids: [] }); const emptyMessageDraft = (): MessageDraft => ({ postbox_id: "", subject: "", body_text: "", classification: "internal", recipients: "" }); export default function PostboxPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo; }) { const requestedMessageId = useRef( new URLSearchParams(window.location.search).get("message") ?? "" ); const requestedMessageLoaded = useRef(false); const [postboxes, setPostboxes] = useState([]); const [groupings, setGroupings] = useState([]); const [selectedScope, setSelectedScope] = useState("all"); const [selectedPostboxId, setSelectedPostboxId] = useState(""); const [messages, setMessages] = useState([]); const [selectedMessageId, setSelectedMessageId] = useState(""); const [selectedMessage, setSelectedMessage] = useState(null); const [unavailableSelection, setUnavailableSelection] = useState(""); const [total, setTotal] = useState(0); const [messageState, setMessageState] = useState("all"); const [searchDraft, setSearchDraft] = useState(""); const [messageQuery, setMessageQuery] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [loadingDirectory, setLoadingDirectory] = useState(true); const [loadingMessages, setLoadingMessages] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [groupingDialogOpen, setGroupingDialogOpen] = useState(false); const [groupingDraft, setGroupingDraft] = useState(emptyGrouping); const [groupingBaseline, setGroupingBaseline] = useState(emptyGrouping); const [deleteGroupingTarget, setDeleteGroupingTarget] = useState(null); const [messageDialogOpen, setMessageDialogOpen] = useState(false); const [replyParent, setReplyParent] = useState(null); const [messageDraft, setMessageDraft] = useState(emptyMessageDraft); const [messageBaseline, setMessageBaseline] = useState(emptyMessageDraft); const { language } = usePlatformLanguage(); const { requestDiscard } = useUnsavedChanges(); const canAcknowledge = hasScope(auth, "postbox:message:acknowledge"); const canSend = hasScope(auth, "postbox:message:write"); const canReply = hasScope(auth, "postbox:message:reply"); const groupingDirty = groupingDialogOpen && draftKey(groupingDraft) !== draftKey(groupingBaseline); const messageDirty = messageDialogOpen && draftKey(messageDraft) !== draftKey(messageBaseline); const selectedPostbox = useMemo( () => postboxes.find((postbox) => postbox.id === selectedPostboxId) ?? null, [postboxes, selectedPostboxId] ); const selectedGrouping = useMemo( () => groupings.find((grouping) => grouping.id === selectedScope) ?? null, [groupings, selectedScope] ); const scopePostboxIds = useMemo(() => { if (selectedPostboxId) return [selectedPostboxId]; if (selectedGrouping) { const visible = new Set(postboxes.map((postbox) => postbox.id)); return selectedGrouping.postbox_ids.filter((postboxId) => visible.has(postboxId)); } return postboxes.map((postbox) => postbox.id); }, [postboxes, selectedGrouping, selectedPostboxId]); const scopeKey = scopePostboxIds.join("|"); const composeDisabledReason = postboxBusyReason(false, busy) ?? (!canSend ? POSTBOX_INTERFACE_I18N.noSendReason : undefined) ?? (!postboxes.length ? POSTBOX_INTERFACE_I18N.noPostbox : undefined); const replyDisabledReason = postboxBusyReason(false, busy) ?? (!canReply ? POSTBOX_INTERFACE_I18N.noReplyReason : undefined) ?? (!selectedMessage ? POSTBOX_INTERFACE_I18N.noMessage : undefined) ?? (selectedMessage?.availability !== "available" ? POSTBOX_INTERFACE_I18N.unavailableMessage : undefined); const acknowledgeDisabledReason = postboxBusyReason(false, busy) ?? (!canAcknowledge ? POSTBOX_INTERFACE_I18N.noAcknowledgeReason : undefined) ?? (!selectedMessage ? POSTBOX_INTERFACE_I18N.noMessage : undefined) ?? (selectedMessage?.availability !== "available" ? POSTBOX_INTERFACE_I18N.unavailableMessage : undefined) ?? (selectedMessage?.acknowledged_at ? POSTBOX_INTERFACE_I18N.alreadyAcknowledged : undefined); useUnsavedDraftGuard({ dirty: groupingDirty || messageDirty, title: "Unsaved Postbox draft", message: "Save or discard the open Postbox draft before leaving this surface.", onSave: () => groupingDirty ? saveGrouping() : submitMessage(), onDiscard: discardOpenDraft }); const loadDirectory = useCallback(async () => { setLoadingDirectory(true); setError(""); try { const [nextPostboxes, nextGroupings] = await Promise.all([ listPostboxes(settings), listPostboxGroupings(settings) ]); setPostboxes(nextPostboxes); setGroupings(nextGroupings); setSelectedScope((current) => { if (current === "all" || nextGroupings.some((grouping) => grouping.id === current)) { return current; } return nextGroupings.find((grouping) => grouping.is_default)?.id ?? "all"; }); setSelectedPostboxId((current) => current && nextPostboxes.some((postbox) => postbox.id === current) ? current : "" ); } catch (loadError) { setError(errorMessage(loadError)); } finally { setLoadingDirectory(false); } }, [settings]); const loadMessages = useCallback(async () => { if (!scopePostboxIds.length) { setMessages([]); setSelectedMessageId(""); setSelectedMessage(null); setUnavailableSelection(""); setTotal(0); return; } setLoadingMessages(true); setError(""); try { const response = await listPostboxMessages( settings, scopePostboxIds, pageSize, (page - 1) * pageSize, messageQuery, messageState ); setMessages(response.messages); setTotal(response.total); setSelectedMessageId((current) => current && ( response.messages.some((message) => message.id === current) || current === requestedMessageId.current ) ? current : "" ); if (!response.messages.length) setSelectedMessage(null); } catch (loadError) { setError(errorMessage(loadError)); } finally { setLoadingMessages(false); } }, [messageQuery, messageState, page, pageSize, scopeKey, settings]); useEffect(() => { void loadDirectory(); }, [loadDirectory]); useEffect(() => { void loadMessages(); }, [loadMessages]); useEffect(() => { const messageId = requestedMessageId.current; if ( requestedMessageLoaded.current || !messageId || loadingDirectory || !postboxes.length ) { return; } requestedMessageLoaded.current = true; let cancelled = false; void (async () => { try { const message = await getPostboxMessage(settings, messageId); if (cancelled) return; if (!postboxes.some((postbox) => postbox.id === message.postbox_id)) { setSelectedMessageId(messageId); setSelectedMessage(null); setUnavailableSelection( "This message is outside your current Postbox assignments." ); return; } setSelectedScope("all"); setSelectedPostboxId(message.postbox_id); setSelectedMessageId(message.id); setSelectedMessage(message); setUnavailableSelection(""); } catch (loadError) { if (!cancelled && isApiError(loadError, 403, 404)) { setSelectedMessageId(messageId); setSelectedMessage(null); setUnavailableSelection( "This message is no longer available or is outside your current Postbox assignments." ); } else if (!cancelled) { setError(errorMessage(loadError)); } } })(); return () => { cancelled = true; }; }, [loadingDirectory, postboxes, settings]); useEffect(() => { if (!selectedMessageId) return; let cancelled = false; setUnavailableSelection(""); void (async () => { try { let message = await getPostboxMessage(settings, selectedMessageId); if (message.availability === "available" && !message.read_at) { message = await markPostboxMessage(settings, selectedMessageId, "read"); } if (cancelled) return; setSelectedMessage(message); setMessages((items) => items.map((item) => (item.id === message.id ? message : item)) ); } catch (loadError) { if (!cancelled && isApiError(loadError, 403, 404)) { setSelectedMessage(null); setUnavailableSelection( "This message is no longer available or is outside your current Postbox assignments." ); } else if (!cancelled) { setError(errorMessage(loadError)); } } })(); return () => { cancelled = true; }; }, [selectedMessageId, settings]); async function acknowledgeSelected() { if ( !selectedMessage || selectedMessage.availability !== "available" || !canAcknowledge ) { return; } setBusy(true); setError(""); try { const next = await markPostboxMessage( settings, selectedMessage.id, "acknowledged" ); setSelectedMessage(next); setMessages((items) => items.map((item) => (item.id === next.id ? next : item)) ); } catch (actionError) { setError(errorMessage(actionError)); } finally { setBusy(false); } } function selectPostbox(postboxId: string) { setSelectedPostboxId(postboxId); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); setUnavailableSelection(""); } function selectScope(scopeId: string) { setSelectedScope(scopeId); setSelectedPostboxId(""); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); setUnavailableSelection(""); } function openNewGrouping() { const next = emptyGrouping(); setGroupingDraft(next); setGroupingBaseline(next); setGroupingDialogOpen(true); } function openGrouping(grouping: PostboxGrouping) { const next = { id: grouping.id, name: grouping.name, is_default: grouping.is_default, postbox_ids: [...grouping.postbox_ids] }; setGroupingDraft(next); setGroupingBaseline(next); setGroupingDialogOpen(true); } async function saveGrouping(): Promise { if (!groupingDraft.name.trim()) return false; setBusy(true); setError(""); const payload = { name: groupingDraft.name.trim(), is_default: groupingDraft.is_default, postbox_ids: groupingDraft.postbox_ids }; try { const existing = groupings.find((item) => item.id === groupingDraft.id); const saved = existing ? await updatePostboxGrouping(settings, existing, payload) : await createPostboxGrouping(settings, payload); await loadDirectory(); setSelectedScope(saved.id); setSelectedPostboxId(""); setGroupingDialogOpen(false); setGroupingBaseline(groupingDraft); return true; } catch (actionError) { setError(errorMessage(actionError)); return false; } finally { setBusy(false); } } async function removeGrouping() { if (!deleteGroupingTarget?.id) return; setBusy(true); setError(""); try { const existing = groupings.find((item) => item.id === deleteGroupingTarget.id); if (!existing) throw new Error("The grouping is no longer available."); await deletePostboxGrouping(settings, existing); setSelectedScope("all"); setSelectedPostboxId(""); setGroupingDialogOpen(false); setDeleteGroupingTarget(null); await loadDirectory(); } catch (actionError) { setError(errorMessage(actionError)); } finally { setBusy(false); } } function openCompose() { const postbox = selectedPostbox ?? postboxes[0] ?? null; if (!postbox) return; setReplyParent(null); const next = { ...emptyMessageDraft(), postbox_id: postbox.id, classification: postbox.classification }; setMessageDraft(next); setMessageBaseline(next); setMessageDialogOpen(true); } function openReply() { if (!selectedMessage) return; setReplyParent(selectedMessage); const next = { ...emptyMessageDraft(), postbox_id: selectedMessage.postbox_id, subject: selectedMessage.subject.toLowerCase().startsWith("re:") ? selectedMessage.subject : `Re: ${selectedMessage.subject}`, classification: selectedMessage.classification }; setMessageDraft(next); setMessageBaseline(next); setMessageDialogOpen(true); } async function submitMessage(): Promise { if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return false; setBusy(true); setError(""); const participants = messageDraft.recipients .split(/[;,\n]/) .map((address) => address.trim()) .filter(Boolean) .map((address) => ({ kind: "to", reference_type: "address", address })); const payload = { idempotency_key: crypto.randomUUID(), subject: messageDraft.subject.trim(), body_text: messageDraft.body_text || null, classification: messageDraft.classification, participants, attachments: [], metadata: {} }; try { const message = replyParent ? await replyToPostboxMessage(settings, replyParent.id, payload) : await createPostboxMessage( settings, messageDraft.postbox_id, payload ); setMessageDialogOpen(false); setReplyParent(null); setSelectedPostboxId(message.postbox_id); setSelectedScope("all"); setSelectedMessageId(message.id); setSelectedMessage(message); setPage(1); const refreshed = await listPostboxMessages( settings, [message.postbox_id], pageSize, 0, messageQuery, messageState ); setMessages(refreshed.messages); setTotal(refreshed.total); setMessageBaseline(messageDraft); return true; } catch (actionError) { setError(errorMessage(actionError)); return false; } finally { setBusy(false); } } function discardOpenDraft() { if (groupingDialogOpen) { setGroupingDraft(groupingBaseline); setGroupingDialogOpen(false); } if (messageDialogOpen) { setMessageDraft(messageBaseline); setMessageDialogOpen(false); setReplyParent(null); } } function closeGroupingDialog() { const close = () => { setGroupingDraft(groupingBaseline); setGroupingDialogOpen(false); }; if (groupingDirty) requestDiscard(close); else close(); } function closeMessageDialog() { const close = () => { setMessageDraft(messageBaseline); setMessageDialogOpen(false); setReplyParent(null); }; if (messageDirty) requestDiscard(close); else close(); } return (
{selectedPostbox ? ( <>
} onClick={openCompose} disabled={Boolean(composeDisabledReason)} disabledReason={composeDisabledReason} /> } onClick={() => requestDiscard(() => void loadMessages())} disabled={loadingMessages || busy} disabledReason={postboxBusyReason(loadingMessages, busy)} />
setSearchDraft(event.target.value)} onKeyDown={(event) => { if (event.key !== "Enter") return; event.preventDefault(); setMessageQuery(searchDraft.trim()); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); }} /> {searchDraft || messageQuery ? ( } onClick={() => { setSearchDraft(""); setMessageQuery(""); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); }} /> ) : null} } onClick={() => { setMessageQuery(searchDraft.trim()); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); }} />
ariaLabel="Message state" width="fill" options={[ { id: "all", label: "All" }, { id: "unread", label: "Unread" }, { id: "read", label: "Read" }, { id: "acknowledged", label: "Acknowledged" } ]} value={messageState} onChange={(next) => { setMessageState(next); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); }} />
{error ? ( {error} ) : null}
{loadingMessages ?

Loading messages

: null} {!loadingMessages && !messages.length ? (
No messages

This view has no delivered Postbox messages.

) : null} {messages.length ? ( {messages.map((message) => ( setSelectedMessageId(message.id)} > {message.subject} {message.sender_label || message.producer_module || "Platform"} {sourceName(postboxes, message.postbox_id)} {message.attachments.length ? ( {message.attachments.length} ) : null} {message.acknowledged_at ? ( Acknowledged ) : null} {message.availability !== "available" ? ( ) : null} ))} ) : null}
{ setPage(next); setSelectedMessageId(""); setSelectedMessage(null); }} onPageSizeChange={(next) => { setPageSize(next); setPage(1); setSelectedMessageId(""); setSelectedMessage(null); }} />
{selectedMessage ? ( item.id === selectedMessage.postbox_id)} /> ) : unavailableSelection ? (
Message unavailable

{unavailableSelection}

) : (
Select a message

Source, function context, content, and evidence remain attached to the originating postbox.

)}
{groupingDraft.id ? ( ) : }
} >
setGroupingDraft((current) => ({ ...current, name: event.target.value })) } />
setGroupingDraft((current) => ({ ...current, is_default: checked })) } />
Source postboxes {postboxes.map((postbox) => ( ))}
} >
setMessageDraft((current) => ({ ...current, recipients: event.target.value })) } />
setMessageDraft((current) => ({ ...current, subject: event.target.value })) } />