From 304283c8d8c377f460bd2ca7bcb584dc9c470077 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 24 Sep 2026 16:09:50 +0800 Subject: [PATCH] fix(messages): @mentions leaked the sender's private remark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported + reproduced live: a group owner's own private friend remark for a member ("小号1"/"私密备注勿外传" in testing) ended up baked into that member's @mention text, visible to every other group member with no idea what it referred to. Root cause confirmed by reading server/im_http.go's userSummaryPayload: GET .../members?mentionable=true's `display_name` unconditionally prefers the CALLING viewer's own remark for that member when one is set. That's correct for a list only the viewer sees, but MentionPicker was using it to build the mention's label, which gets baked into the message's shared Drafty content once (mentions.ts's buildMentionedContent) and read identically by everyone from then on -- unlike a sender-name label, which every viewer's own client re-renders fresh per message and can legitimately show their own remark for. Switches MentionPicker's label (both the picker's own visible list and the mention baked into the sent message) to the same response's `nickname` field (copyUserNickname in the same backend function), which reads straight off the member's public profile and is never remark-shadowed. Verified live: set a real friend remark, confirmed the picker still showed the real nickname (not the remark), picked it, and confirmed the actually-sent message's @mention text was the real nickname too -- while the unrelated sender-name label elsewhere in the same conversation correctly kept showing the remark, confirming the fix didn't touch that intentionally-different behavior. Co-Authored-By: Claude Sonnet 5 --- web/src/features/messages/MentionPicker.tsx | 52 ++++++++++++--------- web/src/lib/api/groups.ts | 16 +++++++ 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/web/src/features/messages/MentionPicker.tsx b/web/src/features/messages/MentionPicker.tsx index 3e6b8e5b..c1a2983e 100644 --- a/web/src/features/messages/MentionPicker.tsx +++ b/web/src/features/messages/MentionPicker.tsx @@ -50,28 +50,38 @@ export function MentionPicker({ topicId, query, onPick }: MentionPickerProps) { 所有人 ) : null} - {items.map((m) => ( - - ))} + {label} + {m.role !== 'member' ? ( + + {ROLE_LABEL[m.role]} + + ) : null} + + ); + })} )} diff --git a/web/src/lib/api/groups.ts b/web/src/lib/api/groups.ts index d3dd340b..04d8e4f7 100644 --- a/web/src/lib/api/groups.ts +++ b/web/src/lib/api/groups.ts @@ -147,7 +147,23 @@ export type GroupRole = 'owner' | 'admin' | 'member'; export interface GroupMember { user_id: string; + /** + * Real, live-confirmed gotcha (2026-09-24): this is NOT always the member's own real name. + * Reading server/im_http.go's userSummaryPayload confirms `display_name` here unconditionally + * prefers the CALLING viewer's own private remark for that member when one is set, falling back + * to their real public nickname only when the viewer has no remark for them -- correct and + * intended for rendering a list to yourself (README's member list/search), completely wrong for + * anything that gets baked into shared message CONTENT once and read by everyone from then on + * (an @mention's label): a real reported bug was exactly this -- a group owner's own private + * remark for a member ("小号1") ended up as that member's permanent @mention text, which every + * OTHER member then saw with no idea who it meant. Use `nickname` (below) instead for that case. + */ display_name?: string; + /** The member's own real public profile nickname -- same backend response, but from + * copyUserNickname(), which reads straight off their public profile and is NEVER shadowed by the + * viewer's own remark the way `display_name` above is. This is what stays correct for everyone + * once baked into shared content (an @mention's label), not just for the viewer's own screen. */ + nickname?: string; avatar_file_id?: string; role: GroupRole; mentionable?: boolean;