refactor(desktop): reuse composer in Bestie handoff

Co-authored-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Arjun Mahanti
2026-08-26 08:36:20 -04:00
parent 84ce7ab95f
commit 2108724710
4 changed files with 78 additions and 53 deletions

View File

@@ -1,5 +1,4 @@
import * as React from "react";
import { Send } from "lucide-react";
import { toast } from "sonner";
import { pickBestieAgent } from "@/features/agents/lib/bestie";
@@ -10,12 +9,14 @@ import { buildMessageLink } from "@/features/messages/lib/messageLink";
import { getThreadReference } from "@/features/messages/lib/threading";
import { useSendMessageMutation } from "@/features/messages/hooks";
import type { TimelineMessage } from "@/features/messages/types";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { useIdentityQuery } from "@/shared/api/hooks";
import { Button } from "@/shared/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { Textarea } from "@/shared/ui/textarea";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
const ACTION_BUTTON_CLASS = "h-8 w-8 rounded-full p-0";
@@ -35,7 +36,6 @@ export function BestieMessagePopover({
const managedAgentsQuery = useManagedAgentsQuery();
const openDmMutation = useOpenDmMutation();
const sendMessageMutation = useSendMessageMutation(null, identityQuery.data);
const [note, setNote] = React.useState("");
const bestie = React.useMemo(
() =>
pickBestieAgent(managedAgentsQuery.data ?? [], activeCommunity?.relayUrl),
@@ -45,7 +45,11 @@ export function BestieMessagePopover({
if (!bestie) return null;
const submit = async () => {
const submit = async (
note: string,
mentionPubkeys: string[],
mediaTags?: string[][],
) => {
const { rootId } = getThreadReference(message.tags ?? []);
const link = buildMessageLink({
channelId,
@@ -65,14 +69,16 @@ export function BestieMessagePopover({
});
await sendMessageMutation.mutateAsync({
content,
mediaTags,
mentionPubkeys,
targetChannel: dm,
});
setNote("");
onOpenChange(false);
toast.success(`Sent to ${bestie.name}`);
} catch (error) {
console.error("Failed to send message to Bestie", error);
toast.error(`Couldn't send to ${bestie.name}`);
throw error;
}
};
@@ -103,64 +109,66 @@ export function BestieMessagePopover({
</Tooltip>
<PopoverContent
align="end"
className="w-[min(22rem,calc(100vw-2rem))] rounded-2xl p-0 shadow-lg"
className="w-[min(30rem,calc(100vw-2rem))] rounded-3xl p-0 shadow-xl"
data-testid={`bestie-popover-${message.id}`}
side="top"
sideOffset={10}
>
<form
className="space-y-4 p-4"
onSubmit={(event) => {
event.preventDefault();
void submit();
}}
>
<div className="space-y-3.5 p-4">
<div className="flex items-center gap-2.5">
<ProfileAvatar
avatarUrl={bestie.avatarUrl}
className="size-8 text-xs"
label={bestie.name}
/>
<div className="min-w-0">
<p className="text-sm font-semibold">Send to Bestie</p>
<p className="truncate text-xs text-muted-foreground">
Share this message with {bestie.name}
</p>
<p className="min-w-0 truncate text-sm font-semibold">Bestie</p>
</div>
<div
className="rounded-2xl border border-border/60 bg-background px-3 py-3 shadow-md"
data-testid={`bestie-message-snapshot-${message.id}`}
>
<div className="flex min-w-0 items-start gap-2.5">
<UserAvatar
avatarUrl={message.avatarUrl ?? null}
className="size-7 shrink-0"
displayName={message.author}
size="sm"
/>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1.5">
<p className="truncate text-xs font-semibold leading-4">
{message.author}
</p>
<p className="shrink-0 text-[10px] leading-4 text-muted-foreground/70">
{message.time}
</p>
</div>
<div className="mt-0.5 max-h-12 overflow-hidden">
<VideoReviewCommentMarkdown
className="line-clamp-3 text-xs leading-4 text-foreground/80 [&_p]:leading-4"
content={message.body}
interactive={false}
messageId={message.id}
/>
</div>
</div>
</div>
</div>
<div className="rounded-xl border border-border/70 bg-muted/45 px-3 py-2.5">
<p className="line-clamp-3 text-sm leading-5 text-foreground/80">
{message.body}
</p>
</div>
<Textarea
aria-label="Note for Bestie"
autoFocus
className="min-h-24 resize-none"
<MessageComposer
allowEmptySend
channelName="Bestie"
channelType="dm"
containerClassName="px-0 pb-0"
disabled={isPending}
onChange={(event) => setNote(event.target.value)}
draftKey={`bestie-share:${message.id}`}
isSending={isPending}
onSend={submit}
placeholder="Add a note (optional)"
value={note}
showBackgroundUploadProgress={false}
/>
<div className="flex justify-end gap-2">
<Button
disabled={isPending}
onClick={() => onOpenChange(false)}
size="sm"
type="button"
variant="ghost"
>
Cancel
</Button>
<Button disabled={isPending} size="sm" type="submit">
<Send aria-hidden className="size-4" />
{isPending ? "Sending…" : "Send"}
</Button>
</div>
</form>
</div>
</PopoverContent>
</Popover>
);

View File

@@ -69,6 +69,7 @@ import { useComposerLinkPreviews } from "./useComposerLinkPreviews";
import { scheduleSettleGatedAutoSubmit } from "./messageComposerAutoSubmit";
import type { MessageComposerProps } from "./MessageComposer.types";
function MessageComposerImpl({
allowEmptySend = false,
audienceContext = null,
channelId = null,
channelName,
@@ -597,7 +598,7 @@ function MessageComposerImpl({
const hasMedia =
currentPendingImeta.length > 0 || currentQueuedAttachments.length > 0;
if (
(!trimmed && !hasMedia) ||
(!allowEmptySend && !trimmed && !hasMedia) ||
disabledRef.current ||
isSendingRef.current ||
isSubmitLockedRef.current ||
@@ -644,6 +645,7 @@ function MessageComposerImpl({
onPreparingMentionSendChange?.(false);
}
}, [
allowEmptySend,
channelId,
channelLinks.clearChannels,
customEmoji,
@@ -775,7 +777,8 @@ function MessageComposerImpl({
composerDisabled ||
media.isUploading ||
mentionSendFlow.isPreparingMentionSend ||
(isContentEmpty &&
(!allowEmptySend &&
isContentEmpty &&
media.pendingImeta.length === 0 &&
media.queuedAttachments.length === 0);
const handleCaptureSelection = React.useCallback(() => {}, []);

View File

@@ -24,6 +24,11 @@ export type MessageComposerEditTarget = {
};
export type MessageComposerProps = {
/**
* Keeps the send control available without authored text. The owner must
* supply meaningful fallback content in onSend.
*/
allowEmptySend?: boolean;
audienceContext?: {
type: "channel" | "thread";
} | null;

View File

@@ -55,7 +55,8 @@ test("message action sends a message link and optional note to Bestie", async ({
await page.evaluate((id) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: "Please fold this launch decision into tomorrow's priorities.",
content:
"Please fold this launch decision into tomorrow's priorities. Product and engineering need one owner, a clear deadline, and a short note explaining the tradeoff before the review begins.",
id,
});
}, messageId);
@@ -67,17 +68,25 @@ test("message action sends a message link and optional note to Bestie", async ({
const popover = page.getByTestId(`bestie-popover-${messageId}`);
await expect(popover).toBeVisible();
await expect(popover).toContainText("Bestie");
await expect(popover).not.toContainText("Share this message with Bestie");
await expect(popover).toContainText("Please fold this launch decision");
await expect(
popover.getByTestId(`bestie-message-snapshot-${messageId}`),
).toBeVisible();
const composer = popover.getByTestId("message-composer");
await expect(composer).toBeVisible();
await expect(composer.getByRole("button", { name: "Send" })).toBeEnabled();
if (process.env.BUZZ_BESTIE_POPOVER_SCREENSHOT) {
await page.screenshot({
animations: "disabled",
path: process.env.BUZZ_BESTIE_POPOVER_SCREENSHOT,
});
}
await popover
.getByRole("textbox", { name: "Note for Bestie" })
await composer
.locator('[contenteditable="true"]')
.fill("Make sure product and engineering agree on the owner.");
await popover.getByRole("button", { name: "Send" }).click();
await composer.getByRole("button", { name: "Send" }).click();
await expect(popover).toBeHidden();
await page.getByTestId("open-bestie-dm").click();