import type { StoredMessage } from 'tinode-sdk'; /** * Two different "message is gone" protocol paths render differently (confirmed against the old * app's chat-message.jsx, a real, previously-hit gap): `msg.hi` is the server's own soft-delete * marker (renders as a centered system-style line, no bubble at all); Tinode's recall protocol * instead nulls out `content` on an otherwise-normal message (still a regular aligned bubble, just * with placeholder text). Getting these swapped either crashes on a null content or silently * misrenders a recalled message as a deleted-system-line. */ export function isServerDeleted(msg: StoredMessage): boolean { return !!(msg as StoredMessage & { hi?: boolean }).hi; } export function isRecalled(msg: StoredMessage): boolean { return !isServerDeleted(msg) && msg.content == null; } interface SystemNoticeContent { text: string; } function isSystemNoticeContent(content: unknown): content is SystemNoticeContent { return !!content && typeof content === 'object' && typeof (content as SystemNoticeContent).text === 'string'; } /** * Plain centered system-event text (e.g. "你们已成为好友,现在可以开始聊天了") -- backend-custom * `head['x-im-type']`, not a Drafty content type. Doesn't yet cover the actionable group-invitation * card variant (same head type, but with a real join button) since that's group-chat territory * (Phase 3); when that lands it needs to be excluded here the same way the old app excludes it. */ 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; }