二,消息收发的更多方式,离线推送与消息同步,多设备登录
本章导读
在前一章从简单的单聊、群聊、收发图文消息开始里面,我们说明了如何在产品中增加一个基本的单聊/群聊页面,并响应服务端实时事件通知。接下来,在本篇文档中我们会讲解如何实现一些更复杂的业务需求,例如:
- 支持消息被接收和被阅读的状态回执,实现「Ding」一下的效果
- 发送带有成员提醒的消息(@ 某人),在超多用户群聊的场合提升目标用户的响应积极性
- 支持消息的撤回和修改
- 解决成员离线状态下的推送通知与重新上线后的消息同步,确保不丢消息
- 支持多设备登录,或者强制用户单点登录
- 扩展新的消息类型
消息收发的更多方式
在一个偏重工作协作或社交沟通的产品里,除了简单的消息收发之外,我们还会碰到更多需求,例如:
- 在消息中能否直接提醒某人,类似于很多 IM 工具中提供的 @ 消息,这样接收方能更明确地知道哪些消息需要及时响应;
- 消息发出去之后才发现内容不对,这时候能否修改或者撤回?
- 除了普通的聊天内容之外,是否支持发送类似于「XX 正在输入」这样的状态消息?
- 消息是否被其他人接收、读取,这样的状态能否反馈给发送者?
- 客户端掉线一段时间之后,可能会错过一批消息,能否提醒并同步一下未读消息?
等等,所有这些需求都可以通过即时通讯服务解决,下面我们来逐一看看具体的做法。
@ 成员提醒消息
在一些多人聊天群里面,因为消息量较大,很容易就导致某一条重要的消息没有被目标用户看到就被刷下去了,所以在消息中「@成员」是一种提醒接收者注意的有效办法。在微信这样的聊天工具里面,甚至会在对话列表页对有提醒的消息进行特别展示,用来通知消息目标高优先级查看和处理。
一般提醒消息都使用「@ + 人名」来表示目标用户,但是这里「人名」是一个由应用层决定的属性,可能有的产品使用全名,有的使用昵称,并且这个名字和即时通讯服务里面标识一个用户使用的 clientId
可能根本不一样(毕竟一个是给人看的,一个是给机器读的)。使用「人名」来圈定用户,也存在一种例外,就是聊天群组里面的用户名是可以改变的,如果消息发送的时候「王五」还叫「王五」,等发送出来之后他恰好同步改成了「王大麻子」,这时候接收方的处理就比较麻烦了。还有第三个原因,就是「提醒全部成员」的表示方式,可能「@all」、「@group」、「@所有人」都会被选择,这是一个完全依赖应用层 UI 的选项。
所以「@ 成员」提醒消息并不能简单在文本消息中加入「@ + 人名」,解决方案是给普通消息(LCIMMessage
)增加两个额外的属性:
mentionList
,是一个字符串的数组,用来单独记录被提醒的clientId
列表;mentionAll
,是一个Bool
型的标志位,用来表示是否要提醒全部成员。
带有提醒信息的消息,有可能既有提醒全部成员的标志,也还单独设置了 mentionList
,这由应用层去控制。发送方在发送「@ 成员」提醒消息的时候,如何输入、选择成员名称,这是业务方 UI 层面需要解决的问题,即时通讯 SDK 不关心其实现逻辑,SDK 只要求开发者在发送一条「@ 成员」消息的时候,调用 mentionList
和 mentionAll
的 setter 方法,设置正确的成员列表即可。示例代码如下:
- Unity
- Android
- iOS
LCIMTextMessage textMessage = new LCIMTextMessage("@Tom 早点回家") {
MentionIdList = new string[] { "Tom" }
};
await conversation.Send(textMessage);
String content = "@Tom 早点回家";
LCIMTextMessage message = new LCIMTextMessage();
message.setText(content);
List<String> list = new ArrayList<>(); // 部分用户的 mention list,你可以像下面代码这样来填充
list.add("Tom");
message.setMentionList(list);
imConversation.sendMessage(message, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
}
});
LCIMMessage *message = [LCIMTextMessage messageWithText:@"@Tom 早点回家" attributes:nil];
message.mentionList = @[@"Tom"];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) {
/* 一条提及 Tom 的消息已发出 */
}];
或者也可以通过设置 mentionAll
属性值提醒所有人:
- Unity
- Android
- iOS
LCIMTextMessage textMessage = new LCIMTextMessage("@all") {
MentionAll = true
};
await conv.Send(textMessage);
String content = "@all";
LCIMTextMessage message = new LCIMTextMessage();
message.setText(content);
boolean mentionAll = true; // 指示是否提及了所有人
message.mentionAll(mentionAll);
imConversation.sendMessage(message, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
}
});
LCIMMessage *message = [LCIMTextMessage messageWithText:@"@all" attributes:nil];
message.mentionAll = YES;
[conversation sendMessage:message callback:^(BOOL succeeded, NSError * _Nullable error) {
/* 一条提及所有用户的消息已发出 */
}];
对于消息的接收方来说,可以通过调用 mentionList
和 mentionAll
的 getter 方法来获得提醒目标用户的信息,示例代码如下:
- Unity
- Android
- iOS
jerry.onMessage = (conv, msg) => {
List<string> mentionIds = msg.MentionIdList;
};
@Override
public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) {
// 读取消息 @ 的 clientId 列表
List<String> currentMsgMentionUserList = message.getMentionList();
}
// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息提醒的 clientId 列表,同理可以用类似的代码操作 LCIMMessage 的其他子类
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
// 读取消息 @ 的 clientId 列表
NSArray *mentionList = message.mentionList;
}
此外,并且为了方便应用层 UI 展现,我们特意为 LCIMMessage
增加了两个标识位,用来显示被提醒的状态:
- 一个是
mentionedAll
标识位,用来表示该消息是否提醒了当前对话的全体成员。只有mentionAll
属性为true
,这个标识位才为true
,否则就为false
。 - 另一个是
mentioned
标识位,用来快速判断该消息是否提醒了当前登录用户。如果mentionList
属性列表中包含有当前登录用户的clientId
,或者mentionAll
属性为true
,那么mentioned
方法都会返回true
,否则返回false
。
调用示例如下:
- Unity
- Android
- iOS
client.OnMessage = (conv, msg) => {
bool mentioned = msg.MentionAll || msg.MentionList.Contains("Tom");
};
@Override
public void onMessage(LCIMAudioMessage msg, LCIMConversation conv, LCIMClient client) {
// 读取消息是否 @ 了对话的所有成员
boolean currentMsgMentionAllUsers = message.isMentionAll();
// 读取消息是否 @ 了当前用户
boolean currentMsgMentionedMe = message.mentioned();
}
// 示例代码演示 LCIMTypedMessage 接收时,获取该条消息是否 @ 了当前对话里的所有成员或当前用户,同理可以用类似的代码操作 LCIMMessage 的其他子类
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
// 读取消息是否 @ 了对话的所有成员
BOOL mentionAll = message.mentionAll;
// 读取消息是否 @ 了当前用户
BOOL mentionedMe = message.mentioned;
}
修改消息
在 云服务控制台 > 即时通讯 > 设置 > 即时通讯选项 启用 「允许通过 SDK 编辑消息」后,终端用户可以对自己已经发送的消息进行修改(Conversation#updateMessage
方法)。目前即时通讯服务端并没有在时效性上进行限制,不过只允许用户修改自己发出去的消息,不允许修改别人的消息。
修改已经发送的消息,并不是直接在老的消息对象上修改,而是像发新消息一样创建一个消息实例,然后调用 Conversation#updateMessage(oldMessage, newMessage)
方法来向云端提交请求,示例代码如下:
- Unity
- Android
- iOS
LCIMTextMessage newMessage = new LCIMTextMessage("修改后的消息内容");
await conversation.UpdateMessage(oldMessage, newMessage);
LCIMTextMessage textMessage = new LCIMTextMessage();
textMessage.setContent("修改后的消息");
imConversation.updateMessage(oldMessage, textMessage, new LCIMMessageUpdatedCallback() {
@Override
public void done(LCIMMessage avimMessage, LCException e) {
if (null == e) {
// 消息修改成功,avimMessage 即为被修改后的最新的消息
}
}
});
LCIMMessage *oldMessage = <#MessageYouWantToUpdate#>;
LCIMMessage *newMessage = [LCIMTextMessage messageWithText:@"Just a new message" attributes:nil];
[conversation updateMessage:oldMessage
toNewMessage:newMessage
callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"消息已被修改。");
}
}];
消息修改成功之后,对话内的其他成员会立刻接收到 MESSAGE_UPDATE
事件:
- Unity
- Android
- iOS
tom.OnMessageUpdated = (conv, msg) => {
if (msg is LCIMTextMessage textMessage) {
WriteLine($"内容 {textMessage.Text}, 消息 ID {textMessage.Id}");
}
};
void onMessageUpdated(LCIMClient client, LCIMConversation conversation, LCIMMessage message) {
// message 即为被修改的消息
}
/* 实现 delegate 方法,以处理消息修改的事件 */
- (void)conversation:(LCIMConversation *)conversation messageHasBeenUpdated:(LCIMMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason {
/* 有消息被修改 */
}
对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部会先从缓存中修改这条消息记录,然后再通知应用层。所以对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(这时候消息列表会出现内容变化)。
如果系统修改了消息(例如触发了内置的敏感词过滤功能,或者云引擎的 hook 函数),发送者会收到 MESSAGE_UPDATE
事件,其他对话成员接收到的是修改过的消息。
撤回消息
除了修改消息,终端用户还可以撤回一条自己之前发送过的消息。 和修改消息类似,这一功能需要在控制台启用(云服务控制台 > 即时通讯 > 设置 > 即时通讯选项 启用「允许通过 SDK 撤回消息」)。 同样,即时通讯服务端并没有在时效性上进行限制,不过只允许用户撤回自己发出去的消息,不允许撤回别人的消息。
撤回消息调用 Conversation#recallMessage
方法,示例代码如下:
- Unity
- Android
- iOS
await conversation.RecallMessage(message);
conversation.recallMessage(message, new LCIMMessageRecalledCallback() {
@Override
public void done(LCIMRecalledMessage recalledMessage, LCException e) {
if (null == e) {
// 消息撤回成功,可以更新 UI
}
}
});
LCIMMessage *oldMessage = <#MessageYouWantToRecall#>;
[conversation recallMessage:oldMessage callback:^(BOOL succeeded, NSError * _Nullable error, LCIMRecalledMessage * _Nullable recalledMessage) {
if (succeeded) {
NSLog(@"消息已被撤回。");
}
}];
成功撤回消息后,对话内的其他成员会接收到 MESSAGE_RECALL
的事件:
- Unity
- Android
- iOS
tom.OnMessageRecalled = (conv, recalledMsg) => {
// recalledMsg 即为被撤回的消息
};
void onMessageRecalled(LCIMClient client, LCIMConversation conversation, LCIMMessage message) {
// message 即为被撤回的消息
}
/* 实现 delegate 方法,以处理消息撤回的事件 */
- (void)conversation:(LCIMConversation *)conversation messageHasBeenRecalled:(LCIMRecalledMessage *)message reason:(LCIMMessagePatchedReason * _Nullable)reason {
/* 有消息被撤回 */
}
对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部需要保证数据的一致性,所以会先从缓存中删除这条消息记录,然后再通知应用层。对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(此时消息列表中的消息会直接变少,或者显示撤回提示)。
暂态消息
有时候我们需要发送一些特殊的消息,譬如聊天过程中 「某某正在输入…」这样的实时状态信息,或者当群聊的名称修改以后给该群成员发送「群名称被某某修改为 XX」这样的通知信息。这类消息与终端用户发送的消息不一样,发送者不要求把它保存到历史记录里,也不要求一定会被送达(如果成员不在线或者现在网络异常,那么没有下发下去也无所谓),这种需求可以使用「暂态消息」来实现。
「暂态消息」是一种特殊的消息,与普通消息相比有以下几点不同:
- 它不会被自动保存到云端,以后在历史消息中无法找到它
- 只发送给当时在线的成员,不支持延迟接收,离线用户更不会收到推送通知
- 对当时在线成员也不保证百分百送达,如果因为当时网络原因导致下发失败,服务端不会重试
我们可以用「暂态消息」发送一些实时的、频繁变化的状态信息,或者用来实现简单的控制协议。
暂态消息的数据和构造方式与普通消息是一样的,只是其发送方式与普通消息有一些区别。到目前为止,我们演示的 LCIMConversation
发送消息接口都是这样的:
- Unity
- Android
- iOS
public async Task<LCIMMessage> Send(LCIMMessage message, LCIMMessageSendOptions options = null);
/**
* 发送一条消息
*/
public void sendMessage(LCIMMessage message, final LCIMConversationCallback callback)
/*!
往对话中发送消息。
*/
- (void)sendMessage:(LCIMMessage *)message
callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback;
其实即时通讯 SDK 还允许在发送一条消息的时候,指定额外的参数 LCIMMessageOption
,LCIMConversation
完整的消息发送接口如下:
- Unity
- Android
- iOS
/// <summary>
/// Sends a message in this conversation.
/// </summary>
/// <param name="message">The message to send.</param>
/// <returns></returns>
public async Task<LCIMMessage> Send(LCIMMessage message, LCIMMessageSendOptions options = null);
/**
* 发送消息
* @param message
* @param messageOption
* @param callback
*/
public void sendMessage(final LCIMMessage message, final LCIMMessageOption messageOption, final LCIMConversationCallback callback);
/*!
往对话中发送消息。
@param message - 消息对象
@param option - 消息发送选项
@param callback - 结果回调
*/
- (void)sendMessage:(LCIMMessage *)message
option:(nullable LCIMMessageOption *)option
callback:(void (^)(BOOL succeeded, NSError * _Nullable error))callback;
通过 LCIMMessageOption
参数我们可以指定:
- 是否作为暂态消息发送(设置
transient
属性); - 服务端是否需要通知该消息的接收状态(设置
receipt
属性,消息回执,后续章节会进行说明); - 消息的优先级(设置
priority
属性,后续章节会说明); - 是否为「遗愿消息」(设置
will
属性,后续章节会说明); - 消息对应的离线推送内容(设置
pushData
属性,后续章节会说明),如果消息接收方不在线,会推送指定的内容。
如果我们需要让 Tom 在聊天页面的输入框获得焦点的时候,给群内成员同步一条「Tom 正在输入…」的状态信息,可以使用如下代码:
- Unity
- Android
- iOS
LCIMTextMessage textMessage = new LCIMTextMessage("Tom 正在输入…");
LCIMMessageSendOptions option = new LCIMMessageSendOptions() {
Transient = true
};
await conversation.Send(textMessage, option);
String content = "Tom 正在输入…";
LCIMTextMessage message = new LCIMTextMessage();
message.setText(content);
LCIMMessageOption option = new LCIMMessageOption();
option.setTransient(true);
imConversation.sendMessage(message, option, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
}
});
LCIMMessage *message = [LCIMTextMessage messageWithText:@"Tom 正在输入…" attributes:nil];
LCIMMessageOption *option = [[LCIMMessageOption alloc] init];
option.transient = true;
[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) {
/* 一条暂态消息已发出 */
}];
暂 态消息的接收逻辑和普通消息一样,开发者可以按照消息类型进行判断和处理,这里不再赘述。上面使用了内建的文本消息只是一种示例,从展现端来说,我们如果使用特定的类型来表示「暂态消息」,是一种更好的方案。即时通讯 SDK 并没有提供固定的「暂态消息」类型,可以由开发者根据自己的业务需要来实现专门的自定义,具体可以参考后述章节:扩展自己的消息类型。
消息回执
即时通讯服务端在进行消息投递的时候,会按照消息上行的时间先后顺序下发(先收到的消息先下发,保证顺序性),且内部协议上会要求 SDK 对收到的每一条消息进行确认(ack)。如果 SDK 收到了消息,但是在发送 ack 的过程中出现网络丢包,即时通讯服务端还是会认为消息没有投递下去,之后会再次投递,直到收到 SDK 的应答确认为止。与之对应,SDK 内部也进行了消息去重处理,保证在上面这种异常条件下应用层也不会收到重复的消息。所以我们的消息系统从协议上是可以保证不丢任何一条消息的。
不过,有些业务场景会对消息投递的细节有更高的要求,例如消息的发送方要能知道什么时候接收方收到了这条消息,什么时候 ta 又点开阅读了这条消息。有一些偏重工作写作或者私密沟通的产品,消息发送者在发送一条消息之后,还希望能看到消息被送达和阅读的实时状态,甚至还要提醒未读成员。这样「苛刻」的需求,就依赖于我们的「消息回执」功能来实现。
与上一节「暂态消息」的发送类似,要使用消息回执功 能,需要在发送消息时在 LCIMMessageOption
参数中标记「需要回执」选项:
- Unity
- Android
- iOS
LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。");
LCIMMessageSendOptions option = new LCIMMessageSendOptions {
Receipt = true
};
await conversation.Send(textMessage, option);
LCIMMessageOption messageOption = new LCIMMessageOption();
messageOption.setReceipt(true);
imConversation.sendMessage(message, messageOption, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
}
});
LCIMMessageOption *option = [[LCIMMessageOption alloc] init];
option.receipt = true;
[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!需要回执。");
}
}];
注意:
只有在发送时设置了「需要回执」的标记,云端才会发送回执,默认不发送回执,且目前消息回执只支持单聊对话(成员不超过 2 人)。
那么发送方后续该如何响应回执的通知消息呢?
送达回执
当接收方收到消息之后,云端会向发送方发出一个回执通知,表明消息已经送达。请注意与「已读回执」区别开。
- Unity
- Android
- iOS
// Tom 用自己的名字作为 clientId 建立了一个 LCIMClient
LCIMClient client = new LCIMClient("Tom");
// Tom 登录到系统
await client.Open();
// 设置送达回执
client.OnMessageDelivered = (conv, msgId) => {
// 在这里可以书写消息送达之后的业务逻辑代码
};
// 发送消息
LCIMTextMessage textMessage = new LCIMTextMessage("夜访蛋糕店,约吗?");
await conversation.Send(textMessage);
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本地方法来处理对方已经接收消息的通知
*/
public void onLastDeliveredAtUpdated(LCIMClient client, LCIMConversation conversation) {
;
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
// 监听消息是否已送达实现 `conversation:messageDelivered` 即可。
- (void)conversation:(LCIMConversation *)conversation messageDelivered:(LCIMMessage *)message {
NSLog(@"%@", @"消息已送达。"); // 打印消息
}
请注意这里送达回执的内容,不是某一条具体的消息,而是当前对话内最后一次送达消息的时间戳(lastDeliveredAt
)。最开始我们有过解释,服务端在下发消息的时候,是能 够保证顺序的,所以在送达回执的通知里面,我们不需要对逐条消息进行确认,只给出当前确认送达的最新消息的时间戳,那么在这之前的所有消息就都是已经送达的状态。在 UI 层展示的时候,可以将早于 lastDeliveredAt
的消息都标记为「已送达」。
已读回执
消息送达只是即时通讯服务端和客户端之间的投递行为完成了,可能终端用户并没有进入对话聊天页面,或者根本没有激活应用(Android 平台应用在后台也是可以收到消息的),所以「送达」并不等于终端用户真正「看到」了这条消息。
即时通讯服务还支持「已读」消息的回执,不过这首先需要接收方显式完成消息「已读」的确认。
由于即时通讯服务端是顺序下发新消息的,客户端不需要对每一条消息单独进行「已读」确认。我们设想的场景如下图所示:
用户在进入一个对话的时候,一次性清除当前对话的所有未读消息即可。Conversation
的清除接口如下:
- Unity
- Android
- iOS
/// <summary>
/// Mark the last message of this conversation as read.
/// </summary>
/// <returns></returns>
public Task Read();
/**
* 清除未读消息
*/
public void read();
/*!
将对话标记为已读。
该方法将本地对话中其他成员发出的最新消息标记为已读,该消息的发送者会收到已读通知。
*/
- (void)readInBackground;
对方「阅读」了消息之后,云端会向发送方发出一个回执通知,表明消息已被阅读。
Tom 和 Jerry 聊天,Tom 想及时知道 Jerry 是否阅读了自己发去的消息,这时候双方的处理流程是这样的:
-
Tom 向 Jerry 发送一条消息,且标记为「需要回执」:
- Unity
- Android
- iOS
LCIMTextMessage textMessage = new LCIMTextMessage("一条非常重要的消息。");
LCIMMessageSendOptions options = new LCIMMessageSendOptions {
Receipt = true
};
await conversation.Send(textMessage);LCIMClient tom = LCIMClient.getInstance("Tom");
LCIMConversation conv = client.getConversation("551260efe4b01608686c3e0f");
LCIMTextMessage textMessage = new LCIMTextMessage();
textMessage.setText("Hello, Jerry!");
LCIMMessageOption option = new LCIMMessageOption();
option.setReceipt(true); /* 将消息设置为需要回执。 */
conv.sendMessage(textMessage, option, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
/* 发送成功 */
}
}
});LCIMMessageOption *option = [[LCIMMessageOption alloc] init];
option.receipt = YES; /* 将消息设置为需要回执。 */
LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"Hello, Jerry!" attributes:nil];
[conversation sendMessage:message option:option callback:^(BOOL succeeded, NSError * _Nullable error) {
if (!error) {
/* 发送成功 */
}
}]; -
Jerry 阅读 Tom 发的消息后,调用对话上的
read
方法把「对话中最近的消息」标记为已读:- Unity
- Android
- iOS
await conversation.Read();
conversation.read();
[conversation readInBackground];
-
Tom 将收到一个已读回执,对话的
lastReadAt
属性会更新。此时可以更新 UI,把时间戳小于lastReadAt
的消息都标记为已读:- Unity
- Android
- iOS
tom.OnLastReadAtUpdated = (conv) => {
// Jerry 阅读了你的消息。可以通过调用 conversation.LastReadAt 来获得对方已经读取到的时间
};public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本地方法来处理对方已经阅读消息的通知
*/
public void onLastReadAtUpdated(LCIMClient client, LCIMConversation conversation) {
/* Jerry 阅读了你的消息。可以通过调用 conversation.getLastReadAt() 来获得对方已经读取到的时间点 */
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());// Tom 可以在 client 的 delegate 方法中捕捉到 lastReadAt 的更新
- (void)conversation:(LCIMConversation *)conversation didUpdateForKey:(LCIMConversationUpdatedKey)key {
if ([key isEqualToString:LCIMConversationUpdatedKeyLastReadAt]) {
NSDate *lastReadAt = conversation.lastReadAt;
/* Jerry 阅读了 你的消息。可以使用 lastReadAt 更新 UI,例如把时间戳小于 lastReadAt 的消息都标记为已读。 */
}
}
注意:
要使用已读回执,应用需要在初始化的时候开启 未读消息数更新通知 选项。
消息免打扰
假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。具体可以参考即时通讯开发指南第三篇的《消息免打扰》一节。