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); }