cheogram-www

created pr with cheogram-www/click-nickname.1 on 2026-11-07T22:43:19Z · by 9e834e0d
added cheogram-www/click-nickname.2 on 2026-11-07T22:43:54Z · by 9e834e0d
1: 09a926a ! 1: b380266 Click on nickname to mention
commented on 2026-11-09T14:41:33Z · by 3c31bc8c
lgtm
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:click-nickname | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:click-nickname [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:click-nickname
add comment to patch request:
ssh pr.pico.sh comment cheogram-www:click-nickname "lgtm!"
+8 -0 src/App.scss #
......@@ -785,6 +785,14 @@ div.cs-message__content {
785785 border: 0.125em solid lightcoral;
786786 }
787787
788+.clickable {
789+ cursor: pointer;
790+
791+ &:hover {
792+ text-decoration: underline;
793+ }
794+}
795+
788796 .cs-conversation-header__back {
789797 button {
790798 font-size: inherit;
+1 -0 src/components/Chat.tsx #
......@@ -1406,6 +1406,7 @@ export const ChatInner = () => {
14061406 client={client}
14071407 members={members}
14081408 setSelectedThread={setSelectedThread}
1409+ onMention={addMention}
14091410 m={m}
14101411 >
14111412 {m.type !==
+81 -0 src/components/MessageBubble.test.tsx #
......@@ -0,0 +1,81 @@
1+// @vitest-environment jsdom
2+
3+import * as borogove from "borogove";
4+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5+import { MemoryRouter } from "react-router";
6+import { afterEach, describe, expect, it, vi } from "vitest";
7+import { mock } from "vitest-mock-extended";
8+import { MessageBubble } from "./MessageBubble";
9+
10+afterEach(cleanup);
11+
12+const html = {
13+ reduce: () => [0],
14+ asDOM: () => document.createDocumentFragment(),
15+ toPlainText: () => "",
16+} as unknown as borogove.Html;
17+
18+const message = (senderId: string) =>
19+ ({
20+ senderId,
21+ isIncoming: () => true,
22+ moderationReason: () => "",
23+ body: () => html,
24+ subject: () => "",
25+ attachments: [],
26+ linkMetadata: [],
27+ versions: [],
28+ reactions: new Map(),
29+ timeSent: new Date().toISOString(),
30+ serverId: "message-1",
31+ threadId: "thread-1",
32+ threadIcon: () => null,
33+ }) as unknown as borogove.ChatMessage;
34+
35+const sender = mock<borogove.Member>({
36+ id: "alice@example.com",
37+ displayName: "Alice",
38+});
39+
40+const setup = (onMention?: (member: borogove.Member) => void) => {
41+ const setSelectedThread = vi.fn();
42+ const view = render(
43+ <MemoryRouter>
44+ <MessageBubble
45+ chat={mock<borogove.Chat>({ chatId: "room@example.com" })}
46+ client={mock<borogove.Client>({ displayName: () => "Me" })}
47+ members={
48+ new Map([
49+ ["room@example.com", mock<borogove.Member>()],
50+ [sender.id, sender],
51+ ])
52+ }
53+ m={message(sender.id)}
54+ position="single"
55+ setSelectedThread={setSelectedThread}
56+ onMention={onMention}
57+ >
58+ {null}
59+ </MessageBubble>
60+ </MemoryRouter>,
61+ );
62+ return { ...view, setSelectedThread };
63+};
64+
65+describe("MessageBubble nickname", () => {
66+ it("inserts a mention and selects the thread when the nickname is clicked", () => {
67+ const onMention = vi.fn();
68+ const { setSelectedThread } = setup(onMention);
69+
70+ fireEvent.click(screen.getByText("Alice"));
71+
72+ expect(onMention).toHaveBeenCalledWith(sender);
73+ expect(setSelectedThread).toHaveBeenCalledWith("thread-1");
74+ });
75+
76+ it("does not make the nickname clickable without a mention handler", () => {
77+ setup();
78+
79+ expect(screen.getByText("Alice").className).not.toContain("clickable");
80+ });
81+});
+12 -6 src/components/MessageBubble.tsx #
......@@ -221,6 +221,7 @@ export const MessageBubble = ({
221221 setSelectedThread,
222222 className,
223223 children,
224+ onMention,
224225 }: {
225226 chat: borogove.Chat;
226227 client: borogove.Client;
......@@ -230,6 +231,7 @@ export const MessageBubble = ({
230231 setSelectedThread: (id: string | null) => void;
231232 className?: string;
232233 children: ReactNode;
234+ onMention?: (member: borogove.Member) => void;
233235 }) => {
234236 const navigate = useNavigate();
235237
......@@ -265,6 +267,12 @@ export const MessageBubble = ({
265267 .replace(/\s*\b[A-Z]{2,4}$/, ""); // remove trailing TZ abbrev
266268
267269 const sender = members.get(m.senderId);
270+ const showSender =
271+ m.senderId !== chat.chatId &&
272+ ["last", "single"].includes(position) &&
273+ !m.moderationReason() &&
274+ m.isIncoming();
275+ const mention = sender && onMention ? () => onMention(sender) : undefined;
268276
269277 return (
270278 <Message
......@@ -327,13 +335,11 @@ export const MessageBubble = ({
327335 <div className="meta">
328336 <div
329337 className={
330- (m.senderId !== chat.chatId &&
331- ["last", "single"].includes(position) &&
332- !m.moderationReason() &&
333- m.isIncoming()
334- ? ""
335- : "sr-only") + " h-card sender"
338+ (showSender ? "" : "sr-only") +
339+ " h-card sender " +
340+ (mention ? "clickable" : "")
336341 }
342+ onClick={mention}
337343 >
338344 {sender?.displayName}
339345 </div>
Back to top