feat: add options to hide account switcher and show account avatars on navigation rail
This commit is contained in:
@@ -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<HTMLButtonElement>(null);
|
||||
const logoutPopoverRef = useRef<HTMLDivElement>(null);
|
||||
const [logoutPopoverStyle, setLogoutPopoverStyle] = useState<React.CSSProperties>({});
|
||||
|
||||
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({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{onLogout && (
|
||||
{onLogout && showRailAccountList && accounts.length > 0 && (
|
||||
<>
|
||||
<div className="w-8 border-t" style={{ borderColor: 'rgba(128, 128, 128, 0.3)' }} />
|
||||
|
||||
{/* Account circles */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{accounts.map((account) => {
|
||||
const isActive = account.id === activeAccountId;
|
||||
const initials = getInitials(account.displayName || account.label, account.email || account.username);
|
||||
return (
|
||||
<button
|
||||
key={account.id}
|
||||
onClick={() => {
|
||||
if (!isActive) switchAccount(account.id);
|
||||
}}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center w-9 h-9 rounded-full text-white text-xs font-medium transition-all flex-shrink-0",
|
||||
isActive
|
||||
? "ring-2 ring-primary ring-offset-2 ring-offset-background"
|
||||
: "opacity-70 hover:opacity-100"
|
||||
)}
|
||||
style={{ backgroundColor: account.avatarColor }}
|
||||
title={`${account.displayName || account.label} (${account.email || account.username})`}
|
||||
>
|
||||
{initials}
|
||||
{isActive && (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-primary flex items-center justify-center">
|
||||
<Check className="w-2 h-2 text-primary-foreground" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Logout button with popover */}
|
||||
<button
|
||||
ref={logoutBtnRef}
|
||||
onClick={() => setLogoutMenuOpen(!logoutMenuOpen)}
|
||||
className="flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
title={t("sign_out")}
|
||||
aria-expanded={logoutMenuOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{logoutMenuOpen && createPortal(
|
||||
<div
|
||||
ref={logoutPopoverRef}
|
||||
style={logoutPopoverStyle}
|
||||
className="w-56 rounded-lg border border-border bg-background text-foreground shadow-lg z-50 overflow-hidden"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
onClick={() => { setLogoutMenuOpen(false); logout(); }}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 text-sm text-foreground hover:bg-muted transition-colors"
|
||||
role="menuitem"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
{t("sign_out")}
|
||||
</button>
|
||||
{accounts.length > 1 && (
|
||||
<button
|
||||
onClick={() => { setLogoutMenuOpen(false); logoutAll(); }}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 text-sm text-destructive hover:bg-muted transition-colors"
|
||||
role="menuitem"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
{t("sign_out_all")}
|
||||
</button>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{onLogout && !showRailAccountList && (
|
||||
<AccountSwitcher variant="rail" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 ? <ChevronsRight className="w-4 h-4" /> : <ChevronsLeft className="w-4 h-4" />}
|
||||
</Button>
|
||||
|
||||
{!isCollapsed && (
|
||||
{!isCollapsed && !hideAccountSwitcher && (
|
||||
<AccountSwitcher variant="expanded" className="flex-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Hide Account Switcher */}
|
||||
<SettingItem label={t('hide_account_switcher.label')} description={t('hide_account_switcher.description')}>
|
||||
<ToggleSwitch
|
||||
checked={hideAccountSwitcher}
|
||||
onChange={(checked) => updateSetting('hideAccountSwitcher', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Show Rail Account List */}
|
||||
<SettingItem label={t('show_rail_account_list.label')} description={t('show_rail_account_list.description')}>
|
||||
<ToggleSwitch
|
||||
checked={showRailAccountList}
|
||||
onChange={(checked) => updateSetting('showRailAccountList', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Animations */}
|
||||
{!isSettingHidden('animationsEnabled') && (
|
||||
<SettingItem label={t('animations.label')} description={t('animations.description')} locked={isSettingLocked('animationsEnabled')}>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<SettingsState>()(
|
||||
showTimeInMonthView: state.showTimeInMonthView,
|
||||
showWeekNumbers: state.showWeekNumbers,
|
||||
toolbarPosition: state.toolbarPosition,
|
||||
hideAccountSwitcher: state.hideAccountSwitcher,
|
||||
showRailAccountList: state.showRailAccountList,
|
||||
senderFavicons: state.senderFavicons,
|
||||
folderIcons: state.folderIcons,
|
||||
emailKeywords: state.emailKeywords,
|
||||
|
||||
Reference in New Issue
Block a user