From 9ce7ac81724190c0a65f0b56e1fa36fd00a931eb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:06:37 +0100 Subject: [PATCH] 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. --- components/__tests__/favicon-badge.test.tsx | 47 +++++++++++ components/favicon-badge.tsx | 25 +++++- .../account-switcher-mountain-view.test.tsx | 10 ++- .../account-switcher-unread.test.tsx | 82 +++++++++++++++++++ components/layout/account-switcher.tsx | 40 ++++++--- components/layout/navigation-rail.tsx | 4 +- components/settings/notification-settings.tsx | 15 ++++ .../use-pro-multi-account-mailboxes.test.tsx | 39 ++++++++- hooks/use-pro-multi-account-mailboxes.ts | 48 +++++++++-- lib/account-utils.ts | 16 ++++ lib/plugin-sandbox/host-api-app.ts | 2 +- lib/settings-schema.ts | 1 + lib/settings-search.ts | 1 + locales/ar/common.json | 7 ++ locales/ca/common.json | 7 ++ locales/cs/common.json | 7 ++ locales/da/common.json | 7 ++ locales/de/common.json | 7 ++ locales/en/common.json | 7 ++ locales/es/common.json | 7 ++ locales/et/common.json | 7 ++ locales/fa/common.json | 7 ++ locales/fr/common.json | 7 ++ locales/he/common.json | 7 ++ locales/hu/common.json | 7 ++ locales/it/common.json | 7 ++ locales/ja/common.json | 7 ++ locales/ko/common.json | 7 ++ locales/lv/common.json | 7 ++ locales/mn/common.json | 7 ++ locales/nb/common.json | 7 ++ locales/nl/common.json | 7 ++ locales/pl/common.json | 7 ++ locales/pt/common.json | 7 ++ locales/ro/common.json | 7 ++ locales/ru/common.json | 7 ++ locales/sk/common.json | 7 ++ locales/th/common.json | 7 ++ locales/tr/common.json | 7 ++ locales/uk/common.json | 7 ++ locales/vi/common.json | 7 ++ locales/zh-TW/common.json | 7 ++ locales/zh/common.json | 7 ++ stores/settings-store.ts | 3 + 44 files changed, 511 insertions(+), 32 deletions(-) create mode 100644 components/layout/__tests__/account-switcher-unread.test.tsx diff --git a/components/__tests__/favicon-badge.test.tsx b/components/__tests__/favicon-badge.test.tsx index 651ee5578..dcf8cdab4 100644 --- a/components/__tests__/favicon-badge.test.tsx +++ b/components/__tests__/favicon-badge.test.tsx @@ -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(); + // 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(); + 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(); + expect(useAppBadgeMock).toHaveBeenLastCalledWith(11, true); + }); + }); + it('badges zero when there is no inbox yet', () => { useEmailStore.setState({ mailboxes: [] }); diff --git a/components/favicon-badge.tsx b/components/favicon-badge.tsx index 976de641d..1bc199925 100644 --- a/components/favicon-badge.tsx +++ b/components/favicon-badge.tsx @@ -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); +} diff --git a/components/layout/__tests__/account-switcher-mountain-view.test.tsx b/components/layout/__tests__/account-switcher-mountain-view.test.tsx index 5a099dc24..a404219f0 100644 --- a/components/layout/__tests__/account-switcher-mountain-view.test.tsx +++ b/components/layout/__tests__/account-switcher-mountain-view.test.tsx @@ -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(); 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(); }); }); diff --git a/components/layout/__tests__/account-switcher-unread.test.tsx b/components/layout/__tests__/account-switcher-unread.test.tsx new file mode 100644 index 000000000..f3d315f5d --- /dev/null +++ b/components/layout/__tests__/account-switcher-unread.test.tsx @@ -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 = {}) { + 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(); + 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(); + 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(); + expect(screen.queryByTestId('other-accounts-unread')).toBeNull(); + }); + + it('shows no count with a single account', () => { + useAccountStore.setState({ accounts: [account('work')] } as never); + render(); + 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(); + 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(); + }); +}); diff --git a/components/layout/account-switcher.tsx b/components/layout/account-switcher.tsx index cbb81ba9f..e4aebf87e 100644 --- a/components/layout/account-switcher.tsx +++ b/components/layout/account-switcher.tsx @@ -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 ( (null); @@ -297,12 +298,16 @@ export function AccountSwitcher({ const unreadByAccount: Record = 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 ? ( <> - + + + {othersUnreadText && ( + {variant === "expanded" && ( <>
@@ -382,7 +396,7 @@ export function AccountSwitcher({ {mvPopover && activeAccount && (
- +

{displayName}

{displaySubtitle &&

{displaySubtitle}

} @@ -455,7 +469,7 @@ export function AccountSwitcher({ disabled={isActive} >
- + {isActive && (
diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx index bd7dbcefb..dda7d0ad4 100644 --- a/components/layout/navigation-rail.tsx +++ b/components/layout/navigation-rail.tsx @@ -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)})`} > 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)} /> + + {/* Only means something with a second account. */} + {severalAccounts && ( + + updateSetting('unreadBadgeAllAccounts', checked)} + disabled={!faviconUnreadBadge} + /> + + )} diff --git a/hooks/__tests__/use-pro-multi-account-mailboxes.test.tsx b/hooks/__tests__/use-pro-multi-account-mailboxes.test.tsx index 223420172..7fc74e156 100644 --- a/hooks/__tests__/use-pro-multi-account-mailboxes.test.tsx +++ b/hooks/__tests__/use-pro-multi-account-mailboxes.test.tsx @@ -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); + }); }); diff --git a/hooks/use-pro-multi-account-mailboxes.ts b/hooks/use-pro-multi-account-mailboxes.ts index 60c8cf455..5487a5633 100644 --- a/hooks/use-pro-multi-account-mailboxes.ts +++ b/hooks/use-pro-multi-account-mailboxes.ts @@ -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>(); + 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; diff --git a/lib/account-utils.ts b/lib/account-utils.ts index 301333af0..2081326cf 100644 --- a/lib/account-utils.ts +++ b/lib/account-utils.ts @@ -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; diff --git a/lib/plugin-sandbox/host-api-app.ts b/lib/plugin-sandbox/host-api-app.ts index 67323171c..99544462c 100644 --- a/lib/plugin-sandbox/host-api-app.ts +++ b/lib/plugin-sandbox/host-api-app.ts @@ -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', diff --git a/lib/settings-schema.ts b/lib/settings-schema.ts index 995bb4e41..b5ac5b036 100644 --- a/lib/settings-schema.ts +++ b/lib/settings-schema.ts @@ -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`), diff --git a/lib/settings-search.ts b/lib/settings-search.ts index 982b15062..383a25826 100644 --- a/lib/settings-search.ts +++ b/lib/settings-search.ts @@ -86,6 +86,7 @@ export const tabSearchEntries: Record = { { 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' }, diff --git a/locales/ar/common.json b/locales/ar/common.json index ed9cbecb0..0537b16ed 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -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:.", diff --git a/locales/ca/common.json b/locales/ca/common.json index 5e3d4916d..0b3ee80b6 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -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:.", diff --git a/locales/cs/common.json b/locales/cs/common.json index ace3e4105..5929cbfd9 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -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:.", diff --git a/locales/da/common.json b/locales/da/common.json index b473c8379..6f3733da6 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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.", diff --git a/locales/de/common.json b/locales/de/common.json index 9cb2097f7..67ff209b4 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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:.", diff --git a/locales/en/common.json b/locales/en/common.json index 84f9389a7..d32a34bc9 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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.", diff --git a/locales/es/common.json b/locales/es/common.json index a44fce868..f7f50067d 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -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:.", diff --git a/locales/et/common.json b/locales/et/common.json index ace51f3b5..a40644ac4 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -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.", diff --git a/locales/fa/common.json b/locales/fa/common.json index 9961aab01..157117c51 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -215,6 +215,13 @@ "mv_storage_used": "{percent}٪ از {total} استفاده شده", "accessibility": "دسترس‌پذیری" }, + "account_unread": { + "others": "خوانده‌نشده در حساب‌های دیگر: {count}", + "badge_all_accounts": { + "label": "شمارش نامه‌های خوانده‌نشده همه حساب‌ها", + "description": "عدد روی زبانه، نماد برنامه و سینی سیستم، صندوق‌های ورودی همه حساب‌های واردشده را با هم جمع می‌کند." + } + }, "protocol_handlers": { "title": "برنامه‌های پیش‌فرض", "description": "انتخاب کنید لینک‌های ایمیل و تقویم در {appName} باز شوند.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 750e84417..c3829f87e 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -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:.", diff --git a/locales/he/common.json b/locales/he/common.json index 23cb11fa8..7ad373151 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -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": "הוסף אפליקציה", diff --git a/locales/hu/common.json b/locales/hu/common.json index 172fa6df6..67fc90fa7 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -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.", diff --git a/locales/it/common.json b/locales/it/common.json index 71f13cca0..29326a99f 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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:.", diff --git a/locales/ja/common.json b/locales/ja/common.json index 7d382e843..98020f31e 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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: リンクのプロトコル ハンドラーとして登録されます。", diff --git a/locales/ko/common.json b/locales/ko/common.json index 9f9643ad0..897e453ef 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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: 링크의 프로토콜 핸들러로 등록됩니다.", diff --git a/locales/lv/common.json b/locales/lv/common.json index a4bc8ef83..2599cc500 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -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.", diff --git a/locales/mn/common.json b/locales/mn/common.json index 152574c4e..68967ba2e 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -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: холбоосуудад протокол зохицуулагчаар бүртгүүлдэг.", diff --git a/locales/nb/common.json b/locales/nb/common.json index 9a6c14f76..bcda16c1d 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -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.", diff --git a/locales/nl/common.json b/locales/nl/common.json index 3c57a4007..9eb958686 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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.", diff --git a/locales/pl/common.json b/locales/pl/common.json index 20bd079ae..6712c74bc 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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:.", diff --git a/locales/pt/common.json b/locales/pt/common.json index 2452edf4a..dfc82a03e 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -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:.", diff --git a/locales/ro/common.json b/locales/ro/common.json index 03bfef66a..5f007a176 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -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:.", diff --git a/locales/ru/common.json b/locales/ru/common.json index ebb25a96f..be20722d5 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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:.", diff --git a/locales/sk/common.json b/locales/sk/common.json index 43343c99e..8e05da256 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -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:.", diff --git a/locales/th/common.json b/locales/th/common.json index d4aef2af2..a65253edd 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -345,6 +345,13 @@ "mv_storage_used": "ใช้ไป {percent}% จาก {total}", "accessibility": "การเข้าถึง" }, + "account_unread": { + "others": "ยังไม่ได้อ่านในบัญชีอื่น: {count}", + "badge_all_accounts": { + "label": "นับอีเมลที่ยังไม่ได้อ่านของทุกบัญชี", + "description": "ตัวเลขบนแท็บ ไอคอนแอป และถาดระบบ จะรวมกล่องจดหมายเข้าของทุกบัญชีที่ลงชื่อเข้าใช้" + } + }, "language": { "title": "ภาษา", "english": "English", diff --git a/locales/tr/common.json b/locales/tr/common.json index 03393f7f0..478699e25 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -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.", diff --git a/locales/uk/common.json b/locales/uk/common.json index f033cd1ee..a968e8ad0 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -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:.", diff --git a/locales/vi/common.json b/locales/vi/common.json index 9faa09bde..664a14d64 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -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:.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index eed67bf21..c613c620a 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -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: 連結的通訊協定處理常式。", diff --git a/locales/zh/common.json b/locales/zh/common.json index 0edeb8c81..938b5c782 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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: 链接的协议处理程序。", diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 1d771cb5f..9cb1d4e07 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -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()( senderFavicons: state.senderFavicons, showAvatarsInJunk: state.showAvatarsInJunk, faviconUnreadBadge: state.faviconUnreadBadge, + unreadBadgeAllAccounts: state.unreadBadgeAllAccounts, colorfulSidebarIcons: state.colorfulSidebarIcons, tintListRowsByTag: state.tintListRowsByTag, tintListRowsByAccount: state.tintListRowsByAccount,