From 9140110435288f4d70b4c8db79f185f4c763bf72 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:20:25 +0100 Subject: [PATCH] feat: add options to hide account switcher and show account avatars on navigation rail --- components/layout/navigation-rail.tsx | 127 +++++++++++++++++++- components/layout/sidebar.tsx | 3 +- components/settings/appearance-settings.tsx | 18 ++- locales/en/common.json | 8 ++ stores/settings-store.ts | 6 + 5 files changed, 158 insertions(+), 4 deletions(-) diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx index c997a7d4..5e78e395 100644 --- a/components/layout/navigation-rail.tsx +++ b/components/layout/navigation-rail.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; -import { Mail, Calendar, BookUser, HardDrive, Settings, Keyboard, Plus, Shield } from "lucide-react"; +import { Mail, Calendar, BookUser, HardDrive, Settings, Keyboard, Plus, Shield, LogOut, Check } from "lucide-react"; import { AccountSwitcher } from "./account-switcher"; import { icons as lucideIcons, type LucideIcon } from "lucide-react"; import { useConfig } from "@/hooks/use-config"; @@ -16,6 +16,8 @@ import { useWebDAVStore } from "@/stores/webdav-store"; import { useSettingsStore } from "@/stores/settings-store"; import { usePolicyStore } from "@/stores/policy-store"; import { useAuthStore } from "@/stores/auth-store"; +import { useAccountStore } from "@/stores/account-store"; +import { getInitials } from "@/lib/account-utils"; import { cn, formatFileSize } from "@/lib/utils"; import { PluginSlot } from "@/components/plugins/plugin-slot"; @@ -164,11 +166,54 @@ export function NavigationRail({ const { mailboxes } = useEmailStore(); const { supportsWebDAV } = useWebDAVStore(); const sidebarApps = useSettingsStore((s) => s.sidebarApps); + const showRailAccountList = useSettingsStore((s) => s.showRailAccountList); const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled')); const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : []; const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0; const [isStalwartAdmin, setIsStalwartAdmin] = useState(false); + // Account list for rail + const accounts = useAccountStore((s) => s.accounts); + const activeAccountId = useAccountStore((s) => s.activeAccountId); + const switchAccount = useAuthStore((s) => s.switchAccount); + const logout = useAuthStore((s) => s.logout); + const logoutAll = useAuthStore((s) => s.logoutAll); + const [logoutMenuOpen, setLogoutMenuOpen] = useState(false); + const logoutBtnRef = useRef(null); + const logoutPopoverRef = useRef(null); + const [logoutPopoverStyle, setLogoutPopoverStyle] = useState({}); + + const updateLogoutPosition = useCallback(() => { + if (!logoutBtnRef.current) return; + const rect = logoutBtnRef.current.getBoundingClientRect(); + setLogoutPopoverStyle({ + position: "fixed", + left: rect.right + 8, + bottom: Math.max(8, window.innerHeight - rect.bottom), + }); + }, []); + + useEffect(() => { + if (!logoutMenuOpen) return; + updateLogoutPosition(); + const handleClickOutside = (e: MouseEvent) => { + if ( + logoutBtnRef.current?.contains(e.target as Node) || + logoutPopoverRef.current?.contains(e.target as Node) + ) return; + setLogoutMenuOpen(false); + }; + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") setLogoutMenuOpen(false); + }; + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [logoutMenuOpen, updateLogoutPosition]); + useEffect(() => { let cancelled = false; const { client } = useAuthStore.getState(); @@ -525,7 +570,85 @@ export function NavigationRail({ )} - {onLogout && ( + {onLogout && showRailAccountList && accounts.length > 0 && ( + <> +
+ + {/* Account circles */} +
+ {accounts.map((account) => { + const isActive = account.id === activeAccountId; + const initials = getInitials(account.displayName || account.label, account.email || account.username); + return ( + + ); + })} +
+ + {/* Logout button with popover */} + + + {logoutMenuOpen && createPortal( +
+ + {accounts.length > 1 && ( + + )} +
, + document.body + )} + + )} + + {onLogout && !showRailAccountList && ( )}
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index b11fcb90..cca675d9 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -438,6 +438,7 @@ export function Sidebar({ } catch { return true; } }); const emailKeywords = useSettingsStore(s => s.emailKeywords); + const hideAccountSwitcher = useSettingsStore(s => s.hideAccountSwitcher); const tagCounts = useEmailStore(s => s.tagCounts); const t = useTranslations('sidebar'); @@ -539,7 +540,7 @@ export function Sidebar({ {isCollapsed ? : } - {!isCollapsed && ( + {!isCollapsed && !hideAccountSwitcher && ( )} diff --git a/components/settings/appearance-settings.tsx b/components/settings/appearance-settings.tsx index ead997af..02ad83bc 100644 --- a/components/settings/appearance-settings.tsx +++ b/components/settings/appearance-settings.tsx @@ -67,7 +67,7 @@ export function AppearanceSettings() { const t = useTranslations('settings.appearance'); const tTour = useTranslations('tour'); const { theme, setTheme } = useThemeStore(); - const { fontSize, density, animationsEnabled, toolbarPosition, showToolbarLabels, updateSetting } = useSettingsStore(); + const { fontSize, density, animationsEnabled, toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, updateSetting } = useSettingsStore(); const { startTour, resetTourCompletion } = useTour(); const { isSettingLocked, isSettingHidden } = usePolicyStore(); @@ -145,6 +145,22 @@ export function AppearanceSettings() { /> + {/* Hide Account Switcher */} + + updateSetting('hideAccountSwitcher', checked)} + /> + + + {/* Show Rail Account List */} + + updateSetting('showRailAccountList', checked)} + /> + + {/* Animations */} {!isSettingHidden('animationsEnabled') && ( diff --git a/locales/en/common.json b/locales/en/common.json index 578ac44f..915043b1 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -683,6 +683,14 @@ "toolbar_labels": { "label": "Show Toolbar Labels", "description": "Display text labels next to toolbar icons. Disable to save space once you are familiar with the icons." + }, + "hide_account_switcher": { + "label": "Hide Sidebar Account Switcher", + "description": "Hide the account selector at the top of the folder sidebar. You can still switch accounts from the bottom navigation rail." + }, + "show_rail_account_list": { + "label": "Show Account Avatars on Navigation Rail", + "description": "Display individual account circles at the bottom of the navigation rail for quick switching, with a sign-out button below." } }, "keywords": { diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 8a75158d..594b7bec 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -152,6 +152,8 @@ interface SettingsState { // Layout toolbarPosition: ToolbarPosition; showToolbarLabels: boolean; + hideAccountSwitcher: boolean; + showRailAccountList: boolean; // Email Display disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation @@ -273,6 +275,8 @@ const DEFAULT_SETTINGS = { // Layout toolbarPosition: 'top' as ToolbarPosition, showToolbarLabels: true, + hideAccountSwitcher: false, + showRailAccountList: false, // Email Display disableThreading: false, @@ -367,6 +371,8 @@ export const useSettingsStore = create()( showTimeInMonthView: state.showTimeInMonthView, showWeekNumbers: state.showWeekNumbers, toolbarPosition: state.toolbarPosition, + hideAccountSwitcher: state.hideAccountSwitcher, + showRailAccountList: state.showRailAccountList, senderFavicons: state.senderFavicons, folderIcons: state.folderIcons, emailKeywords: state.emailKeywords,