"use client"; import { useState } from 'react'; import { useTranslations } from 'next-intl'; import { useSettingsStore } from '@/stores/settings-store'; import { useEffectiveMailLayout } from '@/hooks/use-effective-mail-layout'; import { useMediaQuery } from '@/hooks/use-media-query'; import type { ArchiveMode, HoverAction, MvInboxType, SwipeAction } from '@/stores/settings-store'; import { ALL_HOVER_ACTIONS } from '@/stores/settings-store'; import { useAuthStore } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { useFaviconRefreshStore } from '@/stores/favicon-refresh-store'; import { toast } from '@/stores/toast-store'; import { cn } from '@/lib/utils'; import { IS_LITE } from '@/lib/lite'; import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section'; import { MessageListOrderSettings } from './message-list-order-settings'; import { Button } from '@/components/ui/button'; import { AlertTriangle, FolderSync, Loader2, RefreshCw } from '@/components/icons'; import { usePolicyStore } from '@/stores/policy-store'; import { allowEmailMove, createMailboxWithHooks, emitEmailMove } from '@/lib/plugin-mail-hooks'; import type { Mailbox } from '@/lib/jmap/types'; /** * The Message list page: what the list shows, who the senders look like, * what one hover or swipe does, and what Delete and Archive mean. These * rows used to be spread over Reading, Layout and Appearance. */ export function MessageListSettings() { const t = useTranslations('settings.email_behavior'); const tPage = useTranslations('settings.message_list'); const tAppearance = useTranslations('settings.appearance'); const tAdvanced = useTranslations('settings.advanced'); const [isReorganizing, setIsReorganizing] = useState(false); const [reorganizeResult, setReorganizeResult] = useState(null); const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore(); const archiveActionEnabled = isFeatureEnabled('archiveActionEnabled'); const refreshFavicons = useFaviconRefreshStore((s) => s.refresh); // Swipes exist on touch screens only; a mouse user never sees these rows. const touchDevice = useMediaQuery('(pointer: coarse)'); // With the Archive action off, a stored 'archive' swipe does nothing (the row // has no archive handler), so the select shows it as "none". const swipeShown = (action: SwipeAction) => (archiveActionEnabled || action !== 'archive' ? action : 'none'); const swipeOptions = [ { value: 'none', label: t('swipe_actions.none') }, ...(archiveActionEnabled ? [{ value: 'archive', label: t('swipe_actions.archive') }] : []), { value: 'delete', label: t('swipe_actions.delete') }, { value: 'markRead', label: t('swipe_actions.mark_read') }, { value: 'star', label: t('swipe_actions.star') }, { value: 'spam', label: t('swipe_actions.spam') }, ]; const { deleteAction, permanentlyDeleteJunk, confirmBulkDelete, swipeRightAction, swipeLeftAction, clearSearchOnFolderChange, showPreview, showVerificationCodes, disableThreading, emailsPerPage, archiveMode, hoverActions, hoverActionsMode, hoverActionsCorner, listColumns, mailLayout, interfaceLayout, mvInboxType, tintListRowsByTag, senderFavicons, showAvatarsInJunk, updateSetting, } = useSettingsStore(); const isFocusedLayout = useEffectiveMailLayout() === 'focus'; const mountainView = interfaceLayout === 'mountain-view'; const handleReorganizeArchive = async () => { const { client } = useAuthStore.getState(); const { mailboxes, fetchMailboxes } = useEmailStore.getState(); if (!client) return; const archiveMailbox = mailboxes.find(m => m.role === 'archive' || m.name.toLowerCase() === 'archive'); if (!archiveMailbox) return; setIsReorganizing(true); setReorganizeResult(null); try { const archiveId = archiveMailbox.originalId || archiveMailbox.id; const emails = await client.getEmailsInMailbox(archiveId); // One group per destination ("2024" or "2024/03"): plugins are asked // once per folder and move, and can refuse either; the groups they // refuse stay where they are. const groups = new Map(); for (const email of emails) { const emailDate = new Date(email.receivedAt); const year = emailDate.getFullYear().toString(); const month = (emailDate.getMonth() + 1).toString().padStart(2, '0'); const key = archiveMode === 'year' ? year : `${year}/${month}`; const group = groups.get(key) ?? { year, month, ids: [] }; group.ids.push(email.id); groups.set(key, group); } // An existing folder, or a new one; null when a plugin refused it. const folder = async (name: string, parentId: string): Promise => { const existing = useEmailStore.getState().mailboxes.find(m => m.name === name && m.parentId === parentId); if (existing) return existing; const created = await createMailboxWithHooks(client, name, parentId); if (created) await fetchMailboxes(client); return useEmailStore.getState().mailboxes.find(m => m.name === name && m.parentId === parentId) ?? created; }; let movedCount = 0; for (const { year, month, ids } of groups.values()) { const yearMailbox = await folder(year, archiveId); if (!yearMailbox) continue; const destination = archiveMode === 'year' ? yearMailbox : await folder(month, yearMailbox.originalId || yearMailbox.id); if (!destination) continue; const known = useEmailStore.getState().mailboxes; const move = { emailIds: ids, fromMailboxId: archiveMailbox.id, toMailboxId: destination.id }; if (!(await allowEmailMove(move, known))) continue; await client.batchMoveEmails(ids, destination.originalId || destination.id); emitEmailMove(move, known); movedCount += ids.length; } setReorganizeResult(t('archive_mode.reorganize_success', { count: movedCount })); } catch (error) { console.error('Failed to reorganize archive:', error); setReorganizeResult(t('archive_mode.reorganize_error')); } finally { setIsReorganizing(false); } }; const segment = (active: boolean) => cn( 'px-3 py-1.5 text-xs rounded-md transition-colors duration-150', active ? 'bg-primary text-primary-foreground font-medium' : 'bg-muted hover:bg-accent text-foreground' ); return (
{/* Stored as "disable threading"; shown as the positive choice. */} updateSetting('disableThreading', !checked)} /> {!isSettingHidden('showPreview') && ( updateSetting('showPreview', checked)} /> )} {/* Only the layouts with one-line rows can draw them as a table. */} {!isSettingHidden('listColumns') && !mountainView && mailLayout !== 'split' && ( updateSetting('listColumns', checked)} /> )} {/* Gmail's inbox types; "Unread first" puts the newest unread mail in a section above the rest. */} {mountainView && ( updateSetting('swipeRightAction', value as typeof swipeRightAction)} options={swipeOptions} /> )} {touchDevice && ( updateSetting('deleteAction', value as 'trash' | 'trash-and-read' | 'permanent')} options={[ { value: 'trash', label: t('delete_action.trash') }, { value: 'trash-and-read', label: t('delete_action.trash_and_read') }, { value: 'permanent', label: t('delete_action.permanent') }, ]} /> {deleteAction === 'permanent' && (
{t('delete_action.warning')}
)}
)} {archiveActionEnabled && (
updateSetting('emailsPerPage', parseInt(value))} options={[ { value: '10', label: t('emails_per_page.10') }, { value: '25', label: t('emails_per_page.25') }, { value: '50', label: t('emails_per_page.50') }, { value: '100', label: t('emails_per_page.100') }, ]} /> )} updateSetting('clearSearchOnFolderChange', checked)} />
); }