Merge remote-tracking branch 'origin/main' into feat/all-mail-cross-account-views
# Conflicts: # stores/settings-store.ts
This commit is contained in:
@@ -3194,7 +3194,17 @@ export default function Home() {
|
|||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
onReplyAll={handleReplyAll}
|
onReplyAll={handleReplyAll}
|
||||||
onForward={handleForward}
|
onForward={handleForward}
|
||||||
onDelete={() => handleDelete()}
|
onDelete={() => {
|
||||||
|
// Deleting the open message returns to the list (Gmail-style),
|
||||||
|
// not the next email — unless the user turned the setting off.
|
||||||
|
// Deselect first so the store's remove-and-advance sees no
|
||||||
|
// selection and doesn't auto-open the next message.
|
||||||
|
const target = selectedEmail;
|
||||||
|
if (useSettingsStore.getState().returnToListAfterAction) {
|
||||||
|
handleMobileBack();
|
||||||
|
}
|
||||||
|
handleDelete(target);
|
||||||
|
}}
|
||||||
onArchive={() => handleArchive()}
|
onArchive={() => handleArchive()}
|
||||||
onToggleStar={handleToggleStar}
|
onToggleStar={handleToggleStar}
|
||||||
onSetColorTag={handleSetColorTag}
|
onSetColorTag={handleSetColorTag}
|
||||||
@@ -3203,6 +3213,12 @@ export default function Home() {
|
|||||||
onMarkAsRead={async (emailId, read) => {
|
onMarkAsRead={async (emailId, read) => {
|
||||||
if (client) {
|
if (client) {
|
||||||
await markAsRead(client, emailId, read);
|
await markAsRead(client, emailId, read);
|
||||||
|
// Marking the open message unread returns to the list
|
||||||
|
// (Gmail-style, gated on returnToListAfterAction). Staying
|
||||||
|
// in the reading pane would just re-mark it read on view.
|
||||||
|
if (!read && useSettingsStore.getState().returnToListAfterAction) {
|
||||||
|
handleMobileBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDownloadAttachment={handleDownloadAttachment}
|
onDownloadAttachment={handleDownloadAttachment}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export function LayoutSettings() {
|
|||||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
||||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||||
const accounts = useAccountStore(s => s.accounts);
|
const accounts = useAccountStore(s => s.accounts);
|
||||||
|
const activeAccountId = useAccountStore(s => s.activeAccountId);
|
||||||
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');
|
const allMailViewAllowed = isFeatureEnabled('allMailViewEnabled');
|
||||||
@@ -131,20 +132,32 @@ export function LayoutSettings() {
|
|||||||
{ setting: 'enableCrossAllView', value: enableCrossAllView, allowed: isFeatureEnabled('crossAllViewEnabled'), labelKey: 'cross_all.label', descKey: 'cross_all.description' },
|
{ setting: 'enableCrossAllView', value: enableCrossAllView, allowed: isFeatureEnabled('crossAllViewEnabled'), labelKey: 'cross_all.label', descKey: 'cross_all.description' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Own (non-shared) folders and the current All Mail selection. `null` =
|
// Own (non-shared) folders and the active account's All Mail selection. The
|
||||||
// never configured, which defaults to all non-special (no-role) folders.
|
// 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 ownMailboxes = useMemo(() => mailboxes.filter(m => !m.isShared), [mailboxes]);
|
||||||
|
const currentAllMailEntry = activeAccountId ? allMailFolderIds[activeAccountId] : undefined;
|
||||||
const allMailSelected = new Set(
|
const allMailSelected = new Set(
|
||||||
allMailFolderIds === null
|
currentAllMailEntry === undefined
|
||||||
? ownMailboxes.filter(m => !m.role).map(m => m.id)
|
? ownMailboxes.filter(m => !m.role).map(m => m.id)
|
||||||
: allMailFolderIds
|
: currentAllMailEntry
|
||||||
);
|
);
|
||||||
const toggleAllMailFolder = (id: string) => {
|
const toggleAllMailFolder = (id: string) => {
|
||||||
|
if (!activeAccountId) return;
|
||||||
const next = new Set(allMailSelected);
|
const next = new Set(allMailSelected);
|
||||||
if (next.has(id)) next.delete(id);
|
if (next.has(id)) next.delete(id);
|
||||||
else next.add(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 (
|
return (
|
||||||
<SettingsSection title={t('title')} description={t('description')}>
|
<SettingsSection title={t('title')} description={t('description')}>
|
||||||
@@ -270,6 +283,9 @@ export function LayoutSettings() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</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 className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div>
|
||||||
|
{allMailAccountHint && (
|
||||||
|
<div className="text-xs italic text-muted-foreground mt-0.5">{allMailAccountHint}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ownMailboxes.length === 0 ? (
|
{ownMailboxes.length === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground">{t('all_mail.no_folders')}</p>
|
<p className="text-xs text-muted-foreground">{t('all_mail.no_folders')}</p>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function ReadingSettings() {
|
|||||||
markAsReadDelay,
|
markAsReadDelay,
|
||||||
deleteAction,
|
deleteAction,
|
||||||
permanentlyDeleteJunk,
|
permanentlyDeleteJunk,
|
||||||
|
returnToListAfterAction,
|
||||||
showPreview,
|
showPreview,
|
||||||
mailLayout,
|
mailLayout,
|
||||||
disableThreading,
|
disableThreading,
|
||||||
@@ -176,6 +177,13 @@ export function ReadingSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label={t('return_to_list_after_action.label')} description={t('return_to_list_after_action.description')}>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={returnToListAfterAction}
|
||||||
|
onChange={(checked) => updateSetting('returnToListAfterAction', checked)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
{!isSettingHidden('showPreview') && (
|
{!isSettingHidden('showPreview') && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t('show_preview.label')}
|
label={t('show_preview.label')}
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ describe('stripSubjectPrefixes', () => {
|
|||||||
expect(stripSubjectPrefixes('Re: AW: WG: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re: AW: WG: foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('strips the Outlook [N] counter and Eudora *N counter', () => {
|
it('strips the Outlook [N] and Eudora *N counters', () => {
|
||||||
expect(stripSubjectPrefixes('Re[2]: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re[2]: foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('Re*3: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Re*3: foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('Re*: foo')).toBe('foo');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is case-insensitive and idempotent', () => {
|
it('is case-insensitive and idempotent', () => {
|
||||||
@@ -21,47 +20,37 @@ describe('stripSubjectPrefixes', () => {
|
|||||||
expect(stripSubjectPrefixes(stripSubjectPrefixes('RE: Re: foo'))).toBe('foo');
|
expect(stripSubjectPrefixes(stripSubjectPrefixes('RE: Re: foo'))).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('strips a Cyrillic reply token', () => {
|
it('strips a Cyrillic token and an ASCII-colon Chinese token', () => {
|
||||||
expect(stripSubjectPrefixes('Ответ: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('Ответ: foo')).toBe('foo');
|
||||||
});
|
|
||||||
|
|
||||||
it('strips a Chinese token followed by an ASCII colon', () => {
|
|
||||||
expect(stripSubjectPrefixes('回复: foo')).toBe('foo');
|
expect(stripSubjectPrefixes('回复: foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CHARACTERISATION: does NOT strip a token followed by a full-width colon', () => {
|
it('strips a token followed by a full-width colon (CJK clients)', () => {
|
||||||
// The colon in the regex is ASCII ":"; a full-width ":" (U+FF1A), as some
|
expect(stripSubjectPrefixes('回复:foo')).toBe('foo');
|
||||||
// CJK mail clients emit, is left untouched. Likely a bug — see follow-ups.
|
expect(stripSubjectPrefixes('回覆:foo')).toBe('foo');
|
||||||
expect(stripSubjectPrefixes('回复:foo')).toBe('回复:foo');
|
expect(stripSubjectPrefixes('Re:foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does NOT strip a bare single-letter "R:" (would eat real subjects)', () => {
|
it('still does not strip a bare single-letter "R:"', () => {
|
||||||
expect(stripSubjectPrefixes('R: budget 2024')).toBe('R: budget 2024');
|
expect(stripSubjectPrefixes('R: budget 2024')).toBe('R: budget 2024');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns "" for empty / null / undefined', () => {
|
it('returns "" for empty / null / undefined and leaves clean subjects alone', () => {
|
||||||
expect(stripSubjectPrefixes('')).toBe('');
|
expect(stripSubjectPrefixes('')).toBe('');
|
||||||
expect(stripSubjectPrefixes(null)).toBe('');
|
expect(stripSubjectPrefixes(null)).toBe('');
|
||||||
expect(stripSubjectPrefixes(undefined)).toBe('');
|
expect(stripSubjectPrefixes(undefined)).toBe('');
|
||||||
});
|
|
||||||
|
|
||||||
it('leaves a prefix-free subject untouched', () => {
|
|
||||||
expect(stripSubjectPrefixes('foo')).toBe('foo');
|
expect(stripSubjectPrefixes('foo')).toBe('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildReplySubject / buildForwardSubject', () => {
|
describe('buildReplySubject / buildForwardSubject', () => {
|
||||||
it('replaces an existing prefix chain with the given prefix', () => {
|
it('replaces a prefix chain (incl. a full-width colon) with the given prefix', () => {
|
||||||
expect(buildReplySubject('AW: WG: foo', 'Re:')).toBe('Re: foo');
|
expect(buildReplySubject('回复:foo', 'Re:')).toBe('Re: foo');
|
||||||
expect(buildForwardSubject('Re: foo', 'Fwd:')).toBe('Fwd: foo');
|
expect(buildForwardSubject('Re: foo', 'Fwd:')).toBe('Fwd: foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prepends the prefix to a prefix-free subject', () => {
|
it('prepends to a clean subject and returns the bare prefix for empty input', () => {
|
||||||
expect(buildReplySubject('foo', 'AW:')).toBe('AW: foo');
|
expect(buildReplySubject('foo', 'AW:')).toBe('AW: foo');
|
||||||
});
|
|
||||||
|
|
||||||
it('returns just the bare prefix for an empty subject', () => {
|
|
||||||
expect(buildReplySubject('', 'AW:')).toBe('AW:');
|
expect(buildReplySubject('', 'AW:')).toBe('AW:');
|
||||||
expect(buildForwardSubject(null, 'Fwd:')).toBe('Fwd:');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ function buildPrefixRegex(tokens: string[]): RegExp {
|
|||||||
// Sort by length DESC so longer tokens (e.g. "Пересл") win over their
|
// Sort by length DESC so longer tokens (e.g. "Пересл") win over their
|
||||||
// shorter prefixes (e.g. "Пер") during alternation matching.
|
// shorter prefixes (e.g. "Пер") during alternation matching.
|
||||||
escaped.sort((a, b) => b.length - a.length);
|
escaped.sort((a, b) => b.length - a.length);
|
||||||
|
// Accept both the ASCII colon and the full-width colon ":" (U+FF1A) that CJK
|
||||||
|
// mail clients emit after a localized prefix (e.g. "回复:foo").
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
`^\\s*(?:${escaped.join("|")})(?:\\[\\d+\\]|\\*\\d*)?\\s*:\\s*`,
|
`^\\s*(?:${escaped.join("|")})(?:\\[\\d+\\]|\\*\\d*)?\\s*[:\\uFF1A]\\s*`,
|
||||||
"i",
|
"i",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Vypnuto",
|
"off": "Vypnuto",
|
||||||
"seconds": "{seconds} sekund",
|
"seconds": "{seconds} sekund",
|
||||||
"unsupported": "Aktuální účet neoznamuje podporu odloženého odeslání. Nastavení zůstane uloženo pro jiné účty."
|
"unsupported": "Aktuální účet neoznamuje podporu odloženého odeslání. Nastavení zůstane uloženo pro jiné účty."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -925,6 +925,7 @@
|
|||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1245,6 +1246,10 @@
|
|||||||
"off": "Fra",
|
"off": "Fra",
|
||||||
"seconds": "{seconds} sekunder",
|
"seconds": "{seconds} sekunder",
|
||||||
"unsupported": "Den aktuelle konto annoncerer ikke understøttelse af forsinket afsendelse. Indstillingen gemmes stadig for andre konti."
|
"unsupported": "Den aktuelle konto annoncerer ikke understøttelse af forsinket afsendelse. Indstillingen gemmes stadig for andre konti."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Gilt für {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Aus",
|
"off": "Aus",
|
||||||
"seconds": "{seconds} Sekunden",
|
"seconds": "{seconds} Sekunden",
|
||||||
"unsupported": "Das aktuelle Konto meldet keine Unterstützung für verzögertes Senden. Die Einstellung bleibt für andere Konten gespeichert."
|
"unsupported": "Das aktuelle Konto meldet keine Unterstützung für verzögertes Senden. Die Einstellung bleibt für andere Konten gespeichert."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -925,6 +925,7 @@
|
|||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1245,6 +1246,10 @@
|
|||||||
"off": "Off",
|
"off": "Off",
|
||||||
"seconds": "{seconds} seconds",
|
"seconds": "{seconds} seconds",
|
||||||
"unsupported": "The current account does not advertise delayed-send support. The setting is still saved for other accounts."
|
"unsupported": "The current account does not advertise delayed-send support. The setting is still saved for other accounts."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Desactivado",
|
"off": "Desactivado",
|
||||||
"seconds": "{seconds} segundos",
|
"seconds": "{seconds} segundos",
|
||||||
"unsupported": "La cuenta actual no anuncia compatibilidad con envío demorado. La opción seguirá guardada para otras cuentas."
|
"unsupported": "La cuenta actual no anuncia compatibilidad con envío demorado. La opción seguirá guardada para otras cuentas."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Désactivé",
|
"off": "Désactivé",
|
||||||
"seconds": "{seconds} secondes",
|
"seconds": "{seconds} secondes",
|
||||||
"unsupported": "Le compte actuel n’annonce pas la prise en charge de l’envoi différé. Le réglage reste enregistré pour d’autres comptes."
|
"unsupported": "Le compte actuel n’annonce pas la prise en charge de l’envoi différé. Le réglage reste enregistré pour d’autres comptes."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -925,6 +925,7 @@
|
|||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1245,6 +1246,10 @@
|
|||||||
"off": "Kikapcsolva",
|
"off": "Kikapcsolva",
|
||||||
"seconds": "{seconds} másodperc",
|
"seconds": "{seconds} másodperc",
|
||||||
"unsupported": "A jelenlegi fiók nem támogatja a késleltetett küldést. A beállítás más fiókokhoz elmentésre kerül."
|
"unsupported": "A jelenlegi fiók nem támogatja a késleltetett küldést. A beállítás más fiókokhoz elmentésre kerül."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Disattivato",
|
"off": "Disattivato",
|
||||||
"seconds": "{seconds} secondi",
|
"seconds": "{seconds} secondi",
|
||||||
"unsupported": "L’account corrente non dichiara il supporto all’invio ritardato. L’impostazione resta salvata per altri account."
|
"unsupported": "L’account corrente non dichiara il supporto all’invio ritardato. L’impostazione resta salvata per altri account."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "オフ",
|
"off": "オフ",
|
||||||
"seconds": "{seconds} 秒",
|
"seconds": "{seconds} 秒",
|
||||||
"unsupported": "現在のアカウントは遅延送信のサポートを通知していません。この設定は他のアカウント用に保存されます。"
|
"unsupported": "現在のアカウントは遅延送信のサポートを通知していません。この設定は他のアカウント用に保存されます。"
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "끔",
|
"off": "끔",
|
||||||
"seconds": "{seconds}초",
|
"seconds": "{seconds}초",
|
||||||
"unsupported": "현재 계정은 지연 보내기 지원을 알리지 않습니다. 설정은 다른 계정을 위해 계속 저장됩니다."
|
"unsupported": "현재 계정은 지연 보내기 지원을 알리지 않습니다. 설정은 다른 계정을 위해 계속 저장됩니다."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Izslēgts",
|
"off": "Izslēgts",
|
||||||
"seconds": "{seconds} sekundes",
|
"seconds": "{seconds} sekundes",
|
||||||
"unsupported": "Pašreizējais konts neziņo par aizturētas sūtīšanas atbalstu. Iestatījums joprojām tiks saglabāts citiem kontiem."
|
"unsupported": "Pašreizējais konts neziņo par aizturētas sūtīšanas atbalstu. Iestatījums joprojām tiks saglabāts citiem kontiem."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Uit",
|
"off": "Uit",
|
||||||
"seconds": "{seconds} seconden",
|
"seconds": "{seconds} seconden",
|
||||||
"unsupported": "Het huidige account meldt geen ondersteuning voor vertraagd verzenden. De instelling blijft opgeslagen voor andere accounts."
|
"unsupported": "Het huidige account meldt geen ondersteuning voor vertraagd verzenden. De instelling blijft opgeslagen voor andere accounts."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Wyłączone",
|
"off": "Wyłączone",
|
||||||
"seconds": "{seconds} sekund",
|
"seconds": "{seconds} sekund",
|
||||||
"unsupported": "Bieżące konto nie zgłasza obsługi opóźnionej wysyłki. Ustawienie pozostanie zapisane dla innych kont."
|
"unsupported": "Bieżące konto nie zgłasza obsługi opóźnionej wysyłki. Ustawienie pozostanie zapisane dla innych kont."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Desativado",
|
"off": "Desativado",
|
||||||
"seconds": "{seconds} segundos",
|
"seconds": "{seconds} segundos",
|
||||||
"unsupported": "A conta atual não anuncia suporte a envio atrasado. A configuração continuará salva para outras contas."
|
"unsupported": "A conta atual não anuncia suporte a envio atrasado. A configuração continuará salva para outras contas."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -925,6 +925,7 @@
|
|||||||
"description": "Afișați o intrare „Toate mesajele” deasupra folderelor, care reunește mesajele din toate folderele acestui cont într-o singură listă.",
|
"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_label": "Dosare în „Toate mesajele”",
|
||||||
"folders_description": "Alegeți ce dosare să fie incluse în vizualizarea „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."
|
"no_folders": "Nu sunt disponibile foldere."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1245,6 +1246,10 @@
|
|||||||
"off": "Oprit",
|
"off": "Oprit",
|
||||||
"seconds": "{seconds} secunde",
|
"seconds": "{seconds} secunde",
|
||||||
"unsupported": "Contul curent nu indică faptul că acceptă trimiterea amânată. Setarea este totuși salvată pentru alte conturi."
|
"unsupported": "Contul curent nu indică faptul că acceptă trimiterea amânată. Setarea este totuși salvată pentru alte conturi."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Выкл.",
|
"off": "Выкл.",
|
||||||
"seconds": "{seconds} сек.",
|
"seconds": "{seconds} сек.",
|
||||||
"unsupported": "Текущая учетная запись не заявляет поддержку отложенной отправки. Настройка сохранится для других учетных записей."
|
"unsupported": "Текущая учетная запись не заявляет поддержку отложенной отправки. Настройка сохранится для других учетных записей."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Kapalı",
|
"off": "Kapalı",
|
||||||
"seconds": "{seconds} saniye",
|
"seconds": "{seconds} saniye",
|
||||||
"unsupported": "Geçerli hesap gecikmeli gönderim desteği bildirmiyor. Ayar diğer hesaplar için kaydedilmeye devam eder."
|
"unsupported": "Geçerli hesap gecikmeli gönderim desteği bildirmiyor. Ayar diğer hesaplar için kaydedilmeye devam eder."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "Вимкнено",
|
"off": "Вимкнено",
|
||||||
"seconds": "{seconds} с",
|
"seconds": "{seconds} с",
|
||||||
"unsupported": "Поточний обліковий запис не повідомляє про підтримку відкладеного надсилання. Налаштування залишиться збереженим для інших облікових записів."
|
"unsupported": "Поточний обліковий запис не повідомляє про підтримку відкладеного надсилання. Налаштування залишиться збереженим для інших облікових записів."
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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.",
|
"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_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
"colorful_sidebar_icons": {
|
"colorful_sidebar_icons": {
|
||||||
@@ -1242,6 +1243,10 @@
|
|||||||
"off": "关闭",
|
"off": "关闭",
|
||||||
"seconds": "{seconds} 秒",
|
"seconds": "{seconds} 秒",
|
||||||
"unsupported": "当前账户未声明支持延迟发送。该设置仍会为其他账户保存。"
|
"unsupported": "当前账户未声明支持延迟发送。该设置仍会为其他账户保存。"
|
||||||
|
},
|
||||||
|
"return_to_list_after_action": {
|
||||||
|
"label": "Return to list after delete or mark unread",
|
||||||
|
"description": "After deleting or marking the open message unread, go back to the message list instead of opening the next message."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
|||||||
@@ -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'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -331,15 +331,19 @@ function resolveActionMailboxes(): Mailbox[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the JMAP mailbox ids that make up the gated "All Mail" view for the
|
* 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
|
* active/viewing account. Honors that account's `allMailFolderIds` entry; when
|
||||||
* unset (null) it defaults to every non-special (no-role) folder. Shared
|
* not configured it defaults to every non-special (no-role) folder. Shared
|
||||||
* folders are excluded - All Mail is scoped to a single account. Returns
|
* folders are excluded - All Mail is scoped to a single account. Returns
|
||||||
* JMAP-side ids (originalId for namespaced mailboxes).
|
* JMAP-side ids (originalId for namespaced mailboxes).
|
||||||
*/
|
*/
|
||||||
function resolveAllMailJmapIds(): string[] {
|
function resolveAllMailJmapIds(): string[] {
|
||||||
const mailboxes = resolveActionMailboxes().filter((mb) => !mb.isShared);
|
const mailboxes = resolveActionMailboxes().filter((mb) => !mb.isShared);
|
||||||
const configured = useSettingsStore.getState().allMailFolderIds;
|
// Per-account selection: read the entry for the account the view is scoped to
|
||||||
const selected = configured === null
|
// (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) => !mb.role)
|
||||||
: mailboxes.filter((mb) => configured.includes(mb.id));
|
: mailboxes.filter((mb) => configured.includes(mb.id));
|
||||||
return selected.map((mb) => mb.originalId || mb.id);
|
return selected.map((mb) => mb.originalId || mb.id);
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const syncLog = (...args: unknown[]) => console.log('[SETTINGS_SYNC]', ...args);
|
|||||||
const syncWarn = (...args: unknown[]) => console.warn('[SETTINGS_SYNC]', ...args);
|
const syncWarn = (...args: unknown[]) => console.warn('[SETTINGS_SYNC]', ...args);
|
||||||
const syncError = (...args: unknown[]) => console.error('[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<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
// Settings sync state (module-level, not persisted)
|
// Settings sync state (module-level, not persisted)
|
||||||
let syncEnabled = false;
|
let syncEnabled = false;
|
||||||
let syncUsername: string | null = null;
|
let syncUsername: string | null = null;
|
||||||
@@ -137,6 +142,7 @@ interface SettingsState {
|
|||||||
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
||||||
deleteAction: DeleteAction;
|
deleteAction: DeleteAction;
|
||||||
permanentlyDeleteJunk: boolean; // Permanently delete emails from junk/spam instead of moving to trash
|
permanentlyDeleteJunk: boolean; // Permanently delete emails from junk/spam instead of moving to trash
|
||||||
|
returnToListAfterAction: boolean; // After delete / mark-unread in an open message, return to the list instead of opening the next message
|
||||||
showPreview: boolean;
|
showPreview: boolean;
|
||||||
mailLayout: MailLayout;
|
mailLayout: MailLayout;
|
||||||
emailsPerPage: number;
|
emailsPerPage: number;
|
||||||
@@ -226,10 +232,10 @@ interface SettingsState {
|
|||||||
enableCrossStarredView: boolean;
|
enableCrossStarredView: boolean;
|
||||||
enableCrossAllView: boolean;
|
enableCrossAllView: boolean;
|
||||||
|
|
||||||
// Folder ids merged into the virtual "All Mail" mailbox. `null` = never
|
// Per-account "All Mail" folder selection, keyed by AccountEntry.id. A
|
||||||
// configured, in which case the view defaults to all non-special (no-role)
|
// missing entry = "not configured" -> defaults to every no-role folder; an
|
||||||
// folders of the active account.
|
// explicit [] = "no folders". (Replaced the legacy global string[] | null.)
|
||||||
allMailFolderIds: string[] | null;
|
allMailFolderIds: Record<string, string[]>;
|
||||||
|
|
||||||
// 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
|
||||||
@@ -339,6 +345,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
markAsReadDelay: 0, // Instant
|
markAsReadDelay: 0, // Instant
|
||||||
deleteAction: 'trash' as DeleteAction,
|
deleteAction: 'trash' as DeleteAction,
|
||||||
permanentlyDeleteJunk: false,
|
permanentlyDeleteJunk: false,
|
||||||
|
returnToListAfterAction: true,
|
||||||
showPreview: true,
|
showPreview: true,
|
||||||
mailLayout: 'split' as MailLayout,
|
mailLayout: 'split' as MailLayout,
|
||||||
emailsPerPage: 50,
|
emailsPerPage: 50,
|
||||||
@@ -416,7 +423,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
|
|
||||||
// All Mail view (gated)
|
// All Mail view (gated)
|
||||||
enableAllMailView: false,
|
enableAllMailView: false,
|
||||||
allMailFolderIds: null as string[] | null,
|
allMailFolderIds: {} as Record<string, string[]>,
|
||||||
|
|
||||||
enableCrossUnreadView: false,
|
enableCrossUnreadView: false,
|
||||||
enableCrossStarredView: false,
|
enableCrossStarredView: false,
|
||||||
@@ -546,6 +553,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
firstDayOfWeek: state.firstDayOfWeek,
|
firstDayOfWeek: state.firstDayOfWeek,
|
||||||
markAsReadDelay: state.markAsReadDelay,
|
markAsReadDelay: state.markAsReadDelay,
|
||||||
deleteAction: state.deleteAction,
|
deleteAction: state.deleteAction,
|
||||||
|
returnToListAfterAction: state.returnToListAfterAction,
|
||||||
showPreview: state.showPreview,
|
showPreview: state.showPreview,
|
||||||
mailLayout: state.mailLayout,
|
mailLayout: state.mailLayout,
|
||||||
emailsPerPage: state.emailsPerPage,
|
emailsPerPage: state.emailsPerPage,
|
||||||
@@ -646,6 +654,11 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
set({ sendDelaySeconds: 0 });
|
set({ sendDelaySeconds: 0 });
|
||||||
return;
|
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)) {
|
if (DEVICE_LOCAL_SETTING_KEYS.has(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -836,7 +849,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: 4,
|
version: 5,
|
||||||
migrate: (persisted, version) => {
|
migrate: (persisted, version) => {
|
||||||
const state = persisted as Record<string, unknown>;
|
const state = persisted as Record<string, unknown>;
|
||||||
if (version < 2 && state.listDensity) {
|
if (version < 2 && state.listDensity) {
|
||||||
@@ -856,11 +869,25 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
if (version < 4) {
|
if (version < 4) {
|
||||||
state.dateFormat = 'smart';
|
state.dateFormat = 'smart';
|
||||||
}
|
}
|
||||||
|
// v5: allMailFolderIds went from a global `string[] | null` to a
|
||||||
|
// per-account `Record<accountId, string[]>`. 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;
|
return state as unknown as SettingsState;
|
||||||
},
|
},
|
||||||
onRehydrateStorage: () => {
|
onRehydrateStorage: () => {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
if (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);
|
applyFontSize(state.fontSize);
|
||||||
applyDensity(state.density);
|
applyDensity(state.density);
|
||||||
applyAnimations(state.animationsEnabled);
|
applyAnimations(state.animationsEnabled);
|
||||||
|
|||||||
Reference in New Issue
Block a user