Skip to content

Messaging

Send and receive end-to-end encrypted messages. The SDK handles encryption, delivery, receipts, and local persistence.

Sending Text Messages

typescript
const messageId = await client.sendMessage(
  conversationId,    // e.g. "conv_abc123"
  toAliasId,         // e.g. "u87654321"
  'Hello, World!'
);

The SDK automatically:

  1. Encrypts the text with AES-256-GCM using the session key
  2. Sends via WebSocket (or queues if offline)
  3. Saves to local IndexedDB
  4. Returns the generated message ID

Sending Images

typescript
const file = inputElement.files[0]; // File from <input type="file">

const messageId = await client.sendImage(
  conversationId,
  toAliasId,
  file,
  thumbnailBase64  // Optional: low-res preview for skeleton loading
);

Images are automatically compressed and encrypted before upload.

Sending Files

typescript
const messageId = await client.sendFile(conversationId, toAliasId, file);

Files are uploaded without compression, encrypted end-to-end.

Sending Voice Messages

typescript
const messageId = await client.sendVoice(
  conversationId,
  toAliasId,
  audioBlob,     // Blob from MediaRecorder
  durationMs     // Recording duration in milliseconds
);

Receiving Messages

typescript
client.on('message', (msg) => {
  // msg: StoredMessage
  console.log(msg.id);              // Message UUID
  console.log(msg.conversationId);  // Conversation ID
  console.log(msg.text);            // Decrypted text (or JSON for media)
  console.log(msg.isMe);            // true if sent by current user
  console.log(msg.time);            // Timestamp (ms)
  console.log(msg.status);          // 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
                                    // ⚠️ UI 显示建议:'sent' 和 'delivered' 在 UI 层合并显示
                                    // (用户分不清"服务端收到"和"对方设备收到"的差别)
                                    // 仅 'read' 单独显示「已读」 — 否则用户会把 'delivered'
                                    // 的双勾误认为已读。详见下面「状态显示规范」章节。
  console.log(msg.msgType);         // undefined | 'retracted' | 'image' | 'file' | 'voice'
  console.log(msg.fromAliasId);     // Sender's alias ID
  console.log(msg.replyToId);       // Original message ID (if reply)
});

Message History

Messages are persisted locally in IndexedDB:

typescript
// Get all messages in a conversation
const messages = await client.getHistory(conversationId);

// Paginated loading (older messages)
const olderMessages = await client.getHistory(conversationId, {
  limit: 20,
  before: oldestTimestamp,
});

// Get a single message
const msg = await client.getMessageData(messageId);

Read Receipts

typescript
// Mark messages as read (sends receipt to the sender)
client.markAsRead(conversationId, maxSeq, toAliasId);

何时调用 markAsRead

只在「用户真的看到了消息」时调用,不要在 message 事件回调里盲目调。

正确触发条件(三选一,任一满足都算"用户在看"):

  1. 当前会话页面打开 + App 处于前台 (document.visibilityState === 'visible' / Android Lifecycle.RESUMED)
  2. 消息进入视口(IntersectionObserver / Android Compose onPlaced)
  3. 用户主动滚动到底部

错误触发(导致「我没看就显示已读」线上 bug):

  • ❌ App 在后台时(用户切到别的 app)收到消息也调
  • ❌ 锁屏状态下 listener 仍触发并调
  • ❌ 历史消息 catch-up 时一次性给所有未读消息全调

参考实现见 template-app-pwa/src/components/chat/ChatWindow.tsxtemplate-app-android/.../ChatScreen.kthandleIncoming 逻辑。

状态显示规范(UI 层必读)

StoredMessage.status 的协议层有 5 档,但 UI 层应当只显示 4 档:

协议状态UI 应显示为图标建议说明
sending"发送中"⏱ 时钟 / · 灰还没收到服务端 ack
sent"已发送"✓ 单勾灰服务端 ack 了
deliveredsent,合并显示✓ 单勾灰协议层概念,UI 不区分
read"已读"✓✓ 双勾亮蓝 + "已读"文字对方真看了
failed"失败"❗ 红发送失败

为什么 delivered 不单独显示?

  • 用户分不清"服务端收到"vs"对方设备收到"的差别(都是"还没看")
  • 之前 PWA / Android 给 delivered 用了 ✓✓ 双勾(只是颜色比 read 浅), 用户视觉上误以为双勾就是已读 — 引发线上反馈「我没看就显示已读」
  • 主流 IM (iMessage / Telegram) 也是把 sent + delivered 合并显示, 只有 WhatsApp 单独显示但用了完全不同的颜色饱和度(亮蓝 vs 灰)

正确做法:sentdelivered 用同一个图标(✓ 单勾灰), read✓✓ 双勾 + 「已读」文字 + 亮蓝色拉开视觉差距。

Typing Indicator

typescript
// Send typing status (SDK handles throttling)
client.sendTyping(conversationId, toAliasId);

// Listen for typing events
client.on('typing', (event) => {
  console.log(`${event.fromAliasId} is typing in ${event.conversationId}`);
});

Message Retraction

typescript
// Retract a message you sent (no time limit)
await client.retractMessage(messageId, toAliasId, conversationId);

The retracted message is replaced locally with a system message and the retraction is sent to the recipient.

Reply to Messages

typescript
const messageId = await client.sendMessage(
  conversationId,
  toAliasId,
  'Great idea!',
  originalMessageId  // The message being replied to
);

Clear History

typescript
// Clear a single conversation's local history
await client.clearHistory(conversationId);

// Clear all local history
await client.clearAllHistory();

Download Media

typescript
// Download and decrypt an image/file/voice message
const buffer = await client.media.downloadDecryptedMedia(mediaKey, conversationId);
const blob = new Blob([buffer]);
const url = URL.createObjectURL(blob);

Zero-Knowledge E2EE Protocol — Decentralized Communication