Files
SRCmail/components/settings/email-settings.tsx
T

302 lines
12 KiB
TypeScript

"use client";
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { useSettingsStore } from '@/stores/settings-store';
import type { ArchiveMode, HoverAction } 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 { cn } from '@/lib/utils';
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
import { ChevronRight, AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
export function EmailSettings() {
const t = useTranslations('settings.email_behavior');
const [showTrustedModal, setShowTrustedModal] = useState(false);
const [isReorganizing, setIsReorganizing] = useState(false);
const [reorganizeResult, setReorganizeResult] = useState<string | null>(null);
const {
markAsReadDelay,
deleteAction,
permanentlyDeleteJunk,
showPreview,
emailsPerPage,
externalContentPolicy,
mailAttachmentAction,
attachmentPosition,
emailAlwaysLightMode,
archiveMode,
hoverActions,
trustedSenders,
updateSetting,
} = useSettingsStore();
// Get count label for trusted senders button
const getTrustedSendersCount = () => {
const count = trustedSenders.length;
if (count === 0) return t('trusted_senders.count_zero');
if (count === 1) return t('trusted_senders.count_one');
return t('trusted_senders.count_other', { count });
};
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;
// Fetch all emails in the root archive mailbox
const emails = await client.getEmailsInMailbox(archiveId);
let movedCount = 0;
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');
// Re-read mailboxes from store each iteration in case new ones were created
let currentMailboxes = useEmailStore.getState().mailboxes;
// Find or create year subfolder
let yearMailbox = currentMailboxes.find(
m => m.name === year && m.parentId === archiveId
);
if (!yearMailbox) {
yearMailbox = await client.createMailbox(year, archiveId);
await fetchMailboxes(client);
currentMailboxes = useEmailStore.getState().mailboxes;
}
if (archiveMode === 'year') {
await client.moveEmail(email.id, yearMailbox.id);
movedCount++;
} else {
// month mode
const yearId = yearMailbox.originalId || yearMailbox.id;
let monthMailbox = currentMailboxes.find(
m => m.name === month && m.parentId === yearId
);
if (!monthMailbox) {
monthMailbox = await client.createMailbox(month, yearId);
await fetchMailboxes(client);
}
await client.moveEmail(email.id, monthMailbox.id);
movedCount++;
}
}
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);
}
};
return (
<SettingsSection title={t('title')} description={t('description')}>
{/* Mark as Read */}
<SettingItem label={t('mark_read.label')} description={t('mark_read.description')}>
<Select
value={markAsReadDelay.toString()}
onChange={(value) => updateSetting('markAsReadDelay', parseInt(value))}
options={[
{ value: '0', label: t('mark_read.instant') },
{ value: '3000', label: t('mark_read.delay_3s') },
{ value: '5000', label: t('mark_read.delay_5s') },
{ value: '-1', label: t('mark_read.never') },
]}
/>
</SettingItem>
{/* Delete Action */}
<SettingItem label={t('delete_action.label')} description={t('delete_action.description')}>
<div className="flex flex-col gap-2">
<Select
value={deleteAction}
onChange={(value) => updateSetting('deleteAction', value as 'trash' | 'permanent')}
options={[
{ value: 'trash', label: t('delete_action.trash') },
{ value: 'permanent', label: t('delete_action.permanent') },
]}
/>
{deleteAction === 'permanent' && (
<div className="flex items-start gap-2 p-2 rounded-md bg-destructive/10 text-destructive text-xs">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<span>{t('delete_action.warning')}</span>
</div>
)}
</div>
</SettingItem>
{/* Archive Mode */}
<SettingItem label={t('archive_mode.label')} description={t('archive_mode.description')}>
<div className="flex flex-col gap-2">
<Select
value={archiveMode}
onChange={(value) => updateSetting('archiveMode', value as ArchiveMode)}
options={[
{ value: 'single', label: t('archive_mode.single') },
{ value: 'year', label: t('archive_mode.year') },
{ value: 'month', label: t('archive_mode.month') },
]}
/>
{archiveMode !== 'single' && (
<div className="flex flex-col gap-2">
<button
onClick={handleReorganizeArchive}
disabled={isReorganizing}
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors text-sm disabled:opacity-50"
>
{isReorganizing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<FolderSync className="w-4 h-4" />
)}
<span>{t('archive_mode.reorganize')}</span>
</button>
{reorganizeResult && (
<p className="text-xs text-muted-foreground">{reorganizeResult}</p>
)}
</div>
)}
</div>
</SettingItem>
{/* Permanently Delete Junk */}
<SettingItem label={t('permanently_delete_junk.label')} description={t('permanently_delete_junk.description')}>
<ToggleSwitch
checked={permanentlyDeleteJunk}
onChange={(checked) => updateSetting('permanentlyDeleteJunk', checked)}
/>
</SettingItem>
{/* Show Preview */}
<SettingItem label={t('show_preview.label')} description={t('show_preview.description')}>
<ToggleSwitch checked={showPreview} onChange={(checked) => updateSetting('showPreview', checked)} />
</SettingItem>
{/* Quick Hover Actions */}
<div className="py-3 border-b border-border space-y-3">
<div>
<label className="text-sm font-medium text-foreground">{t('hover_actions.label')}</label>
<p className="text-xs text-muted-foreground mt-1">{t('hover_actions.description')}</p>
</div>
<div className="flex flex-wrap gap-2">
{ALL_HOVER_ACTIONS.map((action) => {
const isEnabled = hoverActions.includes(action.id);
return (
<button
key={action.id}
type="button"
onClick={() => {
const newActions = isEnabled
? hoverActions.filter((a: HoverAction) => a !== action.id)
: [...hoverActions, action.id];
updateSetting('hoverActions', newActions);
}}
className={cn(
'px-3 py-1.5 text-xs rounded-md transition-colors duration-150',
isEnabled
? 'bg-primary text-primary-foreground font-medium'
: 'bg-muted hover:bg-accent text-foreground'
)}
>
{t(`hover_actions.${action.labelKey}`)}
</button>
);
})}
</div>
</div>
<SettingItem label={t('attachment_click_action.label')} description={t('attachment_click_action.description')}>
<Select
value={mailAttachmentAction}
onChange={(value) => updateSetting('mailAttachmentAction', value as 'preview' | 'download')}
options={[
{ value: 'preview', label: t('attachment_click_action.preview') },
{ value: 'download', label: t('attachment_click_action.download') },
]}
/>
</SettingItem>
<SettingItem label={t('attachment_position.label')} description={t('attachment_position.description')}>
<Select
value={attachmentPosition}
onChange={(value) => updateSetting('attachmentPosition', value as 'beside-sender' | 'below-header')}
options={[
{ value: 'beside-sender', label: t('attachment_position.beside-sender') },
{ value: 'below-header', label: t('attachment_position.below-header') },
]}
/>
</SettingItem>
{/* Emails Per Page */}
<SettingItem label={t('emails_per_page.label')} description={t('emails_per_page.description')}>
<Select
value={emailsPerPage.toString()}
onChange={(value) => 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') },
]}
/>
</SettingItem>
{/* Always Light Mode for Emails */}
<SettingItem label={t('always_light_mode.label')} description={t('always_light_mode.description')}>
<ToggleSwitch
checked={emailAlwaysLightMode}
onChange={(checked) => updateSetting('emailAlwaysLightMode', checked)}
/>
</SettingItem>
{/* External Content */}
<SettingItem label={t('external_content.label')} description={t('external_content.description')}>
<Select
value={externalContentPolicy}
onChange={(value) =>
updateSetting('externalContentPolicy', value as 'ask' | 'block' | 'allow')
}
options={[
{ value: 'ask', label: t('external_content.ask') },
{ value: 'block', label: t('external_content.block') },
{ value: 'allow', label: t('external_content.allow') },
]}
/>
</SettingItem>
{/* Trusted Senders */}
<SettingItem label={t('trusted_senders.label')} description={t('trusted_senders.description')}>
<button
onClick={() => setShowTrustedModal(true)}
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
>
<span className="text-sm text-foreground">{getTrustedSendersCount()}</span>
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</button>
</SettingItem>
{/* Trusted Senders Modal */}
<TrustedSendersModal
isOpen={showTrustedModal}
onClose={() => setShowTrustedModal(false)}
/>
</SettingsSection>
);
}