Files
im_webapp/web/src/lib/messages/dispatch.ts
T
Your NameandClaude Sonnet 5 f549248c08 feat(web): real chat list + P2P conversation view against live Tinode data
Phase 2 core loop: chat list and single-chat conversation now read/write real
Tinode topic data end to end, replacing the earlier hardcoded placeholders.

- lib/tinode/topics.ts: per-topic pub-sub layered on connection.ts's pattern
  (Topic.onData/onInfo/etc are single-slot callbacks, claimed once and fanned
  out). Implements the old app's subscribe lifecycle: `me` subscribed once
  per session (metadata only), each contact gets a one-shot preview
  subscription (withLaterData(1), never left), opening a conversation
  leaves-then-resubscribes with a full history page (withData, not
  withLaterData -- a preview prefetch already nudging _maxSeq would make
  withLaterData return empty).
- lib/tinode/hooks.ts: useChatList()/useTopicMessages() via
  useSyncExternalStore, snapshots cached and only invalidated on real
  topic-change notifications (not recomputed every render).
- lib/tinode/send.ts: sendText/retryMessage via createMessage+publishDraft
  (optimistic local echo), not the simpler publish() -- matches the old
  app's approach. retryMessage flushes the failed local entry and rebuilds
  before republishing, per the old app's context-menu retry handler.
- lib/messages/{time,avatar,attachments,status,preview,dispatch,drafty}:
  formatting utilities. Avatar color hash ports the old app's Java-style
  string hash but with modulus 11 (this design's palette) not the old app's
  16. @mention renders flat accent blue per the new design, not the old
  app's per-user hash color -- a deliberate simplification. File-type icons
  (PDF/Word/Excel/PPT) are new-design-only; the old app buckets all
  documents identically. Date/time formatting is written fresh against the
  design spec (today+time combined, weekday tier) since the old app never
  combines a relative-date word with a time in one string.
- ChatListColumn, MessageBubble, ConversationView: real components replacing
  the placeholder chat list and "占位" conversation view.
- tinode-sdk.d.ts: expanded with the real Topic/MetaGetBuilder/Drafty
  surface needed above, verified against the installed SDK source
  (createMessage/publishDraft/messages/msgStatus/noteRead/flushMessage/
  Drafty.format's exact treeBottomUp contract, MESSAGE_STATUS_* statics).

Verified end to end against the real backend: used the still-functional old
app (a second account, ixp355087) to create a real friendship and exchange
messages with the new app's test account, confirming the new chat list and
conversation view correctly render pre-existing real data, sending a new
message shows the right optimistic-then-delivered status transition, and
the old app receives it live in return.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 01:49:14 +08:00

38 lines
1.7 KiB
TypeScript

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