mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
Add profile hover wave action
This commit is contained in:
@@ -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}
|
||||
</>
|
||||
|
||||
@@ -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.",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user