diff --git a/components/settings/layout-settings.tsx b/components/settings/layout-settings.tsx index b069a0af..44ba417c 100644 --- a/components/settings/layout-settings.tsx +++ b/components/settings/layout-settings.tsx @@ -121,24 +121,37 @@ export function LayoutSettings() { const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore(); const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore(); const accounts = useAccountStore(s => s.accounts); + const activeAccountId = useAccountStore(s => s.activeAccountId); const mailboxes = useEmailStore(s => s.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. + // Own (non-shared) folders and the active account's All Mail selection. The + // selection is per account: a missing entry = never configured, which + // defaults to all no-role folders; an explicit [] = no folders. const ownMailboxes = useMemo(() => mailboxes.filter(m => !m.isShared), [mailboxes]); + const currentAllMailEntry = activeAccountId ? allMailFolderIds[activeAccountId] : undefined; const allMailSelected = new Set( - allMailFolderIds === null + currentAllMailEntry === undefined ? ownMailboxes.filter(m => !m.role).map(m => m.id) - : allMailFolderIds + : currentAllMailEntry ); const toggleAllMailFolder = (id: string) => { + if (!activeAccountId) return; 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)); + updateSetting('allMailFolderIds', { + ...allMailFolderIds, + [activeAccountId]: ownMailboxes.filter(m => next.has(m.id)).map(m => m.id), + }); }; + // Name the account the selection applies to, but only when more than one is + // logged in (otherwise it's unambiguous). + const activeAccount = accounts.find(a => a.id === activeAccountId); + const allMailAccountHint = accounts.length > 1 && activeAccount + ? t('all_mail.account_hint', { account: activeAccount.displayName || activeAccount.email }) + : null; return ( @@ -244,6 +257,9 @@ export function LayoutSettings() {
{t('all_mail.folders_label')}
{t('all_mail.folders_description')}
+ {allMailAccountHint && ( +
{allMailAccountHint}
+ )}
{ownMailboxes.length === 0 ? (

{t('all_mail.no_folders')}

diff --git a/locales/cs/common.json b/locales/cs/common.json index 1772e60c..60158ecd 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/da/common.json b/locales/da/common.json index d6987861..c1f05ead 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -922,6 +922,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/de/common.json b/locales/de/common.json index 3e8e8d57..5b25d5ee 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Gilt für {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/en/common.json b/locales/en/common.json index c2cace8c..01e3748e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -922,6 +922,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/es/common.json b/locales/es/common.json index 9c136e5c..07212004 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/fr/common.json b/locales/fr/common.json index aaa889cb..483f108e 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/hu/common.json b/locales/hu/common.json index 380a03d4..b2912dac 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -922,6 +922,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/it/common.json b/locales/it/common.json index 295a2df2..d20e4175 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/ja/common.json b/locales/ja/common.json index 14a786d6..33d992b8 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/ko/common.json b/locales/ko/common.json index f99baaaf..ac169ef3 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/lv/common.json b/locales/lv/common.json index b28caccc..37dee663 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 4e486450..54ffa80f 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/pl/common.json b/locales/pl/common.json index 64107616..e7fab6f8 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/pt/common.json b/locales/pt/common.json index 9a358039..7ea40a34 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/ro/common.json b/locales/ro/common.json index 60c20915..eba273d7 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -922,6 +922,7 @@ "description": "Afișați o intrare „Toate mesajele” deasupra folderelor, care reunește mesajele din toate folderele acestui cont într-o singură listă.", "folders_label": "Dosare în „Toate mesajele”", "folders_description": "Alegeți ce dosare să fie incluse în vizualizarea „Toate mesajele”.", + "account_hint": "Se aplică pentru {account}.", "no_folders": "Nu sunt disponibile foldere." }, "colorful_sidebar_icons": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 474cedc0..2336c3ed 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/tr/common.json b/locales/tr/common.json index 71bac54c..3f600c5c 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/uk/common.json b/locales/uk/common.json index 8cb6c033..d624d191 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/locales/zh/common.json b/locales/zh/common.json index a06642ec..12b67a51 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -919,6 +919,7 @@ "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.", + "account_hint": "Applies to {account}.", "no_folders": "No folders available." }, "colorful_sidebar_icons": { diff --git a/stores/__tests__/settings-store-all-mail.test.ts b/stores/__tests__/settings-store-all-mail.test.ts new file mode 100644 index 00000000..616a8b35 --- /dev/null +++ b/stores/__tests__/settings-store-all-mail.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useSettingsStore } from '../settings-store'; + +describe('settings-store per-account allMailFolderIds', () => { + beforeEach(() => { + useSettingsStore.setState({ allMailFolderIds: {} }); + }); + + it('defaults to an empty record (every account "not configured")', () => { + expect(useSettingsStore.getState().allMailFolderIds).toEqual({}); + }); + + it('keeps each account selection independent', () => { + useSettingsStore.setState({ + allMailFolderIds: { 'acct-1': ['inbox', 'archive'], 'acct-2': ['sent'] }, + }); + const map = useSettingsStore.getState().allMailFolderIds; + expect(map['acct-1']).toEqual(['inbox', 'archive']); + expect(map['acct-2']).toEqual(['sent']); + // A third account remains unconfigured (no entry). + expect(map['acct-3']).toBeUndefined(); + }); + + it('distinguishes explicit-empty ([] = no folders) from not-configured (undefined)', () => { + useSettingsStore.setState({ allMailFolderIds: { 'acct-1': [] } }); + const map = useSettingsStore.getState().allMailFolderIds; + expect(map['acct-1']).toEqual([]); // explicit "no folders" + expect(map['acct-2']).toBeUndefined(); // never configured + }); + + describe('importSettings legacy-shape guard', () => { + it('ignores a legacy global array shape', () => { + useSettingsStore.setState({ allMailFolderIds: { 'acct-1': ['inbox'] } }); + const ok = useSettingsStore.getState().importSettings( + JSON.stringify({ allMailFolderIds: ['inbox', 'sent'] }), + ); + expect(ok).toBe(true); + // unchanged - the array shape was rejected + expect(useSettingsStore.getState().allMailFolderIds).toEqual({ 'acct-1': ['inbox'] }); + }); + + it('ignores a null legacy value', () => { + useSettingsStore.setState({ allMailFolderIds: { 'acct-1': ['inbox'] } }); + useSettingsStore.getState().importSettings(JSON.stringify({ allMailFolderIds: null })); + expect(useSettingsStore.getState().allMailFolderIds).toEqual({ 'acct-1': ['inbox'] }); + }); + + it('accepts a proper per-account record', () => { + useSettingsStore.getState().importSettings( + JSON.stringify({ allMailFolderIds: { 'acct-9': ['inbox', 'spam'] } }), + ); + expect(useSettingsStore.getState().allMailFolderIds).toEqual({ 'acct-9': ['inbox', 'spam'] }); + }); + }); +}); diff --git a/stores/email-store.ts b/stores/email-store.ts index 498e6173..4a9688c3 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -321,15 +321,19 @@ function resolveActionMailboxes(): Mailbox[] { /** * 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 + * active/viewing account. Honors that account's `allMailFolderIds` entry; when + * not configured 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 + // Per-account selection: read the entry for the account the view is scoped to + // (the Pro viewing override, else the global active account). A missing entry + // = "not configured" -> all no-role folders; an explicit [] = no folders. + const accountId = useEmailStore.getState().viewingAccountId ?? useAuthStore.getState().activeAccountId; + const configured = accountId ? useSettingsStore.getState().allMailFolderIds[accountId] : undefined; + const selected = configured === undefined ? mailboxes.filter((mb) => !mb.role) : mailboxes.filter((mb) => configured.includes(mb.id)); return selected.map((mb) => mb.originalId || mb.id); diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 7ec486d6..c024fc15 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -15,6 +15,11 @@ const syncLog = (...args: unknown[]) => console.log('[SETTINGS_SYNC]', ...args); const syncWarn = (...args: unknown[]) => console.warn('[SETTINGS_SYNC]', ...args); const syncError = (...args: unknown[]) => console.error('[SETTINGS_SYNC]', ...args); +/** True for a non-null, non-array plain object (the allMailFolderIds map shape). */ +function isPlainRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + // Settings sync state (module-level, not persisted) let syncEnabled = false; let syncUsername: string | null = null; @@ -221,7 +226,10 @@ interface SettingsState { // configured, in which case the view defaults to all non-special (no-role) // folders of the active account. enableAllMailView: boolean; - allMailFolderIds: string[] | null; + // Per-account "All Mail" folder selection, keyed by AccountEntry.id. A + // missing entry = "not configured" -> defaults to every no-role folder; an + // explicit [] = "no folders". (Replaced the legacy global string[] | null.) + allMailFolderIds: Record; // Email Display disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation @@ -409,7 +417,7 @@ const DEFAULT_SETTINGS = { // All Mail view (gated) enableAllMailView: false, - allMailFolderIds: null as string[] | null, + allMailFolderIds: {} as Record, // Email Display disableThreading: false, @@ -633,6 +641,11 @@ export const useSettingsStore = create()( set({ sendDelaySeconds: 0 }); return; } + // Ignore a legacy global allMailFolderIds (string[] | null) or any + // non-record value - this build keys it per account. + if (key === 'allMailFolderIds' && !isPlainRecord(settings[key])) { + return; + } if (DEVICE_LOCAL_SETTING_KEYS.has(key)) { return; } @@ -823,7 +836,7 @@ export const useSettingsStore = create()( }), { name: 'settings-storage', - version: 4, + version: 5, migrate: (persisted, version) => { const state = persisted as Record; if (version < 2 && state.listDensity) { @@ -843,11 +856,25 @@ export const useSettingsStore = create()( if (version < 4) { state.dateFormat = 'smart'; } + // v5: allMailFolderIds went from a global `string[] | null` to a + // per-account `Record`. The legacy global list + // can't be attributed to a specific account here (the active account + // isn't known at migrate time), so it's dropped - each account starts + // "not configured" (defaults to all no-role folders). + if (version < 5 || !isPlainRecord(state.allMailFolderIds)) { + state.allMailFolderIds = {}; + } return state as unknown as SettingsState; }, onRehydrateStorage: () => { return (state) => { if (state) { + // Defensive: a legacy global array or any non-record value (e.g. + // synced from an older client) is coerced to an empty map so + // per-account consumers never see a non-record. + if (!isPlainRecord(state.allMailFolderIds)) { + state.allMailFolderIds = {}; + } applyFontSize(state.fontSize); applyDensity(state.density); applyAnimations(state.animationsEnabled);