import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Archive, Building2, CheckCheck, Download, Inbox, Layers3, MailOpen, Paperclip, Pencil, Plus, Reply, Search, Send, Trash2, UserRoundCheck, X } from "lucide-react"; import { FormGrid, ActionBlockerHint, Button, ConfirmDialog, CountBadge, DataGridPaginationBar, Dialog, DismissibleAlert, DocumentationHelpLink, FormField, FilterBar, IconButton, SegmentedControl, SelectionList, SelectionListItem, StatePanel, StatusBadge, ToggleSwitch, hasScope, i18nMessage, isApiError, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, WorkspaceActionBar, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { useLocation, useNavigate } from "react-router"; import { createPostboxGrouping, createPostboxMessage, deletePostboxGrouping, downloadPostboxAttachment, getPostboxMessage, listPostboxGroupings, listPostboxMessages, listPostboxes, markPostboxMessage, resolvePostboxAttachments, replyToPostboxMessage, updatePostboxGrouping, type PostboxDirectoryItem, type PostboxGrouping, type PostboxAttachmentResolution, 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 location = useLocation(); const navigate = useNavigate(); const requestedMessageId = useRef( new URLSearchParams(location.search).get("message") ?? "" ); const requestedPostboxId = useRef( new URLSearchParams(location.search).get("postbox") ?? "" ); const requestedMessageLoaded = useRef(false); const requestedComposeLoaded = 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 [attachmentResolutions, setAttachmentResolutions] = useState([]); 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 composeTarget = useMemo( () => postboxes.find((postbox) => postbox.id === messageDraft.postbox_id) ?? null, [messageDraft.postbox_id, postboxes] ); 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?.encryption_profile === "external_e2ee_v1" ? "Replies to E2EE messages must be created by an approved encryption client." : 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 : nextPostboxes.some((postbox) => postbox.id === requestedPostboxId.current) ? requestedPostboxId.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(() => { const parameters = new URLSearchParams(location.search); if ( requestedComposeLoaded.current || parameters.get("quickAction") !== "compose" || loadingDirectory ) { return; } requestedComposeLoaded.current = true; const requestedId = parameters.get("postbox"); const postbox = postboxes.find((item) => item.id === requestedId) ?? postboxes[0] ?? null; if (canSend && postbox) { setSelectedPostboxId(postbox.id); openComposeFor(postbox); } else if (!canSend) { setError(POSTBOX_INTERFACE_I18N.noSendReason); } else { setError(POSTBOX_INTERFACE_I18N.noPostbox); } parameters.delete("quickAction"); const search = parameters.toString(); navigate( { pathname: location.pathname, search: search ? `?${search}` : "" }, { replace: true, state: location.state } ); }, [ canSend, loadingDirectory, location.pathname, location.search, location.state, navigate, postboxes ]); 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"); } const resolutions = message.attachments.length ? await resolvePostboxAttachments(settings, message.id) : []; if (cancelled) return; setSelectedMessage(message); setAttachmentResolutions(resolutions); setMessages((items) => items.map((item) => (item.id === message.id ? message : item)) ); } catch (loadError) { if (!cancelled && isApiError(loadError, 403, 404)) { setSelectedMessage(null); setAttachmentResolutions([]); 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.find((item) => item.encryption_profile !== "external_e2ee_v1") ?? postboxes[0] ?? null; if (!postbox) return; openComposeFor(postbox); } function openComposeFor(postbox: PostboxDirectoryItem) { 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() || composeTarget?.encryption_profile === "external_e2ee_v1" ) 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 (
void loadMessages(), loading: loadingMessages || busy, label: "Refresh messages" }} className="postbox-bar" contextActions={
{selectedPostbox ? ( <>
} createAction={} onClick={openCompose} disabled={Boolean(composeDisabledReason)} disabledReason={composeDisabledReason} />} />
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 ? ( } title="No messages" description="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); }} />
} primaryActions={
} /> {selectedMessage ? ( item.id === selectedMessage.postbox_id)} attachmentResolutions={attachmentResolutions} onDownload={(attachment) => { void downloadPostboxAttachment(settings, attachment).catch((downloadError) => { setError(errorMessage(downloadError)); }); }} /> ) : unavailableSelection ? ( } title="Message unavailable" description={unavailableSelection} className="postbox-unavailable-message" /> ) : ( } title="Select a message" description="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) => ( ))}
} > {composeTarget?.encryption_profile === "external_e2ee_v1" ? (
This Postbox requires externally produced E2EE. Use an approved client that supplies ciphertext, a signed manifest, wrapped keys, and a verified content digest; this browser editor never asks for or stores the private key.
) : null}
setMessageDraft((current) => ({ ...current, recipients: event.target.value })) } />
setMessageDraft((current) => ({ ...current, subject: event.target.value })) } />