const message = new TextMessage(`@all`).mentionAll();
conversation.send(message).then(function(message) {
console.log('发送成功!');
}).catch(console.error);
do {
let message = IMTextMessage(text: "@all")
message.isAllMembersMentioned = true
try conversation.send(message: message, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
var mentionedAll = receivedMessage.mentionedAll;
var mentionedMe = receivedMessage.mentioned;
});
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case .message(event: let messageEvent):
switch messageEvent {
case .received(message: let message):
print(message.isCurrentClientMentioned)
default:
break
}
default:
break
}
}
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) {
// 异常处理
});
do {
let newMessage = IMTextMessage(text: "Just a new message")
try conversation.update(oldMessage: oldMessage, to: newMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
do {
try conversation.recall(message: oldMessage, completion: { (result) in
switch result {
case .success(value: let recalledMessage):
print(recalledMessage)
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
/// Message Sending Option
public struct MessageSendOptions: OptionSet {
/// Get Receipt when other client received message or read message.
public static let needReceipt = MessageSendOptions(rawValue: 1 << 0)
/// Indicates whether this message is transient.
public static let isTransient = MessageSendOptions(rawValue: 1 << 1)
/// Indicates whether this message will be auto delivering to other client when this client disconnected.
public static let isAutoDeliveringWhenOffline = MessageSendOptions(rawValue: 1 << 2)
}
/// Send Message.
///
/// - Parameters:
/// - message: The message to be sent.
/// - options: @see `MessageSendOptions`.
/// - priority: @see `IMChatRoom.MessagePriority`.
/// - pushData: The push data of APNs.
/// - progress: The file uploading progress.
/// - completion: callback.
public func send(message: IMMessage, options: MessageSendOptions = .default, priority: IMChatRoom.MessagePriority? = nil, pushData: [String : Any]? = nil, progress: ((Double) -> Void)? = nil, completion: @escaping (LCBooleanResult) -> Void) throws
const message = new TextMessage('Tom 正在输入…');
conversation.send(message, {transient: true});
do {
let message = IMTextMessage(text: "Tom 正在输入…")
try conversation.send(message: message, options: [.isTransient], completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
不过,有些业务场景会对消息投递的细节有更高的要求,例如消息的发送方要能知道什么时候接收方收到了这条消息,什么时候 ta 又点开阅读了这条消息。有一些偏重工作写作或者私密沟通的产品,消息发送者在发送一条消息之后,还希望能看到消息被送达和阅读的实时状态,甚至还要提醒未读成员。这样「苛刻」的需求,就依赖于我们的「消息回执」功能来实现。
var message = new TextMessage('一条非常重要的消息。');
conversation.send(message, {
receipt: true,
});
do {
let message = IMTextMessage(text: "一条非常重要的消息。")
try conversation.send(message: message, options: [.needReceipt], completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
/// Clear unread messages that its sent timestamp less than the sent timestamp of the parameter message.
///
/// - Parameter message: The default is the last message.
public func read(message: IMMessage? = nil)
await conversation.read();
对方「阅读」了消息之后,云端会向发送方发出一个回执通知,表明消息已被阅读。
Tom 和 Jerry 聊天,Tom 想及时知道 Jerry 是否阅读了自己发去的消息,这时候双方的处理流程是这样的:
Tom 向 Jerry 发送一条消息,且标记为「需要回执」:
var message = new TextMessage('一条非常重要的消息。');
conversation.send(message, {
receipt: true,
});
do {
let message = IMTextMessage(text: "Hello, Jerry!")
try conversation.send(message: message, options: [.needReceipt], completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
var message = new TextMessage('我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。');
conversation.send(message, { will: true }).then(function() {
// 发送成功,当前 client 掉线的时候,这条消息会被下发给对话里面的其他成员
}).catch(function(error) {
// 异常处理
});
do {
let message = IMTextMessage(text: "我是一条遗愿消息,当发送者意外下线的时候,我会被下发给对话里面的其他成员。")
try conversation.send(message: message, options: [.isAutoDeliveringWhenOffline], completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
do {
try conversation.insertFailedMessageToCache(failedMessage) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
// 暂不支持
将消息从缓存中删除:
// 暂不支持
conversation.removeFromLocalCache(message);
[conversation removeMessageFromCache:message];
// 暂不支持
do {
try conversation.removeFailedMessageFromCache(failedMessage) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
基于 Apple 推荐使用的 Token Authentication 方式进行推送时,如果应用配置了多个不同 Team ID 的 Private Key,请确认目标用户设备使用的 APNs Team ID 并将其填写在 _apns_team_id 参数内,以保证推送正常进行,只有指定 Team ID 的设备能收到推送(Apple 不允许在一次推送请求中向多个从属于不同 Team ID 的设备发推送)。如:
do {
let client = try IMClient(ID: "CLIENT_ID", tag: "Mobile")
client.open { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
do {
let client = try IMClient(ID: "Tom", tag: "Mobile")
client.open(options: [.reconnect]) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
if error.code == 4111 {
// 冲突时登录失败,不会踢掉较早登录的设备
}
}
}
} catch {
print(error)
}
class EmojiMessage : LCIMTypedMessage {
public const int EmojiMessageType = 1;
public override int MessageType => EmojiMessageType;
public string Ecode {
get {
return data["ecode"] as string;
} set {
data["ecode"] = value;
}
}
}
// 注册子类
LCIMTypedMessage.Register(EmojiMessage.EmojiMessageType, () => new EmojiMessage());
@LCIMMessageType(type = 123)
public class CustomMessage extends LCIMTypedMessage {
// 空的构造方法,不可遗漏
public CustomMessage() {
}
@LCIMMessageField(name = "_lctext")
String text;
@LCIMMessageField(name = "_lcattrs")
Map<String, Object> attrs;
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
public Map<String, Object> getAttrs() {
return this.attrs;
}
public void setAttrs(Map<String, Object> attr) {
this.attrs = attr;
}
}
// 注册自定义类型
LCIMMessageManager.registerLCIMMessageType(CustomMessage.class);
二,消息收发的更多方式,离线推送与消息同步,多设备登录
本章导读
在前一章从简单的单聊、群聊、收发图文消息开始里面,我们说明了如何在产品中增加一个基本的单聊/群聊页面,并响应服务端实时事件通知。接下来,在本篇文档中我们会讲解如何实现一些更复杂的业务需求,例如:
消息收发的更多方式
在一个偏重工作协作或社交沟通的产品里,除了简单的消息收发之外,我们还会碰到更多需求,例如:
等等,所有这些需求都可以通过即时通讯服务解决,下面我们来逐一看看具体的做法。
@ 成员提醒消息
在一些多人聊天群里面,因为消息量较大,很容易就导致某一条重要的消息没有被目标用户看到就被刷下去了,所以在消息中「@成员」是一种提醒接收者注意的有效办法。在微信这样的聊天工具里面,甚至会在对话列表页对有提醒的消息进行特别展示,用来通知消息目标高优先级查看和处理。
一般提醒消息都使用「@ + 人名」来表示目标用户,但是这里「人名」是一个由应用层决定的属性,可能有的产品使用全名,有的使用昵称,并且这个名字和即时通讯服务里面标识一个用户使用的
clientId
可能根本不一样(毕竟一个是给人看的,一个是给机器读的)。使用「人名」来圈定用户,也存在一种例外,就是聊天群组里面的用户名是可以改变的,如果消息发送的时候「王五」还叫「王五」,等发送出来之后他恰好同步改成了「王大麻子」,这时候接收方的处理就比较麻烦了。还有第三个原因,就是「提醒全部成员」的表示方式,可能「@all」、「@group」、「@所有人」都会被选择,这是一个完全依赖应用层 UI 的选项。所以「@ 成员」提醒消息并不能简单在文本消息中加入「@ + 人名」,解决方案是给普通消息(
LCIMMessage
)增加两个额外的属性:mentionList
,是一个字符串的数组,用来单独记录被提醒的clientId
列表;mentionAll
,是一个Bool
型的标志位,用来表示是否要提醒全部成员。带有提醒信息的消息,有可能既有提醒全部成员的标志,也还单独设置了
mentionList
,这由应用层去控制。发送方在发送「@ 成员」提醒消息的时候,如何输入、选择成员名称,这是业务方 UI 层面需要解决的问题,即时通讯 SDK 不关心其实现逻辑,SDK 只要求开发者在发送一条「@ 成员」消息的时候,调用mentionList
和mentionAll
的 setter 方法,设置正确的成员列表即可。示例代码如下:或者也可以通过设置
mentionAll
属性值提醒所有人:对于消息的接收方来说,可以通过调用
mentionList
和mentionAll
的 getter 方法来获得提醒目标用户的信息,示例代码如下:此外,并且为了方便应用层 UI 展现,我们特意为
LCIMMessage
增加了两个标识位,用来显示被提醒的状态:mentionedAll
标识位,用来表示该消息是否提醒了当前对话的全体成员。只有mentionAll
属性为true
,这个标识位才为true
,否则就为false
。mentioned
标识位,用来快速判断该消息是否提醒了当前登录用户。如果mentionList
属性列表中包含有当前登录用户的clientId
,或者mentionAll
属性为true
,那么mentioned
方法都会返回true
,否则返回false
。调用示例如下:
修改消息
在 云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 启用 「允许通过 SDK 编辑消息」后,终端用户可以对自己已经发送的消息进行修改(
Conversation#updateMessage
方法)。目前即时通讯服务端并没有在时效性上进行限制,不过只允许用户修改自己发出去的消息,不允许修改别人的消息。修改已经发送的消息,并不是直接在老的消息对象上修改,而是像发新消息一样创建一个消息实例,然后调用
Conversation#updateMessage(oldMessage, newMessage)
方法来向云端提交请求,示例代码如下:消息修改成功之后,对话内的其他成员会立刻接收到
MESSAGE_UPDATE
事件:对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部会先从缓存中修改这条消息记录,然后再通知应用层。所以对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(这时候消息列表会出现内容变化)。
如果系统修改了消息(例如触发了内置的敏感词过滤功能,或者云引擎的 hook 函数),发送者会收到
MESSAGE_UPDATE
事件,其他对话成员接收到的是修改过的消息。撤回消息
除了修改消息,终端用户还可以撤回一条自己之前发送过的消息。 和修改消息类似,这一功能需要在控制台启用(云服务控制台 > 即时通讯 > 设置 > 即时通讯设置 启用「允许通过 SDK 撤回消息」)。 同样,即时通讯服务端并没有在时效性上进行限制,不过只允许用户撤回自己发出去的消息,不允许撤回别人的消息。
撤回消息调用
Conversation#recallMessage
方法,示例代码如下:成功撤回消息后,对话内的其他成员会接收到
MESSAGE_RECALL
的事件:对于 Android 和 iOS SDK 来说,如果开启了消息缓存的选项的话(默认开启),SDK 内部需要保证数据的一致性,所以会先从缓存中删除这条消息记录,然后再通知应用层。对于开发者来说,收到这条通知之后刷新一下目标聊天页面,让消息列表更新即可(此时消息列表中的消息会直接变少,或者显示撤回提示)。
暂态消息
有时候我们需要发送一些特殊的消息,譬如聊天过程中「某某正在输入…」这样的实时状态信息,或者当群聊的名称修改以后给该群成员发送「群名称被某某修改为 XX」这样的通知信息。这类消息与终端用户发送的消息不一样,发送者不要求把它保存到历史记录里,也不要求一定会被送达(如果成员不在线或者现在网络异常,那么没有下发下去也无所谓),这种需求可以使用「暂态消息」来实现。
「暂态消息」是一种特殊的消息,与普通消息相比有以下几点不同:
我们可以用「暂态消息」发送一些实时的、频繁变化的状态信息,或者用来实现简单的控制协议。
暂态消息的数据和构造方式与普通消息是一样的,只是其发送方式与普通消息有一些区别。到目前为止,我们演示的
LCIMConversation
发送消息接口都是这样的:其实即时通讯 SDK 还允许在发送一条消息的时候,指定额外的参数
LCIMMessageOption
,LCIMConversation
完整的消息发送接口如下:通过
LCIMMessageOption
参数我们可以指定:transient
属性);receipt
属性,消息回执,后续章节会进行说明);priority
属性,后续章节会说明);will
属性,后续章节会说明);pushData
属性,后续章节会说明),如果消息接收方不在线,会推送指定的内容。如果我们需要让 Tom 在聊天页面的输入框获得焦点的时候,给群内成员同步一条「Tom 正在输入…」的状态信息,可以使用如下代码:
暂态消息的接收逻辑和普通消息一样,开发者可以按照消息类型进行判断和处理,这里不再赘述。上面使用了内建的文本消息只是一种示例,从展现端来说,我们如果使用特定的类型来表示「暂态消息」,是一种更好的方案。即时通讯 SDK 并没有提供固定的「暂态消息」类型,可以由开发者根据自己的业务需要来实现专门的自定义,具体可以参考后述章节:扩展自己的消息类型。
消息回执
即时通讯服务端在进行消息投递的时候,会按照消息上行的时间先后顺序下发(先收到的消息先下发,保证顺序性),且内部协议上会要求 SDK 对收到的每一条消息进行确认(ack)。如果 SDK 收到了消息,但是在发送 ack 的过程中出现网络丢包,即时通讯服务端还是会认为消息没有投递下去,之后会再次投递,直到收到 SDK 的应答确认为止。与之对应,SDK 内部也进行了消息去重处理,保证在上面这种异常条件下应用层也不会收到重复的消息。所以我们的消息系统从协议上是可以保证不丢任何一条消息的。
不过,有些业务场景会对消息投递的细节有更高的要求,例如消息的发送方要能知道什么时候接收方收到了这条消息,什么时候 ta 又点开阅读了这条消息。有一些偏重工作写作或者私密沟通的产品,消息发送者在发送一条消息之后,还希望能看到消息被送达和阅读的实时状态,甚至还要提醒未读成员。这样「苛刻」的需求,就依赖于我们的「消息回执」功能来实现。
与上一节「暂态消息」的发送类似,要使用消息回执功能,需要在发送消息时在
LCIMMessageOption
参数中标记「需要回执」选项:那么发送方后续该如何响应回执的通知消息呢?
送达回执
当接收方收到消息之后,云端会向发送方发出一个回执通知,表明消息已经送达。请注意与「已读回执」区别开。
请注意这里送达回执的内容,不是某一条具体的消息,而是当前对话内最后一次送达消息的时间戳(
lastDeliveredAt
)。最开始我们有过解释,服务端在下发消息的时候,是能够保证顺序的,所以在送达回执的通知里面,我们不需要对逐条消息进行确认,只给出当前确认送达的最新消息的时间戳,那么在这之前的所有消息就都是已经送达的状态。在 UI 层展示的时候,可以将早于lastDeliveredAt
的消息都标记为「已送达」。已读回执
消息送达只是即时通讯服务端和客户端之间的投递行为完成了,可能终端用户并没有进入对话聊天页面,或者根本没有激活应用(Android 平台应用在后台也是可以收到消息的),所以「送达」并不等于终端用户真正「看到」了这条消息。
即时通讯服务还支持「已读」消息的回执,不过这首先需要接收方显式完成消息「已读」的确认。
由于即时通讯服务端是顺序下发新消息的,客户端不需要对每一条消息单独进行「已读」确认。我们设想的场景如下图所示:
用户在进入一个对话的时候,一次性清除当前对话的所有未读消息即可。
Conversation
的清除接口如下:对方「阅读」了消息之后,云端会向发送方发出一个回执通知,表明消息已被阅读。
Tom 和 Jerry 聊天,Tom 想及时知道 Jerry 是否阅读了自己发去的消息,这时候双方的处理流程是这样的:
Tom 向 Jerry 发送一条消息,且标记为「需要回执」:
Jerry 阅读 Tom 发的消息后,调用对话上的
read
方法把「对话中最近的消息」标记为已读:Tom 将收到一个已读回执,对话的
lastReadAt
属性会更新。此时可以更新 UI,把时间戳小于lastReadAt
的消息都标记为已读:消息免打扰
假如某一用户不想再收到某对话的消息提醒,但又不想直接退出对话,可以使用静音操作,即开启「免打扰模式」。具体可以参考《即时通讯开发指南》第三篇的《消息免打扰》一节。
Will(遗愿)消息
即时通讯服务还支持一类比较特殊的消息:Will(遗愿)消息。「Will 消息」是在一个用户突然掉线之后,系统自动通知对话的其他成员关于该成员已掉线的消息,好似在掉线后要给对话中的其他成员一个妥善的交待,所以被戏称为「遗愿」消息,如下图中的「Tom 已断线,无法收到消息」:
要发送 Will 消息,用户需要设定好消息内容发给云端,云端并不会将其马上发送给对话的成员,而是缓存下来,一旦检测到该用户掉线,云端立即将这条遗愿消息发送出去。开发者可以利用它来构建自己的断线通知的逻辑。
客户端发送完毕之后就完全不用再关心这条消息了,云端会自动在发送方异常掉线后通知其他成员,接收端则根据自己的需求来做 UI 的展现。
Will 消息有 如下限制:
消息内容过滤
对于多人参与的聊天群组来说,内容的审核和实时过滤是产品运营上的基本要求。我们即时通讯服务默认提供了敏感词过滤的功能,具体可以参考《即时通讯开发指南》第三篇的《消息内容的实时过滤》一节。
本地发送失败的消息
有时你可能需要将发送失败的消息临时保存到客户端本地的缓存中,等到合适时机再进行处理。例如,将由于网络突然中断而发送失败的消息先保留下来,在消息列表中展示这种消息时,额外添加出错的提示符号和重发按钮,待网络恢复后再由用户选择是否重发。
即时通讯 Android 和 iOS SDK 默认提供了消息本地缓存的功能,消息缓存中保存的都是已经成功上行到云端的消息,并且能够保证和云端的数据同步。为了方便开发者,SDK 也支持将一时失败的消息加入到缓存中。
将消息加入缓存的代码如下:
将消息从缓存中删除:
从缓存中取出来的消息,在 UI 展示的时候可以根据
message.status
的属性值来做不同的处理,status
属性为LCIMMessageStatusFailed
时即表示是发送失败了的本地消息,这时可以在消息旁边显示一个重新发送的按钮。通过将失败消息加入到 SDK 缓存中,还有一个好处就是,消息从缓存中取出来再次发送,不会造成服务端消息重复,因为 SDK 有做专门的去重处理。离线推送通知
对于移动设备来说,在聊天的过程中部分客户端难免会临时下线,如何保证离线用户也能及时收到消息,是我们需要考虑的重要问题。即时通讯云端会在用户下线的时候,主动通过「Push Notification」这种外部方式来通知客户端新消息到达事件,以促使用户尽快打开应用查看新消息。
iOS 和 Android 分别提供了内置的离线消息推送通知服务,但是使用的前提是按照推送文档配置 iOS 的推送证书和 Android 开启推送的开关,详细请阅读如下文档:
云端会将用户的即时通讯
clientId
与推送服务的设备数据_Installation
自动进行关联。当用户 A 发出消息后,如果对话中部分成员当前不在线,而且这些成员使用的是 iOS 设备,或者是成功开通混合推送功能的 Android 设备的话,云端会自动将即时通讯消息转成特定的推送通知发送至客户端,同时我们也提供扩展机制,允许开发者对接第三方的消息推送服务。要有效使用本功能,关键在于 自定义推送的内容。我们提供三种方式允许开发者来指定推送内容:
静态配置提醒消息
用户可以在控制台中为应用设置一个全局的静态 JSON 字符串,指定固定内容来发送通知。例如,我们进入 云服务控制台 > 即时通讯 > 设置 > 离线推送,填入:
那么在有新消息到达的时候,符合条件的离线用户会收到一条「您有新的消息」的通知栏消息。
注意,这里
badge
参数为 iOS 设备专用,且Increment
大小写敏感,表示自动增加应用 badge 上的数字计数。 通常需要在打开或退出应用时,通过设置 Installation 的 badge 字段清零 badge 计数。此外,对于 iOS 设备您还可以设置声音等推送属性,具体的字段可以参考《推送 REST API 使用指南》的《消息内容参数》一节。
客户端发送消息的时候额外指定推送信息
第一种方法虽然发出去了通知,但是因为通知文本与实际消息内容完全无关,存在一些不足。有没有办法让推送消息的内容与即时通讯消息动态相关呢?
还记得我们发送「暂态消息」时的
LCIMMessageOption
参数吗?即时通讯 SDK 允许客户端在发送消息的时候,指定附加的推送信息(在LCIMMessageOption
中设置pushData
属性),这样在需要离线推送的时候我们就会使用这里设置的内容来发出推送通知。示例代码如下:服务端动态生成通知内容
第二种方法虽然动态,但是需要在客户端发送消息的时候提前准备好推送内容,这对于开发阶段的要求比较高,并且在灵活性上有比较大的限制,所以看上去也不够完美。
我们还提供了第三种方式,让开发者在推送动态内容的时候,也不失实现上的灵活性。这种方式需要使用即时通讯 Hook 机制在服务端来统一指定离线推送消息内容,感兴趣的开发者可以参阅《即时通讯开发指南》第三篇。
三种方式之间的优先级如下:服务端动态生成通知 > 客户端发送消息的时候额外指定推送信息 > 静态配置提醒消息。
也就是说如果开发者同时采用了多种方式来指定消息推送,那么有服务端动态生成的通知的话,最后以它为准进行推送。其次是客户端发送消息的时候额外指定推送内容,最后是静态配置的提醒消息。
实现原理与限制
同时使用了推送服务和即时通讯服务的应用,客户端在成功登录即时通讯服务时,SDK 会自动关联当前的
clientId
和设备数据(推送服务中的Installation
表)。关联的方式是通过让目标设备 订阅 名为clientId
的 Channel 实现的。开发者可以在数据存储的_Installation
表中的channels
字段查到这组关联关系。在实际离线推送时,云端系统会根据用户clientId
找到对应的关联设备进行推送。由于即时通讯触发的推送量比较大,内容单一,所以推送服务云端不会保留这部分记录,开发者在 云服务控制台 > 推送 > 推送记录 中也无法找到这些记录。
推送服务的通知过期时间是 7 天,也就是说,如果一个设备 7 天内没有连接到 APNs、MPNs 或设备对应的混合推送平台,系统将不会再给这个设备推送通知。
其他推送设置
iOS 环境下,离线消息默认推送至 APNs 的生产环境。 指定附加推送信息时使用
"_profile": "dev"
可以切换至 APNs 的开发环境(如果基于证书鉴权方式进行推送,此时会使用开发证书):基于 Apple 推荐使用的 Token Authentication 方式进行推送时,如果应用配置了多个不同 Team ID 的 Private Key,请确认目标用户设备使用的 APNs Team ID 并将其填写在
_apns_team_id
参数内,以保证推送正常进行,只有指定 Team ID 的设备能收到推送(Apple 不允许在一次推送请求中向多个从属于不同 Team ID 的设备发推送)。如:_profile
和_apns_team_id
属性为推送服务内部使用,均不会实际推送。 指定附加推送信息时,支持为不同种类的设备(比如ios
、android
)附加不同的推送信息,需要特别注意的是,_profile
和_apns_team_id
这两个内部属性不要在ios
对象内部指定,否则不会生效。 例如,这样的附加推送消息会导致离线消息被推送至 APNs 的生产环境:这样才能推送至开发环境:
目前,云服务控制台 > 即时通讯 > 设置 > 离线推送 这里的推送内容也支持一些内置变量,你可以将上下文信息直接设置到推送内容中:
${convId}
推送相关的对话 ID${timestamp}
触发推送的时间戳(Unix 时间戳)${fromClientId}
消息发送者的clientId
离线消息同步
离线推送通知是一种提醒用户的非常有效手段,但是如果用户不上线,即时通讯的消息就总是无法下发,客户端如果长时间下线,会导致大量消息堆积在云端,此后如果用户再上线,我们该如何处理才能保证消息完全不丢失呢?
即时通讯服务提供客户端主动从云端「拉」的方式。云端会记录下用户在每一个参与对话中接收的最后一条消息的位置,在用户重新登录上线后,实时计算出用户离线期间产生未读消息的对话列表及对应的未读消息数,以「未读消息数更新」的事件通知到客户端,然后客户端在需要的时候来主动拉取这些离线消息。
未读消息数更新通知
在客户端重新登录上线后,即时通讯云端会实时计算下线时间段内当前用户参与过的对话中的新消息数量。
客户端只有设置了主动拉取的方式,云端才会在必要的时候下发这一通知。如前所述,对于 JavaScript / Android / iOS SDK 来说,仅支持客户端主动拉取未读消息,所以不需要再做什么设置。
客户端 SDK 会在
IMConversation
上维护一个unreadMessagesCount
字段,来统计当前对话中存在有多少未读消息。客户端用户登录之后,云端会以「未读消息数更新」事件的形式,将当前用户所在的多对
<Conversation, UnreadMessageCount, LastMessage>
数据通知到客户端,这就是客户端维护的<Conversation, UnreadMessageCount>
初始值。之后 SDK 在收到新的在线消息的时候,会自动增加对应的unreadMessageCount
计数。直到用户把某一个对话的未读消息清空,这时候云端和 SDK 的<Conversation, UnreadMessageCount>
计数都会清零。客户端 SDK 在
<Conversation, UnreadMessageCount>
数字变化的时候,会通过IMClient
派发「未读消息数量更新(UNREAD_MESSAGES_COUNT_UPDATE
)」事件到应用层。开发者可以监听UNREAD_MESSAGES_COUNT_UPDATE
事件,在对话列表界面上更新这些对话的未读消息数量。建议开发者在应用层面对未读计数的结果进行持久化缓存,如果同一个对话有两个不同的未读数,则使用新数据直接覆盖老数据,这样对话列表里面展示的未读数会比较准确。对开发者来说,在
UNREAD_MESSAGES_COUNT_UPDATE
事件响应的时候,SDK 传给应用层的Conversation
对象,其lastMessage
应该是当前时点当前用户在当前对话里面接收到的最后一条消息,开发者如果要展示更多的未读消息,就需要通过消息拉取的接口来主动获取了(参见《即时通讯开发指南》第一篇的《聊天记录查询》一节。清除对话未读消息数的唯一方式是调用
Conversation#read
方法将对话标记为已读,一般来说开发者至少需要在下面两种情况下将对话标记为已读:iOS 和 Android 应用层需要持久化缓存未读计数的细节说明
对于未读通知的下发时机和数量,iOS 和 Java/Android 两个平台的 SDK 在内部处理上稍有差异:iOS SDK(Objective-C 和 Swift 都包括)在每次登录即时通讯云端的时候,都会获得云端下发的大量未读通知;而 Java/Android SDK 由于内部持久化缓存了通知的时间戳(能减轻服务端压力),所以登录即时通讯云端之后客户端只会收到上次通知时间戳之后发生了变化的部分未读数通知。
因此 Java SDK 的开发者需要在应用层缓存收到的未读数通知(同一个对话的未读数采用覆盖的方式来更新),而 iOS SDK 这里收到的大量未读通知并不等于全量数据(云端追踪的有未读消息的对话数不超过 50 个),所以也是一样需要在应用层面缓存收到的未读计数结果,这样才能保证对话列表超过 50 个之后未读计数值的准确性。
多端登录与单设备登录
一个用户可以使用相同的账号在不同的客户端上登录(例如 QQ 网页版和手机客户端可以同时接收到消息和回复消息,实现多端消息同步),而有一些场景下,需要禁止一个用户同时在不同客户端登录,例如我们不能用同一个微信账号在两个手机上同时登录。即时通讯服务提供了灵活的机制,来满足 多端登录 和 单设备登录 这两种完全相反的需求。
即时通讯 SDK 在生成
IMClient
实例的时候,允许开发者在clientId
之外,增加一个额外的tag
标记。云端在用户主动登录的时候,会检查<ClientId, Tag>
组合的唯一性。如果当前用户已经在其他设备上使用同样的tag
登录了,那么云端会强制让之前登录的设备下线。如果多个tag
不发生冲突,那么云端会把他们当成独立的设备进行处理,应该下发给该用户的消息会分别下发给所有设备,不同设备上的未读消息计数则是合并在一起的(各端之间消息状态是同步的);该用户在单个设备上发出来的上行消息,云端也会默认同步到其他设备。基于以上机制,即时通讯可以支持应用实现多种业务需求:
tag
,默认对用户的多端登录不作限制。用户可以在多个设备上登录,比如在手机和平板上同时登录,甚至在两台不同的手机上登录,多个设备可以同时接收和回复消息。tag
,限制用户只能在一台设备上登录。tag
,允许用户在多台不同类型的设备上登录。例如,我们可以设计三种tag
:Mobile
、Pad
、Web
,分别对应三种类型的设备:手机、平板和电脑,那么用户分别在三种设备上登录就都是允许的,但是却不能同时在两台电脑上登录。详见下面的代码示例。设置登录标记
按照上面的方案,以手机端登录为例,在创建
IMClient
实例的时候,我们增加tag: Mobile
这样的标记:之后如果同一个用户在另一个手机上再次登录,则较早前登录系统的客户端会被强制下线。
处理登录冲突
即时通讯云端在登录用户的
<ClientId, Tag>
相同的时候,总是踢掉较早登录的设备,这时候较早登录设备端会收到被云端下线(CONFLICT
)的事件通知:如上述代码中,被动下线的时候,云端会告知原因,因此客户端在做展现的时候也可以做出类似于 QQ 一样友好的通知。
以上提到的登录均指用户主动进行登录操作。 已登录用户在应用启动、网络中断等场景下,SDK 会自动重新登录。 这种情况下,如果触发登录冲突,云端并不会踢掉较早登录的设备,自动重新登录的设备则会收到登录冲突的报错,登录失败。
相应地,应用开发者如果希望在用户主动登录触发冲突时,不踢掉较早登录的设备,而提示用户登录失败,可以在登录时传入参数指明这一点:
扩展自己的消息类型
尽管即时通讯服务默认已经包含了丰富的消息类型,但是我们依然支持开发者根据业务需要扩展自己的消息类型,例如允许用户之间发送名片、红包等等。这里「名片」和「红包」就可以是应用层定义的自己的消息类型。
自定义消息属性
即时通讯 SDK 默认提供了多种消息类型用来满足常见的需求:
TextMessage
文本消息ImageMessage
图像消息AudioMessage
音频消息VideoMessage
视频消息FileMessage
普通文件消息(.txt/.doc/.md 等各种)LocationMessage
地理位置消息这些消息类型还支持应用层设置若干 key-value 自定义属性来实现扩展。譬如有一条文本消息需要附带城市信息,这时候开发者使用消息类中预留的
attributes
属性就可以保存额外信息了。自定义消息类型
在默认的消息类型完全无法满足需求的时候,可以实现和使用自定义的消息类型。
通过继承
TypedMessage
,开发者也可以扩展自己的富媒体消息。其要求和步骤是:TypedMessage
或其子类,然后:messageType(123)
装饰器,具体消息类型的值(这里是123
)由开发者自己决定(内建消息类型使用负数,所有正数都预留给开发者扩展使用)。messageField(['fieldName'])
装饰器来声明需要发送的字段。Realtime#register()
函数注册这个消息类型。举个例子,实现一个在 暂态消息 中提出的
OperationMessage
:继承于
IMCategorizedMessage
,开发者也可以扩展自己的富媒体消息。其要求和步骤是:IMMessageCategorizing
协议;AppDelegate
的application(_:didFinishLaunchingWithOptions:)
方法里面调用try CustomMessage.register()
。继承于
LCIMTypedMessage
,开发者也可以扩展自己的富媒体消息。其要求和步骤是:LCIMTypedMessageSubclassing
协议;+load
方法或者UIApplication
的-application:didFinishLaunchingWithOptions:
方法里面调用[YourClass registerSubclass]
。继承于
LCIMTypedMessage
,开发者也可以扩展自己的富媒体消息。其要求和步骤是:LCIMTypedMessage
。这里需要注意:@LCIMMessageType(type=123)
的 Annotation具体消息类型的值(这里是
123
)由开发者自己决定。内建消息类型使用负数,所有正数都预留给开发者扩展使用。@LCIMMessageField(name="")
的 Annotationname
为可选字段,同时自定义的字段要有对应的 getter/setter 方法。LCIMMessageManager.registerLCIMMessageType()
函数进行类型注册。LCIMMessageManager.registerMessageHandler()
函数进行消息处理 handler 注册。继承于
LCIMTypedMessage
,开发者也可以扩展自己的富媒体消息。其要求和步骤是:LCIMTypedMessage
。继承于
TypedMessage
,开发者也可以扩展自己的富媒体消息。步骤是:自定义消息的接收,可以参看《即时通讯开发指南》第一篇的《再谈接收消息》。
进一步阅读