From 823a47e1828347bf026a4cf45cad3a67df39ebff Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 16:37:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BE=A4=E5=85=AC=E5=91=8A?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E9=80=9A=E7=9F=A5=E6=B6=88=E6=81=AF=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E5=AE=8C=E5=85=A8=E4=B8=8D=E6=98=BE=E7=A4=BA=E3=80=81?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=88=97=E8=A1=A8=E9=A2=84=E8=A7=88=E6=98=BE?= =?UTF-8?q?=E7=A4=BA"undefined"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 群公告发布后自动生成的通知消息,真实content是{text:"群公告\n<内容>"}这种 扁平对象(不是纯字符串,也不是标准Drafty的{txt,fmt,ent}结构),且 head['x-im-type']是"text"而不是已处理的"system_notice",落到Drafty处理 分支后既渲染不出气泡正文、列表预览也变成字面的"undefined"字符串。 抽出dispatch.ts里原本只服务system_notice的{text}扁平内容识别逻辑 (重命名为flatTextContentOf并导出),在renderDrafty和 formatMessagePreview的Drafty分支之前都接上——不改变已有的居中系统提示 样式(这条消息本来就该是普通气泡,发送者信息一直是正确的,只是正文 丢了),只是把"文本内容是扁平{text}对象"这一种情况也当作合法纯文本处理。 实测:真实发布一条群公告后,气泡正文和列表预览都正确显示"群公告 全功能测试公告内容",不再是空白或"undefined"。 --- web/src/lib/messages/dispatch.ts | 24 ++++++++++++++++++++---- web/src/lib/messages/drafty.tsx | 3 +++ web/src/lib/messages/preview.ts | 9 ++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/web/src/lib/messages/dispatch.ts b/web/src/lib/messages/dispatch.ts index 2d42478f..52d287cf 100644 --- a/web/src/lib/messages/dispatch.ts +++ b/web/src/lib/messages/dispatch.ts @@ -18,12 +18,28 @@ export function isRecalled(msg: StoredMessage): boolean { return !isServerDeleted(msg) && msg.content == null; } -interface SystemNoticeContent { +interface FlatTextContent { text: string; } -function isSystemNoticeContent(content: unknown): content is SystemNoticeContent { - return !!content && typeof content === 'object' && typeof (content as SystemNoticeContent).text === 'string'; +function isFlatTextContent(content: unknown): content is FlatTextContent { + return !!content && typeof content === 'object' && typeof (content as FlatTextContent).text === 'string'; +} + +/** + * `{text: "..."}` -- a flat, non-Drafty content shape (no `txt`/`fmt`/`ent`) this backend uses for + * more than one kind of message, not just the centered system-event lines `systemNoticeText` below + * renders: a real, live-confirmed example is the auto-posted "群公告\n" message + * a group's announcement publish creates, which has an ordinary sender/avatar (NOT a centered system + * line) and `head['x-im-type']:"text"` (not `system_notice`) -- but the SAME flat `{text}` content + * shape regardless. `renderDrafty`/`formatMessagePreview` both need this recognized as plain text + * before falling through to real Drafty processing (`Drafty.format`/`Drafty.preview` on an object + * with none of the fields they expect renders empty/`undefined`, confirmed live: a group's own + * announcement-notice bubble showed no text at all, and the chat list's preview literally read "我: + * undefined"). + */ +export function flatTextContentOf(content: unknown): string | null { + return isFlatTextContent(content) ? content.text : null; } /** @@ -35,7 +51,7 @@ function isSystemNoticeContent(content: unknown): content is SystemNoticeContent export function systemNoticeText(msg: StoredMessage): string | null { const headType = msg.head?.['x-im-type']; if (headType !== 'system_notice' && headType !== 'system_action') return null; - return isSystemNoticeContent(msg.content) ? msg.content.text : null; + return flatTextContentOf(msg.content); } export interface ContactCardContent { diff --git a/web/src/lib/messages/drafty.tsx b/web/src/lib/messages/drafty.tsx index e8e77c69..34cda388 100644 --- a/web/src/lib/messages/drafty.tsx +++ b/web/src/lib/messages/drafty.tsx @@ -4,6 +4,7 @@ import { Drafty, type DraftyContent } from 'tinode-sdk'; import { AudioAttachment, FileAttachment, ImageAttachment, VideoAttachment } from '@/features/messages/AttachmentPreview'; import { SafeLink } from '@/features/messages/SafeLink'; +import { flatTextContentOf } from '@/lib/messages/dispatch'; /** * Bubble content renderer for the Drafty style tags this app currently sends/receives. Grown @@ -124,6 +125,8 @@ function makeFormatNode(highlightQuery: string | undefined) { export function renderDrafty(content: DraftyContent | string | null | undefined, highlightQuery?: string): ReactNode { if (content == null) return null; if (typeof content === 'string') return highlightQuery ? <>{highlightMatches(content, highlightQuery, 'plain')} : content; + const flatText = flatTextContentOf(content); + if (flatText != null) return highlightQuery ? <>{highlightMatches(flatText, highlightQuery, 'plain')} : flatText; if (Drafty.isPlainText(content)) { const text = Drafty.toPlainText(content); return highlightQuery ? <>{highlightMatches(text, highlightQuery, 'plain')} : text; diff --git a/web/src/lib/messages/preview.ts b/web/src/lib/messages/preview.ts index 4ee9e59a..d03035dc 100644 --- a/web/src/lib/messages/preview.ts +++ b/web/src/lib/messages/preview.ts @@ -1,6 +1,6 @@ import { Drafty, type DraftyContent, type StoredMessage } from 'tinode-sdk'; -import { callResultDisplay, contactCardContentOf, isServerDeleted, systemNoticeText } from '@/lib/messages/dispatch'; +import { callResultDisplay, contactCardContentOf, flatTextContentOf, isServerDeleted, systemNoticeText } from '@/lib/messages/dispatch'; /** * One-line chat-list preview text (README §2.1): plain text renders as-is, everything else falls @@ -26,6 +26,13 @@ export function formatMessagePreview(msg: StoredMessage | undefined, myUserId: s if (contactCardContentOf(msg)) return `${prefix}[名片]`; const callResult = callResultDisplay(msg, msg.from === myUserId); if (callResult) return `${prefix}${callResult.text}`; + // Same flat `{text}` shape `systemNoticeText` above already knows about, but NOT gated to + // `system_notice`/`system_action` -- a real, live-confirmed example is a group's own + // announcement-publish notice (`head['x-im-type']:"text"`, ordinary sender, not a centered system + // line), which fell through everything above straight into the Drafty-entity branches below and + // produced the literal string "undefined" (confirmed live: chat list showed "我:undefined"). + const flatText = flatTextContentOf(content); + if (flatText != null) return prefix + flatText; if (typeof content === 'string' || Drafty.isPlainText(content)) { return prefix + Drafty.toPlainText(content); }