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 { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { createPortal } from "react-dom";
|
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 { AccountSwitcher } from "./account-switcher";
|
||||||
import { icons as lucideIcons, type LucideIcon } from "lucide-react";
|
import { icons as lucideIcons, type LucideIcon } from "lucide-react";
|
||||||
import { useConfig } from "@/hooks/use-config";
|
import { useConfig } from "@/hooks/use-config";
|
||||||
@@ -16,6 +16,8 @@ import { useWebDAVStore } from "@/stores/webdav-store";
|
|||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
import { usePolicyStore } from "@/stores/policy-store";
|
import { usePolicyStore } from "@/stores/policy-store";
|
||||||
import { useAuthStore } from "@/stores/auth-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 { cn, formatFileSize } from "@/lib/utils";
|
||||||
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
import { PluginSlot } from "@/components/plugins/plugin-slot";
|
||||||
|
|
||||||
@@ -164,11 +166,54 @@ export function NavigationRail({
|
|||||||
const { mailboxes } = useEmailStore();
|
const { mailboxes } = useEmailStore();
|
||||||
const { supportsWebDAV } = useWebDAVStore();
|
const { supportsWebDAV } = useWebDAVStore();
|
||||||
const sidebarApps = useSettingsStore((s) => s.sidebarApps);
|
const sidebarApps = useSettingsStore((s) => s.sidebarApps);
|
||||||
|
const showRailAccountList = useSettingsStore((s) => s.showRailAccountList);
|
||||||
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
|
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
|
||||||
const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : [];
|
const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : [];
|
||||||
const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
|
const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
|
||||||
const [isStalwartAdmin, setIsStalwartAdmin] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const { client } = useAuthStore.getState();
|
const { client } = useAuthStore.getState();
|
||||||
@@ -525,7 +570,85 @@ export function NavigationRail({
|
|||||||
</span>
|
</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" />
|
<AccountSwitcher variant="rail" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ export function Sidebar({
|
|||||||
} catch { return true; }
|
} catch { return true; }
|
||||||
});
|
});
|
||||||
const emailKeywords = useSettingsStore(s => s.emailKeywords);
|
const emailKeywords = useSettingsStore(s => s.emailKeywords);
|
||||||
|
const hideAccountSwitcher = useSettingsStore(s => s.hideAccountSwitcher);
|
||||||
const tagCounts = useEmailStore(s => s.tagCounts);
|
const tagCounts = useEmailStore(s => s.tagCounts);
|
||||||
const t = useTranslations('sidebar');
|
const t = useTranslations('sidebar');
|
||||||
|
|
||||||
@@ -539,7 +540,7 @@ export function Sidebar({
|
|||||||
{isCollapsed ? <ChevronsRight className="w-4 h-4" /> : <ChevronsLeft className="w-4 h-4" />}
|
{isCollapsed ? <ChevronsRight className="w-4 h-4" /> : <ChevronsLeft className="w-4 h-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && !hideAccountSwitcher && (
|
||||||
<AccountSwitcher variant="expanded" className="flex-1" />
|
<AccountSwitcher variant="expanded" className="flex-1" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function AppearanceSettings() {
|
|||||||
const t = useTranslations('settings.appearance');
|
const t = useTranslations('settings.appearance');
|
||||||
const tTour = useTranslations('tour');
|
const tTour = useTranslations('tour');
|
||||||
const { theme, setTheme } = useThemeStore();
|
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 { startTour, resetTourCompletion } = useTour();
|
||||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||||
|
|
||||||
@@ -145,6 +145,22 @@ export function AppearanceSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</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 */}
|
{/* Animations */}
|
||||||
{!isSettingHidden('animationsEnabled') && (
|
{!isSettingHidden('animationsEnabled') && (
|
||||||
<SettingItem label={t('animations.label')} description={t('animations.description')} locked={isSettingLocked('animationsEnabled')}>
|
<SettingItem label={t('animations.label')} description={t('animations.description')} locked={isSettingLocked('animationsEnabled')}>
|
||||||
|
|||||||
@@ -683,6 +683,14 @@
|
|||||||
"toolbar_labels": {
|
"toolbar_labels": {
|
||||||
"label": "Show 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."
|
"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": {
|
"keywords": {
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ interface SettingsState {
|
|||||||
// Layout
|
// Layout
|
||||||
toolbarPosition: ToolbarPosition;
|
toolbarPosition: ToolbarPosition;
|
||||||
showToolbarLabels: boolean;
|
showToolbarLabels: boolean;
|
||||||
|
hideAccountSwitcher: boolean;
|
||||||
|
showRailAccountList: boolean;
|
||||||
|
|
||||||
// Email Display
|
// Email Display
|
||||||
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
|
disableThreading: boolean; // Show emails as individual messages instead of grouped by conversation
|
||||||
@@ -273,6 +275,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
// Layout
|
// Layout
|
||||||
toolbarPosition: 'top' as ToolbarPosition,
|
toolbarPosition: 'top' as ToolbarPosition,
|
||||||
showToolbarLabels: true,
|
showToolbarLabels: true,
|
||||||
|
hideAccountSwitcher: false,
|
||||||
|
showRailAccountList: false,
|
||||||
|
|
||||||
// Email Display
|
// Email Display
|
||||||
disableThreading: false,
|
disableThreading: false,
|
||||||
@@ -367,6 +371,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
showTimeInMonthView: state.showTimeInMonthView,
|
showTimeInMonthView: state.showTimeInMonthView,
|
||||||
showWeekNumbers: state.showWeekNumbers,
|
showWeekNumbers: state.showWeekNumbers,
|
||||||
toolbarPosition: state.toolbarPosition,
|
toolbarPosition: state.toolbarPosition,
|
||||||
|
hideAccountSwitcher: state.hideAccountSwitcher,
|
||||||
|
showRailAccountList: state.showRailAccountList,
|
||||||
senderFavicons: state.senderFavicons,
|
senderFavicons: state.senderFavicons,
|
||||||
folderIcons: state.folderIcons,
|
folderIcons: state.folderIcons,
|
||||||
emailKeywords: state.emailKeywords,
|
emailKeywords: state.emailKeywords,
|
||||||
|
|||||||
Reference in New Issue
Block a user