"use client"; import { useState, useEffect, useRef, useMemo } from 'react'; import { useRouter } from '@/i18n/navigation'; import { useTranslations, useMessages } from 'next-intl'; import { ArrowLeft, ChevronRight, LogOut, Settings as SettingsIcon, Palette, Search, User, Shield, UserPen, PalmtreeIcon, Calendar, Filter, FileText, FolderOpen, Tags, HardDrive, BookUser, KeyRound, PanelLeftClose, Bell, Puzzle, LayoutGrid, Link as LinkIcon, BookOpen, PenLine, EyeOff, Languages, Info, Bug, X, type LucideIcon, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { AppearanceSettings } from '@/components/settings/appearance-settings'; import { AppTopBannerSlot } from '@/components/plugins/app-top-banner-slot'; import { LayoutSettings } from '@/components/settings/layout-settings'; import { LanguageSettings } from '@/components/settings/language-settings'; import { ReadingSettings } from '@/components/settings/reading-settings'; import { ComposingSettings } from '@/components/settings/composing-settings'; import { ContentSendersSettings } from '@/components/settings/content-senders-settings'; import { AccountSettings } from '@/components/settings/account-settings'; import { IdentitySettings } from '@/components/settings/identity-settings'; import { VacationSettings } from '@/components/settings/vacation-settings'; import { CalendarSettings } from '@/components/settings/calendar-settings'; import { CalendarManagementSettings } from '@/components/settings/calendar-management-settings'; import { AddressBookManagementSettings } from '@/components/settings/address-book-management-settings'; import { FilterSettings } from '@/components/settings/filter-settings'; import { TemplateSettings } from '@/components/settings/template-settings'; import { AboutDataSettings } from '@/components/settings/about-data-settings'; import { DebugSettings } from '@/components/settings/debug-settings'; import { FolderSettings } from '@/components/settings/folder-settings'; import { KeywordSettings } from '@/components/settings/keyword-settings'; import { AccountSecuritySettings } from '@/components/settings/account-security-settings'; import { FilesSettingsComponent } from '@/components/settings/files-settings'; import { ContactsSettings } from '@/components/settings/contacts-settings'; import { SmimeSettings } from '@/components/settings/smime-settings'; import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings'; import { NotificationSettings } from '@/components/settings/notification-settings'; import { ThemesSettings } from '@/components/settings/themes-settings'; import { PluginsSettings } from '@/components/settings/plugins-settings'; import { ProtocolHandlerSettings } from '@/components/settings/protocol-handler-settings'; import { useAuthStore, redirectToLogin } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { usePluginStore } from '@/stores/plugin-store'; import { useThemeStore } from '@/stores/theme-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useIsDesktop } from '@/hooks/use-media-query'; 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 { useIsEmbedded } from '@/hooks/use-is-embedded'; import { ResizeHandle } from '@/components/layout/resize-handle'; import { useConfig } from '@/hooks/use-config'; import { usePolicyStore } from '@/stores/policy-store'; import { cn } from '@/lib/utils'; type Tab = | 'account' | 'language' | 'notifications' | 'appearance' | 'layout' | 'reading' | 'composing' | 'identities' | 'vacation' | 'filters' | 'templates' | 'folders' | 'keywords' | 'security' | 'encryption' | 'content_senders' | 'calendar' | 'contacts' | 'files' | 'protocol_handlers' | 'sidebar_apps' | 'about_data' | 'themes' | 'plugins' | 'debug'; type TabGroup = 'general' | 'appearance' | 'mail' | 'privacy' | 'apps' | 'advanced'; interface TabDef { id: Tab; label: string; icon: LucideIcon; group: TabGroup; } const tabIcons: Record = { account: User, language: Languages, notifications: Bell, appearance: Palette, layout: LayoutGrid, reading: BookOpen, composing: PenLine, identities: UserPen, vacation: PalmtreeIcon, filters: Filter, templates: FileText, folders: FolderOpen, keywords: Tags, security: Shield, encryption: KeyRound, content_senders: EyeOff, calendar: Calendar, contacts: BookUser, files: HardDrive, protocol_handlers: LinkIcon, sidebar_apps: PanelLeftClose, about_data: Info, themes: Palette, plugins: Puzzle, debug: Bug, }; const tabGroupOrder: TabGroup[] = ['general', 'appearance', 'mail', 'privacy', 'apps', 'advanced']; // Translation paths per tab. Tabs that share a namespace (email_behavior, // appearance) explicitly list the subkeys they actually render so sub-results // are attributed to the correct tab. Tabs with their own namespace just point // at the namespace root. const tabSearchPaths: Record = { account: [ 'settings.account.name_label', 'settings.account.username_label', 'settings.account.account_type_label', 'settings.account.auth_method_label', 'settings.account.email', 'settings.account.server', 'settings.account.storage', 'settings.account.accounts', ], language: ['settings.appearance.language'], notifications: ['settings.notifications'], appearance: [ 'settings.appearance.theme', 'settings.appearance.font_size', 'settings.appearance.list_density', 'settings.appearance.animations', ], layout: [ 'settings.appearance.toolbar_position', 'settings.appearance.toolbar_labels', 'settings.appearance.hide_account_switcher', 'settings.appearance.show_rail_account_list', 'settings.appearance.unified_mailbox', 'settings.appearance.colorful_sidebar_icons', 'settings.email_behavior.mail_layout', ], reading: [ 'settings.email_behavior.mark_read', 'settings.email_behavior.archive_mode', 'settings.email_behavior.delete_action', 'settings.email_behavior.attachment_click_action', 'settings.email_behavior.attachment_image_previews', 'settings.email_behavior.attachment_position', 'settings.email_behavior.disable_threading', 'settings.email_behavior.emails_per_page', 'settings.email_behavior.hide_inline_image_attachments', 'settings.email_behavior.hover_actions', 'settings.email_behavior.permanently_delete_junk', 'settings.email_behavior.show_preview', 'settings.email_behavior.plain_text_mode', ], composing: [ 'settings.email_behavior.attachment_reminder', 'settings.email_behavior.auto_select_reply_identity', 'settings.email_behavior.default_mail_program', 'settings.email_behavior.signature_position', 'settings.email_behavior.sub_address_delimiter', ], identities: ['settings.identities'], vacation: ['settings.vacation'], filters: ['settings.filters'], templates: ['settings.templates'], folders: ['settings.folders'], keywords: ['settings.keywords'], security: ['settings.security'], encryption: ['smime'], content_senders: [ 'settings.email_behavior.always_light_mode', 'settings.email_behavior.external_content', 'settings.email_behavior.trusted_senders', ], calendar: ['calendar.settings', 'calendar.management'], contacts: ['settings.contacts', 'contacts'], files: ['settings.files'], protocol_handlers: ['protocol_handlers'], sidebar_apps: ['settings.sidebar_apps', 'sidebar_apps'], about_data: ['settings.advanced'], themes: [], plugins: [], debug: ['settings.advanced'], }; // Extra English keywords per tab so common search terms hit even when the // translation doesn't contain the literal word. const tabKeywords: Record = { account: 'profile email password user signin signout reorder rearrange drag dropdown switcher multi-account', language: 'locale region timezone date time format', notifications: 'sound alert push badge', appearance: 'theme dark light font size accent color animation density', layout: 'toolbar sidebar account switcher unified mailbox icons rail', reading: 'mark read preview thread conversation archive delete attachment open', composing: 'editor signature plain text reply forward draft compose', identities: 'from address signature email', vacation: 'auto reply away out of office holiday responder', filters: 'sieve rules block junk forward', templates: 'snippet quick reply', folders: 'mailbox subscribe', keywords: 'tags labels colors', security: 'password 2fa two-factor passkey app password mfa', encryption: 's/mime smime certificate pgp gpg', content_senders: 'block sender remote images privacy tracking', calendar: 'event schedule appointment meeting timezone', contacts: 'address book contact', files: 'attachments cloud drive storage upload', protocol_handlers: 'mailto webcal links default app protocol handler', sidebar_apps: 'apps webview iframe', about_data: 'export import storage quota privacy backup', themes: 'custom theme css skin appearance', plugins: 'extensions addons', debug: 'logs developer console diagnostic', }; function flattenStrings(node: unknown, sink: string[]): void { if (typeof node === 'string') { sink.push(node); return; } if (Array.isArray(node)) { for (const item of node) flattenStrings(item, sink); return; } if (node && typeof node === 'object') { for (const value of Object.values(node)) flattenStrings(value, sink); } } interface SubResult { label: string; description?: string; // For plugin setting fields: the id of the plugin whose card needs to be // expanded before the field becomes visible in the DOM. pluginId?: string; } // Walk a translation subtree and emit sub-results for renderable settings. // Picks up: // - bare string leaves (when a tab path points directly at a flat label) // - objects with a `label` or `title` field (the standard pattern) // - flat `*_label` string keys at any object level (e.g. `name_label`) function collectSubResults(node: unknown, sink: SubResult[]): void { if (typeof node === 'string') { sink.push({ label: node }); return; } if (!node || typeof node !== 'object' || Array.isArray(node)) return; const obj = node as Record; const label = typeof obj.label === 'string' ? obj.label : (typeof obj.title === 'string' ? obj.title : undefined); if (label) { sink.push({ label, description: typeof obj.description === 'string' ? obj.description : undefined, }); } for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string' && key !== 'label' && key !== 'title' && key.endsWith('_label')) { sink.push({ label: value }); } } for (const value of Object.values(obj)) { if (value && typeof value === 'object' && !Array.isArray(value)) { collectSubResults(value, sink); } } } function getByPath(obj: unknown, path: string): unknown { let cur: unknown = obj; for (const key of path.split('.')) { if (cur && typeof cur === 'object' && key in (cur as Record)) { cur = (cur as Record)[key]; } else { return undefined; } } return cur; } // Map legacy tab IDs to current ones; runs once on read of localStorage. const LEGACY_TAB_MAP: Record = { email: 'reading', advanced: 'about_data', }; function readPersistedTab(): Tab { try { const saved = localStorage.getItem('settings-active-tab'); if (!saved) return 'appearance'; if (saved in LEGACY_TAB_MAP) { const migrated = LEGACY_TAB_MAP[saved]; try { localStorage.setItem('settings-active-tab', migrated); } catch { /* ignore */ } return migrated; } return saved as Tab; } catch { return 'appearance'; } } export default function SettingsPage() { const router = useRouter(); const t = useTranslations('settings'); const tSidebar = useTranslations('sidebar'); const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const isEmbedded = useIsEmbedded(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); const { quota, isPushConnected } = useEmailStore(); const { stalwartFeaturesEnabled } = useConfig(); const { isFeatureEnabled } = usePolicyStore(); const [activeTab, setActiveTab] = useState(readPersistedTab); const [mobileShowContent, setMobileShowContent] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [pendingHighlight, setPendingHighlight] = useState<{ tab: Tab; label: string; pluginId?: string } | null>(null); const isDesktop = useIsDesktop(); const messages = useMessages() as Record; const installedPlugins = usePluginStore((s) => s.plugins); const installedThemes = useThemeStore((s) => s.installedThemes); const sidebarAppsList = useSettingsStore((s) => s.sidebarApps); const proInterface = useSettingsStore((s) => s.proInterface); // Build a per-tab haystack for fulltext search and a list of sub-results // (individual settings) per tab. Sub-results come from translation entries // that have a `label`/`title` field, plus dynamic content (installed // plugins/themes/sidebar apps). const { tabSearchHaystacks, tabSubResults } = useMemo(() => { const haystacks: Partial> = {}; const subs: Partial> = {}; const tabIds = Object.keys(tabSearchPaths) as Tab[]; for (const tabId of tabIds) { const strings: string[] = [tabId.replace(/_/g, ' '), tabKeywords[tabId] ?? '']; const list: SubResult[] = []; for (const path of tabSearchPaths[tabId]) { const node = getByPath(messages, path); flattenStrings(node, strings); collectSubResults(node, list); } // Dedupe sub-results by label const seen = new Set(); subs[tabId] = list.filter((r) => { if (seen.has(r.label)) return false; seen.add(r.label); return true; }); haystacks[tabId] = strings.join(' ').toLowerCase(); } if (installedPlugins.length) { const haystackText = installedPlugins.map((p) => { const fieldText = p.settingsSchema ? Object.values(p.settingsSchema) .map((s) => `${s.label} ${s.description ?? ''}`) .join(' ') : ''; return `${p.name} ${p.description} ${p.author} ${fieldText}`; }).join(' '); haystacks.plugins = `${haystacks.plugins ?? ''} ${haystackText}`.toLowerCase(); const pluginSubs: SubResult[] = installedPlugins.flatMap((p) => { const items: SubResult[] = [{ label: p.name, description: p.description }]; if (p.settingsSchema) { for (const schema of Object.values(p.settingsSchema)) { items.push({ label: schema.label, description: schema.description, pluginId: p.id, }); } } return items; }); subs.plugins = [...(subs.plugins ?? []), ...pluginSubs]; } if (installedThemes.length) { const text = installedThemes.map((th) => `${th.name} ${th.description} ${th.author}`).join(' '); haystacks.themes = `${haystacks.themes ?? ''} ${text}`.toLowerCase(); subs.themes = [ ...(subs.themes ?? []), ...installedThemes.map((th) => ({ label: th.name, description: th.description })), ]; } if (sidebarAppsList.length) { const text = sidebarAppsList.map((a) => `${a.name} ${a.url}`).join(' '); haystacks.sidebar_apps = `${haystacks.sidebar_apps ?? ''} ${text}`.toLowerCase(); subs.sidebar_apps = [ ...(subs.sidebar_apps ?? []), ...sidebarAppsList.map((a) => ({ label: a.name, description: a.url })), ]; } return { tabSearchHaystacks: haystacks, tabSubResults: subs }; }, [messages, installedPlugins, installedThemes, sidebarAppsList]); // Sidebar resize state const [settingsSidebarWidth, setSettingsSidebarWidth] = useState(() => { try { const v = localStorage.getItem('settings-sidebar-width'); return v ? Number(v) : 256; } catch { return 256; } }); const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(256); // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { const state = useAuthStore.getState(); if (state.isAuthenticated && state.client) { setInitialCheckDone(true); return; } checkAuth().finally(() => { setInitialCheckDone(true); }); }, [checkAuth]); // Listen for tab change events from child components (with legacy migration) useEffect(() => { const handler = (e: Event) => { const raw = (e as CustomEvent).detail as string; if (!raw) return; const tab = (LEGACY_TAB_MAP[raw] ?? raw) as Tab; setActiveTab(tab); try { localStorage.setItem('settings-active-tab', tab); } catch { /* ignore */ } }; window.addEventListener('settings-tab-change', handler); return () => window.removeEventListener('settings-tab-change', handler); }, []); useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ } redirectToLogin(); } }, [initialCheckDone, isAuthenticated, authLoading]); // Sync the mobile submenu view with browser history so the system back // button (or gesture) returns to the settings list before exiting /settings. useEffect(() => { if (isDesktop) return; if (typeof window === 'undefined') return; if (!mobileShowContent) return; window.history.pushState({ __settingsSubmenu: true }, ''); const handlePop = () => { setMobileShowContent(false); }; window.addEventListener('popstate', handlePop); return () => window.removeEventListener('popstate', handlePop); }, [isDesktop, mobileShowContent]); // After clicking a search sub-result, scroll the matching setting into view // and add a temporary highlight class. Some tabs fetch data and render // their SettingItems only after a loading state, so retry until the element // shows up (or we give up after ~2s). useEffect(() => { if (!pendingHighlight) return; if (pendingHighlight.tab !== activeTab) return; if (typeof window === 'undefined') return; // For plugin-setting sub-results, ask the plugins tab to expand the // matching card so the field becomes part of the DOM. Dispatched here // (not in the click handler) because PluginsSettings only mounts after // the tab switches, and its listener registers in its own useEffect - // child effects run before parent effects, so by the time we get here // the listener is guaranteed to be in place. if (pendingHighlight.pluginId) { window.dispatchEvent( new CustomEvent('settings-plugin-expand', { detail: { pluginId: pendingHighlight.pluginId } }) ); } let cancelled = false; let retryTimer: ReturnType | undefined; let cleanupTimer: ReturnType | undefined; let highlightedEl: HTMLElement | null = null; const escaped = pendingHighlight.label.replace(/"/g, '\\"'); const selector = `[data-search-label="${escaped}"]`; const deadline = Date.now() + 2000; const tryHighlight = () => { if (cancelled) return; const el = document.querySelector(selector); if (!el) { if (Date.now() < deadline) { retryTimer = setTimeout(tryHighlight, 80); } return; } el.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Remove + reflow + add restarts the CSS animation if the class was // already present (re-clicking the same sub-result). el.classList.remove('settings-search-highlight'); void el.offsetWidth; el.classList.add('settings-search-highlight'); highlightedEl = el; cleanupTimer = setTimeout(() => { el.classList.remove('settings-search-highlight'); highlightedEl = null; }, 1800); }; // First attempt next frame so the freshly-mounted tab content is in DOM. const raf = window.requestAnimationFrame(tryHighlight); // Do NOT reset pendingHighlight here - that would retrigger this effect // and the cleanup below would strip the class right after we added it. return () => { cancelled = true; window.cancelAnimationFrame(raf); if (retryTimer) clearTimeout(retryTimer); if (cleanupTimer) clearTimeout(cleanupTimer); if (highlightedEl) highlightedEl.classList.remove('settings-search-highlight'); }; }, [pendingHighlight, activeTab]); if (!isAuthenticated) { return null; } const supportsVacation = client?.supportsVacationResponse() ?? false; const supportsCalendar = client?.supportsCalendars() ?? false; const supportsSieve = client?.supportsSieve() ?? false; const supportsFiles = client?.supportsFiles() ?? false; const tabs: TabDef[] = [ // General { id: 'account', label: t('tabs.account'), icon: tabIcons.account, group: 'general' }, { id: 'language', label: t('tabs.language'), icon: tabIcons.language, group: 'general' }, { id: 'notifications', label: t('tabs.notifications'), icon: tabIcons.notifications, group: 'general' }, { id: 'protocol_handlers', label: t('tabs.protocol_handlers'), icon: tabIcons.protocol_handlers, group: 'general' }, // Appearance { id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'appearance' }, { id: 'layout', label: t('tabs.layout'), icon: tabIcons.layout, group: 'appearance' }, // Mail { id: 'reading', label: t('tabs.reading'), icon: tabIcons.reading, group: 'mail' }, { id: 'composing', label: t('tabs.composing'), icon: tabIcons.composing, group: 'mail' }, { id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'mail' }, ...(supportsVacation ? [{ id: 'vacation' as Tab, label: t('tabs.vacation'), icon: tabIcons.vacation, group: 'mail' as TabGroup }] : []), ...(supportsSieve ? [{ id: 'filters' as Tab, label: t('tabs.filters'), icon: tabIcons.filters, group: 'mail' as TabGroup }] : []), ...(isFeatureEnabled('templatesEnabled') ? [{ id: 'templates' as Tab, label: t('tabs.templates'), icon: tabIcons.templates, group: 'mail' as TabGroup }] : []), { id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'mail' }, ...(isFeatureEnabled('customKeywordsEnabled') ? [{ id: 'keywords' as Tab, label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'mail' as TabGroup }] : []), // Privacy & Security ...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security'), icon: tabIcons.security, group: 'privacy' as TabGroup }] : []), ...(isFeatureEnabled('smimeEnabled') ? [{ id: 'encryption' as Tab, label: t('tabs.encryption'), icon: tabIcons.encryption, group: 'privacy' as TabGroup }] : []), { id: 'content_senders', label: t('tabs.content_senders'), icon: tabIcons.content_senders, group: 'privacy' }, // Apps ...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []), ...(isFeatureEnabled('contactsEnabled') ? [{ id: 'contacts' as Tab, label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' as TabGroup }] : []), ...(supportsFiles && isFeatureEnabled('filesEnabled') ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []), ...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []), // Advanced { id: 'about_data', label: t('tabs.about_data'), icon: tabIcons.about_data, group: 'advanced' }, ...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'advanced' as TabGroup }] : []), ...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'advanced' as TabGroup }] : []), ...(isFeatureEnabled('debugModeEnabled') ? [{ id: 'debug' as Tab, label: t('tabs.debug'), icon: tabIcons.debug, group: 'advanced' as TabGroup }] : []), ]; // Group tabs by category const groupedTabs = tabGroupOrder .map((group) => ({ group, label: t(`tab_groups.${group}`), items: tabs.filter((tab) => tab.group === group), })) .filter((g) => g.items.length > 0); const trimmedQuery = searchQuery.trim().toLowerCase(); const matchesQuery = (tab: TabDef) => { if (!trimmedQuery) return true; if (tab.label.toLowerCase().includes(trimmedQuery)) return true; return tabSearchHaystacks[tab.id]?.includes(trimmedQuery) ?? false; }; const subResultsForTab = (tabId: Tab): SubResult[] => { if (!trimmedQuery) return []; const list = tabSubResults[tabId] ?? []; return list .filter((r) => r.label.toLowerCase().includes(trimmedQuery) || (r.description?.toLowerCase().includes(trimmedQuery) ?? false) ) .slice(0, 6); }; const filteredGroupedTabs = trimmedQuery ? groupedTabs .map((g) => ({ ...g, items: g.items.filter(matchesQuery) })) .filter((g) => g.items.length > 0) : groupedTabs; // If active tab is not in the visible list (e.g., feature disabled), fall back. const isActiveVisible = tabs.some((tab) => tab.id === activeTab); const effectiveActiveTab: Tab = isActiveVisible ? activeTab : 'appearance'; const handleTabSelect = (tabId: Tab) => { setActiveTab(tabId); try { localStorage.setItem('settings-active-tab', tabId); } catch { /* ignore */ } if (!isDesktop) { setMobileShowContent(true); } }; const handleSubResultSelect = (tabId: Tab, sub: SubResult) => { handleTabSelect(tabId); setPendingHighlight({ tab: tabId, label: sub.label, pluginId: sub.pluginId }); }; const activeTabLabel = tabs.find((tab) => tab.id === effectiveActiveTab)?.label ?? ''; const renderTabContent = () => ( <> {effectiveActiveTab === 'account' && } {effectiveActiveTab === 'language' && } {effectiveActiveTab === 'notifications' && } {effectiveActiveTab === 'appearance' && } {effectiveActiveTab === 'layout' && } {effectiveActiveTab === 'reading' && } {effectiveActiveTab === 'composing' && } {effectiveActiveTab === 'identities' && } {effectiveActiveTab === 'vacation' && } {effectiveActiveTab === 'filters' && } {effectiveActiveTab === 'templates' && } {effectiveActiveTab === 'folders' && } {effectiveActiveTab === 'keywords' && } {effectiveActiveTab === 'security' && } {effectiveActiveTab === 'encryption' && } {effectiveActiveTab === 'content_senders' && } {effectiveActiveTab === 'calendar' && <>
} {effectiveActiveTab === 'contacts' && <>
} {effectiveActiveTab === 'files' && } {effectiveActiveTab === 'protocol_handlers' && } {effectiveActiveTab === 'sidebar_apps' && } {effectiveActiveTab === 'about_data' && } {effectiveActiveTab === 'themes' && } {effectiveActiveTab === 'plugins' && } {effectiveActiveTab === 'debug' && } ); // Mobile layout if (!isDesktop) { if (mobileShowContent) { return (

{activeTabLabel}

{renderTabContent()}
{!isEmbedded && ( )}
); } return (

{t('title')}

setSearchQuery(e.target.value)} placeholder={t('search_placeholder')} className="pl-9 pr-9 h-10" aria-label={t('search_placeholder')} /> {searchQuery && ( )}
{filteredGroupedTabs.length === 0 && (
{t('search_no_results')}
)} {filteredGroupedTabs.map((group, groupIndex) => (
{groupIndex > 0 &&
}
{group.label}
{group.items.map((tab) => { const Icon = tab.icon; const subs = subResultsForTab(tab.id); return (
{subs.map((sub) => ( ))}
); })}
))}
{!isEmbedded && ( )}
); } // Desktop layout return (
{!isEmbedded && (
)} {inlineApp && ( )} {!inlineApp && ( <>
{!proInterface && (
)}
setSearchQuery(e.target.value)} placeholder={t('search_placeholder')} className="pl-8 pr-8 h-9 text-sm" aria-label={t('search_placeholder')} /> {searchQuery && ( )}
{filteredGroupedTabs.length === 0 && (
{t('search_no_results')}
)} {filteredGroupedTabs.map((group, groupIndex) => (
{groupIndex > 0 &&
}
{group.label}
{group.items.map((tab) => { const Icon = tab.icon; const subs = subResultsForTab(tab.id); return (
{subs.map((sub) => ( ))}
); })}
))}
{ dragStartWidth.current = settingsSidebarWidth; setIsResizing(true); }} onResize={(delta) => setSettingsSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))} onResizeEnd={() => { setIsResizing(false); localStorage.setItem('settings-sidebar-width', String(settingsSidebarWidth)); }} onDoubleClick={() => { setSettingsSidebarWidth(256); localStorage.setItem('settings-sidebar-width', '256'); }} />
{renderTabContent()}
)}
); }