mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Render @mentions as chips in chat messages
Chat messages rendered markdown but never passed the mention props, so @mentions stayed plain text. ChatMessageRow now resolves the message's p/mention tags into name→pubkey maps (people vs agent chips, same treatment as channel messages) for every author — own bubbles, other humans, and agents. Mentioned users' pubkeys join the chat's profile batch so a chip resolves even when that user never posted in the chat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
48a9ff9742
commit
b6af79309f
@@ -5,6 +5,10 @@ import { cleanAssistantMessageText } from "@/features/chats/ui/chatActivityText"
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import {
|
||||
resolveMentionNames,
|
||||
resolveMentionPubkeysByName,
|
||||
} from "@/shared/lib/resolveMentionNames";
|
||||
import { Bubble } from "@/shared/ui/bubble";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
@@ -58,6 +62,28 @@ export function ChatMessageRow({
|
||||
const content = isAgent
|
||||
? cleanAssistantMessageText(event.content)
|
||||
: event.content;
|
||||
// @mentions render as chips, resolved from the message's p/mention tags —
|
||||
// same treatment as channel messages, split into people vs agent chips.
|
||||
const mentionNames = React.useMemo(
|
||||
() => resolveMentionNames(event.tags, profiles),
|
||||
[event.tags, profiles],
|
||||
);
|
||||
const mentionPubkeysByName = React.useMemo(
|
||||
() => resolveMentionPubkeysByName(event.tags, profiles),
|
||||
[event.tags, profiles],
|
||||
);
|
||||
const agentMentionPubkeysByName = React.useMemo(() => {
|
||||
if (!mentionPubkeysByName) {
|
||||
return undefined;
|
||||
}
|
||||
const values: Record<string, string> = {};
|
||||
for (const [name, pubkey] of Object.entries(mentionPubkeysByName)) {
|
||||
if (profiles?.[pubkey.toLowerCase()]?.isAgent) {
|
||||
values[name] = pubkey;
|
||||
}
|
||||
}
|
||||
return Object.keys(values).length > 0 ? values : undefined;
|
||||
}, [mentionPubkeysByName, profiles]);
|
||||
|
||||
return (
|
||||
<Message side={isOwn ? "right" : "left"}>
|
||||
@@ -84,12 +110,16 @@ export function ChatMessageRow({
|
||||
{isAgent ? (
|
||||
<Markdown
|
||||
agentAuthored
|
||||
agentMentionPubkeysByName={agentMentionPubkeysByName}
|
||||
className="w-full max-w-none text-sm leading-6"
|
||||
content={content || " "}
|
||||
mentionNames={mentionNames}
|
||||
mentionPubkeysByName={mentionPubkeysByName}
|
||||
/>
|
||||
) : (
|
||||
<Bubble side={isOwn ? "right" : "left"}>
|
||||
<Markdown
|
||||
agentMentionPubkeysByName={agentMentionPubkeysByName}
|
||||
className={cn(
|
||||
"min-w-0",
|
||||
isOwn &&
|
||||
@@ -97,6 +127,8 @@ export function ChatMessageRow({
|
||||
)}
|
||||
compact
|
||||
content={content || " "}
|
||||
mentionNames={mentionNames}
|
||||
mentionPubkeysByName={mentionPubkeysByName}
|
||||
/>
|
||||
</Bubble>
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,7 @@ import { addChannelMembers, getCanvas, setCanvas } from "@/shared/api/tauri";
|
||||
import { extractSupportedLinkPreviews } from "@/shared/lib/linkPreview";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { getMentionTagPubkey } from "@/shared/lib/resolveMentionNames";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
type ChatsScreenProps = {
|
||||
@@ -293,6 +294,15 @@ export function ChatsScreen({
|
||||
identityQuery.data?.pubkey,
|
||||
defaultAgent?.pubkey,
|
||||
...messages.map((message) => message.pubkey),
|
||||
// Mentioned users too — chips need the mentioned profile's display
|
||||
// name even when that user never posted in the chat.
|
||||
...messages.flatMap(
|
||||
(message) =>
|
||||
message.tags?.flatMap((tag) => {
|
||||
const pubkey = getMentionTagPubkey(tag);
|
||||
return pubkey ? [pubkey] : [];
|
||||
}) ?? [],
|
||||
),
|
||||
]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((value) => value.toLowerCase()),
|
||||
|
||||
@@ -98,6 +98,7 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
|
||||
channelName: string;
|
||||
content: string;
|
||||
createdAt?: number;
|
||||
mentionPubkeys?: string[];
|
||||
pubkey?: string;
|
||||
}) => unknown;
|
||||
};
|
||||
@@ -117,10 +118,30 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
|
||||
createdAt: base + 2,
|
||||
pubkey: pubkey ?? undefined,
|
||||
});
|
||||
// A human message with a mention tag: @bob must render as a chip
|
||||
// (alice's pubkey is a mock profile fixture; bob's resolves via the
|
||||
// message's p tag).
|
||||
win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "Hello Fizz, first message",
|
||||
content: "Loop in @bob for the review.",
|
||||
createdAt: base + 4,
|
||||
mentionPubkeys: [
|
||||
"bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260",
|
||||
],
|
||||
pubkey:
|
||||
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f",
|
||||
});
|
||||
},
|
||||
{ pubkey: fizzPubkey },
|
||||
);
|
||||
|
||||
// Mentions in chat messages render as chips, same as channels.
|
||||
await expect(
|
||||
page.getByLabel("Chat messages").locator("[data-mention]", {
|
||||
hasText: "bob",
|
||||
}),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "First message" }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
Reference in New Issue
Block a user