diff --git a/desktop/src/features/profile/ui/ProfilePopover.tsx b/desktop/src/features/profile/ui/ProfilePopover.tsx index c75ba6b14..515870736 100644 --- a/desktop/src/features/profile/ui/ProfilePopover.tsx +++ b/desktop/src/features/profile/ui/ProfilePopover.tsx @@ -1,12 +1,10 @@ import * as React from "react"; -import { MessageSquare, Settings } from "lucide-react"; +import { ChevronRight, MessageSquare, Settings } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; -import { - PresenceDot, - PresenceBadge, -} from "@/features/presence/ui/PresenceBadge"; +import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; +import { getPresenceLabel } from "@/features/presence/lib/presence"; import { SetStatusDialog } from "@/features/user-status/ui/SetStatusDialog"; import type { PresenceStatus } from "@/shared/api/types"; @@ -40,12 +38,6 @@ const MENU_ITEM_CLASS = const ALL_STATUSES: PresenceStatus[] = ["online", "away", "offline"]; -const STATUS_ACTION_LABELS: Record = { - online: "Set yourself as online", - away: "Set yourself as away", - offline: "Set yourself as offline", -}; - // --------------------------------------------------------------------------- // ProfilePopover // --------------------------------------------------------------------------- @@ -66,16 +58,59 @@ export function ProfilePopover({ onOpenSettings, children, }: ProfilePopoverProps) { - const otherStatuses = ALL_STATUSES.filter((s) => s !== currentStatus); const isMac = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); const [statusDialogOpen, setStatusDialogOpen] = React.useState(false); + const [presenceMenuOpen, setPresenceMenuOpen] = React.useState(false); + const presenceHoverTimer = React.useRef(null); const hasUserStatus = Boolean(userStatusText || userStatusEmoji); + function clearPresenceHoverTimer() { + if (presenceHoverTimer.current !== null) { + window.clearTimeout(presenceHoverTimer.current); + presenceHoverTimer.current = null; + } + } + + function schedulePresenceMenu(nextOpen: boolean) { + clearPresenceHoverTimer(); + presenceHoverTimer.current = window.setTimeout( + () => setPresenceMenuOpen(nextOpen), + nextOpen ? 80 : 160, + ); + } + + React.useEffect( + () => () => { + if (presenceHoverTimer.current !== null) { + window.clearTimeout(presenceHoverTimer.current); + } + }, + [], + ); + + function handlePopoverOpenChange(nextOpen: boolean) { + if (!nextOpen) { + setPresenceMenuOpen(false); + } + onOpenChange(nextOpen); + } + + function closePopover() { + clearPresenceHoverTimer(); + setPresenceMenuOpen(false); + onOpenChange(false); + } + + function handlePresenceSelect(status: PresenceStatus) { + onSetStatus(status); + closePopover(); + } + return ( <> - + {children} {nip05 ? @{nip05} : null} {nip05 ? : null} - + > + + {getPresenceLabel(currentStatus)} + {hasUserStatus ? (

{ - onOpenChange(false); + closePopover(); window.requestAnimationFrame(() => { setStatusDialogOpen(true); }); @@ -144,45 +181,70 @@ export function ProfilePopover({ {hasUserStatus ? "Update status" : "Set a status"} - {hasUserStatus ? ( - - ) : null}


{/* ── Presence status options ───────────────────────── */}
- {otherStatuses.map((status) => ( - + + schedulePresenceMenu(true)} + onMouseLeave={() => schedulePresenceMenu(false)} + side="right" + sideOffset={4} > - - - {STATUS_ACTION_LABELS[status]} - - - ))} +
+ {ALL_STATUSES.map((status) => ( + + ))} +
+
+

@@ -193,7 +255,7 @@ export function ProfilePopover({ className={MENU_ITEM_CLASS} data-testid="profile-popover-settings" onClick={() => { - onOpenChange(false); + closePopover(); window.requestAnimationFrame(() => { onOpenSettings(); }); diff --git a/desktop/src/features/user-status/ui/SetStatusDialog.tsx b/desktop/src/features/user-status/ui/SetStatusDialog.tsx index e936a2a7a..feacc2509 100644 --- a/desktop/src/features/user-status/ui/SetStatusDialog.tsx +++ b/desktop/src/features/user-status/ui/SetStatusDialog.tsx @@ -1,4 +1,7 @@ import * as React from "react"; +import Picker from "@emoji-mart/react"; +import data from "@emoji-mart/data"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; import { Dialog, @@ -9,21 +12,7 @@ import { } from "@/shared/ui/dialog"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; - -// --------------------------------------------------------------------------- -// Curated emoji list -// --------------------------------------------------------------------------- - -const EMOJI_OPTIONS = [ - { emoji: "\uD83D\uDDE3\uFE0F", label: "In a meeting" }, - { emoji: "\uD83D\uDE8C", label: "Commuting" }, - { emoji: "\uD83E\uDD12", label: "Out sick" }, - { emoji: "\uD83C\uDFD6\uFE0F", label: "Vacationing" }, - { emoji: "\uD83C\uDFE0", label: "Working remotely" }, - { emoji: "\uD83C\uDF54", label: "Lunch" }, - { emoji: "\uD83C\uDFAF", label: "Focus" }, - { emoji: "\uD83D\uDCAA", label: "Exercising" }, -] as const; +import { Popover, PopoverTrigger } from "@/shared/ui/popover"; const PRESETS = [ { text: "In a meeting", emoji: "\uD83D\uDDE3\uFE0F" }, @@ -62,6 +51,7 @@ export function SetStatusDialog({ }: SetStatusDialogProps) { const [text, setText] = React.useState(initialText); const [emoji, setEmoji] = React.useState(initialEmoji); + const [pickerOpen, setPickerOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -70,15 +60,16 @@ export function SetStatusDialog({ } }, [open, initialText, initialEmoji]); - function handleEmojiClick(clickedEmoji: string) { - setEmoji((prev) => (prev === clickedEmoji ? "" : clickedEmoji)); - } - function handlePresetClick(preset: { text: string; emoji: string }) { setText(preset.text); setEmoji(preset.emoji); } + function handleEmojiSelect(selectedEmoji: { native: string }) { + setEmoji(selectedEmoji.native); + setPickerOpen(false); + } + function handleSave() { onSave(text.trim(), emoji); onOpenChange(false); @@ -111,9 +102,48 @@ export function SetStatusDialog({
- - {emoji || "\uD83D\uDCAC"} - + +
+ + + + {emoji ? ( + + ) : null} +
+ + + +
-
- {EMOJI_OPTIONS.map((option) => ( - - ))} -
-
{PRESETS.map((preset) => (