feat(desktop): redesign status UI with full emoji picker and presence sub-menu (#760)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-05-27 13:13:33 -07:00
committed by GitHub
parent 8946653af8
commit 35821574c0
3 changed files with 167 additions and 93 deletions
@@ -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<PresenceStatus, string> = {
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<number | null>(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 (
<>
<Popover open={open} onOpenChange={onOpenChange}>
<Popover open={open} onOpenChange={handlePopoverOpenChange}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent
@@ -103,11 +138,13 @@ export function ProfilePopover({
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
{nip05 ? <span className="truncate">@{nip05}</span> : null}
{nip05 ? <span aria-hidden="true">·</span> : null}
<PresenceBadge
className="border-0 bg-transparent px-0 py-0 text-xs"
<span
className="inline-flex items-center gap-1.5"
data-testid="profile-popover-current-status"
status={currentStatus}
/>
>
<PresenceDot status={currentStatus} />
<span>{getPresenceLabel(currentStatus)}</span>
</span>
</div>
{hasUserStatus ? (
<p
@@ -131,7 +168,7 @@ export function ProfilePopover({
className={MENU_ITEM_CLASS}
data-testid="profile-popover-set-status"
onClick={() => {
onOpenChange(false);
closePopover();
window.requestAnimationFrame(() => {
setStatusDialogOpen(true);
});
@@ -144,45 +181,70 @@ export function ProfilePopover({
{hasUserStatus ? "Update status" : "Set a status"}
</span>
</button>
{hasUserStatus ? (
<button
className="w-full px-3 py-1 text-left text-xs text-muted-foreground hover:text-foreground"
data-testid="profile-popover-clear-status"
onClick={() => {
onClearUserStatus();
onOpenChange(false);
}}
role="menuitem"
type="button"
>
Clear status
</button>
) : null}
</div>
<hr className="my-1 h-px border-0 bg-border" />
{/* ── Presence status options ───────────────────────── */}
<div className="px-1.5 py-1">
{otherStatuses.map((status) => (
<button
key={status}
className={MENU_ITEM_CLASS}
data-testid={`profile-popover-status-${status}`}
disabled={isStatusPending}
onClick={() => {
onSetStatus(status);
onOpenChange(false);
}}
role="menuitem"
type="button"
<Popover
onOpenChange={setPresenceMenuOpen}
open={presenceMenuOpen}
>
<PopoverTrigger asChild>
<button
aria-expanded={presenceMenuOpen}
aria-haspopup="menu"
className={MENU_ITEM_CLASS}
data-testid="profile-popover-presence-trigger"
disabled={isStatusPending}
onClick={() => {
clearPresenceHoverTimer();
setPresenceMenuOpen((prev) => !prev);
}}
onMouseEnter={() => schedulePresenceMenu(true)}
onMouseLeave={() => schedulePresenceMenu(false)}
role="menuitem"
type="button"
>
<PresenceDot
className="h-2.5 w-2.5"
status={currentStatus}
/>
<span className="flex-1 text-sm text-popover-foreground">
{getPresenceLabel(currentStatus)}
</span>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-44 rounded-xl border border-border bg-popover p-1.5 shadow-lg"
onMouseEnter={() => schedulePresenceMenu(true)}
onMouseLeave={() => schedulePresenceMenu(false)}
side="right"
sideOffset={4}
>
<PresenceDot className="h-2.5 w-2.5" status={status} />
<span className="text-sm text-popover-foreground">
{STATUS_ACTION_LABELS[status]}
</span>
</button>
))}
<div aria-label="Presence status" role="menu">
{ALL_STATUSES.map((status) => (
<button
className={MENU_ITEM_CLASS}
data-testid={`profile-popover-status-${status}`}
disabled={isStatusPending}
key={status}
onClick={() => handlePresenceSelect(status)}
role="menuitem"
type="button"
>
<PresenceDot className="h-2.5 w-2.5" status={status} />
<span className="text-sm text-popover-foreground">
{getPresenceLabel(status)}
</span>
</button>
))}
</div>
</PopoverContent>
</Popover>
</div>
<hr className="my-1 h-px border-0 bg-border" />
@@ -193,7 +255,7 @@ export function ProfilePopover({
className={MENU_ITEM_CLASS}
data-testid="profile-popover-settings"
onClick={() => {
onOpenChange(false);
closePopover();
window.requestAnimationFrame(() => {
onOpenSettings();
});
@@ -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({
<div className="flex flex-col gap-4 pt-2">
<div className="flex items-center gap-2">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-input text-lg">
{emoji || "\uD83D\uDCAC"}
</span>
<Popover onOpenChange={setPickerOpen} open={pickerOpen}>
<div className="relative shrink-0">
<PopoverTrigger asChild>
<button
aria-label="Choose status emoji"
className="flex h-9 w-9 items-center justify-center rounded-md border border-input text-lg transition-colors hover:bg-accent"
type="button"
>
{emoji || "\uD83D\uDCAC"}
</button>
</PopoverTrigger>
{emoji ? (
<button
aria-label="Clear status emoji"
className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full border border-background bg-muted text-[10px] leading-none text-muted-foreground hover:bg-accent hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
setEmoji("");
}}
type="button"
>
×
</button>
) : null}
</div>
<PopoverPrimitive.Content
align="start"
sideOffset={4}
className="z-50 w-auto overflow-hidden rounded-2xl shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
>
<Picker
data={data}
maxFrequentRows={2}
onEmojiSelect={handleEmojiSelect}
perLine={8}
previewPosition="none"
set="native"
skinTonePosition="search"
theme="auto"
/>
</PopoverPrimitive.Content>
</Popover>
<Input
autoFocus
data-testid="set-status-input"
@@ -124,26 +154,6 @@ export function SetStatusDialog({
/>
</div>
<div className="flex flex-wrap gap-1.5">
{EMOJI_OPTIONS.map((option) => (
<button
aria-label={option.label}
className={`flex h-8 w-8 items-center justify-center rounded-md text-base transition-colors ${
emoji === option.emoji
? "bg-accent ring-1 ring-ring"
: "hover:bg-accent/60"
}`}
data-testid={`set-status-emoji-${option.label.toLowerCase().replace(/\s+/g, "-")}`}
key={option.emoji}
onClick={() => handleEmojiClick(option.emoji)}
title={option.label}
type="button"
>
{option.emoji}
</button>
))}
</div>
<div className="flex flex-wrap gap-1.5">
{PRESETS.map((preset) => (
<button
+2
View File
@@ -53,12 +53,14 @@ test("updates presence from the profile menu", async ({ page }) => {
page.getByTestId("profile-popover-current-status"),
).toContainText("Online");
await page.getByTestId("profile-popover-presence-trigger").click();
await page.getByTestId("profile-popover-status-away").click();
await openProfileMenu(page);
await expect(
page.getByTestId("profile-popover-current-status"),
).toContainText("Away");
await page.getByTestId("profile-popover-presence-trigger").click();
await page.getByTestId("profile-popover-status-offline").click();
await openProfileMenu(page);
await expect(