diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index d6b2f3447..46c90670a 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -61,6 +61,7 @@ import { MAILBOX_DRAG_MIME } from "@/components/pro/pro-shell-drop"; import { useTagDrop } from "@/hooks/use-tag-drop"; import { useUIStore } from "@/stores/ui-store"; import { useAuthStore } from "@/stores/auth-store"; +import { unifiedLoadProgress } from "@/lib/unified-mailbox"; import { useVacationStore } from "@/stores/vacation-store"; import { useSettingsStore, getKeywordVisibility } from "@/stores/settings-store"; import { useEmailStore } from "@/stores/email-store"; @@ -406,6 +407,7 @@ function SidebarSectionHeader({ sub, testId, onContextMenu, + status, }: { label: string; expanded: boolean; @@ -418,6 +420,8 @@ function SidebarSectionHeader({ sub?: boolean; testId?: string; onContextMenu?: (event: React.MouseEvent) => void; + /** A short note after the label, e.g. how many accounts have loaded. */ + status?: ReactNode; }) { if (isCollapsed) { return first ? null :
; @@ -451,6 +455,7 @@ function SidebarSectionHeader({ {label} + {status} {onSettings && ( s.unifiedScope); + const restoringAccounts = useAuthStore((s) => s.restoringAccounts); + const registeredAccounts = useAccountStore((s) => s.accounts); + const unifiedProgress = useMemo(() => unifiedLoadProgress({ + crossAccountActive, restoring: restoringAccounts, scope: unifiedScope, accounts: registeredAccounts, + }), [crossAccountActive, restoringAccounts, registeredAccounts, unifiedScope]); const t = useTranslations('sidebar'); useEffect(() => { @@ -1254,6 +1269,16 @@ export function Sidebar({ onToggle={toggleUnified} isCollapsed={isCollapsed} first + status={unifiedProgress && ( + + {unifiedProgress.loaded}/{unifiedProgress.total} + + )} /> {((unifiedExpanded && !isCollapsed) || isCollapsed) && ( <> diff --git a/lib/__tests__/unified-load-progress.test.ts b/lib/__tests__/unified-load-progress.test.ts new file mode 100644 index 000000000..7b9c02e92 --- /dev/null +++ b/lib/__tests__/unified-load-progress.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { unifiedLoadProgress } from '@/lib/unified-mailbox'; + +// While a browser restores its logins the unified counts cover only the ones +// already back. The sidebar says how many, so a partial total is not read as +// the whole - and stops saying it the moment the restore is over. +const scope = (...ids: string[]) => ids.map((id) => ({ clientAccountId: id, isShared: false })); +const accounts = (n: number, failed = 0) => + Array.from({ length: n }, (_, i) => ({ hasError: i < failed })); + +describe('unifiedLoadProgress', () => { + it('counts the logins already in the scope against those expected', () => { + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: true, scope: scope('a', 'b', 'c', 'd'), accounts: accounts(10) })) + .toEqual({ loaded: 4, total: 10 }); + }); + + it('leaves out logins whose restore failed', () => { + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: true, scope: scope('a', 'b'), accounts: accounts(4, 1) })) + .toEqual({ loaded: 2, total: 3 }); + }); + + it('counts a login once however many shared owners it brings', () => { + const withShared = [...scope('a'), { clientAccountId: 'a', isShared: true }, { clientAccountId: 'a', isShared: true }]; + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: true, scope: withShared, accounts: accounts(3) })) + .toEqual({ loaded: 1, total: 3 }); + }); + + it('says nothing once the scope is complete, the restore is over, or there is one login', () => { + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: true, scope: scope('a', 'b'), accounts: accounts(2) })).toBeNull(); + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: false, scope: scope('a'), accounts: accounts(5) })).toBeNull(); + expect(unifiedLoadProgress({ crossAccountActive: true, restoring: true, scope: [], accounts: accounts(1) })).toBeNull(); + }); + + it('says nothing when the unified mailbox stays within one login', () => { + expect(unifiedLoadProgress({ crossAccountActive: false, restoring: true, scope: scope('a'), accounts: accounts(5) })).toBeNull(); + }); +}); diff --git a/lib/unified-mailbox.ts b/lib/unified-mailbox.ts index 062208fe9..f65303c35 100644 --- a/lib/unified-mailbox.ts +++ b/lib/unified-mailbox.ts @@ -722,3 +722,22 @@ export function connectedAccountsGrew(previous: string | null, current: string): const before = new Set(previous ? previous.split(',').filter(Boolean) : []); return current.split(',').some((id) => id !== '' && !before.has(id)); } + +/** + * How far the unified scope has got while logins are still reconnecting: + * `{ loaded, total }` while some logins expected in it are not there yet, + * `null` once it is complete, when it spans only one login, or when no + * restore is running (a login that did not make it must not keep the note up). + */ +export function unifiedLoadProgress(input: { + crossAccountActive: boolean; + restoring: boolean; + scope: Pick[]; + accounts: { hasError?: boolean }[]; +}): { loaded: number; total: number } | null { + if (!input.crossAccountActive || !input.restoring) return null; + const total = input.accounts.filter((a) => !a.hasError).length; + if (total < 2) return null; + const loaded = new Set(input.scope.filter((e) => !e.isShared).map((e) => e.clientAccountId)).size; + return loaded < total ? { loaded, total } : null; +} diff --git a/locales/ar/common.json b/locales/ar/common.json index 47f70f28b..b5942aa1c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -120,6 +120,7 @@ "unified_archive": "كل الأرشيف", "unified_junk": "كل البريد المزعج", "all_accounts": "كل الحسابات", + "all_accounts_loading": "جارٍ تحميل الحسابات: {loaded} من {total}", "unified_mailbox": "صندوق البريد الموحّد", "expand": "توسيع", "collapse": "طي", diff --git a/locales/ca/common.json b/locales/ca/common.json index ac53ba2fe..7980d9384 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -120,6 +120,7 @@ "unified_archive": "Tot l'arxiu", "unified_junk": "Tot el correu brossa", "all_accounts": "Tots els comptes", + "all_accounts_loading": "Carregant comptes: {loaded} de {total}", "unified_mailbox": "Bústia unificada", "expand": "Desplega", "collapse": "Replega", diff --git a/locales/cs/common.json b/locales/cs/common.json index a29ce634c..e75d45a38 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -120,6 +120,7 @@ "unified_archive": "Všechny archivy", "unified_junk": "Všechen spam", "all_accounts": "Všechny účty", + "all_accounts_loading": "Načítání účtů: {loaded} z {total}", "unified_mailbox": "Sjednocená schránka", "expand": "Rozbalit", "collapse": "Sbalit", diff --git a/locales/da/common.json b/locales/da/common.json index 42b3776e9..74645b094 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -120,6 +120,7 @@ "unified_archive": "Alle arkiver", "unified_junk": "Alt spam", "all_accounts": "Alle konti", + "all_accounts_loading": "Indlæser konti: {loaded} af {total}", "unified_mailbox": "Samlet postkasse", "expand": "Udvid", "collapse": "Skjul", diff --git a/locales/de/common.json b/locales/de/common.json index 54147f319..2555d7790 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -120,6 +120,7 @@ "unified_archive": "Alle Archive", "unified_junk": "Alle Spam", "all_accounts": "Alle Konten", + "all_accounts_loading": "Konten werden geladen: {loaded} von {total}", "unified_mailbox": "Gemeinsames Postfach", "expand": "Erweitern", "collapse": "Einklappen", diff --git a/locales/en/common.json b/locales/en/common.json index a3323e62c..ad2c3f45e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -120,6 +120,7 @@ "unified_archive": "All Archive", "unified_junk": "All Junk", "all_accounts": "All Accounts", + "all_accounts_loading": "Loading accounts: {loaded} of {total}", "unified_mailbox": "Unified Mailbox", "expand": "Expand", "collapse": "Collapse", diff --git a/locales/es/common.json b/locales/es/common.json index 72ec44153..21a79d162 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -120,6 +120,7 @@ "unified_archive": "Todos los archivos", "unified_junk": "Todo el spam", "all_accounts": "Todas las cuentas", + "all_accounts_loading": "Cargando cuentas: {loaded} de {total}", "unified_mailbox": "Buzón unificado", "expand": "Expandir", "collapse": "Contraer", diff --git a/locales/fa/common.json b/locales/fa/common.json index 2fead4e4c..723dfdfd7 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -120,6 +120,7 @@ "unified_archive": "همه بایگانی‌ها", "unified_junk": "همه هرزنامه‌ها", "all_accounts": "همه حساب‌ها", + "all_accounts_loading": "در حال بارگیری حساب‌ها: {loaded} از {total}", "unified_mailbox": "صندوق پستی یکپارچه", "expand": "باز کردن", "collapse": "جمع کردن", diff --git a/locales/fr/common.json b/locales/fr/common.json index 2efcf454a..2d8e737f8 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -120,6 +120,7 @@ "unified_archive": "Toutes les archives", "unified_junk": "Tous les indésirables", "all_accounts": "Tous les comptes", + "all_accounts_loading": "Chargement des comptes : {loaded} sur {total}", "unified_mailbox": "Boîte aux lettres unifiée", "expand": "Développer", "collapse": "Réduire", diff --git a/locales/he/common.json b/locales/he/common.json index 0bfd3df48..234e6b769 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -142,6 +142,7 @@ "unified_archive": "הכל ארכיון", "unified_junk": "הכל דואר זבל", "all_accounts": "כל החשבונות", + "all_accounts_loading": "טוען חשבונות: {loaded} מתוך {total}", "shared": "משותף", "scheduled": "מתוכננים", "unified_all_unread": "הכל שלא נקראו", diff --git a/locales/hu/common.json b/locales/hu/common.json index 704beb680..98ac6fcc5 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -120,6 +120,7 @@ "unified_archive": "Összes archívum", "unified_junk": "Összes spam", "all_accounts": "Minden fiók", + "all_accounts_loading": "Fiókok betöltése: {loaded} / {total}", "unified_mailbox": "Egységes postafiók", "expand": "Kibontás", "collapse": "Összecsukás", diff --git a/locales/it/common.json b/locales/it/common.json index 1e1e4335f..3f7671ebf 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -120,6 +120,7 @@ "unified_archive": "Tutti gli archivi", "unified_junk": "Tutto lo spam", "all_accounts": "Tutti gli account", + "all_accounts_loading": "Caricamento account: {loaded} di {total}", "unified_mailbox": "Casella di posta unificata", "expand": "Espandi", "collapse": "Comprimi", diff --git a/locales/ja/common.json b/locales/ja/common.json index 76cca8f72..299da4724 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -120,6 +120,7 @@ "unified_archive": "すべてのアーカイブ", "unified_junk": "すべての迷惑メール", "all_accounts": "すべてのアカウント", + "all_accounts_loading": "アカウントを読み込み中: {total} 件中 {loaded} 件", "unified_mailbox": "統合メールボックス", "expand": "展開", "collapse": "折りたたむ", diff --git a/locales/ko/common.json b/locales/ko/common.json index 4533a4c43..5e4f69096 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -120,6 +120,7 @@ "unified_archive": "모든 보관함", "unified_junk": "모든 스팸함", "all_accounts": "모든 계정", + "all_accounts_loading": "계정 불러오는 중: {total}개 중 {loaded}개", "unified_mailbox": "통합 메일함", "expand": "펼치기", "collapse": "접기", diff --git a/locales/lv/common.json b/locales/lv/common.json index 90cf433d1..627ced780 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -120,6 +120,7 @@ "unified_archive": "Visi arhīvi", "unified_junk": "Viss mēstules", "all_accounts": "Visi konti", + "all_accounts_loading": "Ielādē kontus: {loaded} no {total}", "unified_mailbox": "Apvienotā pastkaste", "expand": "Izvērst", "collapse": "Sairt", diff --git a/locales/mn/common.json b/locales/mn/common.json index 25d779f07..a494a2a8d 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -120,6 +120,7 @@ "unified_archive": "Бүх архив", "unified_junk": "Бүх хог", "all_accounts": "Бүх дансууд", + "all_accounts_loading": "Бүртгэлүүдийг ачаалж байна: {total}-аас {loaded}", "unified_mailbox": "Нэгдсэн шуудангийн хайрцаг", "expand": "Өргөтгөх", "collapse": "Нуналт", diff --git a/locales/nb/common.json b/locales/nb/common.json index 9a1afb0e6..acdc41371 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -120,6 +120,7 @@ "unified_archive": "Alle arkiver", "unified_junk": "All søppelpost", "all_accounts": "Alle kontoer", + "all_accounts_loading": "Laster inn kontoer: {loaded} av {total}", "unified_mailbox": "Samlet postkasse", "expand": "Utvid", "collapse": "Fold sammen", diff --git a/locales/nl/common.json b/locales/nl/common.json index 965ebb1f9..3470bc713 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -120,6 +120,7 @@ "unified_archive": "Alle archieven", "unified_junk": "Alle spam", "all_accounts": "Alle accounts", + "all_accounts_loading": "Accounts laden: {loaded} van {total}", "unified_mailbox": "Gecombineerd postvak", "expand": "Uitklappen", "collapse": "Inklappen", diff --git a/locales/pl/common.json b/locales/pl/common.json index fc105f237..6e087623d 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -120,6 +120,7 @@ "unified_archive": "Wszystkie archiwa", "unified_junk": "Wszystkie spam", "all_accounts": "Wszystkie konta", + "all_accounts_loading": "Wczytywanie kont: {loaded} z {total}", "unified_mailbox": "Wspólna skrzynka", "expand": "Rozwiń", "collapse": "Zwiń", diff --git a/locales/pt/common.json b/locales/pt/common.json index 9aa5aba90..56ba1e162 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -120,6 +120,7 @@ "unified_archive": "Todos os arquivos", "unified_junk": "Todo o spam", "all_accounts": "Todas as contas", + "all_accounts_loading": "A carregar contas: {loaded} de {total}", "unified_mailbox": "Caixa de correio unificada", "expand": "Expandir", "collapse": "Recolher", diff --git a/locales/ro/common.json b/locales/ro/common.json index 2bbf38651..f00cb6e54 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -120,6 +120,7 @@ "unified_archive": "Arhivă completă", "unified_junk": "Toate mesajele nedorite", "all_accounts": "Toate conturile", + "all_accounts_loading": "Se încarcă conturile: {loaded} din {total}", "unified_mailbox": "Căsuță poștală unificată", "expand": "Extindeți", "collapse": "Reduceți", diff --git a/locales/ru/common.json b/locales/ru/common.json index 1691cd9d0..f636e4b19 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -120,6 +120,7 @@ "unified_archive": "Все архивы", "unified_junk": "Весь спам", "all_accounts": "Все аккаунты", + "all_accounts_loading": "Загрузка аккаунтов: {loaded} из {total}", "unified_mailbox": "Общий почтовый ящик", "expand": "Развернуть", "collapse": "Свернуть", diff --git a/locales/sk/common.json b/locales/sk/common.json index 36494f77c..63f90755e 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -121,6 +121,7 @@ "unified_archive": "Všetky archívy", "unified_junk": "Všetok spam", "all_accounts": "Všetky účty", + "all_accounts_loading": "Načítavajú sa účty: {loaded} z {total}", "expand": "Rozbaliť", "collapse": "Zbaliť", "expand_tooltip": "Rozbaliť", diff --git a/locales/tr/common.json b/locales/tr/common.json index 74ab00670..4d35b4958 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -120,6 +120,7 @@ "unified_archive": "Tüm Arşiv", "unified_junk": "Tüm Önemsiz", "all_accounts": "Tüm Hesaplar", + "all_accounts_loading": "Hesaplar yükleniyor: {loaded}/{total}", "unified_mailbox": "Birleşik Posta Kutusu", "expand": "Genişlet", "collapse": "Daralt", diff --git a/locales/uk/common.json b/locales/uk/common.json index f3ac459f0..6434bf637 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -120,6 +120,7 @@ "unified_archive": "Усі архіви", "unified_junk": "Весь спам", "all_accounts": "Усі облікові записи", + "all_accounts_loading": "Завантаження облікових записів: {loaded} з {total}", "unified_mailbox": "Спільна поштова скринька", "expand": "Розгорнути", "collapse": "Згорнути", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 3ac3b27af..a207746ac 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -120,6 +120,7 @@ "unified_archive": "所有封存郵件", "unified_junk": "所有垃圾郵件", "all_accounts": "所有帳號", + "all_accounts_loading": "正在載入帳號:{loaded}/{total}", "unified_mailbox": "整合信箱", "expand": "展開", "collapse": "收合", diff --git a/locales/zh/common.json b/locales/zh/common.json index 2fd64f7c6..6983d8e17 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -120,6 +120,7 @@ "unified_archive": "所有归档", "unified_junk": "所有垃圾邮件", "all_accounts": "所有账户", + "all_accounts_loading": "正在加载账户:{loaded}/{total}", "unified_mailbox": "统一邮箱", "expand": "展开", "collapse": "收起", diff --git a/stores/auth-store.ts b/stores/auth-store.ts index ba05aed93..b1c2eb4c3 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -72,6 +72,12 @@ interface AuthState { * (push notifications) re-run over the now-complete client set. */ connectedAccountsRevision: number; + /** + * True while the logins other than the one on screen are still being + * restored after a load, so views that span every login (the unified + * mailbox) can say they are not complete yet. + */ + restoringAccounts: boolean; login: (serverUrl: string, username: string, password: string, totp?: string, rememberMe?: boolean) => Promise; /** @@ -1248,6 +1254,7 @@ export const useAuthStore = create()( activeAccountId: null, isDemoMode: false, connectedAccountsRevision: 0, + restoringAccounts: false, login: async (serverUrl, typedUsername, password, totp, rememberMe) => { set({ isLoading: true, error: null, isRateLimited: false, rateLimitUntil: null }); @@ -2649,8 +2656,9 @@ export const useAuthStore = create()( if (clients.has(targetId)) { if (otherAccounts.length > 0) { + set({ restoringAccounts: true }); void restoreRemaining().then(() => { - set((state) => ({ connectedAccountsRevision: state.connectedAccountsRevision + 1 })); + set((state) => ({ connectedAccountsRevision: state.connectedAccountsRevision + 1, restoringAccounts: false })); }); } } else {