feat: show unread per account in the switcher and count every account on the app badge and tray

The default layout's switcher listed accounts without their unread, so an
existing test asserted that (account-switcher-mountain-view, 'unread-free');
it now expects each account's count. The idle-outside-Pro hook test now
uses one account, since a second connected account loads in every layout.
This commit is contained in:
Linus Rath
2026-11-04 06:06:37 +01:00
parent 0e400dbcf4
commit 9ce7ac8172
44 changed files with 511 additions and 32 deletions
@@ -5,6 +5,8 @@ import { useFaviconBadge } from '@/hooks/use-favicon-badge';
import { useAppBadge } from '@/hooks/use-app-badge';
import { useEmailStore } from '@/stores/email-store';
import { useSettingsStore } from '@/stores/settings-store';
import { useAccountStore } from '@/stores/account-store';
import { useAuthStore } from '@/stores/auth-store';
import type { Mailbox } from '@/lib/jmap/types';
vi.mock('@/hooks/use-favicon-badge', () => ({
@@ -96,6 +98,51 @@ describe('FaviconBadge', () => {
expect(useAppBadgeMock).toHaveBeenCalledWith(4, true);
});
// Usability test 2026-11-04, mail-6: with two accounts the badge counted
// only the one on screen (11 instead of 31).
describe('with several accounts', () => {
const accounts = (ids: string[], connected = true) =>
ids.map((id) => ({ id, label: id, email: id, username: id, isConnected: connected }));
beforeEach(() => {
useAccountStore.setState({ accounts: accounts(['a', 'b', 'c']) } as never);
useAuthStore.setState({ activeAccountId: 'a' } as never);
useEmailStore.setState({
mailboxes: [mailbox({ id: 'inbox', role: 'inbox', unreadEmails: 11 })],
accountMailboxes: {
// The active login's cached copy is older; `mailboxes` is the live one.
a: [mailbox({ id: 'inbox', role: 'inbox', unreadEmails: 2 })],
b: [mailbox({ id: 'inbox', role: 'inbox', unreadEmails: 20 }), mailbox({ id: 'shared', role: 'inbox', isShared: true, unreadEmails: 50 })],
},
});
});
afterEach(() => {
useAccountStore.setState({ accounts: [] } as never);
useEmailStore.setState({ accountMailboxes: {} });
useSettingsStore.setState({ unreadBadgeAllAccounts: true });
});
it('adds up the inbox of every signed-in account', () => {
render(<FaviconBadge />);
// c's folders are not loaded yet and add nothing.
expect(useAppBadgeMock).toHaveBeenLastCalledWith(31, true);
expect(useFaviconBadgeMock).toHaveBeenLastCalledWith(31, true);
});
it('counts only the open account when the setting is off', () => {
useSettingsStore.setState({ unreadBadgeAllAccounts: false });
render(<FaviconBadge />);
expect(useAppBadgeMock).toHaveBeenLastCalledWith(11, true);
});
it('leaves out an account that is signed out', () => {
useAccountStore.setState({ accounts: [...accounts(['a']), ...accounts(['b'], false)] } as never);
render(<FaviconBadge />);
expect(useAppBadgeMock).toHaveBeenLastCalledWith(11, true);
});
});
it('badges zero when there is no inbox yet', () => {
useEmailStore.setState({ mailboxes: [] });
+23 -2
View File
@@ -4,6 +4,10 @@ import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useFaviconBadge } from "@/hooks/use-favicon-badge";
import { useAppBadge } from "@/hooks/use-app-badge";
import { useShallow } from "zustand/react/shallow";
import { useAccountStore } from "@/stores/account-store";
import { useAuthStore } from "@/stores/auth-store";
import { totalInboxUnread } from "@/lib/account-inbox-unread";
/**
* Badges the browser-tab favicon, and — where supported — the installed PWA's
@@ -34,9 +38,26 @@ export function FaviconBadge() {
(s) => s.mailboxes.find((m) => m.role === "inbox" && !m.isShared)?.unreadEmails ?? 0,
);
const enabled = useSettingsStore((s) => s.faviconUnreadBadge);
const allAccounts = useSettingsStore((s) => s.unreadBadgeAllAccounts);
// With several accounts the badge counts the inbox of each (on by default):
// new mail in an account that is not on screen still shows on the icon and
// in the desktop tray. The other accounts' folder lists are what the
// account switcher reads (useProMultiAccountMailboxes); one not loaded yet
// adds nothing.
const othersUnread = useOtherAccountsUnread(allAccounts);
const count = inboxUnread + othersUnread;
useFaviconBadge(inboxUnread, enabled);
useAppBadge(inboxUnread, enabled);
useFaviconBadge(count, enabled);
useAppBadge(count, enabled);
return null;
}
function useOtherAccountsUnread(enabled: boolean): number {
const activeAccountId = useAuthStore((s) => s.activeAccountId);
const otherIds = useAccountStore(
useShallow((s) => (enabled ? s.accounts.filter((a) => a.isConnected && a.id !== activeAccountId).map((a) => a.id) : [])),
);
const accountMailboxes = useEmailStore((s) => s.accountMailboxes);
return totalInboxUnread(otherIds, accountMailboxes);
}
@@ -119,13 +119,17 @@ describe('AccountSwitcher under Mountain View', () => {
expect(screen.queryByTestId('all-inboxes')).toBeNull();
});
it('leaves the default layout listing every account, unread-free', () => {
// With several accounts the default layout shows the unread of each too
// (usability test 2026-11-04, mail-6): before, it listed them without.
it('leaves the default layout listing every account, each with its unread', () => {
useSettingsStore.setState({ interfaceLayout: 'default' } as never);
render(<AccountSwitcher variant="header" onSelectAllInboxes={vi.fn()} />);
openPopover();
expect(screen.getAllByTestId('account-option')).toHaveLength(2);
const options = screen.getAllByTestId('account-option');
expect(options).toHaveLength(2);
expect(screen.queryByTestId('all-inboxes')).toBeNull();
expect(screen.queryByText('5')).toBeNull();
expect(within(options[0]).getByText('3')).toBeInTheDocument();
expect(within(options[1]).getByText('5')).toBeInTheDocument();
});
});
@@ -0,0 +1,82 @@
import { render, screen, fireEvent, within } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
// Usability test 2026-11-04, mail-6: with two accounts the new mail in the
// other one was nowhere to be seen. The switcher now lists the unread of every
// account, and the avatar carries a dot while another account has unread mail.
const { AccountSwitcher } = await import('../account-switcher');
const { useAccountStore } = await import('@/stores/account-store');
const { useAuthStore } = await import('@/stores/auth-store');
const { useEmailStore } = await import('@/stores/email-store');
const { useSettingsStore } = await import('@/stores/settings-store');
const inbox = (unread: number): Mailbox[] =>
[{ id: 'inbox', name: 'Inbox', role: 'inbox', totalEmails: 40, unreadEmails: unread }] as unknown as Mailbox[];
function account(id: string, overrides: Record<string, unknown> = {}) {
return {
id,
label: `${id}@example.com`,
email: `${id}@example.com`,
username: `${id}@example.com`,
displayName: 'Robin Kessler',
serverUrl: 'https://mail.example.com',
avatarColor: '#336699',
cookieSlot: 0,
isConnected: true,
isDefault: id === 'work',
...overrides,
};
}
describe('AccountSwitcher unread per account', () => {
beforeEach(() => {
useAccountStore.setState({ accounts: [account('work'), account('home')] } as never);
useAuthStore.setState({ activeAccountId: 'work' } as never);
useEmailStore.setState({ mailboxes: inbox(11), accountMailboxes: { home: inbox(20) } } as never);
useSettingsStore.setState({ interfaceLayout: 'default' } as never);
});
it('lists the unread of each account in the rail menu', () => {
render(<AccountSwitcher variant="rail" />);
fireEvent.click(screen.getByTestId('account-switcher'));
const [work, home] = screen.getAllByTestId('account-option');
expect(work).toHaveAttribute('data-account-id', 'work');
expect(within(work).getByText('11')).toBeInTheDocument();
expect(within(home).getByText('20')).toBeInTheDocument();
});
it('puts a dot on the avatar and says why while another account has unread mail', () => {
render(<AccountSwitcher variant="rail" />);
const button = screen.getByTestId('account-switcher');
expect(within(button).getByTestId('other-accounts-unread')).toBeInTheDocument();
expect(button.getAttribute('aria-label')).toContain('others');
});
it('shows no dot when the other accounts are read', () => {
useEmailStore.setState({ accountMailboxes: { home: inbox(0) } } as never);
render(<AccountSwitcher variant="rail" />);
expect(screen.queryByTestId('other-accounts-unread')).toBeNull();
});
it('shows no count with a single account', () => {
useAccountStore.setState({ accounts: [account('work')] } as never);
render(<AccountSwitcher variant="rail" />);
fireEvent.click(screen.getByTestId('account-switcher'));
expect(within(screen.getByTestId('account-option')).queryByText('11')).toBeNull();
});
it('gives two accounts with the same name different initials', () => {
useAccountStore.setState({
accounts: [account('work', { email: 'dso-ux-mail@example.org' }), account('home', { email: 'dso-ux-mail-b@example.org' })],
} as never);
render(<AccountSwitcher variant="rail" />);
fireEvent.click(screen.getByTestId('account-switcher'));
const [work, home] = screen.getAllByTestId('account-option');
expect(within(work).getByText('DM')).toBeInTheDocument();
expect(within(home).getByText('DB')).toBeInTheDocument();
});
});
+27 -13
View File
@@ -12,7 +12,7 @@ import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { HelpMenuItems } from "@/components/layout/help-links";
import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText } from "@/lib/account-utils";
import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText, accountAvatarName } from "@/lib/account-utils";
import { inboxUnread, totalInboxUnread } from "@/lib/account-inbox-unread";
import { isDocumentRTL } from "@/i18n/direction";
import { cn, formatFileSize } from "@/lib/utils";
@@ -40,10 +40,10 @@ interface AccountSwitcherProps {
allInboxesSelected?: boolean;
}
function AccountAvatar({ account, size = "sm" }: { account: AccountEntry; size?: "sm" | "md" | "xl" }) {
function AccountAvatar({ account, accounts, size = "sm" }: { account: AccountEntry; accounts: AccountEntry[]; size?: "sm" | "md" | "xl" }) {
return (
<Avatar
name={account.displayName || account.label}
name={accountAvatarName(account, accounts)}
email={account.email || account.username}
size="sm"
className={cn(
@@ -74,6 +74,7 @@ export function AccountSwitcher({
allInboxesSelected = false,
}: AccountSwitcherProps) {
const t = useTranslations("sidebar");
const tUnread = useTranslations("account_unread");
const router = useRouter();
const [open, setOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -297,12 +298,16 @@ export function AccountSwitcher({
const unreadByAccount: Record<string, Mailbox[] | undefined> = activeAccountId
? { ...accountMailboxes, [activeAccountId]: ownMailboxes }
: accountMailboxes;
// With several accounts every list carries the unread of each, so the new
// mail in another account shows without switching to it.
const severalAccounts = accounts.length > 1;
const unreadFor = (account: AccountEntry) =>
mvPopover ? inboxUnread(unreadByAccount[account.id]) : undefined;
const allInboxesUnread = totalInboxUnread(
accounts.filter((account) => account.isConnected).map((account) => account.id),
unreadByAccount
);
mvPopover || severalAccounts ? inboxUnread(unreadByAccount[account.id]) : undefined;
const connectedIds = accounts.filter((account) => account.isConnected).map((account) => account.id);
const allInboxesUnread = totalInboxUnread(connectedIds, unreadByAccount);
// What waits in the accounts not on screen: a dot on the avatar says so.
const othersUnread = totalInboxUnread(connectedIds.filter((id) => id !== activeAccountId), unreadByAccount);
const othersUnreadText = othersUnread > 0 ? tUnread("others", { count: othersUnread }) : null;
return (
<>
@@ -320,15 +325,24 @@ export function AccountSwitcher({
: "w-full px-2 py-1.5 hover:bg-muted text-start min-w-0",
className
)}
title={variant !== "expanded" ? displayName : undefined}
aria-label={t("switch_account")}
title={variant !== "expanded" ? [displayName, othersUnreadText].filter(Boolean).join("\n") : undefined}
aria-label={othersUnreadText ? `${t("switch_account")}, ${othersUnreadText}` : t("switch_account")}
aria-expanded={open}
aria-haspopup="menu"
aria-controls={open ? menuId : undefined}
>
{activeAccount ? (
<>
<AccountAvatar account={activeAccount} size={variant === "expanded" ? "md" : "sm"} />
<span className="relative flex-shrink-0">
<AccountAvatar account={activeAccount} accounts={accounts} size={variant === "expanded" ? "md" : "sm"} />
{othersUnreadText && (
<span
aria-hidden="true"
data-testid="other-accounts-unread"
className="absolute -top-0.5 -end-0.5 w-2.5 h-2.5 rounded-full bg-primary ring-2 ring-background"
/>
)}
</span>
{variant === "expanded" && (
<>
<div className="min-w-0 flex-1">
@@ -382,7 +396,7 @@ export function AccountSwitcher({
{mvPopover && activeAccount && (
<div className="flex items-center gap-4 rounded-t-3xl bg-background px-4 pt-4 pb-3" data-mv-account-current="">
<AccountAvatar account={activeAccount} size="xl" />
<AccountAvatar account={activeAccount} accounts={accounts} size="xl" />
<div className="min-w-0 flex-1">
<p className="text-lg leading-6 truncate">{displayName}</p>
{displaySubtitle && <p className="text-sm text-muted-foreground truncate">{displaySubtitle}</p>}
@@ -455,7 +469,7 @@ export function AccountSwitcher({
disabled={isActive}
>
<div className="relative flex-shrink-0">
<AccountAvatar account={account} size="md" />
<AccountAvatar account={account} accounts={accounts} size="md" />
{isActive && (
<div className="absolute -bottom-0.5 -end-0.5 w-4 h-4 rounded-full bg-primary flex items-center justify-center">
<Check className="w-2.5 h-2.5 text-primary-foreground" />
+2 -2
View File
@@ -19,7 +19,7 @@ import { useResolvedSidebarApps } from "@/hooks/use-resolved-sidebar-apps";
import { useAuthStore } from "@/stores/auth-store";
import { useAccountStore } from "@/stores/account-store";
import { useUpdateStore, selectHasUpdate } from "@/stores/update-store";
import { getMaxAccounts } from "@/lib/account-utils";
import { accountAvatarName, getMaxAccounts } from "@/lib/account-utils";
import { isDocumentRTL } from "@/i18n/direction";
import { cn, formatFileSize } from "@/lib/utils";
import { quotaLevel, type StorageQuota } from "@/lib/quota";
@@ -925,7 +925,7 @@ export function NavigationRail({
title={`${toUnicodeEmail(account.displayName || account.label)} (${toUnicodeEmail(account.email || account.username)})`}
>
<Avatar
name={account.displayName || account.label}
name={accountAvatarName(account, accounts)}
email={account.email || account.username}
size="sm"
disableFavicon
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
import { Loader2, RefreshCw, Volume2, XCircle } from '@/components/icons';
import { usePolicyStore } from '@/stores/policy-store';
import { useAuthStore } from '@/stores/auth-store';
import { useAccountStore } from '@/stores/account-store';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { MultiAccountPushSettings } from './multi-account-push';
import { useConfirmDialog } from '@/hooks/use-confirm-dialog';
@@ -51,6 +52,8 @@ type DevicesState =
export function NotificationSettings() {
const t = useTranslations('settings.notifications');
const tAppearance = useTranslations('settings.appearance');
const tUnread = useTranslations('account_unread');
const severalAccounts = useAccountStore((s) => s.accounts.length > 1);
const tThirdParty = useTranslations('third_party');
// The relay list depends on it; re-render once the config is known (#903).
const noThirdParty = useNoThirdPartyTraffic();
@@ -62,6 +65,7 @@ export function NotificationSettings() {
calendarNotificationsEnabled,
calendarNotificationSound,
faviconUnreadBadge,
unreadBadgeAllAccounts,
pushRelayUrl,
updateSetting,
} = useSettingsStore();
@@ -353,6 +357,17 @@ export function NotificationSettings() {
onChange={(checked) => updateSetting('faviconUnreadBadge', checked)}
/>
</SettingItem>
{/* Only means something with a second account. */}
{severalAccounts && (
<SettingItem label={tUnread('badge_all_accounts.label')} description={tUnread('badge_all_accounts.description')}>
<ToggleSwitch
checked={unreadBadgeAllAccounts}
onChange={(checked) => updateSetting('unreadBadgeAllAccounts', checked)}
disabled={!faviconUnreadBadge}
/>
</SettingItem>
)}
</SettingsSection>
<SettingsSection id="calendar" title={t('calendar.title')} description={t('calendar.description')}>
@@ -8,6 +8,7 @@ import { useAccountStore } from '@/stores/account-store';
import { useAuthStore } from '@/stores/auth-store';
import { invalidateUnifiedMailboxes, useEmailStore } from '@/stores/email-store';
import { useSettingsStore } from '@/stores/settings-store';
import { emitPushStateChange } from '@/lib/push-events';
/**
* The Pro shell needs every login's folders for its per-account tree. While
@@ -86,11 +87,45 @@ describe('useProMultiAccountMailboxes', () => {
expect(client('b').getAllMailboxes).toHaveBeenCalledTimes(2);
});
it('stays idle outside the Pro shell', () => {
it('stays idle outside the Pro shell with one account', () => {
useSettingsStore.setState({ proInterface: false } as never);
connected(['a', 'b']);
connected(['a']);
renderHook(() => useProMultiAccountMailboxes());
expect(client('a').getAllMailboxes).not.toHaveBeenCalled();
expect(client('a').getMailboxes).not.toHaveBeenCalled();
});
// Usability test 2026-11-04, mail-6: the account switcher and the badge
// count every account's unread in every layout, not only Mountain View.
it("loads each login's own folders outside the Pro shell once a second account is connected", async () => {
useSettingsStore.setState({ proInterface: false, interfaceLayout: 'default' } as never);
connected(['a', 'b']);
renderHook(() => useProMultiAccountMailboxes());
await waitFor(() => expect(useEmailStore.getState().accountMailboxes.b).toBeDefined());
expect(client('b').getMailboxes).toHaveBeenCalledTimes(1);
expect(client('b').getAllMailboxes).not.toHaveBeenCalled();
});
it("reads a background login's folders again after its push, once per burst", async () => {
useSettingsStore.setState({ proInterface: false, interfaceLayout: 'default' } as never);
connected(['a', 'b'], 'a');
renderHook(() => useProMultiAccountMailboxes());
await waitFor(() => expect(client('b').getMailboxes).toHaveBeenCalledTimes(1));
vi.useFakeTimers();
try {
const change = { '@type': 'StateChange', changed: { b: { Mailbox: 's2', Email: 'e2' } } } as never;
// What the mail view's own push handler does first (createMailPushListener).
invalidateUnifiedMailboxes(client('b') as never);
emitPushStateChange(change, client('b') as never, 'background');
emitPushStateChange(change, client('b') as never, 'background');
// The active login's push is the mail view's business, not this hook's.
emitPushStateChange(change, client('a') as never, 'active');
await vi.advanceTimersByTimeAsync(2000);
} finally {
vi.useRealTimers();
}
await waitFor(() => expect(client('b').getMailboxes).toHaveBeenCalledTimes(2));
expect(client('a').getMailboxes).toHaveBeenCalledTimes(1);
});
});
+39 -9
View File
@@ -7,6 +7,7 @@ import { invalidateUnifiedMailboxes, loadAccountMailboxes } from "@/stores/email
import { useSettingsStore } from "@/stores/settings-store";
import { useIsEmbedded } from "@/hooks/use-is-embedded";
import type { IJMAPClient } from "@/lib/jmap/client-interface";
import { changesAnyType, subscribePushStateChanges } from "@/lib/push-events";
/**
* Keeps `useEmailStore.accountMailboxes` populated with one entry per
@@ -14,11 +15,14 @@ import type { IJMAPClient } from "@/lib/jmap/client-interface";
* sidebar reads this cache to render a Thunderbird-style per-account folder
* tree (see [[project_pro_mode]]). Outside Pro the cache stays empty.
*
* Mountain View needs the same cache for a different reason: its account
* popover puts the unread waiting in each account next to that address, and
* the active login's folder list knows nothing about the others. It is only
* worth the extra round trips once a second account is connected - with one
* account the popover has nothing to compare.
* Every layout needs the same cache once a second account is connected: the
* account switcher puts the unread waiting in each account next to that
* address, and the app badge and the desktop tray count every account's
* inbox, while the active login's folder list knows nothing about the others.
* With one account there is nothing to compare, and nothing is fetched.
*
* A background login's push says when its counts moved; its list is then read
* again (one Mailbox/get), a moment later so a burst of changes costs one.
*
* Each login is loaded once per client, not once per connection: while a
* browser restores its logins they connect one after another, and reloading
@@ -31,7 +35,6 @@ import type { IJMAPClient } from "@/lib/jmap/client-interface";
export function useProMultiAccountMailboxes(): void {
const isEmbedded = useIsEmbedded();
const proInterface = useSettingsStore((s) => s.proInterface);
const mountainView = useSettingsStore((s) => s.interfaceLayout === "mountain-view");
// The ids of the connected logins, as one string: `accounts` itself changes
// for display-name and login-time updates that call for no fetch at all.
const connectedSignature = useAccountStore((s) =>
@@ -43,10 +46,10 @@ export function useProMultiAccountMailboxes(): void {
useEffect(() => {
const connected = connectedSignature ? connectedSignature.split("\n") : [];
if (!proInterface && !isEmbedded && !(mountainView && connected.length > 1)) return;
if (!proInterface && !isEmbedded && connected.length < 2) return;
if (connected.length === 0) return;
// The Pro sidebar lists each login's shared mailboxes under it (#1486);
// Mountain View's account popover only counts the login's own unread.
// the account switcher and the badge only count the login's own unread.
const withShared = proInterface || isEmbedded;
if (lastActive.current !== null && lastActive.current !== activeAccountId) {
@@ -68,5 +71,32 @@ export function useProMultiAccountMailboxes(): void {
if (!ok && loaded.current.get(id) === client) loaded.current.delete(id);
});
}
}, [proInterface, isEmbedded, mountainView, connectedSignature, activeAccountId]);
}, [proInterface, isEmbedded, connectedSignature, activeAccountId]);
// The active login's counts live in `mailboxes` and follow its push there.
// A background login's change re-reads its list. The mail view's own push
// handler (createMailPushListener) drops that login's cached list on the
// push itself, so this is a fetch, or nothing when the all-accounts views
// have already read and published the list again.
useEffect(() => {
const timers = new Map<string, ReturnType<typeof setTimeout>>();
const stop = subscribePushStateChanges((change, client, role) => {
if (role === "active" || !changesAnyType(change, ["Mailbox", "Email"])) return;
const id = [...loaded.current].find(([, c]) => c === client)?.[0];
if (!id || timers.has(id)) return;
timers.set(id, setTimeout(() => {
timers.delete(id);
if (loaded.current.get(id) !== client) return;
const withShared = useSettingsStore.getState().proInterface || isEmbedded;
void loadAccountMailboxes(client, id, { withShared });
}, BACKGROUND_REFRESH_DELAY_MS));
});
return () => {
stop();
for (const timer of timers.values()) clearTimeout(timer);
};
}, [isEmbedded]);
}
/** How long a background login's changes are gathered before its folders are read again. */
const BACKGROUND_REFRESH_DELAY_MS = 1500;
+16
View File
@@ -174,6 +174,22 @@ export function accountRowText(
return { title: name, subtitle: address };
}
/**
* The name an account's avatar takes its initials from. Usually the account's
* name. When another account has the same name, both would show the same two
* letters, so the initials come from the address instead: its local part split
* at dots, dashes, underscores and plus signs ("dso-ux-mail-b" gives "DB",
* "dso-ux-mail" gives "DM").
*/
export function accountAvatarName(account: NamedAccount, accounts: NamedAccount[]): string {
const name = account.displayName || account.label;
const { title, subtitle } = accountRowText(account, accounts);
if (subtitle !== null || title !== (account.email || account.username)) return name;
const local = title.split('@')[0] ?? title;
const words = local.split(/[.\-_+]+/).filter(Boolean);
return words.length > 0 ? words.join(' ') : name || title;
}
/** Minimal shape needed to order accounts (structural — avoids importing AccountEntry). */
export interface OrderableAccount {
id: string;
+1 -1
View File
@@ -147,7 +147,7 @@ export const SHARED_SETTINGS = [
'filenameSpaceReplacement', 'filenameLowercase', 'filenameStripDiacritics', 'filenameCollapseSeparators', 'postExportAction',
'recipientMentionsEnabled', 'mentionAddsRecipientTo', 'rtlEditingSupport', 'quickReplyEnabled', 'replyIdentityMatch',
'attachmentReminderEnabled', 'attachmentReminderKeywords', 'emptySubjectWarningEnabled', 'placeholderWarningEnabled',
'notificationSoundChoice', 'pushNotifyInboxOnly', 'faviconUnreadBadge', 'calendarNotificationSound', 'doNotDisturb', 'visualAlerts',
'notificationSoundChoice', 'pushNotifyInboxOnly', 'faviconUnreadBadge', 'unreadBadgeAllAccounts', 'calendarNotificationSound', 'doNotDisturb', 'visualAlerts',
'calendarInvitationParsingEnabled', 'showTimeInMonthView', 'calendarTintedEvents', 'calendarMonthTimedEvents',
'calendarLimitHours', 'calendarDayStartHour', 'calendarDayEndHour', 'calendarHideNonWorkingDays',
'calendarFreeScroll', 'calendarHoverPreview', 'showTasksOnCalendar',
+1
View File
@@ -306,6 +306,7 @@ const ENTRIES: SettingSchemaEntry[] = [
choice('notificationSoundChoice', 'notifications', `${NO}sound_selection.choose`, opts(`${NO}sounds.`, NOTIFICATION_SOUND_IDS)),
bool('pushNotifyInboxOnly', 'notifications', `${NO}email.inbox_only`),
bool('faviconUnreadBadge', 'notifications', `${AP}favicon_unread_badge.label`),
bool('unreadBadgeAllAccounts', 'notifications', 'account_unread.badge_all_accounts.label'),
bool('calendarNotificationsEnabled', 'notifications', `${NO}calendar.enabled`),
bool('calendarNotificationSound', 'notifications', `${NO}calendar.sound`),
bool('doNotDisturb', 'accessibility', `${A}do_not_disturb.label`),
+1
View File
@@ -86,6 +86,7 @@ export const tabSearchEntries: Record<Tab, SettingsSearchEntry[]> = {
{ label: 'settings.notifications.email.enabled' },
{ label: 'settings.notifications.email.sound' },
{ label: 'settings.appearance.favicon_unread_badge.label' },
{ label: 'account_unread.badge_all_accounts.label' },
{ label: 'settings.notifications.calendar.title' },
{ label: 'settings.notifications.calendar.enabled' },
{ label: 'settings.notifications.sound_selection.title' },
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "تم استخدام {percent}% من {total}",
"accessibility": "إمكانية الوصول"
},
"account_unread": {
"others": "غير مقروءة في الحسابات الأخرى: {count}",
"badge_all_accounts": {
"label": "احتساب البريد غير المقروء في كل الحسابات",
"description": "الرقم على علامة التبويب وأيقونة التطبيق وشريط النظام يجمع صناديق الوارد لكل الحسابات المسجّل دخولها."
}
},
"protocol_handlers": {
"title": "التطبيقات الافتراضية",
"description": "اختر ما إذا كانت روابط البريد والتقويم تُفتح في {appName}. من الناحية التقنية، يسجّل {appName} نفسه كمعالج بروتوكول لروابط mailto: وwebcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% de {total} utilitzat",
"accessibility": "Accessibilitat"
},
"account_unread": {
"others": "Sense llegir en altres comptes: {count}",
"badge_all_accounts": {
"label": "Compta el correu no llegit de tots els comptes",
"description": "El número de la pestanya, la icona de l'aplicació i la safata suma les safates d'entrada de tots els comptes amb la sessió iniciada."
}
},
"protocol_handlers": {
"title": "Aplicacions predeterminades",
"description": "Trieu si els enllaços de correu i calendari s'obren al {appName}. Tècnicament, el {appName} es registra com a gestor de protocol per als enllaços mailto: i webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Využito {percent} % z {total}",
"accessibility": "Přístupnost"
},
"account_unread": {
"others": "Nepřečtené v ostatních účtech: {count}",
"badge_all_accounts": {
"label": "Počítat nepřečtenou poštu ve všech účtech",
"description": "Číslo na kartě, na ikoně aplikace a v oznamovací oblasti sčítá doručené složky všech přihlášených účtů."
}
},
"protocol_handlers": {
"title": "Výchozí aplikace",
"description": "Zvolte, zda se mají e-mailové a kalendářové odkazy otevírat v aplikaci {appName}. Technicky se {appName} registruje jako obslužná aplikace protokolu pro odkazy mailto: a webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% af {total} brugt",
"accessibility": "Tilgængelighed"
},
"account_unread": {
"others": "Ulæste i andre konti: {count}",
"badge_all_accounts": {
"label": "Tæl ulæst post i alle konti",
"description": "Tallet på fanen, appikonet og i proceslinjen lægger indbakkerne for alle loggede-ind konti sammen."
}
},
"protocol_handlers": {
"title": "Standardapps",
"description": "Vælg om e-mail- og kalenderlinks åbnes i {appName}. Teknisk set registrerer {appName} sig som protokolhåndtering for mailto:- og webcal:-links.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent} % von {total} belegt",
"accessibility": "Barrierefreiheit"
},
"account_unread": {
"others": "Ungelesen in anderen Konten: {count}",
"badge_all_accounts": {
"label": "Ungelesene Mails aller Konten zählen",
"description": "Die Zahl am Tab, am App-Symbol und im Infobereich zählt die Posteingänge aller angemeldeten Konten zusammen."
}
},
"protocol_handlers": {
"title": "Standard-Apps",
"description": "Legen Sie fest, ob E-Mail- und Kalender-Links in {appName} geöffnet werden. Technisch registriert sich {appName} dafür als Protokoll-Handler für mailto: und webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% of {total} used",
"accessibility": "Accessibility"
},
"account_unread": {
"others": "Unread in other accounts: {count}",
"badge_all_accounts": {
"label": "Count unread mail in all accounts",
"description": "The number on the tab, the app icon and the tray adds up the inboxes of every signed-in account."
}
},
"protocol_handlers": {
"title": "Default apps",
"description": "Choose whether email and calendar links open in {appName}. Technically, {appName} registers as a protocol handler for mailto: and webcal: links.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% de {total} usado",
"accessibility": "Accesibilidad"
},
"account_unread": {
"others": "Sin leer en otras cuentas: {count}",
"badge_all_accounts": {
"label": "Contar el correo sin leer de todas las cuentas",
"description": "El número de la pestaña, el icono de la aplicación y la bandeja del sistema suma las bandejas de entrada de todas las cuentas con sesión iniciada."
}
},
"protocol_handlers": {
"title": "Aplicaciones predeterminadas",
"description": "Elige si los enlaces de correo y calendario se abren en {appName}. Técnicamente, {appName} se registra como controlador de protocolo para enlaces mailto: y webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% mahust {total} kasutatud",
"accessibility": "Juurdepääsetavus"
},
"account_unread": {
"others": "Lugemata teistes kontodes: {count}",
"badge_all_accounts": {
"label": "Loe kõigi kontode lugemata kirjad kokku",
"description": "Vahekaardi, rakenduse ikooni ja teavitusala arv liidab kokku kõigi sisselogitud kontode postkastid."
}
},
"protocol_handlers": {
"title": "Vaikerakendused",
"description": "Vali, kas e-posti ja kalendri lingid avanevad rakenduses {appName}. Tehniliselt registreerib {appName} end mailto: ja webcal: linkide protokollikäsitlejana.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}٪ از {total} استفاده شده",
"accessibility": "دسترس‌پذیری"
},
"account_unread": {
"others": "خوانده‌نشده در حساب‌های دیگر: {count}",
"badge_all_accounts": {
"label": "شمارش نامه‌های خوانده‌نشده همه حساب‌ها",
"description": "عدد روی زبانه، نماد برنامه و سینی سیستم، صندوق‌های ورودی همه حساب‌های واردشده را با هم جمع می‌کند."
}
},
"protocol_handlers": {
"title": "برنامه‌های پیش‌فرض",
"description": "انتخاب کنید لینک‌های ایمیل و تقویم در {appName} باز شوند.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent} % de {total} utilisés",
"accessibility": "Accessibilité"
},
"account_unread": {
"others": "Non lus dans les autres comptes : {count}",
"badge_all_accounts": {
"label": "Compter les messages non lus de tous les comptes",
"description": "Le nombre sur l'onglet, l'icône de l'application et la zone de notification additionne les boîtes de réception de tous les comptes connectés."
}
},
"protocol_handlers": {
"title": "Applications par défaut",
"description": "Choisissez si les liens d'e-mail et de calendrier s'ouvrent dans {appName}. Techniquement, {appName} s'enregistre comme gestionnaire de protocole pour les liens mailto: et webcal:.",
+7
View File
@@ -214,6 +214,13 @@
"mv_storage_used": "נוצלו {percent}% מתוך {total}",
"accessibility": "נגישות"
},
"account_unread": {
"others": "לא נקראו בחשבונות האחרים: {count}",
"badge_all_accounts": {
"label": "ספירת דואר שלא נקרא בכל החשבונות",
"description": "המספר בכרטיסייה, בסמל האפליקציה ובמגש המערכת מחבר את תיבות הדואר הנכנס של כל החשבונות המחוברים."
}
},
"sidebar_apps": {
"modal_title": "אפליקציות בסרגל הצד",
"add_new": "הוסף אפליקציה",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{total} {percent}%-a foglalt",
"accessibility": "Kisegítő lehetőségek"
},
"account_unread": {
"others": "Olvasatlan a többi fiókban: {count}",
"badge_all_accounts": {
"label": "Az összes fiók olvasatlan leveleinek számlálása",
"description": "A lapon, az alkalmazásikonon és a tálcán látható szám az összes bejelentkezett fiók beérkezett leveleit adja össze."
}
},
"protocol_handlers": {
"title": "Alapértelmezett alkalmazások",
"description": "Válaszd ki, hogy az e-mail és naptár hivatkozások a(z) {appName} alkalmazásban nyíljanak meg. Technikailag a(z) {appName} protokollkezelőként regisztrálja magát a mailto: és webcal: hivatkozásokhoz.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% di {total} utilizzato",
"accessibility": "Accessibilità"
},
"account_unread": {
"others": "Non letti negli altri account: {count}",
"badge_all_accounts": {
"label": "Conta la posta non letta di tutti gli account",
"description": "Il numero sulla scheda, sull'icona dell'app e nell'area di notifica somma le caselle di posta in arrivo di tutti gli account collegati."
}
},
"protocol_handlers": {
"title": "App predefinite",
"description": "Scegli se i link e-mail e calendario devono aprirsi in {appName}. Tecnicamente, {appName} si registra come gestore di protocollo per i link mailto: e webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{total} のうち {percent}% を使用中",
"accessibility": "アクセシビリティ"
},
"account_unread": {
"others": "他のアカウントの未読: {count}",
"badge_all_accounts": {
"label": "すべてのアカウントの未読を数える",
"description": "タブ、アプリのアイコン、通知領域の数字に、サインイン中のすべてのアカウントの受信トレイを合計して表示します。"
}
},
"protocol_handlers": {
"title": "既定のアプリ",
"description": "メールとカレンダーのリンクを {appName} で開くかどうかを選択します。技術的には、{appName} は mailto: と webcal: リンクのプロトコル ハンドラーとして登録されます。",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{total} 중 {percent}% 사용",
"accessibility": "접근성"
},
"account_unread": {
"others": "다른 계정의 읽지 않은 메일: {count}",
"badge_all_accounts": {
"label": "모든 계정의 읽지 않은 메일 세기",
"description": "탭, 앱 아이콘, 알림 영역의 숫자에 로그인한 모든 계정의 받은편지함을 합산해 표시해요."
}
},
"protocol_handlers": {
"title": "기본 앱",
"description": "이메일 및 캘린더 링크를 {appName}에서 열지 선택하세요. 기술적으로 {appName}은(는) mailto: 및 webcal: 링크의 프로토콜 핸들러로 등록됩니다.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Izmantoti {percent}% no {total}",
"accessibility": "Pieejamība"
},
"account_unread": {
"others": "Nelasītas citos kontos: {count}",
"badge_all_accounts": {
"label": "Skaitīt nelasītās vēstules visos kontos",
"description": "Skaitlis cilnē, lietotnes ikonā un paziņojumu apgabalā saskaita visu pierakstīto kontu iesūtnes."
}
},
"protocol_handlers": {
"title": "Noklusējuma lietotnes",
"description": "Izvēlieties, vai e-pasta un kalendāra saites atvērt {appName}. Tehniski {appName} reģistrējas kā protokola apstrādātājs mailto: un webcal: saitēm.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{total}-с {percent}% ашигласан",
"accessibility": "Хүртээмж"
},
"account_unread": {
"others": "Бусад бүртгэлд уншаагүй: {count}",
"badge_all_accounts": {
"label": "Бүх бүртгэлийн уншаагүй захидлыг тоолох",
"description": "Таб, аппын дүрс болон мэдэгдлийн хэсэг дээрх тоо нь нэвтэрсэн бүх бүртгэлийн ирсэн захидлыг нэгтгэнэ."
}
},
"protocol_handlers": {
"title": "Өгөгдмөл програмууд",
"description": "И-мэйл болон хуанлийн холбоосууд {appName} дээр нээгдэх эсэхийг сонгоно уу. Техникийн хувьд {appName} нь mailto: болон webcal: холбоосуудад протокол зохицуулагчаар бүртгүүлдэг.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent} % av {total} brukt",
"accessibility": "Tilgjengelighet"
},
"account_unread": {
"others": "Uleste i andre kontoer: {count}",
"badge_all_accounts": {
"label": "Tell ulest e-post i alle kontoer",
"description": "Tallet på fanen, appikonet og i systemstatusfeltet legger sammen innboksene til alle påloggede kontoer."
}
},
"protocol_handlers": {
"title": "Standardapper",
"description": "Velg om e-post- og kalenderlenker skal åpnes i {appName}. Teknisk sett registrerer {appName} seg som protokollbehandler for mailto:- og webcal:-lenker.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% van {total} gebruikt",
"accessibility": "Toegankelijkheid"
},
"account_unread": {
"others": "Ongelezen in andere accounts: {count}",
"badge_all_accounts": {
"label": "Ongelezen mail van alle accounts tellen",
"description": "Het getal op het tabblad, het app-pictogram en in het systeemvak telt de inboxen van alle aangemelde accounts bij elkaar op."
}
},
"protocol_handlers": {
"title": "Standaardapps",
"description": "Kies of e-mail- en kalenderlinks in {appName} worden geopend. Technisch registreert {appName} zich als protocolhandler voor mailto:- en webcal:-links.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Wykorzystano {percent}% z {total}",
"accessibility": "Dostępność"
},
"account_unread": {
"others": "Nieprzeczytane na innych kontach: {count}",
"badge_all_accounts": {
"label": "Licz nieprzeczytaną pocztę ze wszystkich kont",
"description": "Liczba na karcie, ikonie aplikacji i w zasobniku sumuje skrzynki odbiorcze wszystkich zalogowanych kont."
}
},
"protocol_handlers": {
"title": "Aplikacje domyślne",
"description": "Wybierz, czy linki e-mail i kalendarza mają otwierać się w {appName}. Technicznie {appName} rejestruje się jako obsługa protokołu dla linków mailto: i webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% de {total} utilizado",
"accessibility": "Acessibilidade"
},
"account_unread": {
"others": "Não lidas em outras contas: {count}",
"badge_all_accounts": {
"label": "Contar os e-mails não lidos de todas as contas",
"description": "O número na aba, no ícone do app e na bandeja do sistema soma as caixas de entrada de todas as contas conectadas."
}
},
"protocol_handlers": {
"title": "Aplicativos padrão",
"description": "Escolha se links de e-mail e calendário devem abrir no {appName}. Tecnicamente, o {appName} se registra como manipulador de protocolo para links mailto: e webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{percent}% din {total} folosit",
"accessibility": "Accesibilitate"
},
"account_unread": {
"others": "Necitite în alte conturi: {count}",
"badge_all_accounts": {
"label": "Numără mesajele necitite din toate conturile",
"description": "Numărul de pe filă, de pe pictograma aplicației și din zona de notificare adună căsuțele de primire ale tuturor conturilor conectate."
}
},
"protocol_handlers": {
"title": "Aplicații implicite",
"description": "Alegeți dacă linkurile către e-mail și calendar se deschid în {appName}. Din punct de vedere tehnic, {appName} se înregistrează ca gestionar de protocol pentru linkurile de tip mailto: și webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Использовано {percent}% из {total}",
"accessibility": "Специальные возможности"
},
"account_unread": {
"others": "Непрочитанные в других аккаунтах: {count}",
"badge_all_accounts": {
"label": "Считать непрочитанные письма во всех аккаунтах",
"description": "Число на вкладке, на значке приложения и в трее складывает входящие всех аккаунтов, в которые выполнен вход."
}
},
"protocol_handlers": {
"title": "Приложения по умолчанию",
"description": "Выберите, должны ли ссылки электронной почты и календаря открываться в {appName}. Технически {appName} регистрируется как обработчик протокола для ссылок mailto: и webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Využité {percent} % z {total}",
"accessibility": "Prístupnosť"
},
"account_unread": {
"others": "Neprečítané v ostatných účtoch: {count}",
"badge_all_accounts": {
"label": "Počítať neprečítanú poštu vo všetkých účtoch",
"description": "Číslo na karte, na ikone aplikácie a v oblasti oznámení sčítava doručenú poštu všetkých prihlásených účtov."
}
},
"protocol_handlers": {
"title": "Predvolené aplikácie",
"description": "Vyberte, či sa e-mailové a kalendárové odkazy otvárajú v aplikácii {appName}. {appName} sa registruje ako obslužná aplikácia protokolu pre odkazy mailto: a webcal:.",
+7
View File
@@ -345,6 +345,13 @@
"mv_storage_used": "ใช้ไป {percent}% จาก {total}",
"accessibility": "การเข้าถึง"
},
"account_unread": {
"others": "ยังไม่ได้อ่านในบัญชีอื่น: {count}",
"badge_all_accounts": {
"label": "นับอีเมลที่ยังไม่ได้อ่านของทุกบัญชี",
"description": "ตัวเลขบนแท็บ ไอคอนแอป และถาดระบบ จะรวมกล่องจดหมายเข้าของทุกบัญชีที่ลงชื่อเข้าใช้"
}
},
"language": {
"title": "ภาษา",
"english": "English",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "{total} alanın %{percent} kadarı kullanıldı",
"accessibility": "Erişilebilirlik"
},
"account_unread": {
"others": "Diğer hesaplarda okunmamış: {count}",
"badge_all_accounts": {
"label": "Tüm hesaplardaki okunmamış postaları say",
"description": "Sekmedeki, uygulama simgesindeki ve sistem tepsisindeki sayı, oturum açılmış tüm hesapların gelen kutularını toplar."
}
},
"protocol_handlers": {
"title": "Varsayılan uygulamalar",
"description": "E-posta ve takvim bağlantılarının {appName} uygulamasında açılıp açılmayacağını seçin. Teknik olarak {appName}, mailto: ve webcal: bağlantıları için protokol işleyicisi olarak kaydolur.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Використано {percent}% з {total}",
"accessibility": "Доступність"
},
"account_unread": {
"others": "Непрочитані в інших облікових записах: {count}",
"badge_all_accounts": {
"label": "Рахувати непрочитані листи в усіх облікових записах",
"description": "Число на вкладці, на значку застосунку та в треї підсумовує вхідні всіх облікових записів, у які виконано вхід."
}
},
"protocol_handlers": {
"title": "Програми за замовчуванням",
"description": "Виберіть, чи відкривати посилання електронної пошти та календаря в {appName}. Технічно {appName} реєструється як обробник протоколу для посилань mailto: і webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "Đã dùng {percent}% dung lượng {total}",
"accessibility": "Hỗ trợ tiếp cận"
},
"account_unread": {
"others": "Chưa đọc trong các tài khoản khác: {count}",
"badge_all_accounts": {
"label": "Đếm thư chưa đọc của mọi tài khoản",
"description": "Con số trên tab, biểu tượng ứng dụng và khay hệ thống cộng hộp thư đến của mọi tài khoản đã đăng nhập."
}
},
"protocol_handlers": {
"title": "Ứng dụng mặc định",
"description": "Chọn xem các liên kết email và lịch có mở trong {appName} hay không. Về mặt kỹ thuật, {appName} đăng ký làm trình xử lý giao thức cho các liên kết mailto: và webcal:.",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "已使用 {total} 中的 {percent}%",
"accessibility": "無障礙"
},
"account_unread": {
"others": "其他帳號中的未讀郵件:{count}",
"badge_all_accounts": {
"label": "計算所有帳號的未讀郵件",
"description": "分頁、應用程式圖示和系統匣上的數字會合計所有已登入帳號的收件匣。"
}
},
"protocol_handlers": {
"title": "預設應用程式",
"description": "選擇電子郵件與行事曆連結是否要在 {appName} 開啟。{appName} 會註冊為 mailto: 與 webcal: 連結的通訊協定處理常式。",
+7
View File
@@ -215,6 +215,13 @@
"mv_storage_used": "已使用 {total} 中的 {percent}%",
"accessibility": "无障碍"
},
"account_unread": {
"others": "其他账户中的未读邮件:{count}",
"badge_all_accounts": {
"label": "统计所有账户的未读邮件",
"description": "标签页、应用图标和托盘上的数字会合计所有已登录账户的收件箱。"
}
},
"protocol_handlers": {
"title": "默认应用",
"description": "选择是否在 {appName} 中打开电子邮件和日历链接。从技术上讲,{appName} 会注册为 mailto: 和 webcal: 链接的协议处理程序。",
+3
View File
@@ -763,6 +763,7 @@ interface SettingsState extends AccessibilitySettings {
senderFavicons: boolean;
showAvatarsInJunk: boolean; // Show profile images/favicons in the junk folder
faviconUnreadBadge: boolean; // Badge the browser-tab icon with the inbox unread count
unreadBadgeAllAccounts: boolean; // That badge (and the desktop tray) counts every signed-in account's inbox, not only the open one
// Sidebar
colorfulSidebarIcons: boolean; // Tint folder icons by role (inbox blue, junk red, etc.)
@@ -1068,6 +1069,7 @@ const DEFAULT_SETTINGS = {
senderFavicons: true,
showAvatarsInJunk: false,
faviconUnreadBadge: true,
unreadBadgeAllAccounts: true,
// Sidebar
colorfulSidebarIcons: true,
@@ -1483,6 +1485,7 @@ export const useSettingsStore = create<SettingsState>()(
senderFavicons: state.senderFavicons,
showAvatarsInJunk: state.showAvatarsInJunk,
faviconUnreadBadge: state.faviconUnreadBadge,
unreadBadgeAllAccounts: state.unreadBadgeAllAccounts,
colorfulSidebarIcons: state.colorfulSidebarIcons,
tintListRowsByTag: state.tintListRowsByTag,
tintListRowsByAccount: state.tintListRowsByAccount,