import { ArchiveIcon, BellIcon, CheckIcon, GlobeIcon, InfoIcon, LogOutIcon, MoreVerticalIcon, PaletteIcon, SettingsIcon, SquareUserIcon, User2Icon, } from "lucide-react"; import { matchPath, useLocation } from "react-router-dom"; import { useAppSidebar } from "@/contexts/AppSidebarContext"; import { useAuth } from "@/contexts/AuthContext"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useSSEConnectionStatus } from "@/hooks/useLiveMemoRefresh"; import useNavigateTo from "@/hooks/useNavigateTo"; import { useNotifications, useUpdateUserGeneralSetting } from "@/hooks/useUserQueries"; import { cn } from "@/lib/utils"; import { Routes } from "@/router"; import { UserNotification_Status } from "@/types/proto/api/v1/user_service_pb"; import { getLocaleWithFallback, loadLocale, useTranslate } from "@/utils/i18n"; import { getThemeWithFallback, loadTheme, THEME_OPTIONS } from "@/utils/theme"; import { SIDEBAR_LEADING_SLOT_CLASSES, sidebarSurfaceVariants } from "./AppSidebar/sidebar-layout"; import { LocaleSearchList } from "./LocalePicker"; import UserAvatar from "./UserAvatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; interface Props { collapsed?: boolean; } const UserMenu = (props: Props) => { const { collapsed } = props; const t = useTranslate(); const location = useLocation(); const navigateTo = useNavigateTo(); const { setMobileOpen } = useAppSidebar(); const currentUser = useCurrentUser(); const { userGeneralSetting, refetchSettings, logout } = useAuth(); const { mutate: updateUserGeneralSetting } = useUpdateUserGeneralSetting(currentUser?.name); const { data: notifications = [] } = useNotifications(); const sseStatus = useSSEConnectionStatus(); const currentLocale = getLocaleWithFallback(userGeneralSetting?.locale); const currentTheme = getThemeWithFallback(userGeneralSetting?.theme); const inboxActive = Boolean(matchPath(Routes.INBOX, location.pathname)); const archivedActive = Boolean(matchPath(Routes.ARCHIVED, location.pathname)); const unreadCount = notifications.filter((notification) => notification.status === UserNotification_Status.UNREAD).length; const userLabel = currentUser?.displayName || currentUser?.username || t("common.profile"); const triggerLabel = `${userLabel}, ${t("common.more")}${unreadCount > 0 ? `, ${unreadCount} ${t("inbox.unread")}` : ""}`; const inboxLabel = unreadCount > 0 ? `${t("common.inbox")}, ${unreadCount} ${t("inbox.unread")}` : t("common.inbox"); const handleLocaleChange = async (locale: Locale) => { if (!currentUser) return; // Apply locale immediately for instant UI feedback and persist to localStorage loadLocale(locale); // Persist to user settings updateUserGeneralSetting( { generalSetting: { locale }, updateMask: ["locale"] }, { onSuccess: () => { refetchSettings(); }, }, ); }; const handleThemeChange = async (theme: string) => { if (!currentUser) return; // Apply theme immediately for instant UI feedback loadTheme(theme); // Persist to user settings updateUserGeneralSetting( { generalSetting: { theme }, updateMask: ["theme"] }, { onSuccess: () => { refetchSettings(); }, }, ); }; const handleSignOut = async () => { // First, clear auth state and cache BEFORE doing anything else await logout(); try { // Then clear user-specific localStorage items // Preserve app-wide settings (theme, locale, view preferences, tag view settings) const keysToPreserve = ["memos-theme", "memos-locale", "memos-view-setting", "tag-view-as-tree"]; const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && !keysToPreserve.includes(key)) { keysToRemove.push(key); } } keysToRemove.forEach((key) => localStorage.removeItem(key)); } catch { // Ignore errors from localStorage operations } // Always redirect to auth page (use replace to prevent back navigation) window.location.replace(Routes.AUTH); }; const navigateFromMenu = (path: string) => { setMobileOpen(false); navigateTo(path); }; return (
{currentUser?.avatarUrl ? ( ) : ( )} {sseStatus !== "connected" && ( } /> {t(`live-update.${sseStatus}` as Parameters[0])} )}
{!collapsed && ( {userLabel} )} {!collapsed && ( {unreadCount > 0 && ( )}
navigateFromMenu(`/u/${encodeURIComponent(currentUser?.username ?? "")}`)}> {t("common.profile")} navigateFromMenu(Routes.INBOX)} > {t("common.inbox")} {unreadCount > 0 && ( )} navigateFromMenu(Routes.ARCHIVED)} > {t("common.archived")} {t("common.language")} {t("setting.preference.theme")} {THEME_OPTIONS.map((option) => ( handleThemeChange(option.value)}> {currentTheme === option.value && } {currentTheme !== option.value && } {option.label} ))} navigateFromMenu(Routes.ABOUT)}> {t("common.about")} navigateFromMenu(Routes.SETTING)}> {t("common.settings")} {t("common.sign-out")}
); }; export default UserMenu;