From 406450ccb5c8b1133a54a564842806e85fc3ec14 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:47:47 +0200 Subject: [PATCH 1/4] docs: clarify why the Unified Mailbox section can look empty (#843) Two reporters on #843 filed it as a bug before clarified in the thread that an empty Unified Mailbox section is expected once none of its sub-features are on: the combined folder rows need 2+ connected accounts (or a group inbox), and All mail/Unread/Starred are each a separate admin policy gate. --- FEATURES.md | 4 ++-- locales/en/common.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 1577a2db6..d37939c92 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -4,8 +4,8 @@ - Read, compose, reply, reply-all, and forward in a Tiptap rich-text editor that handles inline images, drag-and-drop embedding, and tables - Gmail-style threading, expanded inline, with a conversation toggle you can switch off -- The Unified Mailbox combines Inbox, Sent, Drafts, Junk, Archive, and Trash. By default it stays inside the active account and its shared/group folders; an admin can unlock a cross-account mode that spans every connected account. -- All mail, Unread, and Starred obey that same account boundary and can be narrowed to a per-account folder selection. Every row names the folder its message came from. +- The Unified Mailbox combines Inbox, Sent, Drafts, Junk, Archive, and Trash. By default it stays inside the active account and its shared/group folders; an admin can unlock a cross-account mode that spans every connected account. The combined folder rows only appear once 2+ accounts are connected (or "Include group inboxes" is on and a group inbox exists) — with just one account and no group inbox, turning the setting on adds the section header but nothing under it. +- All mail, Unread, and Starred obey that same account boundary and can be narrowed to a per-account folder selection. Every row names the folder its message came from. Each of these three is a separate admin policy gate (see [Admin & extensibility](#admin--extensibility)) — if none are enabled for your instance, the toggles for them do not even appear in Settings → Appearance, and the Unified Mailbox section can look empty even with the feature turned on and multiple accounts connected. Ask your admin to enable the ones you need. - Search runs across all unified views; the per-role mailboxes add the full filter panel on top - Three mail layouts: split three-pane, focused list, or reading pane at the bottom - Drafts auto-save, keeping the chosen identity, the HTML body, and correct `In-Reply-To` / `References` headers on replies diff --git a/locales/en/common.json b/locales/en/common.json index 68874b945..e95ed6dd1 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1031,7 +1031,7 @@ }, "unified_mailbox": { "label": "Unified Mailbox", - "description": "Show combined folders (Inbox, Sent, etc.) for the active account and its shared folders.", + "description": "Show combined folders (Inbox, Sent, etc.) for the active account and its shared folders. Needs more than one connected account (or a group inbox included below) before any folders appear here - with just one account, this only adds the section header. The All mail / Unread / Starred entries below are separate, admin-gated options that won't show up until your admin turns them on.", "cross_account": { "label": "Across all accounts", "description": "Merge the unified mailbox across every connected account instead of staying within the active account." From 4947c6c7a43e930554cb45637105df3a996ea1f7 Mon Sep 17 00:00:00 2001 From: Almost-Senseless-Coder Date: Wed, 9 Sep 2026 19:37:43 +0200 Subject: [PATCH 2/4] Improved accessibility of the email list --- components/email/__tests__/tag-badge.test.tsx | 2 +- components/email/tag-badge.tsx | 5 +- components/email/thread-list-item.tsx | 137 ++++++++++++------ locales/ar/common.json | 4 + locales/ca/common.json | 4 + locales/cs/common.json | 4 + locales/da/common.json | 4 + locales/de/common.json | 4 + locales/en/common.json | 4 + locales/es/common.json | 4 + locales/fa/common.json | 4 + locales/fr/common.json | 4 + locales/he/common.json | 4 + locales/hu/common.json | 4 + locales/it/common.json | 4 + locales/ja/common.json | 4 + locales/ko/common.json | 4 + locales/lv/common.json | 4 + locales/mn/common.json | 4 + locales/nb/common.json | 4 + locales/nl/common.json | 4 + locales/pl/common.json | 4 + locales/pt/common.json | 4 + locales/ro/common.json | 4 + locales/ru/common.json | 4 + locales/sk/common.json | 4 + locales/tr/common.json | 4 + locales/uk/common.json | 4 + locales/zh-TW/common.json | 4 + locales/zh/common.json | 4 + 30 files changed, 201 insertions(+), 51 deletions(-) diff --git a/components/email/__tests__/tag-badge.test.tsx b/components/email/__tests__/tag-badge.test.tsx index 70775ff6a..990e84bff 100644 --- a/components/email/__tests__/tag-badge.test.tsx +++ b/components/email/__tests__/tag-badge.test.tsx @@ -36,6 +36,6 @@ describe('TagBadge', () => { it('leaves the dot alone, having nowhere to put the control', () => { render( {}} />); expect(screen.queryByRole('button')).not.toBeInTheDocument(); - expect(screen.getByLabelText('Work')).toBeInTheDocument(); + expect(screen.getByText('Work')).toHaveClass('sr-only'); }); }); diff --git a/components/email/tag-badge.tsx b/components/email/tag-badge.tsx index ca36f35f1..0cfa385c9 100644 --- a/components/email/tag-badge.tsx +++ b/components/email/tag-badge.tsx @@ -63,8 +63,9 @@ export function TagBadge({ + > + {name} + ); } diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index 9a64524db..189e8a60b 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -10,7 +10,7 @@ import type { LucideIcon } from "lucide-react"; import { useSettingsStore } from "@/stores/settings-store"; import { useUIStore } from "@/stores/ui-store"; import { useEmailStore } from "@/stores/email-store"; -import { useAccountStore } from "@/stores/account-store"; +import { useAccountStore, type AccountEntry } from "@/stores/account-store"; import { getThreadTagIds, getEmailTagIds } from "@/lib/thread-utils"; import { useKeywordFormat } from "@/hooks/use-keyword-format"; import { useTagDisplay } from "@/hooks/use-tag-display"; @@ -34,6 +34,7 @@ import { useTranslations } from "next-intl"; * a column with. Anchor = top padding + half an avatar. */ function UnreadDot({ density, compactAvatar }: { density: string; compactAvatar: boolean }) { + const t = useTranslations('email_viewer'); const halfFirstLine = density === 'extra-compact' ? '0.625rem' : compactAvatar ? '1rem' : '1.25rem'; return (
+ {t('unread')}
); } +function StatusIcon({ icon: Icon, label, className }: { icon: LucideIcon; label: string; className: string }) { + return ( + <> + + {label} + + ); +} + +function describeAccount(label: string | undefined, account: AccountEntry | undefined): string | undefined { + const address = account?.email || account?.username; + if (!label) return address; + if (!address || address.toLowerCase() === label.toLowerCase()) return label; + return `${label}, ${address}`; +} + /** * Small chip showing the originating folder of a message, rendered in the * aggregate "All …" views (All Mail / unified / cross-account) where rows come @@ -79,7 +97,8 @@ function ThreadCountPill({ count, hasUnread, title }: { count: number; hasUnread title={title} > - {count} + + {title} ); } @@ -136,6 +155,7 @@ const SingleEmailItem = React.forwardRef( function SingleEmailItem({ email, selected, onClick, onDoubleClick, onContextMenu, showPreview, rowTint, onToggleStar, onMarkAsRead, onDelete, onArchive, onSetTag, onMarkAsSpam, onUndoSpam }, ref) { const t = useTranslations('email_viewer'); const tBatch = useTranslations('email_list.batch_actions'); + const tStatus = useTranslations('email_list'); const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const isPinned = email.keywords?.['$pinned'] === true; @@ -160,7 +180,9 @@ const SingleEmailItem = React.forwardRef( // Show the originating folder in the aggregate "All …" views. const showSourceFolder = isUnifiedView && !!email.sourceFolder; const getAccountById = useAccountStore((state) => state.getAccountById); - const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined; + const account = email.accountId ? getAccountById(email.accountId) : undefined; + const accountColor = account?.avatarColor; + const accountDescription = describeAccount(email.accountLabel, account); const isChecked = selectedEmailIds.has(email.id); const isMobile = useUIStore((state) => state.isMobile); // The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile. @@ -275,6 +297,7 @@ const SingleEmailItem = React.forwardRef( data-subject={email.subject || ''} data-unread={isUnread ? 'true' : 'false'} data-starred={email.keywords?.$flagged ? 'true' : 'false'} + aria-current={selected ? 'true' : undefined} className={cn( "relative group cursor-pointer select-none transition-shadow duration-200 border-b border-border overflow-hidden", resolvedRowTint ? resolvedRowTint : ( @@ -324,6 +347,9 @@ const SingleEmailItem = React.forwardRef( {density === 'extra-compact' && selectedEmailIds.size > 0 && ( )} - {isUnread && ( - - )} - {density !== 'extra-compact' && ( ( /> )} + {isUnread && ( + + )} +
{isFocusedMailLayout ? (
@@ -366,8 +392,10 @@ const SingleEmailItem = React.forwardRef( + title={accountDescription} + > + {accountDescription} + )} (
- {isPinned && } - {isStarred && } - {isAnswered && !isForwarded && } - {isForwarded && !isAnswered && } + {isPinned && } + {isStarred && } + {isAnswered && !isForwarded && } + {isForwarded && !isAnswered && } {isAnswered && isForwarded && ( <> - - + + )} - {email.hasAttachment && } + {email.hasAttachment && } {showSourceFolder && } {scheduledSendLabel ? ( ( + title={accountDescription} + > + {accountDescription} + )} ( )}
{isPinned && ( - + )} {isStarred && ( - + )} {isAnswered && !isForwarded && ( - + )} {isForwarded && !isAnswered && ( - + )} {isAnswered && isForwarded && ( <> - - + + )} {email.hasAttachment && ( - + )}
@@ -578,6 +608,7 @@ export const ThreadListItem = React.forwardRef state.showPreview); const density = useSettingsStore((state) => state.density); const mailLayout = useSettingsStore((state) => state.mailLayout); @@ -601,7 +632,9 @@ export const ThreadListItem = React.forwardRef state.getAccountById); - const threadAccountColor = latestEmail.accountId ? getAccountById(latestEmail.accountId)?.avatarColor : undefined; + const threadAccount = latestEmail.accountId ? getAccountById(latestEmail.accountId) : undefined; + const threadAccountColor = threadAccount?.avatarColor; + const threadAccountDescription = describeAccount(latestEmail.accountLabel, threadAccount); // In Sent/Drafts folders, show recipient instead of sender (which is always // "me"). Aggregate role-views use a virtual selected mailbox → fall back to // the unified role so junk-contextual UI and avatar hiding work. @@ -732,6 +765,7 @@ export const ThreadListItem = React.forwardRef 0 && (