import { useEffect, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Home, Mail, Paperclip, RefreshCw, Search, X } from "lucide-react"; import { Button, DismissibleAlert, ExplorerTree, LoadingIndicator, MessageDisplayPanel, formatDateTime, i18nMessage, type ApiSettings } from "@govoplan/core-webui"; import { getMailboxMessage, listMailboxFolders, listMailboxMessages, listMailServerProfiles, type MailImapFolderResponse, type MailMailboxMessageDetail, type MailMailboxMessageSummary, type MailServerProfile } from "../../api/mail"; import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders"; export default function MailboxPage({ settings }: {settings: ApiSettings;}) { const [profiles, setProfiles] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState(""); const [folders, setFolders] = useState([]); const [selectedFolder, setSelectedFolder] = useState("INBOX"); const [foldersLoadedForProfile, setFoldersLoadedForProfile] = useState(""); const [expandedFolders, setExpandedFolders] = useState>(() => new Set()); const [messages, setMessages] = useState([]); const [messageTotalCount, setMessageTotalCount] = useState(null); const [messagePage, setMessagePage] = useState(1); const [messagePageSize, setMessagePageSize] = useState(10); const [messageQuery, setMessageQuery] = useState(""); const [selectedMessage, setSelectedMessage] = useState(null); const [selectedMessageKeyState, setSelectedMessageKeyState] = useState(""); const [pendingMessageKey, setPendingMessageKey] = useState(""); const [loadingProfiles, setLoadingProfiles] = useState(true); const [loadingFolders, setLoadingFolders] = useState(false); const [loadingMessages, setLoadingMessages] = useState(false); const [loadingMessage, setLoadingMessage] = useState(false); const [error, setError] = useState(""); const [folderError, setFolderError] = useState(""); const [messageError, setMessageError] = useState(""); const [detailError, setDetailError] = useState(""); const selectedMessageKeyRef = useRef(""); const folderRequestRef = useRef(0); const messageListRequestRef = useRef(0); const messageDetailRequestRef = useRef(0); const mailboxPageCursorsRef = useRef>({}); const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null; const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]); const folderTree = useMemo(() => buildMailboxFolderTree(folders), [folders]); const selectedFolderNodeId = useMemo(() => findFolderNodeId(folderTree, selectedFolder) ?? "", [folderTree, selectedFolder]); const filteredMessages = useMemo(() => filterMessages(messages, messageQuery), [messageQuery, messages]); const messagePageCount = Math.max(1, Math.ceil((messageTotalCount ?? 0) / messagePageSize)); const shellBusy = loadingProfiles || loadingFolders || loadingMessages; const noImapProfiles = !loadingProfiles && imapProfiles.length === 0; const foldersReady = Boolean(selectedProfileId) && foldersLoadedForProfile === selectedProfileId; const selectedMessageKey = pendingMessageKey || selectedMessageKeyState || (selectedMessage ? mailboxMessageKey(selectedMessage.folder || selectedFolder, selectedMessage.uid) : ""); const messageCountLabel = messageListCountLabel(messages.length, messageTotalCount, loadingMessages, foldersReady); const folderEmptyText = folderError || (noImapProfiles ? "i18n:govoplan-mail.no_imap_enabled_mail_profiles.61ae44d8" : loadingFolders ? "i18n:govoplan-mail.loading_folders.17f9f0e2" : "i18n:govoplan-mail.no_folders_available.14133b26"); const messageEmptyText = messageError || (!selectedProfileId ? "i18n:govoplan-mail.select_an_imap_profile.5445648c" : !foldersReady || loadingMessages ? "i18n:govoplan-mail.loading_messages.77b62232" : messages.length > 0 && filteredMessages.length === 0 ? "i18n:govoplan-mail.no_messages_match_the_current_filter_on_this_pag.9dda6916" : "i18n:govoplan-mail.no_messages_in_this_folder.5c7fa25d"); const previewEmptyText = detailError || (loadingMessage ? "i18n:govoplan-mail.loading_message.815c2094" : "i18n:govoplan-mail.select_a_message_to_inspect_its_content.5f3d1342"); const loadingLabel = loadingProfiles ? "i18n:govoplan-mail.loading_mail_profiles.87de3560" : loadingFolders ? "i18n:govoplan-mail.loading_folders.17f9f0e2" : loadingMessages ? "i18n:govoplan-mail.loading_messages.77b62232" : "i18n:govoplan-mail.loading_message.815c2094"; useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]); useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]); useEffect(() => {if (selectedProfileId) void loadFolders(selectedProfileId);}, [selectedProfileId]); useEffect(() => {if (selectedProfileId && selectedFolder && foldersReady) void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]); useEffect(() => { const handlePreviewShortcut = (event: KeyboardEvent) => { if (isEditableTarget(event.target)) return; if (event.key === "Escape") { if (selectedMessage || selectedMessageKey || pendingMessageKey || detailError) { event.preventDefault(); setSelectedMessage(null); setSelectedMessageKeyState(""); selectedMessageKeyRef.current = ""; setPendingMessageKey(""); setDetailError(""); } return; } if (shellBusy || loadingMessage || filteredMessages.length === 0) return; const selectedIndex = filteredMessages.findIndex((message) => mailboxMessageKey(message.folder || selectedFolder, message.uid) === selectedMessageKey); let nextIndex: number | null = null; if (event.key === "ArrowLeft") nextIndex = selectedIndex > 0 ? selectedIndex - 1 : 0;else if (event.key === "ArrowRight") nextIndex = selectedIndex >= 0 ? Math.min(filteredMessages.length - 1, selectedIndex + 1) : 0;else if (event.key === "Home") nextIndex = 0;else if (event.key === "End") nextIndex = filteredMessages.length - 1; if (nextIndex === null) return; event.preventDefault(); const nextMessage = filteredMessages[nextIndex]; if (nextMessage) void openMessage(nextMessage); }; window.addEventListener("keydown", handlePreviewShortcut); return () => window.removeEventListener("keydown", handlePreviewShortcut); }, [detailError, filteredMessages, loadingMessage, pendingMessageKey, selectedFolder, selectedMessage, selectedMessageKey, shellBusy]); useEffect(() => { const ancestorIds = folderAncestorIds(folderTree, selectedFolder); if (ancestorIds.length === 0) return; setExpandedFolders((current) => { const next = new Set(current); ancestorIds.forEach((id) => next.add(id)); return next; }); }, [folderTree, selectedFolder]); async function loadProfiles() { setLoadingProfiles(true); setError(""); try { const loaded = await listMailServerProfiles(settings); const usable = loaded.filter((profile) => profile.is_active && profile.imap); setProfiles(loaded); setSelectedProfileId((current) => current && usable.some((profile) => profile.id === current) ? current : usable[0]?.id ?? ""); if (usable.length === 0) { setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setSelectedMessageKeyState(""); setPendingMessageKey(""); } } catch (err) { setError(errorText(err)); } finally { setLoadingProfiles(false); } } async function loadFolders(profileId = selectedProfileId) { if (!profileId) return; const requestId = ++folderRequestRef.current; messageListRequestRef.current += 1; messageDetailRequestRef.current += 1; setLoadingFolders(true); setFoldersLoadedForProfile(""); setMessageTotalCount(null); setMessagePage(1); setSelectedMessage(null); setSelectedMessageKeyState(""); setPendingMessageKey(""); setFolderError(""); setMessageError(""); setDetailError(""); setError(""); try { const response = await listMailboxFolders(settings, profileId); if (requestId !== folderRequestRef.current) return; if (!response.ok) throw new Error(response.message || "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e"); const loaded = response.folders?.length ? response.folders : [{ name: "INBOX", flags: [] }]; setFolders(loaded); setExpandedFolders(new Set()); let nextFolder = selectedFolder; if (!nextFolder || !loaded.some((folder) => folder.name === nextFolder)) { nextFolder = loaded.some((folder) => folder.name === "INBOX") ? "INBOX" : response.detected_sent_folder || loaded[0]?.name || "INBOX"; } setSelectedFolder(nextFolder); setFoldersLoadedForProfile(profileId); } catch (err) { if (requestId !== folderRequestRef.current) return; setFolderError(errorText(err)); setError(errorText(err)); setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setSelectedMessageKeyState(""); setPendingMessageKey(""); } finally { if (requestId === folderRequestRef.current) setLoadingFolders(false); } } async function loadMessages(profileId = selectedProfileId, folder = selectedFolder, page = messagePage, pageSize = messagePageSize) { if (!profileId || !folder) return; const requestId = ++messageListRequestRef.current; const offset = (Math.max(1, page) - 1) * pageSize; const cursorKey = mailboxCursorKey(profileId, folder, pageSize); const cursor = page <= 1 ? null : mailboxPageCursorsRef.current[`${cursorKey}:${page}`] || null; setLoadingMessages(true); setMessageError(""); setDetailError(""); setError(""); try { const response = await listMailboxMessages(settings, profileId, folder, pageSize, offset, cursor); if (requestId !== messageListRequestRef.current) return; const loaded = response.messages ?? []; const total = response.total_count ?? loaded.length; if (page <= 1) mailboxPageCursorsRef.current[`${cursorKey}:1`] = null; if (response.next_cursor) mailboxPageCursorsRef.current[`${cursorKey}:${page + 1}`] = response.next_cursor; setMessages(loaded); setMessageTotalCount(total); setFolderMessageCount(folder, total); const rememberedKey = selectedMessageKeyRef.current; if (rememberedKey && !loaded.some((message) => mailboxMessageKey(message.folder || folder, message.uid) === rememberedKey)) { setSelectedMessage(null); setSelectedMessageKeyState(""); setPendingMessageKey(""); } } catch (err) { if (requestId !== messageListRequestRef.current) return; const message = errorText(err); setMessageError(message); setError(message); setMessages([]); setMessageTotalCount(null); setSelectedMessage(null); setSelectedMessageKeyState(""); setPendingMessageKey(""); } finally { if (requestId === messageListRequestRef.current) setLoadingMessages(false); } } async function openMessage(message: MailMailboxMessageSummary) { if (!selectedProfileId) return; const folderName = message.folder || selectedFolder; const requestId = ++messageDetailRequestRef.current; const nextKey = mailboxMessageKey(folderName, message.uid); setSelectedMessageKeyState(nextKey); selectedMessageKeyRef.current = nextKey; setPendingMessageKey(nextKey); setSelectedMessage(null); setDetailError(""); setLoadingMessage(true); setError(""); try { const response = await getMailboxMessage(settings, selectedProfileId, folderName, message.uid); if (requestId !== messageDetailRequestRef.current) return; setSelectedMessage(response.message); setSelectedMessageKeyState(mailboxMessageKey(response.message.folder || folderName, response.message.uid)); setPendingMessageKey(""); } catch (err) { if (requestId !== messageDetailRequestRef.current) return; const messageText = errorText(err); setDetailError(messageText); setError(messageText); setPendingMessageKey(""); } finally { if (requestId === messageDetailRequestRef.current) setLoadingMessage(false); } } function selectProfile(profileId: string) { folderRequestRef.current += 1; messageListRequestRef.current += 1; messageDetailRequestRef.current += 1; setSelectedProfileId(profileId); setFolders([]); setFoldersLoadedForProfile(""); setMessages([]); setMessageTotalCount(null); setMessagePage(1); setSelectedMessage(null); setSelectedMessageKeyState(""); selectedMessageKeyRef.current = ""; setPendingMessageKey(""); setExpandedFolders(new Set()); setLoadingFolders(false); setLoadingMessages(false); setLoadingMessage(false); } function openFolderNode(node: MailFolderNode) { if (node.folderName) { if (node.folderName === selectedFolder && foldersReady) void loadMessages(selectedProfileId, node.folderName, messagePage, messagePageSize);else { setSelectedFolder(node.folderName); setMessagePage(1); setSelectedMessage(null); setSelectedMessageKeyState(""); selectedMessageKeyRef.current = ""; setPendingMessageKey(""); setDetailError(""); } return; } toggleFolderNode(node); } function toggleFolderNode(node: MailFolderNode) { setExpandedFolders((current) => { const next = new Set(current); if (next.has(node.id)) next.delete(node.id);else next.add(node.id); return next; }); } function setFolderMessageCount(folderName: string, count: number) { setFolders((current) => current.map((folder) => folder.name === folderName ? { ...folder, message_count: count } : folder)); } function changeMessagePage(page: number) { const nextPage = Math.min(messagePageCount, Math.max(1, page)); if (nextPage !== messagePage) setMessagePage(nextPage); } function changeMessagePageSize(pageSize: number) { setMessagePageSize(pageSize); setMessagePage(1); } return (
{error && {error}}
{selectedProfile?.imap ? transportLabel(selectedProfile) : "i18n:govoplan-mail.no_imap_profile_selected.e7d1516f"}
{messageCountLabel} {selectedFolder} {messageQuery && {filteredMessages.length} match{filteredMessages.length === 1 ? "" : "es"} i18n:govoplan-mail.on_page.ca7166f4} {shellBusy && i18n:govoplan-mail.working.049ac820} {loadingMessage && {i18nMessage("i18n:govoplan-mail.loading_preview.ebd86225")}}
i18n:govoplan-mail.subject.8d183dbd i18n:govoplan-mail.date.eb9a4bc1 i18n:govoplan-mail.size.b7152342
{filteredMessages.length === 0 &&
{messageEmptyText}
} {filteredMessages.map((message) => { const key = mailboxMessageKey(message.folder || selectedFolder, message.uid); const selected = key === selectedMessageKey; const loadingSelected = loadingMessage && key === pendingMessageKey; return (
void openMessage(message)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); void openMessage(message); } }}>
{formatDateTime(message.date, { fallback: "-" })} {message.attachment_count ? : null} {formatBytes(message.size_bytes)}
); })}
i18n:govoplan-mail.preview.f1fbb2b4
({ filename: attachment.filename, contentType: attachment.content_type, sizeBytes: attachment.size_bytes }))} emptyText={previewEmptyText} />
{shellBusy &&
{i18nMessage(loadingLabel)}
}
); } function MailboxPagination({ page, pageSize, totalRows, disabled, onPageChange, onPageSizeChange }: {page: number;pageSize: number;totalRows: number;disabled: boolean;onPageChange: (page: number) => void;onPageSizeChange: (pageSize: number) => void;}) { const pageCount = Math.max(1, Math.ceil(totalRows / pageSize)); const first = totalRows === 0 ? 0 : (page - 1) * pageSize + 1; const last = Math.min(totalRows, page * pageSize); const options = [10, 25, 50, 100]; return (
{first}-{last} of {totalRows}
i18n:govoplan-mail.page.fb06270f {page} of {pageCount}
); } function mailboxMessageKey(folder: string, uid: string): string { return `${folder || "INBOX"}::${uid}`; } function mailboxCursorKey(profileId: string, folder: string, pageSize: number): string { return `${profileId}::${folder || "INBOX"}::${pageSize}`; } function filterMessages(messages: MailMailboxMessageSummary[], query: string): MailMailboxMessageSummary[] { const normalized = query.trim().toLocaleLowerCase(); if (!normalized) return messages; return messages.filter((message) => [ message.subject, message.from_header, message.to_header, message.cc_header, message.date, message.body_preview]. some((value) => String(value ?? "").toLocaleLowerCase().includes(normalized))); } function messageListCountLabel(visibleCount: number, totalCount: number | null, loading: boolean, ready: boolean): string { if (!ready) return "i18n:govoplan-mail.no_folder_loaded.889bdb1b"; if (loading) return "i18n:govoplan-mail.loading_messages.4294022c"; if (totalCount !== null && totalCount > visibleCount) return i18nMessage("i18n:govoplan-mail.value_of_value_messages.981a1618", { value0: visibleCount, value1: totalCount }); const count = totalCount ?? visibleCount; return i18nMessage(count === 1 ? "i18n:govoplan-mail.value_message" : "i18n:govoplan-mail.value_messages", { value0: count }); } function folderFlagLabel(flags: string[]): string { const labels = flags.map(displayFolderFlag).filter((label): label is string => Boolean(label)); return Array.from(new Set(labels)).join(" "); } function displayFolderFlag(flag: string): string | null { const normalized = flag.replace(/^\\/, "").toLowerCase(); switch (normalized) { case "sent": return "i18n:govoplan-mail.sent.35f49dcf"; case "drafts": return "i18n:govoplan-mail.drafts.22a31d86"; case "trash": return "i18n:govoplan-mail.trash.e3bf62bb"; case "archive": return "i18n:govoplan-mail.archive.2621c6fd"; case "junk": return "i18n:govoplan-mail.junk.86c7d94c"; case "flagged": return "i18n:govoplan-mail.flagged.f8db8a17"; case "haschildren": case "hasnochildren": case "noselect": return null; default: return null; } } function transportLabel(profile: MailServerProfile): string { const imap = profile.imap; if (!imap?.host) return "i18n:govoplan-mail.imap_not_configured.b2892af3"; return `${imap.host}:${imap.port ?? "?"} ${imap.security ?? ""}`.trim(); } function formatBytes(value?: number | null): string { if (!value) return "-"; if (value < 1024) return i18nMessage("i18n:govoplan-mail.bytes_b", { value0: value }); if (value < 1024 * 1024) return i18nMessage("i18n:govoplan-mail.bytes_kb", { value0: (value / 1024).toFixed(1) }); return i18nMessage("i18n:govoplan-mail.bytes_mb", { value0: (value / 1024 / 1024).toFixed(1) }); } function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); } function isEditableTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName); }