會話
會話是兩個使用者之間(單聊)或一個群中(群聊)的訊息流。本頁介紹用 im.conversations 顯示會話列表、未讀數和草稿,置頂、標記為未讀、刪除會話,以及開啟一個會話、檢視會話詳情。會話中的訊息見訊息,免打擾的設定見提醒、推送設定與舉報。
典型用法:登入後呼叫 im.conversations.list() 取得會話列表並訂閱它,列表隨新訊息、已讀、其他裝置上的操作自動更新;使用者點選一個會話時,用它的 conversation_key 開啟訊息列表。
會話列表和未讀數都先從瀏覽器的本地儲存讀取,頁面重新整理、離線時也能立即顯示,隨後與服務端同步。同一瀏覽器的多個標籤頁共用同一份資料,在一個標籤頁中置頂、刪除會話,其他標籤頁的列表同時更新。
| 要做什麼 | 用什麼 |
|---|---|
| 顯示會話列表並自動更新 | im.conversations.list(),見顯示會話列表 |
| 顯示未讀數、導航欄角標 | unread_count、getUnreadTotal(),見未讀數 |
| 使用者看過後清除未讀 | markRead()、markAllRead(),見標記已讀 |
| 置頂、標記為未讀、刪除會話 | setPinned()、setMarkedUnread()、remove() |
| 儲存輸入框中沒發出的文字 | setDraft(),見草稿 |
| 顯示“對方正在輸入…” | typing()、typing.changed,見正在輸入 |
| 從聯絡人或群進入聊天 | openSingle()、群 ID,見開啟會話 |
顯示會話列表
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 表示一直有效 |
