"use client"; 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 { useTranslations } from "next-intl"; import { useAccountStore, type AccountEntry } from "@/stores/account-store"; import { useAuthStore } from "@/stores/auth-store"; import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds } from "@/lib/account-utils"; import { cn } from "@/lib/utils"; import { useRouter } from "@/i18n/navigation"; import { Avatar } from "@/components/ui/avatar"; interface AccountSwitcherProps { /** "rail" = small avatar only (NavigationRail), "expanded" = avatar + name + email (Sidebar) */ variant?: "rail" | "expanded"; className?: string; } function AccountAvatar({ account, size = "sm" }: { account: AccountEntry; size?: "sm" | "md" }) { return ( ); } export function AccountSwitcher({ variant = "rail", className }: AccountSwitcherProps) { const t = useTranslations("sidebar"); const router = useRouter(); const [open, setOpen] = useState(false); const buttonRef = useRef(null); const popoverRef = useRef(null); const [popoverStyle, setPopoverStyle] = useState({}); const accounts = useAccountStore((s) => s.accounts); const setDefaultAccount = useAccountStore((s) => s.setDefaultAccount); const reorderAccounts = useAccountStore((s) => s.reorderAccounts); // Read activeAccountId from authStore so the selector matches the actually-loaded // session (primaryIdentity, JMAP client). accountStore.activeAccountId is a separate // persisted copy that can drift out of sync across hydration / partial persist writes. const activeAccountId = useAuthStore((s) => s.activeAccountId); const activeAccount = accounts.find((a) => a.id === activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const logout = useAuthStore((s) => s.logout); const logoutAll = useAuthStore((s) => s.logoutAll); const updatePosition = useCallback(() => { if (!buttonRef.current) return; const rect = buttonRef.current.getBoundingClientRect(); if (variant === "rail") { setPopoverStyle({ position: "fixed", left: rect.right + 8, bottom: Math.max(8, window.innerHeight - rect.bottom), }); } else { setPopoverStyle({ position: "fixed", left: rect.left, top: rect.bottom + 4, }); } }, [variant]); useEffect(() => { if (!open) return; updatePosition(); const handleClickOutside = (e: MouseEvent) => { if ( buttonRef.current?.contains(e.target as Node) || popoverRef.current?.contains(e.target as Node) ) return; setOpen(false); }; const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleEscape); }; }, [open, updatePosition]); const handleSwitch = async (accountId: string) => { if (accountId === activeAccountId) return; setOpen(false); await switchAccount(accountId); }; const handleAddAccount = () => { setOpen(false); router.push(`/login?mode=add-account` as never); }; const handleLogout = () => { setOpen(false); logout(); }; const handleLogoutAll = () => { setOpen(false); logoutAll(); }; const handleSetDefault = (accountId: string) => { setDefaultAccount(accountId); }; // Display order: default account pinned to the top, the rest reorderable. const displayAccounts = useMemo(() => sortDefaultFirst(accounts), [accounts]); // Drag-to-rearrange (non-default accounts only; the default stays pinned). const [dragId, setDragId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const resetDrag = () => { setDragId(null); setDragOverId(null); }; const handleDragStart = (e: React.DragEvent, id: string) => { setDragId(id); e.dataTransfer.effectAllowed = "move"; }; const handleDragOver = (e: React.DragEvent, overId: string) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (overId !== dragOverId) setDragOverId(overId); }; const handleDrop = (e: React.DragEvent, overId: string) => { e.preventDefault(); if (dragId) { const next = reorderNonDefaultIds(accounts, dragId, overId); if (next) reorderAccounts(next); } resetDrag(); }; // Show the account's own identity, not the preferred sending identity - // primaryIdentity can be an alias (e.g. info@korazo.net) that differs from // the actually logged-in account (info@linusrath.de). const displayName = activeAccount?.displayName || activeAccount?.label || ""; const displayEmail = activeAccount?.email || activeAccount?.username || ""; return ( <> {open && createPortal(
{/* Account List */}
{displayAccounts.map((account) => { const isActive = account.id === activeAccountId; const isDraggable = !account.isDefault && accounts.length > 2; return (
handleDragStart(e, account.id) : undefined} onDragOver={isDraggable ? (e) => handleDragOver(e, account.id) : undefined} onDrop={isDraggable ? (e) => handleDrop(e, account.id) : undefined} onDragEnd={isDraggable ? resetDrag : undefined} className={cn( "group/acct relative", dragId === account.id && "opacity-50", dragOverId === account.id && dragId !== account.id && "border-t-2 border-primary" )} > {isDraggable && ( )}
); })}
{/* Separator + Add Account */} {accounts.length < getMaxAccounts() && (
)} {/* Separator + Actions */}
{activeAccount && !activeAccount.isDefault && accounts.length > 1 && ( )} {accounts.length > 1 && ( )}
, document.body )} ); }