二,消息收发的更多方式,离线推送与消息同步,多设备登录
本章导读
在前一章从简单的单聊、群聊、收发图文消息开始里面,我们说明了如何在产品中增加一个基本的单聊/群聊页面,并响应服务端实时事件通知。接下来,在本篇文档中我们会讲解如何实现一些更复杂的业务需求,例如:
- 支持消息被接收和被阅读的状态回执,实现「Ding」一下的效果
- 发送带有成员提醒的消息(@ 某人),在超多用户群聊的场合提升目标用户的响应积极性
- 支持消息的撤回和修改
- 解决成员离线状态下的推送通知与重新上线后的消息同步,确保不丢消息
- 支持多设备登录,或者强制用户单点登录
- 扩展新的消息类型
消息收发的更多方式
在一个偏重工作协作或社交沟通的产品里,除了简单的消息收发之外,我们还会碰到更多需求,例如:
- 在消息中能否直接提醒某人,类似于很多 IM 工具中提供的 @ 消息,这样接收方能更明确地知道哪些消息需要及时响应;
- 消息发出去之后才发现内容不对,这时候能否修改或者撤回?
- 除了普通的聊天内容之外,是否支持发送类似于「XX 正在输入」这样的状态消息?
- 消息是否被其他人接收、读取,这样的状态能否反馈给发送者?
- 客户端掉线一段时间之后,可能会错过一批消息,能否提醒并同步一下未读消息?
等等,所有这些需求都可以通过即时通讯服务解决,下面我们来逐一看看具体的做法。
@ 成员提醒消息
在一些多人聊天群里面,因为消息量较大,很容易就导致某一条重要的消息没有被目标用户看到就被刷下去了,所以在消息中「@成员」是一种提醒接收者注意的有效办法。在微信这样的聊天工具里面,甚至会在对话列表页对有提醒的消息进行特别展示,用来通知消息目标高优先级查看和处理。
一般提醒消息都使用「@ + 人名」来表示目标用户,但是这里「人名」是一个由应用层决定的属性,可能有的产品使用全名,有的使用昵称,并且这个名字和即时通讯服务里面标识一个用户使用的 clientId 可能根本不一样(毕竟一个是给人看的,一个是给机器读的)。使用「人名」来圈定用户,也存在一种例外,就是聊天群组里面的用户名是可以改变的,如果消息发送的时 候「王五」还叫「王五」,等发送出来之后他恰好同步改成了「王大麻子」,这时候接收方的处理就比较麻烦了。还有第三个原因,就是「提醒全部成员」的表示方式,可能「@all」、「@group」、「@所有人」都会被选择,这是一个完全依赖应用层 UI 的选项。
所以「@ 成员」提醒消息并不能简单在文本消息中加入「@ + 人名」,解决方案是给普通消息(LCIMMessage)增加两个额外的属性:
mentionList,是一个字符串的数组,用来单独记录被提醒的clientId列表;mentionAll,是一个Bool型的标志位,用来表示是否要提醒全部成员。
带有提醒信息的消息,有可能既有提醒全部成员的标志,也还单独设置了 mentionList,这由应用层去控制。发送方在发送「@ 成员」提醒消息的时候,如何输入、选择成员名称,这是业务方 UI 层面需要解决的问题,即时通讯 SDK 不关心其实现逻辑,SDK 只要求开发者在发送一条「@ 成员」消息的时候,调用 mentionList 和 mentionAll 的 setter 方法,设置正确的成员列表即可。示例代码如下:
- Unity
- Android
- iOS
- JavaScript
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 的消息 已发出 */
}];
const message = new TextMessage(`@Tom 早点回家`).setMentionList(["Tom"]);
conversation
.send(message)
.then(function (message) {
console.log("发送成功!");
})
.catch(console.error);
或者也可以通过设置 mentionAll 属性值提醒所有人:
- Unity
- Android
- iOS
- JavaScript
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) {
/* 一条提及所有用户的消息已发出 */
}];
const message = new TextMessage(`@all`).mentionAll();
conversation
.send(message)
.then(function (message) {
console.log("发送成功!");
})
.catch(console.error);
对于消息的接收方来说,可以通过调用 mentionList 和 mentionAll 的 getter 方法来获得提醒目标用户的信息,示例代码如下:
- Unity
- Android
- iOS
- JavaScript
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;
}
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
var mentionList = receivedMessage.getMentionList();
});
此外,为了方便应用层 UI 展现,我们特意为 LCIMMessage 增加了两个标识位,用来显示被提醒的状态:
- 一个是
mentionedAll标识位,用来表示该消息是否提醒了当前对话的全体成员。只有mentionAll属性为true,这个标识位才为true,否则就为false。 - 另一个是
mentioned标识位,用来快速判断该消息是否提醒了当前登录用户。如果mentionList属性列表中包含有当前登录用户的clientId,或者mentionAll属性为true,那么mentioned方法都会返回true,否则返回false。
调用示例如下:
- Unity
- Android
- iOS
- JavaScript
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;
}
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
var mentionedAll = receivedMessage.mentionedAll;
var mentionedMe = receivedMessage.mentioned;
});
修改消息
在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 即时通讯 > 设置 > 即时通讯设置 启用「允许通过 SDK 编辑消息」后,终端用户可以对自己已经发送的消息进行修改(Conversation#updateMessage 方法)。目前即时通讯服务端并没有在时效性上进行限制,不过只允许用户修改自己发出去的消息,不允许修改别人的消息。
修改已经发送的消息,并不是直接在老的消息对象上修改,而是像发新消息一样创建一个消息实例,然后调用 Conversation#updateMessage(oldMessage, newMessage) 方法来向云端提交请求,示例代码如下:
- Unity
- Android
- iOS
- JavaScript
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(@"消息已被修改。");
}
}];
var newMessage = new TextMessage("new message");
conversation
.update(oldMessage, newMessage)
.then(function () {
// 修改成功
})
.catch(function (error) {
// 异常处理
});
消息修改成功之后,对话内的其他成员会立刻接收到 MESSAGE_UPDATE 事件:
- Unity
- Android
- iOS
- JavaScript
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 {
/* 有消息被修改 */
}
var { Event } = require("leancloud-realtime");
conversation.on(Event.MESSAGE_UPDATE, function (newMessage, reason) {
// newMessage 为修改后的消息
// 在视图层可以通过消息的 ID 找到原来的消息并用 newMessage 替换
// reason (可选)对象表示消息修改的原因,
// reason 不存在表示发送者主动修改。
// reason 的 code 属性为正数时,表示因触发云引擎 hook 而导致消息修改
// (具体数值由开发者在 hook 函数定义中自行指定),
// reason 的 code 属性为负数时,表示因触发系统内置机制而导致消息修改,
// 例如 -4408 表示因敏感词过滤被修改。
// reason 的 detail 属性是一个字符串,指明具体的修改原因。
});
对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部会先从缓存中修改这条消息记录,然后再通知应用层。所以对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(这时候消息列表会出现内容变化)。
如果系统修改了消息(例如触发了内置的敏感词过滤功能,或者云引擎的 hook 函数),发送者会收到 MESSAGE_UPDATE 事件,其他对话成员接收到的是修改过的消息。