feat: add "All Mail" view

This commit is contained in:
Linus Rath
2026-06-15 18:42:23 +02:00
parent 404a1e847c
commit c51c3655d5
27 changed files with 315 additions and 11 deletions
+11 -2
View File
@@ -11,8 +11,9 @@ import type { ComposerDraftData } from "@/components/email/email-composer";
import { ProtocolAccountPicker } from "@/components/protocol/protocol-account-picker"; import { ProtocolAccountPicker } from "@/components/protocol/protocol-account-picker";
import { ThreadConversationView } from "@/components/email/thread-conversation-view"; import { ThreadConversationView } from "@/components/email/thread-conversation-view";
import { MobileHeader } from "@/components/layout/mobile-header"; import { MobileHeader } from "@/components/layout/mobile-header";
import { ThreadGroup, Email, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID } from "@/lib/jmap/types"; import { ThreadGroup, Email, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID, ALL_MAIL_MAILBOX_ID } from "@/lib/jmap/types";
import { useAccountStore } from "@/stores/account-store"; import { useAccountStore } from "@/stores/account-store";
import { usePolicyStore } from "@/stores/policy-store";
import type { UnifiedAccountClient } from "@/lib/unified-mailbox"; import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
import { useEmailStore, buildUnifiedAccountClients } from "@/stores/email-store"; import { useEmailStore, buildUnifiedAccountClients } from "@/stores/email-store";
@@ -341,7 +342,10 @@ export default function Home() {
useProMultiAccountMailboxes(); useProMultiAccountMailboxes();
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox); const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
const enableAllMailView = useSettingsStore((s) => s.enableAllMailView);
const delayedSendSupported = client?.hasDelayedSend() ?? true; const delayedSendSupported = client?.hasDelayedSend() ?? true;
const allMailViewEnabled = usePolicyStore((s) => s.isFeatureEnabled('allMailViewEnabled'));
const showAllMailMailbox = allMailViewEnabled && enableAllMailView;
const activeEmails = isScheduledView ? scheduledEmails : emails; const activeEmails = isScheduledView ? scheduledEmails : emails;
const activeHasMore = isScheduledView ? scheduledHasMore : hasMoreEmails; const activeHasMore = isScheduledView ? scheduledHasMore : hasMoreEmails;
const activeIsLoading = isScheduledView ? isLoadingScheduled : isLoading; const activeIsLoading = isScheduledView ? isLoadingScheduled : isLoading;
@@ -2213,7 +2217,11 @@ export default function Home() {
} }
// Get current mailbox name for mobile header // Get current mailbox name for mobile header
const currentMailboxName = isScheduledView ? t('sidebar.scheduled') : mailboxes.find(m => m.id === selectedMailbox)?.name || "Inbox"; const currentMailboxName = isScheduledView
? t('sidebar.scheduled')
: selectedMailbox === ALL_MAIL_MAILBOX_ID
? t('sidebar.mailboxes.all_mail')
: mailboxes.find(m => m.id === selectedMailbox)?.name || "Inbox";
const isFocusedMailLayout = mailLayout === 'focus'; const isFocusedMailLayout = mailLayout === 'focus';
const isHorizontalMailLayout = mailLayout === 'horizontal' && !isMobile && !isTablet; const isHorizontalMailLayout = mailLayout === 'horizontal' && !isMobile && !isTablet;
const hasViewerContent = showComposer || Boolean(conversationThread) || Boolean(selectedEmail); const hasViewerContent = showComposer || Boolean(conversationThread) || Boolean(selectedEmail);
@@ -2475,6 +2483,7 @@ export default function Home() {
selectedKeyword={selectedKeyword} selectedKeyword={selectedKeyword}
scheduledTotal={scheduledTotal} scheduledTotal={scheduledTotal}
showScheduledMailbox={delayedSendSupported} showScheduledMailbox={delayedSendSupported}
showAllMailMailbox={showAllMailMailbox}
onMailboxSelect={handleMailboxSelect} onMailboxSelect={handleMailboxSelect}
onTagSelect={handleTagSelect} onTagSelect={handleTagSelect}
onUnreadFilterClick={handleUnreadFilterClick} onUnreadFilterClick={handleUnreadFilterClick}
+1
View File
@@ -183,6 +183,7 @@ const tabSearchPaths: Record<Tab, string[]> = {
'settings.appearance.hide_account_switcher', 'settings.appearance.hide_account_switcher',
'settings.appearance.show_rail_account_list', 'settings.appearance.show_rail_account_list',
'settings.appearance.unified_mailbox', 'settings.appearance.unified_mailbox',
'settings.appearance.all_mail',
'settings.appearance.colorful_sidebar_icons', 'settings.appearance.colorful_sidebar_icons',
'settings.email_behavior.mail_layout', 'settings.email_behavior.mail_layout',
], ],
+1
View File
@@ -21,6 +21,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
folderIconsEnabled: { label: 'Folder Icons', description: 'Allow custom folder icon picker' }, folderIconsEnabled: { label: 'Folder Icons', description: 'Allow custom folder icon picker' },
hoverActionsConfigEnabled: { label: 'Hover Actions Config', description: 'Allow users to customize email hover actions' }, hoverActionsConfigEnabled: { label: 'Hover Actions Config', description: 'Allow users to customize email hover actions' },
filesEnabled: { label: 'Files (WebDAV)', description: 'Enable file storage via WebDAV. WARNING: Large uploads can cause Stalwart/RocksDB instability. Not recommended for production.' }, filesEnabled: { label: 'Files (WebDAV)', description: 'Enable file storage via WebDAV. WARNING: Large uploads can cause Stalwart/RocksDB instability. Not recommended for production.' },
allMailViewEnabled: { label: 'All Mail View', description: 'Show a virtual "All Mail" folder that merges messages from across an accounts folders into one list. Users choose which folders are included.' },
}; };
const RESTRICTABLE_SETTINGS = [ const RESTRICTABLE_SETTINGS = [
+14
View File
@@ -33,6 +33,7 @@ import {
NotebookPen, NotebookPen,
CalendarClock, CalendarClock,
BellOff, BellOff,
Mails,
} from "lucide-react"; } from "lucide-react";
import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils"; import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
import { Mailbox } from "@/lib/jmap/types"; import { Mailbox } from "@/lib/jmap/types";
@@ -76,6 +77,8 @@ interface SidebarProps {
onRefreshMailboxes?: () => void; onRefreshMailboxes?: () => void;
scheduledTotal?: number; scheduledTotal?: number;
showScheduledMailbox?: boolean; showScheduledMailbox?: boolean;
/** Gated "All Mail" virtual folder that merges all of the account's folders. */
showAllMailMailbox?: boolean;
className?: string; className?: string;
/** /**
* Multi-account (Pro) mode props. When `multiAccountMode` is true, the * Multi-account (Pro) mode props. When `multiAccountMode` is true, the
@@ -678,6 +681,7 @@ export function Sidebar({
onRefreshMailboxes, onRefreshMailboxes,
scheduledTotal = 0, scheduledTotal = 0,
showScheduledMailbox = false, showScheduledMailbox = false,
showAllMailMailbox = false,
className, className,
multiAccountMode = false, multiAccountMode = false,
accountMailboxes, accountMailboxes,
@@ -978,6 +982,16 @@ export function Sidebar({
{/* Mailbox List */} {/* Mailbox List */}
<div className="flex-1 overflow-y-auto" data-tour="sidebar"> <div className="flex-1 overflow-y-auto" data-tour="sidebar">
{showAllMailMailbox && (
<SidebarRow
icon={<Mails className={cn("w-4 h-4 flex-shrink-0", selectedMailbox === '__all_mail__' ? "text-foreground" : "text-muted-foreground")} />}
label={t('mailboxes.all_mail')}
depth={0}
isSelected={!selectedKeyword && selectedMailbox === '__all_mail__'}
onClick={() => onMailboxSelect?.('__all_mail__')}
isCollapsed={isCollapsed}
/>
)}
{showUnified && ( {showUnified && (
<div> <div>
<SidebarSectionHeader <SidebarSectionHeader
+69 -2
View File
@@ -2,6 +2,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Check, Folder } from 'lucide-react';
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store'; import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section'; import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -117,11 +118,27 @@ function MailLayoutPreview({
export function LayoutSettings() { export function LayoutSettings() {
const t = useTranslations('settings.appearance'); const t = useTranslations('settings.appearance');
const tEmail = useTranslations('settings.email_behavior'); const tEmail = useTranslations('settings.email_behavior');
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore(); const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
const { isSettingLocked, isSettingHidden } = usePolicyStore(); const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
const accounts = useAccountStore(s => s.accounts); const accounts = useAccountStore(s => s.accounts);
const mailboxes = useEmailStore(s => s.mailboxes); const mailboxes = useEmailStore(s => s.mailboxes);
const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]); const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]);
const allMailViewAllowed = isFeatureEnabled('allMailViewEnabled');
// Own (non-shared) folders and the current All Mail selection. `null` =
// never configured, which defaults to all non-special (no-role) folders.
const ownMailboxes = useMemo(() => mailboxes.filter(m => !m.isShared), [mailboxes]);
const allMailSelected = new Set(
allMailFolderIds === null
? ownMailboxes.filter(m => !m.role).map(m => m.id)
: allMailFolderIds
);
const toggleAllMailFolder = (id: string) => {
const next = new Set(allMailSelected);
if (next.has(id)) next.delete(id);
else next.add(id);
updateSetting('allMailFolderIds', ownMailboxes.filter(m => next.has(m.id)).map(m => m.id));
};
return ( return (
<SettingsSection title={t('title')} description={t('description')}> <SettingsSection title={t('title')} description={t('description')}>
@@ -209,6 +226,56 @@ export function LayoutSettings() {
</div> </div>
)} )}
{allMailViewAllowed && !isSettingHidden('enableAllMailView') && (
<SettingItem
label={t('all_mail.label')}
description={t('all_mail.description')}
locked={isSettingLocked('enableAllMailView')}
>
<ToggleSwitch
checked={enableAllMailView}
onChange={(v) => updateSetting('enableAllMailView', v)}
/>
</SettingItem>
)}
{allMailViewAllowed && enableAllMailView && (
<div className="ml-4 border-l-2 border-border pl-4 -mt-2 space-y-2">
<div>
<div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</div>
<div className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div>
</div>
{ownMailboxes.length === 0 ? (
<p className="text-xs text-muted-foreground">{t('all_mail.no_folders')}</p>
) : (
<div className="space-y-0.5">
{ownMailboxes.map((mb) => {
const checked = allMailSelected.has(mb.id);
return (
<button
key={mb.id}
type="button"
onClick={() => toggleAllMailFolder(mb.id)}
className="w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md hover:bg-muted/50 text-left"
role="checkbox"
aria-checked={checked}
>
<span className={cn(
"flex items-center justify-center w-4 h-4 rounded border flex-shrink-0 transition-colors",
checked ? "bg-primary border-primary text-primary-foreground" : "border-border"
)}>
{checked && <Check className="w-3 h-3" />}
</span>
<Folder className={cn("w-4 h-4 flex-shrink-0", mb.role ? "text-primary" : "text-muted-foreground")} />
<span className="text-sm text-foreground truncate">{mb.name}</span>
</button>
);
})}
</div>
)}
</div>
)}
<SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}> <SettingItem label={t('pro_interface.label')} description={t('pro_interface.description')}>
<ToggleSwitch <ToggleSwitch
checked={proInterface} checked={proInterface}
+2
View File
@@ -59,6 +59,7 @@ export interface FeatureGates {
hoverActionsConfigEnabled: boolean; hoverActionsConfigEnabled: boolean;
filesEnabled: boolean; filesEnabled: boolean;
contactsEnabled: boolean; contactsEnabled: boolean;
allMailViewEnabled: boolean;
} }
export const DEFAULT_FEATURE_GATES: FeatureGates = { export const DEFAULT_FEATURE_GATES: FeatureGates = {
@@ -79,6 +80,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
hoverActionsConfigEnabled: true, hoverActionsConfigEnabled: true,
filesEnabled: true, filesEnabled: true,
contactsEnabled: true, contactsEnabled: true,
allMailViewEnabled: false,
}; };
export interface ThemePolicy { export interface ThemePolicy {
+8
View File
@@ -871,3 +871,11 @@ export const UNIFIED_ROLE_BY_ID: Record<string, UnifiedMailboxRole> = Object.fro
export function isUnifiedMailboxId(id: string): boolean { export function isUnifiedMailboxId(id: string): boolean {
return id in UNIFIED_ROLE_BY_ID; return id in UNIFIED_ROLE_BY_ID;
} }
/**
* Virtual mailbox id for the gated "All Mail" view: every folder of a single
* account merged into one date-sorted list. Distinct from the unified mailbox
* ids above, which merge one role across multiple accounts. Which folders are
* included is a per-user setting (see `allMailFolderIds`).
*/
export const ALL_MAIL_MAILBOX_ID = '__all_mail__';
+7
View File
@@ -914,6 +914,13 @@
"description": "Zahrnout do sjednoceného zobrazení také sdílené/skupinové schránky." "description": "Zahrnout do sjednoceného zobrazení také sdílené/skupinové schránky."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Barevné ikony postranního panelu", "label": "Barevné ikony postranního panelu",
"description": "Obarví ikony složek a štítků podle typu (modré Doručené, červený Spam, zelené Odeslané atd.). Vypněte pro jednobarevný postranní panel." "description": "Obarví ikony složek a štítků podle typu (modré Doručené, červený Spam, zelené Odeslané atd.). Vypněte pro jednobarevný postranní panel."
+7
View File
@@ -917,6 +917,13 @@
"description": "Inkluder også delte/gruppepostkasser i den samlede visning." "description": "Inkluder også delte/gruppepostkasser i den samlede visning."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Farverige sidepane-ikoner", "label": "Farverige sidepane-ikoner",
"description": "Farvelæg mappe- og tag-ikoner efter type (blå indbakke, rød spam, grøn sendt osv.). Deaktivér for et monokromt sidepanel." "description": "Farvelæg mappe- og tag-ikoner efter type (blå indbakke, rød spam, grøn sendt osv.). Deaktivér for et monokromt sidepanel."
+7
View File
@@ -914,6 +914,13 @@
"description": "Gemeinsam genutzte/Gruppenpostfächer ebenfalls in die vereinheitlichte Ansicht aufnehmen." "description": "Gemeinsam genutzte/Gruppenpostfächer ebenfalls in die vereinheitlichte Ansicht aufnehmen."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Farbige Seitenleistensymbole", "label": "Farbige Seitenleistensymbole",
"description": "Ordner- und Tag-Symbole nach Typ einfärben (blauer Posteingang, roter Spam, grüner Gesendet usw.). Für eine monochrome Seitenleiste deaktivieren." "description": "Ordner- und Tag-Symbole nach Typ einfärben (blauer Posteingang, roter Spam, grüner Gesendet usw.). Für eine monochrome Seitenleiste deaktivieren."
+7
View File
@@ -917,6 +917,13 @@
"description": "Also merge shared/group inboxes into the unified view." "description": "Also merge shared/group inboxes into the unified view."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Colorful Sidebar Icons", "label": "Colorful Sidebar Icons",
"description": "Tint folder and tag icons by type (blue Inbox, red Junk, green Sent, etc.). Disable for a monochrome sidebar." "description": "Tint folder and tag icons by type (blue Inbox, red Junk, green Sent, etc.). Disable for a monochrome sidebar."
+7
View File
@@ -914,6 +914,13 @@
"description": "Incluir también los buzones compartidos o de grupo en la vista unificada." "description": "Incluir también los buzones compartidos o de grupo en la vista unificada."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Iconos de barra lateral a color", "label": "Iconos de barra lateral a color",
"description": "Colorea los iconos de carpetas y etiquetas según su tipo (azul para Bandeja de entrada, rojo para Spam, verde para Enviados, etc.). Desactívalo para una barra lateral monocroma." "description": "Colorea los iconos de carpetas y etiquetas según su tipo (azul para Bandeja de entrada, rojo para Spam, verde para Enviados, etc.). Desactívalo para una barra lateral monocroma."
+7
View File
@@ -914,6 +914,13 @@
"description": "Inclure également les boîtes partagées ou de groupe dans la vue unifiée." "description": "Inclure également les boîtes partagées ou de groupe dans la vue unifiée."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Icônes colorées dans la barre latérale", "label": "Icônes colorées dans la barre latérale",
"description": "Colore les icônes de dossiers et d'étiquettes par type (Boîte de réception en bleu, Indésirable en rouge, Envoyés en vert, etc.). Désactivez pour une barre latérale monochrome." "description": "Colore les icônes de dossiers et d'étiquettes par type (Boîte de réception en bleu, Indésirable en rouge, Envoyés en vert, etc.). Désactivez pour une barre latérale monochrome."
+7
View File
@@ -917,6 +917,13 @@
"description": "Megosztott/csoportos postafiókok egyesítése az egységes nézetbe." "description": "Megosztott/csoportos postafiókok egyesítése az egységes nézetbe."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Színes oldalsáv ikonok", "label": "Színes oldalsáv ikonok",
"description": "Mappa és címke ikonok színezése típus szerint (kék Beérkező, piros Spam, zöld Elküldött, stb.). Kapcsold ki az egyszínű oldalsávhoz." "description": "Mappa és címke ikonok színezése típus szerint (kék Beérkező, piros Spam, zöld Elküldött, stb.). Kapcsold ki az egyszínű oldalsávhoz."
+7
View File
@@ -914,6 +914,13 @@
"description": "Includi anche le caselle condivise o di gruppo nella vista unificata." "description": "Includi anche le caselle condivise o di gruppo nella vista unificata."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Icone colorate nella barra laterale", "label": "Icone colorate nella barra laterale",
"description": "Colora le icone di cartelle ed etichette per tipo (Posta in arrivo blu, Spam rosso, Inviati verde, ecc.). Disattiva per una barra laterale monocromatica." "description": "Colora le icone di cartelle ed etichette per tipo (Posta in arrivo blu, Spam rosso, Inviati verde, ecc.). Disattiva per una barra laterale monocromatica."
+7
View File
@@ -914,6 +914,13 @@
"description": "共有/グループ受信トレイも統合ビューに含めます。" "description": "共有/グループ受信トレイも統合ビューに含めます。"
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "カラフルなサイドバーアイコン", "label": "カラフルなサイドバーアイコン",
"description": "フォルダーとタグのアイコンを種類別に色分けします(受信トレイは青、迷惑メールは赤、送信済みは緑など)。モノクロのサイドバーにするには無効にしてください。" "description": "フォルダーとタグのアイコンを種類別に色分けします(受信トレイは青、迷惑メールは赤、送信済みは緑など)。モノクロのサイドバーにするには無効にしてください。"
+7
View File
@@ -914,6 +914,13 @@
"description": "공유/그룹 받은편지함도 통합 보기에 포함합니다." "description": "공유/그룹 받은편지함도 통합 보기에 포함합니다."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "컬러풀한 사이드바 아이콘", "label": "컬러풀한 사이드바 아이콘",
"description": "폴더와 태그 아이콘을 유형별로 색상 표시합니다(받은편지함 파란색, 스팸 빨간색, 보낸편지함 녹색 등). 모노크롬 사이드바를 원하면 비활성화하세요." "description": "폴더와 태그 아이콘을 유형별로 색상 표시합니다(받은편지함 파란색, 스팸 빨간색, 보낸편지함 녹색 등). 모노크롬 사이드바를 원하면 비활성화하세요."
+7
View File
@@ -914,6 +914,13 @@
"description": "Iekļaut apvienotajā skatā arī koplietotās/grupas pastkastes." "description": "Iekļaut apvienotajā skatā arī koplietotās/grupas pastkastes."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Krāsainas sānjoslas ikonas", "label": "Krāsainas sānjoslas ikonas",
"description": "Iekrāsojiet mapju un birku ikonas pēc to veida (zila Iesūtne, sarkana Mēstules, zaļa Nosūtītie utt.). Atspējojiet, lai iegūtu vienkrāsainu sānjoslu." "description": "Iekrāsojiet mapju un birku ikonas pēc to veida (zila Iesūtne, sarkana Mēstules, zaļa Nosūtītie utt.). Atspējojiet, lai iegūtu vienkrāsainu sānjoslu."
+7
View File
@@ -914,6 +914,13 @@
"description": "Gedeelde/groepspostvakken ook in de gecombineerde weergave opnemen." "description": "Gedeelde/groepspostvakken ook in de gecombineerde weergave opnemen."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Gekleurde zijbalkpictogrammen", "label": "Gekleurde zijbalkpictogrammen",
"description": "Kleur map- en tagpictogrammen op type (blauw Postvak IN, rood Spam, groen Verzonden, enz.). Schakel uit voor een monochrome zijbalk." "description": "Kleur map- en tagpictogrammen op type (blauw Postvak IN, rood Spam, groen Verzonden, enz.). Schakel uit voor een monochrome zijbalk."
+7
View File
@@ -914,6 +914,13 @@
"description": "Dodaj również udostępnione/grupowe skrzynki do widoku wspólnego." "description": "Dodaj również udostępnione/grupowe skrzynki do widoku wspólnego."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Kolorowe ikony paska bocznego", "label": "Kolorowe ikony paska bocznego",
"description": "Koloruj ikony folderów i tagów według typu (niebieska Skrzynka odbiorcza, czerwona Spam, zielona Wysłane itp.). Wyłącz, aby uzyskać monochromatyczny pasek boczny." "description": "Koloruj ikony folderów i tagów według typu (niebieska Skrzynka odbiorcza, czerwona Spam, zielona Wysłane itp.). Wyłącz, aby uzyskać monochromatyczny pasek boczny."
+7
View File
@@ -914,6 +914,13 @@
"description": "Incluir também as caixas partilhadas ou de grupo na vista unificada." "description": "Incluir também as caixas partilhadas ou de grupo na vista unificada."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Ícones coloridos na barra lateral", "label": "Ícones coloridos na barra lateral",
"description": "Colorir ícones de pastas e etiquetas por tipo (Caixa de entrada azul, Spam vermelho, Enviados verde, etc.). Desative para uma barra lateral monocromática." "description": "Colorir ícones de pastas e etiquetas por tipo (Caixa de entrada azul, Spam vermelho, Enviados verde, etc.). Desative para uma barra lateral monocromática."
+7
View File
@@ -914,6 +914,13 @@
"description": "Также объединять общие/групповые ящики в едином представлении." "description": "Также объединять общие/групповые ящики в едином представлении."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Цветные значки боковой панели", "label": "Цветные значки боковой панели",
"description": "Окрашивать значки папок и тегов по типу (синий «Входящие», красный «Спам», зелёный «Отправленные» и т. д.). Отключите для монохромной боковой панели." "description": "Окрашивать значки папок и тегов по типу (синий «Входящие», красный «Спам», зелёный «Отправленные» и т. д.). Отключите для монохромной боковой панели."
+7
View File
@@ -914,6 +914,13 @@
"description": "Paylaşılan/grup gelen kutularını da birleşik görünüme dahil et." "description": "Paylaşılan/grup gelen kutularını da birleşik görünüme dahil et."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Renkli Kenar Çubuğu Simgeleri", "label": "Renkli Kenar Çubuğu Simgeleri",
"description": "Klasör ve etiket simgelerini türe göre renklendir (mavi Gelen Kutusu, kırmızı İstem Dışı vb.). Tek renkli kenar çubuğu için kapatın." "description": "Klasör ve etiket simgelerini türe göre renklendir (mavi Gelen Kutusu, kırmızı İstem Dışı vb.). Tek renkli kenar çubuğu için kapatın."
+7
View File
@@ -914,6 +914,13 @@
"description": "Також об'єднувати спільні/групові скриньки у спільному перегляді." "description": "Також об'єднувати спільні/групові скриньки у спільному перегляді."
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "Кольорові значки бічної панелі", "label": "Кольорові значки бічної панелі",
"description": "Забарвлюйте значки папок і тегів за типом (синя «Вхідні», червоний «Спам», зелена «Надіслані» тощо). Вимкніть для монохромної бічної панелі." "description": "Забарвлюйте значки папок і тегів за типом (синя «Вхідні», червоний «Спам», зелена «Надіслані» тощо). Вимкніть для монохромної бічної панелі."
+7
View File
@@ -914,6 +914,13 @@
"description": "在统一视图中也合并共享/群组收件箱。" "description": "在统一视图中也合并共享/群组收件箱。"
} }
}, },
"all_mail": {
"label": "All Mail",
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
"folders_label": "Folders in All Mail",
"folders_description": "Choose which folders are merged into the All Mail view.",
"no_folders": "No folders available."
},
"colorful_sidebar_icons": { "colorful_sidebar_icons": {
"label": "彩色侧边栏图标", "label": "彩色侧边栏图标",
"description": "按类型为文件夹和标签图标着色(蓝色收件箱、红色垃圾邮件、绿色已发送等)。禁用以获得单色侧边栏。" "description": "按类型为文件夹和标签图标着色(蓝色收件箱、红色垃圾邮件、绿色已发送等)。禁用以获得单色侧边栏。"
+70 -7
View File
@@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { Email, Mailbox, StateChange, ScheduledEmail, SendEmailResult } from "@/lib/jmap/types"; import { Email, Mailbox, StateChange, ScheduledEmail, SendEmailResult, ALL_MAIL_MAILBOX_ID } from "@/lib/jmap/types";
import type { UnifiedMailboxRole } from "@/lib/jmap/types"; import type { UnifiedMailboxRole } from "@/lib/jmap/types";
import type { IJMAPClient } from "@/lib/jmap/client-interface"; import type { IJMAPClient } from "@/lib/jmap/client-interface";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
@@ -315,6 +315,33 @@ function resolveActionMailboxes(): Mailbox[] {
return state.mailboxes; return state.mailboxes;
} }
/**
* Resolves the JMAP mailbox ids that make up the gated "All Mail" view for the
* active/viewing account. Honors the per-user `allMailFolderIds` setting; when
* unset (null) it defaults to every non-special (no-role) folder. Shared
* folders are excluded - All Mail is scoped to a single account. Returns
* JMAP-side ids (originalId for namespaced mailboxes).
*/
function resolveAllMailJmapIds(): string[] {
const mailboxes = resolveActionMailboxes().filter((mb) => !mb.isShared);
const configured = useSettingsStore.getState().allMailFolderIds;
const selected = configured === null
? mailboxes.filter((mb) => !mb.role)
: mailboxes.filter((mb) => configured.includes(mb.id));
return selected.map((mb) => mb.originalId || mb.id);
}
/**
* Builds the JMAP Email/query filter for the All Mail view from a set of
* mailbox ids - an OR of `inMailbox` conditions (or a single condition).
*/
function buildAllMailFilter(jmapMailboxIds: string[]): Record<string, unknown> {
if (jmapMailboxIds.length === 1) {
return { inMailbox: jmapMailboxIds[0] };
}
return { operator: 'OR', conditions: jmapMailboxIds.map((id) => ({ inMailbox: id })) };
}
/** /**
* Resolves the JMAP client, mailbox list, and JMAP accountId to use for a * Resolves the JMAP client, mailbox list, and JMAP accountId to use for a
* single-email action. * single-email action.
@@ -658,6 +685,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
// doesn't exist in the fetched list (e.g. after an account switch) // doesn't exist in the fetched list (e.g. after an account switch)
const currentSelectedMailbox = get().selectedMailbox; const currentSelectedMailbox = get().selectedMailbox;
const selectionValid = currentSelectedMailbox === VIRTUAL_SCHEDULED_MAILBOX_ID const selectionValid = currentSelectedMailbox === VIRTUAL_SCHEDULED_MAILBOX_ID
|| currentSelectedMailbox === ALL_MAIL_MAILBOX_ID
|| (currentSelectedMailbox && mailboxes.some(m => m.id === currentSelectedMailbox)); || (currentSelectedMailbox && mailboxes.some(m => m.id === currentSelectedMailbox));
const loadingPatch = isInitialLoad ? { isLoading: false } : {}; const loadingPatch = isInitialLoad ? { isLoading: false } : {};
if (!selectionValid) { if (!selectionValid) {
@@ -715,6 +743,24 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
await get().fetchScheduledEmails(client); await get().fetchScheduledEmails(client);
return; return;
} }
if (targetMailboxId === ALL_MAIL_MAILBOX_ID) {
const jmapIds = resolveAllMailJmapIds();
if (jmapIds.length === 0) {
set({ emails: [], hasMoreEmails: false, totalEmails: 0, isLoading: false });
return;
}
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
const result = await resolveActionClient(client).advancedSearchEmails(
buildAllMailFilter(jmapIds), undefined, emailsPerPage, 0,
);
set({
emails: annotateScheduledEmails(result.emails, get().scheduledSubmissionByEmailId),
hasMoreEmails: result.hasMore,
totalEmails: result.total,
isLoading: false,
});
return;
}
const effectiveClient = resolveActionClient(client); const effectiveClient = resolveActionClient(client);
// Find the mailbox to get its accountId (for shared folder support) // Find the mailbox to get its accountId (for shared folder support)
@@ -822,7 +868,21 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
const { searchFilters } = get(); const { searchFilters } = get();
const hasFilters = !isFilterEmpty(searchFilters); const hasFilters = !isFilterEmpty(searchFilters);
if (searchQuery || hasFilters) { if (selectedMailbox === ALL_MAIL_MAILBOX_ID) {
if (searchQuery || hasFilters) {
// Search within All Mail spans the whole account (no inMailbox).
result = hasFilters
? await effectiveClient.advancedSearchEmails(buildJMAPFilter(searchQuery, searchFilters, undefined), undefined, emailsPerPage, position)
: await effectiveClient.searchEmails(searchQuery, undefined, undefined, emailsPerPage, position);
} else {
const jmapIds = resolveAllMailJmapIds();
if (jmapIds.length === 0) {
set({ hasMoreEmails: false, isLoadingMore: false });
return;
}
result = await effectiveClient.advancedSearchEmails(buildAllMailFilter(jmapIds), undefined, emailsPerPage, position);
}
} else if (searchQuery || hasFilters) {
const mailboxes = resolveActionMailboxes(); const mailboxes = resolveActionMailboxes();
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
const jmapMailboxId = mailbox?.originalId || selectedMailbox; const jmapMailboxId = mailbox?.originalId || selectedMailbox;
@@ -1484,14 +1544,16 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
return; return;
} }
// Get the current mailbox to scope the search // Get the current mailbox to scope the search. In the All Mail view the
// search spans every folder of the account (no inMailbox constraint).
const selectedMailbox = get().selectedMailbox; const selectedMailbox = get().selectedMailbox;
const isAllMail = selectedMailbox === ALL_MAIL_MAILBOX_ID;
const mailboxes = resolveActionMailboxes(); const mailboxes = resolveActionMailboxes();
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
// Use originalId for shared mailboxes // Use originalId for shared mailboxes
const jmapMailboxId = mailbox?.originalId || selectedMailbox; const jmapMailboxId = isAllMail ? undefined : (mailbox?.originalId || selectedMailbox);
// Only pass accountId for shared mailboxes, not for primary account // Only pass accountId for shared mailboxes, not for primary account
const accountId = mailbox?.isShared ? mailbox.accountId : undefined; const accountId = isAllMail ? undefined : (mailbox?.isShared ? mailbox.accountId : undefined);
const result = await resolveActionClient(client).searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0); const result = await resolveActionClient(client).searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters }); const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
@@ -1559,9 +1621,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
return; return;
} }
const isAllMail = selectedMailbox === ALL_MAIL_MAILBOX_ID;
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
const jmapMailboxId = mailbox?.originalId || selectedMailbox; const jmapMailboxId = isAllMail ? undefined : (mailbox?.originalId || selectedMailbox);
const accountId = mailbox?.isShared ? mailbox.accountId : undefined; const accountId = isAllMail ? undefined : (mailbox?.isShared ? mailbox.accountId : undefined);
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId); const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
const result = await resolveActionClient(client).advancedSearchEmails(filter, accountId, emailsPerPage, 0); const result = await resolveActionClient(client).advancedSearchEmails(filter, accountId, emailsPerPage, 0);
+13
View File
@@ -215,6 +215,13 @@ interface SettingsState {
enableUnifiedMailbox: boolean; enableUnifiedMailbox: boolean;
includeGroupInUnified: boolean; includeGroupInUnified: boolean;
// All Mail view (gated): user toggle (like the unified mailbox) plus the set
// of folder ids merged into the virtual "All Mail" mailbox. `null` = never
// configured, in which case the view defaults to all non-special (no-role)
// folders of the active account.
enableAllMailView: boolean;
allMailFolderIds: string[] | null;
// Email Display // Email Display
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
@@ -398,6 +405,10 @@ const DEFAULT_SETTINGS = {
enableUnifiedMailbox: false, enableUnifiedMailbox: false,
includeGroupInUnified: false, includeGroupInUnified: false,
// All Mail view (gated)
enableAllMailView: false,
allMailFolderIds: null as string[] | null,
// Email Display // Email Display
disableThreading: false, disableThreading: false,
@@ -570,6 +581,8 @@ export const useSettingsStore = create<SettingsState>()(
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced. // (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
enableUnifiedMailbox: state.enableUnifiedMailbox, enableUnifiedMailbox: state.enableUnifiedMailbox,
includeGroupInUnified: state.includeGroupInUnified, includeGroupInUnified: state.includeGroupInUnified,
enableAllMailView: state.enableAllMailView,
allMailFolderIds: state.allMailFolderIds,
senderFavicons: state.senderFavicons, senderFavicons: state.senderFavicons,
showAvatarsInJunk: state.showAvatarsInJunk, showAvatarsInJunk: state.showAvatarsInJunk,
colorfulSidebarIcons: state.colorfulSidebarIcons, colorfulSidebarIcons: state.colorfulSidebarIcons,