From 96c7c0455d841e98f2642254f40aef2fcf2e965c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:50:08 +0100 Subject: [PATCH] fix: give user folders a folder icon even when their name looks like a system folder #80 --- src/components/SidebarDrawer.tsx | 62 +++++++++++--------------- src/lib/__tests__/mailbox-icon.test.ts | 28 ++++++++++++ src/lib/mailbox-icon.ts | 36 +++++++++++++++ 3 files changed, 89 insertions(+), 37 deletions(-) create mode 100644 src/lib/__tests__/mailbox-icon.test.ts create mode 100644 src/lib/mailbox-icon.ts diff --git a/src/components/SidebarDrawer.tsx b/src/components/SidebarDrawer.tsx index aab8d8ab..f36a270b 100644 --- a/src/components/SidebarDrawer.tsx +++ b/src/components/SidebarDrawer.tsx @@ -46,6 +46,7 @@ import { type NotificationMutes, } from '../lib/notification-mutes'; import { serverSupportsEmailPush } from '../lib/push-notifications'; +import { mailboxIconKind, type MailboxIconKind } from '../lib/mailbox-icon'; const CHEVRON_SLOT = 20; const INDENT_STEP = 12; @@ -64,41 +65,28 @@ const UNIFIED_ROLES: UnifiedRole[] = ['sent', 'drafts', 'archive', 'junk', 'tras // in for them (#495). const HIDDEN_WITH_VIRTUAL_ROW = new Set(['scheduled']); -// Distinct icons for the special-use roles (#288), then a name-based guess -// for servers that don't set roles, then a plain folder. -function iconFor( - role: string | null | undefined, - name: string | undefined, - hasChildren: boolean, - isExpanded: boolean, -): AppIcon { - switch (role) { - case 'inbox': return Inbox; - case 'sent': return Send; - case 'drafts': return FileIcon; - case 'trash': return Trash2; - case 'junk': - case 'spam': return Ban; - case 'archive': return Archive; - case 'important': return Flag; - case 'flagged': return Star; - case 'all': return Mails; - case 'memos': return StickyNote; - case 'scheduled': return Clock; - case 'snoozed': return AlarmClock; - case 'shared': return Users; - default: break; - } - const lower = (name ?? '').toLowerCase(); - if (lower.includes('inbox')) return Inbox; - if (lower.includes('sent')) return Send; - if (lower.includes('draft')) return FileIcon; - if (lower.includes('trash') || lower.includes('deleted')) return Trash2; - if (lower.includes('junk') || lower.includes('spam')) return Ban; - if (lower.includes('archive')) return Archive; - if (lower.includes('star') || lower.includes('flag')) return Star; - if (hasChildren) return isExpanded ? FolderOpen : Folder; - return Folder; +// Distinct icons for the special-use roles (#288), else a plain folder: by +// role only, never by name (lib/mailbox-icon.ts, #80). +const ICONS: Record = { + inbox: Inbox, + sent: Send, + drafts: FileIcon, + trash: Trash2, + junk: Ban, + archive: Archive, + important: Flag, + flagged: Star, + all: Mails, + memos: StickyNote, + scheduled: Clock, + snoozed: AlarmClock, + shared: Users, + folder: Folder, + 'folder-open': FolderOpen, +}; + +function iconFor(role: string | null | undefined, hasChildren: boolean, isExpanded: boolean): AppIcon { + return ICONS[mailboxIconKind(role, hasChildren, isExpanded)]; } const ROLE_COLOR_FIXED: Record = { @@ -745,7 +733,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side }; }, [mailboxes, accountSnapshots, activeAccountId, unifiedCrossAccount, includeGroupInUnified]); - const unifiedIcon = (role: UnifiedRole): AppIcon => iconFor(role, undefined, false, false); + const unifiedIcon = (role: UnifiedRole): AppIcon => iconFor(role, false, false); const slideX = React.useRef(new Animated.Value(-Dimensions.get('window').width)).current; const overlayOpacity = React.useRef(new Animated.Value(0)).current; @@ -1139,7 +1127,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side // tapping it only opens or closes that account's folders. const Icon = node.isAccountNode ? Users - : iconFor(node.role, node.name, hasChildren, isExpanded); + : iconFor(node.role, hasChildren, isExpanded); const isSelected = !node.isAccountNode && !tagViewActive && node.id === currentMailboxId; return ( { + it('gives each special-use role its icon', () => { + expect(mailboxIconKind('sent', false, false)).toBe('sent'); + expect(mailboxIconKind('junk', false, false)).toBe('junk'); + expect(mailboxIconKind('spam', false, false)).toBe('junk'); + expect(mailboxIconKind('trash', true, true)).toBe('trash'); + }); + + // native #80: a sieve-sorted folder "web/sentry" got the Sent icon. + it('gives a user folder a folder icon whatever its name says', () => { + for (const name of ['sentry', 'Sent Items', 'Drafts old', 'memory', 'spam-reports', 'archive 2019']) { + expect(mailboxIconKind(null, false, false), name).toBe('folder'); + } + expect(mailboxIconKind(undefined, true, true)).toBe('folder-open'); + expect(mailboxIconKind(null, true, false)).toBe('folder'); + }); + + it('is what the sidebar uses, with no guess from the folder name', () => { + const source = readFileSync(path.join(process.cwd(), 'src/components/SidebarDrawer.tsx'), 'utf8'); + expect(source).toContain('mailboxIconKind('); + expect(source).not.toMatch(/lower\.includes\('(?:sent|inbox|draft|trash|junk|archive|star)'\)/); + }); +}); diff --git a/src/lib/mailbox-icon.ts b/src/lib/mailbox-icon.ts new file mode 100644 index 00000000..2d8b0189 --- /dev/null +++ b/src/lib/mailbox-icon.ts @@ -0,0 +1,36 @@ +// Which icon a folder in the sidebar gets: one per special-use role (#288), +// else a plain folder. By role only, as the webmail does +// (components/layout/sidebar.tsx): a guess from the name gave a user folder +// "web/sentry" the Sent icon (native #80, webmail #1235), and a folder that +// holds no sent mail must not look like one that does. + +export type MailboxIconKind = + | 'inbox' | 'sent' | 'drafts' | 'trash' | 'junk' | 'archive' | 'important' | 'flagged' + | 'all' | 'memos' | 'scheduled' | 'snoozed' | 'shared' | 'folder' | 'folder-open'; + +const ROLE_ICONS: Record = { + inbox: 'inbox', + sent: 'sent', + drafts: 'drafts', + trash: 'trash', + junk: 'junk', + spam: 'junk', + archive: 'archive', + important: 'important', + flagged: 'flagged', + all: 'all', + memos: 'memos', + scheduled: 'scheduled', + snoozed: 'snoozed', + shared: 'shared', +}; + +export function mailboxIconKind( + role: string | null | undefined, + hasChildren: boolean, + isExpanded: boolean, +): MailboxIconKind { + const byRole = role ? ROLE_ICONS[role] : undefined; + if (byRole) return byRole; + return hasChildren && isExpanded ? 'folder-open' : 'folder'; +}