Add profile hover wave action

This commit is contained in:
klopez4212
2026-06-29 10:33:45 +01:00
parent c78e2c1c78
commit 01290d3ba7
2 changed files with 128 additions and 6 deletions

View File

@@ -10,7 +10,8 @@ import {
useChannelsQuery,
useOpenDmMutation,
} from "@/features/channels/hooks";
import { useUserProfileQuery } from "@/features/profile/hooks";
import { useProfileQuery, useUserProfileQuery } from "@/features/profile/hooks";
import { channelMessagesKey } from "@/features/messages/lib/messageQueryKeys";
import {
useRelayAgentsQuery,
useManagedAgentsQuery,
@@ -26,6 +27,7 @@ import { StatusEmoji } from "@/features/user-status/ui/StatusEmoji";
import { ProfileAvatarWithStatus } from "@/features/profile/ui/ProfileAvatarWithStatus";
import { useAgentSession } from "@/shared/context/AgentSessionContext";
import { useProfilePanel } from "@/shared/context/ProfilePanelContext";
import { sendChannelMessage } from "@/shared/api/tauri";
import { Popover, PopoverAnchor, PopoverContent } from "@/shared/ui/popover";
import { BotIdenticon } from "@/features/messages/ui/BotIdenticon";
@@ -120,7 +122,7 @@ export function UserProfilePopover({
}: UserProfilePopoverProps) {
const [open, setOpen] = React.useState(false);
const [pendingAction, setPendingAction] = React.useState<
"message" | "huddle" | null
"message" | "huddle" | "wave" | null
>(null);
const isMountedRef = React.useRef(false);
const hoverTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
@@ -159,11 +161,13 @@ export function UserProfilePopover({
// this only decides whether to paint the "View activity log" button.
const isOwner = useIsManagedAgent(role === "bot" ? pubkey : null);
const ownerPubkey = profile?.ownerPubkey ?? null;
const currentPubkey = useIdentityQuery().data?.pubkey;
const identityQuery = useIdentityQuery();
const currentPubkey = identityQuery.data?.pubkey;
const isSelf =
currentPubkey !== undefined &&
currentPubkey.toLowerCase() === pubkey.toLowerCase();
const showProfileActions = currentPubkey !== undefined && !isSelf;
const selfProfileQuery = useProfileQuery(open && showProfileActions);
const isCurrentUserOwner =
currentPubkey !== undefined &&
ownerPubkey !== null &&
@@ -294,6 +298,46 @@ export function UserProfilePopover({
startHuddle,
]);
const handleWave = React.useCallback(async () => {
if (!showProfileActions || pendingAction !== null) return;
clearHoverTimer();
setPendingAction("wave");
try {
const dm = await openDmMutation.mutateAsync({ pubkeys: [pubkey] });
const senderName =
selfProfileQuery.data?.displayName?.trim() ||
(currentPubkey ? truncatePubkey(currentPubkey) : "Someone");
await sendChannelMessage(dm.id, `${senderName} waved at you.`);
await queryClient.invalidateQueries({
queryKey: channelMessagesKey(dm.id),
});
await goChannel(dm.id);
if (isMountedRef.current) {
setOpen(false);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Failed to send wave.",
);
} finally {
if (isMountedRef.current) {
setPendingAction(null);
}
}
}, [
clearHoverTimer,
currentPubkey,
goChannel,
openDmMutation,
pendingAction,
pubkey,
queryClient,
selfProfileQuery.data?.displayName,
showProfileActions,
]);
React.useEffect(() => {
isMountedRef.current = true;
@@ -440,9 +484,9 @@ export function UserProfilePopover({
</StatusLine>
) : null}
{showProfileActions ? (
<div className="grid grid-cols-2 gap-2">
<div className="flex gap-2">
<Button
className="w-full"
className="min-w-0 flex-1"
data-testid={`user-profile-popover-message-${pubkey}`}
disabled={
pendingAction !== null || openDmMutation.isPending
@@ -465,7 +509,7 @@ export function UserProfilePopover({
Message
</Button>
<Button
className="w-full"
className="min-w-0 flex-1"
data-testid={`user-profile-popover-huddle-${pubkey}`}
disabled={
pendingAction !== null ||
@@ -489,6 +533,31 @@ export function UserProfilePopover({
)}
Huddle
</Button>
<Button
className="shrink-0 px-3"
data-testid={`user-profile-popover-wave-${pubkey}`}
disabled={
pendingAction !== null || openDmMutation.isPending
}
onClick={() => {
void handleWave();
}}
size="sm"
type="button"
variant="outline"
>
{pendingAction === "wave" ? (
<Spinner
aria-hidden="true"
className="h-3.5 w-3.5 border-2"
/>
) : (
<span aria-hidden="true" className="text-sm leading-none">
👋
</span>
)}
Wave
</Button>
</div>
) : null}
</>

View File

@@ -42,6 +42,21 @@ async function readCommandLog(page: import("@playwright/test").Page) {
});
}
async function readCommandPayloadLog(page: import("@playwright/test").Page) {
return page.evaluate(() => {
return (
(
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: Array<{
command: string;
payload: unknown;
}>;
}
).__BUZZ_E2E_COMMAND_LOG__ ?? []
);
});
}
function commandCount(commands: string[], command: string) {
return commands.filter((entry) => entry === command).length;
}
@@ -1171,3 +1186,41 @@ test("hovering avatar opens popover, clicking opens profile panel", async ({
await expect(profilePopover).toHaveCount(0);
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
});
test("profile popover wave sends a direct message", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const aliceMessage = page
.getByTestId("message-row")
.filter({ hasText: "Hey team — checking in." })
.first();
await aliceMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-wave-${TEST_IDENTITIES.alice.pubkey}`)
.click();
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
await waitForTimelineSettled(page);
await expect(page.getByTestId("message-row").last()).toContainText(
"npub1mock... waved at you.",
);
const commandLog = await readCommandPayloadLog(page);
expect(commandLog).toEqual(
expect.arrayContaining([
expect.objectContaining({
command: "send_channel_message",
payload: expect.objectContaining({
content: "npub1mock... waved at you.",
}),
}),
]),
);
});