Skip to main content

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:

LCIMClient tom = new LCIMClient("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:

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:

var user = await LCUser.Login("USER_NAME", "PASSWORD");
var client = new LCIMClient(user);

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:

var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true);

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:

/// <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);
}

Although SDKs for different languages/platforms share different interfaces, they take in the similar set of parameters when creating a conversation:

  1. members: Required; includes the initial list of members in the conversation. The initiator of the conversation is included by default, so members does not have to include the clientId of the current user.
  2. name: The name of the conversation; optional. The code above puts "Tom & Jerry" for it.
  3. 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 with AVIMConversation. Such attributes will be stored in the attr field of the _Conversation table.
  4. unique/isUnique or AVIMConversationOptionUnique: 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 be true when creating conversations.
  5. Other parameters specifying the type of the conversation; optional. For example, transient/isTransient specifies if it is a chat room, and tempConv/tempConvTTL or AVIMConversationOptionTemporary 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:

var textMessage = new LCIMTextMessage("Get up, Jerry!");
await conversation.Send(textMessage);

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):

var jerry = new LCIMClient("Jerry");

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:

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
}
};
With the two event handling functions above, Jerry will be able to receive messages from Tom. Jerry can send messages to Tom as well, as long as Tom has the same functions on his side.

Now let's take a look at the sequence diagram showing how the first message sent from Tom to Jerry is processed:

sequenceDiagram Tom->>Cloud: 1. Tom adds Jerry into the conversation Cloud-->>Jerry: 2. Sends notification: you are invited to the conversatio Jerry-->>UI: 3.. Loads UI Tom->>Cloud: 4. Sends message Cloud-->>Jerry: 5. Sends notification: you have a new message Jerry-->>UI: 6. Shows the message

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:

// Get the conversation with ID
var conversation = await tom.GetConversation("CONVERSATION_ID");
// Invite Mary
await conversation.AddMembers(new string[] { "Mary" });

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:

jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话");
}

AVIMOnInvitedEventArgs contains the following fields:

  1. InvitedBy: The inviter
  2. JoinedMembers: The list of members being added
  3. ConversationId: The conversation

Here is the sequence diagram of the operation:

sequenceDiagram Tom->>Cloud: 1. Adds Mary Cloud->>Tom: 2. Sends notification: you invited Mary to the conversation Cloud-->>Mary: 2. Sends notification: you are added to the conversation by Tom Cloud-->>Jerry: 2. Sends notification: Mary is added to the conversation by Tom

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:

var conversation = await tom.CreateConversationAsync(new string[]{ "Jerry","Mary" }, name:"Tom & Jerry & friends", isUnique:true);

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:

var textMessage = new AVIMTextMessage("Welcome everyone!");
await conversation.SendMessageAsync(textMessage);

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?

await conversation.RemoveMembersAsync("Mary");

The following process will be triggered:

sequenceDiagram Tom->>Cloud: 1. Removes Mary Cloud-->>Mary: 2. Send notification: You are removed by Tom Cloud-->>Jerry: 2. Send notification: Mary is removed by Tom Cloud-->>Tom: 2. Send notification: Mary is removed

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:

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;

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:

await william.JoinAsync("CONVERSATION_ID");

The following process will be triggered:

sequenceDiagram William->Cloud: 1. Joins the conversations Cloud-->William: 2. Sends notification: you joined the conversation Cloud-->Tom: 2. Sends notification: William joined the conversation Cloud-->Jerry: 2. Sends notification: William joined the conversation

Other members can listen to MEMBERS_JOINED to know that William joined the conversation:

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;

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:

```cs await jerry.LeaveAsync(conversation); ```

After leaving the conversation, Jerry will no longer receive messages from it. Here is the sequence diagram of the operation:

sequenceDiagram Jerry->Cloud: 1. Leaves the conversation Cloud-->Jerry: 2. Sends notification: You left the conversation Cloud-->Mary: 2. Sends notification: Jerry left the conversation Cloud-->Tom: 2. Sends notification: Jerry left the conversation

Other members can listen to MEMBERS_LEFT to know that Jerry left the conversation:

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));
}

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:

OperationTomJerryMary
Tom invites MaryMEMBERS_JOINEDMEMBERS_JOINEDINVITED
Tom removes MaryMEMBERS_LEFTMEMBERS_LEFTKICKED
William joinsMEMBERS_JOINEDMEMBERS_JOINED/
Jerry leavesMEMBERS_LEFTMEMBERS_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 message
  • ImageMessage Image message
  • AudioMessage Audio message
  • VideoMessage Video message
  • FileMessage File message (.txt, .doc, .md, etc.)
  • LocationMessage Location message

All of them are derived from AVIMMessage, with the following properties available for each:

NameTypeDescription
contentNSStringThe content of the message.
clientIdNSStringThe clientId of the sender.
conversationIdNSStringThe ID of the conversation.
messageIdNSStringA unique ID for each message. Assigned by the cloud automatically.
sendTimestampint64_tThe time the message is sent. Assigned by the cloud automatically.
deliveredTimestampint64_tThe time the message is delivered. Assigned by the cloud automatically.
statusA member of AVIMMessageStatusThe status of the message. Could be one of:

AVIMMessageStatusNone (unknown)
AVIMMessageStatusSending (sending)
AVIMMessageStatusSent (sent)
AVIMMessageStatusDelivered (delivered)
AVIMMessageStatusFailed (failed)
ioTypeA member of AVIMMessageIOTypeThe direction of the message. Could be one of:

AVIMMessageIOTypeIn (sent to the current user)
AVIMMessageIOTypeOut (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 TypeNumber
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:

sequenceDiagram Tom-->Local: 1. Get the content of the image Tom-->Storage: 2. The SDK uploads the file (AVFile) to the cloud Storage-->Tom: 3. Return the URL of the image Tom-->Cloud: 4. The SDK sends the image message to the cloud Cloud->Jerry: 5. Receive the image message and display that in UI

Notes:

  1. The "Local" in the diagram could be localStorage or camera, 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.
  2. 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:

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);

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:

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);

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:

private void OnMessageReceived(object sender, AVIMMessageEventArgs e)
{
if (e.Message is AVIMImageMessage imageMessage)
{
AVFile file = imageMessage.File;
Debug.Log(file.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:

  1. Construct a local AVFile
  2. Upload the AVFile to the cloud and retrieve its metaData
  3. Embed the objectId, URL, and metadata of the file into the message
  4. Send the message

When constructing files with URLs:

  1. Embed the URL into the message without metadata (like the length of audio) or objectId
  2. 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.

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);

Similar to image messages, you can construct audio messages from URLs as well:

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);

Sending Location Messages

The code below sends a message containing a location:

var locationMessage = new AVIMLocationMessage();
locationMessage.Location = new AVGeoPoint(31.3753285, 120.9664658);
await conversation.SendMessageAsync(locationMessage);

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:

Property of AVIMConversationField in _ConversationDescription
CurrentClientN/AThe Client the conversation belongs to.
ConversationIdobjectIdA globally unique ID.
NamenameThe name of the conversation. Shared by all members.
MemberIdsmThe list of members.
MuteMemberIdsmuThe list of members that muted the conversation.
CreatorcThe creator of the conversation.
IsTransienttrWhether it is a chat room.
IsSystemsysWhether it is a system conversation.
IsUniqueuniqueIf 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.
IsTemporaryN/AWhether it is a temporary conversation that will not be saved in the _Conversation class.
CreatedAtcreatedAtThe time the conversation is created.
UpdatedAtupdatedAtThe time the conversation is updated.
LastMessageAtlmThe time the last message is sent.

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:

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);

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:

conversation.Name = "Tom is Smart";
await conversation.SaveAsync();

Custom attributes can also be retrieved or updated by all the members:

// Retrieve custom attribute
var type = conversation["attr.type"];
// Set new value for pinned
conversation["attr.pinned"] = false;
// Save
await conversation.SaveAsync();

Notes about custom attributes:

The custom attributes specified with IMClient#createConversation will be stored in the field attr of the _Conversation table. If you need to retrieve or update them later, the full path needs to be specified, like attr.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):

// Not supported yet

Notes:

You can either retrieve the properties being updated from the callback function or directly read the latest values from the Conversation object.

Retrieving Member Lists

To get the list of members in a conversation, we can call the method for fetching on a Conversation object and then get the result from it:

// Not supported yet

Notes:

You can only get member lists of basic conversations. Chat rooms and system conversations don't have member lists.

Querying Conversations with Custom Conditions

There are more ways to get a Conversation beside listening to incoming events. You might want your users to search chat rooms by the names or locations of them, or to look for conversations that has certain members in them. All these requirements can be satisfied with the help of queries.

Queries on ID

Here ID refers to the objectId in the _Conversation table. Since IDs are indexed, querying by ID is the easiest and most efficient way to look for a conversation:

var query = tom.GetQuery();
var conversation = await query.GetAsync("551260efe4b01608686c3e0f");

Querying by Conditions

LeanMessage offers a variety of ways for you to look for conversations that satisfy certain conditions.

Let's start with equalTo which is the simplest method for querying conversations. The code below looks for all the conversations that have type (a string field) to be private:

// Since WhereXXX returns a new query instance each time, the code below will not work:
// var query = tom.GetQuery();
// query.WhereEqualTo("attr.type","private");
// You can use this way:
// var query = tom.GetQuery();
// query = query.WhereEqualTo("attr.type","private");
// This way is more recommended:
// var query = tom.GetQuery().WhereEqualTo("attr.type","private");
var query = tom.GetQuery().WhereEqualTo("attr.type","private");
await query.FindAsync();

The interface for querying conversations is very similar to that for querying objects in LeanStorage. If you're already familiar with LeanStorage, it shouldn't be hard for you to learn how to query conversations:

  • You can get query results with find
  • You can get number of results with count
  • You can get the first conversation satisfying conditions with first
  • You can implement pagination with skip and limit

You can also apply conditions like "greater than", "greater than or equal to", "less than", and "less than or equal to" to Number and Date fields:

LogicAVIMConversationQuery Method
Equal toWhereEqualTo
Not equal toWhereNotEqualsTo
Greater thanWhereGreaterThan
Greater than or equal toWhereGreaterThanOrEqualsTo
Less thanWhereLessThan
Less than or equal toWhereLessThanOrEqualsTo

Notes about default query conditions:

When querying conversations, if there isn't any where condition specified, ConversationQuery will look for conversations containing the current user by default. Such condition will be dismissed if any where condition is applied to the query. If you want to look for conversations containing certain clientId, you can follow the way introduced in Queries on Array Values to perform queries on m with the value of clientId. This won't cause any conflict with the default condition.

Using Regular Expressions

You can use regular expressions as conditions when querying with ConversationsQuery. For example, to look for all the conversations that have language to be Chinese:

query.WhereMatches("language","[\\u4e00-\\u9fa5]"); // language is Chinese characters

Queries on String Values

You can look for conversations with string values that start with a particular string, which is similar to LIKE 'keyword%' in SQL. For example, to look for all conversations with names starting with education:

query.WhereStartsWith("name","education");

You can also look for conversations with string values that include a particular string, which is similar to LIKE '%keyword%' in SQL. For example, to look for all conversations with names including education:

query.WhereContains("name","education");

If you want to look for conversations with string values that exclude a particular string, you can use regular expressions. For example, to look for all conversations with names excluding education:

query.WhereMatches("name","^((?!education).)* $ ");

Queries on Array Values

You can use containsAll, containedIn, and notContainedIn to perform queries on array values. For example, to look for all conversations containing Tom:

List<string> members = new List<string>();
members.Add("Tom");
query.WhereContainedIn("m", members);

Queries on Existence

You can look for conversations with or without certain fields to be empty. For example, to look for all conversations with lm to be empty:

query.WhereDoesNotExist("lm");

Or, to look for all conversations with lm not to be empty:

query.WhereExists("lm");

Compound Queries

To look for all conversations with age to be less than 18 and keywords containing education:

query.WhereContains("keywords", "'education'").WhereLessThan("age", 18);

You can also connect two queries with and or or to form a new query.

For example, to look for all conversations that either has age to be less than 18 or has keywords containing education:

var ageQuery = tom.GetQuery().WhereLessThan('age', 18);

var keywordsQuery = tom.GetQuery().WhereContains('keywords', 'education').

var query = AVIMConversationQuery.or(new AVIMConversationQuery[] { ageQuery, keywordsQuery});

Sorting

You can sort the results of a query by ascending or descending order on certain fields. For example:

// Not supported yet

Excluding Member Lists from Results

When searching conversations, you can exclude the lists of members from query results if you don't need them. By doing so, their members fields will become empty arrays. This helps you improve the speed of your app and reduces the bandwidth needed.

// Not supported yet

Including Latest Messages in Results

Many chatting apps show the latest messages of conversations together in a list. If you want the similar function in your app, you can turn on the option when querying conversations:

// Not supported yet

Keep in mind that what this option really does is to refresh the latest messages of conversations. Due to the existence of cache, it is still possible for you to retrieve the outdated "latest messages" even though you set the option to be false.

Caching Results

Optimizing Performance

Since Conversation objects are stored on LeanStorage, you can make use of indexes to improve the efficiency of querying, just like how you would do to other classes. Here are some suggestions for optimizing performance:

  • By default, indexes are created for objectId, updatedAt, and createdAt of Conversation, so querying by these fields would be naturally fast.
  • Although it's possible to implement pagination with skip and limit, the speed would slow down when the dataset grows larger. It would be more efficient to make use of updatedAt or lastMessageAt instead.
  • When searching for conversations containing a certain user by using contains on m, it's recommended that you stick to the default limit (which is 10) and make use of updatedAt or lastMessageAt for pagination.
  • If you app has too many conversations, consider creating a cloud function that periodically cleans up inactive conversations.

Retrieving Messages

By default, message histories are stored on the cloud for 180 days. You may either pay to extend the period (contact us at leancloud-support@xd.com) or synchronize them to your own server with REST API.

Our SDKs offer various of ways for you to retrieve message histories. iOS and Android SDKs also provide caching mechanism to help you reduce the number of queries you have to perform and display message histories to users even their devices are offline.

Retrieving Messages Chronologically (New to Old)

The most common way to retrieve messages is to fetch them from new to old with the help of pagination:

// limit could be any number from 1 to 100 (defaults to 20)
var messages = await conversation.QueryMessageAsync(limit: 10);
foreach (var message in messages)
{
if (message is AVIMTextMessage)
{
var textMessage = (AVIMTextMessage)message;
}
}

Here queryMessage supports pagination. Given the fact that you can locate a single message with its messageId and timestamp, this means that you can retrieve the next few messages after a given message by providing the messageId and timestamp of that message:

// limit could be any number from 1 to 1000 (defaults to 100)
var messages = await conversation.QueryMessageAsync(limit: 10);
var oldestMessage = messages.ToList()[0];
var messagesInPage = await conversation.QueryMessageAsync(beforeMessageId: oldestMessage.Id, beforeTimeStamp: oldestMessage.ServerTimestamp);

Retrieving Messages by Types

Beside retrieving messages in time orders, you can also do that based on the types of messages. This could be helpful in scenarios like displaying all the images in a conversation.

queryMessage can take in the type of messages:

// Pass in a generic type parameter and the SDK will automatically read the type and send it to the server for searching messages
var imageMessages = await conversation.QueryMessageAsync<AVIMImageMessage>();

To retrieve more images, follow the way introduced in the previous section to go through different pages.

Retrieving Messages Chronologically (Old to New)

Beside the two ways mentioned above, you can also retrieve messages from old to new. The code below shows how you can retrieve messages starting from the time the conversation is created:

var earliestMessages = await conversation.QueryMessageAsync(direction: 0);

It is a bit more complicated to implement pagination with this method. See the next section for more explanations.

Retrieving Messages Chronologically (From a Timestamp to a Direction)

You can retrieve messages starting from a given message (determined by ID and timestamp) toward a certain direction:

  • New to old: Retrieve messages sent before a given message
  • Old to new: Retrieve messages sent after a given message

Now we can implement pagination on different directions.

var earliestMessages = await conversation.QueryMessageAsync(direction: 0, limit: 1);
// Get messages sent after earliestMessages.Last()
var nextPageMessages = await conversation.QueryMessageAfterAsync(earliestMessages.Last());

Retrieving Messages Within a Period of Time

Beside retrieving messages chronologically, you can also retrieve messages within a period of time. For example, if you already have two messages, you can have one of them to be the starting point and another one to be the ending point to retrieve all the messages between them:

Note: The limit of 100 messages per query still applies here. To fetch more messages, keep changing the starting point or the ending point until all the messages are retrieved.

var earliestMessage = await conversation.QueryMessageAsync(direction: 0, limit: 1);
var latestMessage = await conversation.QueryMessageAsync(limit: 1);
// messagesInInterval can get at most 100 messages
var messagesInInterval = await conversation.QueryMessageInIntervalAsync(earliestMessage.FirstOrDefault(), latestMessage.FirstOrDefault());

Caching Messages

iOS and Android SDKs come with the mechanism that automatically caches all the messages received and retrieved on the local device (not supported by JavaScript or C# SDKs). It provides the following benefits:

  1. Message histories can be viewed even devices are offline
  2. The frequency of querying and the consumption of data can be minimized
  3. The speed for viewing messages can be increased

Caching is enabled by default. You can turn it off with the following interface:

// Not supported yet

Logging out and Network Changes

Logging out

If your app allows users to log out, you can use the close method provided by AVIMClient to properly close the connection to the cloud:

await tom.CloseAsync();

After the function is called, the connection between the client and the server will be terminated. If you check the status of the corresponding clientId on the cloud, it would show as "offline".

Network Changes

The availability of the messaging service is highly dependent on the Internet connection. If the connection is lost, all the operations regarding messages and conversations will fail. At this time, there needs to be some indicators on the UI to tell users about the network status.

Our SDKs maintain a heartbeat mechanism with the cloud which detects the change of network status and have your app notified if certain events occur. To be specific, if the connection status changes (becomes lost or recovered), the following events will be populated:

The following events will be populated on `AVRealtime`:
  • OnDisconnected occurs when the connection is lost. The messaging service is unavailable at this time.
  • OnReconnecting occurs when trying to reconnect. The messaging service is still unavailable at this time.
  • OnReconnected occurs when the connection is recovered. The messaging service is available at this time.
  • OnReconnectFailed occurs when it fails to reconnect. The messaging service is unavailable at this time.

More Suggestions

Sorting Conversations by Last Activities

In many scenarios you may need to sort conversations based on the time the last message in each of them is sent. Here we offer a property lastMessageAt for each AVIMConversation (lm in the _Conversation table) which dynamically changes to reflect the time of the last message. The time is server-based (accurate to a second) so you don't have to worry about the time on the clients. AVIMConversation also offers a method for you to retrieve the last message of each conversation, which gives you more flexibility to design the UI of your app.

Auto Reconnecting

If the connection between a client and the cloud is not properly closed, our iOS and Android SDKs will automatically reconnect when the network is recovered. You can listen to IMClient to get updated about the network status.

More Conversation Types

Beside the one-on-one chatting and group chats mentioned earlier, LeanMessage also supports these types of conversations:

  • Chat room: This can be used to build conversations that serve scenarios like live streaming. It's different than a basic group chat on the number of members supported and the deliverability promised. See 3. Chat Rooms for more details.
  • Temporary conversation: This can be used to build conversations between users and customer service representatives. It's different than a basic one-on-one chatting on the fact that it has a shorter TTL which brings higher flexibility and lower cost (on data storage). See 3. Temporary Conversations for more details.
  • System conversation: This can be used to build accounts that could broadcast messages to all their subscribers. It's different than a basic group chat on the fact that users can subscribe to it and there isn't a number limit of members. Subscribers can also send one-on-one messages to these accounts and these messages won't be seen by other users. See 4. System Conversations for more details.

Continue Reading

2. Advanced Messaging Features, Push Notifications, Synchronization, and Multi Device Sign-on

3. Security, Permission Management, Chat Rooms, and Temporary Conversations

4. Hooks and System Conversations