Introduction
A lot of products today have the needs to offer instant messaging functions to their users. For example:
- To have the staff behind the product talk to the users.
- To have the workers in a company communicate with each other.
- To have the audience of live-streamed contents interact with each other.
- To have the users of an app or players of a game chat with each other.
Based on the hierarchy of needs and the difficulty of implementation, we wrote four chapters of documentation for you to learn how you can embed LeanMessage into your app:
- In this chapter, we will introduce how you can implement one-on-one chatting and group chats, how you can create and join conversations, and how you can send and receive rich media messages. We will also introduce how history messages are kept on the cloud and how you can retrieve them. By the end of this chapter, you should be able to build a simple chatting page in your app.
- In the second chapter, we will introduce some advanced features built around messaging, including mentioning people with "@", recalling messages, editing messages, getting receipts when messages are delivered and read, sending push notifications, and synchronizing messages. The implementation of multi device sign-on and custom message types will also be covered. By the end of this chapter, you should be able to integrate a chatting component into your app with these features.
- In the third chapter, we will introduce the security features offered by our services, including third-party signing mechanism, permission management of members, and blacklisting. We will also go over the usage of chat rooms and temporary conversations. By the end of this chapter, you will get a set of skills to improve the security and usability of your app, as well as to build conversations that serve different purposes.
- In the last chapter, we will introduce the usage of hooks and system conversations, plus how you can build your own chatbots based on them. By the end of this chapter, you will learn how you can make your app extensible and adapted to a wide variety of requirements.
We aim our documentation to not only help you complete the functions you are currently building but also give you a better understanding of all the things LeanMessage can do (which you will find helpful when you plan to add more features into your app).
Before you continue:
Take a look at LeanMessage Overview if you haven't done it yet. Also make sure you have already followed SDK Installation to install and initialize the SDK for the platform (language) you are using.
One-on-One Chatting
Before diving into the main topic, let's see what an IMClient
object is in LeanMessage SDK:
An
IMClient
refers to an actual user, meaning that the user logged in to the system as a client.
See LeanMessage Overview for more details.
Creating IMClient
Assuming that there is a user named "Tom". Now let's create an IMClient
instance for him:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
LCIMClient tom = new LCIMClient("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 logs in with his name as clientId
realtime
.createIMClient("Tom")
.then(function (tom) {
// Successfully logged in
})
.catch(console.error);
var tom: IMClient
do {
tom = try IMClient(ID: "Tom")
} catch {
print(error)
}
// clientId is Tom
Client tom = Client(id: 'Tom');
Keep in mind that an IMClient
refers to an actual user. It should be stored globally since all the further actions done by this user will have to access it.
Logging in to the LeanMessage Server
After creating the IMClient
instance for Tom, we will need to have this instance log in to the LeanMessage server. Only clients that are logged in can chat with other users and receive notifications from the cloud.
For JavaScript and C# (Unity3D) SDKs, clients will be automatically logged in when IMClient
instances are created; for iOS (both Objective-C and Swift) and Android (including Java) SDKs, clients need to be logged in manually with the open
method:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await tom.Open();
// Tom creates a client and logs in with his name as clientId
LCIMClient tom = LCIMClient.getInstance("Tom");
// Tom logs in
tom.open(new LCIMClientCallback() {
@Override
public void done(LCIMClient client, LCIMException e) {
if (e == null) {
// Successfully connected
}
}
});
@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 logs in with his name as clientId and gets the IMClient instance
realtime
.createIMClient("Tom")
.then(function (tom) {
// Successfully logged in
})
.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)
}
Client tom = Client(id: 'Tom');
await tom.open();
Logging in with _User
Beside specifying a clientId
within the app, you can also log in directly with a _User
object after an IMClient
is created. By doing so, the signing process for logging in can be skipped which helps you easily integrate LeanStorage with LeanMessage:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var user = await LCUser.Login("USER_NAME", "PASSWORD");
var client = new LCIMClient(user);
// Tom creates a client and logs in with his name as clientId
LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer<LCUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(LCUser user) {
// Successfully connected
LCIMClient client = LCIMClient.getInstance(user);
client.open(new LCIMClientCallback() {
@Override
public void done(final LCIMClient avimClient, LCIMException e) {
}
});
}
public void onError(Throwable throwable) {
// Error connected
}
public void onComplete() {}
});
@property (nonatomic) LCIMClient *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
// Log in to LeanMessage with the username and password of an AVUser
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)
}
}
// Not supported yet
Creating Conversations
A Conversation
needs to be created before a user can chat with others.
[Conversations] are the carriers of messages. All the messages are sent to conversations to be delivered to the members in them.
Since Tom is already logged in, he can start chatting with other users now. If he wants to chat with Jerry, he can create a Conversation
containing Jerry and himself:
- 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) {
// Successfullly created
}
}
});
// Create a conversation with Jerry
[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) {
// handle callback
}];
// Create a conversation with Jerry
tom
.createConversation({
// Members of the conversation include Tom (the SDK will automatically add the current user into the conversation) and Jerry
members: ["Jerry"],
// Name of the conversation
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 {
// Create a conversation with Jerry
Conversation conversation = await tom.createConversation(
isUnique: true, members: {'Jerry'}, name: 'Tom & Jerry');
} catch (e) {
print('Failed to create a conversation: $e');
}
createConversation
creates a new conversation and stores it into the _Conversation
table which can be found in your app's Dashboard > LeanStorage > Data. Below are the interfaces offered by different SDKs for creating conversations:
- 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);
}
/**
* Create or find an existing conversation
*
* @param members The members in the conversation
* @param name The name of the conversation
* @param attributes Custom attributes
* @param isTransient Whether the conversation is a chat room
* @param isUnique Whether return the existing conversation satisfying conditions
* If false, create a new conversation
* If true, find if there is an existing conversation satisfying conditions; if so, return the conversation, otherwise create a new conversation
* If true, only members is the valid query condition
* @param callback The callback after the conversation is created
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient, final boolean isUnique,
final AVIMConversationCreatedCallback callback);
/**
* Create a conversation
*
* @param members The members in the conversation
* @param attributes Custom attributes
* @param isTransient Whether the conversation is a chat room
* @param callback The callback after the conversation is created
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient,
final AVIMConversationCreatedCallback callback);
/**
* Create a conversation
*
* @param conversationMembers The members in the conversation
* @param name The name of the conversation
* @param attributes Custom attributes
* @param callback The callback after the conversation is created
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers, String name,
final Map<String, Object> attributes, final AVIMConversationCreatedCallback callback);
/**
* Create a conversation
*
* @param conversationMembers The members in the conversation
* @param attributes Custom attributes
* @param callback The callback after the conversation is created
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers,
final Map<String, Object> attributes, final AVIMConversationCreatedCallback 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;
/**
* Create a conversation
* @param {Object} options The fields beside the following ones will be treated as custom attributes
* @param {String[]} options.members The members of the conversation; required; include the current client by default
* @param {String} [options.name] The name of the conversation; optional; defaults to null
* @param {Boolean} [options.transient=false] Whether the conversation is a chat room; optional
* @param {Boolean} [options.unique=false] Whether the 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 {Boolean} [options.tempConv=false] Whether the conversation is temporary; optional
* @param {Integer} [options.tempConvTTL=0] Optional; if tempConv is true, the TTL of the conversation can be specified here
* @return {Promise.<Conversation>}
*/
async createConversation({
members: m,
name,
transient,
unique,
tempConv,
tempConvTTL,
// You may add more properties
});
/// 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 {}
Although SDKs for different languages/platforms share different interfaces, they take in the similar set of parameters when creating a conversation:
members
: Required; includes the initial list of members in the conversation. The initiator of the conversation is included by default, somembers
does not have to include theclientId
of the current user.name
: The name of the conversation; optional. The code above puts "Tom & Jerry" for it.attributes
: The custom attributes of the conversation; optional. The code above does not specify any attributes. If you ever specify them for your conversations, you can retrieve them later withAVIMConversation
. Such attributes will be stored in theattr
field of the_Conversation
table.unique
/isUnique
orAVIMConversationOptionUnique
: Marks if the conversation is unique; optional.- If true, the cloud will perform a query on conversations with the list of members specified. If an existing conversation contains the same members, the conversation will be returned, otherwise a new conversation will be created.
- If false, a new conversation will be created each time
createConversation
is called. - If not specified, it defaults to true for JavaScript, Java, Swift, and C# SDKs and false for Objective-C and Python SDKs (for compatibility).
- In general, it is more reasonable that there is only one conversation existing for the same composition of members, otherwise it could be messy since multiple sets of message histories are available for the same group of people. We strongly recommend that you set
unique
to betrue
when creating conversations.
- Other parameters specifying the type of the conversation; optional. For example,
transient
/isTransient
specifies if it is a chat room, andtempConv
/tempConvTTL
orAVIMConversationOptionTemporary
specifies if it is a temporary conversation. If nothing is specified, it will be a basic conversation. We will talk more about them later.
The built-in properties of a conversation can be retrieved once the conversation is created. For example, a globally unique ID will be created for each conversation which can be retrieved with Conversation.id
. This is the field often used for querying conversations.
Sending Messages
Now that the conversation is created, Tom can start sending messages to it:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var textMessage = new LCIMTextMessage("Get up, Jerry!");
await conversation.Send(textMessage);
LCIMTextMessage msg = new LCIMTextMessage();
msg.setText("Get up, Jerry!");
conversation.sendMessage(msg, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
Log.d("Tom & Jerry", "Message sent!");
}
}
});
LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Message sent!");
}
}];
var { TextMessage } = require("leancloud-realtime");
conversation
.send(new TextMessage("Get up, Jerry!"))
.then(function (message) {
console.log("Tom & Jerry", "Message sent!");
})
.catch(console.error);
do {
let textMessage = IMTextMessage(text: "Get up, 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 = 'Get up, Jerry!';
await conversation.send(message: textMessage);
} catch (e) {
print(e);
}
Conversation#send
sends a message to the conversation specified. All the other members who are online will immediately receive the message.
So how would Jerry see the message on his device?
Receiving Messages
On another device, we create an AVIMClient
with Jerry
as clientId
and log in to the server (just as how we did for Tom):
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var jerry = new LCIMClient("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();
As the receiver of the message, Jerry doesn't have to create a conversation with Tom and may as well not know that Tom created a conversation with him. Jerry needs to set up a callback function to get notified for the things Tom did.
By setting up callbacks, clients will be able to handle notifications sent from the cloud. Here we focus on the following two events:
- The user is invited to a conversation. At the moment Tom creates a new conversation with Jerry, Jerry will receive a notification saying something like "Tom invited you to a conversation".
- A new message is delivered to a conversation the user is already in. At the moment Tom sends out the message "Get up, Jerry!", Jerry will receive a notification including the message itself as well as the context information like the conversation the message is sent to and the sender of the message.
Now let's see how clients should handle such notifications. The code below handles both "joining conversation" and "getting new message" events for 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 is the ID of the conversation
// textMessage.TextContent is the text content of the message
// textMessage.FromClientId is the clientId of the sender
}
};
// Java/Android SDK responds to notifications with custom event handlers
public class CustomConversationEventHandler extends AVIMConversationEventHandler {
/**
* The current user is added to a conversation
*
* @param client
* @param conversation The conversation
* @param operator The inviter
* @since 3.0
*/
@Override
public void onInvited(AVIMClient client, AVIMConversation conversation, String invitedBy) {
// Things to do after the current clientId (Jerry) is invited to the conversation
}
}
// Set up global conversation event handler
AVIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
// Java/Android SDK responds to notifications with custom event handlers
public static class CustomMessageHandler extends AVIMMessageHandler{
/**
* Reload this method to handle message receiving
*
* @param message
* @param conversation
* @param client
*/
@Override
public void onMessage(AVIMMessage message,AVIMConversation conversation,AVIMClient client){
if(message instanceof AVIMTextMessage){
Log.d(((AVIMTextMessage)message).getText());// Get up, Jerry!
}
}
}
// Set up global message handling handler
AVIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler());
// Objective-C SDK responds to notifications with AVIMClientDelegate
// For those unfamiliar with the delegation concept, please refer to:
// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html
jerry.delegate = delegator;
/*!
The current user is added to a conversation
@param conversation - The conversation
@param clientId - The ID of the inviter
*/
-(void)conversation:(AVIMConversation *)conversation invitedByClientId:(NSString *)clientId{
NSLog(@"%@", [NSString stringWithFormat:@"Current clientId (Jerry) is invited by %@ to join the conversation.",clientId]);
}
/*!
The current user receives a message
@param conversation - The conversation
@param message - The content of the message
*/
- (void)conversation:(AVIMConversation *)conversation didReceiveTypedMessage:(AVIMTypedMessage *)message {
NSLog(@"%@", message.text); // Get up, Jerry!
}
// JS SDK responds to notifications by binding events on IMClient with callbacks
// The current user is added to a conversation
jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) {
console.log(payload.invitedBy, conversation.id);
});
// The current user receives a message; can be handled by responding to Event.MESSAGE
jerry.on(Event.MESSAGE, function(message, conversation) {
console.log('Message received: ' + 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('Received message: ${message.stringContent}');
}
};
Now let's take a look at the sequence diagram showing how the first message sent from Tom to Jerry is processed:
Beside responding to notifications about new messages, clients also need to respond to those indicating the change of members in a conversation, like "XX invited XX into the conversation", "XX left the conversation", and "XX is removed by the admin". Such notifications will be delivered to clients in real time. See Summary of Event Notifications Regarding Changes of Members for more details.
Group Chats
We just discussed how we can create a conversation between two users. Now let's see how we can create a group chat with more people.
There aren't many differences between the two types of conversations and a major one would be the amount of members in them. You can either specify all the members of a group chat when creating it, or add them later after the conversation is created.
Creating Group Chats
In the previous conversation between Tom and Jerry (assuming conversation ID to be CONVERSATION_ID
), if Tom wants to add Mary into the conversation, the following code can be used:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
// Get the conversation with ID
var conversation = await tom.GetConversation("CONVERSATION_ID");
// Invite Mary
await conversation.AddMembers(new string[] { "Mary" });
// Get the conversation with ID
final LCIMConversation conv = client.getConversation("CONVERSATION_ID");
// Invite Mary
conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() {
@Override
public void done(LCIMException e, List<String> successfulClientIds, List<LCIMOperationFailure> failures) {
}
});
// Get the conversation with ID
LCIMConversationQuery *query = [self.client conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) {
// Invite Mary
[conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Member added!");
}
}];
}];
// Get the conversation with ID
tom
.getConversation("CONVERSATION_ID")
.then(function (conversation) {
// Invite Mary
return conversation.add(["Mary"]);
})
.then(function (conversation) {
console.log('Member added!', conversation.members);
// The conversation now contains ['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 {
// Get the conversation with ID
ConversationQuery query = tom.conversationQuery();
query.whereEqualTo('objectId', 'CONVERSATION_ID');
conversations = await query.find();
} catch (e) {
print(e);
}
try {
Conversation conversation = conversations.first;
// Invite Mary
MemberResult addResult = await conversation.addMembers(
members: {'Mary'},
);
} catch (e) {
print(e);
}
On Jerry's side, he can add a listener for handling events regarding "new members being added". With the code below, he will be notified once Tom invites Mary to the conversation:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话");
}
AVIMOnInvitedEventArgs
contains the following fields:
InvitedBy
: The inviterJoinedMembers
: The list of members being addedConversationId
: The conversation
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
contains the following fields:
members
: Array of strings; the list ofclientId
s of the members being addedinvitedBy
: String; theclientId
of the inviter
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()} 加入会话');
};
Here is the sequence diagram of the operation:
On Mary's side, to know that she is added to the conversation between Tom and Jerry, she can follow the way Jerry listens to the INVITED
event, which can be found in One-on-One Chatting.
If Tom wants to create a new conversation with all the members included, the following code can be used:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var conversation = await tom.CreateConversationAsync(new string[]{ "Jerry","Mary" }, name:"Tom & Jerry & friends", isUnique:true);
tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null,
new AVIMConversationCreatedCallback() {
@Override
public void done(AVIMConversation conversation, AVIMException e) {
if (e == null) {
// Conversation created
}
}
});
// Tom creates a conversation with his friends
NSArray *friends = @[@"Jerry", @"Mary"];
[tom createConversationWithName:@"Tom & Jerry & friends" clientIds:friends
options:AVIMConversationOptionUnique
callback:^(AVIMConversation *conversation, NSError *error) {
if (!error) {
NSLog(@"Conversation created!");
}
}];
tom.createConversation({
// Add Jerry and Mary to the conversation when creating it; more members can be added later as well
members: ['Jerry','Mary'],
// The name of the conversation
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);
}
Sending Group Messages
In a group chat, if a member sends a message, the message will be delivered to all the online members in the group. The process is the same as how Jerry receives the message from Tom.
For example, if Tom sends a welcoming message to the group:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var textMessage = new AVIMTextMessage("Welcome everyone!");
await conversation.SendMessageAsync(textMessage);
AVIMTextMessage msg = new AVIMTextMessage();
msg.setText("Welcome everyone!");
// Send the message
conversation.sendMessage(msg, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (e == null) {
Log.d("Group chat", "Message sent!");
}
}
});
[conversation sendMessage:[AVIMTextMessage messageWithText:@"Welcome everyone!" attributes:nil] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Message sent!");
}
}];
conversation.send(new TextMessage('Welcome everyone!'));
do {
let textMessage = IMTextMessage(text: "Welcome everyone!")
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 = 'Welcome everyone!';
await conversation.send(message: textMessage);
} catch (e) {
print(e);
}
Both Jerry and Mary will have Event.MESSAGE
event triggered which can be used to retrieve the message and have it displayed on the UI.
Removing Members
One day Mary spoke something that made Tom angry and Tom wants to kick her out of the group chat. How would Tom do that?
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await conversation.RemoveMembersAsync("Mary");
conv.kickMembers(Arrays.asList("Mary"), new AVIMOperationPartiallySucceededCallback() {
@Override
public void done(AVIMException e, List<String> successfulClientIds, List<AVIMOperationFailure> failures){
}
});
[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Member removed!");
}
}];
conversation.remove(['Mary']).then(function(conversation) {
console.log('Member removed!', 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);
}
The following process will be triggered:
Here we see that Mary receives KICKED
which indicates that she (the current user) is removed. Other members (Jerry and Tom) will receive MEMBERS_LEFT
which indicates that someone else in the conversation is removed. Such events can be handled with the following code:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
private void OnMembersLeft(object sender, AVIMOnInvitedEventArgs e)
{
Debug.Log(string.Format("{0} removed {1} from {2}", e.KickedBy, e.JoinedMembers, e.ConversationId));
}
private void OnKicked(object sender, AVIMOnInvitedEventArgs e)
{
Debug.Log(string.Format("You are removed from {2} by {1}", e.KickedBy, e.ConversationId));
}
jerry.OnMembersLeft += OnMembersLeft;
jerry.OnKicked += OnKicked;
public class CustomConversationEventHandler extends AVIMConversationEventHandler {
/**
* Someone else is removed
*
* @param client
* @param conversation
* @param members The members being removed
* @param kickedBy The ID of the operator; could be the current user itself
* @since 3.0
*/
@Override
public abstract void onMemberLeft(AVIMClient client,
AVIMConversation conversation, List<String> members, String kickedBy) {
Toast.makeText(AVOSCloud.applicationContext,
members + " are removed from " + conversation.getConversationId() + " by "
+ kickedBy, Toast.LENGTH_SHORT).show();
}
/**
* The current user is removed
*
* @param client
* @param conversation
* @param kickedBy The person who removed you
* @since 3.0
*/
@Override
public abstract void onKicked(AVIMClient client, AVIMConversation conversation,
String kickedBy) {
Toast.makeText(AVOSCloud.applicationContext,
"You are removed from " + conversation.getConversationId() + " by "
+ kickedBy, Toast.LENGTH_SHORT).show();
}
}
// Set up global event handler
AVIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
jerry.delegate = delegator;
#pragma mark - AVIMClientDelegate
/*!
Someone else is removed
@param conversation - The conversation
@param clientIds - The list of members being removed
@param clientId - The ID of the operator
*/
- (void)conversation:(AVIMConversation *)conversation membersRemoved:(NSArray<NSString *> * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId {
;
}
/*!
The current user is removed
@param conversation - The conversation
@param clientId - The ID of the operator
*/
- (void)conversation:(AVIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId {
;
}
// Someone else is removed
jerry.on(Event.MEMBERS_LEFT, function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
});
// The current user is removed
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('$byClientID removed ${members.toString()}.');
};
jerry.onKicked = ({
Client client,
Conversation conversation,
String byClientID,
DateTime atDate,
}) {
print('You are removed by $byClientID');
};
Joining Conversations
Tom is feeling bored after removing Mary. He goes to William and tells him that there is a group chat that Jerry and himself are in. He gives the ID (or name) of the group chat to William which makes him curious about what's going on in it. William then adds himself to the group:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
await william.JoinAsync("CONVERSATION_ID");
AVIMConversation conv = william.getConversation("CONVERSATION_ID");
conv.join(new AVIMConversationCallback(){
@Override
public void done(AVIMException e){
if(e==null){
// Successfully joined
}
}
});
AVIMConversationQuery *query = [william conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(AVIMConversation *conversation, NSError *error) {
[conversation joinWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Successfully joined!");
}
}];
}];
william.getConversation('CONVERSATION_ID').then(function(conversation) {
return conversation.join();
}).then(function(conversation) {
console.log('Successfully joined!', conversation.members);
// The conversation now contains ['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);
}
The following process will be triggered:
Other members can listen to MEMBERS_JOINED
to know that William joined the conversation:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
private void OnMembersJoined(object sender, AVIMOnInvitedEventArgs e)
{
// e.InvitedBy is the operator; e.ConversationId is the ID of the conversation
Debug.Log(string.Format("{0} joined {1}; operated by {2}",e.JoinedMembers, e.ConversationId, e.InvitedBy));
}
jerry.OnMembersJoined += OnMembersJoined;
public class CustomConversationEventHandler extends AVIMConversationEventHandler {
@Override
public void onMemberJoined(AVIMClient client, AVIMConversation conversation,
List<String> members, String invitedBy) {
// Shows that William joined 551260efe4b01608686c3e0f; operated by William
Toast.makeText(AVOSCloud.applicationContext,
members + " joined " + conversation.getConversationId() + "; operated by "
+ invitedBy, Toast.LENGTH_SHORT).show();
}
}
- (void)conversation:(AVIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ joined the conversation; operated by %@",[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()} joined');
};
Leaving Conversations
With more and more people being invited by Tom, Jerry feels that he doesn't like most of them and wants to leave the conversation. He can do that with Conversation#quit
:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
conversation.quit(new AVIMConversationCallback(){
@Override
public void done(AVIMException e){
if(e==null){
// You left the conversation
}
}
});
[conversation quitWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"You left the conversation!");
}
}];
conversation.quit().then(function(conversation) {
console.log('You left the conversation!', 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);
}
After leaving the conversation, Jerry will no longer receive messages from it. Here is the sequence diagram of the operation:
Other members can listen to MEMBERS_LEFT
to know that Jerry left the conversation:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
mary.OnMembersLeft += OnMembersLeft;
private void OnMembersLeft(object sender, AVIMOnMembersLeftEventArgs e)
{
// e.KickedBy is the operator; e.ConversationId is the ID of the conversation
Debug.Log(string.Format("{0} left {1}; operated by {2}",e.JoinedMembers, e.ConversationId, e.KickedBy));
}
public class CustomConversationEventHandler extends AVIMConversationEventHandler {
@Override
public void onMemberLeft(AVIMClient client, AVIMConversation conversation, List<String> members,
String kickedBy) {
// Things to do after someone left
}
}
// If Mary is logged in, the following callback will be triggered when Jerry leaves the conversation
-(void)conversation:(AVIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId{
NSLog(@"%@", [NSString stringWithFormat:@"%@ left the conversation; operated by %@",[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()} left');
};
Summary of Event Notifications Regarding Changes of Members
The sequence diagrams displayed earlier already described what would happen when certain events are triggered. The table below serves as a summary of them.
Assuming that Tom and Jerry are already in the conversation:
Operation | Tom | Jerry | Mary |
---|---|---|---|
Tom invites Mary | MEMBERS_JOINED | MEMBERS_JOINED | INVITED |
Tom removes Mary | MEMBERS_LEFT | MEMBERS_LEFT | KICKED |
William joins | MEMBERS_JOINED | MEMBERS_JOINED | / |
Jerry leaves | MEMBERS_LEFT | MEMBERS_LEFT | / |
Rich Media Messages
We've seen how we can send messages containing plain text. Now let's see how we can send rich media messages like images, videos, and locations.
By default LeanCloud supports text messages, files, images, audios, videos, locations, and binary data. All of them, except binary data, are sent as strings, though there are some slight differences between text messages and rich media messages (files, images, audios, and videos):
- When sending text messages, the messages themselves are sent directly as strings.
- When sending rich media messages (like images), the SDK will first upload the binary files to the cloud with LeanStorage's
AVFile
interface, then embed the URLs of them into the messages being sent. We can say that the essence of an image message is a text message holding the URL of the image.
Files stored on LeanStorage have CDN enabled by default. Therefore, binary data (like images) are not directly encoded as part of text messages. This helps users access them faster and the cost on you can be lowered at the same time.
Default Message Types
The following message types are offered by default:
TextMessage
Text messageImageMessage
Image messageAudioMessage
Audio messageVideoMessage
Video messageFileMessage
File message (.txt, .doc, .md, etc.)LocationMessage
Location message
All of them are derived from AVIMMessage
, with the following properties available for each:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
Name | Type | Description |
---|---|---|
content | NSString | The content of the message. |
clientId | NSString | The clientId of the sender. |
conversationId | NSString | The ID of the conversation. |
messageId | NSString | A unique ID for each message. Assigned by the cloud automatically. |
sendTimestamp | int64_t | The time the message is sent. Assigned by the cloud automatically. |
deliveredTimestamp | int64_t | The time the message is delivered. Assigned by the cloud automatically. |
status | A member of AVIMMessageStatus | The status of the message. Could be one of:AVIMMessageStatusNone (unknown)AVIMMessageStatusSending (sending)AVIMMessageStatusSent (sent)AVIMMessageStatusDelivered (delivered)AVIMMessageStatusFailed (failed) |
ioType | A member of AVIMMessageIOType | The direction of the message. Could be one of:AVIMMessageIOTypeIn (sent to the current user)AVIMMessageIOTypeOut (sent by the current user) |
Name | Type | Description |
---|---|---|
content | String | The content of the message. |
clientId | String | The clientId of the sender. |
conversationId | String | The ID of the conversation. |
messageId | String | A unique ID for each message. Assigned by the cloud automatically. |
timestamp | long | The time the message is sent. Assigned by the cloud automatically. |
receiptTimestamp | long | The time the message is delivered. Assigned by the cloud automatically. |
status | A member of AVIMMessageStatus | The status of the message. Could be one of:AVIMMessageStatusNone (unknown)AVIMMessageStatusSending (sending)AVIMMessageStatusSent (sent)AVIMMessageStatusReceipt (delivered)AVIMMessageStatusFailed (failed) |
ioType | A member of AVIMMessageIOType | The direction of the message. Could be one of:AVIMMessageIOTypeIn (sent to the current user)AVIMMessageIOTypeOut (sent by the current user) |
Name | Type | Description |
---|---|---|
content | String | The content of the message. |
clientId | String | The clientId of the sender. |
conversationId | String | The ID of the conversation. |
messageId | String | A unique ID for each message. Assigned by the cloud automatically. |
timestamp | long | The time the message is sent. Assigned by the cloud automatically. |
receiptTimestamp | long | The time the message is delivered. Assigned by the cloud automatically. |
status | A member of AVIMMessageStatus | The status of the message. Could be one of:AVIMMessageStatusNone (unknown)AVIMMessageStatusSending (sending)AVIMMessageStatusSent (sent)AVIMMessageStatusReceipt (delivered)AVIMMessageStatusFailed (failed) |
ioType | A member of AVIMMessageIOType | The direction of the message. Could be one of:AVIMMessageIOTypeIn (sent to the current user)AVIMMessageIOTypeOut (sent by the current user) |
Name | Type | Description |
---|---|---|
from | String | The clientId of the sender. |
cid | String | The ID of the conversation. |
id | String | A unique ID for each message. Assigned by the cloud automatically. |
timestamp | Date | The time the message is sent. Assigned by the cloud automatically. |
deliveredAt | Date | The time the message is delivered. Assigned by the cloud automatically. |
status | Symbol | The status of the message. Could be one of the members of [MessageStatus ](https://leancloud.github.io/js-realtime-sdk/docs/module-leancloud-realtime.html#.MessageStatus:MessageStatus.NONE (unknown)MessageStatus.SENDING (sending)MessageStatus.SENT (sent)MessageStatus.DELIVERED (delivered)MessageStatus.FAILED (failed) |
Name | Type | Description |
---|---|---|
content | IMMessage.Content | The content of the message. Could be String or Data . |
fromClientID | String | The clientId of the sender. |
currentClientID | String | The clientId of the receiver. |
conversationID | String | The ID of the conversation. |
ID | String | A unique ID for each message. Assigned by the cloud automatically. |
sentTimestamp | int64_t | The time the message is sent. Assigned by the cloud automatically. |
deliveredTimestamp | int64_t | The time the message is received. |
readTimestamp | int64_t | The time the message is read. |
patchedTimestamp | int64_t | The time the message is edited. |
isAllMembersMentioned | Bool | Whether all members are mentioned. |
mentionedMembers | [String] | A list of members being mentioned. |
isCurrentClientMentioned | Bool | Whether the current Client is mentioned. |
status | IMMessage.Status | The status of the message. Could be one of:none (unknown)sending (sending)sent (sent)delivered (delivered)read (read)failed (failed) |
ioType | IMMessage.IOType | The direction of the message. Could be one of:in (sent to the current user)out (sent by the current user) |
A number is assigned to each message type which can be used by your app to identify it. Negative numbers are for those defined by the SDK (see the table below) and positive ones are for your own types. 0
is reserved for untyped messages.
Message Type | Number |
---|---|
Text messages | -1 |
Image messages | -2 |
Audio messages | -3 |
Video messages | -4 |
Location messages | -5 |
File messages | -6 |
Image Messages
Sending Image Files
An image message can be constructed from either binary data or a local path. The diagram below shows the sequence of it:
Notes:
- The "Local" in the diagram could be
localStorage
orcamera
, meaning that the image could be either from the local storage of the phone (like iPhone's Camera Roll) or taken in real time with camera API. AVFile
is the file object used by LeanStorage. See AVFile for more details.
The diagram above may look complicated, but the code itself is quite simple since the image gets automatically uploaded when being sent with send
method:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var image = new AVFile("screenshot.png", "https://p.ssl.qhimg.com/dmfd/400_300_/t0120b2f23b554b8402.jpg");
// Save as AVFile object
await image.SaveAsync();
var imageMessage = new AVIMImageMessage();
imageMessage.File = image;
imageMessage.TextContent = "Sent via Windows.";
await conversation.SendMessageAsync(imageMessage);
AVFile file = AVFile.withAbsoluteLocalPath("San_Francisco.png", Environment.getExternalStorageDirectory() + "/San_Francisco.png");
// Create an image message
AVIMImageMessage m = new AVIMImageMessage(file);
m.setText("Sent via Android.");
conv.sendMessage(m, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (e == null) {
// Sent
}
}
});
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"LeanCloud.png"];
NSError *error;
AVFile *file = [AVFile fileWithLocalPath:imagePath error:&error];
AVIMImageMessage *message = [AVIMImageMessage messageWithText:@"She is sweet." file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Sent!");
}
}];
// ImageMessage and other rich media messages depends on LeanStorage service.
// Refer to SDK setup guide for details on how to import and initialize SDKs.
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('Sent via Ins.');
message.setAttributes({ location: 'San Francisco' });
return conversation.send(message);
}).then(function() {
console.log('Sent!');
}).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;
// Assuming there is an `assets` directory under project root,
// and this directory is included in pubspec.yaml.
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);
}
Sending Image URLs
Beside sending an image directly, a user may also copy the URL of an image from somewhere else and send it to a conversation:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var image = new AVFile("Satomi_Ishihara.gif", "http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif");
var imageMessage = new AVIMImageMessage();
imageMessage.File = image;
imageMessage.TextContent = "Sent via Windows.";
await conversation.SendMessageAsync(imageMessage);
AVFile file = new AVFile("cute-girl","http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif", null);
AVIMImageMessage m = new AVIMImageMessage(file);
m.setText("She is sweet.");
// Create an image message
conv.sendMessage(m, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (e == null) {
// Sent
}
}
});
// Tom sends an image to Jerry
AVFile *file = [AVFile fileWithURL:[self @"http://ww3.sinaimg.cn/bmiddle/596b0666gw1ed70eavm5tg20bq06m7wi.gif"]];
AVIMImageMessage *message = [AVIMImageMessage messageWithText:@"She is sweet." file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Sent!");
}
}];
var AV = require('leancloud-storage');
var { ImageMessage } = require('leancloud-realtime-plugin-typed-messages');
// Create an image message from URL
var file = new AV.File.withURL('cute-girl', 'http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif');
file.save().then(function() {
var message = new ImageMessage(file);
message.setText('She is sweet.');
return conversation.send(message);
}).then(function() {
console.log('Sent!');
}).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);
}
Receiving Image Messages
The way to receive image messages is similar to that for basic messages. The only thing that needs to be added is to have the callback function retrieve the image and render it on the UI. For example:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
private void OnMessageReceived(object sender, AVIMMessageEventArgs e)
{
if (e.Message is AVIMImageMessage imageMessage)
{
AVFile file = imageMessage.File;
Debug.Log(file.Url);
}
}
AVIMMessageManager.registerMessageHandler(AVIMImageMessage.class,
new AVIMTypedMessageHandler<AVIMImageMessage>() {
@Override
public void onMessage(AVIMImageMessage msg, AVIMConversation conv, AVIMClient client) {
// Only handle messages from Jerry
// sent to the conversation with conversationId 55117292e4b065f7ee9edd29
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:(AVIMConversation *)conversation didReceiveTypedMessage:(AVIMTypedMessage *)message {
AVIMImageMessage *imageMessage = (AVIMImageMessage *)message;
// The ID of the message
NSString *messageId = imageMessage.messageId;
// The URL of the image file
NSString *imageUrl = imageMessage.file.url;
// The clientId of the sender
NSString *fromClientId = message.clientId;
}
var { Event, TextMessage } = require('leancloud-realtime');
var { ImageMessage } = require('leancloud-realtime-plugin-typed-messages');
client.on(Event.MESSAGE, function messageEventHandler(message, conversation) {
var file;
switch (message.type) {
case ImageMessage.TYPE:
file = message.getFile();
console.log('Image received. 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('Received an image: ${message.url}');
}
};
Sending Audios, Videos, and Files
The Flow
The SDK follows the steps below to send images, audios, videos, and files:
When constructing files from data streams using client API:
- Construct a local
AVFile
- Upload the
AVFile
to the cloud and retrieve itsmetaData
- Embed the
objectId
, URL, and metadata of the file into the message - Send the message
When constructing files with URLs:
- Embed the URL into the message without metadata (like the length of audio) or
objectId
- Send the message
For example, when sending an audio message, the basic flow would be: read the audio file (or record a new one) > construct an audio message > send the message.
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var audio = new AVFile("never-gonna-give-you-up.mp3", Path.Combine(Application.persistentDataPath, "never-gonna-give-you-up.mp3"));
var audioMessage = new AVIMAudioMessage();
audioMessage.File = audio;
audioMessage.TextContent = "I heard this song became a meme.";
await conversation.SendMessageAsync(audioMessage);
AVFile file = AVFile.withAbsoluteLocalPath("never-gonna-give-you-up.mp3",localFilePath);
AVIMAudioMessage m = new AVIMAudioMessage(file);
m.setText("I heard this song became a meme.");
// Create an audio message
conv.sendMessage(m, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (e == null) {
// Sent
}
}
});
NSError *error = nil;
AVFile *file = [AVFile fileWithLocalPath:localPath error:&error];
if (!error) {
AVIMAudioMessage *message = [AVIMAudioMessage messageWithText:@"I heard this song became a meme." file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Sent!");
}
}];
}
var AV = require('leancloud-storage');
var { AudioMessage } = require('leancloud-realtime-plugin-typed-messages');
var fileUploadControl = $('#musicFileUpload')[0];
var file = new AV.File('never-gonna-give-you-up.mp3', fileUploadControl.files[0]);
file.save().then(function() {
var message = new AudioMessage(file);
message.setText('I heard this song became a meme.');
return conversation.send(message);
}).then(function() {
console.log('Sent!');
}).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 = "I heard this song became a meme."
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;
ByteData audioData = await rootBundle.load('assets/test.mp3');
AudioMessage audioMessage = AudioMessage.from(
binaryData: audioData.buffer.asUint8List(),
format: 'mp3',
);
audioMessage.text = 'I heard this song became a meme.';
try {
await conversation.send(message: audioMessage);
} catch (e) {
print(e);
}
Similar to image messages, you can construct audio messages from URLs as well:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var audio = new AVFile("apple.aac", "https://some.website.com/apple.aac");
var audioMessage = new AVIMAudioMessage();
audioMessage.File = audio;
audioMessage.TextContent = "Here is the recording from Apple Special Event.";
await conversation.SendMessageAsync(audioMessage);
AVFile file = new AVFile("apple.aac", "https://some.website.com/apple.aac", null);
AVIMAudioMessage m = new AVIMAudioMessage(file);
m.setText("Here is the recording from Apple Special Event.");
conv.sendMessage(m, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (e == null) {
// Sent
}
}
});
AVFile *file = [AVFile fileWithRemoteURL:[NSURL URLWithString:@"https://some.website.com/apple.aac"]];
AVIMAudioMessage *message = [AVIMAudioMessage messageWithText:@"Here is the recording from Apple Special Event." file:file attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Sent!");
}
}];
var AV = require('leancloud-storage');
var { AudioMessage } = require('leancloud-realtime-plugin-typed-messages');
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('Here is the recording from Apple Special Event.');
return conversation.send(message);
}).then(function() {
console.log('Sent!');
}).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 = "Here is the recording from Apple Special Event."
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);
}
Sending Location Messages
The code below sends a message containing a location:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
var locationMessage = new AVIMLocationMessage();
locationMessage.Location = new AVGeoPoint(31.3753285, 120.9664658);
await conversation.SendMessageAsync(locationMessage);
final AVIMLocationMessage locationMessage = new AVIMLocationMessage();
// The location here is hardcoded for demonstration; you can get actual locations with the API offered by the device
locationMessage.setLocation(new AVGeoPoint(31.3753285,120.9664658));
locationMessage.setText("Here is the location of the bakery.");
conversation.sendMessage(locationMessage, new AVIMConversationCallback() {
@Override
public void done(AVIMException e) {
if (null != e) {
e.printStackTrace();
} else {
// Sent
}
}
});
AVIMLocationMessage *message = [AVIMLocationMessage messageWithText:@"Here is the location of the bakery." latitude:31.3753285 longitude:120.9664658 attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Sent!");
}
}];
var AV = require('leancloud-storage');
var { LocationMessage } = require('leancloud-realtime-plugin-typed-messages');
var location = new AV.GeoPoint(31.3753285, 120.9664658);
var message = new LocationMessage(location);
message.setText('Here is the location of the bakery.');
conversation.send(message).then(function() {
console.log('Sent!');
}).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);
}
Custom Attributes
A Conversation
object holds some built-in properties which match the fields in the _Conversation
table. The table below shows these built-in properties:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
Property of AVIMConversation | Field in _Conversation | Description |
---|---|---|
CurrentClient | N/A | The Client the conversation belongs to. |
ConversationId | objectId | A globally unique ID. |
Name | name | The name of the conversation. Shared by all members. |
MemberIds | m | The list of members. |
MuteMemberIds | mu | The list of members that muted the conversation. |
Creator | c | The creator of the conversation. |
IsTransient | tr | Whether it is a chat room. |
IsSystem | sys | Whether it is a system conversation. |
IsUnique | unique | If this is true , the same conversation will be reused when a new conversation is created with the same composition of members and unique to be true . |
IsTemporary | N/A | Whether it is a temporary conversation that will not be saved in the _Conversation class. |
CreatedAt | createdAt | The time the conversation is created. |
UpdatedAt | updatedAt | The time the conversation is updated. |
LastMessageAt | lm | The time the last message is sent. |
Getters of AVIMConversation | Field in _Conversation | Description |
---|---|---|
getAttributes | attr | Custom attributes. |
getConversationId | objectId | A globally unique ID. |
getCreatedAt | createdAt | The time the conversation is created. |
getCreator | c | The creator of the conversation. |
getLastDeliveredAt | N/A | The time the last message being delivered is sent (for one-on-one chatting only). |
getLastMessage | N/A | The last message. Could be empty. |
getLastMessageAt | lm | The time the last message is sent. |
getLastReadAt | N/A | The time the last message being read is sent (for one-on-one chatting only). |
getMembers | m | The list of members. |
getName | name | The name of the conversation. Shared by all members. |
getTemporaryExpiredat | N/A | Time to live (applicable for temporary conversations only). |
getUniqueId | uniqueId | A globally unique ID for Unique Conversation . |
getUnreadMessagesCount | N/A | The number of unread messages. |
getUpdatedAt | updatedAt | The time the conversation is updated. |
isSystem | sys | Whether it is a system conversation. |
isTemporary | N/A | Whether it is a temporary conversation that will not be saved in the _Conversation class. |
isTransient | tr | Whether it is a chat room. |
isUnique | unique | Whether it is a Unique Conversation . |
Property of AVIMConversation | Field in _Conversation | Description |
---|---|---|
clientID | N/A | The ID of the Client the conversation belongs to. |
conversationId | objectId | A globally unique ID. |
creator | c | The creator of the conversation. |
createdAt | createdAt | The time the conversation is created. |
updatedAt | updatedAt | The time the conversation is updated. |
lastMessage | N/A | The last message. Could be empty. |
lastMessageAt | lm | The time the last message is sent. |
lastReadAt | N/A | The time the last message being read is sent (for one-on-one chatting only). |
lastDeliveredAt | N/A | The time the last message being delivered is sent (for one-on-one chatting only). |
unreadMessagesCount | N/A | The number of unread messages. |
unreadMessageContainMention | N/A | Whether the conversation mentioned the current client. |
name | name | The name of the conversation. Shared by all members. |
members | m | The list of members. |
attributes | attr | Custom attributes. |
uniqueID | uniqueId | A globally unique ID for Unique Conversation . |
unique | unique | Whether it is a Unique Conversation . |
transient | tr | Whether it is a chat room. |
system | sys | Whether it is a system conversation. |
temporary | N/A | Whether it is a temporary conversation that will not be saved in the _Conversation class. |
temporaryTTL | N/A | Time to live (applicable for temporary conversations only). |
muted | N/A | Whether the current user muted the conversation. |
imClient | N/A | The AVIMClient the conversation belongs to. |
Property of Conversation | Field in _Conversation | Description |
---|---|---|
createdAt | createdAt | The time the conversation is created. |
creator | c | The creator of the conversation. |
id | objectId | A globally unique ID. |
lastDeliveredAt | N/A | The time the last message being delivered is sent (for one-on-one chatting only). |
lastMessage | N/A | The last message. Could be empty. |
lastMessageAt | lm | The time the last message is sent. |
lastReadAt | N/A | The time the last message being read is sent (for one-on-one chatting only). |
members | m | The list of members. |
muted | N/A | Whether the current user muted the conversation. |
mutedMembers | mu | The list of members that muted the conversation. |
name | name | The name of the conversation. Shared by all members. |
system | sys | Whether it is a system conversation. |
transient | tr | Whether it is a chat room. |
unreadMessagesCount | N/A | The number of unread messages. |
updatedAt | updatedAt | The time the conversation is updated. |
Property of IMConversation | Field in _Conversation | Description |
---|---|---|
client | N/A | The Client the conversation belongs to. |
ID | objectId | A globally unique ID . |
clientID | N/A | The ID of the Client the conversation belongs to. |
isUnique | unique | Whether it is a Unique Conversation . |
uniqueID | uniqueId | A globally unique ID for Unique Conversation . |
name | name | The name of the conversation. |
creator | c | The creator of the conversation. |
createdAt | createdAt | The time the conversation is created. |
updatedAt | updatedAt | The time the conversation is updated. |
attributes | attr | Custom attributes. |
members | m | The list of members. |
isMuted | N/A | Whether the current user muted the conversation. |
isOutdated | N/A | Whether the properties of the conversation are outdated. Can be used to determine if the data of the conversation needs to be updated. |
lastMessage | N/A | The last message. Could be empty. |
unreadMessageCount | N/A | The number of unread messages. |
isUnreadMessageContainMention | N/A | Whether an unread message mentions the current Client . |
memberInfoTable | N/A | A table of member information. |
Property of Conversation | Field in _Conversation | Description |
---|---|---|
attributes | attr | Custom attributes. |
client | N/A | The Client the conversation belongs to. |
createdAt | createdAt | The time the conversation is created. |
creator | c | The creator of the conversation. |
id | objectId | A globally unique ID . |
isMuted | N/A | Whether the current user muted the conversation. |
isUnique | unique | Whether it is a Unique Conversation . |
lastDeliveredAt . | N/A | The time the last message being delivered is sent (for one-on-one chatting only). |
lastMessage | N/A | The last message. Could be empty. |
lastReadAt | N/A | The time the last message being read is sent (for one-on-one chatting only). |
members | m | The list of members. |
name | name | The name of the conversation. |
uniqueID | uniqueId | A globally unique ID for Unique Conversation . |
unreadMessageCount | N/A | The number of unread messages. |
unreadMessagesMentioned | N/A | Whether an unread message mentions the current Client . |
updatedAt | updatedAt | The time the conversation is updated. |
However, direct write operations on the _Conversation
table are frowned upon:
- The conversation queries sent by client-side SDKs in websocket connections will first reach the LeanMessage server's in-memory cache. Direct write operations on the
_Conversation
table will not update the cache, which may cause cache inconsistency. - With direct write operations on the
_Conversation
table, the LeanMessage server has no chance to notify the client-side. Thus the client-side will not receive any corresponding events. - If LeanMessage hooks are defined, direct write operations on the
_Conversation
table will not trigger them.
For administrative tasks, the dedicated LeanMessage REST API interface is recommended.
Beside these built-in properties, you can also define your custom attributes to store more data with each conversation.
Creating Custom Attributes
When introducing one-on-one conversations, we mentioned that IMClient#createConversation
allows you to attach custom attributes to a conversation. Now let's see how we can do that.
Assume that we need to add two properties { "type": "private", "pinned": true }
to a conversation we are creating. We can do so by passing in the properties when calling IMClient#createConversation
:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
vars options = new Dictionary<string, object>();
options.Add("type", "private");
options.Add("pinned",true);
var conversation = await tom.CreateConversationAsync("Jerry", name:"Tom & Jerry", isUnique:true, options:options);
HashMap<String,Object> attr = new HashMap<String,Object>();
attr.put("type","private");
attr.put("pinned",true);
client.createConversation(Arrays.asList("Jerry"),"Tom & Jerry", attr, false, true,
new AVIMConversationCreatedCallback(){
@Override
public void done(AVIMConversation conv,AVIMException e){
if(e==null){
// Conversation created
}
}
});
// Tom creates a conversation named "Tom & Jerry" and attaches custom attributes to it
NSDictionary *attributes = @{
@"type": @"private",
@"pinned": @(YES)
};
[tom createConversationWithName:@"Tom & Jerry" clientIds:@[@"Jerry"] attributes:attributes options:AVIMConversationOptionUnique callback:^(AVIMConversation *conversation, NSError *error) {
if (succeeded) {
NSLog(@"Conversation created!");
}
}];
tom.createConversation({
members: ['Jerry'],
name: 'Tom & Jerry',
unique: true,
type: 'private',
pinned: true,
}).then(function(conversation) {
console.log('Conversation created! ID: ' + conversation.id);
}).catch(console.error.bind(console));
do {
try tom.createConversation(clientIDs: ["Jerry"], name: "Tom & Jerry", attributes: ["type": "private", "pinned": true], 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(
members: {'client1.id', 'client2.id'},
attributes: {
'members': ['Jerry'],
'name': 'Tom & Jerry',
'unique': true,
'type': 'private',
'pinned': true,
},
);
} catch (e) {
print(e);
}
The SDK allows everyone in a conversation to access its custom attributes. You can even query conversations that satisfy certain attributes. See Querying Conversations with Custom Conditions.
Updating and Retrieving Properties
The built-in properties (like name
) of a Conversation
object can be updated by all the members unless you set restrictions in your app:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
conversation.Name = "Tom is Smart";
await conversation.SaveAsync();
AVIMConversation conversation = client.getConversation("55117292e4b065f7ee9edd29");
conversation.setName("Tom is Smart");
conversation.updateInfoInBackground(new AVIMConversationCallback(){
@Override
public void done(AVIMException e){
if(e==null){
// Updated
}
}
});
conversation[@"name"] = @"Tom is Smart";
[conversation updateWithCallback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Updated!");
}
}];
conversation.name = 'Tom is Smart';
conversation.save();
do {
try conversation.update(attribution: ["name": "Tom is Smart"], completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
} catch {
print(error)
}
try {
await conversation.updateInfo(attributes: {
'name': 'Tom is Smart',
});
} catch (e) {
print(e);
}
Custom attributes can also be retrieved or updated by all the members:
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
// Retrieve custom attribute
var type = conversation["attr.type"];
// Set new value for pinned
conversation["attr.pinned"] = false;
// Save
await conversation.SaveAsync();
// Retrieve custom attribute
String type = conversation.get("attr.type");
// Set new value for pinned
conversation.set("attr.pinned",false);
// Save
conversation.updateInfoInBackground(new AVIMConversationCallback(){
@Override
public void done(AVIMException e){
if(e==null){
// Saved
}
}
});
// Retrieve custom attribute
NSString *type = conversation.attributes[@"type"];
// Set new value for pinned
[conversation setObject:@(NO) forKey:@"attr.pinned"];
// Save
[conversation updateWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"Saved!");
}
}];
// Retrieve custom attribute
var type = conversation.get('attr.type');
// Set new value for pinned
conversation.set('attr.pinned',false);
// Save
conversation.save();
do {
let type = conversation.attributes?["type"] as? String
try conversation.update(attribution: ["attr.pinned": false]) { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
try {
// Retrieve custom attribute
String type = conversation.attributes['type'];
// Set new value for pinned
await conversation.updateInfo(attributes: {
'pinned': false,
});
} catch (e) {
print(e);
}
Notes about custom attributes:
The custom attributes specified with
IMClient#createConversation
will be stored in the fieldattr
of the_Conversation
table. If you need to retrieve or update them later, the full path needs to be specified, likeattr.type
.
Synchronization of Properties
The properties of a conversation (like name) are shared by everyone in it. If someone ever changes a property, other members need to get updated on it. In the example we used earlier, a user changed the name of a conversation to "Tom is Smart". How would other members get to know about it?
LeanMessage offers the mechanism that automatically delivers the change made by a user to a conversation to all the members in it (for those who are offline, they will receive updates once they get online):
- C#
- Java
- Objective-C
- JavaScript
- Swift
- Flutter
// Not supported yet
// The following definition exists in AVIMConversationEventHandler
/**
* The properties of a conversation are updated
*
* @param client
* @param conversation
* @param attr The properties being updated
* @param operator The ID of the operator
*/
public void onInfoChanged(AVIMClient client, AVIMConversation conversation, JSONObject attr,
String operator)
/**
The properties of a conversation are updated
@param conversation The conversation
@param date The time of the update
@param clientId The ID of the operator
@param data The data being updated
*/
- (void)conversation:(AVIMConversation *)conversation didUpdateAt:(NSDate * _Nullable)date byClientId:(NSString * _Nullable)clientId updatedData:(NSDictionary * _Nullable)data;
/**
* The properties of a conversation are updated
* @event IMClient#CONVERSATION_INFO_UPDATED
* @param {Object} payload
* @param {Object} payload.attributes The properties being updated
* @param {String} payload.updatedBy The ID of the operator
*/
var { Event } = require('leancloud-realtime');
client.on(Event.CONVERSATION_INFO_UPDATED, function(payload) {
});
func client(_ client: IMClient, conversation: IMConversation, event: IMConversationEvent) {
switch event {
case let .dataUpdated(updatingData: updatingData, updatedData: updatedData, byClientID: byClientID, at: atDate):
print(updatingData)
print(updatedData<