diff --git a/components/layout/account-switcher.tsx b/components/layout/account-switcher.tsx index 179aeddd..f8c1d6c6 100644 --- a/components/layout/account-switcher.tsx +++ b/components/layout/account-switcher.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { createPortal } from "react-dom"; -import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical } from "lucide-react"; +import { Check, Plus, LogOut, Star, ChevronDown, AlertCircle, GripVertical, X } from "lucide-react"; import { useTranslations } from "next-intl"; import { useAccountStore, type AccountEntry } from "@/stores/account-store"; import { useAuthStore } from "@/stores/auth-store"; @@ -48,6 +48,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher const activeAccount = accounts.find((a) => a.id === activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const logout = useAuthStore((s) => s.logout); + const removeAccount = useAuthStore((s) => s.removeAccount); const logoutAll = useAuthStore((s) => s.logoutAll); const updatePosition = useCallback(() => { @@ -100,6 +101,13 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher router.push(`/login?mode=add-account` as never); }; + const handleRemove = (e: React.MouseEvent, account: AccountEntry) => { + e.stopPropagation(); + const label = account.email || account.username; + if (!window.confirm(t("remove_account_confirm", { account: label }))) return; + removeAccount(account.id); + }; + const handleLogout = () => { setOpen(false); logout(); @@ -221,7 +229,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher className={cn( "w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors", isActive ? "bg-accent/50" : "hover:bg-muted", - isDraggable && "pe-7" + (!isActive && !account.isDefault) ? (isDraggable ? "pe-14" : "pe-8") : (isDraggable && "pe-7") )} role="menuitem" disabled={isActive} @@ -264,11 +272,22 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher {isDraggable && ( )} + {!isActive && !account.isDefault && ( + + )} ); })} diff --git a/locales/cs/common.json b/locales/cs/common.json index c50565f9..19273b00 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -138,7 +138,9 @@ "scheduled": "Naplánováno", "unified_all_unread": "Vše nepřečtené", "unified_all_starred": "Vše s hvězdičkou", - "unified_all_mail": "Veškerá pošta" + "unified_all_mail": "Veškerá pošta", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Výchozí aplikace", diff --git a/locales/da/common.json b/locales/da/common.json index d1956177..db1ca4ed 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -138,7 +138,9 @@ "scheduled": "Planlagt", "unified_all_unread": "Alle ulæste", "unified_all_starred": "Alle med stjerne", - "unified_all_mail": "Al post" + "unified_all_mail": "Al post", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Standardapps", diff --git a/locales/de/common.json b/locales/de/common.json index b4676020..5889ac66 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -138,7 +138,9 @@ "scheduled": "Geplant", "unified_all_unread": "Alle ungelesenen", "unified_all_starred": "Alle markierten", - "unified_all_mail": "Alle Mails" + "unified_all_mail": "Alle Mails", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Standard-Apps", diff --git a/locales/en/common.json b/locales/en/common.json index 9fbf67fb..ca9d8e70 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -138,7 +138,9 @@ "scheduled": "Scheduled", "unified_all_unread": "All unread", "unified_all_starred": "All starred", - "unified_all_mail": "All mail" + "unified_all_mail": "All mail", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Default apps", diff --git a/locales/es/common.json b/locales/es/common.json index 2a2b0ffd..d14a3c25 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -138,7 +138,9 @@ "scheduled": "Programados", "unified_all_unread": "Todo no leído", "unified_all_starred": "Todo destacado", - "unified_all_mail": "Todo el correo" + "unified_all_mail": "Todo el correo", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Aplicaciones predeterminadas", diff --git a/locales/fa/common.json b/locales/fa/common.json index 69982ebc..773d58cd 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -138,7 +138,9 @@ "scheduled": "زمان‌بندی شده", "unified_all_mail": "همه ایمیل‌ها", "unified_all_starred": "همه ستاره‌دارها", - "unified_all_unread": "همه خوانده‌نشده‌ها" + "unified_all_unread": "همه خوانده‌نشده‌ها", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "برنامه‌های پیش‌فرض", diff --git a/locales/fr/common.json b/locales/fr/common.json index 9603a2c5..b37907ff 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -138,7 +138,9 @@ "scheduled": "Planifiés", "unified_all_unread": "Tous les non lus", "unified_all_starred": "Tous les favoris", - "unified_all_mail": "Tout le courrier" + "unified_all_mail": "Tout le courrier", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Applications par défaut", diff --git a/locales/hu/common.json b/locales/hu/common.json index e45826d2..df095070 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -138,7 +138,9 @@ "scheduled": "Ütemezett", "unified_all_unread": "Összes olvasatlan", "unified_all_starred": "Összes csillagozott", - "unified_all_mail": "Összes levél" + "unified_all_mail": "Összes levél", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Alapértelmezett alkalmazások", diff --git a/locales/it/common.json b/locales/it/common.json index 7d276203..f6de43a3 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -138,7 +138,9 @@ "scheduled": "Programmate", "unified_all_unread": "Tutti i non letti", "unified_all_starred": "Tutti gli speciali", - "unified_all_mail": "Tutta la posta" + "unified_all_mail": "Tutta la posta", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "App predefinite", diff --git a/locales/ja/common.json b/locales/ja/common.json index 234390a5..ab7c653f 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -138,7 +138,9 @@ "scheduled": "予約済み", "unified_all_unread": "すべての未読", "unified_all_starred": "すべてのスター付き", - "unified_all_mail": "すべてのメール" + "unified_all_mail": "すべてのメール", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "既定のアプリ", diff --git a/locales/ko/common.json b/locales/ko/common.json index ef5723bb..80295ed7 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -138,7 +138,9 @@ "scheduled": "예약됨", "unified_all_unread": "모든 읽지 않음", "unified_all_starred": "모든 별표", - "unified_all_mail": "모든 메일" + "unified_all_mail": "모든 메일", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "기본 앱", diff --git a/locales/lv/common.json b/locales/lv/common.json index 22fddbe9..f1a233f4 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -138,7 +138,9 @@ "scheduled": "Ieplānots", "unified_all_unread": "Visi nelasītie", "unified_all_starred": "Visi ar zvaigzni", - "unified_all_mail": "Visas vēstules" + "unified_all_mail": "Visas vēstules", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Noklusējuma lietotnes", diff --git a/locales/nl/common.json b/locales/nl/common.json index 43c63cc0..4438c9ec 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -138,7 +138,9 @@ "scheduled": "Gepland", "unified_all_unread": "Alle ongelezen", "unified_all_starred": "Alle met ster", - "unified_all_mail": "Alle e-mail" + "unified_all_mail": "Alle e-mail", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Standaardapps", diff --git a/locales/pl/common.json b/locales/pl/common.json index 4c4656be..be6642d9 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -138,7 +138,9 @@ "scheduled": "Zaplanowane", "unified_all_unread": "Wszystkie nieprzeczytane", "unified_all_starred": "Wszystkie oznaczone gwiazdką", - "unified_all_mail": "Cała poczta" + "unified_all_mail": "Cała poczta", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Aplikacje domyślne", diff --git a/locales/pt/common.json b/locales/pt/common.json index 17453f63..81cce094 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -138,7 +138,9 @@ "scheduled": "Agendados", "unified_all_unread": "Tudo não lido", "unified_all_starred": "Tudo com estrela", - "unified_all_mail": "Todo o correio" + "unified_all_mail": "Todo o correio", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Aplicativos padrão", diff --git a/locales/ro/common.json b/locales/ro/common.json index 3c33493a..70a438c3 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -138,7 +138,9 @@ "scheduled": "Programat", "unified_all_unread": "Toate necitite", "unified_all_starred": "Toate cu stea", - "unified_all_mail": "Toată poșta" + "unified_all_mail": "Toată poșta", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Aplicații implicite", diff --git a/locales/ru/common.json b/locales/ru/common.json index f3859b57..4ad2a908 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -138,7 +138,9 @@ "scheduled": "Запланировано", "unified_all_unread": "Все непрочитанные", "unified_all_starred": "Все помеченные", - "unified_all_mail": "Вся почта" + "unified_all_mail": "Вся почта", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Приложения по умолчанию", diff --git a/locales/tr/common.json b/locales/tr/common.json index d6ab561e..2a4ac304 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -138,7 +138,9 @@ "scheduled": "Zamanlandı", "unified_all_unread": "Tüm okunmamışlar", "unified_all_starred": "Tüm yıldızlılar", - "unified_all_mail": "Tüm postalar" + "unified_all_mail": "Tüm postalar", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Varsayılan uygulamalar", diff --git a/locales/uk/common.json b/locales/uk/common.json index 95b1e129..63636400 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -138,7 +138,9 @@ "scheduled": "Заплановано", "unified_all_unread": "Усі непрочитані", "unified_all_starred": "Усі позначені", - "unified_all_mail": "Уся пошта" + "unified_all_mail": "Уся пошта", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "Програми за замовчуванням", diff --git a/locales/zh/common.json b/locales/zh/common.json index 637b257b..4c57d997 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -138,7 +138,9 @@ "scheduled": "已计划", "unified_all_unread": "全部未读", "unified_all_starred": "全部加星标", - "unified_all_mail": "全部邮件" + "unified_all_mail": "全部邮件", + "remove_account": "Remove account", + "remove_account_confirm": "Remove {account} from this device? You can add it back later." }, "protocol_handlers": { "title": "默认应用", diff --git a/stores/auth-store.ts b/stores/auth-store.ts index e195d65c..392216ae 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -44,6 +44,7 @@ interface AuthState { refreshAccessToken: () => Promise; logout: () => void; logoutAll: () => void; + removeAccount: (accountId: string) => void; switchAccount: (accountId: string) => Promise; checkAuth: () => Promise; clearError: () => void; @@ -1243,6 +1244,31 @@ export const useAuthStore = create()( redirectToLogin(); }, + // Remove a specific (typically non-active) account: tear down its client, + // drop it from the registry, and clear its per-slot cookies. If asked to + // remove the active account, defer to logout() which handles switching + // away or redirecting. + removeAccount: (accountId: string) => { + if (accountId === get().activeAccountId) { get().logout(); return; } + const accountStore = useAccountStore.getState(); + const account = accountStore.getAccountById(accountId); + if (!account) return; + const slot = account.cookieSlot ?? 0; + const wasOAuth = account.authMode === 'oauth'; + + clearRefreshTimer(accountId); + const client = clients.get(accountId); + if (client) { try { client.disconnect(); } catch { /* noop */ } } + clients.delete(accountId); + evictAccount(accountId); + accountStore.removeAccount(accountId); + + apiFetch(`/api/auth/session?slot=${slot}`, { method: 'DELETE', keepalive: true }).catch(() => {}); + if (wasOAuth) { + apiFetch(`/api/auth/token?slot=${slot}`, { method: 'DELETE', keepalive: true }).catch(() => {}); + } + }, + logoutAll: () => { // Disconnect all clients for (const c of clients.values()) {