会话
会话是两个用户之间(单聊)或一个群中(群聊)的消息流。本页介绍用 im.conversations 显示会话列表、未读数和草稿,置顶、标记为未读、删除会话,以及打开一个会话、查看会话详情。会话中的消息见消息,免打扰的设置见提醒、推送设置与举报。
典型用法:登录后调用 im.conversations.list() 取得会话列表并订阅它,列表随新消息、已读、其他设备上的操作自动更新;用户点击一个会话时,用它的 conversation_key 打开消息列表。
会话列表和未读数都先从浏览器的本地存储读取,页面刷新、离线时也能立即显示,随后与服务端同步。同一浏览器的多个标签页共用同一份数据,在一个标签页中置顶、删除会话,其他标签页的列表同时更新。
显示会话列表
im.conversations.list() 返回一个会持续更新的列表(ConversationList)。用 getSnapshot() 读取当前的快照,用 subscribe() 订阅变化,不再显示时调用 dispose() 释放:
import { createDRClient } from '@deeprespond/im-web';
import { conversationTitle, displayName, summarize } from '@deeprespond/im-web/render';
const im = createDRClient({ appKey: 'org#app', apiUrl: 'https://im.example.com' });
await im.auth.loginWithTicket({ ticket: await fetchTicketFromYourServer() });
const list = im.conversations.list();
const me = im.auth.currentUser?.username ?? '';
function renderList(): void {
const { items, hasMore, loading } = list.getSnapshot();
const rows = items.map((c) => ({
key: c.conversation_key,
title: conversationTitle(im, c),
preview: c.draft
? `[草稿] ${c.draft.text}`
: c.last_message
? summarize(c.last_message, { nameOf: (u) => displayName(im, u), self: me })
: '',
unread: c.unread_count,
dot: c.unread_count === 0 && c.state?.marked_unread === true,
mentioned: c.state?.mention_seq != null,
pinned: c.state?.pinned_at != null,
muted: c.muted !== null,
}));
render(rows, { hasMore, loading });
}
const unsubscribe = list.subscribe(renderList);
renderList();
// 离开页面时
unsubscribe();
list.dispose();- 快照:
items是排好序的 ConversationView 数组;hasMore表示还有更多会话可以加载;loading表示正在加载;读取出错时带error。 - 引用不变:没有变化时
getSnapshot()返回同一个对象;有变化时返回新的快照,其中没有变化的会话沿用原来的对象,界面可以按引用判断哪些行要重新渲染。快照和其中的对象都已冻结,不能修改。 - 合并通知:同一轮事件循环中的多次变化合并为一次通知,监听函数被调用时
getSnapshot()已是新的快照。 - 释放:退出登录、切换用户时 SDK 先把快照换成空列表并通知一次,然后释放列表,界面不会继续显示上一个用户的会话。重新登录后要再调用
list()。未登录时调用list()抛出not_signed_in。 - 显示名称:
conversationTitle()、displayName()、summarize()来自@deeprespond/im-web/render,见消息的“显示辅助”。
排序
- 置顶的会话在前,按置顶时间倒序;
- 其余的按最后一条消息的时间倒序,还没有消息的群按入群时间;
- 还没有服务端会话的单聊(见打开会话)按其中最新一条待发消息的时间,只有草稿的按草稿的创建时间;
- 输入框中的草稿不改变已有会话的位置;
- 删除了的会话(
state.hidden为true)不显示,收到新消息时重新出现。
加载更多
第一次只把排在前面的会话读入内存(默认 1000 个,可以用创建客户端时的 conversationWindow 选项调整,见初始化与登录)。用户翻到末尾时,如果 hasMore 为 true,调用 loadMore():SDK 先从本地再读 100 个,本地读完后再向服务端取更早的会话。
async function onReachEnd(): Promise<void> {
const { hasMore, loading } = list.getSnapshot();
if (hasMore && !loading) await list.loadMore();
}在新设备上第一次登录时,本地还没有全部会话,SDK 先同步最近的会话,更早的在 loadMore() 时从服务端获取。
未读数
每个会话的 unread_count 是这个会话的未读消息数。群提示、通话记录、本人发的消息和发送时设置了 exclude_from_unread 的消息不计入。
- 标记为未读:
unread_count为 0 而state.marked_unread为true时,显示一个未读圆点,见标记为未读。 - 有人 @ 我:
state.mention_seq不为null时,会话中有 @ 本人或 @ 全体成员、本人还没有读到的消息,可以在摘要前显示“[有人@我]”。读到这条消息后它变回null;这条消息被撤回时,提醒退回到之前一条仍然有效的 @。 - 正在看的会话:用户正在看某个会话时(消息列表打开、停在最新处并且页面可见,见消息),这个会话的新消息不计入本地的未读数。请在用户看到新消息时标记已读,或者在创建客户端时开启
autoMarkRead,否则下一次同步后未读数会以服务端为准重新出现。
未读总数
im.conversations.getUnreadTotal() 同步返回全部会话的未读总数,用于标签页标题、导航栏的角标。总数变化时发出 conversations.unreadChanged 事件:
function updateBadge(): void {
const total = im.conversations.getUnreadTotal({ excludeMuted: true });
document.title = total > 0 ? `(${total}) 消息` : '消息';
}
im.on('conversations.unreadChanged', updateBadge);
updateBadge();- 总数覆盖全部会话,不只是已读入内存的那些;删除了的会话不计入。
- 只是标记为未读、没有未读消息的会话算 1。
excludeMuted: true时不计入设置了免打扰的会话,见提醒、推送设置与举报。- 在新设备上本地的会话列表还不完整时,SDK 用服务端统计的未读总数,这时
excludeMuted不起作用。事件载荷的source为'server'时就是这种情况,本地的会话列表完整之后变为'local'。
标记已读
用户看到一个会话的最新消息时,调用 markRead() 把已读位置推进到最新一条:
im.conversations.markRead(conversationId);
// 只标记到某一条(如用户只看到了第 120 条)
im.conversations.markRead(conversationId, { seq: 120 });markRead()不等待、不返回结果。SDK 把同一会话的多次调用合并,至少间隔 1 秒发送一次;本地的未读数立即清零,已读位置已经是最新的不再发送。- 标记已读同时取消“标记为未读”和“有人 @ 我”。单聊开启了已读回执时,对方随之看到“已读”,见消息的“已读回执”。
- 已打开的消息列表也有
markRead(),作用相同。也可以在创建客户端时设置autoMarkRead: true,由 SDK 在会话正在被看时自动标记。
把全部会话标记为已读:
const { count, truncated } = await im.conversations.markAllRead();count 为标记的会话数。服务端只处理当前所在的群和最近的 1000 个单聊和已离开的群,更早的会话还有未读时 truncated 为 true。markAllRead() 每分钟最多调用 2 次,超出时以 rate_limited 拒绝(retryAfter 为需要等待的秒数)。
标记为未读
用户想稍后再处理一个会话时,可以把它标记为未读:
await im.conversations.setMarkedUnread(conversationId, true);
// 取消
await im.conversations.setMarkedUnread(conversationId, false);标记为未读不改变已读位置,对方看到的“已读”也不变。标记已读、清空聊天记录、删除会话时自动取消。设置在本人的全部设备间同步。
置顶会话
await im.conversations.setPinned(conversationId, true);
await im.conversations.setPinned(conversationId, false);置顶的会话排在列表最前面,state.pinned_at 为置顶的时间。每个用户最多置顶 100 个会话,超出时以 limit_exceeded(reason 为 pinned_conversation_limit)拒绝。删除会话时自动取消置顶。
删除会话与清空聊天记录
// 从会话列表中删除,消息仍然保留
await im.conversations.remove(conversationId);
// 删除并清空聊天记录
await im.conversations.remove(conversationId, { clear_messages: true });
// 只清空聊天记录,会话仍在列表中
await im.conversations.clearMessages(conversationId);- 删除会话:会话从本人的会话列表中移除,未读数清零,同时取消置顶和标记为未读。对方或群里有新消息时会话重新出现,删除前的消息仍可以翻看。
- 清空聊天记录:本人看不到此前的全部消息,本地保存的也一并删除;未读数清零。对方和其他群成员不受影响。
- 都只影响本人,在本人的全部设备间同步。
- 对还没有服务端会话的单聊(键以
draft:开头)调用remove(),只删除它的草稿。
草稿
用户在输入框中输入了文字但没有发出时,可以保存为这个会话的草稿,下次打开会话时恢复:
// 离开会话时保存
await im.conversations.setDraft(conversationKey, { text: '明天上午十点', reply_to: { seq: 42 } });
// 打开会话时恢复
const view = await im.conversations.get(conversationKey);
const draftText = view?.draft?.text ?? '';
// 发出后清除
await im.conversations.setDraft(conversationKey, null);- 草稿只保存在本浏览器中,不同步到其他设备。同一浏览器的各个标签页共用。
- 草稿可以带
mentions(输入框中已 @ 的人)和reply_to(正在引用的消息),恢复时一起取回。text为空字符串且没有reply_to的视为清除。 - 会话列表中的
draft字段随之更新,可以在摘要处显示“[草稿]”。草稿不改变已有会话的排序。
免打扰的显示
用户对一个会话设置了免打扰后,ConversationView.muted 不为 null:mode 为 'none' 表示不提醒,'mention_only' 表示只在有人 @ 本人时提醒(只用于群),until 为到期时间(null 表示一直有效)。会话列表中可以显示一个免打扰图标,未读数显示为灰色圆点而不是数字。
免打扰由 im.push.mute() 设置,到期后 SDK 自动恢复并更新会话列表,见提醒、推送设置与举报。
正在输入
用户在输入框中输入时调用 typing(),对方的界面上显示“对方正在输入…”:
input.addEventListener('input', () => im.conversations.typing(conversationKey));
im.on('typing.changed', ({ conversation_key, usernames }) => {
if (conversation_key === conversationKey) {
render(usernames.length > 0 ? '对方正在输入…' : '');
}
});typing()可以在每次输入时调用,SDK 每 3 秒最多发送一次;长连接没有连上时直接丢弃。单聊和 100 人以内的群可用。- 收到的“正在输入”5 秒内没有再收到就自动消失。
im.conversations.getTyping(conversation_key)同步返回这个会话中正在输入的人,会话列表中每一项的typing字段相同。
打开会话
会话列表中的每一项都有 conversation_key,用它打开消息列表。从其他地方(如联系人、群列表)进入聊天时:
单聊:用 openSingle() 按对方的用户名取得会话:
const view = await im.conversations.openSingle('lisi');
const messages = im.messages.open(view.conversation_key);两人之间已经聊过时返回已有的会话;还没有会话时返回一个本地的“草稿会话”,conversation_key 为 draft:lisi,conversation_id 和 state 为 null。在草稿会话中发出第一条消息后,服务端创建正式的会话,SDK 自动把草稿会话换成正式会话:
- 会话列表中草稿会话被正式会话替换,位置不变,
conversations.changed事件同时带上被移除的草稿键和新的会话 ID; - 用草稿键打开的消息列表自动切换到正式会话,它的
conversation_key变为新的会话 ID; - 之后用旧键调用
typing()、setDraft()、im.messages.send()等方法时,SDK 自动换成新的会话 ID。
草稿会话只在有草稿或待发消息时出现在会话列表中;只是打开、什么也没写的不出现。对方先发来消息,或本人在其他设备上先和他聊了的,草稿会话同样并入正式会话。
群聊:群会话的 ID 就是群 ID,直接用群 ID 打开:
const messages = im.messages.open(groupId);会话详情
im.conversations.get() 从本地读取一个会话:
const view = await im.conversations.get(conversationId);
if (view?.state) {
const { membership, peer_read_seq, start_seq, max_seq } = view.state;
if (view.conversation_type === 'group' && membership !== 'member') {
render('你已不在群中'); // left 已退出 / removed 已被移出 / dismissed 群已解散
}
if (view.conversation_type === 'single' && view.peer === null) {
render('对方已注销');
}
}- 本地没有这个会话时返回
undefined。会话列表中的每一项与它相同。 state是服务端的会话状态(ConversationState),原样提供,如已读位置、对方已读到的位置、在群里的身份。- 已离开的群:退出、被移出或群解散后,会话仍在列表中,
state.membership不再是member。可以翻看离开前的消息、标记已读、置顶和删除会话,但不能再发送消息、撤回、表情回应和置顶消息。 - 对方已删除:单聊的对方被删除后
peer为null,用conversationTitle()显示为“已删除的用户”,不能再发送。
需要立即从服务端取最新状态时(如收到你的业务服务端的通知),调用 refresh():
const latest = await im.conversations.refresh(conversationId);通常不需要调用:SDK 通过长连接实时更新会话状态,断线重连后自动同步。
监听会话的变化
会话列表会自动更新,大多数情况下只需订阅列表。要在其他地方响应会话的变化(如更新另一个组件),可以监听事件:
im.on('conversations.changed', ({ upserted, removed }) => {
// upserted:新增或有变化的会话键;removed:从本地移除的会话键(含并入正式会话的草稿键)
console.log(upserted, removed);
});事件在本地数据更新之后发出,载荷只说明哪些会话变了,最新的内容用 get() 或列表的快照读取。全部事件见事件与错误。
接口参考
im.conversations 的方法。除 list()、markRead()、getUnreadTotal()、getTyping()、typing() 之外都返回 Promise,失败时以 DRError 拒绝;没有登录时以 not_signed_in 拒绝,网络错误为 network_error、timeout。错误码见事件与错误和服务端的错误码。
参数中的 conversation_id 是服务端的会话 ID;conversation_key 是会话列表中的键,正式会话就是会话 ID,还没有服务端会话的单聊为 draft:{username}。
im.conversations.list()
取得会话列表。每次调用返回一个新的列表对象,不再使用时调用它的 dispose()。
返回值:ConversationList,即 LiveList<ConversationView>:
| 方法 | 说明 |
|---|---|
getSnapshot() | 当前的快照 { items, hasMore, loading, error? } |
subscribe(listener) | 快照变化时调用 listener(不带参数),返回取消订阅的函数 |
loadMore() | 加载更多会话,返回 Promise<void>;没有更多时立即返回 |
dispose() | 释放列表。释放后 loadMore() 以 invalid_state(reason 为 disposed)拒绝 |
可能的错误:未登录时同步抛出 not_signed_in。
im.conversations.get()
从本地读取一个会话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID;也可以是 draft:{username},返回这个草稿会话(没有草稿和待发消息时为 undefined) |
返回值:Promise<ConversationView | undefined>,本地没有时为 undefined。
可能的错误:not_signed_in、storage_error。
im.conversations.openSingle()
按对方的用户名取得单聊会话:本地有的直接返回;没有的向服务端查询,两人之间还没有会话时返回草稿会话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 对方的用户名,不区分大小写 |
返回值:Promise<ConversationView>。草稿会话的 conversation_key 为 draft:{username}(小写),conversation_id、state 为 null。用户名不存在、已删除或就是本人时同样返回草稿会话,发送时才会失败。
可能的错误:local_validation(reason 为 required,没有给出用户名)、network_error。
im.conversations.refresh()
从服务端重新获取会话状态,合并到本地后返回。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID |
返回值:Promise<ConversationView>。
可能的错误:not_found(会话不存在或本人不可见)、local_validation(reason 为 not_found,服务端返回了会话但本地没有保存)、network_error。
im.conversations.markRead()
把已读位置推进到最新一条或指定的一条。不等待、不返回结果,SDK 合并发送,失败时只记日志。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID。草稿会话和未登录时什么也不做 |
opts.seq | number | 否 | 已读到的序号,默认为最新一条 |
返回值:无。
im.conversations.markAllRead()
把全部会话标记为已读。成功后 SDK 立即同步一次会话状态。
返回值:Promise<{ count: number; truncated: boolean }>。count 为标记的会话数;truncated 为 true 时还有更早的会话没有处理,本地的未读数等同步后再更新。
可能的错误:rate_limited(每分钟超过 2 次,source 为 local,retryAfter 为需要等待的秒数)、network_error。
im.conversations.setMarkedUnread()
标记为未读或取消标记。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID |
marked | boolean | 是 | true 标记为未读,false 取消 |
返回值:Promise<void>。
可能的错误:not_found、network_error。
im.conversations.setPinned()
置顶或取消置顶会话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID |
pinned | boolean | 是 | true 置顶,false 取消 |
返回值:Promise<void>。
可能的错误:limit_exceeded(reason 为 pinned_conversation_limit,已置顶 100 个)、not_found、network_error。
im.conversations.remove()
删除会话,可以同时清空聊天记录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID;为 draft:{username} 时只删除草稿 |
opts.clear_messages | boolean | 否 | 同时清空聊天记录,默认 false |
返回值:Promise<void>。
可能的错误:not_found、network_error。
im.conversations.clearMessages()
清空本人在这个会话中的聊天记录,会话仍在列表中。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 会话 ID |
返回值:Promise<void>。
可能的错误:not_found、network_error。
im.conversations.setDraft()
保存或清除会话的草稿(只保存在本浏览器中)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_key | string | 是 | 会话键,可以是草稿会话的 draft:{username} |
draft | Draft | null | 是 | 草稿,见 Draft;null 清除 |
返回值:Promise<void>。
可能的错误:local_validation(reason 为 required,draft.text 不是字符串)、storage_error。
im.conversations.getUnreadTotal()
同步返回未读总数。变化时发出 conversations.unreadChanged。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
opts.excludeMuted | boolean | 否 | 不计入免打扰的会话,默认 false。用服务端的总数时不起作用 |
返回值:number。
im.conversations.getTyping()
同步返回一个会话中正在输入的人。变化时发出 typing.changed。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_key | string | 是 | 会话键 |
返回值:readonly string[],正在输入的人的用户名,没有时为空数组。
im.conversations.typing()
告诉对方本人正在输入。每 3 秒最多发送一次,长连接没有连上时直接丢弃。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_key | string | 是 | 会话键;草稿会话同样可用 |
返回值:无。
数据结构
ConversationView
会话列表中的一项,由服务端的会话状态和本地的数据组合而成。
| 字段 | 类型 | 说明 |
|---|---|---|
conversation_key | string | 会话键:正式会话为会话 ID;还没有服务端会话的单聊为 draft:{username} |
conversation_id | string | null | 会话 ID,草稿会话为 null |
conversation_type | 'single' | 'group' | 单聊或群聊 |
peer | string | null | 单聊的对方用户名,对方已删除时为 null;群聊为 null |
group_id | string | null | 群聊的群 ID;单聊为 null |
state | ConversationState | null | 服务端的会话状态,见 ConversationState;草稿会话为 null |
last_message | MessageView | null | 最后一条消息,包括发送队列中还没发出的;见消息 |
unread_count | number | 未读消息数(不含“标记为未读”) |
draft | Draft | null | 本地草稿 |
muted | MuteInfo | null | 会话免打扰,没有设置或已到期为 null,见 MuteInfo |
typing | string[] | 正在输入的人 |
ConversationState
本人在一个会话中的状态,与服务端的用户会话对象相同。
| 字段 | 类型 | 说明 |
|---|---|---|
conversation_id | string | 会话 ID;群会话的 ID 就是群 ID |
conversation_type | 'single' | 'group' | 单聊或群聊 |
peer | string | null | 单聊的对方用户名,对方已删除时为 null |
group_id | string | null | 群聊的群 ID |
membership | 'member' | 'left' | 'removed' | 'dismissed' | null | 群聊中本人的身份:成员、已退出、已被移出、群已解散。单聊为 null |
start_seq | number | 本人能看到的第一条消息的序号(入群、清空聊天记录、消息过期都会让它变大) |
max_seq | number | 本人能看到的最后一条消息的序号 |
read_seq | number | 本人的已读位置 |
unread_count | number | 服务端统计的未读数。界面请用 ConversationView.unread_count,它包含了本地刚收到的消息 |
marked_unread | boolean | 是否标记为未读 |
peer_read_seq | number | null | 单聊中对方已读到的序号;应用没有开启单聊已读回执时为 null。群聊为 null |
mention_seq | number | null | 本人还没读到的、@ 了本人或全体成员的最近一条消息的序号 |
cleared_seq | number | 清空聊天记录的位置,0 表示没有清空过 |
hidden_seq | number | 删除会话时的位置,0 表示没有删除过 |
hidden | boolean | 会话是否处于删除状态 |
pinned_at | string | null | 置顶的时间 |
joined_at | string | null | 群聊中本次入群的时间 |
left_at | string | null | 群聊中离开的时间 |
message_version | number | 会话中消息的变更版本号 |
last_message_at | string | null | 最后一条消息的时间 |
version | number | 状态的版本号,SDK 同步用 |
Draft
| 字段 | 类型 | 说明 |
|---|---|---|
text | string | 输入框中的文字 |
mentions | { usernames?: string[]; all?: boolean } | 可选,已 @ 的人 |
reply_to | { seq: number } | 可选,正在引用的消息 |
created_at | number | 第一次保存的时刻(毫秒时间戳),由 SDK 填写,传入的忽略 |
updated_at | number | 最近一次保存的时刻,由 SDK 填写 |
MuteInfo
| 字段 | 类型 | 说明 |
|---|---|---|
mode | 'none' | 'mention_only' | none 不提醒;mention_only 只在 @ 本人时提醒(只用于群) |
until | string | null | 到期时间,null 表示一直有效 |
