@nealcaffrey
2015-03-24T15:04:11.000000Z
字数 15407
阅读 512
在阅读本开发指南之前,请先阅读下《实时通信开发指南(v2)》,了解实时通信的基本概念和模型。
目前我们的实时通信服务仅支持 Windows Phone Silverlight 运行时,支持微软新一代的全平台统一运行时的 LeanCloud SDK for Windows Runtime 会尽快发布,本文档所提及的概念以及示例代码都兼容以上提及的 2 个运行时。
为了支持实时聊天,LeanCloud SDK for Windows Phone Silverlight 依赖于一个开源的第三方的 WebSocket 的库,所以推荐开发者从 Nuget 上下载我们的 SDK。
导入 SDK 之后,在 App.xaml 的构造函数中添加如下代码:
public App(){//generated code by visual studio...AVClient.Initialize("你的 AppId", "你的 AppKey");...}
此场景类似于微信的私聊,微博的私信以及 QQ 单聊的场景,我们在 V2 版本内建立了一个统一的概念来描述聊天的各种场景:对话 — AVIMConversation,在《实时通信开发指南(v2)》里面有详细的介绍。
刘备想发送一条消息给曹操,下面的代码将帮助他实现这一功能:
public async void Sanguo_Episode1(){AVIMClient client = new AVIMClient("Liubei");//刘备用自己的名字作为 ClientId。//注:ClientId 在单个应用中保持唯一即可,值为任意长度不超50的字符,由开发者自己维护await client.ConnectAsync();//刘备登陆到 LeanCloud 服务端AVIMConversation conversationWithCaocao = await client.CreateConversationAsync("Caocao");//创建一个只包含 刘备 和 曹操 对话,这就是单聊。await conversationWithCaocao.SendTextMessageAsync("丞相,皇上又赏赐我新手机了!");//调用 AVIMConversation.SendTextMessageAsync 方法发送消息。}
曹操如果想收到刘备的消息,他需要如下代码:
public async void Sanguo_Episode1_1(){AVIMClient client = new AVIMClient("CaoCao");//刘备用自己的名字作为 ClientId。await client.ConnectAsync();client.OnMessageReceieved += (s, e) =>{if (e.Message is AVIMTextMessage){string words = ((AVIMTextMessage)e.Message).TextContent;//words 的内容就是:丞相,皇上又送我新手机了!}};}
运行以上代码之后,在 LeanCloud 网站的控制台找到指定的应用,打开存储管理控制台,可以看到默认表 _Conversation中多了一条数据,该条数据的字段解释如下:
此场景类似于微信的多人聊天群组,以及 QQ 群 ,请注意这里的群聊指的是持久化存储的一个群组的概念,比如 QQ 群,除非群主解散该群,这个群应该是一直存在于 我的QQ群 列表中。关于临时群组聊天(聊天室)会在之后做单独解释。
刘备想建立一个群,把自己家里人都拉进这个群,然后给他们发消息,他需要做的事情是:
以下代码将实现这个需求:
public async void Sanguo_Eposode2(){AVIMClient client = new AVIMClient("LiuBei");//刘备用自己的名字作为 ClientId。await client.ConnectAsync();//刘备登陆到 LeanCloud 服务端#region 第一步:建立一个家庭成员列表IList<string> familyMembers = new List<string>();familyMembers.Add("GuanYu");familyMembers.Add("ZhangFei");familyMembers.Add("Wife01");familyMembers.Add("Wife02");//把家里人都作为群成员#endregion#region 第二步:新建一个对话,把家庭成员列表列为对话的参与人员AVIMConversation familyConversation = await client.CreateConversationAsync(familyMembers);//创建一个对话,对话包含了 刘关张以及两位夫人#endregion#region 第三步:发送一条消息await familyConversation.SendTextMessageAsync("曹操已对我起了疑心,我们还是赶紧跑路吧……");#endregion}
群聊的接受消息于单聊的接受消息是一样的。
AVIMConversation NitifiedConversation = null;public async void Sanguo_Episode2_1(){AVIMClient client = new AVIMClient("GuanYu");//刘备用自己的名字作为 ClientId。await client.ConnectAsync();client.OnMessageReceieved += (s, e) =>{if (e.Message is AVIMTextMessage){string words = ((AVIMTextMessage)e.Message).TextContent;//words 的内容就是:曹操已对我起了疑心,我们还是赶紧跑路吧……NitifiedConversation = e.Conversation;}};}public async void SendToBoss(){if (NitifiedConversation != null){await NitifiedConversation.SendTextMessageAsync("大哥,莫怕!有我和三弟保护你!");}}
而以上刘备和关羽发送的消息,张飞上线的时候都会收到。
富媒体消息的支持是 V2 版本针对 V1 的一个核心提升,我们目前 SDK 已经支持的富媒体消息类型有以下几种:
AVIMImageMessageAVIMAudioMessageAVIMVideoMessageAVIMFileMessageAVIMLocationMessage图片消息可以由系统提供的拍照 API,以及媒体库中获取,也可以是可访问的图片有效 Url,只要开发者调用一个构造方法,构造出一个 AVIMImageMessage,然后把 AVIMImageMessage 对象当做参数交由 AVConversation 发送出去即可。
比如从微博拷贝了一个图片链接,然后可以通过 SDK 直接构建一个 AVIMImageMessage并且发送出去:
public async void SendImageMessageAsync_Test(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆AVIMConversation conversation = await client.CreateConversationAsync("Jerry", "猫和老鼠");//创建对话AVIMImageMessage imgMessage = new AVIMImageMessage("http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif");//从外部链接创建图片消息await conversation.SendImageMessageAsync(imgMessage);//发送给 Jerry}
系统也提供了 API 去获取媒体库里面的照片,开发者只需要调用系统的 API 获取图片文件的数据流,然后构造出一个 AVIMImageMessage,再调用 AVIMConversation.SendImageMessageAsync去发送图片:
MediaLibrary library = new MediaLibrary();//系统媒体库var photo = library.Pictures[0];//获取第一张照片,运行这段代码,确保手机以及虚拟机里面的媒体库至少有一张照片AVIMImageMessage imgMessage = new AVIMImageMessage(photo.Name, photo.GetImage());//构造 AVIMImageMessageimgMessage.Attributes = new Dictionary<string, object>(){{"location","San Francisco"}};imgMessage.Title = "发自我的WP";await conversation.SendImageMessageAsync(imgMessage);
code
code
code
code
以上 2 节基本演示了 V2 版本的实时聊天 SDK 的核心概念——AVIMConversation ,LeanCloud 将单聊和群聊(包括聊天室)的消息发送和接受都依托于 AVIMConversation这个统一的概念进行操作。
所以,开发者需要强化理解的一个概念就是:SDK 层面是不再区分单聊以及群聊。
对话的管理包括成员管理,属性管理两个方面。
首先来介绍对话的成员管理:
贾诩想主动加入到曹操的「谋士群」 中,以下代码将帮助他实现这个功能:
code
该群的其他成员(比如程昱)会根据自身客户端的状态不同会出现以下 2 种情况:
AVIMClient.OnConversationMembersChanged 的响应的相关操作,代码如下:
code
AVIMClient.OnConversationMembersChanged 的响应的相关操作,也会收到 AVIMConversation.OnMembersJoined 的响应的相关操作,代码如下:
code
关羽被俘虏之后,曹操想把关羽拉到自己的「武将群」 中,需要如下代码帮助他实现这个功能:
code
该群的其他成员(例如张辽)也会受到该项操作的影响,收到事件被响应的通知,类似于第一小节自身主动加入中**贾诩加入「谋士群」之后,程昱受到的影响 **
这里一定要区分自身退出对话的主动性,它与自身被动被剔除(下一小节)在逻辑上完全是不一样的。
刘备主动从袁绍的「谋士群」中退出,他需要如下代码实现需求:
code
该群的其他成员(例如沮授)也会受到该项操作的影响,收到事件被响应的通知,类似于第一小节自身主动加入中**贾诩加入「谋士群」之后,程昱受到的影响 **
关羽从曹操势力范围中出逃,程昱把关羽从群组中剔除,
注:关于程昱如何获得权限在后面的签名和安全一章节会做详细阐述,此处不宜扩大话题范围)
以下代码可以帮助程昱实现这一功能:
code
对话(AVIMConversation)与控制台中 _Conversation 表是一一对应的,默认提供的属性对应的关系如下:
| AVIMConversation 属性名 | _Conversation 字段 | 含义 |
|---|---|---|
AVIMConversation.ConversationId |
_Conversation.objectId |
全局唯一的 Id |
AVIMConversation.Name |
_Conversation.name |
成员共享的统一的名字 |
AVIMConversation.MemberIds |
_Conversation.m |
成员列表 |
AVIMConversation.MuteMemberIds |
_Conversation.mu |
静音成员列表 |
AVIMConversation.Creator |
_Conversation.c |
对话创建者 |
AVIMConversation.LastMesaageAt |
_Conversation.lm |
对话最后一条消息发送的时间 |
AVIMConversation.Attributes |
_Conversation.attr |
自定义属性 |
AVIMConversation.IsTransient |
_Conversation.tr |
是否为聊天室(暂态对话) |
这个属性是全员共享的一个属性,他可以在创建时指定,也可以在日后的维护中被修改。
诸葛亮想建立一个名字叫「聪明人」 对话,需要写的代码如下:
code
曹丕登基之后,把文武百官所在的群「汉」的名字改成了「魏」,他需要如下代码:
code
该属性表示当前对话中所包含的成员的 clientId ,这个属性强烈建议开发者切勿在控制台中随意修改,所有关于成员的操作请参照上一章节中的 成员管理 来进行。
假如某一个用户不想再收到某对话的消息,又不想直接退出对话,可以使用静音操作。
比如 Tom 工作繁忙,对某个对话设置了静音:
code
此属性也强烈建议开发者切勿在控制台中随意修改。
对话的创建者可以帮助实现类似于 QQ 群中区别管理员和群所有者,它的值是对话创建者的 clientId。
code
此属性是为了帮助开发者给对话添加自定义属性。开发者可以随意存储自己的键值对,以帮助开发者实现自己的业务逻辑需求。
典型的场景是,我想对某个对话设置 tag 是 private,表示我要标记这个对话是私有的:
如下代码:
code
注意:AVIMConversation.Attributes 在 SDK 级别是对所有成员可见的,如果要控制所谓的可见性,开发者需要自己维护这一属性的读取权限
AVIMConversation.Attributes 在对话查询一节还有更多的用法。
假如已知某一对话的 Id,可以利用查询该对话的详细信息
代码实例:
code
条件查询包含分类有比较查询,匹配查询
比较查询在一般的理解上都包含以下几种:
| 逻辑操作 | AVIMConversationQuery 对应的方法 |
|---|---|
| 等于 | WhereEqualTo |
| 不等于 | WhereNotEqualTo |
| 大于 | WhereGreaterThan |
| 大于等于 | WhereGreaterThanOrEqualTo |
| 小于 | WhereLessThan |
| 小于等于 | WhereLessThanOrEqualTo |
比较查询最常用的是 WhereEqualTo:
public async void WhereEqualTo_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereEqualTo("attr.topic", "movie");//构建 topic 是 movie 的查询var result = await query.FindAsync();//执行查询}
目前条件查询只针对 AVIMConversatioon 的 Attributes 属性进行的,也就是针对 _Conversation 表中的 attr 字段进行的 AVQuery 查询。
实际上为了方便开发者自动为了自定义属性的 key 值增加 attr. 的前缀,SDK 特地添加了一个针对 string 类型的拓展方法
/// <summary>/// 为聊天的自定义属性查询自动添加 "attr." 的前缀/// </summary>/// <param name="key">属性 key 值,例如 type </param>/// <returns>添加前缀的值,例如,attr.type </returns>public static string InsertAttrPrefix(this string key){return key.Insert(0, "attr.");}
导入 SDK 之后在 Visual Studio 里面使用 string 类型的时候可以智能感应提示该方法。
AVIMConversationQuery query = client.GetQuery().WhereEqualTo("topic".InsertAttrPrefix(), "movie");//这样就可以实现自动为 `topic` 添加 `attr.` 前缀的效果的效果。
与 WhereEqualTo 相对的就是 WhereNotEqualTo ,以下代码将查询到类型不是私有的对话:
public async void WhereNotEqualTo_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereNotEqualTo("attr.type", "private");//构建 type 不等于 movie 的查询var result = await query.FindAsync();//执行查询}
对于可以比较大小的整型,浮点等常用类型,可以参照以下示例代码进行拓展:
public async void WhereGreaterThan_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereGreaterThan("attr.age", 18);//构建 年龄大于 18 的查询var result = await query.FindAsync();//执行查询}
匹配查询指的是在 AVIMConversationQuery 中以 WhereMatches 为前缀的方法。
Match 类的方法最大的便捷之处就是使用了正则表达式匹配,这样使得,客户端在构建基于正则表达式的查询的时候可以利用 .NET 里面诸多已经熟悉了的概念和接口。
比如要查询所有 tag 是中文的对话可以如下进行:
public async void WhereMatchs_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereMatches("attr.tag", "[\u4e00-\u9fa5]");//查询 tag 是中文的对话var result = await query.FindAsync();//执行查询}
包含查询指的是 方法名字包含 Contains 单词的方法,例如查询关键字包含「教育」的对话:
public async void WhereContains_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereContains("attr.keywords", "教育");//查询 keywords 包含教育var result = await query.FindAsync();//执行查询}
另外,包含查询在关于成员的查询中也可以有很大的作用。
以下代码将帮助 Tom 查找出 Jerry 以及 Bob 都存在的对话:
public async void QueryMembers_SampleCode(){AVIMClient client = new AVIMClient("Tom");IList<string> clientIds = new List<string>();clientIds.Add("Bob");clientIds.Add("Jerry");AVIMConversationQuery query = client.GetQuery().WhereContainedIn<string>("m", clientIds);//查询对话成员 Bob 以及 Jerry 的对话var result = await query.FindAsync();//执行查询}
组合查询的概念就是把诸多查询条件合并成一个查询,再交给 SDK 去服务端进行查询。
LeanCloud .NET SDK 的风格一致保持一个链式的方式提供给开着去组合符合自己业务逻辑的查询,例如,要查询年龄小于18岁,并且关键字包含「教育」的对话:
public async void CombinationQuery_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereContains("attr.keywords", "教育").WhereLessThan("age", 18);//查询 keywords 包含教并且年龄小于18的对话var result = await query.FindAsync();//执行查询}
组合查询的性能开发者不必担心,只要合理地构造查询,性能完全不需要开发者去担心。
任意的查询,不管是单查询还是组合查询,都支持计数查询:
public async void QueryCount_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereContains("attr.keywords", "教育").WhereLessThan("attr.age", 18);//查询 keywords 包含教并且年龄小于18的对话var count = await query.CountAsync();//执行查询,获取符合条件的对话的数量}
开放聊天室在本质上就是一个对话,所以以上章节中提到了所有的属性,方法,操作以及管理,都对开放聊天室适用,它仅仅是在逻辑上是一种暂态的,临时的对话。
比如某项比赛正在直播,解说员可以通过以下代码创建一个临时聊天是与球迷进行互动聊天:
public async void ChatRoom_SampleCode(){AVIMClient client = new AVIMClient("Dendi");await client.ConnectAsync();//Tom 登陆客户端var chatroom = client.CreateConversationAsync(null, "DK VS NewBee", null, true);//详细解释最后一个参数,transient 如果为 true 就说明是聊天室,逻辑上就是暂态对话}
另外,为了方便开发者快速创建聊天室,SDK 提供了一个快捷方法创建聊天室:
var chatroom = client.CreateChatRoomAsync("皇马 VS 巴萨");//可以理解为一个语法糖,与调用CreateConversationAsync 没有本质区别
AVIMConversation.CountMembersAsync 不但可以用来查询普通对话的成员总数,在聊天室中,它返回的就是实时在线的人数:
public async void CountMembers_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversation conversation = (await client.GetQuery().FindAsync()).FirstOrDefault();int membersCount = await conversation.CountMembersAsync();}
开发者需要注意的是,AVIMConversationQuery 调用 Where 开头的方法都是查询全部对话的,也就是说,如果想单独查询聊天室的话,需要在额外再调用一次 WhereEqulaTo 方法:
比如我想查询主题包含《奔跑吧,兄弟》的聊天室,如下做即可:
public async void QueryChatRoom_SampleCode(){AVIMClient client = new AVIMClient("Tom");await client.ConnectAsync();//Tom 登陆客户端AVIMConversationQuery query = client.GetQuery().WhereContains("topic".InsertAttrPrefix(), "奔跑吧,兄弟").WhereEqualTo("tr", true);//比如我想查询主题包含《奔跑吧,兄弟》的聊天室var result = await query.FindAsync();//执行查询}
从代码上可以看出,仅仅是多了一个额外的 WhereEqualTo("tr", true) 的链式查询即可。
聊天记录一直是客户端开发的一个重点难题,类似于的 QQ 和 微信的解决方案都是依托客户端做缓存,收到一条消息,就按照自己的业务逻辑存储在客户端的文件或者是各种客户端数据库中。
目前为了满足需求,我们特地开放了从服务端获取聊天记录的功能:
AVIMConversation con = new AVIMConversation() { ConversationId = "2f08e882f2a11ef07902eeb510d4223b" };await con.QueryHistory(DateTime.Now, 0, "UserA")//查询 UserA 在 ConversationId 为 `2f08e882f2a11ef07902eeb510d4223b` 中的聊天记录。
类似以查询,它提供了 limit 和 skip 操作,可以帮助开发者实现翻页等功能。
在继续阅读本文档之前,请确保您已经对 实时通信服务开发指南v2—权限和认证 有了一定的了解。
AVIMClient 有一个属性:
/// <summary>/// 获取签名的接口/// </summary>public ISignatureFactoryV2 SignatureFactory { get; set; }
是预留给开发者实现签名需求的接口,开发者只需要在登陆之前实现这个接口即可。
为了方便开发者理解签名,我们特地开源了签名的云代码实例,只要按照要求正确配置,就可以在客户端通过调用云代码的具体的函数实现签名。
演示实例的步骤:
首先您需要下载最新版本的云代码实例到本地,然后部署到您的应用中,详细请参考云代码命令行工具使用详解
其次,在 Visaul Studio 中,新建一个类叫做 SampleSignatureFactory ,把下面这段代码拷贝到其中:
/// <summary>/// 签名示例类,推荐开发者用这段代码理解签名的整体概念,正式生产环境,请慎用/// </summary>public class SampleSignatureFactory : ISignatureFactoryV2{/// <summary>/// 为更新对话成员的操作进行签名/// </summary>/// <param name="conversationId">对话的Id</param>/// <param name="clientId">当前的 clientId</param>/// <param name="targetIds">被操作所影响到的 clientIds</param>/// <param name="action">执行的操作,目前只有 add,remove</param>/// <returns></returns>public Task<AVIMSignatureV2> CreateConversationSignature(string conversationId, string clientId, IList<string> targetIds, string action){var data = new Dictionary<string, object>();data.Add("client_id", clientId);//表示当前是谁在操作。data.Add("member_ids", targetIds);//memberIds不要包含当前的ClientId。data.Add("conversation_id", conversationId);//conversationId是签名必须的参数。data.Add("action", action);//conversationId是签名必须的参数。//调用云代码进行签名。return AVCloud.CallFunctionAsync<IDictionary<string, object>>("actionOnCoversation", data).ContinueWith<AVIMSignatureV2>(t =>{return MakeSignature(t.Result); ;//拼装成一个 Signature 对象});//以上这段代码,开发者无需手动调用,只要开发者对一个 AVIMClient 设置了 SignatureFactory,SDK 会在执行对应的操作时主动调用这个方法进行签名。}/// <summary>/// 登陆签名/// </summary>/// <param name="clientId">当前的 clientId</param>/// <returns></returns>public Task<AVIMSignatureV2> CreateConnectSignature(string clientId){var data = new Dictionary<string, object>();data.Add("client_id", clientId);//表示当前是谁要求连接服务器。//调用云代码进行签名。return AVCloud.CallFunctionAsync<IDictionary<string, object>>("connect", data).ContinueWith<AVIMSignatureV2>(t =>{return MakeSignature(t.Result); ;//拼装成一个 Signature 对象});}/// <summary>/// 为创建对话签名/// </summary>/// <param name="clientId">当前的 clientId </param>/// <param name="targetIds">被影响的 clientIds </param>/// <returns></returns>public Task<AVIMSignatureV2> CreateStartConversationSignature(string clientId, IList<string> targetIds){var data = new Dictionary<string, object>();data.Add("client_id", clientId);//表示当前是谁在操作。data.Add("member_ids", targetIds);//memberIds不要包含当前的ClientId。//调用云代码进行签名。return AVCloud.CallFunctionAsync<IDictionary<string, object>>("startConversation", data).ContinueWith<AVIMSignatureV2>(t =>{return MakeSignature(t.Result); ;//拼装成一个 Signature 对象});}/// <summary>/// 获取签名信息并且把它返回给 SDK 去进行下一步的操作/// </summary>/// <param name="dataFromCloudcode"></param>/// <returns></returns>protected AVIMSignatureV2 MakeSignature(IDictionary<string, object> dataFromCloudcode){AVIMSignatureV2 signature = new AVIMSignatureV2();signature.Nonce = dataFromCloudcode["nonce"].ToString();signature.SignatureContent = dataFromCloudcode["signature"].ToString();signature.Timestamp = (long)dataFromCloudcode["timestamp"];return signature;//拼装成一个 Signature 对象}/// <summary>/// 为获取聊天记录的操作签名/// </summary>/// <param name="clientId">当前的 clientId </param>/// <param name="conversationId">对话 Id</param>/// <returns></returns>public Task<AVIMSignatureV2> CreateQueryHistorySignature(string clientId, string conversationId){var data = new Dictionary<string, object>();data.Add("client_id", clientId);//表示当前是谁在操作。data.Add("convid", conversationId);//memberIds不要包含当前的ClientId。//调用云代码进行签名。return AVCloud.CallFunctionAsync<IDictionary<string, object>>("queryHistory", data).ContinueWith<AVIMSignatureV2>(t =>{return MakeSignature(t.Result); ;//拼装成一个 Signature 对象});}}
AVIMClient client = new AVIMClient("Tom");client.SignatureFactory = new SampleSignatureFactory();//这里是一个开发者自己实现的接口的具体的类await client.ConnectAsync();//Tom 登陆客户端
本章节,是为了讲述在使用 LeanCloud 实时通信 SDK v2 版本时候的一些比较推荐的最佳实践。
开发者如果已经把本章节之前的实例代码都运行过一遍或者至少都创建过至少 1 个对话,发送过各种富媒体消息至少一次,就会发现 v2 接口定义的实际上还是偏灵活性大于定制性,所以很多场景我们单独列出来,作为最佳实践的例子给开发者仔细讲解一下。
微信的单聊实际上在 v2 中我们解释为只包含 2 个人的对话,所以为了避免频繁的创建对话,A 与 B成功开始聊天之后,推荐开发者在客户端把当前对话的 ConversationId 保存在本地缓存中,文件,数据库均可,实际上从功能表现来分析,微信本身就是这么做的。
微信的群聊可以由2种方式生成:
1. 创建初始就选择了不少于 3 个人进行聊天
2. 由单聊的页面,点击加号,选择其他人加入
以上 2 种方式,从功能表现开看,微信的客户端都是重新创建了一个新的对话,尤其是针对第 2 种,点击加号,并不是直接在当前对话里面加入其他成员,而是新建的对话,只不过默认把之前单聊的对象也一并加入到新的对话当中。