"use client"; import { useEffect, useState, useRef, useMemo, useCallback } from "react"; import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; import { Sidebar } from "@/components/layout/sidebar"; import { EmailList } from "@/components/email/email-list"; import { EmailViewer } from "@/components/email/email-viewer"; import { EmailComposer } from "@/components/email/email-composer"; import type { ComposerDraftData } from "@/components/email/email-composer"; import { ThreadConversationView } from "@/components/email/thread-conversation-view"; import { MobileHeader, MobileViewerHeader } from "@/components/layout/mobile-header"; import { ThreadGroup, Email } from "@/lib/jmap/types"; import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; import { useEmailStore } from "@/stores/email-store"; import { useAuthStore } from "@/stores/auth-store"; import { useSettingsStore } from "@/stores/settings-store"; import { useIdentityStore } from "@/stores/identity-store"; import { useUIStore } from "@/stores/ui-store"; import { useDeviceDetection } from "@/hooks/use-media-query"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; import { debug } from "@/lib/debug"; import { playNotificationSound } from "@/lib/notification-sound"; import { cn } from "@/lib/utils"; import { ErrorBoundary, SidebarErrorFallback, EmailListErrorFallback, EmailViewerErrorFallback, ComposerErrorFallback, } from "@/components/error"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { DragDropProvider } from "@/contexts/drag-drop-context"; import { isFilterEmpty, activeFilterCount } from "@/lib/jmap/search-utils"; import { WelcomeBanner } from "@/components/ui/welcome-banner"; import { NavigationRail } from "@/components/layout/navigation-rail"; import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; import { useSidebarApps } from "@/hooks/use-sidebar-apps"; import { Input } from "@/components/ui/input"; import { FilePreviewModal } from "@/components/files/file-preview-modal"; import { isFilePreviewable } from "@/lib/file-preview"; import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square } from "lucide-react"; import { ResizeHandle } from "@/components/layout/resize-handle"; import { Button } from "@/components/ui/button"; import { useConfig } from "@/hooks/use-config"; export default function Home() { const router = useRouter(); const t = useTranslations(); const tCommon = useTranslations('common'); const { appName } = useConfig(); const [showComposer, setShowComposer] = useState(false); const [composerMode, setComposerMode] = useState<'compose' | 'reply' | 'replyAll' | 'forward'>('compose'); const [composerDraftText, setComposerDraftText] = useState(""); const [pendingDraft, setPendingDraft] = useState(null); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); const [showShortcutsModal, setShowShortcutsModal] = useState(false); const [showAdvancedFields, setShowAdvancedFields] = useState(false); // Column resize state (disable transitions during drag) const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(0); // Mobile conversation view state const [conversationThread, setConversationThread] = useState(null); const [conversationEmails, setConversationEmails] = useState([]); const [isLoadingConversation, setIsLoadingConversation] = useState(false); const [previewAttachment, setPreviewAttachment] = useState<{ blobId: string; name: string; type?: string } | null>(null); const markAsReadTimeoutRef = useRef(null); const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading, connectionLost } = useAuthStore(); const { identities } = useIdentityStore(); // Mobile/tablet responsive hooks const { isMobile, isTablet } = useDeviceDetection(); const { activeView, sidebarOpen, setSidebarOpen, setActiveView, tabletListVisible, setTabletListVisible, sidebarWidth, emailListWidth, setSidebarWidth, setEmailListWidth, persistColumnWidths, sidebarCollapsed, resetSidebarWidth, resetEmailListWidth } = useUIStore(); const { emails, mailboxes, selectedEmail, selectedMailbox, quota, isPushConnected, newEmailNotification, selectEmail, selectMailbox, selectedEmailIds, selectAllEmails, clearSelection, toggleEmailSelection, fetchMailboxes, fetchEmails, fetchQuota, sendEmail, deleteEmail, markAsRead, toggleStar, moveToMailbox, searchEmails, searchQuery, setSearchQuery, isLoading, isLoadingEmail, setLoadingEmail, setPushConnected, handleStateChange, clearNewEmailNotification, markAsSpam, undoSpam, searchFilters, isAdvancedSearchOpen, setSearchFilters, clearSearchFilters, toggleAdvancedSearch, advancedSearch, selectedKeyword, selectKeyword, hasMoreEmails, fetchTagCounts, fetchEmailContent, } = useEmailStore(); // Keyboard shortcuts handlers const keyboardHandlers = useMemo(() => ({ onNextEmail: () => { if (emails.length === 0) return; const currentIndex = selectedEmail ? emails.findIndex(e => e.id === selectedEmail.id) : -1; const nextIndex = currentIndex < emails.length - 1 ? currentIndex + 1 : currentIndex; if (nextIndex >= 0 && nextIndex < emails.length) { handleEmailSelect(emails[nextIndex]); } }, onPreviousEmail: () => { if (emails.length === 0) return; const currentIndex = selectedEmail ? emails.findIndex(e => e.id === selectedEmail.id) : emails.length; const prevIndex = currentIndex > 0 ? currentIndex - 1 : 0; if (prevIndex >= 0 && prevIndex < emails.length) { handleEmailSelect(emails[prevIndex]); } }, onOpenEmail: () => { // Email is already opened when selected }, onCloseEmail: () => { selectEmail(null); if (isMobile) { setActiveView("list"); } if (isTablet) { setTabletListVisible(true); } }, onReply: () => { if (selectedEmail) handleReply(); }, onReplyAll: () => { if (selectedEmail) handleReplyAll(); }, onForward: () => { if (selectedEmail) handleForward(); }, onToggleStar: () => { if (selectedEmail) handleToggleStar(); }, onArchive: () => { if (selectedEmail) handleArchive(); }, onDelete: () => { if (selectedEmail) handleDelete(); }, onMarkAsUnread: async () => { if (selectedEmail && client) { await markAsRead(client, selectedEmail.id, false); } }, onMarkAsRead: async () => { if (selectedEmail && client) { await markAsRead(client, selectedEmail.id, true); } }, onToggleSpam: () => { if (selectedEmail) { // Check if we're in junk folder const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); const isInJunk = currentMailbox?.role === 'junk'; if (isInJunk) { handleUndoSpam(); } else { handleMarkAsSpam(); } } }, onCompose: () => { setComposerMode('compose'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }, onFocusSearch: () => { const searchInput = document.querySelector('[data-search-input]') as HTMLInputElement; if (searchInput) { searchInput.focus(); searchInput.select(); } }, onShowHelp: () => { setShowShortcutsModal(true); }, onRefresh: async () => { if (client && selectedMailbox) { await fetchEmails(client, selectedMailbox); } }, onSelectAll: () => { selectAllEmails(); }, onDeselectAll: () => { clearSelection(); }, // eslint-disable-next-line react-hooks/exhaustive-deps }), [emails, selectedEmail, client, selectedMailbox, isMobile, isTablet]); // Initialize keyboard shortcuts useKeyboardShortcuts({ enabled: isAuthenticated && !showComposer, emails, selectedEmailId: selectedEmail?.id, handlers: keyboardHandlers, }); // Update page title based on context useEffect(() => { let title = appName; if (showComposer) { // Composing email const modeText = { compose: t('email_composer.new_message'), reply: t('email_composer.reply'), replyAll: t('email_composer.reply_all'), forward: t('email_composer.forward'), }[composerMode] || t('email_composer.new_message'); title = `${modeText} - ${appName}`; } else if (selectedEmail) { // Reading email const subject = selectedEmail.subject || t('email_viewer.no_subject'); title = `${subject} - ${appName}`; } else if (selectedMailbox && mailboxes.length > 0) { // Mailbox view const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); if (mailbox) { const mailboxName = mailbox.name; const unreadCount = mailbox.unreadEmails || 0; title = unreadCount > 0 ? `${mailboxName} (${unreadCount}) - ${appName}` : `${mailboxName} - ${appName}`; } } document.title = title; }, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, appName]); // Check auth on mount useEffect(() => { checkAuth().finally(() => { setInitialCheckDone(true); }); }, [checkAuth]); // Hydrate persisted column widths from localStorage useEffect(() => { try { const stored = localStorage.getItem("column-widths"); if (stored) { const parsed = JSON.parse(stored); if (parsed.sidebarWidth) setSidebarWidth(parsed.sidebarWidth); if (parsed.emailListWidth) setEmailListWidth(parsed.emailListWidth); } } catch { /* ignore parse errors */ } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Redirect to login if not authenticated useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ } router.push('/login'); } }, [initialCheckDone, isAuthenticated, authLoading, router]); // Load mailboxes and emails when authenticated (only if not already loaded) useEffect(() => { if (isAuthenticated && client && mailboxes.length === 0) { const loadData = async () => { try { // First fetch mailboxes and quota (inbox will be auto-selected in fetchMailboxes) await Promise.all([ fetchMailboxes(client), fetchQuota(client) ]); // Get the selected mailbox (should be inbox by default) const state = useEmailStore.getState(); const selectedMailboxId = state.selectedMailbox; // Fetch emails for the selected mailbox if (selectedMailboxId) { await fetchEmails(client, selectedMailboxId); } else { await fetchEmails(client); } // Fetch tag counts fetchTagCounts(client); // Setup push notifications after successful data load try { // Register state change callback client.onStateChange((change) => handleStateChange(change, client)); // Start receiving push notifications const pushEnabled = client.setupPushNotifications(); if (pushEnabled) { setPushConnected(true); debug.log('[Push] Push notifications successfully enabled'); } else { debug.log('[Push] Push notifications not available on this server'); } } catch (error) { // Push notifications are optional - don't break the app if they fail debug.log('[Push] Failed to setup push notifications:', error); } } catch (error) { console.error('Error loading email data:', error); } }; loadData(); } // Cleanup push notifications on unmount return () => { if (client) { client.closePushNotifications(); } }; }, [isAuthenticated, client, mailboxes.length, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, handleStateChange, setPushConnected]); // Auto-fetch full email content when an email is auto-selected (e.g. after delete/archive) useEffect(() => { if (!selectedEmail || !client) return; // If the email lacks bodyValues, it was auto-selected from the list and needs full content if (!selectedEmail.bodyValues) { setLoadingEmail(true); fetchEmailContent(client, selectedEmail.id).finally(() => { setLoadingEmail(false); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedEmail?.id]); // Handle mark-as-read with delay based on settings useEffect(() => { // Clear any existing timeout when email changes if (markAsReadTimeoutRef.current) { debug.log('[Mark as Read] Clearing previous timeout'); clearTimeout(markAsReadTimeoutRef.current); markAsReadTimeoutRef.current = null; } // Only set timeout if there's a selected email, it's unread, and we have a client if (!selectedEmail || !client || selectedEmail.keywords?.$seen) { return; } // Get current setting value const markAsReadDelay = useSettingsStore.getState().markAsReadDelay; debug.log('[Mark as Read] Delay setting:', markAsReadDelay, 'ms for email:', selectedEmail.id); if (markAsReadDelay === -1) { // Never mark as read automatically debug.log('[Mark as Read] Never mode - email will stay unread'); } else if (markAsReadDelay === 0) { // Mark as read instantly debug.log('[Mark as Read] Instant mode - marking as read now'); markAsRead(client, selectedEmail.id, true); } else { // Mark as read after delay debug.log('[Mark as Read] Delayed mode - will mark as read in', markAsReadDelay, 'ms'); markAsReadTimeoutRef.current = setTimeout(() => { debug.log('[Mark as Read] Timeout fired - marking as read now'); markAsRead(client, selectedEmail.id, true); markAsReadTimeoutRef.current = null; }, markAsReadDelay); } // Cleanup on unmount or when dependencies change return () => { if (markAsReadTimeoutRef.current) { debug.log('[Mark as Read] Cleanup - clearing timeout'); clearTimeout(markAsReadTimeoutRef.current); markAsReadTimeoutRef.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedEmail?.id]); // Handle new email notifications - play sound useEffect(() => { if (newEmailNotification) { playNotificationSound(); debug.log('New email received:', newEmailNotification.subject); clearNewEmailNotification(); } }, [newEmailNotification, clearNewEmailNotification]); // Lock body scroll when sidebar is open on mobile/tablet useEffect(() => { if ((isMobile || isTablet) && sidebarOpen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; } return () => { document.body.style.overflow = ''; }; }, [isMobile, isTablet, sidebarOpen]); const handleEmailSend = async (data: { to: string[]; cc: string[]; bcc: string[]; subject: string; body: string; htmlBody?: string; draftId?: string; fromEmail?: string; fromName?: string; identityId?: string; }) => { if (!client) return; try { await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody); setShowComposer(false); // Refresh the current mailbox to update the UI await fetchEmails(client, selectedMailbox); } catch (error) { console.error("Failed to send email:", error); } }; const handleDiscardDraft = async (draftId: string) => { if (!client) return; try { await client.deleteEmail(draftId); } catch (error) { console.error("Failed to discard draft:", error); } }; const handleReply = (draftText?: string) => { setComposerDraftText(draftText || ""); setComposerMode('reply'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const handleReplyAll = () => { setComposerMode('replyAll'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const handleForward = () => { setComposerMode('forward'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const handleDelete = async () => { if (!client || !selectedEmail) return; // Check if we're currently in the trash or junk folder const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); const isInTrash = currentMailbox?.role === 'trash'; const isInJunk = currentMailbox?.role === 'junk'; const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk; if (isInTrash || (isInJunk && permanentlyDeleteJunk)) { // In trash or junk with permanent delete enabled: confirm before permanently deleting const confirmed = await confirmDialog({ title: t('email_list.permanent_delete_confirm_title'), message: t('email_list.permanent_delete_confirm_message'), confirmText: t('email_list.permanent_delete'), variant: "destructive", }); if (!confirmed) return; try { await deleteEmail(client, selectedEmail.id, true); } catch (error) { console.error("Failed to permanently delete email:", error); } } else { // Not in trash: always move to trash const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared); if (trashMailbox) { try { await moveToMailbox(client, selectedEmail.id, trashMailbox.id); } catch (error) { console.error("Failed to move email to trash:", error); } } } }; const handleArchive = async () => { if (!client || !selectedEmail) return; // Find archive mailbox const archiveMailbox = mailboxes.find(m => m.role === "archive" || m.name.toLowerCase() === "archive"); if (!archiveMailbox) return; const { archiveMode } = useSettingsStore.getState(); try { if (archiveMode === 'single') { await moveToMailbox(client, selectedEmail.id, archiveMailbox.id); } else { // Determine year/month from the email's received date const emailDate = new Date(selectedEmail.receivedAt); const year = emailDate.getFullYear().toString(); const month = (emailDate.getMonth() + 1).toString().padStart(2, '0'); const archiveId = archiveMailbox.originalId || archiveMailbox.id; // Find or create year subfolder under archive let yearMailbox = mailboxes.find( m => m.name === year && m.parentId === archiveId ); if (!yearMailbox) { yearMailbox = await client.createMailbox(year, archiveId); await fetchMailboxes(client); } if (archiveMode === 'year') { await moveToMailbox(client, selectedEmail.id, yearMailbox.id); } else { // archiveMode === 'month' — find or create month subfolder under year const yearId = yearMailbox.originalId || yearMailbox.id; let monthMailbox = mailboxes.find( m => m.name === month && m.parentId === yearId ); if (!monthMailbox) { monthMailbox = await client.createMailbox(month, yearId); await fetchMailboxes(client); } await moveToMailbox(client, selectedEmail.id, monthMailbox.id); } } } catch (error) { console.error("Failed to archive email:", error); } }; const handleToggleStar = async () => { if (!client || !selectedEmail) return; try { await toggleStar(client, selectedEmail.id); } catch (error) { console.error("Failed to toggle star:", error); } }; const handleMarkAsSpam = async () => { if (!client || !selectedEmail) return; const emailId = selectedEmail.id; try { await markAsSpam(client, emailId); const toastInstance = (await import('sonner')).toast; toastInstance.success(t('email_viewer.spam.toast_success'), { action: { label: t('email_viewer.spam.toast_undo'), onClick: async () => { try { await undoSpam(client, emailId); toastInstance.success(t('notifications.email_moved')); } catch (_error) { console.error("Failed to undo spam:", _error); toastInstance.error(t('email_viewer.spam.error')); } }, }, duration: 5000, }); } catch (_error) { console.error("Failed to mark as spam:", _error); const toastInstance = (await import('sonner')).toast; toastInstance.error(t('email_viewer.spam.error')); } }; const handleUndoSpam = async () => { if (!client || !selectedEmail) return; try { await undoSpam(client, selectedEmail.id); const toastInstance = (await import('sonner')).toast; toastInstance.success(t('email_viewer.spam.toast_not_spam_success')); } catch (_error) { console.error("Failed to restore email:", _error); const toastInstance = (await import('sonner')).toast; toastInstance.error(t('email_viewer.spam.error_not_spam')); } }; const handleSetColorTag = async (emailId: string, color: string | null) => { if (!client) return; try { // Remove any existing label/color tags const email = emails.find(e => e.id === emailId); if (!email) return; const keywords = { ...email.keywords }; // Remove old label and legacy color tags - set to false for JMAP to remove them Object.keys(keywords).forEach(key => { if (key.startsWith("$label:") || key.startsWith("$color:")) { keywords[key] = false; } }); // Add new label tag if specified (using new $label: prefix) if (color) { keywords[`$label:${color}`] = true; } // Update email keywords via JMAP await client.updateEmailKeywords(emailId, keywords); // Update local state selectEmail(email.id === selectedEmail?.id ? { ...email, keywords } : selectedEmail); // Refresh emails list to show color in list await fetchEmails(client, selectedMailbox); // Refresh tag counts fetchTagCounts(client); } catch (error) { console.error("Failed to set color tag:", error); } }; const handleMailboxSelect = async (mailboxId: string) => { selectMailbox(mailboxId); selectEmail(null); // Clear selected email when switching mailboxes // On mobile, close sidebar and go to list view if (isMobile) { setSidebarOpen(false); setActiveView("list"); } // On tablet, show the list again if (isTablet) { setTabletListVisible(true); } if (client) { // If there's an active search, re-run it in the new mailbox if (searchQuery) { await searchEmails(client, searchQuery); } else { await fetchEmails(client, mailboxId); } } }; const handleTagSelect = async (keywordId: string | null) => { selectKeyword(keywordId); // On mobile, close sidebar and go to list view if (isMobile) { setSidebarOpen(false); setActiveView("list"); } // On tablet, show the list again if (isTablet) { setTabletListVisible(true); } if (client) { await fetchEmails(client); } }; const handleUnreadFilterClick = async (mailboxId: string) => { const isTogglingOff = selectedMailbox === mailboxId && searchFilters.isUnread === true; // Select the mailbox if not already selected if (selectedMailbox !== mailboxId) { selectMailbox(mailboxId); selectEmail(null); } // On mobile, close sidebar and go to list view if (isMobile) { setSidebarOpen(false); setActiveView("list"); } // On tablet, show the list again if (isTablet) { setTabletListVisible(true); } if (isTogglingOff) { // Disable the unread filter and show all emails clearSearchFilters(); if (client) { await fetchEmails(client, mailboxId); } } else { // Enable unread filter clearSearchFilters(); setSearchFilters({ isUnread: true }); if (client) { await advancedSearch(client); } } }; const handleLogout = () => { logout(); router.push('/login'); }; const handleSearch = async (query: string) => { if (!client) return; setSearchQuery(query); if (!isFilterEmpty(searchFilters)) { await advancedSearch(client); } else { await searchEmails(client, query); } }; const handleClearSearch = async () => { setSearchQuery(""); clearSearchFilters(); if (client && selectedMailbox) { await fetchEmails(client, selectedMailbox); } }; const handleAdvancedSearch = async () => { if (!client) return; await advancedSearch(client); }; const advancedSearchDebounceRef = useRef(null); const handleAdvancedSearchDebounced = useCallback(() => { if (advancedSearchDebounceRef.current) { clearTimeout(advancedSearchDebounceRef.current); } advancedSearchDebounceRef.current = setTimeout(() => { if (client) advancedSearch(client); }, 300); }, [client, advancedSearch]); useEffect(() => { return () => { if (advancedSearchDebounceRef.current) { clearTimeout(advancedSearchDebounceRef.current); } }; }, []); const handleDownloadAttachment = async (blobId: string, name: string, type?: string) => { if (!client) return; try { const { mailAttachmentAction } = useSettingsStore.getState(); if (mailAttachmentAction === 'preview' && isFilePreviewable(name, type)) { setPreviewAttachment({ blobId, name, type }); return; } await client.downloadBlob(blobId, name, type); } catch (error) { console.error("Failed to download attachment:", error); } }; const handlePreviewAttachmentDownload = useCallback(async () => { if (!client || !previewAttachment) return; await client.downloadBlob(previewAttachment.blobId, previewAttachment.name, previewAttachment.type); }, [client, previewAttachment]); const getPreviewAttachmentContent = useCallback(async () => { if (!client || !previewAttachment) { throw new Error('No attachment selected'); } const blob = await client.fetchBlob(previewAttachment.blobId, previewAttachment.name, previewAttachment.type); return { blob, contentType: previewAttachment.type || blob.type || 'application/octet-stream', }; }, [client, previewAttachment]); const handleQuickReply = async (body: string) => { if (!client || !selectedEmail) return; const sender = selectedEmail.from?.[0]; if (!sender?.email) { throw new Error("No sender email found"); } const primaryIdentity = identities[0]; // Append signature from the primary identity let finalBody = body; if (primaryIdentity?.textSignature) { finalBody = body + '\n\n-- \n' + primaryIdentity.textSignature; } // Send reply with just the body text await sendEmail( client, [sender.email], `Re: ${selectedEmail.subject || "(no subject)"}`, finalBody, undefined, undefined, primaryIdentity?.id, primaryIdentity?.email, undefined, primaryIdentity?.name || undefined ); // Refresh emails to show the sent reply await fetchEmails(client, selectedMailbox); }; // Show loading state while checking auth if (!initialCheckDone || authLoading || (!isAuthenticated || !client)) { return (

{t("common.loading")}

); } // Get current mailbox name for mobile header const currentMailboxName = mailboxes.find(m => m.id === selectedMailbox)?.name || "Inbox"; // Handle email selection with mobile view switching const handleEmailSelect = async (email: { id: string }) => { if (!client || !email) return; // If composing, suspend the composer (unmount will trigger onSaveState) if (showComposer) { setShowComposer(false); } // Set loading state immediately (keep current email visible) setLoadingEmail(true); // On mobile, switch to viewer if (isMobile) { setActiveView("viewer"); } // On tablet, hide the list to maximize viewer space if (isTablet) { setTabletListVisible(false); } // Fetch the full content try { // Find selected mailbox to determine accountId (for shared folders) const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); // Only pass accountId for shared mailboxes const accountId = mailbox?.isShared ? mailbox.accountId : undefined; const fullEmail = await client.getEmail(email.id, accountId); if (fullEmail) { selectEmail(fullEmail); // Mark-as-read logic is now handled by useEffect } } catch (error) { console.error('Failed to fetch email content:', error); } finally { setLoadingEmail(false); } }; // Handle back navigation from viewer on mobile const handleMobileBack = () => { // If in conversation view, clear it if (conversationThread) { setConversationThread(null); setConversationEmails([]); } selectEmail(null); setActiveView("list"); }; // Navigate to next/previous email in the list const selectedEmailIndex = selectedEmail ? emails.findIndex(e => e.id === selectedEmail.id) : -1; const handleNavigateNext = selectedEmailIndex >= 0 && selectedEmailIndex < emails.length - 1 ? () => handleEmailSelect(emails[selectedEmailIndex + 1]) : undefined; const handleNavigatePrev = selectedEmailIndex > 0 ? () => handleEmailSelect(emails[selectedEmailIndex - 1]) : undefined; // Handle opening conversation view on mobile const handleOpenConversation = async (thread: ThreadGroup) => { if (!client) return; setConversationThread(thread); setIsLoadingConversation(true); setActiveView("viewer"); try { // Fetch complete thread emails const emails = await client.getThreadEmails(thread.threadId); setConversationEmails(emails); } catch (error) { console.error('Failed to fetch thread emails:', error); // Fall back to thread.emails setConversationEmails(thread.emails); } finally { setIsLoadingConversation(false); } }; // Handle reply from conversation view const handleConversationReply = (email: Email) => { selectEmail(email); setComposerMode('reply'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const handleConversationReplyAll = (email: Email) => { selectEmail(email); setComposerMode('replyAll'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const handleConversationForward = (email: Email) => { selectEmail(email); setComposerMode('forward'); setShowComposer(true); if (isMobile) setActiveView('viewer'); }; const ToggleChip = ({ icon, label, value, onClick }: { icon: React.ReactNode; label: string; value: boolean | null; onClick: () => void }) => ( ); return (
{connectionLost && (
{tCommon('reconnecting')}
)}
{/* Desktop Navigation Rail */} {!isMobile && !isTablet && (
setShowShortcutsModal(true)} onManageApps={handleManageApps} onInlineApp={handleInlineApp} onCloseInlineApp={closeInlineApp} activeAppId={inlineApp?.id ?? null} />
)} {inlineApp && ( )} {/* Mobile/Tablet Sidebar Overlay Backdrop */} {(isMobile || isTablet) && sidebarOpen && !inlineApp && (
setSidebarOpen(false)} /> )} {/* Sidebar - overlay on mobile/tablet, fixed on desktop */}
{ setComposerMode('compose'); setShowComposer(true); if (isMobile) { setSidebarOpen(false); setActiveView('viewer'); } }} onSidebarClose={() => setSidebarOpen(false)} />
{/* Sidebar resize handle (desktop only, hidden when collapsed) */} {!isMobile && !isTablet && !sidebarCollapsed && !inlineApp && ( { dragStartWidth.current = sidebarWidth; setIsResizing(true); }} onResize={(delta) => setSidebarWidth(dragStartWidth.current + delta)} onResizeEnd={() => { setIsResizing(false); persistColumnWidths(); }} onDoubleClick={resetSidebarWidth} /> )} {/* Main Content Area */}
{/* Email List - full width on mobile, fixed width on tablet/desktop */} {/* Email list resize handle (desktop only) */} {!isMobile && !isTablet && ( { dragStartWidth.current = emailListWidth; setIsResizing(true); }} onResize={(delta) => setEmailListWidth(dragStartWidth.current + delta)} onResizeEnd={() => { setIsResizing(false); persistColumnWidths(); }} onDoubleClick={resetEmailListWidth} /> )} {/* Email Viewer / Composer - full screen on mobile, flex on tablet/desktop */}
{/* Inline Composer - shown in viewer pane */} {showComposer ? ( { setShowComposer(false); setComposerMode('compose'); }} > setPendingDraft(data)} onSend={async (data) => { await handleEmailSend(data); setPendingDraft(null); }} onClose={() => { setShowComposer(false); setComposerMode('compose'); setComposerDraftText(""); setPendingDraft(null); if (isMobile) { setActiveView('list'); } }} onDiscardDraft={(draftId) => { handleDiscardDraft(draftId); setPendingDraft(null); }} /> ) : ( <> {/* Pending draft banner */} {pendingDraft && ( )} {/* Mobile Conversation View - shown when thread is selected on mobile */} {isMobile && conversationThread ? ( { if (client) { await markAsRead(client, emailId, read); } }} /> ) : ( <> {/* Mobile Header for Viewer */} {isMobile && activeView === "viewer" && ( )} { if (client) { await markAsRead(client, emailId, read); } }} onDownloadAttachment={handleDownloadAttachment} onQuickReply={handleQuickReply} onBack={() => { setTabletListVisible(true); selectEmail(null); }} onNavigateNext={handleNavigateNext} onNavigatePrev={handleNavigatePrev} onShowShortcuts={() => setShowShortcutsModal(true)} currentUserEmail={client?.["username"]} currentUserName={client?.["username"]?.split("@")[0]} currentMailboxRole={mailboxes.find(m => m.id === selectedMailbox)?.role} mailboxes={mailboxes} selectedMailbox={selectedMailbox} onMoveToMailbox={async (mailboxId) => { if (client && selectedEmail) { await moveToMailbox(client, selectedEmail.id, mailboxId); } }} className={isMobile ? "flex-1" : undefined} /> )} )}
{/* Bottom Navigation - mobile and tablet */} {(isMobile || isTablet) && activeView !== "viewer" && ( )}
{/* Keyboard Shortcuts Modal */} setShowShortcutsModal(false)} /> {previewAttachment && ( setPreviewAttachment(null)} onDownload={handlePreviewAttachmentDownload} getFileContent={getPreviewAttachmentContent} /> )} {/* Screen reader live region for dynamic status announcements */}
); }