一,从简单的单聊、群聊、收发图文消息开始
阅读准备
在阅读本章之前,如果你还不太了解即时通讯服务的总体架构,建议先阅读即时通讯服务总览。 另外,如果你还没有下载对应开发环境(语言)的 SDK,请参考相应语言的 SDK 配置指南完成 SDK 安装与初始化:
本章导读
在很多产品里面,都存在让用户实时沟通的需求,例如:
- 员工与客户之间的实时交流,如房地产行业经纪人与客户的沟通,商业产品客服与客户的沟通,等等。
- 企业内部沟通协作,如内部的工作流系统、文档/知识库系统,增加实时互动的方式可能就会让工作效率得到极大提升。
- 直播互动,不论是文体行业的大型电视节目中的观众互动、重大赛事直播,娱乐行业的游戏现场直播、网红直播,还是教育行业的在线课程直播、KOL 知识分享,在支持超大规模用户积极参与的同时,也需要做好内容审核管理。
- 应用内社交,游戏公会嗨聊,等等。社交产品要能长时间吸引住用户,除了实时性之外,还需要更多的创新玩法,对于标准化通讯服务会存在更多的功能扩展需求。
根据功能需求的层次性和技术实现的难易程度不同,我们分为多篇文档来一步步地讲解如何利用即时通讯服务实现不同业务场景需求:
- 本篇文档,我们会从实现简单的单聊/群聊开始,演示创建和加入「对话」、发送和接收富媒体「消息」的流程,同时让大家了解历史消息云端保存与拉取的机制,希望可以满足在成熟产品中快速集成一个简单的聊天页面的需求。
- 离线消息文档会介绍一些特殊消息的处理,例如 @ 成员提醒、撤回和修改、消息送达和被阅读的回执通知等,离线状态下的推送通知和消息同步机制,多设备登录的支持方案,以及如何扩展自定义消息类型,希望可以满足一个社交类产品的多方面需求。
- 权限与聊天室文档会介绍一下系统的安全机制,包括第三方的操作签名,以及「对话」成员的权限管理和黑名单机制,同时也会介绍直播聊天室和临时对话的用法,希望可以帮助开发者提升产品的安全性和易用性,并满足特殊场景的需求。
- Hook 与系统对话文档会介绍即时通讯服务端 Hook 机制,系统对话的用法,以及给出一个基于这两个功能打造一个属于自己的聊天机器人的方案,希望可以满足业务层多种多样的扩展需求。
希望开发者最终顺利完成产品开发的同时,也对即时通讯服务的体系结构有一个清晰的了解,以便于产品的长期维护和定制化扩展。
一对一单聊
在开始讨论聊天之前,我们需要介绍一下在即时通讯 SDK 中的 IMClient
对象:
IMClient
对应实体的是一个用户,它代表着一个用户以客户端的身份登录到了即时通讯的系统。
具体可以参考即时通讯服务总览中《clientId、用户和登录》一节的说明。
创建 IMClient
假设我们产品中有一个叫「Tom」的用户,首先我们在 SDK 中创建出一个与之对应的 IMClient
实例(创建实例前请确保已经成功初始化了 SDK):
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
LCIMClient tom = new LCIMClient("Tom");
// clientId 为 Tom
LCIMClient tom = LCIMClient.getInstance("Tom");
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *tom;
// 初始化
NSError *error;
tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error];
if (error) {
NSLog(@"init failed with error: %@", error);
} else {
NSLog(@"init succeeded");
}
// Tom 用自己的名字作为 clientId 来登录即时通讯服务
realtime
.createIMClient("Tom")
.then(function (tom) {
// 成功登录
})
.catch(console.error);
// 定义一个常驻内存的全局变量
var tom: IMClient
// 初始化
do {
tom = try IMClient(ID: "Tom")
} catch {
print(error)
}
// clientId 为 Tom
Client tom = Client(id: 'Tom');
注意这里一个 IMClient
实例就代表一个终端用户,我们需要把它全局保存起来,因为后续该用户在即时通讯上的所有操作都需要直接或者间接使用这个实例。
登录即时通讯服务器
创建好了「Tom」这个用户对应的 IMClient
实例之后,我们接下来需要让该实例「登录」即时通讯服务器。
只有登录成功之后客户端才能开始与其他用户聊天,也才能接收到云端下发的各种事件通知。
这里需要说明一点,有些 SDK(比如 C# SDK)在创建 IMClient
实例的同时会自动进行登录,另一些 SDK(比如 iOS 和 Android SDK)则需要调用开发者手动执行 open
方法进行登录:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await tom.Open();
// Tom 创建了一个 client,用自己的名字作为 clientId 登录
LCIMClient tom = LCIMClient.getInstance("Tom");
// Tom 登录
tom.open(new LCIMClientCallback() {
@Override
public void done(LCIMClient client, LCIMException e) {
if (e == null) {
// 成功打开连接
}
}
});
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *tom;
// 初始化,然后登录
NSError *error;
tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error];
if (error) {
NSLog(@"init failed with error: %@", error);
} else {
[tom openWithCallback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// open succeeded
}
}];
}
// Tom 用自己的名字作为 clientId 登录,并且获取 IMClient 对象实例
realtime
.createIMClient("Tom")
.then(function (tom) {
// 成功登录
})
.catch(console.error);
// 定义一个常驻内存的全局变量
var tom: IMClient
// 初始化,然后登录
do {
tom = try IMClient(ID: "Tom")
tom.open { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
// Tom 创建了一个 client,用自己的名字作为 clientId 登录
Client tom = Client(id: 'Tom');
// Tom 登录
await tom.open();
使用 _User
登录
除了应用层指定 clientId
登录之外,我们也支持直接使用 _User
对象来创建 IMClient
并登录。这种方式能直接利用云端内置的用户鉴权系统而省掉登录签名操作,更方便地将存储和即时通讯这两个模块结合起来使用。示例代码如下:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var user = await LCUser.Login("USER_NAME", "PASSWORD");
var client = new LCIMClient(user);
// 以 LCUser 的用户名和密码登录到存储服务
LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer<LCUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(LCUser user) {
// 登录成功,与服务器连接
LCIMClient client = LCIMClient.getInstance(user);
client.open(new LCIMClientCallback() {
@Override
public void done(final LCIMClient avimClient, LCIMException e) {
// 执行其他逻辑
}
});
}
public void onError(Throwable throwable) {
// 登录失败(可能是密码错误)
}
public void onComplete() {}
});
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *client;
// 登录 User,然后使用登录成功的 User 初始化 Client 并登录
[LCUser logInWithUsernameInBackground:USER_NAME password:PASSWORD block:^(LCUser * _Nullable user, NSError * _Nullable error) {
if (user) {
NSError *err;
client = [[LCIMClient alloc] initWithUser:user error:&err];
if (err) {
NSLog(@"init failed with error: %@", err);
} else {
[client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// open succeeded
}
}];
}
}
}];
var AV = require("leancloud-storage");
// 以 AVUser 的用户名和密码登录即时通讯服务
AV.User.logIn("username", "password")
.then(function (user) {
return realtime.createIMClient(user);
})
.catch(console.error.bind(console));
// 定义一个常驻内存的全局变量
var client: IMClient
// 登录 User,然后使用登录成功的 User 初始化 Client 并登录
LCUser.logIn(username: USER_NAME, password: PASSWORD) { (result) in
switch result {
case .success(object: let user):
do {
client = try IMClient(user: user)
client.open { (result) in
// handle result
}
} catch {
print(error)
}
case .failure(error: let error):
print(error)
}
}
// 暂不支持
创建对话 Conversation
用户登录之后,要开始与其他人聊天,需要先创建一个「对话」。
对话(Conversation
)是消息的载体,所有消息都是发送给对话,即时通讯服务端会把消息下发给所有在对话中的成员。
Tom 完成了登录之后,就可以选择用户聊天了。现在他要给 Jerry 发送消息,所以需要先创建一个只有他们两个成员的 Conversation
:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true);
tom.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, false, true,
new LCIMConversationCreatedCallback() {
@Override
public void done(LCIMConversation conversation, LCIMException e) {
if(e == null) {
// 创建成功
}
}
});
// 创建与 Jerry 之间的对话
[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) {
// handle callback
}];
// 创建与 Jerry 之间的对话
tom
.createConversation({
// tom 是一个 IMClient 实例
// 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry
members: ["Jerry"],
// 对话名称
name: "Tom & Jerry",
unique: true,
})
.then(/* 略 */);
do {
try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", isUnique: true, completion: { (result) in
switch result {
case .success(value: let conversation):
print(conversation)
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
try {
// 创建与 Jerry 之间的对话
Conversation conversation = await tom.createConversation(
isUnique: true, members: {'Jerry'}, name: 'Tom & Jerry');
} catch (e) {
print('创建会话失败:$e');
}
createConversation
这个接口会直接创建一个对话,并且该对话会被存储在 _Conversation
表内,可以打开 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 数据存储 > 结构化数据 查看数据。不同 SDK 提供的创建对话接口如下:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
/// <summary>
/// Creates a conversation
/// </summary>
/// <param name="members">The list of clientIds of participants in this conversation (except the creator)</param>
/// <param name="name">The name of this conversation</param>
/// <param name="unique">Whether this conversation is unique;
/// if it is true and an existing conversation contains the same composition of members,
/// the existing conversation will be reused, otherwise a new conversation will be created.</param>
/// <param name="properties">Custom attributes of this conversation</param>
/// <returns></returns>
public async Task<LCIMConversation> CreateConversation(
IEnumerable<string> members,
string name = null,
bool unique = true,
Dictionary<string, object> properties = null) {
return await ConversationController.CreateConv(members: members,
name: name,
unique: unique,
properties: properties);
}
/**
* 创建或查询一个已有 conversation
*
* @param members 对话的成员
* @param name 对话的名字
* @param attributes 对话的额外属性
* @param isTransient 是否是聊天室
* @param isUnique 如果已经存在符合条件的会话,是否返回已有回话
* 为 false 时,则一直为创建新的回话
* 为 true 时,则先查询,如果已有符合条件的回话,则返回已有的,否则,创建新的并返回
* 为 true 时,仅 members 为有效查询条件
* @param callback 结果回调函数
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient, final boolean isUnique,
final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param members 对话参与者
* @param attributes 对话的额外属性
* @param isTransient 是否为聊天室
* @param callback 结果回调函数
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient,
final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param conversationMembers 对话参与者
* @param name 对话名称
* @param attributes 对话属性
* @param callback 结果回调函数
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers, String name,
final Map<String, Object> attributes, final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param conversationMembers 对话参与者
* @param attributes 对话属性
* @param callback 结果回调函数
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers,
final Map<String, Object> attributes, final LCIMConversationCreatedCallback callback);
/// The option of conversation creation.
@interface LCIMConversationCreationOption : NSObject
/// The name of the conversation.
@property (nonatomic, nullable) NSString *name;
/// The attributes of the conversation.
@property (nonatomic, nullable) NSDictionary *attributes;
/// Create or get an unique conversation, default is `true`.
@property (nonatomic) BOOL isUnique;
/// The time interval for the life of the temporary conversation.
@property (nonatomic) NSUInteger timeToLive;
@end
/// Create a Normal Conversation. Default is a Normal Unique Conversation.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned.
/// @param callback Result callback.
- (void)createConversationWithClientIds:(NSArray<NSString *> *)clientIds
callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback;
/// Create a Normal Conversation. Default is a Normal Unique Conversation.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createConversationWithClientIds:(NSArray<NSString *> *)clientIds
option:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback;
/// Create a Chat Room.
/// @param callback Result callback.
- (void)createChatRoomWithCallback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback;
/// Create a Chat Room.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createChatRoomWithOption:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback;
/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID.
/// @param callback Result callback.
- (void)createTemporaryConversationWithClientIds:(NSArray<NSString *> *)clientIds
callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback;
/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createTemporaryConversationWithClientIds:(NSArray<NSString *> *)clientIds
option:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback;
/**
* 创建一个对话
* @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性
* @param {String[]} options.members 对话的初始成员列表,必要参数,默认包含当前 client
* @param {String} [options.name] 对话的名字,可选参数,如果不传默认值为 null
* @param {Boolean} [options.transient=false] 是否为聊天室,可选参数
* @param {Boolean} [options.unique=false] 是否唯一对话,当其为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话
* @param {Boolean} [options.tempConv=false] 是否为临时对话,可选参数
* @param {Integer} [options.tempConvTTL=0] 可选参数,如果 tempConv 为 true,这里可以指定临时对话的生命周期。
* @return {Promise.<Conversation>}
*/
async createConversation({
members: m,
name,
transient,
unique,
tempConv,
tempConvTTL,
// 可添加更多属性
});
/// Create a Normal Conversation. Default is a Unique Conversation.
///
/// - Parameters:
/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned.
/// - name: The name of the conversation.
/// - attributes: The attributes of the conversation.
/// - isUnique: True means create or get a unique conversation, default is true.
/// - completion: callback.
public func createConversation(clientIDs: Set<String>, name: String? = nil, attributes: [String : Any]? = nil, isUnique: Bool = true, completion: @escaping (LCGenericResult<IMConversation>) -> Void) throws
/// Create a Chat Room.
///
/// - Parameters:
/// - name: The name of the chat room.
/// - attributes: The attributes of the chat room.
/// - completion: callback.
public func createChatRoom(name: String? = nil, attributes: [String : Any]? = nil, completion: @escaping (LCGenericResult<IMChatRoom>) -> Void) throws
/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle.
///
/// - Parameters:
/// - clientIDs: The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID.
/// - timeToLive: The time interval for the life of the temporary conversation.
/// - completion: callback.
public func createTemporaryConversation(clientIDs: Set<String>, timeToLive: Int32, completion: @escaping (LCGenericResult<IMTemporaryConversation>) -> Void) throws
/// To create a normal [Conversation].
///
/// [isUnique] is a special parameter, default is `true`, it affects the creation behavior and property [Conversation.isUnique].
/// * When it is `true` and the relevant unique [Conversation] not exists in the server, this method will create a new unique [Conversation].
/// * When it is `true` and the relevant unique [Conversation] exists in the server, this method will return that existing unique [Conversation].
/// * When it is `false`, this method always create a new non-unique [Conversation].
///
/// [members] is the [Conversation.members].
/// [name] is the [Conversation.name].
/// [attributes] is the [Conversation.attributes].
///
/// Returns an instance of [Conversation].
Future<Conversation> createConversation({
bool isUnique = true,
Set<String> members,
String name,
Map<String, dynamic> attributes,
}) async {}
/// To create a new [ChatRoom].
///
/// [name] is the [Conversation.name].
/// [attributes] is the [Conversation.attributes].
///
/// Returns an instance of [ChatRoom].
Future<ChatRoom> createChatRoom({
String name,
Map<String, dynamic> attributes,
}) async {}
/// To create a new [TemporaryConversation].
///
/// [members] is the [Conversation.members].
/// [timeToLive] is the [TemporaryConversation.timeToLive].
///
/// Returns an instance of [TemporaryConversation].
Future<TemporaryConversation> createTemporaryConversation({
Set<String> members,
int timeToLive,
}) async {}
虽然不同语言/平台接口声明有所不同,但是支持的参数是基本一致的。在创建一个对话的时候,我们主要可以指定:
members
:必要参数,包含对话的初始成员列表,请注意当前用户作为对话的创建者,是默认包含在成员里面的,所以members
数组中可以不包含当前用户的clientId
。name
:对话名字,可选参数,上面代码指定为了「Tom & Jerry」。attributes
:对话的自定义属性,可选。上面示例代码没有指定额外属性,开发者如果指定了额外属性的话,以后其他成员可以通过LCIMConversation
的接口获取到这些属性值。附加属性在_Conversation
表中被保存在attr
列中。unique
/isUnique
或者是LCIMConversationOptionUnique
:唯一对话标志位,可选。- 如果设置为唯一对话,云端会根据完整的成员列表先进行一次查询,如果已经有正好包含这些成员的对话存在,那么就返回已经存在的对话,否则才创建一个新的对话。
- 如果指定
unique
标志为假,那么每次调用createConversation
接口都会创建一个新的对话。 - 未指定
unique
时,SDK 默认值为真。 - 从通用的聊天场景来看,不管是 Tom 发出「创建和 Jerry 单聊对话」的请求,还是 Jerry 发出「创建和 Tom 单聊对话」的请求,或者 Tom 以后再次发出创建和 Jerry 单聊对话的请求,都应该是同一个对话才是合理的,否则可能因为聊天记录的不同导致用户混乱。
对话类型的其他标志,可选参数,例如
transient
/isTransient
表示「聊天室」,tempConv
/tempConvTTL
和LCIMConversationOptionTemporary
用来创建「临时对话」等等。什么都不指定就表示创建普通对话,对于这些标志位的含义我们先不管,以后会有说明。
创建对话之后,可以获取对话的内置属性,云端会为每一个对话生成一个全局唯一的 ID 属性:Conversation.id
,它是其他用户查询对话时常用的匹配字段。
发送消息
对话已经创建成功了,接下来 Tom 可以在这个对话中发出第一条文本消息了:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var textMessage = new LCIMTextMessage("Jerry,起床了!");
await conversation.Send(textMessage);
LCIMTextMessage msg = new LCIMTextMessage();
msg.setText("Jerry,起床了!");
// 发送消息
conversation.sendMessage(msg, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
Log.d("Tom & Jerry", "发送成功!");
}
}
});
LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
var { TextMessage } = require("leancloud-realtime");
conversation
.send(new TextMessage("Jerry,起床了!"))
.then(function (message) {
console.log("Tom & Jerry", "发送成功!");
})
.catch(console.error);
do {
let textMessage = IMTextMessage(text: "Jerry,起床了!")
try conversation.send(message: textMessage) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
try {
TextMessage textMessage = TextMessage();
textMessage.text = 'Jerry,起床了!';
await conversation.send(message: textMessage);
} catch (e) {
print(e);
}
上面接口实现的功能就是向对话中发送一条消息,同一对话中其他在线成员会立刻收到此消息。
现在 Tom 发出了消息,那么接收者 Jerry 他要在界面上展示出来这一条新消息,该怎么来处理呢?
接收消息
在另一个设备上,我们用 Jerry
作为 clientId
来创建一个 IMClient
并登录即时通讯服务(与前两节 Tom 的处理流程一样):
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var jerry = new LCIMClient("Jerry");
// Jerry 登录
LCIMClient jerry = LCIMClient.getInstance("Jerry");
jerry.open(new LCIMClientCallback(){
@Override
public void done(LCIMClient client,LCIMException e){
if(e==null){
// 登录成功后的逻辑
}
}
});
NSError *error;
jerry = [[LCIMClient alloc] initWithClientId:@"Jerry" error:&error];
if (!error) {
[jerry openWithCallback:^(BOOL succeeded, NSError *error) {
// handle callback
}];
}
var { Event } = require("leancloud-realtime");
// Jerry 登录
realtime
.createIMClient("Jerry")
.then(function (jerry) {})
.catch(console.error);
do {
let jerry = try IMClient(ID: "Jerry")
jerry.open { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
Client jerry = Client(id: 'Jerry');
await jerry.open();
Jerry 作为消息的被动接收方,他不需要主动创建与 Tom 的对话,可能也无法知道 Tom 创建好的对话信息,Jerry 端需要通过设置即时通讯客户端事件的回调函数,才能获取到 Tom 那边操作的通知。
即时通讯客户端事件回调能处理多种服务端通知,这里我们先关注这里会出现的两个事件:
- 用户被邀请进入某个对话的通知事件。Tom 在创建和 Jerry 的单聊对话的时候,Jerry 这边就能立刻收到一条通知,获知到类似于「Tom 邀请你加入了一个对话」的信息。
- 已加入对话中新消息到达的通知。在 Tom 发出「Jerry,起床了!」这条消息之后,Jerry 这边也能立刻收到一条新消息到达的通知,通知中带有消息具体数据以及对话、发送者等上下文信息。
现在,我们看看具体应该如何响应服务端发过来的通知。Jerry 端会分别处理「加入对话」的事件通知和「新消息到达」的事件通知:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
jerry.OnInvited = (conv, initBy) => {
WriteLine($"{initBy} 邀请 Jerry 加入 {conv.Id} 对话");
};
jerry.OnMessage = (conv, msg) => {
if (msg is LCIMTextMessage textMessage) {
// textMessage.ConversationId 是该条消息所属于的对话 ID
// textMessage.Text 是该文本消息的文本内容
// textMessage.FromClientId 是消息发送者的 clientId
}
};
// Java/Android SDK 通过定制自己的对话事件 Handler 处理服务端下发的对话事件通知
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法来处理当前用户被邀请到某个聊天对话事件
*
* @param client
* @param conversation 被邀请的聊天对话
* @param operator 邀请你的人
* @since 3.0
*/
@Override
public void onInvited(LCIMClient client, LCIMConversation conversation, String invitedBy) {
// 当前 clientId(Jerry)被邀请到对话,执行此处逻辑
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
// Java/Android SDK 通过定制自己的消息事件 Handler 处理服务端下发的消息通知
public static class CustomMessageHandler extends LCIMMessageHandler{
/**
* 重载此方法来处理接收消息
*
* @param message
* @param conversation
* @param client
*/
@Override
public void onMessage(LCIMMessage message,LCIMConversation conversation,LCIMClient client){
if(message instanceof LCIMTextMessage){
Log.d(((LCIMTextMessage)message).getText()); // Jerry,起床了
}
}
}
// 设置全局的消息处理 handler
LCIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler());
// Objective-C SDK 通过实现 LCIMClientDelegate 代理来处理服务端通知
// 不了解 Objective-C 代理(delegate)概念的读者可以参考:
// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html
jerry.delegate = delegator;
/*!
当前用户被邀请加入对话的通知。
@param conversation - 所属对话
@param clientId - 邀请者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation invitedByClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"当前 clientId(Jerry)被 %@ 邀请,加入了对话",clientId]);
}
/*!
接收到新消息(使用内置消息格式)。
@param conversation - 所属对话
@param message - 具体的消息
*/
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
NSLog(@"%@", message.text); // Jerry,起床了!
}
// JS SDK 通过在 IMClient 实例上监听事件回调来响应服务端通知
// 当前用户被添加至某个对话
jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) {
console.log(payload.invitedBy, conversation.id);
});
// 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。
jerry.on(Event.MESSAGE, function (message, conversation) {
console.log("收到新消息:" + message.text);
});
let delegator: Delegator = Delegator()
jerry.delegate = delegator
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case .message(event: let messageEvent):
switch messageEvent {
case .received(message: let message):
print(message)
default:
break
}
default:
break
}
}
jerry.onMessage = ({
Client client,
Conversation conversation,
Message message,
}) {
if (message.stringContent != null) {
print('收到的消息是:${message.stringContent}');
}
};
Jerry 端实现了上面两个事件通知函数之后,就顺利收到 Tom 发送的消息了。之后 Jerry 也可以回复消息给 Tom,而 Tom 端实现类似的接收流程,那么他们俩就可以开始愉快的聊天了。
我们现在可以回顾一下 Tom 和 Jerry 发送第一条消息的过程中,两方完整的处理时序:
在聊天过程中,接收方除了响应新消息到达通知之外,还需要响应多种对话成员变动通知,例如「新用户 XX 被 XX 邀请加入了对话」、「用户 XX 主动退出了对话」、「用户 XX 被管理员剔除出对话」,等等。 云端会实时下发这些事件通知给客户端,具体细节可以参考后续章节:成员变更的事件通知总结。
多人群聊
上面我们讨论了一对一单聊的实现流程,假设我们还需要实现一个「朋友群」的多人聊天,接下来我们就看看怎么完成这一功能。
从即时通讯云端来看,多人群聊与单聊的流程十分接近,主要差别在于对话内成员数量的多少。群聊对话支持在创建对话的时候一次性指定全部成员,也允许在创建之后通过邀请的方式来增加新的成员。
创建多人群聊对话
在 Tom 和 Jerry 的对话中(假设对话 ID 为 CONVERSATION_ID
,这只是一个示例,并不代表实际数据),后来 Tom 又希望把 Mary 也拉进来,他可以使用如下的办法:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
// 首先根据 ID 获取 Conversation 实例
var conversation = await tom.GetConversation("CONVERSATION_ID");
// 邀请 Mary 加入对话
await conversation.AddMembers(new string[] { "Mary" });
// 首先根据 ID 获取 Conversation 实例
final LCIMConversation conv = client.getConversation("CONVERSATION_ID");
// 邀请 Mary 加入对话
conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() {
@Override
public void done(LCIMException e, List<String> successfulClientIds, List<LCIMOperationFailure> failures) {
// 添加成功
}
});
// 首先根据 ID 获取 Conversation 实例
LCIMConversationQuery *query = [self.client conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) {
// 邀请 Mary 加入对话
[conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"邀请成功!");
}
}];
}];
// 首先根据 ID 获取 Conversation 实例
tom
.getConversation("CONVERSATION_ID")
.then(function (conversation) {
// 邀请 Mary 加入对话
return conversation.add(["Mary"]);
})
.then(function (conversation) {
console.log("添加成功", conversation.members);
// 此时对话成员为:['Mary', 'Tom', 'Jerry']
})
.catch(console.error.bind(console));
do {
let conversationQuery = client.conversationQuery
try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in
switch result {
case .success(value: let conversation):
do {
try conversation.add(members: ["Mary"], completion: { (result) in
switch result {
case .allSucceeded:
break
case .failure(error: let error):
print(error)
case let .slicing(success: succeededIDs, failure: failures):
if let succeededIDs = succeededIDs {
print(succeededIDs)
}
for (failedIDs, error) in failures {
print(failedIDs)
print(error)
}
}
})
} catch {
print(error)
}
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
List<Conversation> conversations;
try {
// 首先根据 ID 获取 Conversation 实例
ConversationQuery query = tom.conversationQuery();
query.whereEqualTo('objectId', 'CONVERSATION_ID');
conversations = await query.find();
} catch (e) {
print(e);
}
try {
Conversation conversation = conversations.first;
// 邀请 Mary 加入对话
MemberResult addResult = await conversation.addMembers(
members: {'Mary'},
);
} catch (e) {
print(e);
}
而 Jerry 端增加「新成员加入」的事件通知处理函数,就可以及时获知 Mary 被 Tom 邀请加入当前对话了:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话");
}
其中 AVIMOnInvitedEventArgs
参数包含如下内容:
InvitedBy
:该操作的发起者JoinedMembers
:此次加入对话的包含的成员列表ConversationId
:被操作的对话
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法以处理聊天对话中的参与者加入事件
*
* @param client
* @param conversation
* @param members 加入的参与者
* @param invitedBy 加入事件的邀请人,有可能是加入的参与者本身
* @since 3.0
*/
@Override
public void onMemberJoined(LCIMClient client, LCIMConversation conversation,
List<String> members, String invitedBy) {
// 手机屏幕上会显示一小段文字:Mary 加入到 551260efe4b01608686c3e0f;操作者为:Tom
Toast.makeText(LeanCloud.applicationContext,
members + " 加入到 " + conversation.getConversationId() + ";操作者为:"
+ invitedBy, Toast.LENGTH_SHORT).show();
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
jerry.delegate = delegator;
#pragma mark - LCIMClientDelegate
/*!
对话中有新成员加入时所有成员都会收到这一通知。
@param conversation - 所属对话
@param clientIds - 加入的新成员列表
@param clientId - 邀请者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
// 有用户被添加至某个对话
jerry.on(
Event.MEMBERS_JOINED,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.invitedBy, conversation.id);
}
);
其中 payload
参数包含如下内容:
members
:字符串数组,被添加的用户clientId
列表invitedBy
:字符串,邀请者clientId
jerry.delegate = delegator
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case let .joined(byClientID: byClientID, at: atDate):
print(byClientID)
print(atDate)
case let .membersJoined(members: members, byClientID: byClientID, at: atDate):
print(members)
print(byClientID)
print(atDate)
default:
break
}
}
// 加入成员通知
jerry.onMembersJoined = ({
Client client,
Conversation conversation,
List members,
String byClientID,
DateTime atDate,
}) {
print('成员 ${members.toString()} 加入会话');
};
这一流程的时序图如下:
而 Mary 端如果要能加入到 Tom 和 Jerry 的对话中来,Ta 可以参照 一对一单聊 中 Jerry 侧的做法监听 INVITED
事件,就可以自己被邀请到了一个对话当中。
而 重新创建一个对话,并在创建的时候指定全部成员 的方式如下:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var conversation = await tom.CreateConversation(new string[] { "Jerry","Mary" }, name: "Tom & Jerry & friends", unique: true);
tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null,
new LCIMConversationCreatedCallback() {
@Override
public void done(LCIMConversation conversation, LCIMException e) {
if (e == null) {
// 创建成功
}
}
});
// Tom 建立了与朋友们的会话
[tom createConversationWithClientIds:@[@"Jerry", @"Mary"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) {
if (!error) {
NSLog(@"创建成功!");
}
}];
tom
.createConversation({
// 创建的时候直接指定 Jerry 和 Mary 一起加入多人群聊,当然根据需求可以添加更多成员
members: ["Jerry", "Mary"],
// 对话名称
name: "Tom & Jerry & friends",
unique: true,
})
.catch(console.error);
do {
try tom.createConversation(clientIDs: ["Jerry", "Mary"], name: "Tom & Jerry & friends", isUnique: true, completion: { (result) in
switch result {
case .success(value: let conversation):
print(conversation)
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
try {
Conversation conversation = await jerry.createConversation(
isUnique: true,
members: {'Jerry', 'Mary'},
name: 'Tom & Jerry & friends');
} catch (e) {
print(e);
}
群发消息
多人群聊中一个成员发送的消息,会实时同步到所有其他在线成员,其处理流程与单聊中 Jerry 接收消息的过程是一样的。
例如,Tom 向好友群发送了一条欢迎消息:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var textMessage = new LCIMTextMessage("大家好,欢迎来到我们的群聊对话!");
await conversation.Send(textMessage);
LCIMTextMessage msg = new LCIMTextMessage();
msg.setText("大家好,欢迎来到我们的群聊对话!");
// 发送消息
conversation.sendMessage(msg, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
Log.d("群聊", "发送成功!");
}
}
});
[conversation sendMessage:[LCIMTextMessage messageWithText:@"大家好,欢迎来到我们的群聊对话!" attributes:nil] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
conversation.send(new TextMessage("大家好,欢迎来到我们的群聊对话"));
do {
let textMessage = IMTextMessage(text: "大家好,欢迎来到我们的群聊对话!")
try conversation.send(message: textMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
try {
TextMessage textMessage = TextMessage();
textMessage.text = '大家好,欢迎来到我们的群聊对话!';
await conversation.send(message: textMessage);
} catch (e) {
print(e);
}
而 Jerry 和 Mary 端都会有 Event.MESSAGE
事件触发,利用它来接收群聊消息,并更新产品 UI。
将他人踢出对话
三个好友的群其乐融融不久,后来 Mary 出言不逊,惹恼了群主 Tom,Tom 直接把 Mary 踢出了对话群。Tom 端想要踢人,该怎么实现呢?
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await conversation.RemoveMembers(new string[] { "Mary" });
conv.kickMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() {
@Override
public void done(LCIMException e, List<String> successfulClientIds, List<LCIMOperationFailure> failures) {
}
});
[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"踢人成功!");
}
}];
conversation
.remove(["Mary"])
.then(function (conversation) {
console.log("移除成功", conversation.members);
})
.catch(console.error.bind(console));
do {
try conversation.remove(members: ["Mary"], completion: { (result) in
switch result {
case .allSucceeded:
break
case .failure(error: let error):
print(error)
case let .slicing(success: succeededIDs, failure: failures):
if let succeededIDs = succeededIDs {
print(succeededIDs)
}
for (failedIDs, error) in failures {
print(failedIDs)
print(error)
}
}
})
} catch {
print(error)
}
try {
MemberResult removeMemberResult = await conversation.removeMembers(members: {'Mary'});
} catch (e) {
print(e);
}
Tom 端执行了这段代码之后会触发如下流程:
这里出现了两个新的事件:当前用户被踢出对话 KICKED
(Mary 收到的),成员 XX 被踢出对话 MEMBERS_LEFT
(Jerry 和 Tom 收到的)。其处理方式与邀请人的流程类似:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
jerry.OnMembersLeft = (conv, leftIds, kickedBy) => {
WriteLine($"{leftIds} 离开对话 {conv.Id};操作者为:{kickedBy}");
}
jerry.OnKicked = (conv, initBy) => {
WriteLine($"你已经离开对话 {conv.Id};操作者为:{initBy}");
};
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法以处理聊天对话中的参与者离开事件
*
* @param client
* @param conversation
* @param members 离开的参与者
* @param kickedBy 离开事件的发动者,有可能是离开的参与者本身
* @since 3.0
*/
@Override
public abstract void onMemberLeft(LCIMClient client,
LCIMConversation conversation, List<String> members, String kickedBy) {
Toast.makeText(LeanCloud.applicationContext,
members + " 离开对话 " + conversation.getConversationId() + ";操作者为:"
+ kickedBy, Toast.LENGTH_SHORT).show();
}
/**
* 实现本方法来处理当前用户被踢出某个聊天对话事件
*
* @param client
* @param conversation
* @param kickedBy 踢出你的人
* @since 3.0
*/
@Override
public abstract void onKicked(LCIMClient client, LCIMConversation conversation,
String kickedBy) {
Toast.makeText(LeanCloud.applicationContext,
"你已离开对话 " + conversation.getConversationId() + ";操作者为:"
+ kickedBy, Toast.LENGTH_SHORT).show();
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
jerry.delegate = delegator;
#pragma mark - LCIMClientDelegate
/*!
对话中有成员离开时所有剩余成员都会收到这一通知。
@param conversation - 所属对话
@param clientIds - 离开的成员列表
@param clientId - 操作者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray<NSString *> * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId {
;
}
/*!
当前用户被踢出对话的通知。
@param conversation - 所属对话
@param clientId - 操作者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId {
;
}
// 有成员被从某个对话中移除
jerry.on(
Event.MEMBERS_LEFT,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
}
);
// 有用户被踢出某个对话
jerry.on(
Event.KICKED,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.kickedBy, conversation.id);
}
);
jerry.delegate = delegator
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case let .left(byClientID: byClientID, at: atDate):
print(byClientID)
print(atDate)
case let .membersLeft(members: members, byClientID: byClientID, at: atDate):
print(members)
print(byClientID)
print(atDate)
default:
break
}
}
// 有成员被从某个对话中移除
jerry.onMembersLeft = ({
Client client,
Conversation conversation,
List members,
String byClientID,
DateTime atDate,
}) {
print('成员 ${members.toString()} 离开会话,操作者为:$byClientID');
};
// 有用户被踢出某个对话
jerry.onKicked = ({
Client client,
Conversation conversation,
String byClientID,
DateTime atDate,
}) {
print('你已离开对话,操作者为:$byClientID');
};
用户主动加入对话
把 Mary 踢走之后,Tom 嫌人少不好玩,所以他找到了 William,说他和 Jerry 有一个很好玩的聊天群,并且把群的 ID(或名称)告知给了 William。William 也很想进入这个群看看他们究竟在聊什么,他自己主动加入了对话:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var conv = await william.GetConversation("CONVERSATION_ID");
await conv.Join();
LCIMConversation conv = william.getConversation("CONVERSATION_ID");
conv.join(new LCIMConversationCallback(){
@Override
public void done(LCIMException e){
if(e==null){
// 加入成功
}
}
});
LCIMConversationQuery *query = [william conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) {
[conversation joinWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"加入成功!");
}
}];
}];
william
.getConversation("CONVERSATION_ID")
.then(function (conversation) {
return conversation.join();
})
.then(function (conversation) {
console.log("加入成功", conversation.members);
// 此时对话成员为:['William', 'Tom', 'Jerry']
})
.catch(console.error.bind(console));
do {
let conversationQuery = client.conversationQuery
try conversationQuery.getConversation(by: "CONVERSATION_ID") { (result) in
switch result {
case .success(value: let conversation):
do {
try conversation.join(completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
List<Conversation> conversations;
try {
ConversationQuery query = william.conversationQuery();
query.whereEqualTo('objectId', 'CONVERSATION_ID');
conversations = await query.find();
} catch (e) {
print(e);
}
try {
Conversation conversation = conversations.first;
MemberResult joinResult = await conversation.join();
} catch (e) {
print(e);
}
执行了这段代码之后会触发如下流程:
其他人则通过订阅 MEMBERS_JOINED
来接收 William 加入对话的通知 :
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{memberList} 加入了 {conv.Id} 对话;操作者为:{initBy}");
}
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
@Override
public void onMemberJoined(LCIMClient client, LCIMConversation conversation,
List<String> members, String invitedBy) {
// 手机屏幕上会显示一小段文字:William 加入到 551260efe4b01608686c3e0f;操作者为:William
Toast.makeText(LeanCloud.applicationContext,
members + " 加入到 " + conversation.getConversationId() + ";操作者为:"
+ invitedBy, Toast.LENGTH_SHORT).show();
}
}
- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
jerry.on(
Event.MEMBERS_JOINED,
function membersJoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.invitedBy, conversation.id);
}
);
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case let .membersJoined(members: members, byClientID: byClientID, at: atDate):
print(members)
print(byClientID)
print(atDate)
default:
break
}
}
jerry.onMembersJoined = ({
Client client,
Conversation conversation,
List members,
String byClientID,
DateTime atDate,
}) {
print('成员 ${members.toString()} 加入会话');
};
用户主动退出对话
随着 Tom 邀请进来的人越来越多,Jerry 觉得跟这些人都说不到一块去,他不想继续呆在这个对话里面了,所以选择自己主动退出对话,这时候可以调用下面的方法完成退群的操作:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await conversation.Quit();
conversation.quit(new LCIMConversationCallback(){
@Override
public void done(LCIMException e){
if(e==null){
// 退出成功
}
}
});
[conversation quitWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"退出成功!");
}
}];
conversation
.quit()
.then(function (conversation) {
console.log("退出成功", conversation.members);
})
.catch(console.error.bind(console));
do {
try conversation.leave(completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
try {
MemberResult quitResult = await conversation.quit();
} catch (e) {
print(e);
}
执行了这段代码 Jerry 就离开了这个聊天群,此后群里所有的事件 Jerry 都不会再知晓。各个成员接收到的事件通知流程如下:
而其他人需要通过订阅 MEMBERS_LEFT
来接收 Jerry 离开对话的事件通知:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
mary.OnMembersLeft = (conv, members, initBy) => {
WriteLine($"{members} 离开了 {conv.Id} 对话;操作者为:{initBy}");
}
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
@Override
public void onMemberLeft(LCIMClient client, LCIMConversation conversation, List<String> members,
String kickedBy) {
// 有其他成员离开时,执行此处逻辑
}
}
// Mary 登录之后,Jerry 退出了对话,在 Mary 所在的客户端就会激发以下回调
- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 离开了对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
mary.on(
Event.MEMBERS_LEFT,
function membersLeftEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
}
);
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case let .membersLeft(members: members, byClientID: byClientID, at: atDate):
print(members)
print(byClientID)
print(atDate)
default:
break
}
}
mary.onMembersLeft = ({
Client client,
Conversation conversation,
List members,
String byClientID,
DateTime atDate,
}) {
print('成员 ${members.toString()} 离开会话');
};
成员变更的事件通知总结
前面的时序图和代码针对成员变更的操作做了逐步的分析和阐述,为了确保开发者能够准确的使用事件通知,如下表格做了一个统一的归类和划分:
假设 Tom 和 Jerry 已经在对话内了:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | OnMembersJoined | OnMembersJoined | OnInvited | / |
Tom 剔除 Mary | OnMembersLeft | OnMembersLeft | OnKicked | / |
William 加入 | OnMembersJoined | OnMembersJoined | / | OnMembersJoined |
Jerry 主动退出 | OnMembersLeft | OnMembersLeft | / | OnMembersLeft |
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | onMemberJoined | onMemberJoined | onInvited | / |
Tom 剔除 Mary | onMemberLeft | onMemberLeft | onKicked | / |
William 加入 | onMemberJoined | onMemberJoined | / | onMemberJoined |
Jerry 主动退出 | onMemberLeft | onMemberLeft | / | onMemberLeft |
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | membersAdded | membersAdded | invitedByClientId | / |
Tom 剔除 Mary | membersRemoved | membersRemoved | kickedByClientId | / |
William 加入 | membersAdded | membersAdded | / | membersAdded |
Jerry 主动退出 | membersRemoved | kickedByClientId | / | membersRemoved |
文本之外的聊天消息
上面的示例都是发送文本消息,但是实际上可能图片、视频、位置等消息也是非常常见的消息格式,接下来我们就看看如何发送这些富媒体类型的消息。
即时通讯服务默认支持文本、文件、图像、音频、视频、位置、二进制等不同格式的消息,除了二进制消息之外,普通消息的收发接口都是字符串,但是文本消息和文件、图像、音视频消息有一点区别:
- 文本消息发送的就是本身的内容
- 而其他的多媒体消息,例如一张图片,实际上即时通讯 SDK 会首先调用存储服务的
AVFile
接口,将图像的二进制文件上传到存储服务云端,再把图像下载的 URL 放入即时通讯消息结构体中,所以 图像消息不过是包含了图像下载链接的固定格式文本消息。
图像等二进制数据不随即时通讯消息直接下发的主要原因在于,文件存储服务默认都是开通了 CDN 加速选项的,通过文件下载对于终端用户来说可以有更快的展现速度,同时对于开发者来说也能获得更低的存储成本。
默认消息类型
即时通讯服务内置了多种结构化消息用来满足常见的需求:
TextMessage
文本消息ImageMessage
图像消息AudioMessage
音频消息VideoMessage
视频消息FileMessage
普通文件消息(.txt/.doc/.md 等各种)LocationMessage
地理位置消息
所有消息均派生自 LCIMMessage
,每种消息实例都具备如下属性:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
属性 | 类型 | 描述 |
---|---|---|
content | String | 消息内容。 |
clientId | String | 消息发送者的 clientId 。 |
conversationId | String | 消息所属对话 ID。 |
messageId | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
timestamp | long | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
receiptTimestamp | long | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 |
status | AVIMMessageStatus 枚举 | 消息状态,有五种取值:AVIMMessageStatusNone (未知)AVIMMessageStatusSending (发送中)AVIMMessageStatusSent (发送成功)AVIMMessageStatusReceipt (被接收)AVIMMessageStatusFailed (失败) |
ioType | AVIMMessageIOType 枚举 | 消息传输方向,有两种取值:AVIMMessageIOTypeIn (发给当前用户)AVIMMessageIOTypeOut (由当前用户发出) |
属性 | 类型 | 描述 |
---|---|---|
content | String | 消息内容。 |
clientId | String | 消息发送者的 clientId 。 |
conversationId | String | 消息所属对话 ID。 |
messageId | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
timestamp | long | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
receiptTimestamp | long | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 |
status | MessageStatus 枚举 | 消息状态,有五种取值:StatusNone (未知)StatusSending (发送中)StatusSent (发送成功)StatusReceipt (被接收)StatusFailed (失败) |
ioType | MessageIOType 枚举 | 消息传输方向,有两种取值:TypeIn (发给当前用户)TypeOut (由当前用户发出) |
属性 | 类型 | 描述 |
---|---|---|
content | NSString | 消息内容。 |
clientId | NSString | 消息发送者的 clientId 。 |
conversationId | NSString | 消息所属对话 ID。 |
messageId | NSString | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
sendTimestamp | int64_t | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
deliveredTimestamp | int64_t | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 |
status | AVIMMessageStatus 枚举 | 消息状态,有五种取值:LCIMMessageStatusNone (未知)LCIMMessageStatusSending (发送中)LCIMMessageStatusSent (发送成功)LCIMMessageStatusDelivered (被接收)LCIMMessageStatusFailed (失败) |
ioType | LCIMMessageIOType 枚举 | 消息传输方向,有两种取值:LCIMMessageIOTypeIn (发给当前用户)LCIMMessageIOTypeOut (由当前用户发出) |
Swift
属性 | 类型 | 描述 |
---|---|---|
content | IMMessage.Content | 消息内容,支持 String 和 Data 两种格式。 |
fromClientID | String | 消息发送者的 clientId 。 |
currentClientID | String | 消息接收者的 clientId 。 |
conversationID | String | 消息所属对话 ID。 |
ID | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
sentTimestamp | int64_t | 消息发送的时间。消息发送成功之后,云端赋予的全局的时间戳。 |
deliveredTimestamp | int64_t | 消息被对方接收到的时间戳。 |
readTimestamp | int64_t | 消息被对方阅读的时间戳。 |
patchedTimestamp | int64_t | 消息被修改的时间戳。 |
isAllMembersMentioned | Bool | @ 所有会话成员。 |
mentionedMembers | [String] | @ 会话成员。 |
isCurrentClientMentioned | Bool | 当前 Client 是否被 @。 |
status | IMMessage.Status | 消息状态,有 6 种取值:none (无状态)sending (发送中)sent (发送成功)delivered (已被接收)read (已被读)failed (发送失败) |
ioType | IMMessage.IOType | 消息传输方向,有两种取值:in (当前用户接收到的)out (由当前用户发出的) |
JavaScript
属性 | 类型 | 描述 |
---|---|---|
from | String | 消息发送者的 clientId 。 |
cid | String | 消息所属对话 ID。 |
id | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
timestamp | Date | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
deliveredAt | Date | 消息送达时间。 |
status | Symbol | 消息状态,其值为枚举 MessageStatus 的成员之一:MessageStatus.NONE (未知)MessageStatus.SENDING (发送中)MessageStatus.SENT (发送成功)MessageStatus.DELIVERED (已送达)MessageStatus.FAILED (失败) |
我们为每一种富媒体消息定义了一个消息类型,即时通讯 SDK 自身使用的类型是负数(如下面列表所示),所有正数留给开发者自定义扩展类型使用,0
作为「没有类型」被保留起来。
消息 | 类型 |
---|---|
文本消息 | -1 |
图像消息 | -2 |
音频消息 | -3 |
视频消息 | -4 |
位置消息 | -5 |
文件消息 | -6 |
图像消息
发送图像文件
即时通讯 SDK 支持直接通过二进制数据,或者本地图像文件的路径,来构造一个图像消息并发送到云端。其流程如下:
图解:
- Local 可能是来自于
localStorage
/camera
,表示图像的来源可以是本地存储例如 iPhone 手机的媒体库或者直接调用相机 API 实时地拍照获取的照片。 LCFile
是云服务提供的文件存储对象。
对应的代码并没有时序图那样复杂,因为调用 send
接口的时候,SDK 会自动上传图像,不需要开发者再去关心这一步:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var image = new LCFile("screenshot.png", new Uri("http://example.com/screenshot.png"));
var imageMessage = new LCIMImageMessage(image);
imageMessage.Text = "发自我的 Windows";
await conversation.Send(imageMessage);
LCFile file = LCFile.withAbsoluteLocalPath("San_Francisco.png", Environment.getExternalStorageDirectory() + "/San_Francisco.png");
// 创建一条图像消息
LCIMImageMessage m = new LCIMImageMessage(file);
m.setText("发自我的小米手机");
conv.sendMessage(m, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
// 发送成功
}
}
});
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"Tarara.png"];
NSError *error;
LCFile *file = [LCFile fileWithLocalPath:imagePath error:&error];
LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
// 图像消息等富媒体消息依赖存储 SDK 和富媒体消息插件,
// 具体的引用和初始化步骤请参考 SDK 配置指南
var fileUploadControl = $("#photoFileUpload")[0];
var file = new AV.File("avatar.jpg", fileUploadControl.files[0]);
file
.save()
.then(function () {
var message = new ImageMessage(file);
message.setText("发自我的 Ins");
message.setAttributes({ location: "旧金山" });
return conversation.send(message);
})
.then(function () {
console.log("发送成功");
})
.catch(console.error.bind(console));
do {
if let imageFilePath = Bundle.main.url(forResource: "image", withExtension: "jpg")?.path {
let imageMessage = IMImageMessage(filePath: imageFilePath, format: "jpg")
try conversation.send(message: imageMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
}
} catch {
print(error)
}
import 'package:flutter/services.dart' show rootBundle;
// 假设项目根目录有 assets 文件夹存放图片,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。
ByteData imageData = await rootBundle.load('assets/test.png');
// image message
ImageMessage imageMessage = ImageMessage.from(
binaryData: imageData.buffer.asUint8List(),
format: 'png',
name: 'image.png',
);
try {
conversation.send(message: imageMessage);
} catch (e) {
print(e);
}
发送图像链接
除了上述这种从本地直接发送图片文件的消息之外,在很多时候,用户可能从网络上或者别的应用中拷贝了一个图像的网络连接地址,当做一条图像消息发送到对话中,这种需求可以用如下代码来实现:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var image = new LCFile("girl.gif", new Uri("http://example.com/girl.gif"));
var imageMessage = new LCIMImageMessage(image);
imageMessage.Text = "发自我的 Windows";
await conversation.Send(imageMessage);
LCFile file = new LCFile("萌妹子","http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif", null);
LCIMImageMessage m = new LCIMImageMessage(file);
m.setText("萌妹子一枚");
// 创建一条图像消息
conv.sendMessage(m, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
// 发送成功
}
}
});
// Tom 发了一张图片给 Jerry
LCFile *file = [LCFile fileWithURL:[self @"http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif"]];
LCIMImageMessage *message = [LCIMImageMessage messageWithText:@"萌妹子一枚" file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
var AV = require("leancloud-storage");
var { ImageMessage } = initPlugin(AV, IM);
// 从网络链接直接构建一个图像消息
var file = new AV.File.withURL(
"萌妹子",
"http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif"
);
file
.save()
.then(function () {
var message = new ImageMessage(file);
message.setText("萌妹子一枚");
return conversation.send(message);
})
.then(function () {
console.log("发送成功");
})
.catch(console.error.bind(console));
do {
if let url = URL(string: "http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif") {
let imageMessage = IMImageMessage(url: url, format: "gif")
try conversation.send(message: imageMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
}
} catch {
print(error)
}
ImageMessage imageMessage = ImageMessage.from(
url: 'http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif',
format: 'png',
name: 'image.png',
);
try {
conversation.send(message: imageMessage);
} catch (e) {
print(e);
}
接收图像消息
图像消息的接收机制和之前是一样的,只需要修改一下接收消息的事件回调逻辑,根据消息类型来做不同的 UI 展现即可,例如:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
client.OnMessage = (conv, msg) => {
if (e.Message is LCIMImageMessage imageMessage) {
WriteLine(imageMessage.Url);
}
}
LCIMMessageManager.registerMessageHandler(LCIMImageMessage.class,
new LCIMTypedMessageHandler<LCIMImageMessage>() {
@Override
public void onMessage(LCIMImageMessage msg, LCIMConversation conv, LCIMClient client) {
// 只处理 Jerry 这个客户端的消息
// 并且来自 conversationId 为 55117292e4b065f7ee9edd29 的 conversation 的消息
if ("Jerry".equals(client.getClientId()) && "55117292e4b065f7ee9edd29".equals(conv.getConversationId())) {
String fromClientId = msg.getFrom();
String messageId = msg.getMessageId();
String url = msg.getFileUrl();
Map<String, Object> metaData = msg.getFileMetaData();
if (metaData.containsKey("size")) {
int size = (Integer) metaData.get("size");
}
if (metaData.containsKey("width")) {
int width = (Integer) metaData.get("width");
}
if (metaData.containsKey("height")) {
int height = (Integer) metaData.get("height");
}
if (metaData.containsKey("format")) {
String format = (String) metaData.get("format");
}
}
}
});
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
LCIMImageMessage *imageMessage = (LCIMImageMessage *)message;
// 消息的 ID
NSString *messageId = imageMessage.messageId;
// 图像文件的 URL
NSString *imageUrl = imageMessage.file.url;
// 发该消息的 clientId
NSString *fromClientId = message.clientId;
}
var { Event, TextMessage } = require('leancloud-realtime');
var { ImageMessage } = initPlugin(AV, IM);
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
var file;
switch (message.type) {
case ImageMessage.TYPE:
file = message.getFile();
console.log('收到图像消息,URL:' + file.url());
break;
}
}
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case .message(event: let messageEvent):
switch messageEvent {
case .received(message: let message):
switch message {
case let imageMessage as IMImageMessage:
print(imageMessage)
default:
break
}
default:
break
}
default:
break
}
}
lient.onMessage = ({
Client client,
Conversation conversation,
Message message,
}) {
if (message is ImageMessage) {
print('收到图像消息,URL:${message.url}');
}
};
发送音频消息/视频/文件
发送流程
对于图像、音频、视频和文件这四种类型的消息,SDK 均采取如下的发送流程:
如果文件是从 客户端 API 读取的数据流(Stream),步骤为:
- 从本地构造
LCFile
- 调用
LCFile
的上传方法将文件上传到云端,并获取文件元信息(metaData
) - 把
LCFile
的objectId
、URL、文件元信息都封装在消息体内 - 调用接口发送消息
如果文件是 外部链接的 URL,则:
- 直接将 URL 封装在消息体内,不获取元信息(例如,音频消息的时长),不包含
objectId
- 调用接口发送消息
以发送音频消息为例,基本流程是:读取音频文件(或者录制音频)> 构建音频消息 > 消息发送。
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var audio = new LCFile("tante.mp3", Path.Combine(Application.persistentDataPath, "tante.mp3"));
var audioMessage = new LCIMAudioMessage(audio);
audioMessage.Text = "听听人类的神曲";
await conversation.Send(audioMessage);
LCFile file = LCFile.withAbsoluteLocalPath("忐忑.mp3",localFilePath);
LCIMAudioMessage m = new LCIMAudioMessage(file);
m.setText("听听人类的神曲");
// 创建一条音频消息
conv.sendMessage(m, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
// 发送成功
}
}
});
NSError *error = nil;
LCFile *file = [LCFile fileWithLocalPath:localPath error:&error];
if (!error) {
LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"听听人类的神曲" file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
}
var AV = require("leancloud-storage");
var { AudioMessage } = initPlugin(AV, IM);
var fileUploadControl = $("#musicFileUpload")[0];
var file = new AV.File("忐忑.mp3", fileUploadControl.files[0]);
file
.save()
.then(function () {
var message = new AudioMessage(file);
message.setText("听听人类的神曲");
return conversation.send(message);
})
.then(function () {
console.log("发送成功");
})
.catch(console.error.bind(console));
do {
if let filePath = Bundle.main.url(forResource: "audio", withExtension: "mp3")?.path {
let audioMessage = IMAudioMessage(filePath: filePath, format: "mp3")
audioMessage.text = "听听人类的神曲"
try conversation.send(message: audioMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
}
} catch {
print(error)
}
import 'package:flutter/services.dart' show rootBundle;
// 假设项目根目录有 assets 文件夹存放 mp3 文件,并且在 pubspec.yaml 中已经将 assets 文件夹添加到工程中。
ByteData audioData = await rootBundle.load('assets/test.mp3');
AudioMessage audioMessage = AudioMessage.from(
binaryData: audioData.buffer.asUint8List(),
format: 'mp3',
);
audioMessage.text = '听听人类的神曲';
try {
await conversation.send(message: audioMessage);
} catch (e) {
print(e);
}
与图像消息类似,音频消息也支持从 URL 构建:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var audio = new LCFile("apple.aac", new Uri("https://some.website.com/apple.aac"));
var audioMessage = new LCIMAudioMessage(audio);
audioMessage.Text = "来自苹果发布会现场的录音";
await conversation.Send(audioMessage);
LCFile file = new LCFile("apple.aac", "https://some.website.com/apple.aac", null);
LCIMAudioMessage m = new LCIMAudioMessage(file);
m.setText("来自苹果发布会现场的录音");
conv.sendMessage(m, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
// 发送成功
}
}
});
LCFile *file = [LCFile fileWithRemoteURL:[NSURL URLWithString:@"https://some.website.com/apple.aac"]];
LCIMAudioMessage *message = [LCIMAudioMessage messageWithText:@"来自苹果发布会现场的录音" file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
var AV = require("leancloud-storage");
var { AudioMessage } = initPlugin(AV, IM);
var file = new AV.File.withURL(
"apple.aac",
"https://some.website.com/apple.aac"
);
file
.save()
.then(function () {
var message = new AudioMessage(file);
message.setText("来自苹果发布会现场的录音");
return conversation.send(message);
})
.then(function () {
console.log("发送成功");
})
.catch(console.error.bind(console));
do {
if let url = URL(string: "https://some.website.com/apple.aac") {
let audioMessage = IMAudioMessage(url: url, format: "aac")
audioMessage.text = "来自苹果发布会现场的录音"
try conversation.send(message: audioMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
}
} catch {
print(error)
}
AudioMessage audioMessage = AudioMessage.from(
url: 'https://some.website.com/apple.aac',
name: 'apple.aac',
);
try {
await conversation.send(message: audioMessage);
} catch (e) {
print(e);
}
发送地理位置消息
地理位置消息构建方式如下:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var location = new LCGeoPoint(31.3753285, 120.9664658);
var locationMessage = new LCIMLocationMessage(location);
await conversation.Send(locationMessage);
final LCIMLocationMessage locationMessage = new LCIMLocationMessage();
// 开发者可以通过设备的 API 获取设备的具体地理位置,此处设置了 2 个经纬度常量作为演示
locationMessage.setLocation(new LCGeoPoint(31.3753285,120.9664658));
locationMessage.setText("蛋糕店的位置");
conversation.sendMessage(locationMessage, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (null != e) {
e.printStackTrace();
} else {
// 发送成功
}
}
});
LCIMLocationMessage *message = [LCIMLocationMessage messageWithText:@"蛋糕店的位置" latitude:31.3753285 longitude:120.9664658 attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
var AV = require("leancloud-storage");
var { LocationMessage } = initPlugin(AV, IM);
var location = new AV.GeoPoint(31.3753285, 120.9664658);
var message = new LocationMessage(location);
message.setText("蛋糕店的位置");
conversation
.send(message)
.then(function () {
console.log("发送成功");
})
.catch(console.error.bind(console));
do {
let locationMessage = IMLocationMessage(latitude: 31.3753285, longitude: 120.9664658)
try conversation.send(message: locationMessage, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
LocationMessage locationMessage = LocationMessage.from(
latitude: 22,
longitude: 33,
);
try {
await conversation.send(message: locationMessage);
} catch (e) {
print(e);
}
再谈接收消息
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
C# SDK 通过 OnMessage
事件回调来通知新消息:
jerry.OnMessage = (conv, msg) => {
if (msg is LCIMImageMessage imageMessage) {
} else if (msg is LCIMAudioMessage audioMessage) {
} else if (msg is LCIMVideoMessage videoMessage) {
} else if (msg is LCIMFileMessage fileMessage) {
} else if (msg is AVIMLocationMessage locationMessage) {
} else if (msg is InputtingMessage) {
WriteLine($"收到自定义消息 {inputtingMessage.TextContent} {inputtingMessage.Ecode}");
}
}
Java/Android SDK 中定义了 LCIMMessageHandler
接口来通知应用层新消息到达事件发生,开发者通过调用 LCIMMessageManager.registerDefaultMessageHandler
方法来注册自己的消息处理函数。LCIMMessageManager
提供了两个不同的方法来注册默认的消息处理函数,或特定类型的消息处理函数:
/**
* 注册默认的消息 handler
*
* @param handler
*/
public static void registerDefaultMessageHandler(LCIMMessageHandler handler);
/**
* 注册特定消息格式的处理单元
*
* @param clazz 特定的消息类
* @param handler
*/
public static void registerMessageHandler(Class<? extends LCIMMessage> clazz, MessageHandler<?> handler);
/**
* 取消特定消息格式的处理单元
*
* @param clazz
* @param handler
*/
public static void unregisterMessageHandler(Class<? extends LCIMMessage> clazz, MessageHandler<?> handler);
消息处理函数需要在应用初始化时完成设置,理论上我们支持为每一种消息(包括应用层自定义的消息)分别注册不同的消息处理函数,并且也支持取消注册。
多次调用 LCIMMessageManager
的 registerDefaultMessageHandler
,只有最后一次调用有效;而通过 registerMessageHandler
注册的 LCIMMessageHandler
,则是可以同存的。
当客户端收到一条消息的时候,SDK 内部的处理流程为:
- 首先解析消息的类型,然后找到开发者为这一类型所注册的处理响应 handler chain,再逐一调用这些 handler 的
onMessage
函数。 - 如果没有找到专门处理这一类型消息的 handler,就会转交给
defaultHandler
处理。
这样一来,在开发者为 AVIMTypedMessage
(及其子类)指定了专门的 handler,也指定了全局的 defaultHandler
了的时候,如果发送端发送的是通用的 LCIMMessage
消息,那么接收端就是 LCIMMessageManager.registerDefaultMessageHandler()
中指定的 handler 被调用;如果发送的是 LCIMTypedMessage
(及其子类)的消息,那么接收端就是 LCIMMessageManager.registerMessageHandler()
中指定的 handler 被调用。
// 1. 注册默认 handler,只有其他 handle 都没有被调用到时才会调用
LCIMMessageManager.registerDefaultMessageHandler(new LCIMMessageHandler(){
public void onMessage(LCIMMessage message, LCIMConversation conversation, LCIMClient client) {
// 接收消息
}
public void onMessageReceipt(LCIMMessage message, LCIMConversation conversation, LCIMClient client) {
// 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。
// 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。
}
});
// 2. 为每一种消息类型注册 handler
LCIMMessageManager.registerMessageHandler(LCIMTypedMessage.class, new LCIMTypedMessageHandler<LCIMTypedMessage>(){
public void onMessage(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) {
switch (message.getMessageType()) {
case LCIMMessageType.TEXT_MESSAGE_TYPE:
// 执行其他逻辑
LCIMTextMessage textMessage = (LCIMTextMessage)message;
break;
case LCIMMessageType.IMAGE_MESSAGE_TYPE:
// 执行其他逻辑
LCIMImageMessage imageMessage = (LCIMImageMessage)message;
break;
case LCIMMessageType.AUDIO_MESSAGE_TYPE:
// 执行其他逻辑
LCIMAudioMessage audioMessage = (LCIMAudioMessage)message;
break;
case LCIMMessageType.VIDEO_MESSAGE_TYPE:
// 执行其他逻辑
LCIMVideoMessage videoMessage = (LCIMVideoMessage)message;
break;
case LCIMMessageType.LOCATION_MESSAGE_TYPE:
// 执行其他逻辑
LCIMLocationMessage locationMessage = (LCIMLocationMessage)message;
break;
case LCIMMessageType.FILE_MESSAGE_TYPE:
// 执行其他逻辑
LCIMFileMessage fileMessage = (LCIMFileMessage)message;
break;
case LCIMMessageType.RECALLED_MESSAGE_TYPE:
// 执行其他逻辑
LCIMRecalledMessage recalledMessage = (LCIMRecalledMessage)message;
break;
case 123:
// 这是一个自定义消息类型
// 执行其他逻辑
CustomMessage customMessage = (CustomMessage)message;
break;
}
}
public void onMessageReceipt(LCIMTypedMessage message, LCIMConversation conversation, LCIMClient client) {
// 执行收到消息后的逻辑
}
});
Objective-C SDK 是通过实现 LCIMClientDelegate
代理来响应新消息到达通知的,并且,分别使用了两个方法来分别处理普通的 LCIMMessage
消息和内建的多媒体消息 LCIMTypedMessage
(包括应用层由此派生的自定义消息:
/*!
接收到新的普通消息。
@param conversation - 所属对话
@param message - 具体的消息
*/
- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message;
/*!
接收到新的富媒体消息。
@param conversation - 所属对话
@param message - 具体的消息
*/
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message;
// 处理默认类型消息
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
if (message.mediaType == LCIMMessageMediaTypeImage) {
LCIMImageMessage *imageMessage = (LCIMImageMessage *)message; // 处理图像消息
} else if(message.mediaType == LCIMMessageMediaTypeAudio){
// 处理音频消息
} else if(message.mediaType == LCIMMessageMediaTypeVideo){
// 处理视频消息
} else if(message.mediaType == LCIMMessageMediaTypeLocation){
// 处理位置消息
} else if(message.mediaType == LCIMMessageMediaTypeFile){
// 处理文件消息
} else if(message.mediaType == LCIMMessageMediaTypeText){
// 处理文本消息
} else if(message.mediaType == 123){
// 处理自定义的消息类型
}
}
// 处理未知消息类型
- (void)conversation:(LCIMConversation *)conversation didReceiveCommonMessage:(LCIMMessage *)message {
// 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。
// 因此别忘了在这里处理未知类型,例如提示用户升级客户端至最新版本。
}
不管消息类型如何,JavaScript SDK 都是是通过 IMClient
上的 Event.MESSAGE
事件回调来通知新消息的,应用层只需要在一个地方,统一对不同类型的消息使用不同方式来处理即可。
// 在初始化 Realtime 时,需加载 TypedMessagesPlugin
var { Event, TextMessage } = require("leancloud-realtime");
var { FileMessage, ImageMessage, AudioMessage, VideoMessage, LocationMessage } =
initPlugin(AV, IM);
// 注册 MESSAGE 事件的 handler
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
// 请按自己需求改写
var file;
switch (message.type) {
case TextMessage.TYPE:
console.log(
"收到文本消息,内容:" + message.getText() + ",ID:" + message.id
);
break;
case FileMessage.TYPE:
file = message.getFile(); // file 是 AV.File 实例
console.log(
"收到文件消息,URL:" + file.url() + ",大小:" + file.metaData("size")
);
break;
case ImageMessage.TYPE:
file = message.getFile();
console.log(
"收到图像消息,URL:" + file.url() + ",宽度:" + file.metaData("width")
);
break;
case AudioMessage.TYPE:
file = message.getFile();
console.log(
"收到音频消息,URL:" +
file.url() +
",长度:" +
file.metaData("duration")
);
break;
case VideoMessage.TYPE:
file = message.getFile();
console.log(
"收到视频消息,URL:" +
file.url() +
",长度:" +
file.metaData("duration")
);
break;
case LocationMessage.TYPE:
var location = message.getLocation();
console.log(
"收到位置消息,纬度:" +
location.latitude +
",经度:" +
location.longitude
);
break;
case 1:
console.log("OperationMessage 是自定义消息类型");
default:
// 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。
// 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。
console.warn("收到未知类型消息");
}
});
// 同时,对应的 conversation 上也会派发 `MESSAGE` 事件:
conversation.on(Event.MESSAGE, function messageEventHandler(message) {
// 这里补充业务逻辑
});
Swift SDK 是通过实现 IMClientDelegate
代理来响应新消息到达通知的:
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case .message(event: let messageEvent):
switch messageEvent {
case .received(message: let message):
print(message)
default:
break
}
default:
break
}
}
// 处理默认类型消息
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case .message(event: let messageEvent):
switch messageEvent {
case .received(message: let message):
if let categorizedMessage = message as? IMCategorizedMessage {
switch categorizedMessage {
case let textMessage as IMTextMessage:
print(textMessage)
case let imageMessage as IMImageMessage:
print(imageMessage)
case let audioMessage as IMAudioMessage:
print(audioMessage)
case let videoMessage as IMVideoMessage:
print(videoMessage)
case let fileMessage as IMFileMessage:
print(fileMessage)
case let locationMessage as IMLocationMessage:
print(locationMessage)
case let recalledMessage as IMRecalledMessage:
print(recalledMessage)
case let customMessage as CustomMessage:
print("customMessage 是自定义消息类型")
default:
break
} else {
// 未来可能添加新的自定义消息类型,新版 SDK 也可能添加新的消息类型。
// 因此别忘了在默认分支中处理未知类型,例如提示用户升级客户端至最新版本。
print("收到未知类型消息")
}
default:
break
}
default:
break
}
}
jerry.onMessage = ({
Client client,
Conversation conversation,
Message message,
}) {
if (