diff --git a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx index e9aa770b9..74105a6d9 100644 --- a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx +++ b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx @@ -1,9 +1,26 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as React from "react" +import { useHotkeyScope, useTypeScriptHappyCallback } from "~/hooks/common" import { cn } from "~/lib/utils" -const DropdownMenu = DropdownMenuPrimitive.Root +const DropdownMenu: typeof DropdownMenuPrimitive.Root = (props) => { + const [open, setOpen] = React.useState(!!props.open) + useHotkeyScope("DropdownMenu", open) + + return ( + { + setOpen(open) + props.onOpenChange?.(open) + }, + [props.onOpenChange], + )} + /> + ) +} const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger @@ -54,20 +71,22 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) +>(({ className, sideOffset = 4, ...props }, ref) => { + return ( + + + + ) +}) DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName const DropdownMenuItem = React.forwardRef< diff --git a/apps/renderer/src/constants/hotkeys.ts b/apps/renderer/src/constants/hotkeys.ts index b0d21aaba..90a797b8b 100644 --- a/apps/renderer/src/constants/hotkeys.ts +++ b/apps/renderer/src/constants/hotkeys.ts @@ -2,4 +2,5 @@ export const HotKeyScopeMap = { Home: ["home"], Menu: ["menu"], Modal: ["modal"], + DropdownMenu: ["dropdown-menu"], } diff --git a/apps/renderer/src/hooks/common/useSwitchHotkeyScope.ts b/apps/renderer/src/hooks/common/useSwitchHotkeyScope.ts index 574380aa2..46c0c7477 100644 --- a/apps/renderer/src/hooks/common/useSwitchHotkeyScope.ts +++ b/apps/renderer/src/hooks/common/useSwitchHotkeyScope.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react" +import { useCallback, useLayoutEffect, useRef } from "react" import { useHotkeysContext } from "react-hotkeys-hook" import { HotKeyScopeMap } from "~/constants" @@ -18,9 +18,41 @@ export const useSwitchHotKeyScope = () => { for (const key of allScopes) { disableScope(key) } - - enableScope(nextScope[0]) + for (const key of nextScope) { + enableScope(key) + } }, [disableScope, enableScope], ) } +export const useHotkeyScopeFn = (scope: keyof typeof HotKeyScopeMap) => { + const { enableScope, disableScope, enabledScopes } = useHotkeysContext() + const currentScopeRef = useRef(enabledScopes) + + return useCallback(() => { + const currentScope = currentScopeRef.current + for (const key of allScopes) { + disableScope(key) + } + for (const key of scope) { + enableScope(key) + } + return () => { + for (const key of scope) { + disableScope(key) + } + for (const scope of currentScope) { + enableScope(scope) + } + } + }, [enableScope, disableScope, scope]) +} + +export const useHotkeyScope = (scope: keyof typeof HotKeyScopeMap, when: boolean) => { + const fn = useHotkeyScopeFn(scope) + useLayoutEffect(() => { + if (when) { + return fn() + } + }, [fn, when]) +} diff --git a/apps/renderer/src/modules/user/ProfileButton.tsx b/apps/renderer/src/modules/user/ProfileButton.tsx index b63005308..6b17be689 100644 --- a/apps/renderer/src/modules/user/ProfileButton.tsx +++ b/apps/renderer/src/modules/user/ProfileButton.tsx @@ -1,6 +1,6 @@ import { repository } from "@pkg" import type { FC } from "react" -import { memo, useState } from "react" +import { forwardRef, memo, useCallback, useLayoutEffect, useState } from "react" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" @@ -37,7 +37,7 @@ export const ProfileButton: FC = memo((props) => { const presentUserProfile = usePresentUserProfileModal("dialog") const presentAchievement = useAchievementModal() const { t } = useTranslation() - const [ref, { x, y }] = useMeasure() + const [dropdown, setDropdown] = useState(false) const navigate = useNavigate() @@ -50,9 +50,7 @@ export const ProfileButton: FC = memo((props) => { <> - - - + = memo((props) => { - {x !== 0 && y !== 0 && ( + + ) +}) +ProfileButton.displayName = "ProfileButton" + +const TransitionAvatar = forwardRef< + HTMLButtonElement, + { + stage: "zoom-in" | "" + } & React.HTMLAttributes +>(({ stage, ...props }, forwardRef) => { + const [ref, { x, y }, forceRefresh] = useMeasure() + const [avatarHovered, setAvatarHovered] = useState(false) + + const zoomIn = stage === "zoom-in" + const [currentZoomIn, setCurrentZoomIn] = useState(false) + useLayoutEffect(() => { + if (zoomIn) { + setCurrentZoomIn(true) + } + }, [zoomIn]) + + return ( + <> + { + forceRefresh() + setAvatarHovered(true) + }, [forceRefresh])} + onMouseLeave={useCallback(() => { + setAvatarHovered(false) + }, [])} + > + + + {x !== 0 && y !== 0 && (avatarHovered || zoomIn || currentZoomIn) && ( { + if (!zoomIn && currentZoomIn) { + setCurrentZoomIn(false) + } + }} /> )} ) }) -ProfileButton.displayName = "ProfileButton" function PowerButton() { const wallet = useWallet() diff --git a/apps/renderer/src/modules/user/UserAvatar.tsx b/apps/renderer/src/modules/user/UserAvatar.tsx index 503e44635..29c7cb67a 100644 --- a/apps/renderer/src/modules/user/UserAvatar.tsx +++ b/apps/renderer/src/modules/user/UserAvatar.tsx @@ -20,56 +20,67 @@ export const UserAvatar = forwardRef< hideName?: boolean userId?: string enableModal?: boolean - style?: React.CSSProperties - } & LoginProps ->(({ className, avatarClassName, hideName, userId, enableModal, style, ...props }, ref) => { - const { session, status } = useSession({ - enabled: !userId, - }) - const presentUserProfile = usePresentUserProfileModal("drawer") + } & LoginProps & + React.HTMLAttributes +>( + ( + { className, avatarClassName, hideName, userId, enableModal, style, onClick, ...props }, + ref, + ) => { + const { session, status } = useSession({ + enabled: !userId, + }) + const presentUserProfile = usePresentUserProfileModal("drawer") - const profile = useAuthQuery( - defineQuery(["profiles", userId], async () => { - const res = await apiClient.profiles.$get({ - query: { id: userId! }, - }) - return res.data - }), - { - enabled: !!userId, - }, - ) + const profile = useAuthQuery( + defineQuery(["profiles", userId], async () => { + const res = await apiClient.profiles.$get({ + query: { id: userId! }, + }) + return res.data + }), + { + enabled: !!userId, + }, + ) - if (!userId && status !== "authenticated") { - return - } + if (!userId && status !== "authenticated") { + return + } - const renderUserData = userId ? profile.data : session?.user - return ( -
presentUserProfile(userId) : undefined} - > - { + if (enableModal) { + presentUserProfile(userId) + } + onClick?.(e) + }} + {...props} className={cn( - "aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300", - avatarClassName, + "flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300", + className, )} > - - {renderUserData?.name?.slice(0, 2)} - - {!hideName &&
{renderUserData?.name}
} -
- ) -}) + + + {renderUserData?.name?.slice(0, 2)} + + {!hideName &&
{renderUserData?.name}
} + + ) + }, +) UserAvatar.displayName = "UserAvatar"