Merge branch 'pr-66' into catch-up-2026-11

# Conflicts:
#	locales/rn/en.json
#	src/components/SidebarDrawer.tsx
This commit is contained in:
Linus Rath
2026-10-31 14:07:30 +01:00
33 changed files with 334 additions and 6 deletions
+10 -4
View File
@@ -27,7 +27,7 @@ import {
buildMailboxTree, flattenVisible, mailboxSubtreeIds, ownMailboxes, type MailboxNode,
} from '../lib/mailbox-tree';
import { localizeMailboxName } from '../lib/mailbox-label';
import { showUnifiedSection } from '../lib/unified-section';
import { showUnifiedSection, visibleCrossViews } from '../lib/unified-section';
import { generateAvatarColor, getAccountInitials } from '../lib/avatar-utils';
import { jmapClient } from '../api/jmap-client';
import {
@@ -346,6 +346,9 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
const showFolderTotalCount = useSettingsStore((s) => s.showFolderTotalCount);
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
const unifiedCrossAccount = useSettingsStore((s) => s.unifiedCrossAccount);
const enableCrossUnreadView = useSettingsStore((s) => s.enableCrossUnreadView);
const enableCrossStarredView = useSettingsStore((s) => s.enableCrossStarredView);
const enableCrossAllView = useSettingsStore((s) => s.enableCrossAllView);
const notificationMutes = useSettingsStore((s) => s.notificationMutes);
const updateSetting = useSettingsStore((s) => s.updateSetting);
const keywordDefs = useKeywordsStore((s) => s.keywords);
@@ -713,6 +716,9 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
const showUnified = showUnifiedSection({
accountCount: accounts.length, unifiedCrossAccount, includeGroupInUnified, hasSharedInbox,
});
const crossViews = visibleCrossViews({
showUnified, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView,
});
const unifiedCounts = React.useMemo(() => {
const pools: Mailbox[][] = [mailboxes];
if (unifiedCrossAccount) {
@@ -1054,7 +1060,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
</Pressable>
{/* Unified per-role and cross views */}
{showUnified && (
{(showUnified || crossViews.length > 0) && (
<>
<Pressable style={styles.sectionHeader} onPress={() => toggleSection('unifiedExpanded', setUnifiedExpanded)}>
{unifiedExpanded ? (
@@ -1070,7 +1076,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
{ view: 'unread' as const, Icon: MailOpen, label: t('sidebar.unified_all_unread', 'All unread'), unread: unifiedCounts.crossUnread },
{ view: 'starred' as const, Icon: Star, label: t('sidebar.unified_all_starred', 'All starred'), unread: 0 },
{ view: 'all' as const, Icon: Mails, label: t('sidebar.unified_all_mail', 'All mail'), unread: unifiedCounts.crossUnread },
]).map((row) => (
]).filter((row) => crossViews.includes(row.view)).map((row) => (
<SidebarRow
key={row.view}
icon={<row.Icon size={16} color={c.textSecondary} />}
@@ -1086,7 +1092,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
onToggleExpand={() => {}}
/>
))}
{unifiedCounts.roles.map((r) => {
{showUnified && unifiedCounts.roles.map((r) => {
const Icon = unifiedIcon(r.role);
return (
<SidebarRow
+22 -1
View File
@@ -90,6 +90,9 @@ export function LayoutSettings() {
const showAvatarsInJunk = useSettingsStore((s) => s.showAvatarsInJunk);
const showFolderTotalCount = useSettingsStore((s) => s.showFolderTotalCount);
const unifiedCrossAccount = useSettingsStore((s) => s.unifiedCrossAccount);
const enableCrossUnreadView = useSettingsStore((s) => s.enableCrossUnreadView);
const enableCrossStarredView = useSettingsStore((s) => s.enableCrossStarredView);
const enableCrossAllView = useSettingsStore((s) => s.enableCrossAllView);
const keywordDefs = useKeywordsStore((s) => s.keywords);
const invalidateListOrder = useEmailStore((s) => s.invalidateListOrder);
const order = React.useMemo(() => sanitizeSortLevels(messageListOrderRaw), [messageListOrderRaw]);
@@ -182,10 +185,28 @@ export function LayoutSettings() {
<SettingItem
label={t('settings.layout.unified_cross_account', 'Unified views across accounts')}
description={t('settings.layout.unified_cross_account_description', 'Combine every signed-in account in "All inboxes". Off keeps the unified views inside the active account and its shared folders.')}
noBorder
>
<ToggleSwitch checked={unifiedCrossAccount} onChange={(v) => update('unifiedCrossAccount', v)} />
</SettingItem>
<SettingItem
label={t('settings.appearance.cross_unread.label_mobile', 'All unread')}
description={t('settings.appearance.cross_unread.description', 'Show an Unread entry in the Unified Mailbox listing unread mail across the selected folders.')}
>
<ToggleSwitch checked={enableCrossUnreadView} onChange={(v) => update('enableCrossUnreadView', v)} />
</SettingItem>
<SettingItem
label={t('settings.appearance.cross_starred.label_mobile', 'All starred')}
description={t('settings.appearance.cross_starred.description', 'Show a Starred entry in the Unified Mailbox listing flagged/starred mail across the selected folders.')}
>
<ToggleSwitch checked={enableCrossStarredView} onChange={(v) => update('enableCrossStarredView', v)} />
</SettingItem>
<SettingItem
label={t('settings.appearance.cross_all.label_mobile', 'All mail')}
description={t('settings.appearance.cross_all.description', 'Show an All mail entry in the Unified Mailbox listing all mail across the selected folders.')}
noBorder
>
<ToggleSwitch checked={enableCrossAllView} onChange={(v) => update('enableCrossAllView', v)} />
</SettingItem>
</View>
<View style={{ gap: spacing.sm, marginTop: spacing.lg }}>
+19 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { showUnifiedSection } from '../unified-section';
import { showUnifiedSection, visibleCrossViews } from '../unified-section';
const base = { accountCount: 1, unifiedCrossAccount: false, includeGroupInUnified: true, hasSharedInbox: false };
@@ -19,3 +19,21 @@ describe('showUnifiedSection (#843)', () => {
expect(showUnifiedSection({ ...base, hasSharedInbox: true, includeGroupInUnified: false })).toBe(false);
});
});
const off = { showUnified: false, enableCrossUnreadView: false, enableCrossStarredView: false, enableCrossAllView: false };
describe('visibleCrossViews', () => {
it('lists none for a single account until one is turned on', () => {
expect(visibleCrossViews(off)).toEqual([]);
});
it('lists the turned-on views for a single account, in drawer order', () => {
expect(visibleCrossViews({ ...off, enableCrossAllView: true })).toEqual(['all']);
expect(visibleCrossViews({ ...off, enableCrossStarredView: true })).toEqual(['starred']);
expect(visibleCrossViews({ ...off, enableCrossAllView: true, enableCrossUnreadView: true })).toEqual(['unread', 'all']);
});
it('keeps all three alongside the unified section, whatever the toggles', () => {
expect(visibleCrossViews({ ...off, showUnified: true })).toEqual(['unread', 'starred', 'all']);
});
});
+3
View File
@@ -88,6 +88,9 @@ export const SETTINGS_SEARCH_PATHS: Record<SettingsTabId, string[]> = {
'settings.layout.show_folder_total_count',
'settings.layout.show_avatars_in_junk',
'settings.layout.unified_cross_account',
'settings.appearance.cross_unread',
'settings.appearance.cross_starred',
'settings.appearance.cross_all',
],
reading: [
'settings.email_behavior.mark_read',
+27
View File
@@ -1,3 +1,5 @@
import type { CrossView } from '../api/unified-inbox';
/**
* Whether the drawer offers the unified mailbox (the "All inboxes" row and
* the Unified section), following the webmail's rule (#843): only when the
@@ -18,3 +20,28 @@ export function showUnifiedSection(opts: {
const crossAccountActive = opts.unifiedCrossAccount && opts.accountCount > 1;
return crossAccountActive || (opts.includeGroupInUnified && opts.hasSharedInbox);
}
const CROSS_VIEW_ORDER: CrossView[] = ['unread', 'starred', 'all'];
/**
* Which cross-folder views (All unread / All starred / All mail) the drawer
* lists. Alongside the unified section they all show, as before. Without it -
* a single account, no group inbox merged in - each one shows once turned on
* in Settings > Layout, like the webmail's per-view toggles: they span the
* inbox and the custom folders, so even for one account they are not a repeat
* of any single folder.
*/
export function visibleCrossViews(opts: {
showUnified: boolean;
enableCrossUnreadView: boolean;
enableCrossStarredView: boolean;
enableCrossAllView: boolean;
}): CrossView[] {
if (opts.showUnified) return [...CROSS_VIEW_ORDER];
const enabled: Record<CrossView, boolean> = {
unread: opts.enableCrossUnreadView,
starred: opts.enableCrossStarredView,
all: opts.enableCrossAllView,
};
return CROSS_VIEW_ORDER.filter((view) => enabled[view]);
}
+10
View File
@@ -217,6 +217,13 @@ export interface PersistedSettings {
// Unified views span every logged-in account instead of just the active
// one (own + its shared/group folders). Off by default like the webmail.
unifiedCrossAccount: boolean;
// Cross-folder entries of the unified mailbox (All unread / All starred /
// All mail over the inbox and custom folders). They always come with the
// unified section; these opt them in without it, e.g. for a single account
// (parity with the webmail toggles of the same names, off by default).
enableCrossUnreadView: boolean;
enableCrossStarredView: boolean;
enableCrossAllView: boolean;
mailAttachmentAction: MailAttachmentAction;
attachmentPosition: AttachmentPosition;
// Reader body: font for text/plain bodies (#830), gutter around the body,
@@ -414,6 +421,9 @@ const DEFAULT_PERSISTED: PersistedSettings = {
showFolderTotalCount: true,
clearSearchOnFolderChange: false,
unifiedCrossAccount: false,
enableCrossUnreadView: false,
enableCrossStarredView: false,
enableCrossAllView: false,
mailAttachmentAction: 'preview',
attachmentPosition: 'beside-sender',
plainTextFont: 'sans',