{"action":"created","comment":{"url":"https://api.github.com/repos/leancloud/docs/pulls/comments/27263941","id":27263941,"diff_hunk":"@@ -0,0 +1,1163 @@\n+# Windows Phone 实时通信服务(v2)\n+\n+## 简介\n+\n+在阅读本开发指南之前，请先阅读下[《实时通信开发指南(v2)》](./realtime_v2.html)，了解实时通信的基本概念和模型。\n+\n+目前我们的实时通信服务仅支持 Windows Phone Silverlight 运行时，支持微软新一代的全平台统一运行时的 LeanCloud SDK for Windows Runtime 会尽快发布，本文档所提及的概念以及示例代码都兼容以上提及的 2 个运行时。\n+\n+## 安装\n+为了支持实时聊天，LeanCloud SDK for Windows Phone Silverlight 依赖于一个开源的第三方的 WebSocket 的[库](https://www.nuget.org/packages/WebSocket4Net/)，所以推荐开发者从 [Nuget](https://www.nuget.org/packages/AVOSCloud.Phone/1.2.3.4-beta) 上下载我们的 SDK。\n+\n+## 初始化\n+导入 SDK 之后，在 `App.xaml` 的构造函数中添加如下代码：\n+\n+```c#\n+public App()\n+{\n+   //generated code by visual studio\n+   ...\n+   AVClient.Initialize(\"你的 AppId\", \"你的 AppKey\");\n+   ...\n+}\n+```\n+\n+## 单聊\n+\n+###  发送消息\n+此场景类似于微信的私聊，微博的私信以及 QQ 单聊的场景，我们在 V2 版本内建立了一个统一的概念来描述聊天的各种场景：对话 — AVIMConversation，在[《实时通信开发指南(v2)》](./realtime_v2.html)里面有详细的介绍。\n+\n+刘备想发送一条消息给曹操，下面的代码将帮助他实现这一功能：\n+\n+```c#\n+public async void Sanguo_Episode1()\n+{\n+    AVIMClient client = new AVIMClient(\"Liubei\");//刘备用自己的名字作为 ClientId。\n+    //注：ClientId 在单个应用中保持唯一即可，值为任意长度不超50的字符，由开发者自己维护\n+\n+    await client.ConnectAsync();//刘备登陆到 LeanCloud 服务端\n+\n+    AVIMConversation conversationWithCaocao = await client.CreateConversationAsync(\"Caocao\");\n+    //创建一个只包含 刘备 和 曹操 对话，这就是单聊。\n+\n+    await conversationWithCaocao.SendTextMessageAsync(\"丞相，皇上又赏赐我新手机了！\");\n+    //调用 AVIMConversation.SendTextMessageAsync 方法发送消息。\n+\n+}\n+```\n+\n+### 接受消息\n+\n+曹操如果想收到刘备的消息，他需要如下代码：\n+\n+```c#\n+public async void Sanguo_Episode1_1()\n+{\n+    AVIMClient client = new AVIMClient(\"CaoCao\");//刘备用自己的名字作为 ClientId。\n+    await client.ConnectAsync();\n+    client.OnMessageReceieved += (s, e) =>\n+    {\n+        if (e.Message is AVIMTextMessage)\n+        {\n+            string words = ((AVIMTextMessage)e.Message).TextContent;\n+            //words 的内容就是：丞相，皇上又送我新手机了！\n+        }\n+    };\n+\n+}\n+```\n+运行以上代码之后，在 LeanCloud 网站的控制台找到指定的应用，打开存储管理控制台，可以看到默认表 `_Conversation`中多了一条数据，该条数据的字段解释如下：\n+\n+* name：String，对话唯一的名字。\n+* m：Array，对话中成员的列表。\n+* lm：Date，对话中最后一条消息发送的时间。\n+* c：String，对话的创建者的 ClientId\n+* mu：Array，对话中设置了静音的成员，仅针对 iOS 以及 Windows Phone 用户有效\n+* attr：Object，开发者设置的对话的自定义属性。\n+\n+## 群聊\n+此场景类似于微信的多人聊天群组，以及 QQ 群 ，请注意这里的群聊指的是持久化存储的一个群组的概念，比如 QQ 群，除非群主解散该群，这个群应该是一直存在于 我的QQ群 列表中。关于临时群组聊天（聊天室）会在之后做单独解释。\n+\n+### 发送消息\n+刘备想建立一个群，把自己家里人都拉进这个群，然后给他们发消息，他需要做的事情是：\n+\n+* 第一步：建立一个家庭成员列表\n+* 第二步：新建一个对话，把家庭成员列表里面的成员列为对话的参与人员\n+* 第三步：发送一条消息\n+\n+以下代码将实现这个需求：\n+\n+```c#\n+public async void Sanguo_Eposode2()\n+{\n+    AVIMClient client = new AVIMClient(\"LiuBei\");//刘备用自己的名字作为 ClientId。\n+\n+    await client.ConnectAsync();//刘备登陆到 LeanCloud 服务端\n+\n+    #region 第一步：建立一个家庭成员列表\n+    IList<string> familyMembers = new List<string>();\n+    familyMembers.Add(\"GuanYu\");\n+    familyMembers.Add(\"ZhangFei\");\n+    familyMembers.Add(\"Wife01\");\n+    familyMembers.Add(\"Wife02\");\n+    //把家里人都作为群成员\n+    #endregion\n+\n+    #region 第二步：新建一个对话，把家庭成员列表列为对话的参与人员\n+    AVIMConversation familyConversation = await client.CreateConversationAsync(familyMembers);\n+    //创建一个对话，对话包含了 刘关张以及两位夫人\n+    #endregion\n+\n+    #region 第三步：发送一条消息\n+    await familyConversation.SendTextMessageAsync(\"曹操已对我起了疑心，我们还是赶紧跑路吧……\");\n+    #endregion\n+}\n+```\n+\n+### 接受消息\n+群聊的接受消息于单聊的接受消息是一样的。\n+\n+```c#\n+AVIMConversation NitifiedConversation = null;\n+public async void Sanguo_Episode2_1()\n+{\n+    AVIMClient client = new AVIMClient(\"GuanYu\");//刘备用自己的名字作为 ClientId。\n+    await client.ConnectAsync();\n+    \n+    client.OnMessageReceieved += (s, e) =>\n+    {\n+        if (e.Message is AVIMTextMessage)\n+        {\n+            string words = ((AVIMTextMessage)e.Message).TextContent;\n+            //words 的内容就是：曹操已对我起了疑心，我们还是赶紧跑路吧……\n+            NitifiedConversation = e.Conversation;\n+        }\n+    };\n+}\n+public async void SendToBoss()\n+{\n+    if (NitifiedConversation != null)\n+    {\n+        await NitifiedConversation.SendTextMessageAsync(\"大哥，莫怕！有我和三弟保护你!\");\n+    }\n+}\n+```\n+\n+而以上刘备和关羽发送的消息，张飞上线的时候都会收到。\n+\n+## 消息\n+消息在 v2 版本的 SDK 中做了多层抽象以及封装，详细的我们先从如何发送接收富媒体消息开始。\n+### 富媒体消息\n+富媒体消息的支持是 V2 版本针对 V1 的一个核心提升，我们目前 SDK 已经支持的富媒体消息类型有以下几种：\n+\n+* 图片消息：`AVIMImageMessage`\n+* 音频消息：`AVIMAudioMessage`\n+* 视频消息：`AVIMVideoMessage`\n+*  文件消息：`AVIMFileMessage`\n+* 地理位置消息：`AVIMLocationMessage`\n+\n+\n+#### 图片消息\n+图片消息可以由系统提供的拍照 API，以及媒体库中获取，也可以是可访问的图片有效 Url，只要开发者调用一个构造方法，构造出一个 `AVIMImageMessage`，然后把 `AVIMImageMessage` 对象当做参数交由 `AVConversation` 发送出去即可。\n+\n+##### 发送图片消息\n+比如从微博拷贝了一个图片链接，然后可以通过 SDK 直接构建一个 `AVIMImageMessage`并且发送出去：\n+```c#\n+public async void SendImageMessageAsync_Test()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    \n+    await client.ConnectAsync();//Tom 登陆\n+\n+    AVIMConversation conversation = await client.CreateConversationAsync(\"Jerry\", \"猫和老鼠\");//创建对话\n+\n+    AVIMImageMessage imgMessage = new AVIMImageMessage(\"http://pic2.zhimg.com/6c10e6053c739ed0ce676a0aff15cf1c.gif\");//从外部链接创建图片消息\n+    await conversation.SendImageMessageAsync(imgMessage);//发送给 Jerry\n+}\n+``` \n+\n+系统也提供了 API 去获取媒体库里面的照片，开发者只需要调用系统的 API 获取图片文件的数据流，然后构造出一个 `AVIMImageMessage`，再调用 `AVIMConversation.SendImageMessageAsync`去发送图片：\n+\n+```c#\n+MediaLibrary library = new MediaLibrary();//系统媒体库\n+var photo = library.Pictures[0];//获取第一张照片，运行这段代码，确保手机以及虚拟机里面的媒体库至少有一张照片\n+\n+AVIMImageMessage imgMessage = new AVIMImageMessage(photo.Name, photo.GetImage());//构造 AVIMImageMessage\n+imgMessage.Attributes = new Dictionary<string, object>() \n+{ \n+    {\"location\",\"San Francisco\"}\n+};\n+imgMessage.Title = \"发自我的WP\";\n+await conversation.SendImageMessageAsync(imgMessage);\n+```\n+\n+##### 接受图片消息\n+类似于第一章节中单聊中的接收消息，在 `AVIMClient` 中的 `OnMessageReceived` 可以收到消息，但是假如接收方在客户端也正好加载了这个对话，那么接收方在 `AVIMConversation` 里面也会收到 `OnImageMessageReceived` 的事件响应：\n+\n+```c#\n+public async void ReceiveImageMessageAsync_Test()\n+{\n+    AVIMClient client = new AVIMClient(\"Jerry\");\n+    await client.ConnectAsync();\n+    AVIMConversation conversaion = client.CreateConversationById(\"55117292e4b065f7ee9edd29\");\n+    await conversaion.FetchAsync();\n+    conversaion.OnImageMessageReceived += (sImage, eImage) => \n+    {\n+        string url = eImage.Url;//读取图片的 url\n+        //读取了 ulr 之后，开发者在客户端做展示\n+    };\n+}\n+```\n+\n+#### 音频消息\n+音频消息的常见用法就是发送语音消息，语音消息的获取在 Windows Phone 中可以通过 [AudioVideoCaptureDevice](https://msdn.microsoft.com/en-us/library/windows.phone.media.capture.audiovideocapturedevice.aspx) 相关 API 获取或者其他的方式去获取，然后使用获取到的音频源文件的数据流创建一个 `AVIMAudioMessage` 对象，然后调 用`AVIMConversation.SendAudioMessageAsync` 发送：\n+\n+##### 录制音频\n+```c#\n+#region sample code for send audio message by LeanCloud .NET SDK \n+private string recordAudioFileName = \"record_audio_temp.aac\";\n+IRandomAccessStream randomAccessStream;\n+AudioVideoCaptureDevice micphone;\n+AVIMConversation conversation;\n+public async Task StartRecordingAsync()\n+{\n+    micphone = await AudioVideoCaptureDevice.OpenForAudioOnlyAsync();\n+    micphone.AudioEncodingFormat = CameraCaptureAudioFormat.Amr;\n+    try\n+    {\n+        await CreateFileStreamForAudioAsync(recordAudioFileName);\n+        await micphone.StartRecordingToStreamAsync(randomAccessStream);\n+    }\n+    catch (Exception ex)\n+    {\n+        throw ex;\n+    }\n+}\n+\n+public async Task CreateFileStreamForAudioAsync(string fileName)\n+{\n+    try\n+    {\n+        StorageFolder local = Windows.Storage.ApplicationData.Current.LocalFolder;\n+\n+        StorageFile file = file = await local.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);\n+        randomAccessStream = await file.OpenAsync(FileAccessMode.ReadWrite);\n+    }\n+    catch (Exception ex)\n+    {\n+        throw ex;\n+    }\n+}\n+public async void StopRecord()\n+{\n+    await micphone.StopRecordingAsync();\n+    randomAccessStream.CloneStream();\n+}\n+#endregion\n+```\n+\n+##### 发送音频消息\n+使用上一小节的代码结合下面这段发送音频消息的代码，就是一个从`创建音频`，`构建音频消息`，`消息发送`完整的流程\n+```c#\n+private async void SendAudioMessageAsync()\n+{\n+    StorageFolder local = Windows.Storage.ApplicationData.Current.LocalFolder;\n+    var AudioFile = await local.OpenStreamForReadAsync(recordAudioFileName);\n+    AVIMAudioMessage audioMessage = new AVIMAudioMessage(recordAudioFileName, AudioFile);//创建音频消息\n+\n+    await conversation.SendAudioMessageAsync(audioMessage);\n+    //这段代码运行之前，请确保 `conversation` 已经实例化\n+}\n+``` \n+##### 接受音频消息\n+与接收图片消息一样，在 `AVIMConversation` 中有 `OnAudioMessageReceived` ，实例代码请参照图片消息接收。\n+\n+#### 视频消息\n+\n+##### 录制视频\n+录制视频在 Windows Phone 8 之后可以采用如下方式：\n+\n+```c#\n+private string recordVideoFileName = \"record_video_temp.mp4\";\n+private AudioVideoCaptureDevice camera;\n+private IRandomAccessStream randomAccessStream;\n+AVIMConversation conversation;\n+public async Task StartRecordVideoAsync()\n+{\n+    try\n+    {\n+        IReadOnlyList<Windows.Foundation.Size> supportedResolutions = AudioVideoCaptureDevice.GetAvailableCaptureResolutions(CameraSensorLocation.Back);\n+        Windows.Foundation.Size resolution = supportedResolutions[0];\n+        camera = await AudioVideoCaptureDevice.OpenAsync(CameraSensorLocation.Back, resolution);\n+\n+        StorageFolder applicationFolder = ApplicationData.Current.LocalFolder;\n+        StorageFile storageFile = await applicationFolder.CreateFileAsync(recordVideoFileName, CreationCollisionOption.ReplaceExisting);\n+\n+        randomAccessStream = await storageFile.OpenAsync(FileAccessMode.ReadWrite);\n+\n+        await camera.StartRecordingToStreamAsync(randomAccessStream);\n+    }\n+    catch (Exception ex)\n+    {\n+        MessageBox.Show(ex.ToString());\n+    }\n+}\n+public async void StopRecordVideoAsync(object sender, RoutedEventArgs e)\n+{\n+    await camera.StopRecordingAsync();\n+    randomAccessStream.Dispose();\n+}\n+```\n+##### 发送视频消息\n+配上上节的代码就可以用如下代码发送视频消息：\n+```c#\n+private async void SendVideoMessageAsync()\n+{\n+    StorageFolder local = Windows.Storage.ApplicationData.Current.LocalFolder;\n+\n+    var VideoFile = await local.OpenStreamForReadAsync(recordVideoFileName);\n+\n+    AVIMVideoMessage videoMessage = new AVIMVideoMessage(recordVideoFileName, VideoFile);\n+\n+    await conversation.SendVideoMessageAsync(videoMessage);\n+}\n+```\n+\n+同样我们也支持从一个视频的Url 创建视频消息，然后发送出去：\n+\n+```c#\n+public async void SendVideoMessageAsync()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    await client.ConnectAsync();//Tom 登陆\n+\n+    var conversation = await client.CreateConversationAsync(\"Jerry\", \"猫和老鼠\");//创建对话\n+\n+    AVIMVideoMessage videoMessage = new AVIMVideoMessage(\"http://ac-lhzo7z96.clouddn.com/1427267336319\");//从外部链接创建视频消息\n+    await conversation.SendVideoMessageAsync(videoMessage);//发送给 Jerry\n+}\n+```\n+**注：这里说的视频 Url 指的是物理地址，不是视频网站上的播放地址**\n+\n+##### 接受视频消息\n+与接收图片消息一样，在 `AVIMConversation` 中有 `OnVideoMessageReceived` ，实例代码请参照图片消息接收。\n+\n+#### 通用文件消息\n+Tom 要发送一份 .doc 文件给 Jerry，可以用下面这种方法：\n+\n+##### 发送通用文件消息\n+```c#\n+public async void SendDocAsync()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    await client.ConnectAsync();//Tom 登陆\n+\n+    var conversation = await client.CreateConversationAsync(\"Jerry\", \"猫和老鼠\");//创建对话\n+    StorageFolder local = Windows.Storage.ApplicationData.Current.LocalFolder;\n+    var docFile = await local.OpenStreamForReadAsync(\"leancloud.doc\");//读取本地文件\n+    var avfile = new AVFile(\"leancloud.doc\", docFile);//构造 AVFile\n+    AVIMFileMessage fileMessage = new AVIMFileMessage(avfile);//构造文件消息\n+    await conversation.SendFileMessageAsync(fileMessage);//发送\n+}\n+```\n+\n+##### 接受通用文件消息\n+与接收图片消息一样，在 `AVIMConversation` 中有 `OnFileMessageReceived` ，实例代码请参照图片消息接收。\n+\n+#### 地理位置消息\n+地理位置消息依赖于 AVGeoPoint 进行构建。\n+##### 发送地理位置消息\n+\n+```c#\n+public async void SendLocatioAsync()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    await client.ConnectAsync();//Tom 登陆\n+    var conversation = await client.CreateConversationAsync(\"Jerry\", \"猫和老鼠\");//创建对话\n+\n+    Geolocator geolocator = new Geolocator();\n+    geolocator.DesiredAccuracyInMeters = 50;\n+\n+    try\n+    {\n+        Geoposition geoposition = await geolocator.GetGeopositionAsync(maximumAge: TimeSpan.FromMinutes(5), timeout: TimeSpan.FromSeconds(10));\n+        AVIMLocationMessage locationMessage = new AVIMLocationMessage(geoposition.Coordinate.Latitude, geoposition.Coordinate.Longitude);\n+        await conversation.SendLocationMessageAsync(locationMessage);\n+    }\n+    catch (Exception ex)\n+    {\n+        if ((uint)ex.HResult == 0x80004004)\n+        {\n+\n+            var error = \"location  is disabled in phone settings.\";\n+        }\n+    }\n+}\n+```\n+##### 接受地理位置消息\n+与接收图片消息一样，在 `AVIMConversation` 中有 `OnLocationMessageReceived` ，实例代码请参照图片消息接收。\n+\n+### 消息的发送策略\n+开发者在阅读完前面的富媒体消息并且运行过实例代码之后，应该可以发现图片，音频，视频在类的继承关系上是继承自 `AVIMFileMessageBase`，所有继承自 `AVIMFileMessageBase` 的消息类型的发送策略如下：\n+\n+* 如果文件是从客户端 API 读取的数据流 (Stream) \n+``` \n+第一步：从本地构造 AVFile\n+第二步：调用 AVFile 的上传的方法上传到服务器，并获取文件的元信息（MetaData）\n+第三步：把 AVFile 的 objectId 以及 url ，以及文件的元信息封装在消息体内\n+第四步：发送消息\n+```\n+* 如果文件是外部链接的 Url\n+```\n+第一步：直接将 Url 封装在消息体内，不获取元信息，不包含 objectId\n+第二步：发送消息\n+``` \n+\n+以上逻辑对所有继承 `AVIMFileMessageBase` 的消息类型有效，目前 SDK 内置的继承自 `AVIMFileMessageBase` 包含以下几种：\n+\n+```\n+AVIMImageMessage\n+AVIMAudioMessage\n+AVIMVideoMessage\n+AVIMFileMessage\n+```\n+\n+### 消息的接收策略\n+消息接受有 **2** 个层级：\n+\n+* 第一个是在 `AVIMClient` 上，它是为了帮助开发者实现被动接受消息，尤其是在本地并没有加载任何对话的时候，类似于刚登陆，本地并没有任何 `AVIMConversation` 的时候，如果某个对话产生新的消息，当前 `AVIMClient.OnMessageReceived` 负责接受这类消息，但是它并没有针对消息的类型做区分。\n+\n+* 第二个是在 `AVIMConversation` 上，负责接受对话的全部信息，并且针对不同的消息类型有不同的事件类型做响应。\n+\n+以上 **2** 个层级的消息接受策略可以用下表进行描述，假如正在接收的是 `AVIMTextMessage`：\n+\n+AVIMClient 接收端 | AVIMClient.OnMessageReceived |AVIMConversation.OnMessageReceived| AVIMConversation.OnTextMessageReceived \n+----------- | ------------ | ------------- | ------------- \n+条件① | × | × | × \n+条件② | √ | × | × \n+条件③ | √ | √ | × \n+条件④ | √ | × | √ \n+\n+对应条件如下：\n+\n+* 条件①：\n+```c#\n+AVIMClient.Status != Online\n+``` \n+* 条件②：\n+```c#\n+AVIMClient.Status == Online && AVIMClient.OnMessageReceived != null\n+```\n+* 条件③：\n+```c#\n+AVIMClient.Status == Online \n+&& AVIMClient.OnMessageReceived != null \n+&& AVIMConversation.OnMessageReceived != null\n+```\n+* 条件④：\n+```c#\n+AVIMClient.Status == Online \n+&& AVIMClient.OnMessageReceived != null \n+&& AVIMConversation.OnMessageReceived != null\n+&& AVIMConversation.OnTextMessageReceived != null\n+```\n+条件④需要特殊解释一下：当 AVIMConversation 收到 AVIMTextMessage 时，它会先检测 AVIMConversation.OnTextMessageReceived 是否有值，如果有就直接响应，并且不会再次响应 AVIMConversation.OnMessageReceived，而只有当 AVIMConversation.OnTextMessageReceived 为空的时候，才会再次去响应 AVIMConversation.OnMessageReceived。\n+\n+### 消息类详解\n+![消息的类图](http://ac-lhzo7z96.clouddn.com/1427252943504)\n+\n+* `AVIMMessage` 所有消息的基类，一级抽象类；\n+* `AVIMTypedMessage` 富媒体消息的基类，二级抽象类；\n+* `AVIMFileMessageBase` 所有包含了文件内容的消息的基类，三级抽象类\n+* `AVIMTextMessage` 文本你消息，三级实例类；\n+* `AVIMLocationMessage` 地理位置消息，三级实例类\n+* `AVIMImageMessage` 图片消息，四级实例类\n+* `AVIMAudioMessage` 音频消息，四级实例类\n+* `AVIMVideoMessage` 视频消息，四级实例类\n+* `AVIMFileMessage` 通用文件消息类，四级实例类\n+\n+结合图例，v2 实时通信 SDK 在封装的时候，做了明确的分层，开发者需要根据自己的需求去使用。\n+\n+### 消息的自定义属性\n+有些场景下需要开发者在发送消息的时候附带一下自己业务逻辑需求的自定义属性，比如消息发送的设备名称，或者图片消息的拍摄地点，或者视频消息的来源等等，如果业务需要，开发者都可以通过 `AVIMMessage.Attributes` 实现这一需求。\n+\n+场景1：在发送照片给自己朋友的时候，想告诉朋友这张照片是在旧金山拍摄的，如下代码可以实现这个需求：\n+\n+```c#\n+AVIMImageMessage imgMessage = new AVIMImageMessage(photo.Name, photo.GetImage());//构造 AVIMImageMessage\n+imgMessage.Attributes = new Dictionary<string, object>() \n+{ \n+    {\"location\",\"San Francisco\"}\n+};\n+imgMessage.Title = \"发自我的WP\";\n+await conversation.SendImageMessageAsync(imgMessage);\n+```\n+\n+而接收方在 `OnMessageReceived` 的时候是可以读取到这一属性的：\n+\n+```c#\n+AVIMClient client = new AVIMClient(\"friend\");\n+await client.ConnectAsync();\n+client.OnMessageReceieved += (s, e) =>\n+{\n+    if (e.Message is AVIMImageMessage)\n+    {\n+        AVIMImageMessage imgMessage = (AVIMImageMessage)e.Message;\n+        string url = imgMessage.Url;\n+        string location = imgMessage.Attributes[\"location\"].ToString();//读取的结果就是 San Francisco\n+    }\n+};\n+```\n+所有消息都支持这一属性。\n+\n+## 对话的管理\n+以上 三个章节基本演示了 V2 版本的实时聊天 SDK 的核心概念——AVIMConversation ，LeanCloud 将单聊和群聊（包括聊天室）的消息发送和接受都依托于 `AVIMConversation`这个统一的概念进行操作。\n+\n+所以，开发者需要强化理解的一个概念就是：**SDK 层面是不再区分单聊以及群聊。**\n+\n+### 对话的成员管理\n+对话的管理包括`成员管理`，`属性管理`两个方面。\n+\n+`成员管理`是对话中成员的一个实时生效的操作，一旦操作成功不可逆。\n+\n+#### 准备工作\n+在进行下面几个章节之前，请复制如下代码到 IDE 并且执行，后面的文档中的示例代码是基于以下代码进行的，这是一项**必须的** 的工作： \n+\n+```c#\n+/// <summary>\n+/// 这段代码实现的功能就是 Jerry 创建了一个 包含 Bob，Harry，William 的对话。\n+/// </summary>\n+/// <returns></returns>\n+public async void JerryCreateConversation()\n+{\n+    AVIMClient client = new AVIMClient(\"Jerry\");\n+    await client.ConnectAsync();//Jerry 登陆\n+\n+    IList<string> friends = new List<string>();\n+    friends.Add(\"Bob\");\n+    friends.Add(\"Harry\");\n+    friends.Add(\"William\");\n+    //添加好朋友\n+\n+    await client.CreateConversationAsync(friends);//返回 ConversationId\n+}\n+```\n+#### 自身主动加入\n+Tom 想主动加入到  Jerry ， Bob，Harry，William 的上一节中 JerryCreateConversation 所创建的对话中，以下代码将帮助他实现这个功能：\n+\n+```c#\n+public async void InitiativeJoinAsync()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    await client.ConnectAsync();\n+\n+    string conversationId = \"551260efe4b01608686c3e0f\";//获取 Jerry 创建的对话的 Id，这里是直接从控制台拷贝了上一节准备工作中 JerryCreateConversation 成功之后的 objectId\n+    AVIMConversation conversation = client.CreateConversationById(conversationId);//Tom 获取到这个对话的对象\n+    await conversation.JoinAsync();//Tom 主动加入到对话中\n+}\n+```\n+该群的其他成员（比如 Bob）会根据自身客户端的状态不同会出现以下 2 种情况:\n+\n+* 如果 Bob 仅仅是登陆了应用，并没有加载具体的对话到本地，他只会收到 `AVIMClient.OnConversationMembersChanged` 的响应的相关操作，代码如下:\n+\n+```c#\n+public async void BobOnTomJoined_S1()\n+{\n+    AVIMClient client = new AVIMClient(\"Bob\");\n+    await client.ConnectAsync();\n+\n+    client.OnConversationMembersChanged += (s, e) =>\n+    {\n+        switch (e.AffectedType)\n+        {\n+            case AVIMConversationEventType.MembersJoined:\n+                {\n+                    IList<string> joinedMemberClientIds = e.AffectedMembers;//这里就是本次加入的 ClientIds\n+                    string clientId = joinedMemberClientIds.FirstOrDefault();//因为我们已知本次操作只有 Tom 一个人加入了对话，所以这样就可以直接读取到 Tom 的 clientId\n+                    //开发者可以在这里添加自己的业务逻辑\n+                }\n+                break;\n+        }\n+    };\n+}\n+```\n+\n+* 如果 Bob 不但登陆了，还在客户端加载了当前这个对话，那么他不但会收到 `AVIMClient.OnConversationMembersChanged` 的响应的相关操作，也会收到 `AVIMConversation.OnMembersJoined` 的响应的相关操作，代码如下：\n+\n+```c#\n+public async void BobOnTomJoined_S2()\n+{\n+    AVIMClient client = new AVIMClient(\"Bob\");\n+    await client.ConnectAsync();\n+\n+    client.OnConversationMembersChanged += (s, e) =>\n+    {\n+        switch (e.AffectedType)\n+        {\n+            case AVIMConversationEventType.MembersJoined:\n+                {\n+                    IList<string> joinedMemberClientIds = e.AffectedMembers;//这里就是本次加入的 ClientIds\n+                    string clientId = joinedMemberClientIds.FirstOrDefault();//因为我们已知本次操作只有 Tom 一个人加入了对话，所以这样就可以直接读取到 Tom 的 clientId\n+                    //开发者可以在这里添加自己的业务逻辑\n+                }\n+                break;\n+        }\n+    };\n+\n+    string conversationId = \"551260efe4b01608686c3e0f\";\n+\n+    AVIMConversation conversation = client.CreateConversationById(conversationId);//Bob 获取到这个对话的对象\n+\n+    conversation.OnMembersJoined += (s, e) =>\n+    {\n+        IList<string> joinedMemberClientIds = e.AffectedMembers;//这里就是本次加入的 ClientIds\n+        string clientId = joinedMemberClientIds.FirstOrDefault();//因为我们已知本次操作只有 Tom 一个人加入了对话，所以这样就可以直接读取到 Tom 的 clientId\n+    };\n+}\n+```\n+#### 添加其他成员\n+\n+Jerry 想再把 Mary 加入到对话中，需要如下代码帮助他实现这个功能：\n+\n+```c#\n+public async void InviteMarysync()\n+{\n+    AVIMClient client = new AVIMClient(\"Jerry\");\n+    await client.ConnectAsync();\n+\n+    string conversationId = \"551260efe4b01608686c3e0f\";//对话的 Id\n+    AVIMConversation conversation = client.CreateConversationById(conversationId);//Jerry 获取到这个对话的对象\n+    await conversation.AddMembersAsync(\"Mary\");//Jerry 把 Mary 加入到对话\n+}\n+```\n+该对话的其他成员（例如 Harry）也会受到该项操作的影响，收到事件被响应的通知，类似于第一小节`自身主动加入`中**Tom 加入对话之后，Bob 受到的影响 **\n+\n+#### 自身退出对话\n+这里一定要区分**自身退出对话**的主动性，它与**自身被动被剔除**（下一小节）在逻辑上完全是不一样的。\n+\n+Tom 主动从对话中退出，他需要如下代码实现需求：\n+\n+```c#\n+public async void InitiativeLeftAsync()\n+{\n+    AVIMClient client = new AVIMClient(\"Tom\");\n+    await client.ConnectAsync();\n+\n+    string conversationId = \"551260efe4b01608686c3e0f\";//获取 Jerry 创建的对话的 Id\n+    AVIMConversation conversation = client.CreateConversationById(conversationId);//Tom 获取到这个对话的对象\n+    await conversation.LeftAsync();//Tom 主动从对话中退出\n+}\n+``` \n+\n+#### 剔除其他成员\n+\n+ Harry 被  William 从对话中删除。\n+**注：关于 William 如何获得权限在后面的`签名和安全`一章节会做详细阐述，此处不宜扩大话题范围）**\n+\n+以下代码可以帮助 William 实现这一功能：\n+\n+```c#\n+public async void WilliamKickHarryOutsync()\n+{\n+    AVIMClient client = new AVIMClient(\"William\");\n+    await client.ConnectAsync();\n+\n+    string conversationId = \"551260efe4b01608686c3e0f\";//对话的 Id\n+    AVIMConversation conversation = client.CreateConversationById(conversationId);//William 获取到这个对话的对象\n+    await conversation.RemoveMembersAsync(\"Harry\");//William 把 Harry 从对话中剔除\n+}\n+```\n+\n+以上的描述可以用几张表格来解释：\n+\n+1.假如对话中已经有了 A，C\n+\n+B 的操作 |对 B 的影响|对 C 的影响\n+--- | ------------ | -------------\n+B 加入| `OnConversationMembersChanged && OnJoined`|`OnConversationMembersChanged && OnMembersJoined`\n+B 再离开|`OnConversationMembersChanged && OnLeft`|`OnConversationMembersChanged && OnMembersLeft`\n+\n+2.假如对话中已经有了 A，C\n+\n+A 对 B 的操作 | 对 B 的影响|对 C 的影响\n+--- | ------------ | -------------|\n+A 添加 B | `OnConversationMembersChanged && OnInvited`|`OnConversationMembersChanged && OnMembersJoined`\n+A 再剔除 B|`OnConversationMembersChanged && OnKicked`|`OnConversationMembersChanged && OnMembersLeft`\n+\n+#### 查询成员数量","path":"md/wp_realtime_v2.md","position":687,"original_position":687,"commit_id":"a791743b6de7276f40800cc7e18ce53c999bfc07","original_commit_id":"a791743b6de7276f40800cc7e18ce53c999bfc07","user":{"login":"jwfing","id":985038,"avatar_url":"https://avatars.githubusercontent.com/u/985038?v=3","gravatar_id":"","url":"https://api.github.com/users/jwfing","html_url":"https://github.com/jwfing","followers_url":"https://api.github.com/users/jwfing/followers","following_url":"https://api.github.com/users/jwfing/following{/other_user}","gists_url":"https://api.github.com/users/jwfing/gists{/gist_id}","starred_url":"https://api.github.com/users/jwfing/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jwfing/subscriptions","organizations_url":"https://api.github.com/users/jwfing/orgs","repos_url":"https://api.github.com/users/jwfing/repos","events_url":"https://api.github.com/users/jwfing/events{/privacy}","received_events_url":"https://api.github.com/users/jwfing/received_events","type":"User","site_admin":false},"body":"mute／unmute 的方法还可以说明一下。","created_at":"2015-03-26T22:51:22Z","updated_at":"2015-03-26T22:51:22Z","html_url":"https://github.com/leancloud/docs/pull/170#discussion_r27263941","pull_request_url":"https://api.github.com/repos/leancloud/docs/pulls/170","_links":{"self":{"href":"https://api.github.com/repos/leancloud/docs/pulls/comments/27263941"},"html":{"href":"https://github.com/leancloud/docs/pull/170#discussion_r27263941"},"pull_request":{"href":"https://api.github.com/repos/leancloud/docs/pulls/170"}}},"pull_request":{"url":"https://api.github.com/repos/leancloud/docs/pulls/170","id":31988037,"html_url":"https://github.com/leancloud/docs/pull/170","diff_url":"https://github.com/leancloud/docs/pull/170.diff","patch_url":"https://github.com/leancloud/docs/pull/170.patch","issue_url":"https://api.github.com/repos/leancloud/docs/issues/170","number":170,"state":"open","locked":false,"title":"WP realtime v2 doc","user":{"login":"freewing99","id":5119542,"avatar_url":"https://avatars.githubusercontent.com/u/5119542?v=3","gravatar_id":"","url":"https://api.github.com/users/freewing99","html_url":"https://github.com/freewing99","followers_url":"https://api.github.com/users/freewing99/followers","following_url":"https://api.github.com/users/freewing99/following{/other_user}","gists_url":"https://api.github.com/users/freewing99/gists{/gist_id}","starred_url":"https://api.github.com/users/freewing99/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freewing99/subscriptions","organizations_url":"https://api.github.com/users/freewing99/orgs","repos_url":"https://api.github.com/users/freewing99/repos","events_url":"https://api.github.com/users/freewing99/events{/privacy}","received_events_url":"https://api.github.com/users/freewing99/received_events","type":"User","site_admin":false},"body":"WP realtime v2 doc","created_at":"2015-03-26T04:08:25Z","updated_at":"2015-03-26T22:51:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"718bfa64041365aafbad7a54e408eb829fdef711","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/leancloud/docs/pulls/170/commits","review_comments_url":"https://api.github.com/repos/leancloud/docs/pulls/170/comments","review_comment_url":"https://api.github.com/repos/leancloud/docs/pulls/comments{/number}","comments_url":"https://api.github.com/repos/leancloud/docs/issues/170/comments","statuses_url":"https://api.github.com/repos/leancloud/docs/statuses/a791743b6de7276f40800cc7e18ce53c999bfc07","head":{"label":"freewing99:wp","ref":"wp","sha":"a791743b6de7276f40800cc7e18ce53c999bfc07","user":{"login":"freewing99","id":5119542,"avatar_url":"https://avatars.githubusercontent.com/u/5119542?v=3","gravatar_id":"","url":"https://api.github.com/users/freewing99","html_url":"https://github.com/freewing99","followers_url":"https://api.github.com/users/freewing99/followers","following_url":"https://api.github.com/users/freewing99/following{/other_user}","gists_url":"https://api.github.com/users/freewing99/gists{/gist_id}","starred_url":"https://api.github.com/users/freewing99/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freewing99/subscriptions","organizations_url":"https://api.github.com/users/freewing99/orgs","repos_url":"https://api.github.com/users/freewing99/repos","events_url":"https://api.github.com/users/freewing99/events{/privacy}","received_events_url":"https://api.github.com/users/freewing99/received_events","type":"User","site_admin":false},"repo":{"id":29326977,"name":"docs","full_name":"freewing99/docs","owner":{"login":"freewing99","id":5119542,"avatar_url":"https://avatars.githubusercontent.com/u/5119542?v=3","gravatar_id":"","url":"https://api.github.com/users/freewing99","html_url":"https://github.com/freewing99","followers_url":"https://api.github.com/users/freewing99/followers","following_url":"https://api.github.com/users/freewing99/following{/other_user}","gists_url":"https://api.github.com/users/freewing99/gists{/gist_id}","starred_url":"https://api.github.com/users/freewing99/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freewing99/subscriptions","organizations_url":"https://api.github.com/users/freewing99/orgs","repos_url":"https://api.github.com/users/freewing99/repos","events_url":"https://api.github.com/users/freewing99/events{/privacy}","received_events_url":"https://api.github.com/users/freewing99/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/freewing99/docs","description":"LeanCloud Documentation","fork":true,"url":"https://api.github.com/repos/freewing99/docs","forks_url":"https://api.github.com/repos/freewing99/docs/forks","keys_url":"https://api.github.com/repos/freewing99/docs/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freewing99/docs/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freewing99/docs/teams","hooks_url":"https://api.github.com/repos/freewing99/docs/hooks","issue_events_url":"https://api.github.com/repos/freewing99/docs/issues/events{/number}","events_url":"https://api.github.com/repos/freewing99/docs/events","assignees_url":"https://api.github.com/repos/freewing99/docs/assignees{/user}","branches_url":"https://api.github.com/repos/freewing99/docs/branches{/branch}","tags_url":"https://api.github.com/repos/freewing99/docs/tags","blobs_url":"https://api.github.com/repos/freewing99/docs/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freewing99/docs/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freewing99/docs/git/refs{/sha}","trees_url":"https://api.github.com/repos/freewing99/docs/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freewing99/docs/statuses/{sha}","languages_url":"https://api.github.com/repos/freewing99/docs/languages","stargazers_url":"https://api.github.com/repos/freewing99/docs/stargazers","contributors_url":"https://api.github.com/repos/freewing99/docs/contributors","subscribers_url":"https://api.github.com/repos/freewing99/docs/subscribers","subscription_url":"https://api.github.com/repos/freewing99/docs/subscription","commits_url":"https://api.github.com/repos/freewing99/docs/commits{/sha}","git_commits_url":"https://api.github.com/repos/freewing99/docs/git/commits{/sha}","comments_url":"https://api.github.com/repos/freewing99/docs/comments{/number}","issue_comment_url":"https://api.github.com/repos/freewing99/docs/issues/comments{/number}","contents_url":"https://api.github.com/repos/freewing99/docs/contents/{+path}","compare_url":"https://api.github.com/repos/freewing99/docs/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freewing99/docs/merges","archive_url":"https://api.github.com/repos/freewing99/docs/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freewing99/docs/downloads","issues_url":"https://api.github.com/repos/freewing99/docs/issues{/number}","pulls_url":"https://api.github.com/repos/freewing99/docs/pulls{/number}","milestones_url":"https://api.github.com/repos/freewing99/docs/milestones{/number}","notifications_url":"https://api.github.com/repos/freewing99/docs/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freewing99/docs/labels{/name}","releases_url":"https://api.github.com/repos/freewing99/docs/releases{/id}","created_at":"2015-01-16T01:31:32Z","updated_at":"2015-01-06T06:57:48Z","pushed_at":"2015-03-26T13:55:31Z","git_url":"git://github.com/freewing99/docs.git","ssh_url":"git@github.com:freewing99/docs.git","clone_url":"https://github.com/freewing99/docs.git","svn_url":"https://github.com/freewing99/docs","homepage":"http://leancloud.cn/docs/","size":16646,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"leancloud:master","ref":"master","sha":"35c88643c6eb9871ed7543d3c695c0513d4d918a","user":{"login":"leancloud","id":6660518,"avatar_url":"https://avatars.githubusercontent.com/u/6660518?v=3","gravatar_id":"","url":"https://api.github.com/users/leancloud","html_url":"https://github.com/leancloud","followers_url":"https://api.github.com/users/leancloud/followers","following_url":"https://api.github.com/users/leancloud/following{/other_user}","gists_url":"https://api.github.com/users/leancloud/gists{/gist_id}","starred_url":"https://api.github.com/users/leancloud/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/leancloud/subscriptions","organizations_url":"https://api.github.com/users/leancloud/orgs","repos_url":"https://api.github.com/users/leancloud/repos","events_url":"https://api.github.com/users/leancloud/events{/privacy}","received_events_url":"https://api.github.com/users/leancloud/received_events","type":"Organization","site_admin":false},"repo":{"id":25067518,"name":"docs","full_name":"leancloud/docs","owner":{"login":"leancloud","id":6660518,"avatar_url":"https://avatars.githubusercontent.com/u/6660518?v=3","gravatar_id":"","url":"https://api.github.com/users/leancloud","html_url":"https://github.com/leancloud","followers_url":"https://api.github.com/users/leancloud/followers","following_url":"https://api.github.com/users/leancloud/following{/other_user}","gists_url":"https://api.github.com/users/leancloud/gists{/gist_id}","starred_url":"https://api.github.com/users/leancloud/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/leancloud/subscriptions","organizations_url":"https://api.github.com/users/leancloud/orgs","repos_url":"https://api.github.com/users/leancloud/repos","events_url":"https://api.github.com/users/leancloud/events{/privacy}","received_events_url":"https://api.github.com/users/leancloud/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/leancloud/docs","description":"LeanCloud Documentation","fork":false,"url":"https://api.github.com/repos/leancloud/docs","forks_url":"https://api.github.com/repos/leancloud/docs/forks","keys_url":"https://api.github.com/repos/leancloud/docs/keys{/key_id}","collaborators_url":"https://api.github.com/repos/leancloud/docs/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/leancloud/docs/teams","hooks_url":"https://api.github.com/repos/leancloud/docs/hooks","issue_events_url":"https://api.github.com/repos/leancloud/docs/issues/events{/number}","events_url":"https://api.github.com/repos/leancloud/docs/events","assignees_url":"https://api.github.com/repos/leancloud/docs/assignees{/user}","branches_url":"https://api.github.com/repos/leancloud/docs/branches{/branch}","tags_url":"https://api.github.com/repos/leancloud/docs/tags","blobs_url":"https://api.github.com/repos/leancloud/docs/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/leancloud/docs/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/leancloud/docs/git/refs{/sha}","trees_url":"https://api.github.com/repos/leancloud/docs/git/trees{/sha}","statuses_url":"https://api.github.com/repos/leancloud/docs/statuses/{sha}","languages_url":"https://api.github.com/repos/leancloud/docs/languages","stargazers_url":"https://api.github.com/repos/leancloud/docs/stargazers","contributors_url":"https://api.github.com/repos/leancloud/docs/contributors","subscribers_url":"https://api.github.com/repos/leancloud/docs/subscribers","subscription_url":"https://api.github.com/repos/leancloud/docs/subscription","commits_url":"https://api.github.com/repos/leancloud/docs/commits{/sha}","git_commits_url":"https://api.github.com/repos/leancloud/docs/git/commits{/sha}","comments_url":"https://api.github.com/repos/leancloud/docs/comments{/number}","issue_comment_url":"https://api.github.com/repos/leancloud/docs/issues/comments{/number}","contents_url":"https://api.github.com/repos/leancloud/docs/contents/{+path}","compare_url":"https://api.github.com/repos/leancloud/docs/compare/{base}...{head}","merges_url":"https://api.github.com/repos/leancloud/docs/merges","archive_url":"https://api.github.com/repos/leancloud/docs/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/leancloud/docs/downloads","issues_url":"https://api.github.com/repos/leancloud/docs/issues{/number}","pulls_url":"https://api.github.com/repos/leancloud/docs/pulls{/number}","milestones_url":"https://api.github.com/repos/leancloud/docs/milestones{/number}","notifications_url":"https://api.github.com/repos/leancloud/docs/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/leancloud/docs/labels{/name}","releases_url":"https://api.github.com/repos/leancloud/docs/releases{/id}","created_at":"2014-10-11T07:14:37Z","updated_at":"2015-03-26T03:32:56Z","pushed_at":"2015-03-26T13:56:14Z","git_url":"git://github.com/leancloud/docs.git","ssh_url":"git@github.com:leancloud/docs.git","clone_url":"https://github.com/leancloud/docs.git","svn_url":"https://github.com/leancloud/docs","homepage":"http://leancloud.cn/docs/","size":20479,"stargazers_count":37,"watchers_count":37,"language":"HTML","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":35,"mirror_url":null,"open_issues_count":17,"forks":35,"open_issues":17,"watchers":37,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/leancloud/docs/pulls/170"},"html":{"href":"https://github.com/leancloud/docs/pull/170"},"issue":{"href":"https://api.github.com/repos/leancloud/docs/issues/170"},"comments":{"href":"https://api.github.com/repos/leancloud/docs/issues/170/comments"},"review_comments":{"href":"https://api.github.com/repos/leancloud/docs/pulls/170/comments"},"review_comment":{"href":"https://api.github.com/repos/leancloud/docs/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/leancloud/docs/pulls/170/commits"},"statuses":{"href":"https://api.github.com/repos/leancloud/docs/statuses/a791743b6de7276f40800cc7e18ce53c999bfc07"}}}}