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 {