IM

IM

7
回复

收集基于环信SDK开发的开源项目 开源项目

回眸淡然笑 回复了问题 • 9 人关注 • 3978 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9039 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 7985 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
回复

EMMessageListener 里的 onMessageDelivered为什么不触发 环信_Android IM

回复

朔月之外 发起了问题 • 1 人关注 • 56 次浏览 • 2017-12-03 18:13 • 来自相关话题

0
评论

【环信征文】集成环信,并实现消息免打扰 即时通讯 环信 消息免打扰 IM iOS

cokeyer 发表了文章 • 164 次浏览 • 2017-09-14 11:27 • 来自相关话题

现在大多数社交app都有消息免打扰功能,因为环信SDK主要是对即时通讯模块进行封装,因此如果要实现消息免打扰功能则需要开发者自己对此功能逻辑进行处理。
先解释一下,本人项目中对消息免打扰功能的定义:正常情况下,我们收到的每一条聊天消息都会收到小红点、声音、震动的提示。如果对某个好友设置消息免打扰功能,则只提示小红点,声音和震动则不再提示。

下面简述结合环信SDK时,此功能的实现方法。
环信将即时聊天的所有功能分为四大模块进行管理://聊天模块:
[EMClient sharedClient].chatManager
//好友模块 :
[EMClient sharedClient].contactManager
//群组模块 :
[EMClient sharedClient].groupManager
//聊天室模块:
[EMClient sharedClient].roomManager消息免打扰的功能借助聊天模块[EMClient sharedClient].chatManager的API就能实现。
一般来说,在app内不管当前在哪个界面,只要收到消息都需要被判断是否需要免打扰,因此可以在appdelegate里写如下代码,app通常都有tabBarController,那么也可以让tabBarController来实现如下代码。
首先让控制器tabBarController遵守代理EMChatManagerDelegate
然后成为代理
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

EMChatManagerDelegate中有一个代理方法- (void)didReceiveMessages:(NSArray *)aMessages;实现此代理方法- (void)didReceiveMessages:(NSArray *)aMessages{
[self setupUnreadMessageCount];
EMMessage *message = aMessages[0];
NSString *sendPerson = message.from;
BHNavigatiomController *imNaviCV = self.viewControllers[1];
BHConversationListController *converCV = imNaviCV.viewControllers[0];
[converCV refreshDataSource];//只要收到消息就从服务器拿
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {
if ([hx_name isEqualToString:sendPerson]) {
return;
}
}
switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}
}方法中有一个aMessages参数。这个参数是一个消息组,每一个元素都是EMMessage类型的实例。
因为通常都只有一条信息。因此只需要取出EMMessage *message = aMessages[0];
EMMessage类中有许多属性,因此根据一条信息基本可以获取想知道的所有信息。这里我们只需要知道此消息的发送方即可,也就是发送方的环信idNSString *sendPerson = message.from;下面代码中有一AppEngine.IMDataCent.data_ExcuseFriendsData,这个数组里面装的都是已经被设置消息免打扰的好友的环信id,是请求自己服务器获得的,获取的代码最好在一打开app时,就及时获取到。然后根据对好友免打扰设置的操作,访问后台接口进行增删,并刷新数组与后端保持一致即可。
通过[hx_name isEqualToString:sendPerson],遍历免打扰数组与当前消息的发送方环信id,就可以知道是否需要免打扰了。
 
下面附上完整代码//
// BHTabBarController.m
// ShangHeYiYang
//
// Created by LiBohan on 2017/8/24.
// Copyright © 2017年 xxxxx. All rights reserved.
//


//两次提示的默认间隔
static const CGFloat kDefaultPlaySoundInterval = 3.0;
static NSString *kMessageType = @"MessageType";
static NSString *kConversationChatter = @"ConversationChatter";
static NSString *kGroupName = @"GroupName";


#import "BHTabBarController.h"
#import <UserNotifications/UserNotifications.h>
#import "BHConversationListController.h"

@interface BHTabBarController ()<EMChatManagerDelegate,EMContactManagerDelegate>

@property (strong, nonatomic) NSDate *lastPlaySoundDate;

@end

@implementation BHTabBarController

- (void)viewDidLoad {
[super viewDidLoad];

[DemoCallManager sharedManager].mainController = self;

[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

[[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupUnreadMessageCount) name:@"setupUnreadMessageCount" object:nil];

}

-(void)friendshipDidRemoveByUser:(NSString *)aUsername{

// __weak __typeof(&*self)weakSelf = self;

dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:contactReloadData object:nil];
});


// if ([AppEngine.mainDataCent.data_UserData.data_HxID isEqualToString:aUsername]) {


//删除好友成功后,再删除聊天会话
[[EMClient sharedClient].chatManager deleteConversation:aUsername isDeleteMessages:YES completion:^(NSString *aConversationId, EMError *aError) {

if (!aError) {
//删除聊天会话,成功后,刷新聊天会话列表
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:talkBtnClickThenUpdateConversionlist object:nil];
});


}

}];
}

// 统计未读消息数
-(void)setupUnreadMessageCount
{
NSArray *conversations = [[EMClient sharedClient].chatManager getAllConversations];
NSInteger unreadCount = 0;
for (EMConversation *conversation in conversations) {
unreadCount += conversation.unreadMessagesCount;
}

NSArray *tabBarItems = self.tabBar.items;

UITabBarItem *conlistTabBarItem = [tabBarItems objectAtIndex:1];
if (unreadCount > 0) {
conlistTabBarItem.badgeValue = [NSString stringWithFormat:@"%i",(int)unreadCount];
}else{
conlistTabBarItem.badgeValue = nil;
}


// UIApplication *application = [UIApplication sharedApplication];
// [application setApplicationIconBadgeNumber:unreadCount];
}


- (void)cmdMessagesDidReceive:(NSArray *)aCmdMessages {
for (EMMessage *message in aCmdMessages) {
EMCmdMessageBody *body = (EMCmdMessageBody *)message.body;
NSLog(@"收到的action是 -- %@",body.action);


if (body.action == nil) {

return;

}

NSData *jsonData = [body.action dataUsingEncoding:NSUTF8StringEncoding];

NSError *err;

NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err];

if(err) {

NSLog(@"json解析失败:%@",err);

return;

}

NSNumber *num = dic[@"count"];

NSString *str = [num stringValue];

[AppEngine.IMDataCent requestUpdateUnreadNumberWithUnreadNumber:str];

}


}

- (void)didReceiveMessages:(NSArray *)aMessages{

// [self refresh];

[self setupUnreadMessageCount];

EMMessage *message = aMessages[0];

NSString *sendPerson = message.from;

BHNavigatiomController *imNaviCV = self.viewControllers[1];

BHConversationListController *converCV = imNaviCV.viewControllers[0];

// [converCV refresh];

[converCV refreshDataSource];//只要收到消息就从服务器拿

UIApplicationState state = [[UIApplication sharedApplication] applicationState];

for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {

if ([hx_name isEqualToString:sendPerson]) {
return;
}

}

switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}

}

- (void)playSoundAndVibration{
NSTimeInterval timeInterval = [[NSDate date]
timeIntervalSinceDate:self.lastPlaySoundDate];
if (timeInterval < kDefaultPlaySoundInterval) {
//如果距离上次响铃和震动时间太短, 则跳过响铃
NSLog(@"skip ringing & vibration %@, %@", [NSDate date], self.lastPlaySoundDate);
return;
}

//保存最后一次响铃时间
self.lastPlaySoundDate = [NSDate date];

// 收到消息时,播放音频
[[EMCDDeviceManager sharedInstance] playNewMessageSound];
// 收到消息时,震动
[[EMCDDeviceManager sharedInstance] playVibration];
}


- (void)showNotificationWithMessage:(EMMessage *)message
{
EMPushOptions *options = [[EMClient sharedClient] pushOptions];
NSString *alertBody = nil;
if (options.displayStyle == EMPushDisplayStyleMessageSummary) {
EMMessageBody *messageBody = message.body;
NSString *messageStr = nil;
switch (messageBody.type) {
case EMMessageBodyTypeText:
{
messageStr = ((EMTextMessageBody *)messageBody).text;
}
break;
case EMMessageBodyTypeImage:
{
messageStr = NSLocalizedString(@"message.image", @"Image");
}
break;
case EMMessageBodyTypeLocation:
{
messageStr = NSLocalizedString(@"message.location", @"Location");
}
break;
case EMMessageBodyTypeVoice:
{
messageStr = NSLocalizedString(@"message.voice", @"Voice");
}
break;
case EMMessageBodyTypeVideo:{
messageStr = NSLocalizedString(@"message.video", @"Video");
}
break;
default:
break;
}

do {
// NSString *title = [[UserProfileManager sharedInstance] getNickNameWithUsername:message.from];
NSString *title = @"大佬";

if (message.chatType == EMChatTypeGroupChat) {
NSDictionary *ext = message.ext;
if (ext && ext[kGroupMessageAtList]) {
id target = ext[kGroupMessageAtList];
if ([target isKindOfClass:[NSString class]]) {
if ([kGroupMessageAtAll compare:target options:NSCaseInsensitiveSearch] == NSOrderedSame) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
else if ([target isKindOfClass:[NSArray class]]) {
NSArray *atTargets = (NSArray*)target;
if ([atTargets containsObject:[EMClient sharedClient].currentUsername]) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
}
NSArray *groupArray = [[EMClient sharedClient].groupManager getJoinedGroups];
for (EMGroup *group in groupArray) {
if ([group.groupId isEqualToString:message.conversationId]) {
title = [NSString stringWithFormat:@"%@(%@)", message.from, group.subject];
break;
}
}
}
else if (message.chatType == EMChatTypeChatRoom)
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *key = [NSString stringWithFormat:@"OnceJoinedChatrooms_%@", [[EMClient sharedClient] currentUsername]];
NSMutableDictionary *chatrooms = [NSMutableDictionary dictionaryWithDictionary:[ud objectForKey:key]];
NSString *chatroomName = [chatrooms objectForKey:message.conversationId];
if (chatroomName)
{
title = [NSString stringWithFormat:@"%@(%@)", message.from, chatroomName];
}
}

alertBody = [NSString stringWithFormat:@"%@:%@", title, messageStr];
} while (0);
}
else{
// alertBody = NSLocalizedString(@"receiveMessage", @"you have a new message");
alertBody = @"您有一条消息";
}

NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:self.lastPlaySoundDate];
BOOL playSound = NO;
if (!self.lastPlaySoundDate || timeInterval >= kDefaultPlaySoundInterval) {
self.lastPlaySoundDate = [NSDate date];
playSound = YES;
}

NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:[NSNumber numberWithInt:message.chatType] forKey:kMessageType];
[userInfo setObject:message.conversationId forKey:kConversationChatter];

//发送本地推送
if (NSClassFromString(@"UNUserNotificationCenter")) {
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
if (playSound) {
content.sound = [UNNotificationSound defaultSound];
}
content.body =alertBody;
content.userInfo = userInfo;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:message.messageId content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
}
else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [NSDate date]; //触发通知的时间
notification.alertBody = alertBody;
notification.alertAction = NSLocalizedString(@"open", @"Open");
notification.timeZone = [NSTimeZone defaultTimeZone];
if (playSound) {
notification.soundName = UILocalNotificationDefaultSoundName;
}
notification.userInfo = userInfo;

//发送通知
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
}

- (void)messagesDidDeliver:(NSArray *)aMessages{

NSLog(@"sf");
}


- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/

@end


 
 
本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂 查看全部
现在大多数社交app都有消息免打扰功能,因为环信SDK主要是对即时通讯模块进行封装,因此如果要实现消息免打扰功能则需要开发者自己对此功能逻辑进行处理。
先解释一下,本人项目中对消息免打扰功能的定义:正常情况下,我们收到的每一条聊天消息都会收到小红点、声音、震动的提示。如果对某个好友设置消息免打扰功能,则只提示小红点,声音和震动则不再提示。

下面简述结合环信SDK时,此功能的实现方法。
环信将即时聊天的所有功能分为四大模块进行管理:
//聊天模块:
[EMClient sharedClient].chatManager
//好友模块 :
[EMClient sharedClient].contactManager
//群组模块 :
[EMClient sharedClient].groupManager
//聊天室模块:
[EMClient sharedClient].roomManager
消息免打扰的功能借助聊天模块[EMClient sharedClient].chatManager的API就能实现。
一般来说,在app内不管当前在哪个界面,只要收到消息都需要被判断是否需要免打扰,因此可以在appdelegate里写如下代码,app通常都有tabBarController,那么也可以让tabBarController来实现如下代码。
首先让控制器tabBarController遵守代理EMChatManagerDelegate
然后成为代理
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

EMChatManagerDelegate中有一个代理方法
- (void)didReceiveMessages:(NSArray *)aMessages;
实现此代理方法
- (void)didReceiveMessages:(NSArray *)aMessages{
[self setupUnreadMessageCount];
EMMessage *message = aMessages[0];
NSString *sendPerson = message.from;
BHNavigatiomController *imNaviCV = self.viewControllers[1];
BHConversationListController *converCV = imNaviCV.viewControllers[0];
[converCV refreshDataSource];//只要收到消息就从服务器拿
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {
if ([hx_name isEqualToString:sendPerson]) {
return;
}
}
switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}
}
方法中有一个aMessages参数。这个参数是一个消息组,每一个元素都是EMMessage类型的实例。
因为通常都只有一条信息。因此只需要取出EMMessage *message = aMessages[0];
EMMessage类中有许多属性,因此根据一条信息基本可以获取想知道的所有信息。这里我们只需要知道此消息的发送方即可,也就是发送方的环信idNSString *sendPerson = message.from;下面代码中有一AppEngine.IMDataCent.data_ExcuseFriendsData,这个数组里面装的都是已经被设置消息免打扰的好友的环信id,是请求自己服务器获得的,获取的代码最好在一打开app时,就及时获取到。然后根据对好友免打扰设置的操作,访问后台接口进行增删,并刷新数组与后端保持一致即可。
通过[hx_name isEqualToString:sendPerson],遍历免打扰数组与当前消息的发送方环信id,就可以知道是否需要免打扰了。
 
下面附上完整代码
//
// BHTabBarController.m
// ShangHeYiYang
//
// Created by LiBohan on 2017/8/24.
// Copyright © 2017年 xxxxx. All rights reserved.
//


//两次提示的默认间隔
static const CGFloat kDefaultPlaySoundInterval = 3.0;
static NSString *kMessageType = @"MessageType";
static NSString *kConversationChatter = @"ConversationChatter";
static NSString *kGroupName = @"GroupName";


#import "BHTabBarController.h"
#import <UserNotifications/UserNotifications.h>
#import "BHConversationListController.h"

@interface BHTabBarController ()<EMChatManagerDelegate,EMContactManagerDelegate>

@property (strong, nonatomic) NSDate *lastPlaySoundDate;

@end

@implementation BHTabBarController

- (void)viewDidLoad {
[super viewDidLoad];

[DemoCallManager sharedManager].mainController = self;

[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

[[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupUnreadMessageCount) name:@"setupUnreadMessageCount" object:nil];

}

-(void)friendshipDidRemoveByUser:(NSString *)aUsername{

// __weak __typeof(&*self)weakSelf = self;

dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:contactReloadData object:nil];
});


// if ([AppEngine.mainDataCent.data_UserData.data_HxID isEqualToString:aUsername]) {


//删除好友成功后,再删除聊天会话
[[EMClient sharedClient].chatManager deleteConversation:aUsername isDeleteMessages:YES completion:^(NSString *aConversationId, EMError *aError) {

if (!aError) {
//删除聊天会话,成功后,刷新聊天会话列表
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:talkBtnClickThenUpdateConversionlist object:nil];
});


}

}];
}

// 统计未读消息数
-(void)setupUnreadMessageCount
{
NSArray *conversations = [[EMClient sharedClient].chatManager getAllConversations];
NSInteger unreadCount = 0;
for (EMConversation *conversation in conversations) {
unreadCount += conversation.unreadMessagesCount;
}

NSArray *tabBarItems = self.tabBar.items;

UITabBarItem *conlistTabBarItem = [tabBarItems objectAtIndex:1];
if (unreadCount > 0) {
conlistTabBarItem.badgeValue = [NSString stringWithFormat:@"%i",(int)unreadCount];
}else{
conlistTabBarItem.badgeValue = nil;
}


// UIApplication *application = [UIApplication sharedApplication];
// [application setApplicationIconBadgeNumber:unreadCount];
}


- (void)cmdMessagesDidReceive:(NSArray *)aCmdMessages {
for (EMMessage *message in aCmdMessages) {
EMCmdMessageBody *body = (EMCmdMessageBody *)message.body;
NSLog(@"收到的action是 -- %@",body.action);


if (body.action == nil) {

return;

}

NSData *jsonData = [body.action dataUsingEncoding:NSUTF8StringEncoding];

NSError *err;

NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err];

if(err) {

NSLog(@"json解析失败:%@",err);

return;

}

NSNumber *num = dic[@"count"];

NSString *str = [num stringValue];

[AppEngine.IMDataCent requestUpdateUnreadNumberWithUnreadNumber:str];

}


}

- (void)didReceiveMessages:(NSArray *)aMessages{

// [self refresh];

[self setupUnreadMessageCount];

EMMessage *message = aMessages[0];

NSString *sendPerson = message.from;

BHNavigatiomController *imNaviCV = self.viewControllers[1];

BHConversationListController *converCV = imNaviCV.viewControllers[0];

// [converCV refresh];

[converCV refreshDataSource];//只要收到消息就从服务器拿

UIApplicationState state = [[UIApplication sharedApplication] applicationState];

for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {

if ([hx_name isEqualToString:sendPerson]) {
return;
}

}

switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}

}

- (void)playSoundAndVibration{
NSTimeInterval timeInterval = [[NSDate date]
timeIntervalSinceDate:self.lastPlaySoundDate];
if (timeInterval < kDefaultPlaySoundInterval) {
//如果距离上次响铃和震动时间太短, 则跳过响铃
NSLog(@"skip ringing & vibration %@, %@", [NSDate date], self.lastPlaySoundDate);
return;
}

//保存最后一次响铃时间
self.lastPlaySoundDate = [NSDate date];

// 收到消息时,播放音频
[[EMCDDeviceManager sharedInstance] playNewMessageSound];
// 收到消息时,震动
[[EMCDDeviceManager sharedInstance] playVibration];
}


- (void)showNotificationWithMessage:(EMMessage *)message
{
EMPushOptions *options = [[EMClient sharedClient] pushOptions];
NSString *alertBody = nil;
if (options.displayStyle == EMPushDisplayStyleMessageSummary) {
EMMessageBody *messageBody = message.body;
NSString *messageStr = nil;
switch (messageBody.type) {
case EMMessageBodyTypeText:
{
messageStr = ((EMTextMessageBody *)messageBody).text;
}
break;
case EMMessageBodyTypeImage:
{
messageStr = NSLocalizedString(@"message.image", @"Image");
}
break;
case EMMessageBodyTypeLocation:
{
messageStr = NSLocalizedString(@"message.location", @"Location");
}
break;
case EMMessageBodyTypeVoice:
{
messageStr = NSLocalizedString(@"message.voice", @"Voice");
}
break;
case EMMessageBodyTypeVideo:{
messageStr = NSLocalizedString(@"message.video", @"Video");
}
break;
default:
break;
}

do {
// NSString *title = [[UserProfileManager sharedInstance] getNickNameWithUsername:message.from];
NSString *title = @"大佬";

if (message.chatType == EMChatTypeGroupChat) {
NSDictionary *ext = message.ext;
if (ext && ext[kGroupMessageAtList]) {
id target = ext[kGroupMessageAtList];
if ([target isKindOfClass:[NSString class]]) {
if ([kGroupMessageAtAll compare:target options:NSCaseInsensitiveSearch] == NSOrderedSame) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
else if ([target isKindOfClass:[NSArray class]]) {
NSArray *atTargets = (NSArray*)target;
if ([atTargets containsObject:[EMClient sharedClient].currentUsername]) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
}
NSArray *groupArray = [[EMClient sharedClient].groupManager getJoinedGroups];
for (EMGroup *group in groupArray) {
if ([group.groupId isEqualToString:message.conversationId]) {
title = [NSString stringWithFormat:@"%@(%@)", message.from, group.subject];
break;
}
}
}
else if (message.chatType == EMChatTypeChatRoom)
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *key = [NSString stringWithFormat:@"OnceJoinedChatrooms_%@", [[EMClient sharedClient] currentUsername]];
NSMutableDictionary *chatrooms = [NSMutableDictionary dictionaryWithDictionary:[ud objectForKey:key]];
NSString *chatroomName = [chatrooms objectForKey:message.conversationId];
if (chatroomName)
{
title = [NSString stringWithFormat:@"%@(%@)", message.from, chatroomName];
}
}

alertBody = [NSString stringWithFormat:@"%@:%@", title, messageStr];
} while (0);
}
else{
// alertBody = NSLocalizedString(@"receiveMessage", @"you have a new message");
alertBody = @"您有一条消息";
}

NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:self.lastPlaySoundDate];
BOOL playSound = NO;
if (!self.lastPlaySoundDate || timeInterval >= kDefaultPlaySoundInterval) {
self.lastPlaySoundDate = [NSDate date];
playSound = YES;
}

NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:[NSNumber numberWithInt:message.chatType] forKey:kMessageType];
[userInfo setObject:message.conversationId forKey:kConversationChatter];

//发送本地推送
if (NSClassFromString(@"UNUserNotificationCenter")) {
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
if (playSound) {
content.sound = [UNNotificationSound defaultSound];
}
content.body =alertBody;
content.userInfo = userInfo;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:message.messageId content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
}
else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [NSDate date]; //触发通知的时间
notification.alertBody = alertBody;
notification.alertAction = NSLocalizedString(@"open", @"Open");
notification.timeZone = [NSTimeZone defaultTimeZone];
if (playSound) {
notification.soundName = UILocalNotificationDefaultSoundName;
}
notification.userInfo = userInfo;

//发送通知
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
}

- (void)messagesDidDeliver:(NSArray *)aMessages{

NSLog(@"sf");
}


- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/

@end


 
 
本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂

qrcode_for_gh_bc92a063b4a2_430.jpg
0
评论

【环信征文】基于环信开发一个医疗APP-Ⅰ iOS 环信 即时通讯 IM

cokeyer 发表了文章 • 201 次浏览 • 2017-09-12 17:56 • 来自相关话题

公司又有了新项目,依然是含有即时通讯功能模块的项目。在经历了上个项目对环信sdk的集成后,对环信EaseUI有了大概的了解。这次果断还是集成环信,一回生二回熟,最主要的还是对环信IM稳定性非常放心!项目医疗类的项目,角色分医生和患者,双方都可主动发起会话,但如果是患者找医生聊天,必须先经过预约,并只能在预约时间区间内才能和医生发送聊天消息、图片、语音、实时音视频。项目基于环信最V3.3.4版本开发,首先在会话界面自定义一个类比如BHChatViewController,继承自EaseMessageViewController类,基本上一个简单的界面就有了。以下就是EaseMessageViewController类的发送各种消息的方法,那么根据需要只需重写以下方法即可。/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@result
*/
- (void)sendTextMessage:(NSString *)text;

/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@param ext 扩展信息
@result
*/
- (void)sendTextMessage:(NSString *)text withExt:(NSDictionary*)ext;

/*!
@method
@brief 发送图片消息
@discussion
@param image 发送图片
@result
*/
- (void)sendImageMessage:(UIImage *)image;

/*!
@method
@brief 发送位置消息
@discussion
@param latitude 经度
@param longitude 纬度
@param address 地址
@result
*/
- (void)sendLocationMessageLatitude:(double)latitude
longitude:(double)longitude
andAddress:(NSString *)address;

/*!
@method
@brief 发送语音消息
@discussion
@param localPath 语音本地地址
@param duration 时长
@result
*/
- (void)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration;

/*!
@method
@brief 发送视频消息
@discussion
@param url 视频url
@result
*/
- (void)sendVideoMessageWithURL:(NSURL *)url;我在对发送图片的操作进行处理的时候,发现当拍照发图时,走这个方法//当你拍照发图时,走这个方法
- (void)sendImageMessage:(UIImage *)image;但当从相册选择图片发送时,会发现,不走上面的方法了。
仔细检查代码发现走了EaseMessageViewController.m的如下方法





然而这个方法,环信并没有放在EaseMessageViewController.h成为公开方法。我们只需手动粘贴方法到.h,然后在自己的子类重写就可以了。

还有一个关于更多下图更多功能区域的问题





如上图所示,相册、拍照、视频等附加功能按钮,环信用EaseChatBarMoreView类来管理的。
如果需要增加功能按钮用这个方法/*!
@method
@brief 新增一个新的功能按钮
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@result
*/
- (void)insertItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title;移除某个功能按钮用这个方法/*!
@method
@brief 根据索引删除功能按钮
@discussion
@param index 按钮索引
@result
*/
- (void)removeItematIndex:(NSInteger)index;修改一个功能按钮用这个方法/*!
@method
@brief 修改功能按钮图片
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@param index 按钮索引
@result
*/
- (void)updateItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title
atIndex:(NSInteger)index;可当你添加按钮,或者修改按钮时,会发现按钮的名字设置不了
然后检查环信内部的实现,发现title的值在方法里根本就没用到?!
索性不用updateItemWithImage这个方法了,直接去改内部代码。
修改代码如下

到EaseChatBarMoreView.m修改- (void)setupSubviewsForType:(EMChatToolbarType)type方法。改动的部分在代码后面有标注。- (void)setupSubviewsForType:(EMChatToolbarType)type
{
//self.backgroundColor = [UIColor clearColor];
self.accessibilityIdentifier = @"more_view";

_scrollview = [[UIScrollView alloc] init];
_scrollview.pagingEnabled = YES;
_scrollview.showsHorizontalScrollIndicator = NO;
_scrollview.showsVerticalScrollIndicator = NO;
_scrollview.delegate = self;
[self addSubview:_scrollview];

_pageControl = [[UIPageControl alloc] init];
_pageControl.currentPage = 0;
_pageControl.numberOfPages = 1;
[self addSubview:_pageControl];

CGFloat insets = (self.frame.size.width - 4 * CHAT_BUTTON_SIZE) / 5;

_photoButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_photoButton setTitle:@"相册" forState:UIControlStateNormal];//改动
[_photoButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_photoButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_photoButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_photoButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_photoButton.accessibilityIdentifier = @"image";
[_photoButton setFrame:CGRectMake(insets, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photo"] forState:UIControlStateNormal];
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photoSelected"] forState:UIControlStateHighlighted];
[_photoButton addTarget:self action:@selector(photoAction) forControlEvents:UIControlEventTouchUpInside];
_photoButton.tag = MOREVIEW_BUTTON_TAG;
[_scrollview addSubview:_photoButton];

_locationButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_locationButton setTitle:@"位置" forState:UIControlStateNormal];//改动
[_locationButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_locationButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_locationButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_locationButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_locationButton.accessibilityIdentifier = @"location";
[_locationButton setFrame:CGRectMake(insets * 2 + CHAT_BUTTON_SIZE, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_location"] forState:UIControlStateNormal];
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_locationSelected"] forState:UIControlStateHighlighted];
[_locationButton addTarget:self action:@selector(locationAction) forControlEvents:UIControlEventTouchUpInside];
_locationButton.tag = MOREVIEW_BUTTON_TAG + 1;
[_scrollview addSubview:_locationButton];

_takePicButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_takePicButton setTitle:@"拍照" forState:UIControlStateNormal];//改动
[_takePicButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_takePicButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_takePicButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_takePicButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_takePicButton setFrame:CGRectMake(insets * 3 + CHAT_BUTTON_SIZE * 2, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_camera"] forState:UIControlStateNormal];
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_cameraSelected"] forState:UIControlStateHighlighted];
[_takePicButton addTarget:self action:@selector(takePicAction) forControlEvents:UIControlEventTouchUpInside];
_takePicButton.tag = MOREVIEW_BUTTON_TAG + 2;
_maxIndex = 2;
[_scrollview addSubview:_takePicButton];

CGRect frame = self.frame;
if (type == EMChatToolbarTypeChat) {
frame.size.height = 150;
_audioCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_audioCallButton setTitle:@"语音" forState:UIControlStateNormal];//改动
[_audioCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_audioCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_audioCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_audioCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_audioCallButton setFrame:CGRectMake(insets * 4 + CHAT_BUTTON_SIZE * 3, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCall"] forState:UIControlStateNormal];
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCallSelected"] forState:UIControlStateHighlighted];
[_audioCallButton addTarget:self action:@selector(takeAudioCallAction) forControlEvents:UIControlEventTouchUpInside];
_audioCallButton.tag = MOREVIEW_BUTTON_TAG + 3;
[_scrollview addSubview:_audioCallButton];

_videoCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_videoCallButton setTitle:@"视频" forState:UIControlStateNormal];//改动
[_videoCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_videoCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_videoCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_videoCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_videoCallButton setFrame:CGRectMake(insets, 10 * 2 + CHAT_BUTTON_SIZE + 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCall"] forState:UIControlStateNormal];
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCallSelected"] forState:UIControlStateHighlighted];
[_videoCallButton addTarget:self action:@selector(takeVideoCallAction) forControlEvents:UIControlEventTouchUpInside];
_videoCallButton.tag =MOREVIEW_BUTTON_TAG + 4;
_maxIndex = 4;
[_scrollview addSubview:_videoCallButton];
}
else if (type == EMChatToolbarTypeGroup)
{
frame.size.height = 80;
}
self.frame = frame;
_scrollview.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
_pageControl.frame = CGRectMake(0, CGRectGetHeight(frame) - 20, CGRectGetWidth(frame), 20);
_pageControl.hidden = _pageControl.numberOfPages<=1;
}




本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂





 
项目完整源码 查看全部
公司又有了新项目,依然是含有即时通讯功能模块的项目。在经历了上个项目对环信sdk的集成后,对环信EaseUI有了大概的了解。这次果断还是集成环信,一回生二回熟,最主要的还是对环信IM稳定性非常放心!
项目医疗类的项目,角色分医生和患者,双方都可主动发起会话,但如果是患者找医生聊天,必须先经过预约,并只能在预约时间区间内才能和医生发送聊天消息、图片、语音、实时音视频。项目基于环信最V3.3.4版本开发,首先在会话界面自定义一个类比如BHChatViewController,继承自EaseMessageViewController类,基本上一个简单的界面就有了。以下就是EaseMessageViewController类的发送各种消息的方法,那么根据需要只需重写以下方法即可。
/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@result
*/
- (void)sendTextMessage:(NSString *)text;

/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@param ext 扩展信息
@result
*/
- (void)sendTextMessage:(NSString *)text withExt:(NSDictionary*)ext;

/*!
@method
@brief 发送图片消息
@discussion
@param image 发送图片
@result
*/
- (void)sendImageMessage:(UIImage *)image;

/*!
@method
@brief 发送位置消息
@discussion
@param latitude 经度
@param longitude 纬度
@param address 地址
@result
*/
- (void)sendLocationMessageLatitude:(double)latitude
longitude:(double)longitude
andAddress:(NSString *)address;

/*!
@method
@brief 发送语音消息
@discussion
@param localPath 语音本地地址
@param duration 时长
@result
*/
- (void)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration;

/*!
@method
@brief 发送视频消息
@discussion
@param url 视频url
@result
*/
- (void)sendVideoMessageWithURL:(NSURL *)url;
我在对发送图片的操作进行处理的时候,发现当拍照发图时,走这个方法
//当你拍照发图时,走这个方法
- (void)sendImageMessage:(UIImage *)image;
但当从相册选择图片发送时,会发现,不走上面的方法了。
仔细检查代码发现走了EaseMessageViewController.m的如下方法

环信2.png

然而这个方法,环信并没有放在EaseMessageViewController.h成为公开方法。我们只需手动粘贴方法到.h,然后在自己的子类重写就可以了。

还有一个关于更多下图更多功能区域的问题

EaseChatBarMoreView.png

如上图所示,相册、拍照、视频等附加功能按钮,环信用EaseChatBarMoreView类来管理的。
如果需要增加功能按钮用这个方法
/*!
@method
@brief 新增一个新的功能按钮
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@result
*/
- (void)insertItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title;
移除某个功能按钮用这个方法
/*!
@method
@brief 根据索引删除功能按钮
@discussion
@param index 按钮索引
@result
*/
- (void)removeItematIndex:(NSInteger)index;
修改一个功能按钮用这个方法
/*!
@method
@brief 修改功能按钮图片
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@param index 按钮索引
@result
*/
- (void)updateItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title
atIndex:(NSInteger)index;
可当你添加按钮,或者修改按钮时,会发现按钮的名字设置不了
然后检查环信内部的实现,发现title的值在方法里根本就没用到?!
索性不用updateItemWithImage这个方法了,直接去改内部代码。
修改代码如下

到EaseChatBarMoreView.m修改- (void)setupSubviewsForType:(EMChatToolbarType)type方法。改动的部分在代码后面有标注。
- (void)setupSubviewsForType:(EMChatToolbarType)type
{
//self.backgroundColor = [UIColor clearColor];
self.accessibilityIdentifier = @"more_view";

_scrollview = [[UIScrollView alloc] init];
_scrollview.pagingEnabled = YES;
_scrollview.showsHorizontalScrollIndicator = NO;
_scrollview.showsVerticalScrollIndicator = NO;
_scrollview.delegate = self;
[self addSubview:_scrollview];

_pageControl = [[UIPageControl alloc] init];
_pageControl.currentPage = 0;
_pageControl.numberOfPages = 1;
[self addSubview:_pageControl];

CGFloat insets = (self.frame.size.width - 4 * CHAT_BUTTON_SIZE) / 5;

_photoButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_photoButton setTitle:@"相册" forState:UIControlStateNormal];//改动
[_photoButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_photoButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_photoButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_photoButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_photoButton.accessibilityIdentifier = @"image";
[_photoButton setFrame:CGRectMake(insets, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photo"] forState:UIControlStateNormal];
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photoSelected"] forState:UIControlStateHighlighted];
[_photoButton addTarget:self action:@selector(photoAction) forControlEvents:UIControlEventTouchUpInside];
_photoButton.tag = MOREVIEW_BUTTON_TAG;
[_scrollview addSubview:_photoButton];

_locationButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_locationButton setTitle:@"位置" forState:UIControlStateNormal];//改动
[_locationButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_locationButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_locationButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_locationButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_locationButton.accessibilityIdentifier = @"location";
[_locationButton setFrame:CGRectMake(insets * 2 + CHAT_BUTTON_SIZE, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_location"] forState:UIControlStateNormal];
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_locationSelected"] forState:UIControlStateHighlighted];
[_locationButton addTarget:self action:@selector(locationAction) forControlEvents:UIControlEventTouchUpInside];
_locationButton.tag = MOREVIEW_BUTTON_TAG + 1;
[_scrollview addSubview:_locationButton];

_takePicButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_takePicButton setTitle:@"拍照" forState:UIControlStateNormal];//改动
[_takePicButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_takePicButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_takePicButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_takePicButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_takePicButton setFrame:CGRectMake(insets * 3 + CHAT_BUTTON_SIZE * 2, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_camera"] forState:UIControlStateNormal];
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_cameraSelected"] forState:UIControlStateHighlighted];
[_takePicButton addTarget:self action:@selector(takePicAction) forControlEvents:UIControlEventTouchUpInside];
_takePicButton.tag = MOREVIEW_BUTTON_TAG + 2;
_maxIndex = 2;
[_scrollview addSubview:_takePicButton];

CGRect frame = self.frame;
if (type == EMChatToolbarTypeChat) {
frame.size.height = 150;
_audioCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_audioCallButton setTitle:@"语音" forState:UIControlStateNormal];//改动
[_audioCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_audioCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_audioCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_audioCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_audioCallButton setFrame:CGRectMake(insets * 4 + CHAT_BUTTON_SIZE * 3, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCall"] forState:UIControlStateNormal];
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCallSelected"] forState:UIControlStateHighlighted];
[_audioCallButton addTarget:self action:@selector(takeAudioCallAction) forControlEvents:UIControlEventTouchUpInside];
_audioCallButton.tag = MOREVIEW_BUTTON_TAG + 3;
[_scrollview addSubview:_audioCallButton];

_videoCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_videoCallButton setTitle:@"视频" forState:UIControlStateNormal];//改动
[_videoCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_videoCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_videoCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_videoCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_videoCallButton setFrame:CGRectMake(insets, 10 * 2 + CHAT_BUTTON_SIZE + 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCall"] forState:UIControlStateNormal];
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCallSelected"] forState:UIControlStateHighlighted];
[_videoCallButton addTarget:self action:@selector(takeVideoCallAction) forControlEvents:UIControlEventTouchUpInside];
_videoCallButton.tag =MOREVIEW_BUTTON_TAG + 4;
_maxIndex = 4;
[_scrollview addSubview:_videoCallButton];
}
else if (type == EMChatToolbarTypeGroup)
{
frame.size.height = 80;
}
self.frame = frame;
_scrollview.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
_pageControl.frame = CGRectMake(0, CGRectGetHeight(frame) - 20, CGRectGetWidth(frame), 20);
_pageControl.hidden = _pageControl.numberOfPages<=1;
}




本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂

qrcode_for_gh_bc92a063b4a2_430.jpg

 
项目完整源码
0
评论

基于办公的 IM 的基础设计 IM OA

beyond 发表了文章 • 111 次浏览 • 2017-08-15 10:44 • 来自相关话题

   现在的 IM 在设计上是基于会话的,多个人可以组成一个会话,相当于一个聊天室,当一个人加入到一个会话后,就可以看到从加入开始之后这个聊天室里所有参与人的发言。有的 IM 会把两人对话也抽象成同一个东西,也可能出于优化的考虑把双人对话特殊处理。

所以,这些 IM 在操作界面上会有一个会话列表:表现出来会是联系人名单、聊天群列表等等。选中会话列表中的项目,进入会话查看聊天记录、发言,就是这类 IM 的使用逻辑。

我认为,这种对即时通讯的抽象方式,其实是不适合办公环境的。和日常个人社交环境不同,办公群体其实是一个相对关系密切的团体,我们通常不会拉黑一个同事不让他给你发消息,也不会拒收公司发的通告,也不会因为一个同事平常不和你打交道就拒绝建立联系。项目组里的讨论,也未见得是多么保密的事情,需要防止隔壁组的同事旁听。你也很少会在办公 IM 上和妹子私聊谈人生理想。

我们这几年使用腾讯的 RTX 作为公司办公使用,我就感受到了太多这类设计缺陷。比如,有同事找我有事,我忽略了他的私聊信息;找人一般在对方活跃的项目群组里吼;程序群沦为了日常扯淡的位置,常常同时讨论着不同的问题,线索及其混乱。“群”这个设计,我在很多年前就思考过 ,我一直觉得需要在根本上换个角度看待社交聊天的需求。

我现在的想法是这样的:

作为办公 IM ,我们不应该基于固定会话(群)来设计,而应该是“通知”和“话题”。

所谓通知,就是有人发起了一条消息,他需要把这条消息传达给某些对象,对象可以是人,也可以是某个组织:比如程序、游戏项目组、等等。

组织并非是群那样的聊天室,而仅仅是一个标签,由人来关注标签,而不是去组织这个聊天室里有多少听众。

而话题,则是由消息或旧话题衍生而来。任何通知消息、话题内部消息,都可以变成一个新话题。话题也可以包含在一则消息里转发给某个对象。

用户的客户端应该把所有的通知按时间线排列在一起,呈现在同一个地方。也就是说,无论是谁给我发消息(默认就是通知)都应该投递在一起,而不是像现在 RTX 那样只是在系统托盘里闪烁提醒、也不是微信 qq 那样,联系人名单上多出一个小红点。

而一旦我回复一个通知消息,其实就把这则通知转化成了一个话题,在时间线上,话题内的消息是归属在一起的。同一时刻,无论你的思维切换多么快,其实在短时间内你只能聚焦在一个话题上,所以客户端界面是很容易表达的,把当前话题展开在主界面(通知的时间线)即可,切换话题后自然可以折叠起来。

话题并不是聊天室、它更像是论坛的帖子。一个话题可以有很多人参与(至少发言一次),更应该支持更多的人浏览。我们不应该按聊天室的思路:用户只有在加入聊天室的那一刻开始,才能收到后续的消息,而应该像论坛那样,他只是打开了这个话题帖,可以随时聊天过去到现在发生的事情。话题内的任何一个消息,都可以由用户展开为新话题,老话题对新话题只是一个引用链接而已,并不需要有层级关系,我们也可以把任意一个话题或尚未转化为话题的消息转发出去,如果有人对他评论,就生成了新话题。

话题是一个有时效性的东西,对于办公来说,如果一个话题超过 8 小时没有新的消息,就可以认为这个话题已经结束了。但是事后我们依然可以对老话题浏览,或是继续讨论,而继续讨论就是生成的一个新话题了。

只要生成话题足够方便,每个用户的主时间线上就只会有不多的通知消息,信息传达更为有效。而管理每天的消息、检索旧消息也有很强的时间线。不像现有的 IM 群聊天,每天的聊天内容会被自然的组织成话题,这些话题上标识了参与人数、归属的组织的 tag 、继承于哪个父话题或通知、经历的时间段、衍生出哪些后续话题,等等。

即使是两个人之间的对话,也同样应该是话题的形式,而不应该把消息直接组织成一长串的聊天历史。话题未必有明确的主题,只是一种更自然的信息聚合形式而已。

对于办公场合来说,有意个最重要的优势:用户群有足够的自律。基于这种自律,我认为上面的思路若实现出来很容易推广使用。

在自律之外,或许还需要一些权限管理。这些权限管理应该是相对松散简单的,主要是限制用户订阅特定组织的 tag (比如一般员工不能订阅管理层的 tag ),限制围观特定话题(比如两人之间的私聊话题默认就是不对第三人开放权限的),话题可以锁定不准转发。权限设置的细节还需要进一步推敲。
 
本文转自云风的BLOG,原文地址http://blog.codingnow.com/ 查看全部
   现在的 IM 在设计上是基于会话的,多个人可以组成一个会话,相当于一个聊天室,当一个人加入到一个会话后,就可以看到从加入开始之后这个聊天室里所有参与人的发言。有的 IM 会把两人对话也抽象成同一个东西,也可能出于优化的考虑把双人对话特殊处理。

所以,这些 IM 在操作界面上会有一个会话列表:表现出来会是联系人名单、聊天群列表等等。选中会话列表中的项目,进入会话查看聊天记录、发言,就是这类 IM 的使用逻辑。

我认为,这种对即时通讯的抽象方式,其实是不适合办公环境的。和日常个人社交环境不同,办公群体其实是一个相对关系密切的团体,我们通常不会拉黑一个同事不让他给你发消息,也不会拒收公司发的通告,也不会因为一个同事平常不和你打交道就拒绝建立联系。项目组里的讨论,也未见得是多么保密的事情,需要防止隔壁组的同事旁听。你也很少会在办公 IM 上和妹子私聊谈人生理想。

我们这几年使用腾讯的 RTX 作为公司办公使用,我就感受到了太多这类设计缺陷。比如,有同事找我有事,我忽略了他的私聊信息;找人一般在对方活跃的项目群组里吼;程序群沦为了日常扯淡的位置,常常同时讨论着不同的问题,线索及其混乱。“群”这个设计,我在很多年前就思考过 ,我一直觉得需要在根本上换个角度看待社交聊天的需求。

我现在的想法是这样的:

作为办公 IM ,我们不应该基于固定会话(群)来设计,而应该是“通知”和“话题”。

所谓通知,就是有人发起了一条消息,他需要把这条消息传达给某些对象,对象可以是人,也可以是某个组织:比如程序、游戏项目组、等等。

组织并非是群那样的聊天室,而仅仅是一个标签,由人来关注标签,而不是去组织这个聊天室里有多少听众。

而话题,则是由消息或旧话题衍生而来。任何通知消息、话题内部消息,都可以变成一个新话题。话题也可以包含在一则消息里转发给某个对象。

用户的客户端应该把所有的通知按时间线排列在一起,呈现在同一个地方。也就是说,无论是谁给我发消息(默认就是通知)都应该投递在一起,而不是像现在 RTX 那样只是在系统托盘里闪烁提醒、也不是微信 qq 那样,联系人名单上多出一个小红点。

而一旦我回复一个通知消息,其实就把这则通知转化成了一个话题,在时间线上,话题内的消息是归属在一起的。同一时刻,无论你的思维切换多么快,其实在短时间内你只能聚焦在一个话题上,所以客户端界面是很容易表达的,把当前话题展开在主界面(通知的时间线)即可,切换话题后自然可以折叠起来。

话题并不是聊天室、它更像是论坛的帖子。一个话题可以有很多人参与(至少发言一次),更应该支持更多的人浏览。我们不应该按聊天室的思路:用户只有在加入聊天室的那一刻开始,才能收到后续的消息,而应该像论坛那样,他只是打开了这个话题帖,可以随时聊天过去到现在发生的事情。话题内的任何一个消息,都可以由用户展开为新话题,老话题对新话题只是一个引用链接而已,并不需要有层级关系,我们也可以把任意一个话题或尚未转化为话题的消息转发出去,如果有人对他评论,就生成了新话题。

话题是一个有时效性的东西,对于办公来说,如果一个话题超过 8 小时没有新的消息,就可以认为这个话题已经结束了。但是事后我们依然可以对老话题浏览,或是继续讨论,而继续讨论就是生成的一个新话题了。

只要生成话题足够方便,每个用户的主时间线上就只会有不多的通知消息,信息传达更为有效。而管理每天的消息、检索旧消息也有很强的时间线。不像现有的 IM 群聊天,每天的聊天内容会被自然的组织成话题,这些话题上标识了参与人数、归属的组织的 tag 、继承于哪个父话题或通知、经历的时间段、衍生出哪些后续话题,等等。

即使是两个人之间的对话,也同样应该是话题的形式,而不应该把消息直接组织成一长串的聊天历史。话题未必有明确的主题,只是一种更自然的信息聚合形式而已。

对于办公场合来说,有意个最重要的优势:用户群有足够的自律。基于这种自律,我认为上面的思路若实现出来很容易推广使用。

在自律之外,或许还需要一些权限管理。这些权限管理应该是相对松散简单的,主要是限制用户订阅特定组织的 tag (比如一般员工不能订阅管理层的 tag ),限制围观特定话题(比如两人之间的私聊话题默认就是不对第三人开放权限的),话题可以锁定不准转发。权限设置的细节还需要进一步推敲。
 
本文转自云风的BLOG,原文地址http://blog.codingnow.com/
0
评论

【环信征文】|两处小改动,解决环信V3.0官方版本关于转发的bug 转发 即时通讯 环信 IM

cokeyer 发表了文章 • 218 次浏览 • 2017-08-02 00:57 • 来自相关话题

    (本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
    
    最近接手了一个集成即时通讯功能的项目,用的是环信的SDK。用环信的接口可以快速实现即时通讯的很多功能。并且对官方demo稍加改动基本能够满足项目需求。真机测试时,发现图片的转发,每次都是转发失败。我开始以为是我集成时有疏漏,逐行检查代码。发现并不是我的问题。从app store下载的官方demo同样是转发失败!!坑我啊!!原因是ContactListSelectViewController这个控制器里无法正确获取到想转发的图片的缓存地址。
修改如下图:





ContactListSelectViewController.m
代码拷走直接用- (BOOL)messageViewController:(EaseMessageViewController *)viewController
didLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [self.dataArray objectAtIndex:indexPath.row];
if (![object isKindOfClass:[NSString class]]) {
EaseMessageCell *cell = (EaseMessageCell *)[self.tableView cellForRowAtIndexPath:indexPath];
//////////////////////////解决转发问题的代码///////////////////////////////
EMImageMessageBody *imageBody = (EMImageMessageBody*)[cell.model.message body];
EMMessageBodyType ty = cell.model.bodyType;
if (ty == EMMessageBodyTypeImage) {
NSString *str = cell.model.message == nil ? cell.model.thumbnailFileURLPath : [imageBody localPath];
[[NSUserDefaults standardUserDefaults] setValue:str forKey:@"imgTosand"];
}
/////////////////////////解决转发问题的代码////////////////////////
[cell becomeFirstResponder];
self.menuIndexPath = indexPath;
[self showMenuViewController:cell.bubbleView andIndexPath:indexPath messageType:cell.model.bodyType];
}
return YES;
}ContactListSelectViewController是取数据,那么存数据要在ChatViewController控制器做存数据的操作。消息类型写死为EMChatTypeChat,是因为,不论是从单聊界面转发,还是从群聊界面转发,都只能转发给个人,所以这里写死,目前没有问题。
如下图:





ChatViewController.m
代码拷走直接用#pragma mark - EMUserListViewControllerDelegate
- (void)userListViewController:(EaseUsersListViewController *)userListViewController
didSelectUserModel:(id<IUserModel>)userModel
{
if (!self.messageModel) {
return;
}
if (self.messageModel.bodyType == EMMessageBodyTypeText) {
EMMessage *message = [EaseSDKHelper sendTextMessage:self.messageModel.text to:userModel.buddy messageType:EMChatTypeChat messageExt:self.messageModel.message.ext];
__weak typeof(self) weakself = self;
[[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *aMessage, EMError *aError) {
if (!aError) {
// NSMutableArray *array = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
UIViewController *chatController = nil;
#ifdef REDPACKET_AVALABLE
chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
chatController = [[ChatViewController alloc]
initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? [userModel.nickname copy] : [userModel.buddy copy];
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakself.navigationController setViewControllers:array animated:YES];
[weakself.navigationController popViewControllerAnimated:YES];
} else {
[self showHudInView:self.view hint:Localized(@"transpondFail")];
}
}];
} else if (self.messageModel.bodyType == EMMessageBodyTypeImage) {
[self showHudInView:self.view hint:Localized(@"transponding")];
__weak typeof(self) weakSelf = self;
NSString *localPath = [(EMImageMessageBody *)self.messageModel.message.body thumbnailLocalPath];
//////////////////////////解决转发问题的代码////////////////////////////
localPath = [[NSUserDefaults standardUserDefaults] valueForKey:@"imgTosand"];
//////////////////////////解决转发问题的代码//////////////////////////
UIImage *image = [UIImage imageWithContentsOfFile:localPath];
void (^block)() = ^(EMMessage *message){
EMImageMessageBody *imgBody = (EMImageMessageBody *)message.body;
NSString *from = [[EMClient sharedClient] currentUsername];
EMImageMessageBody *newBody = [[EMImageMessageBody alloc] initWithData:nil thumbnailData:[NSData dataWithContentsOfFile:imgBody.thumbnailLocalPath]];
newBody.thumbnailLocalPath = imgBody.thumbnailLocalPath;
newBody.thumbnailRemotePath = imgBody.thumbnailRemotePath;
newBody.remotePath = imgBody.remotePath;
EMMessage *newMsg = [[EMMessage alloc] initWithConversationID:userModel.buddy from:from to:userModel.buddy body:newBody ext:message.ext];
// newMsg.chatType = message.chatType;//此为环信代码
newMsg.chatType = EMChatTypeChat;//这里是我加的
[[EMClient sharedClient].chatManager sendMessage:newMsg progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}
[(EMImageMessageBody *)message.body setLocalPath:imgBody.localPath];
[[EMClient sharedClient].chatManager updateMessage:message completion:nil];

// NSMutableArray *array = [NSMutableArray arrayWithArray:[weakSelf.navigationController viewControllers]];

#ifdef REDPACKET_AVALABLE
RedPacketChatViewController *chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? userModel.nickname : userModel.buddy;
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakSelf.navigationController setViewControllers:array animated:YES];
[weakSelf.navigationController popViewControllerAnimated:YES];//转发完跳回去
}];
};

if (!image) {
[[EMClient sharedClient].chatManager downloadMessageThumbnail:self.messageModel.message progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}

block(message);
}];
} else {
block(self.messageModel.message);
}
}
}上面一定要判断一下消息体类型,只有消息体为图片类型(EMMessageBodyTypeImage)才需要保存图片本地。如果不做判断的话,点击气泡马上崩掉。
个人感觉虽然能解决图片转发的问题,但并不是最好的解决办法,虽然对环信demo的代码改动最少。有更好的办法,欢迎在评论区交流。
(本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢) 查看全部
    (本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
    
    最近接手了一个集成即时通讯功能的项目,用的是环信的SDK。用环信的接口可以快速实现即时通讯的很多功能。并且对官方demo稍加改动基本能够满足项目需求。真机测试时,发现图片的转发,每次都是转发失败。我开始以为是我集成时有疏漏,逐行检查代码。发现并不是我的问题。从app store下载的官方demo同样是转发失败!!坑我啊!!原因是ContactListSelectViewController这个控制器里无法正确获取到想转发的图片的缓存地址。
修改如下图:

2893691-7bfd41d8c42b5f3d.png

ContactListSelectViewController.m
代码拷走直接用
- (BOOL)messageViewController:(EaseMessageViewController *)viewController
didLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [self.dataArray objectAtIndex:indexPath.row];
if (![object isKindOfClass:[NSString class]]) {
EaseMessageCell *cell = (EaseMessageCell *)[self.tableView cellForRowAtIndexPath:indexPath];
//////////////////////////解决转发问题的代码///////////////////////////////
EMImageMessageBody *imageBody = (EMImageMessageBody*)[cell.model.message body];
EMMessageBodyType ty = cell.model.bodyType;
if (ty == EMMessageBodyTypeImage) {
NSString *str = cell.model.message == nil ? cell.model.thumbnailFileURLPath : [imageBody localPath];
[[NSUserDefaults standardUserDefaults] setValue:str forKey:@"imgTosand"];
}
/////////////////////////解决转发问题的代码////////////////////////
[cell becomeFirstResponder];
self.menuIndexPath = indexPath;
[self showMenuViewController:cell.bubbleView andIndexPath:indexPath messageType:cell.model.bodyType];
}
return YES;
}
ContactListSelectViewController是取数据,那么存数据要在ChatViewController控制器做存数据的操作。消息类型写死为EMChatTypeChat,是因为,不论是从单聊界面转发,还是从群聊界面转发,都只能转发给个人,所以这里写死,目前没有问题。
如下图:

2893691-7beb949a62e8035f.png

ChatViewController.m
代码拷走直接用
#pragma mark - EMUserListViewControllerDelegate
- (void)userListViewController:(EaseUsersListViewController *)userListViewController
didSelectUserModel:(id<IUserModel>)userModel
{
if (!self.messageModel) {
return;
}
if (self.messageModel.bodyType == EMMessageBodyTypeText) {
EMMessage *message = [EaseSDKHelper sendTextMessage:self.messageModel.text to:userModel.buddy messageType:EMChatTypeChat messageExt:self.messageModel.message.ext];
__weak typeof(self) weakself = self;
[[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *aMessage, EMError *aError) {
if (!aError) {
// NSMutableArray *array = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
UIViewController *chatController = nil;
#ifdef REDPACKET_AVALABLE
chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
chatController = [[ChatViewController alloc]
initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? [userModel.nickname copy] : [userModel.buddy copy];
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakself.navigationController setViewControllers:array animated:YES];
[weakself.navigationController popViewControllerAnimated:YES];
} else {
[self showHudInView:self.view hint:Localized(@"transpondFail")];
}
}];
} else if (self.messageModel.bodyType == EMMessageBodyTypeImage) {
[self showHudInView:self.view hint:Localized(@"transponding")];
__weak typeof(self) weakSelf = self;
NSString *localPath = [(EMImageMessageBody *)self.messageModel.message.body thumbnailLocalPath];
//////////////////////////解决转发问题的代码////////////////////////////
localPath = [[NSUserDefaults standardUserDefaults] valueForKey:@"imgTosand"];
//////////////////////////解决转发问题的代码//////////////////////////
UIImage *image = [UIImage imageWithContentsOfFile:localPath];
void (^block)() = ^(EMMessage *message){
EMImageMessageBody *imgBody = (EMImageMessageBody *)message.body;
NSString *from = [[EMClient sharedClient] currentUsername];
EMImageMessageBody *newBody = [[EMImageMessageBody alloc] initWithData:nil thumbnailData:[NSData dataWithContentsOfFile:imgBody.thumbnailLocalPath]];
newBody.thumbnailLocalPath = imgBody.thumbnailLocalPath;
newBody.thumbnailRemotePath = imgBody.thumbnailRemotePath;
newBody.remotePath = imgBody.remotePath;
EMMessage *newMsg = [[EMMessage alloc] initWithConversationID:userModel.buddy from:from to:userModel.buddy body:newBody ext:message.ext];
// newMsg.chatType = message.chatType;//此为环信代码
newMsg.chatType = EMChatTypeChat;//这里是我加的
[[EMClient sharedClient].chatManager sendMessage:newMsg progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}
[(EMImageMessageBody *)message.body setLocalPath:imgBody.localPath];
[[EMClient sharedClient].chatManager updateMessage:message completion:nil];

// NSMutableArray *array = [NSMutableArray arrayWithArray:[weakSelf.navigationController viewControllers]];

#ifdef REDPACKET_AVALABLE
RedPacketChatViewController *chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? userModel.nickname : userModel.buddy;
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakSelf.navigationController setViewControllers:array animated:YES];
[weakSelf.navigationController popViewControllerAnimated:YES];//转发完跳回去
}];
};

if (!image) {
[[EMClient sharedClient].chatManager downloadMessageThumbnail:self.messageModel.message progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}

block(message);
}];
} else {
block(self.messageModel.message);
}
}
}
上面一定要判断一下消息体类型,只有消息体为图片类型(EMMessageBodyTypeImage)才需要保存图片本地。如果不做判断的话,点击气泡马上崩掉。
个人感觉虽然能解决图片转发的问题,但并不是最好的解决办法,虽然对环信demo的代码改动最少。有更好的办法,欢迎在评论区交流。
(本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
4
评论

【开源项目】家聊 -- 一款基于环信开发专为老人打造的轻量级IM开源项目 IM 开源项目 家聊

lwk520136 发表了文章 • 2673 次浏览 • 2016-11-09 16:59 • 来自相关话题

 
写在前面的话:

    简单来说,这是一款做来给家里老人用的APP,核心组件就是采用的环信sdk,感谢环信做的这么棒的sdk!!!感兴趣或者看过项目的人希望给出宝贵意见与我探讨,文章末尾有作者联系方式(放后面是确定你在联系我之前知道了家聊)
 
初衷:
 
   很久之前想就教家里老人学习使用智能机,让他们能用App和家人交流沟通,但是发现市面上流行的社交软件对于他们来说学习成本太高。

   这个项目属于轻量级的IM项目,聊天形式只有文字、语音、图片、短视频、实时音视频。这个软件的定位和市面上大多数社交软件不一样,我希望去掉那些复杂的社交元素,专门做一款能适合老人快速上手智能机的软件。
 
   所以类似群聊、朋友圈那样的社交模块都没有做,力争每个功能的入口简单清晰,老人一眼就能看懂,所以产品逻辑不会特别复杂,想了解的同学可以clone下来运行看看(有时间我会打包个测试apk出来)。
 
项目特点:

  项目里的业务架构类似MVP,在环信官方的基础上加了一些自己的实现,代码阅读更加顺畅。无论是做开源项目还是工作上的项目,我个人更倾向于能用原生实现就用原生实现,类似现在流行的RX系列、注解框架等我都没有采用(这里不是说不要去学,新技术当然值得去学,但是至于要不要在项目中采用需要自己或团队考量)
 
  当然这么多做的前提是有把握做好,不要随随便便就崩溃,目的其实就是为了降低别人阅读或者接管代码时的学习成本,所以大家在看代码的时候应该不会有特别难理解的地方
 
主要功能:
聊天模块,包含文字聊天、语音聊天、发送图片、短视频、实时音频通话、实时视频通话。通讯录:可获取系统通讯录,和环信好友关系整合。拨号器:自定义的简单拨号盘,方便老人直接拨打电话
 
 
项目运行效果图:






























 
联系作者:
如果发现项目bug或者对项目有好的建议,欢迎提交issue,或者通过下面的联系方式联系我.

*QQ:505515031 746604151

*邮箱:505515031@qq.com

*微信:Vanish520136
github地址:https://github.com/Vanish136/FamilyChat
oschina地址:http://git.oschina.net/vanish136/FamilyChat 查看全部
 
写在前面的话:

    简单来说,这是一款做来给家里老人用的APP,核心组件就是采用的环信sdk,感谢环信做的这么棒的sdk!!!感兴趣或者看过项目的人希望给出宝贵意见与我探讨,文章末尾有作者联系方式(放后面是确定你在联系我之前知道了家聊)
 
初衷:
 
   很久之前想就教家里老人学习使用智能机,让他们能用App和家人交流沟通,但是发现市面上流行的社交软件对于他们来说学习成本太高。

   这个项目属于轻量级的IM项目,聊天形式只有文字、语音、图片、短视频、实时音视频。这个软件的定位和市面上大多数社交软件不一样,我希望去掉那些复杂的社交元素,专门做一款能适合老人快速上手智能机的软件。
 
   所以类似群聊、朋友圈那样的社交模块都没有做,力争每个功能的入口简单清晰,老人一眼就能看懂,所以产品逻辑不会特别复杂,想了解的同学可以clone下来运行看看(有时间我会打包个测试apk出来)。
 
项目特点:

  项目里的业务架构类似MVP,在环信官方的基础上加了一些自己的实现,代码阅读更加顺畅。无论是做开源项目还是工作上的项目,我个人更倾向于能用原生实现就用原生实现,类似现在流行的RX系列、注解框架等我都没有采用(这里不是说不要去学,新技术当然值得去学,但是至于要不要在项目中采用需要自己或团队考量)
 
  当然这么多做的前提是有把握做好,不要随随便便就崩溃,目的其实就是为了降低别人阅读或者接管代码时的学习成本,所以大家在看代码的时候应该不会有特别难理解的地方
 
主要功能:
  1. 聊天模块,包含文字聊天、语音聊天、发送图片、短视频、实时音频通话、实时视频通话。
  2. 通讯录:可获取系统通讯录,和环信好友关系整合。
  3. 拨号器:自定义的简单拨号盘,方便老人直接拨打电话

 
 
项目运行效果图:

FC01.png


FC02.png


FC03.png


FC04.png


FC05.png


FC06.png

 
联系作者:

如果发现项目bug或者对项目有好的建议,欢迎提交issue,或者通过下面的联系方式联系我.

*QQ:505515031 746604151

*邮箱:505515031@qq.com

*微信:Vanish520136


github地址:https://github.com/Vanish136/FamilyChat
oschina地址:http://git.oschina.net/vanish136/FamilyChat
1
回复

web 下载的音频文件 保存在什么位置? IM 环信 web

lizg 回复了问题 • 2 人关注 • 641 次浏览 • 2016-09-18 14:40 • 来自相关话题

0
回复

腾讯云开放QQ IM 的能力对外免费服务对家有谁用过吗?腾讯能有优势吗? IM

回复

yongqunxu 发起了问题 • 1 人关注 • 2854 次浏览 • 2015-08-04 11:50 • 来自相关话题

0
评论

招人不易留人更难 创业团队要闯哪些关? 开发者 IM 环信

admin 发表了文章 • 1932 次浏览 • 2015-07-22 16:44 • 来自相关话题

嘉宾简介:马晓宇,环信CTO,18年的老程序员, 先后从事过 IC设计软件,短信网关、电信网管、中间件、手机操作系统和手机App的研发。从2004年开始从事开源软件的开发,参与了Apache,Eclipse,Symbian fundation等开源社区。在创办环信之前,先后在Symbian、Nokia、微软等公司工作。
公司简介:环信即时通讯云是移动即时通讯能力的云计算PaaS (Platform as a Service, 平台即服务) 平台服务商。环信将基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端SDK 包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。环信全面支持Android、iOS、Web等多种平台,在流量、电量、长连接、语音、位置、安全等能力做了极致的优化,让移动开发者摆脱繁重的移动IM通讯底层开发,极大限度地缩短产品开发周期,极短的时间内让App拥有移动IM能力。
现场速记:
马晓宇:大家好,我是环信的马晓宇 Johnson, 很高兴这个机会和大家交流。
18年的老程序员,先后做过IC软件,电信系统,中间件,手机系统等;重度开源参与者,从2004年从事开源软件开发,参与了Apache,Eclipse,Symbian Foundation 等社区。创办环信前,在Iona,Nokia,Symbian,Microsoft 等公司工作,2001到2004年在美国工作,见证了第一次的互联网泡沫。

个人是从何时开始的创业之旅,请分享下创业心得。

马晓宇:2013年初看到移动互联网的爆发,结合我们在服务器端的长期积累,开始做一个移动互联网的BaaS平台,到最后聚焦在IM云平台创立环信。我们做的是面向企业的SaaS,确切的说是toD,对企业的SaaS服务.

体会是两个:
一是市场巨大:这两年在环信平台上,我们见证了新兴移动互联网app的爆发式增长,有些客户已经开始准备IPO,更多的公司经过各轮融资得到了快速发展。20%+的app都有付费能力,另外,大量的传统企业,像国美,链家等也开始使用我们的saas 服务,市场”钱景”广阔。第二个体会是过程刺激。心脏不好的没法做企业SaaS。尤其是像我们的IM 云服务和移动客服产品,都是和客户的业务系统紧耦合的,是他们服务的关键一环。对稳定性要求比人力资源评测,后台数据分析等服务要求高的多。从上线以来,用户每月增长100%造成的容量压力;PaaS 服务商的宕机;DDoS攻击;DNS 域名污染,甚至线上运维错误操作。

介绍下环信目前的情况以及团队构成。在创业方向上环信是如何选择的呢?

马晓宇:环信平台2014年5月上线,目前服务2万多App,日活几千多万,每天消息超过1亿条。公司现有100人,其中研发团队50多人。分为:移动端,IM后台,运维,音视频,大数据,移动客服几个team。
起步是做移动互联网的BaaS(backend as service),在上面又做了企业IM产品,但发展不顺利,13年年底几个创始人闭门开会,决定聚焦在 IM 云服务。在即时通讯云发展起来之后,发现很多我们的客户都有移动客服的需求,去年年底开始开发移动客服saas 服务。

您怎么看移动即时通讯平台技术的现状及发展趋势?

马晓宇:两个趋势:融合通信和连接平台。即时通讯从只提供发送短信,图片到实时语音,实时视频。虽然现在还有牌照限制,最逐渐终会取代传统的移动网络。基于此技术的公共平台,现在是微信和Facebook主推的业务。不光是一个IM服务,而是用来连接生活的方方面面。

环信的创新产品及重点项目有哪些?产品竞争力体现在哪些方面?下一步重点发展方向是什么?

马晓宇:环信是第一个即时通讯云平台。但我们认为,接入IM,提供即时通讯服务只是第一步。我们在定义一个社交模型,通过定量定性分析,来帮助客户增加用户黏性,更好的做社交。同时我们也在重点推荐移动客服平台的开发。

我们是开放的即时通讯云平台,支持千万级并发。环信下一步重点在IM方面是提供增值服务,包括反垃圾,数据挖掘等。同时,我们也在重点开发移动客服云服务。

环信在开发移动即时通讯平台中经历过哪些经验教训?

马晓宇:最主要是经验教训是用户爆发性增长和平台容量的矛盾,和由此推动的平台架构快速演进和扩容。从去年6月上线以来,每月用户量环比增长100%,推动平台快速演进。一年时间,后台架构演化到第6个版本。

从教训角度来看,架构演化是无法一步到位的,一个比较实际的措施是压力测试。应该尽早的,经常性的做系统的压力测试,并且要保持比线上环境高几倍的压力。

请您谈谈环信与开源技术的渊源,环信技术中都涉及哪些开源技术?

马晓宇:环信的三个创始人都是开源社区的重度参与者,我们的平台也用到了大量优先的开源软件,从Kafka 消息队列,Storm 实时处理,到Spark 大数据挖掘。我们在开源方面的目标是在即时通信领域,结合我们的海量用户,打造一个开源的基础软件项目,并逐渐成为一个流行的开源项目,回馈技术社区。

技术团队是什么样的氛围?工作模式是怎样的?

马晓宇:总结一下就是压力、乐观、坦诚。创业团队事比人多,每个人都有足够的技术挑战。虽然遇到各种大大小小的困难,但大家每天工作气氛是乐观而幽默的。我们研发团队50多人,没有专门的管理人员,我是CTO,但每天主要大部分时间也是写程序。基本是工程师文化,没有KPI,讲究code wins。

举几个具体了例子:有些同事一般下午才出现在公司,有的同事喜欢跑步,一看天气好,就去奥森跑步去了,当然跑完再继续工作。

我们希望打造自组织的高效团队。下半年开始周三,周六work from home。希望到明年,team 更加成熟,实现有些同事期待在海南、普吉岛等地remote 办公,每年会北京开两次会。

在培养技术人才方面,环信有哪些举措?

马晓宇:我们有三个措施:
第一内部交流:再忙也要做每周的技术分享,而且要高质量的,充分准备的。这个对年轻工程师帮助很大;第二外部交流。我们自己主办 技术沙龙meetup. 也鼓励技术人员多参加各种会议和线下活动,和同行交流,和比自己更优秀的人交流,这个有助于核心工程师的技术提高;这块其实一直有人提醒我,小心优秀的工程师被挖走。但帮助他们更优秀,在社区更认同是我的责任,如果留不来了,也是我工作中的不足了。第三是梯队培养:对重点的苗子,要越级使用,给比较大的技术挑战和压力,同时由技术负责人对他重点给予相关的指导和帮助。对年轻工程师,我们允许失败,但不允许犯同一个错误。

现在创业公司大批量招人,在招聘过程中您遇到过怎样的瓶颈吗?如何找到优秀的技术人才,有什么好的建议?

马晓宇:招聘上我们走了一些弯路。

现在总结看,通过社招,通过猎头效果都不够好。团队本身就人手紧张,每周安排几个面试,但没有收获,比较浪费时间。我总结通过内部推荐、介绍和线下交流长期跟进最有效。

比如我们移动端团队,就是symbian,nokai 同事逐渐互相推荐来的。对出色的技术人才,要长期跟进。最近我们有两个都是跟进了1年多,有了机会,邀请加盟的。

另外,我们看好的候选人,都是优秀的技术人才,很容易得到 BAT的offer。这种情况下,就要靠工作内容、技术挑战打动他们,一起打造一个业界领先的SaaS平台。还有一个细节的地方是:JD中对要求和岗位责任描述要尽量清楚。每个岗位需要写一个专门的JD。

创业团队该如何留住人?(换血问题)

马晓宇:工作的吸引力主要有三点,不同工程师内心排序不同。
第一是待遇。兄弟们没日没夜的跟你干几年,工资要above market rate。即便没时间花,但给家里有个交待。期权更要多给,公司如果能成功,主要原因是团队每天的拼搏和贡献的积累,而不是因为创始人或投资人。第二是发展。员工在环信的这2,3年技术水平等能不能有比较大的发展,跟上甚至超过公司的发展速度。我们是要求技术团队每人都得有一个6个月的发展目标,根据此目标,我和 team lead 来具体看怎么安排相关工作,并经常给予指导。第三是快乐。个人觉得最重要。团队成员每天能不能高兴的来上班,enjoy 工作中的各种挑战。另外,我一直尽量创造轻松幽默的工作氛围,team内部提倡简单直接的沟通方式。

在公司发展过程中,“换血”也无法避免。

不少创业公司都有类似问题,起步阶段创始人和各个team技术比较强,但整个团队技术水平和经验比较欠缺。

环信2年前从车库咖啡起步,那时候还没有高大上的创业大街。水平不错的应聘者一来,一看是在一个咖啡馆办公,一般转身就走。再加上经费有限,那个阶段,我们只能招到大专毕业,甚至培训班毕业的员工。这样的团队结构,只能把产品做出来。

公司发展起来后,对人员标准的目标变成要能做出技术领先的产品,并且能服务世界各地的用户,能用英语和海外开发者流利交流。为了解决人才瓶颈,最近一年我们逐渐对开发团队进行了“换血”。比如移动端team,开发的主力变成了 计算机专业研究生毕业工作10多年的经验丰富的工程师为主力。这样的中间力量才能支撑我们今后2,3年的快速发展。

在团队逐渐调整过程中,早期的技术人员也没用流失,而是在更合适自身经验和能力的岗位成长。还是我们移动端的例子,核心的sdk core部分是几位10多年经验的Linux C++背景的同事负责,其他工程师负责在此基础上开发Android,iOS sdk 和App,也做的很出色。

创业团队如何创造条件来更好的协调工作和家庭?

马晓宇:环信团队平均年龄30多岁,很有几个40几岁的老程序员。大家都是上有老下有小,更好的协调工作和家庭是决定大家每天能不能来公司开心工作的关键。

我们的经验有几点

事业要获得家庭的理解和支持。让家里人理解我们做的事情,感受到整个团队的拼搏,也能看到我们未来3年的目标和可期待的收获。我多次给核心技术团队的家人们打电话,一打就是1,2个小时,主要是让他们理解外人开来一群酷爱编程的疯子每天在干的事情,汇报我们的发展和我们的目标。

切实关心团队的家庭。家庭遇到什么具体问题,我们能怎么帮助。比如父亲病重,可以安排回老家远程工作1,2个月,边工作边照顾家人;买房首付不够,我们帮助借些钱周转;虽然平时我们没时间照顾小孩,环信工作时间比较灵活,孩子开家长会什么的,倒是爸爸去的比较多。

推行work from home。我们是一周6天工作,但周六在家办公。下半年开始进一步推进周三也在家办公。一方面能节省路上的时间,另一方面能让大家在家的时间多一些,帮帮忙。

要招聘合适创业团队的员工。我们自己的经验看,女生有小孩的,不适合创业公司的开发岗位。但能在销售、市场、人事等岗位做得很好。不过这些都是对团队来说的。做为创始人,根本无法协调工作和家庭了。我每天晚上11,12点下班,除了周日,都见不到小孩。

互动环节:
 
最近腾讯云也推出了即时通讯产品,环信的产品和他们产品对比,有哪些不同

马晓宇:对,阿里去年年底开始搞openim,腾讯云最近也推出了类似服务,我们的区别主要开放和专注。他们的产品是云平台下面的一个小team 在搞,我们是一个整个公司focus 在上面。
 
请问一下没有KPI,那么你们怎么做绩效考核或者说激励的呢

马晓宇:我们每月会统计代码提交,但那是做为reference。team 每个人的效率和output,其实不需要领导打分,大家都知道。奖励是 team lead 给input,由管理层商量。
 
环信做为im平台,会切入移动办公类的工具或系统开发吗,类似阿里钉钉之类的。
 马晓宇:不会,我们只做平台,不做产品。我们希望借鉴开源团队的管理方式。每个人完成的feature,做的技术分享,checkin 的代码质量,其实team 都在看着。 查看全部
嘉宾简介:马晓宇,环信CTO,18年的老程序员, 先后从事过 IC设计软件,短信网关、电信网管、中间件、手机操作系统和手机App的研发。从2004年开始从事开源软件的开发,参与了Apache,Eclipse,Symbian fundation等开源社区。在创办环信之前,先后在Symbian、Nokia、微软等公司工作。
公司简介:环信即时通讯云是移动即时通讯能力的云计算PaaS (Platform as a Service, 平台即服务) 平台服务商。环信将基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端SDK 包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。环信全面支持Android、iOS、Web等多种平台,在流量、电量、长连接、语音、位置、安全等能力做了极致的优化,让移动开发者摆脱繁重的移动IM通讯底层开发,极大限度地缩短产品开发周期,极短的时间内让App拥有移动IM能力。
现场速记:
马晓宇:大家好,我是环信的马晓宇 Johnson, 很高兴这个机会和大家交流。
18年的老程序员,先后做过IC软件,电信系统,中间件,手机系统等;重度开源参与者,从2004年从事开源软件开发,参与了Apache,Eclipse,Symbian Foundation 等社区。创办环信前,在Iona,Nokia,Symbian,Microsoft 等公司工作,2001到2004年在美国工作,见证了第一次的互联网泡沫。

个人是从何时开始的创业之旅,请分享下创业心得。

马晓宇:2013年初看到移动互联网的爆发,结合我们在服务器端的长期积累,开始做一个移动互联网的BaaS平台,到最后聚焦在IM云平台创立环信。我们做的是面向企业的SaaS,确切的说是toD,对企业的SaaS服务.

体会是两个:
  • 一是市场巨大:这两年在环信平台上,我们见证了新兴移动互联网app的爆发式增长,有些客户已经开始准备IPO,更多的公司经过各轮融资得到了快速发展。20%+的app都有付费能力,另外,大量的传统企业,像国美,链家等也开始使用我们的saas 服务,市场”钱景”广阔。
  • 第二个体会是过程刺激。心脏不好的没法做企业SaaS。尤其是像我们的IM 云服务和移动客服产品,都是和客户的业务系统紧耦合的,是他们服务的关键一环。对稳定性要求比人力资源评测,后台数据分析等服务要求高的多。从上线以来,用户每月增长100%造成的容量压力;PaaS 服务商的宕机;DDoS攻击;DNS 域名污染,甚至线上运维错误操作。


介绍下环信目前的情况以及团队构成。在创业方向上环信是如何选择的呢?

马晓宇:环信平台2014年5月上线,目前服务2万多App,日活几千多万,每天消息超过1亿条。公司现有100人,其中研发团队50多人。分为:移动端,IM后台,运维,音视频,大数据,移动客服几个team。
起步是做移动互联网的BaaS(backend as service),在上面又做了企业IM产品,但发展不顺利,13年年底几个创始人闭门开会,决定聚焦在 IM 云服务。在即时通讯云发展起来之后,发现很多我们的客户都有移动客服的需求,去年年底开始开发移动客服saas 服务。

您怎么看移动即时通讯平台技术的现状及发展趋势?

马晓宇:两个趋势:融合通信和连接平台。即时通讯从只提供发送短信,图片到实时语音,实时视频。虽然现在还有牌照限制,最逐渐终会取代传统的移动网络。基于此技术的公共平台,现在是微信和Facebook主推的业务。不光是一个IM服务,而是用来连接生活的方方面面。

环信的创新产品及重点项目有哪些?产品竞争力体现在哪些方面?下一步重点发展方向是什么?

马晓宇:环信是第一个即时通讯云平台。但我们认为,接入IM,提供即时通讯服务只是第一步。我们在定义一个社交模型,通过定量定性分析,来帮助客户增加用户黏性,更好的做社交。同时我们也在重点推荐移动客服平台的开发。

我们是开放的即时通讯云平台,支持千万级并发。环信下一步重点在IM方面是提供增值服务,包括反垃圾,数据挖掘等。同时,我们也在重点开发移动客服云服务。

环信在开发移动即时通讯平台中经历过哪些经验教训?

马晓宇:最主要是经验教训是用户爆发性增长和平台容量的矛盾,和由此推动的平台架构快速演进和扩容。从去年6月上线以来,每月用户量环比增长100%,推动平台快速演进。一年时间,后台架构演化到第6个版本。

从教训角度来看,架构演化是无法一步到位的,一个比较实际的措施是压力测试。应该尽早的,经常性的做系统的压力测试,并且要保持比线上环境高几倍的压力。

请您谈谈环信与开源技术的渊源,环信技术中都涉及哪些开源技术?

马晓宇:环信的三个创始人都是开源社区的重度参与者,我们的平台也用到了大量优先的开源软件,从Kafka 消息队列,Storm 实时处理,到Spark 大数据挖掘。我们在开源方面的目标是在即时通信领域,结合我们的海量用户,打造一个开源的基础软件项目,并逐渐成为一个流行的开源项目,回馈技术社区。

技术团队是什么样的氛围?工作模式是怎样的?

马晓宇:总结一下就是压力、乐观、坦诚。创业团队事比人多,每个人都有足够的技术挑战。虽然遇到各种大大小小的困难,但大家每天工作气氛是乐观而幽默的。我们研发团队50多人,没有专门的管理人员,我是CTO,但每天主要大部分时间也是写程序。基本是工程师文化,没有KPI,讲究code wins。

举几个具体了例子:有些同事一般下午才出现在公司,有的同事喜欢跑步,一看天气好,就去奥森跑步去了,当然跑完再继续工作。

我们希望打造自组织的高效团队。下半年开始周三,周六work from home。希望到明年,team 更加成熟,实现有些同事期待在海南、普吉岛等地remote 办公,每年会北京开两次会。

在培养技术人才方面,环信有哪些举措?

马晓宇:我们有三个措施:
  • 第一内部交流:再忙也要做每周的技术分享,而且要高质量的,充分准备的。这个对年轻工程师帮助很大;
  • 第二外部交流。我们自己主办 技术沙龙meetup. 也鼓励技术人员多参加各种会议和线下活动,和同行交流,和比自己更优秀的人交流,这个有助于核心工程师的技术提高;这块其实一直有人提醒我,小心优秀的工程师被挖走。但帮助他们更优秀,在社区更认同是我的责任,如果留不来了,也是我工作中的不足了。
  • 第三是梯队培养:对重点的苗子,要越级使用,给比较大的技术挑战和压力,同时由技术负责人对他重点给予相关的指导和帮助。对年轻工程师,我们允许失败,但不允许犯同一个错误。


现在创业公司大批量招人,在招聘过程中您遇到过怎样的瓶颈吗?如何找到优秀的技术人才,有什么好的建议?

马晓宇:招聘上我们走了一些弯路。

现在总结看,通过社招,通过猎头效果都不够好。团队本身就人手紧张,每周安排几个面试,但没有收获,比较浪费时间。我总结通过内部推荐、介绍和线下交流长期跟进最有效。

比如我们移动端团队,就是symbian,nokai 同事逐渐互相推荐来的。对出色的技术人才,要长期跟进。最近我们有两个都是跟进了1年多,有了机会,邀请加盟的。

另外,我们看好的候选人,都是优秀的技术人才,很容易得到 BAT的offer。这种情况下,就要靠工作内容、技术挑战打动他们,一起打造一个业界领先的SaaS平台。还有一个细节的地方是:JD中对要求和岗位责任描述要尽量清楚。每个岗位需要写一个专门的JD。

创业团队该如何留住人?(换血问题)

马晓宇:工作的吸引力主要有三点,不同工程师内心排序不同。
  • 第一是待遇。兄弟们没日没夜的跟你干几年,工资要above market rate。即便没时间花,但给家里有个交待。期权更要多给,公司如果能成功,主要原因是团队每天的拼搏和贡献的积累,而不是因为创始人或投资人。
  • 第二是发展。员工在环信的这2,3年技术水平等能不能有比较大的发展,跟上甚至超过公司的发展速度。我们是要求技术团队每人都得有一个6个月的发展目标,根据此目标,我和 team lead 来具体看怎么安排相关工作,并经常给予指导。
  • 第三是快乐。个人觉得最重要。团队成员每天能不能高兴的来上班,enjoy 工作中的各种挑战。另外,我一直尽量创造轻松幽默的工作氛围,team内部提倡简单直接的沟通方式。


在公司发展过程中,“换血”也无法避免。

不少创业公司都有类似问题,起步阶段创始人和各个team技术比较强,但整个团队技术水平和经验比较欠缺。

环信2年前从车库咖啡起步,那时候还没有高大上的创业大街。水平不错的应聘者一来,一看是在一个咖啡馆办公,一般转身就走。再加上经费有限,那个阶段,我们只能招到大专毕业,甚至培训班毕业的员工。这样的团队结构,只能把产品做出来。

公司发展起来后,对人员标准的目标变成要能做出技术领先的产品,并且能服务世界各地的用户,能用英语和海外开发者流利交流。为了解决人才瓶颈,最近一年我们逐渐对开发团队进行了“换血”。比如移动端team,开发的主力变成了 计算机专业研究生毕业工作10多年的经验丰富的工程师为主力。这样的中间力量才能支撑我们今后2,3年的快速发展。

在团队逐渐调整过程中,早期的技术人员也没用流失,而是在更合适自身经验和能力的岗位成长。还是我们移动端的例子,核心的sdk core部分是几位10多年经验的Linux C++背景的同事负责,其他工程师负责在此基础上开发Android,iOS sdk 和App,也做的很出色。

创业团队如何创造条件来更好的协调工作和家庭?

马晓宇:环信团队平均年龄30多岁,很有几个40几岁的老程序员。大家都是上有老下有小,更好的协调工作和家庭是决定大家每天能不能来公司开心工作的关键。

我们的经验有几点

事业要获得家庭的理解和支持。让家里人理解我们做的事情,感受到整个团队的拼搏,也能看到我们未来3年的目标和可期待的收获。我多次给核心技术团队的家人们打电话,一打就是1,2个小时,主要是让他们理解外人开来一群酷爱编程的疯子每天在干的事情,汇报我们的发展和我们的目标。

切实关心团队的家庭。家庭遇到什么具体问题,我们能怎么帮助。比如父亲病重,可以安排回老家远程工作1,2个月,边工作边照顾家人;买房首付不够,我们帮助借些钱周转;虽然平时我们没时间照顾小孩,环信工作时间比较灵活,孩子开家长会什么的,倒是爸爸去的比较多。

推行work from home。我们是一周6天工作,但周六在家办公。下半年开始进一步推进周三也在家办公。一方面能节省路上的时间,另一方面能让大家在家的时间多一些,帮帮忙。

要招聘合适创业团队的员工。我们自己的经验看,女生有小孩的,不适合创业公司的开发岗位。但能在销售、市场、人事等岗位做得很好。不过这些都是对团队来说的。做为创始人,根本无法协调工作和家庭了。我每天晚上11,12点下班,除了周日,都见不到小孩。

互动环节:
 
  • 最近腾讯云也推出了即时通讯产品,环信的产品和他们产品对比,有哪些不同


马晓宇:对,阿里去年年底开始搞openim,腾讯云最近也推出了类似服务,我们的区别主要开放和专注。他们的产品是云平台下面的一个小team 在搞,我们是一个整个公司focus 在上面。
 
  • 请问一下没有KPI,那么你们怎么做绩效考核或者说激励的呢


马晓宇:我们每月会统计代码提交,但那是做为reference。team 每个人的效率和output,其实不需要领导打分,大家都知道。奖励是 team lead 给input,由管理层商量。
 
  • 环信做为im平台,会切入移动办公类的工具或系统开发吗,类似阿里钉钉之类的。

 马晓宇:不会,我们只做平台,不做产品。我们希望借鉴开源团队的管理方式。每个人完成的feature,做的技术分享,checkin 的代码质量,其实team 都在看着。
0
评论

环信CEO:“即时通讯云+移动客服”为App打造用户体验闭环 环信 移动开发 IM

oscar 发表了文章 • 1156 次浏览 • 2015-07-06 14:52 • 来自相关话题

 
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
 
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
 刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。

最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。

常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。

第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。

最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。

InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。

第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。

环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。

InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。

我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。

InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。

除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。

有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。

最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。

InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。

环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。

  查看全部

ban9.png

 
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
 
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
 
刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。
InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?
刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。

最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。

常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。

第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。

最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。

InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?
刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。

第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。

环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。

InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?
刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。

我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。

InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?
刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。

除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。

有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。

最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。

InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?
刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。
InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?
刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。

环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。

 
0
评论

实时网络音视频通讯qos的一种解决方案 IM 移动开发

oscar 发表了文章 • 1452 次浏览 • 2015-06-19 16:58 • 来自相关话题

一、前言
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。




发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。




当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。 查看全部
一、前言
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。
31.png

发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。
33.png

当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。
0
评论

IM客户端数据库加载过程优化 IM

oscar 发表了文章 • 941 次浏览 • 2015-06-19 16:40 • 来自相关话题

IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。




每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()  
FOR (conversation : conversationList) {  
    db.loadMessages(conversation);  
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠 查看全部
IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
1.png

每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()  
FOR (conversation : conversationList) {  
    db.loadMessages(conversation);  
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。

测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠
0
评论

环信SDK与Apple Watch的结合系列讲解(3) IM 移动开发 iOS Apple Watch

oscar 发表了文章 • 1573 次浏览 • 2015-06-19 14:59 • 来自相关话题

第3章主要介绍怎样在Watch App的页面上显示iPhone程序里的数据。主要操作的是“EMWatchOCDemo WatchKit Extension”这个文件夹,附源码EMWatchOCDemo。
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的




一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图




二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class




新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张




2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
         BOOL isLogined = NO;
 
         if ([replyInfo count] > 0) {
            isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
         }
 
          if (isLogined) {
              NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
             NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
             NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
             [self.dataSoure addObject:conversationInfo];
             [self.dataSoure addObject:friendInfo];
             [self.dataSoure addObject:groupInfo];
 
             NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
            [self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
             for (int i = 0; i < count; i++) {
                 RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
                 NSDictionary *dic = self.dataSoure[i];
                 NSString *title = dic[@"title"];
                 [rowController.titleLabel setText:title];
             }
        }
        else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
             [self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
             RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
             [rowController.titleLabel setText:@"登录"];
       }
  }];
 
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
    if ([userInfo count] > 0) {
        NSString *actionString = [userInfo objectForKey:@"action"];
 
        EaseMob *easemob = [EaseMob sharedInstance];
        if ([actionString isEqualToString:@"isLogined"]) {
            reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
        }
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
 
- (void)setRowTypes:(NSArray *)rowTypes;                                         // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
 
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
 
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
 
- (void)scrollToRowAtIndex:(NSInteger)index;
 
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context;  // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
 
  查看全部
第3章主要介绍怎样在Watch App的页面上显示iPhone程序里的数据。主要操作的是“EMWatchOCDemo WatchKit Extension”这个文件夹,附源码EMWatchOCDemo。
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的
41.png

一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图
42.png

二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class
43.png

新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张
44.png

2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
         BOOL isLogined = NO;
 
         if ([replyInfo count] > 0) {
            isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
         }
 
          if (isLogined) {
              NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
             NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
             NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
             [self.dataSoure addObject:conversationInfo];
             [self.dataSoure addObject:friendInfo];
             [self.dataSoure addObject:groupInfo];
 
             NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
            [self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
             for (int i = 0; i < count; i++) {
                 RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
                 NSDictionary *dic = self.dataSoure[i];
                 NSString *title = dic[@"title"];
                 [rowController.titleLabel setText:title];
             }
        }
        else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
             [self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
             RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
             [rowController.titleLabel setText:@"登录"];
       }
  }];
 
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
    if ([userInfo count] > 0) {
        NSString *actionString = [userInfo objectForKey:@"action"];
 
        EaseMob *easemob = [EaseMob sharedInstance];
        if ([actionString isEqualToString:@"isLogined"]) {
            reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
        }
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
 
- (void)setRowTypes:(NSArray *)rowTypes;                                         // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
 
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
 
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
 
- (void)scrollToRowAtIndex:(NSInteger)index;
 
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context;  // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
 
 
0
评论

环信SDK与Apple Watch的结合系列讲解(1) IM iOS 移动开发 Apple Watch

oscar 发表了文章 • 1538 次浏览 • 2015-06-19 14:38 • 来自相关话题

该系列是记录在apple watch上开发IM,用到了最近挺流行的环信IM SDK。
一、先来一段网上随处可查到的信息:

1、两种分辨率
1.65寸 312*390
1.5寸 272*340
2、开发环境
Xcode 6.2 or later
OS X 10.9.4 or later
Watchkit
3、三种展现类型
a>标准的watch app,拥有自身的界面和功能
b>Glances,纯信息浏览,样式也很固定。这一方式适合新闻、天气、股票、运动数据等信息。
c>Notification,用于显示用户的本地通知和远程通知,它包括 Short-Look 和 Long-Look 两种形式。
4、官方文档
https://developer.apple.com/li ... .html
5、怎么在工程中加入apple watch
关于这部分,网上的资料很多,在这外链一些不错的blog:
http://www.tuicool.com/articles/MFJFNny
http://blog.jobbole.com/79984/
 
二、WatchKit和环信SDK的结合

1、在工程中添加WatchKit扩展
你可以新建一个工程,在target上添加,也可以在已有的工程的target上添加,操作步骤是一样一样的。
为了记录的完整性,我新建了一个工程EMWatchOCDemo,看名字可以知道,这是一个ObjC写的工程。




2、EMWatchOCDemo是iphone的程序,先在这个target上接入环信SDK。
按照环信官网上的ios集成文档下载环信sdk,加入依赖库,配置好属性。我不准备实现实时语音,所以只用了lite.a。导入环信sdk之后的工程变成了这个样子




我比较习惯每个步骤都编译一下,如果编译通过,继续进行下边的步骤。
3、初始化环信SDK
环信SDK的初始化几乎都在AppDelegate.m中实现,如注册app,配置apns证书和昵称,进入前台和进入后台的操作,这些在环信官网的ios初始化文档中都有描述,不再赘述。
4、实现一些简单的功能
ConversationViewController 会话获取,展示。
FriendsViewController 好友获取,展示。
GroupsViewController 群组获取,展示。
HomeViewController是首页,Tab容器,实现了登录等操作。
DXEMIMHelper是IM的管理类,定义了全局的宏定义,包括默认登录账号KDEFAULT_USERNAME。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(2)
环信SDK与Apple Watch的结合系列讲解(3) 查看全部
该系列是记录在apple watch上开发IM,用到了最近挺流行的环信IM SDK。
一、先来一段网上随处可查到的信息:

1、两种分辨率
1.65寸 312*390
1.5寸 272*340
2、开发环境
Xcode 6.2 or later
OS X 10.9.4 or later
Watchkit
3、三种展现类型
a>标准的watch app,拥有自身的界面和功能
b>Glances,纯信息浏览,样式也很固定。这一方式适合新闻、天气、股票、运动数据等信息。
c>Notification,用于显示用户的本地通知和远程通知,它包括 Short-Look 和 Long-Look 两种形式。
4、官方文档
https://developer.apple.com/li ... .html
5、怎么在工程中加入apple watch
关于这部分,网上的资料很多,在这外链一些不错的blog:
http://www.tuicool.com/articles/MFJFNny
http://blog.jobbole.com/79984/
 
二、WatchKit和环信SDK的结合

1、在工程中添加WatchKit扩展
你可以新建一个工程,在target上添加,也可以在已有的工程的target上添加,操作步骤是一样一样的。
为了记录的完整性,我新建了一个工程EMWatchOCDemo,看名字可以知道,这是一个ObjC写的工程。
21.png

2、EMWatchOCDemo是iphone的程序,先在这个target上接入环信SDK。
按照环信官网上的ios集成文档下载环信sdk,加入依赖库,配置好属性。我不准备实现实时语音,所以只用了lite.a。导入环信sdk之后的工程变成了这个样子
22.png

我比较习惯每个步骤都编译一下,如果编译通过,继续进行下边的步骤。
3、初始化环信SDK
环信SDK的初始化几乎都在AppDelegate.m中实现,如注册app,配置apns证书和昵称,进入前台和进入后台的操作,这些在环信官网的ios初始化文档中都有描述,不再赘述。
4、实现一些简单的功能
ConversationViewController 会话获取,展示。
FriendsViewController 好友获取,展示。
GroupsViewController 群组获取,展示。
HomeViewController是首页,Tab容器,实现了登录等操作。
DXEMIMHelper是IM的管理类,定义了全局的宏定义,包括默认登录账号KDEFAULT_USERNAME。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(2)
环信SDK与Apple Watch的结合系列讲解(3)
0
评论

IM客户端数据库加载过程优化 IM

oscar1212 发表了文章 • 883 次浏览 • 2015-06-19 12:17 • 来自相关话题

IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。




数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
[datalist]
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions() 
FOR (conversation : conversationList) { 
db.loadMessages(conversation);
 }
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages()
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
1. 每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
2. 对于每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
[]一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
[]mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
[]SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者:李楠 查看全部
IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
11.png

数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
[datalist]
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions() 
FOR (conversation : conversationList) { 
db.loadMessages(conversation);
 }
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages()
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
1. 每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
2. 对于每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
[]一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
[]mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
[]SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者:李楠
11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9039 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 7985 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
7
回复

收集基于环信SDK开发的开源项目 开源项目

回眸淡然笑 回复了问题 • 9 人关注 • 3978 次浏览 • 2017-12-03 00:50 • 来自相关话题

7
回复

收集基于环信SDK开发的开源项目 开源项目

回复

回眸淡然笑 回复了问题 • 9 人关注 • 3978 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9039 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 7985 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
回复

EMMessageListener 里的 onMessageDelivered为什么不触发 环信_Android IM

回复

朔月之外 发起了问题 • 1 人关注 • 56 次浏览 • 2017-12-03 18:13 • 来自相关话题

1
回复

web 下载的音频文件 保存在什么位置? IM 环信 web

回复

lizg 回复了问题 • 2 人关注 • 641 次浏览 • 2016-09-18 14:40 • 来自相关话题

0
回复

腾讯云开放QQ IM 的能力对外免费服务对家有谁用过吗?腾讯能有优势吗? IM

回复

yongqunxu 发起了问题 • 1 人关注 • 2854 次浏览 • 2015-08-04 11:50 • 来自相关话题

7
回复

收集基于环信SDK开发的开源项目 开源项目

回复

回眸淡然笑 回复了问题 • 9 人关注 • 3978 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9039 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 7985 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
评论

【环信征文】集成环信,并实现消息免打扰 即时通讯 环信 消息免打扰 IM iOS

cokeyer 发表了文章 • 164 次浏览 • 2017-09-14 11:27 • 来自相关话题

现在大多数社交app都有消息免打扰功能,因为环信SDK主要是对即时通讯模块进行封装,因此如果要实现消息免打扰功能则需要开发者自己对此功能逻辑进行处理。
先解释一下,本人项目中对消息免打扰功能的定义:正常情况下,我们收到的每一条聊天消息都会收到小红点、声音、震动的提示。如果对某个好友设置消息免打扰功能,则只提示小红点,声音和震动则不再提示。

下面简述结合环信SDK时,此功能的实现方法。
环信将即时聊天的所有功能分为四大模块进行管理://聊天模块:
[EMClient sharedClient].chatManager
//好友模块 :
[EMClient sharedClient].contactManager
//群组模块 :
[EMClient sharedClient].groupManager
//聊天室模块:
[EMClient sharedClient].roomManager消息免打扰的功能借助聊天模块[EMClient sharedClient].chatManager的API就能实现。
一般来说,在app内不管当前在哪个界面,只要收到消息都需要被判断是否需要免打扰,因此可以在appdelegate里写如下代码,app通常都有tabBarController,那么也可以让tabBarController来实现如下代码。
首先让控制器tabBarController遵守代理EMChatManagerDelegate
然后成为代理
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

EMChatManagerDelegate中有一个代理方法- (void)didReceiveMessages:(NSArray *)aMessages;实现此代理方法- (void)didReceiveMessages:(NSArray *)aMessages{
[self setupUnreadMessageCount];
EMMessage *message = aMessages[0];
NSString *sendPerson = message.from;
BHNavigatiomController *imNaviCV = self.viewControllers[1];
BHConversationListController *converCV = imNaviCV.viewControllers[0];
[converCV refreshDataSource];//只要收到消息就从服务器拿
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {
if ([hx_name isEqualToString:sendPerson]) {
return;
}
}
switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}
}方法中有一个aMessages参数。这个参数是一个消息组,每一个元素都是EMMessage类型的实例。
因为通常都只有一条信息。因此只需要取出EMMessage *message = aMessages[0];
EMMessage类中有许多属性,因此根据一条信息基本可以获取想知道的所有信息。这里我们只需要知道此消息的发送方即可,也就是发送方的环信idNSString *sendPerson = message.from;下面代码中有一AppEngine.IMDataCent.data_ExcuseFriendsData,这个数组里面装的都是已经被设置消息免打扰的好友的环信id,是请求自己服务器获得的,获取的代码最好在一打开app时,就及时获取到。然后根据对好友免打扰设置的操作,访问后台接口进行增删,并刷新数组与后端保持一致即可。
通过[hx_name isEqualToString:sendPerson],遍历免打扰数组与当前消息的发送方环信id,就可以知道是否需要免打扰了。
 
下面附上完整代码//
// BHTabBarController.m
// ShangHeYiYang
//
// Created by LiBohan on 2017/8/24.
// Copyright © 2017年 xxxxx. All rights reserved.
//


//两次提示的默认间隔
static const CGFloat kDefaultPlaySoundInterval = 3.0;
static NSString *kMessageType = @"MessageType";
static NSString *kConversationChatter = @"ConversationChatter";
static NSString *kGroupName = @"GroupName";


#import "BHTabBarController.h"
#import <UserNotifications/UserNotifications.h>
#import "BHConversationListController.h"

@interface BHTabBarController ()<EMChatManagerDelegate,EMContactManagerDelegate>

@property (strong, nonatomic) NSDate *lastPlaySoundDate;

@end

@implementation BHTabBarController

- (void)viewDidLoad {
[super viewDidLoad];

[DemoCallManager sharedManager].mainController = self;

[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

[[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupUnreadMessageCount) name:@"setupUnreadMessageCount" object:nil];

}

-(void)friendshipDidRemoveByUser:(NSString *)aUsername{

// __weak __typeof(&*self)weakSelf = self;

dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:contactReloadData object:nil];
});


// if ([AppEngine.mainDataCent.data_UserData.data_HxID isEqualToString:aUsername]) {


//删除好友成功后,再删除聊天会话
[[EMClient sharedClient].chatManager deleteConversation:aUsername isDeleteMessages:YES completion:^(NSString *aConversationId, EMError *aError) {

if (!aError) {
//删除聊天会话,成功后,刷新聊天会话列表
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:talkBtnClickThenUpdateConversionlist object:nil];
});


}

}];
}

// 统计未读消息数
-(void)setupUnreadMessageCount
{
NSArray *conversations = [[EMClient sharedClient].chatManager getAllConversations];
NSInteger unreadCount = 0;
for (EMConversation *conversation in conversations) {
unreadCount += conversation.unreadMessagesCount;
}

NSArray *tabBarItems = self.tabBar.items;

UITabBarItem *conlistTabBarItem = [tabBarItems objectAtIndex:1];
if (unreadCount > 0) {
conlistTabBarItem.badgeValue = [NSString stringWithFormat:@"%i",(int)unreadCount];
}else{
conlistTabBarItem.badgeValue = nil;
}


// UIApplication *application = [UIApplication sharedApplication];
// [application setApplicationIconBadgeNumber:unreadCount];
}


- (void)cmdMessagesDidReceive:(NSArray *)aCmdMessages {
for (EMMessage *message in aCmdMessages) {
EMCmdMessageBody *body = (EMCmdMessageBody *)message.body;
NSLog(@"收到的action是 -- %@",body.action);


if (body.action == nil) {

return;

}

NSData *jsonData = [body.action dataUsingEncoding:NSUTF8StringEncoding];

NSError *err;

NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err];

if(err) {

NSLog(@"json解析失败:%@",err);

return;

}

NSNumber *num = dic[@"count"];

NSString *str = [num stringValue];

[AppEngine.IMDataCent requestUpdateUnreadNumberWithUnreadNumber:str];

}


}

- (void)didReceiveMessages:(NSArray *)aMessages{

// [self refresh];

[self setupUnreadMessageCount];

EMMessage *message = aMessages[0];

NSString *sendPerson = message.from;

BHNavigatiomController *imNaviCV = self.viewControllers[1];

BHConversationListController *converCV = imNaviCV.viewControllers[0];

// [converCV refresh];

[converCV refreshDataSource];//只要收到消息就从服务器拿

UIApplicationState state = [[UIApplication sharedApplication] applicationState];

for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {

if ([hx_name isEqualToString:sendPerson]) {
return;
}

}

switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}

}

- (void)playSoundAndVibration{
NSTimeInterval timeInterval = [[NSDate date]
timeIntervalSinceDate:self.lastPlaySoundDate];
if (timeInterval < kDefaultPlaySoundInterval) {
//如果距离上次响铃和震动时间太短, 则跳过响铃
NSLog(@"skip ringing & vibration %@, %@", [NSDate date], self.lastPlaySoundDate);
return;
}

//保存最后一次响铃时间
self.lastPlaySoundDate = [NSDate date];

// 收到消息时,播放音频
[[EMCDDeviceManager sharedInstance] playNewMessageSound];
// 收到消息时,震动
[[EMCDDeviceManager sharedInstance] playVibration];
}


- (void)showNotificationWithMessage:(EMMessage *)message
{
EMPushOptions *options = [[EMClient sharedClient] pushOptions];
NSString *alertBody = nil;
if (options.displayStyle == EMPushDisplayStyleMessageSummary) {
EMMessageBody *messageBody = message.body;
NSString *messageStr = nil;
switch (messageBody.type) {
case EMMessageBodyTypeText:
{
messageStr = ((EMTextMessageBody *)messageBody).text;
}
break;
case EMMessageBodyTypeImage:
{
messageStr = NSLocalizedString(@"message.image", @"Image");
}
break;
case EMMessageBodyTypeLocation:
{
messageStr = NSLocalizedString(@"message.location", @"Location");
}
break;
case EMMessageBodyTypeVoice:
{
messageStr = NSLocalizedString(@"message.voice", @"Voice");
}
break;
case EMMessageBodyTypeVideo:{
messageStr = NSLocalizedString(@"message.video", @"Video");
}
break;
default:
break;
}

do {
// NSString *title = [[UserProfileManager sharedInstance] getNickNameWithUsername:message.from];
NSString *title = @"大佬";

if (message.chatType == EMChatTypeGroupChat) {
NSDictionary *ext = message.ext;
if (ext && ext[kGroupMessageAtList]) {
id target = ext[kGroupMessageAtList];
if ([target isKindOfClass:[NSString class]]) {
if ([kGroupMessageAtAll compare:target options:NSCaseInsensitiveSearch] == NSOrderedSame) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
else if ([target isKindOfClass:[NSArray class]]) {
NSArray *atTargets = (NSArray*)target;
if ([atTargets containsObject:[EMClient sharedClient].currentUsername]) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
}
NSArray *groupArray = [[EMClient sharedClient].groupManager getJoinedGroups];
for (EMGroup *group in groupArray) {
if ([group.groupId isEqualToString:message.conversationId]) {
title = [NSString stringWithFormat:@"%@(%@)", message.from, group.subject];
break;
}
}
}
else if (message.chatType == EMChatTypeChatRoom)
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *key = [NSString stringWithFormat:@"OnceJoinedChatrooms_%@", [[EMClient sharedClient] currentUsername]];
NSMutableDictionary *chatrooms = [NSMutableDictionary dictionaryWithDictionary:[ud objectForKey:key]];
NSString *chatroomName = [chatrooms objectForKey:message.conversationId];
if (chatroomName)
{
title = [NSString stringWithFormat:@"%@(%@)", message.from, chatroomName];
}
}

alertBody = [NSString stringWithFormat:@"%@:%@", title, messageStr];
} while (0);
}
else{
// alertBody = NSLocalizedString(@"receiveMessage", @"you have a new message");
alertBody = @"您有一条消息";
}

NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:self.lastPlaySoundDate];
BOOL playSound = NO;
if (!self.lastPlaySoundDate || timeInterval >= kDefaultPlaySoundInterval) {
self.lastPlaySoundDate = [NSDate date];
playSound = YES;
}

NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:[NSNumber numberWithInt:message.chatType] forKey:kMessageType];
[userInfo setObject:message.conversationId forKey:kConversationChatter];

//发送本地推送
if (NSClassFromString(@"UNUserNotificationCenter")) {
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
if (playSound) {
content.sound = [UNNotificationSound defaultSound];
}
content.body =alertBody;
content.userInfo = userInfo;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:message.messageId content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
}
else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [NSDate date]; //触发通知的时间
notification.alertBody = alertBody;
notification.alertAction = NSLocalizedString(@"open", @"Open");
notification.timeZone = [NSTimeZone defaultTimeZone];
if (playSound) {
notification.soundName = UILocalNotificationDefaultSoundName;
}
notification.userInfo = userInfo;

//发送通知
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
}

- (void)messagesDidDeliver:(NSArray *)aMessages{

NSLog(@"sf");
}


- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/

@end


 
 
本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂 查看全部
现在大多数社交app都有消息免打扰功能,因为环信SDK主要是对即时通讯模块进行封装,因此如果要实现消息免打扰功能则需要开发者自己对此功能逻辑进行处理。
先解释一下,本人项目中对消息免打扰功能的定义:正常情况下,我们收到的每一条聊天消息都会收到小红点、声音、震动的提示。如果对某个好友设置消息免打扰功能,则只提示小红点,声音和震动则不再提示。

下面简述结合环信SDK时,此功能的实现方法。
环信将即时聊天的所有功能分为四大模块进行管理:
//聊天模块:
[EMClient sharedClient].chatManager
//好友模块 :
[EMClient sharedClient].contactManager
//群组模块 :
[EMClient sharedClient].groupManager
//聊天室模块:
[EMClient sharedClient].roomManager
消息免打扰的功能借助聊天模块[EMClient sharedClient].chatManager的API就能实现。
一般来说,在app内不管当前在哪个界面,只要收到消息都需要被判断是否需要免打扰,因此可以在appdelegate里写如下代码,app通常都有tabBarController,那么也可以让tabBarController来实现如下代码。
首先让控制器tabBarController遵守代理EMChatManagerDelegate
然后成为代理
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

EMChatManagerDelegate中有一个代理方法
- (void)didReceiveMessages:(NSArray *)aMessages;
实现此代理方法
- (void)didReceiveMessages:(NSArray *)aMessages{
[self setupUnreadMessageCount];
EMMessage *message = aMessages[0];
NSString *sendPerson = message.from;
BHNavigatiomController *imNaviCV = self.viewControllers[1];
BHConversationListController *converCV = imNaviCV.viewControllers[0];
[converCV refreshDataSource];//只要收到消息就从服务器拿
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {
if ([hx_name isEqualToString:sendPerson]) {
return;
}
}
switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}
}
方法中有一个aMessages参数。这个参数是一个消息组,每一个元素都是EMMessage类型的实例。
因为通常都只有一条信息。因此只需要取出EMMessage *message = aMessages[0];
EMMessage类中有许多属性,因此根据一条信息基本可以获取想知道的所有信息。这里我们只需要知道此消息的发送方即可,也就是发送方的环信idNSString *sendPerson = message.from;下面代码中有一AppEngine.IMDataCent.data_ExcuseFriendsData,这个数组里面装的都是已经被设置消息免打扰的好友的环信id,是请求自己服务器获得的,获取的代码最好在一打开app时,就及时获取到。然后根据对好友免打扰设置的操作,访问后台接口进行增删,并刷新数组与后端保持一致即可。
通过[hx_name isEqualToString:sendPerson],遍历免打扰数组与当前消息的发送方环信id,就可以知道是否需要免打扰了。
 
下面附上完整代码
//
// BHTabBarController.m
// ShangHeYiYang
//
// Created by LiBohan on 2017/8/24.
// Copyright © 2017年 xxxxx. All rights reserved.
//


//两次提示的默认间隔
static const CGFloat kDefaultPlaySoundInterval = 3.0;
static NSString *kMessageType = @"MessageType";
static NSString *kConversationChatter = @"ConversationChatter";
static NSString *kGroupName = @"GroupName";


#import "BHTabBarController.h"
#import <UserNotifications/UserNotifications.h>
#import "BHConversationListController.h"

@interface BHTabBarController ()<EMChatManagerDelegate,EMContactManagerDelegate>

@property (strong, nonatomic) NSDate *lastPlaySoundDate;

@end

@implementation BHTabBarController

- (void)viewDidLoad {
[super viewDidLoad];

[DemoCallManager sharedManager].mainController = self;

[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];

[[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupUnreadMessageCount) name:@"setupUnreadMessageCount" object:nil];

}

-(void)friendshipDidRemoveByUser:(NSString *)aUsername{

// __weak __typeof(&*self)weakSelf = self;

dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:contactReloadData object:nil];
});


// if ([AppEngine.mainDataCent.data_UserData.data_HxID isEqualToString:aUsername]) {


//删除好友成功后,再删除聊天会话
[[EMClient sharedClient].chatManager deleteConversation:aUsername isDeleteMessages:YES completion:^(NSString *aConversationId, EMError *aError) {

if (!aError) {
//删除聊天会话,成功后,刷新聊天会话列表
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0/*延迟执行时间*/ * NSEC_PER_SEC));

dispatch_after(delay, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:talkBtnClickThenUpdateConversionlist object:nil];
});


}

}];
}

// 统计未读消息数
-(void)setupUnreadMessageCount
{
NSArray *conversations = [[EMClient sharedClient].chatManager getAllConversations];
NSInteger unreadCount = 0;
for (EMConversation *conversation in conversations) {
unreadCount += conversation.unreadMessagesCount;
}

NSArray *tabBarItems = self.tabBar.items;

UITabBarItem *conlistTabBarItem = [tabBarItems objectAtIndex:1];
if (unreadCount > 0) {
conlistTabBarItem.badgeValue = [NSString stringWithFormat:@"%i",(int)unreadCount];
}else{
conlistTabBarItem.badgeValue = nil;
}


// UIApplication *application = [UIApplication sharedApplication];
// [application setApplicationIconBadgeNumber:unreadCount];
}


- (void)cmdMessagesDidReceive:(NSArray *)aCmdMessages {
for (EMMessage *message in aCmdMessages) {
EMCmdMessageBody *body = (EMCmdMessageBody *)message.body;
NSLog(@"收到的action是 -- %@",body.action);


if (body.action == nil) {

return;

}

NSData *jsonData = [body.action dataUsingEncoding:NSUTF8StringEncoding];

NSError *err;

NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err];

if(err) {

NSLog(@"json解析失败:%@",err);

return;

}

NSNumber *num = dic[@"count"];

NSString *str = [num stringValue];

[AppEngine.IMDataCent requestUpdateUnreadNumberWithUnreadNumber:str];

}


}

- (void)didReceiveMessages:(NSArray *)aMessages{

// [self refresh];

[self setupUnreadMessageCount];

EMMessage *message = aMessages[0];

NSString *sendPerson = message.from;

BHNavigatiomController *imNaviCV = self.viewControllers[1];

BHConversationListController *converCV = imNaviCV.viewControllers[0];

// [converCV refresh];

[converCV refreshDataSource];//只要收到消息就从服务器拿

UIApplicationState state = [[UIApplication sharedApplication] applicationState];

for (NSString *hx_name in AppEngine.IMDataCent.data_ExcuseFriendsData) {

if ([hx_name isEqualToString:sendPerson]) {
return;
}

}

switch (state) {
case UIApplicationStateActive:
[self playSoundAndVibration];
break;
case UIApplicationStateInactive:
[self playSoundAndVibration];
break;
case UIApplicationStateBackground:
[self showNotificationWithMessage:message];
break;
default:
break;
}

}

- (void)playSoundAndVibration{
NSTimeInterval timeInterval = [[NSDate date]
timeIntervalSinceDate:self.lastPlaySoundDate];
if (timeInterval < kDefaultPlaySoundInterval) {
//如果距离上次响铃和震动时间太短, 则跳过响铃
NSLog(@"skip ringing & vibration %@, %@", [NSDate date], self.lastPlaySoundDate);
return;
}

//保存最后一次响铃时间
self.lastPlaySoundDate = [NSDate date];

// 收到消息时,播放音频
[[EMCDDeviceManager sharedInstance] playNewMessageSound];
// 收到消息时,震动
[[EMCDDeviceManager sharedInstance] playVibration];
}


- (void)showNotificationWithMessage:(EMMessage *)message
{
EMPushOptions *options = [[EMClient sharedClient] pushOptions];
NSString *alertBody = nil;
if (options.displayStyle == EMPushDisplayStyleMessageSummary) {
EMMessageBody *messageBody = message.body;
NSString *messageStr = nil;
switch (messageBody.type) {
case EMMessageBodyTypeText:
{
messageStr = ((EMTextMessageBody *)messageBody).text;
}
break;
case EMMessageBodyTypeImage:
{
messageStr = NSLocalizedString(@"message.image", @"Image");
}
break;
case EMMessageBodyTypeLocation:
{
messageStr = NSLocalizedString(@"message.location", @"Location");
}
break;
case EMMessageBodyTypeVoice:
{
messageStr = NSLocalizedString(@"message.voice", @"Voice");
}
break;
case EMMessageBodyTypeVideo:{
messageStr = NSLocalizedString(@"message.video", @"Video");
}
break;
default:
break;
}

do {
// NSString *title = [[UserProfileManager sharedInstance] getNickNameWithUsername:message.from];
NSString *title = @"大佬";

if (message.chatType == EMChatTypeGroupChat) {
NSDictionary *ext = message.ext;
if (ext && ext[kGroupMessageAtList]) {
id target = ext[kGroupMessageAtList];
if ([target isKindOfClass:[NSString class]]) {
if ([kGroupMessageAtAll compare:target options:NSCaseInsensitiveSearch] == NSOrderedSame) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
else if ([target isKindOfClass:[NSArray class]]) {
NSArray *atTargets = (NSArray*)target;
if ([atTargets containsObject:[EMClient sharedClient].currentUsername]) {
alertBody = [NSString stringWithFormat:@"%@%@", title, NSLocalizedString(@"group.atPushTitle", @" @ me in the group")];
break;
}
}
}
NSArray *groupArray = [[EMClient sharedClient].groupManager getJoinedGroups];
for (EMGroup *group in groupArray) {
if ([group.groupId isEqualToString:message.conversationId]) {
title = [NSString stringWithFormat:@"%@(%@)", message.from, group.subject];
break;
}
}
}
else if (message.chatType == EMChatTypeChatRoom)
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *key = [NSString stringWithFormat:@"OnceJoinedChatrooms_%@", [[EMClient sharedClient] currentUsername]];
NSMutableDictionary *chatrooms = [NSMutableDictionary dictionaryWithDictionary:[ud objectForKey:key]];
NSString *chatroomName = [chatrooms objectForKey:message.conversationId];
if (chatroomName)
{
title = [NSString stringWithFormat:@"%@(%@)", message.from, chatroomName];
}
}

alertBody = [NSString stringWithFormat:@"%@:%@", title, messageStr];
} while (0);
}
else{
// alertBody = NSLocalizedString(@"receiveMessage", @"you have a new message");
alertBody = @"您有一条消息";
}

NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:self.lastPlaySoundDate];
BOOL playSound = NO;
if (!self.lastPlaySoundDate || timeInterval >= kDefaultPlaySoundInterval) {
self.lastPlaySoundDate = [NSDate date];
playSound = YES;
}

NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:[NSNumber numberWithInt:message.chatType] forKey:kMessageType];
[userInfo setObject:message.conversationId forKey:kConversationChatter];

//发送本地推送
if (NSClassFromString(@"UNUserNotificationCenter")) {
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
if (playSound) {
content.sound = [UNNotificationSound defaultSound];
}
content.body =alertBody;
content.userInfo = userInfo;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:message.messageId content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
}
else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.fireDate = [NSDate date]; //触发通知的时间
notification.alertBody = alertBody;
notification.alertAction = NSLocalizedString(@"open", @"Open");
notification.timeZone = [NSTimeZone defaultTimeZone];
if (playSound) {
notification.soundName = UILocalNotificationDefaultSoundName;
}
notification.userInfo = userInfo;

//发送通知
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
}
}

- (void)messagesDidDeliver:(NSArray *)aMessages{

NSLog(@"sf");
}


- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/

@end


 
 
本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂

qrcode_for_gh_bc92a063b4a2_430.jpg
0
评论

【环信征文】基于环信开发一个医疗APP-Ⅰ iOS 环信 即时通讯 IM

cokeyer 发表了文章 • 201 次浏览 • 2017-09-12 17:56 • 来自相关话题

公司又有了新项目,依然是含有即时通讯功能模块的项目。在经历了上个项目对环信sdk的集成后,对环信EaseUI有了大概的了解。这次果断还是集成环信,一回生二回熟,最主要的还是对环信IM稳定性非常放心!项目医疗类的项目,角色分医生和患者,双方都可主动发起会话,但如果是患者找医生聊天,必须先经过预约,并只能在预约时间区间内才能和医生发送聊天消息、图片、语音、实时音视频。项目基于环信最V3.3.4版本开发,首先在会话界面自定义一个类比如BHChatViewController,继承自EaseMessageViewController类,基本上一个简单的界面就有了。以下就是EaseMessageViewController类的发送各种消息的方法,那么根据需要只需重写以下方法即可。/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@result
*/
- (void)sendTextMessage:(NSString *)text;

/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@param ext 扩展信息
@result
*/
- (void)sendTextMessage:(NSString *)text withExt:(NSDictionary*)ext;

/*!
@method
@brief 发送图片消息
@discussion
@param image 发送图片
@result
*/
- (void)sendImageMessage:(UIImage *)image;

/*!
@method
@brief 发送位置消息
@discussion
@param latitude 经度
@param longitude 纬度
@param address 地址
@result
*/
- (void)sendLocationMessageLatitude:(double)latitude
longitude:(double)longitude
andAddress:(NSString *)address;

/*!
@method
@brief 发送语音消息
@discussion
@param localPath 语音本地地址
@param duration 时长
@result
*/
- (void)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration;

/*!
@method
@brief 发送视频消息
@discussion
@param url 视频url
@result
*/
- (void)sendVideoMessageWithURL:(NSURL *)url;我在对发送图片的操作进行处理的时候,发现当拍照发图时,走这个方法//当你拍照发图时,走这个方法
- (void)sendImageMessage:(UIImage *)image;但当从相册选择图片发送时,会发现,不走上面的方法了。
仔细检查代码发现走了EaseMessageViewController.m的如下方法





然而这个方法,环信并没有放在EaseMessageViewController.h成为公开方法。我们只需手动粘贴方法到.h,然后在自己的子类重写就可以了。

还有一个关于更多下图更多功能区域的问题





如上图所示,相册、拍照、视频等附加功能按钮,环信用EaseChatBarMoreView类来管理的。
如果需要增加功能按钮用这个方法/*!
@method
@brief 新增一个新的功能按钮
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@result
*/
- (void)insertItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title;移除某个功能按钮用这个方法/*!
@method
@brief 根据索引删除功能按钮
@discussion
@param index 按钮索引
@result
*/
- (void)removeItematIndex:(NSInteger)index;修改一个功能按钮用这个方法/*!
@method
@brief 修改功能按钮图片
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@param index 按钮索引
@result
*/
- (void)updateItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title
atIndex:(NSInteger)index;可当你添加按钮,或者修改按钮时,会发现按钮的名字设置不了
然后检查环信内部的实现,发现title的值在方法里根本就没用到?!
索性不用updateItemWithImage这个方法了,直接去改内部代码。
修改代码如下

到EaseChatBarMoreView.m修改- (void)setupSubviewsForType:(EMChatToolbarType)type方法。改动的部分在代码后面有标注。- (void)setupSubviewsForType:(EMChatToolbarType)type
{
//self.backgroundColor = [UIColor clearColor];
self.accessibilityIdentifier = @"more_view";

_scrollview = [[UIScrollView alloc] init];
_scrollview.pagingEnabled = YES;
_scrollview.showsHorizontalScrollIndicator = NO;
_scrollview.showsVerticalScrollIndicator = NO;
_scrollview.delegate = self;
[self addSubview:_scrollview];

_pageControl = [[UIPageControl alloc] init];
_pageControl.currentPage = 0;
_pageControl.numberOfPages = 1;
[self addSubview:_pageControl];

CGFloat insets = (self.frame.size.width - 4 * CHAT_BUTTON_SIZE) / 5;

_photoButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_photoButton setTitle:@"相册" forState:UIControlStateNormal];//改动
[_photoButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_photoButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_photoButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_photoButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_photoButton.accessibilityIdentifier = @"image";
[_photoButton setFrame:CGRectMake(insets, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photo"] forState:UIControlStateNormal];
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photoSelected"] forState:UIControlStateHighlighted];
[_photoButton addTarget:self action:@selector(photoAction) forControlEvents:UIControlEventTouchUpInside];
_photoButton.tag = MOREVIEW_BUTTON_TAG;
[_scrollview addSubview:_photoButton];

_locationButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_locationButton setTitle:@"位置" forState:UIControlStateNormal];//改动
[_locationButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_locationButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_locationButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_locationButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_locationButton.accessibilityIdentifier = @"location";
[_locationButton setFrame:CGRectMake(insets * 2 + CHAT_BUTTON_SIZE, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_location"] forState:UIControlStateNormal];
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_locationSelected"] forState:UIControlStateHighlighted];
[_locationButton addTarget:self action:@selector(locationAction) forControlEvents:UIControlEventTouchUpInside];
_locationButton.tag = MOREVIEW_BUTTON_TAG + 1;
[_scrollview addSubview:_locationButton];

_takePicButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_takePicButton setTitle:@"拍照" forState:UIControlStateNormal];//改动
[_takePicButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_takePicButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_takePicButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_takePicButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_takePicButton setFrame:CGRectMake(insets * 3 + CHAT_BUTTON_SIZE * 2, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_camera"] forState:UIControlStateNormal];
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_cameraSelected"] forState:UIControlStateHighlighted];
[_takePicButton addTarget:self action:@selector(takePicAction) forControlEvents:UIControlEventTouchUpInside];
_takePicButton.tag = MOREVIEW_BUTTON_TAG + 2;
_maxIndex = 2;
[_scrollview addSubview:_takePicButton];

CGRect frame = self.frame;
if (type == EMChatToolbarTypeChat) {
frame.size.height = 150;
_audioCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_audioCallButton setTitle:@"语音" forState:UIControlStateNormal];//改动
[_audioCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_audioCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_audioCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_audioCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_audioCallButton setFrame:CGRectMake(insets * 4 + CHAT_BUTTON_SIZE * 3, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCall"] forState:UIControlStateNormal];
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCallSelected"] forState:UIControlStateHighlighted];
[_audioCallButton addTarget:self action:@selector(takeAudioCallAction) forControlEvents:UIControlEventTouchUpInside];
_audioCallButton.tag = MOREVIEW_BUTTON_TAG + 3;
[_scrollview addSubview:_audioCallButton];

_videoCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_videoCallButton setTitle:@"视频" forState:UIControlStateNormal];//改动
[_videoCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_videoCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_videoCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_videoCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_videoCallButton setFrame:CGRectMake(insets, 10 * 2 + CHAT_BUTTON_SIZE + 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCall"] forState:UIControlStateNormal];
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCallSelected"] forState:UIControlStateHighlighted];
[_videoCallButton addTarget:self action:@selector(takeVideoCallAction) forControlEvents:UIControlEventTouchUpInside];
_videoCallButton.tag =MOREVIEW_BUTTON_TAG + 4;
_maxIndex = 4;
[_scrollview addSubview:_videoCallButton];
}
else if (type == EMChatToolbarTypeGroup)
{
frame.size.height = 80;
}
self.frame = frame;
_scrollview.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
_pageControl.frame = CGRectMake(0, CGRectGetHeight(frame) - 20, CGRectGetWidth(frame), 20);
_pageControl.hidden = _pageControl.numberOfPages<=1;
}




本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂





 
项目完整源码 查看全部
公司又有了新项目,依然是含有即时通讯功能模块的项目。在经历了上个项目对环信sdk的集成后,对环信EaseUI有了大概的了解。这次果断还是集成环信,一回生二回熟,最主要的还是对环信IM稳定性非常放心!
项目医疗类的项目,角色分医生和患者,双方都可主动发起会话,但如果是患者找医生聊天,必须先经过预约,并只能在预约时间区间内才能和医生发送聊天消息、图片、语音、实时音视频。项目基于环信最V3.3.4版本开发,首先在会话界面自定义一个类比如BHChatViewController,继承自EaseMessageViewController类,基本上一个简单的界面就有了。以下就是EaseMessageViewController类的发送各种消息的方法,那么根据需要只需重写以下方法即可。
/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@result
*/
- (void)sendTextMessage:(NSString *)text;

/*!
@method
@brief 发送文本消息
@discussion
@param text 文本消息
@param ext 扩展信息
@result
*/
- (void)sendTextMessage:(NSString *)text withExt:(NSDictionary*)ext;

/*!
@method
@brief 发送图片消息
@discussion
@param image 发送图片
@result
*/
- (void)sendImageMessage:(UIImage *)image;

/*!
@method
@brief 发送位置消息
@discussion
@param latitude 经度
@param longitude 纬度
@param address 地址
@result
*/
- (void)sendLocationMessageLatitude:(double)latitude
longitude:(double)longitude
andAddress:(NSString *)address;

/*!
@method
@brief 发送语音消息
@discussion
@param localPath 语音本地地址
@param duration 时长
@result
*/
- (void)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration;

/*!
@method
@brief 发送视频消息
@discussion
@param url 视频url
@result
*/
- (void)sendVideoMessageWithURL:(NSURL *)url;
我在对发送图片的操作进行处理的时候,发现当拍照发图时,走这个方法
//当你拍照发图时,走这个方法
- (void)sendImageMessage:(UIImage *)image;
但当从相册选择图片发送时,会发现,不走上面的方法了。
仔细检查代码发现走了EaseMessageViewController.m的如下方法

环信2.png

然而这个方法,环信并没有放在EaseMessageViewController.h成为公开方法。我们只需手动粘贴方法到.h,然后在自己的子类重写就可以了。

还有一个关于更多下图更多功能区域的问题

EaseChatBarMoreView.png

如上图所示,相册、拍照、视频等附加功能按钮,环信用EaseChatBarMoreView类来管理的。
如果需要增加功能按钮用这个方法
/*!
@method
@brief 新增一个新的功能按钮
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@result
*/
- (void)insertItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title;
移除某个功能按钮用这个方法
/*!
@method
@brief 根据索引删除功能按钮
@discussion
@param index 按钮索引
@result
*/
- (void)removeItematIndex:(NSInteger)index;
修改一个功能按钮用这个方法
/*!
@method
@brief 修改功能按钮图片
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@param index 按钮索引
@result
*/
- (void)updateItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title
atIndex:(NSInteger)index;
可当你添加按钮,或者修改按钮时,会发现按钮的名字设置不了
然后检查环信内部的实现,发现title的值在方法里根本就没用到?!
索性不用updateItemWithImage这个方法了,直接去改内部代码。
修改代码如下

到EaseChatBarMoreView.m修改- (void)setupSubviewsForType:(EMChatToolbarType)type方法。改动的部分在代码后面有标注。
- (void)setupSubviewsForType:(EMChatToolbarType)type
{
//self.backgroundColor = [UIColor clearColor];
self.accessibilityIdentifier = @"more_view";

_scrollview = [[UIScrollView alloc] init];
_scrollview.pagingEnabled = YES;
_scrollview.showsHorizontalScrollIndicator = NO;
_scrollview.showsVerticalScrollIndicator = NO;
_scrollview.delegate = self;
[self addSubview:_scrollview];

_pageControl = [[UIPageControl alloc] init];
_pageControl.currentPage = 0;
_pageControl.numberOfPages = 1;
[self addSubview:_pageControl];

CGFloat insets = (self.frame.size.width - 4 * CHAT_BUTTON_SIZE) / 5;

_photoButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_photoButton setTitle:@"相册" forState:UIControlStateNormal];//改动
[_photoButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_photoButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_photoButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_photoButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_photoButton.accessibilityIdentifier = @"image";
[_photoButton setFrame:CGRectMake(insets, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photo"] forState:UIControlStateNormal];
[_photoButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_photoSelected"] forState:UIControlStateHighlighted];
[_photoButton addTarget:self action:@selector(photoAction) forControlEvents:UIControlEventTouchUpInside];
_photoButton.tag = MOREVIEW_BUTTON_TAG;
[_scrollview addSubview:_photoButton];

_locationButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_locationButton setTitle:@"位置" forState:UIControlStateNormal];//改动
[_locationButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_locationButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_locationButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_locationButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
_locationButton.accessibilityIdentifier = @"location";
[_locationButton setFrame:CGRectMake(insets * 2 + CHAT_BUTTON_SIZE, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_location"] forState:UIControlStateNormal];
[_locationButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_locationSelected"] forState:UIControlStateHighlighted];
[_locationButton addTarget:self action:@selector(locationAction) forControlEvents:UIControlEventTouchUpInside];
_locationButton.tag = MOREVIEW_BUTTON_TAG + 1;
[_scrollview addSubview:_locationButton];

_takePicButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_takePicButton setTitle:@"拍照" forState:UIControlStateNormal];//改动
[_takePicButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_takePicButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_takePicButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_takePicButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_takePicButton setFrame:CGRectMake(insets * 3 + CHAT_BUTTON_SIZE * 2, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//改动
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_camera"] forState:UIControlStateNormal];
[_takePicButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_cameraSelected"] forState:UIControlStateHighlighted];
[_takePicButton addTarget:self action:@selector(takePicAction) forControlEvents:UIControlEventTouchUpInside];
_takePicButton.tag = MOREVIEW_BUTTON_TAG + 2;
_maxIndex = 2;
[_scrollview addSubview:_takePicButton];

CGRect frame = self.frame;
if (type == EMChatToolbarTypeChat) {
frame.size.height = 150;
_audioCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_audioCallButton setTitle:@"语音" forState:UIControlStateNormal];//改动
[_audioCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_audioCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_audioCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_audioCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_audioCallButton setFrame:CGRectMake(insets * 4 + CHAT_BUTTON_SIZE * 3, 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCall"] forState:UIControlStateNormal];
[_audioCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_audioCallSelected"] forState:UIControlStateHighlighted];
[_audioCallButton addTarget:self action:@selector(takeAudioCallAction) forControlEvents:UIControlEventTouchUpInside];
_audioCallButton.tag = MOREVIEW_BUTTON_TAG + 3;
[_scrollview addSubview:_audioCallButton];

_videoCallButton =[UIButton buttonWithType:UIButtonTypeCustom];
[_videoCallButton setTitle:@"视频" forState:UIControlStateNormal];//改动
[_videoCallButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];//改动
_videoCallButton.titleLabel.font = [UIFont systemFontOfSize: 12.0];//改动
_videoCallButton.imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 20, 0);//改动
_videoCallButton.titleEdgeInsets = UIEdgeInsetsMake(14, -60, -20, 0);//改动
[_videoCallButton setFrame:CGRectMake(insets, 10 * 2 + CHAT_BUTTON_SIZE + 10, CHAT_BUTTON_SIZE , CHAT_BUTTON_SIZE+10)];//
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCall"] forState:UIControlStateNormal];
[_videoCallButton setImage:[UIImage easeImageNamed:@"EaseUIResource.bundle/chatBar_colorMore_videoCallSelected"] forState:UIControlStateHighlighted];
[_videoCallButton addTarget:self action:@selector(takeVideoCallAction) forControlEvents:UIControlEventTouchUpInside];
_videoCallButton.tag =MOREVIEW_BUTTON_TAG + 4;
_maxIndex = 4;
[_scrollview addSubview:_videoCallButton];
}
else if (type == EMChatToolbarTypeGroup)
{
frame.size.height = 80;
}
self.frame = frame;
_scrollview.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
_pageControl.frame = CGRectMake(0, CGRectGetHeight(frame) - 20, CGRectGetWidth(frame), 20);
_pageControl.hidden = _pageControl.numberOfPages<=1;
}




本人github:https://github.com/BHAreslee
本人简书:http://www.jianshu.com/u/bb53043aaa00
以上就是集成环信时暂时发现的问题,欢迎大家分享你们遇到的问题,也欢迎加入QQ群:372251359,一起讨论交流即时通讯的问题。
本人微信公众号:放心安慰剂

qrcode_for_gh_bc92a063b4a2_430.jpg

 
项目完整源码
0
评论

基于办公的 IM 的基础设计 IM OA

beyond 发表了文章 • 111 次浏览 • 2017-08-15 10:44 • 来自相关话题

   现在的 IM 在设计上是基于会话的,多个人可以组成一个会话,相当于一个聊天室,当一个人加入到一个会话后,就可以看到从加入开始之后这个聊天室里所有参与人的发言。有的 IM 会把两人对话也抽象成同一个东西,也可能出于优化的考虑把双人对话特殊处理。

所以,这些 IM 在操作界面上会有一个会话列表:表现出来会是联系人名单、聊天群列表等等。选中会话列表中的项目,进入会话查看聊天记录、发言,就是这类 IM 的使用逻辑。

我认为,这种对即时通讯的抽象方式,其实是不适合办公环境的。和日常个人社交环境不同,办公群体其实是一个相对关系密切的团体,我们通常不会拉黑一个同事不让他给你发消息,也不会拒收公司发的通告,也不会因为一个同事平常不和你打交道就拒绝建立联系。项目组里的讨论,也未见得是多么保密的事情,需要防止隔壁组的同事旁听。你也很少会在办公 IM 上和妹子私聊谈人生理想。

我们这几年使用腾讯的 RTX 作为公司办公使用,我就感受到了太多这类设计缺陷。比如,有同事找我有事,我忽略了他的私聊信息;找人一般在对方活跃的项目群组里吼;程序群沦为了日常扯淡的位置,常常同时讨论着不同的问题,线索及其混乱。“群”这个设计,我在很多年前就思考过 ,我一直觉得需要在根本上换个角度看待社交聊天的需求。

我现在的想法是这样的:

作为办公 IM ,我们不应该基于固定会话(群)来设计,而应该是“通知”和“话题”。

所谓通知,就是有人发起了一条消息,他需要把这条消息传达给某些对象,对象可以是人,也可以是某个组织:比如程序、游戏项目组、等等。

组织并非是群那样的聊天室,而仅仅是一个标签,由人来关注标签,而不是去组织这个聊天室里有多少听众。

而话题,则是由消息或旧话题衍生而来。任何通知消息、话题内部消息,都可以变成一个新话题。话题也可以包含在一则消息里转发给某个对象。

用户的客户端应该把所有的通知按时间线排列在一起,呈现在同一个地方。也就是说,无论是谁给我发消息(默认就是通知)都应该投递在一起,而不是像现在 RTX 那样只是在系统托盘里闪烁提醒、也不是微信 qq 那样,联系人名单上多出一个小红点。

而一旦我回复一个通知消息,其实就把这则通知转化成了一个话题,在时间线上,话题内的消息是归属在一起的。同一时刻,无论你的思维切换多么快,其实在短时间内你只能聚焦在一个话题上,所以客户端界面是很容易表达的,把当前话题展开在主界面(通知的时间线)即可,切换话题后自然可以折叠起来。

话题并不是聊天室、它更像是论坛的帖子。一个话题可以有很多人参与(至少发言一次),更应该支持更多的人浏览。我们不应该按聊天室的思路:用户只有在加入聊天室的那一刻开始,才能收到后续的消息,而应该像论坛那样,他只是打开了这个话题帖,可以随时聊天过去到现在发生的事情。话题内的任何一个消息,都可以由用户展开为新话题,老话题对新话题只是一个引用链接而已,并不需要有层级关系,我们也可以把任意一个话题或尚未转化为话题的消息转发出去,如果有人对他评论,就生成了新话题。

话题是一个有时效性的东西,对于办公来说,如果一个话题超过 8 小时没有新的消息,就可以认为这个话题已经结束了。但是事后我们依然可以对老话题浏览,或是继续讨论,而继续讨论就是生成的一个新话题了。

只要生成话题足够方便,每个用户的主时间线上就只会有不多的通知消息,信息传达更为有效。而管理每天的消息、检索旧消息也有很强的时间线。不像现有的 IM 群聊天,每天的聊天内容会被自然的组织成话题,这些话题上标识了参与人数、归属的组织的 tag 、继承于哪个父话题或通知、经历的时间段、衍生出哪些后续话题,等等。

即使是两个人之间的对话,也同样应该是话题的形式,而不应该把消息直接组织成一长串的聊天历史。话题未必有明确的主题,只是一种更自然的信息聚合形式而已。

对于办公场合来说,有意个最重要的优势:用户群有足够的自律。基于这种自律,我认为上面的思路若实现出来很容易推广使用。

在自律之外,或许还需要一些权限管理。这些权限管理应该是相对松散简单的,主要是限制用户订阅特定组织的 tag (比如一般员工不能订阅管理层的 tag ),限制围观特定话题(比如两人之间的私聊话题默认就是不对第三人开放权限的),话题可以锁定不准转发。权限设置的细节还需要进一步推敲。
 
本文转自云风的BLOG,原文地址http://blog.codingnow.com/ 查看全部
   现在的 IM 在设计上是基于会话的,多个人可以组成一个会话,相当于一个聊天室,当一个人加入到一个会话后,就可以看到从加入开始之后这个聊天室里所有参与人的发言。有的 IM 会把两人对话也抽象成同一个东西,也可能出于优化的考虑把双人对话特殊处理。

所以,这些 IM 在操作界面上会有一个会话列表:表现出来会是联系人名单、聊天群列表等等。选中会话列表中的项目,进入会话查看聊天记录、发言,就是这类 IM 的使用逻辑。

我认为,这种对即时通讯的抽象方式,其实是不适合办公环境的。和日常个人社交环境不同,办公群体其实是一个相对关系密切的团体,我们通常不会拉黑一个同事不让他给你发消息,也不会拒收公司发的通告,也不会因为一个同事平常不和你打交道就拒绝建立联系。项目组里的讨论,也未见得是多么保密的事情,需要防止隔壁组的同事旁听。你也很少会在办公 IM 上和妹子私聊谈人生理想。

我们这几年使用腾讯的 RTX 作为公司办公使用,我就感受到了太多这类设计缺陷。比如,有同事找我有事,我忽略了他的私聊信息;找人一般在对方活跃的项目群组里吼;程序群沦为了日常扯淡的位置,常常同时讨论着不同的问题,线索及其混乱。“群”这个设计,我在很多年前就思考过 ,我一直觉得需要在根本上换个角度看待社交聊天的需求。

我现在的想法是这样的:

作为办公 IM ,我们不应该基于固定会话(群)来设计,而应该是“通知”和“话题”。

所谓通知,就是有人发起了一条消息,他需要把这条消息传达给某些对象,对象可以是人,也可以是某个组织:比如程序、游戏项目组、等等。

组织并非是群那样的聊天室,而仅仅是一个标签,由人来关注标签,而不是去组织这个聊天室里有多少听众。

而话题,则是由消息或旧话题衍生而来。任何通知消息、话题内部消息,都可以变成一个新话题。话题也可以包含在一则消息里转发给某个对象。

用户的客户端应该把所有的通知按时间线排列在一起,呈现在同一个地方。也就是说,无论是谁给我发消息(默认就是通知)都应该投递在一起,而不是像现在 RTX 那样只是在系统托盘里闪烁提醒、也不是微信 qq 那样,联系人名单上多出一个小红点。

而一旦我回复一个通知消息,其实就把这则通知转化成了一个话题,在时间线上,话题内的消息是归属在一起的。同一时刻,无论你的思维切换多么快,其实在短时间内你只能聚焦在一个话题上,所以客户端界面是很容易表达的,把当前话题展开在主界面(通知的时间线)即可,切换话题后自然可以折叠起来。

话题并不是聊天室、它更像是论坛的帖子。一个话题可以有很多人参与(至少发言一次),更应该支持更多的人浏览。我们不应该按聊天室的思路:用户只有在加入聊天室的那一刻开始,才能收到后续的消息,而应该像论坛那样,他只是打开了这个话题帖,可以随时聊天过去到现在发生的事情。话题内的任何一个消息,都可以由用户展开为新话题,老话题对新话题只是一个引用链接而已,并不需要有层级关系,我们也可以把任意一个话题或尚未转化为话题的消息转发出去,如果有人对他评论,就生成了新话题。

话题是一个有时效性的东西,对于办公来说,如果一个话题超过 8 小时没有新的消息,就可以认为这个话题已经结束了。但是事后我们依然可以对老话题浏览,或是继续讨论,而继续讨论就是生成的一个新话题了。

只要生成话题足够方便,每个用户的主时间线上就只会有不多的通知消息,信息传达更为有效。而管理每天的消息、检索旧消息也有很强的时间线。不像现有的 IM 群聊天,每天的聊天内容会被自然的组织成话题,这些话题上标识了参与人数、归属的组织的 tag 、继承于哪个父话题或通知、经历的时间段、衍生出哪些后续话题,等等。

即使是两个人之间的对话,也同样应该是话题的形式,而不应该把消息直接组织成一长串的聊天历史。话题未必有明确的主题,只是一种更自然的信息聚合形式而已。

对于办公场合来说,有意个最重要的优势:用户群有足够的自律。基于这种自律,我认为上面的思路若实现出来很容易推广使用。

在自律之外,或许还需要一些权限管理。这些权限管理应该是相对松散简单的,主要是限制用户订阅特定组织的 tag (比如一般员工不能订阅管理层的 tag ),限制围观特定话题(比如两人之间的私聊话题默认就是不对第三人开放权限的),话题可以锁定不准转发。权限设置的细节还需要进一步推敲。
 
本文转自云风的BLOG,原文地址http://blog.codingnow.com/
0
评论

【环信征文】|两处小改动,解决环信V3.0官方版本关于转发的bug 转发 即时通讯 环信 IM

cokeyer 发表了文章 • 218 次浏览 • 2017-08-02 00:57 • 来自相关话题

    (本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
    
    最近接手了一个集成即时通讯功能的项目,用的是环信的SDK。用环信的接口可以快速实现即时通讯的很多功能。并且对官方demo稍加改动基本能够满足项目需求。真机测试时,发现图片的转发,每次都是转发失败。我开始以为是我集成时有疏漏,逐行检查代码。发现并不是我的问题。从app store下载的官方demo同样是转发失败!!坑我啊!!原因是ContactListSelectViewController这个控制器里无法正确获取到想转发的图片的缓存地址。
修改如下图:





ContactListSelectViewController.m
代码拷走直接用- (BOOL)messageViewController:(EaseMessageViewController *)viewController
didLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [self.dataArray objectAtIndex:indexPath.row];
if (![object isKindOfClass:[NSString class]]) {
EaseMessageCell *cell = (EaseMessageCell *)[self.tableView cellForRowAtIndexPath:indexPath];
//////////////////////////解决转发问题的代码///////////////////////////////
EMImageMessageBody *imageBody = (EMImageMessageBody*)[cell.model.message body];
EMMessageBodyType ty = cell.model.bodyType;
if (ty == EMMessageBodyTypeImage) {
NSString *str = cell.model.message == nil ? cell.model.thumbnailFileURLPath : [imageBody localPath];
[[NSUserDefaults standardUserDefaults] setValue:str forKey:@"imgTosand"];
}
/////////////////////////解决转发问题的代码////////////////////////
[cell becomeFirstResponder];
self.menuIndexPath = indexPath;
[self showMenuViewController:cell.bubbleView andIndexPath:indexPath messageType:cell.model.bodyType];
}
return YES;
}ContactListSelectViewController是取数据,那么存数据要在ChatViewController控制器做存数据的操作。消息类型写死为EMChatTypeChat,是因为,不论是从单聊界面转发,还是从群聊界面转发,都只能转发给个人,所以这里写死,目前没有问题。
如下图:





ChatViewController.m
代码拷走直接用#pragma mark - EMUserListViewControllerDelegate
- (void)userListViewController:(EaseUsersListViewController *)userListViewController
didSelectUserModel:(id<IUserModel>)userModel
{
if (!self.messageModel) {
return;
}
if (self.messageModel.bodyType == EMMessageBodyTypeText) {
EMMessage *message = [EaseSDKHelper sendTextMessage:self.messageModel.text to:userModel.buddy messageType:EMChatTypeChat messageExt:self.messageModel.message.ext];
__weak typeof(self) weakself = self;
[[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *aMessage, EMError *aError) {
if (!aError) {
// NSMutableArray *array = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
UIViewController *chatController = nil;
#ifdef REDPACKET_AVALABLE
chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
chatController = [[ChatViewController alloc]
initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? [userModel.nickname copy] : [userModel.buddy copy];
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakself.navigationController setViewControllers:array animated:YES];
[weakself.navigationController popViewControllerAnimated:YES];
} else {
[self showHudInView:self.view hint:Localized(@"transpondFail")];
}
}];
} else if (self.messageModel.bodyType == EMMessageBodyTypeImage) {
[self showHudInView:self.view hint:Localized(@"transponding")];
__weak typeof(self) weakSelf = self;
NSString *localPath = [(EMImageMessageBody *)self.messageModel.message.body thumbnailLocalPath];
//////////////////////////解决转发问题的代码////////////////////////////
localPath = [[NSUserDefaults standardUserDefaults] valueForKey:@"imgTosand"];
//////////////////////////解决转发问题的代码//////////////////////////
UIImage *image = [UIImage imageWithContentsOfFile:localPath];
void (^block)() = ^(EMMessage *message){
EMImageMessageBody *imgBody = (EMImageMessageBody *)message.body;
NSString *from = [[EMClient sharedClient] currentUsername];
EMImageMessageBody *newBody = [[EMImageMessageBody alloc] initWithData:nil thumbnailData:[NSData dataWithContentsOfFile:imgBody.thumbnailLocalPath]];
newBody.thumbnailLocalPath = imgBody.thumbnailLocalPath;
newBody.thumbnailRemotePath = imgBody.thumbnailRemotePath;
newBody.remotePath = imgBody.remotePath;
EMMessage *newMsg = [[EMMessage alloc] initWithConversationID:userModel.buddy from:from to:userModel.buddy body:newBody ext:message.ext];
// newMsg.chatType = message.chatType;//此为环信代码
newMsg.chatType = EMChatTypeChat;//这里是我加的
[[EMClient sharedClient].chatManager sendMessage:newMsg progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}
[(EMImageMessageBody *)message.body setLocalPath:imgBody.localPath];
[[EMClient sharedClient].chatManager updateMessage:message completion:nil];

// NSMutableArray *array = [NSMutableArray arrayWithArray:[weakSelf.navigationController viewControllers]];

#ifdef REDPACKET_AVALABLE
RedPacketChatViewController *chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? userModel.nickname : userModel.buddy;
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakSelf.navigationController setViewControllers:array animated:YES];
[weakSelf.navigationController popViewControllerAnimated:YES];//转发完跳回去
}];
};

if (!image) {
[[EMClient sharedClient].chatManager downloadMessageThumbnail:self.messageModel.message progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}

block(message);
}];
} else {
block(self.messageModel.message);
}
}
}上面一定要判断一下消息体类型,只有消息体为图片类型(EMMessageBodyTypeImage)才需要保存图片本地。如果不做判断的话,点击气泡马上崩掉。
个人感觉虽然能解决图片转发的问题,但并不是最好的解决办法,虽然对环信demo的代码改动最少。有更好的办法,欢迎在评论区交流。
(本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢) 查看全部
    (本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
    
    最近接手了一个集成即时通讯功能的项目,用的是环信的SDK。用环信的接口可以快速实现即时通讯的很多功能。并且对官方demo稍加改动基本能够满足项目需求。真机测试时,发现图片的转发,每次都是转发失败。我开始以为是我集成时有疏漏,逐行检查代码。发现并不是我的问题。从app store下载的官方demo同样是转发失败!!坑我啊!!原因是ContactListSelectViewController这个控制器里无法正确获取到想转发的图片的缓存地址。
修改如下图:

2893691-7bfd41d8c42b5f3d.png

ContactListSelectViewController.m
代码拷走直接用
- (BOOL)messageViewController:(EaseMessageViewController *)viewController
didLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [self.dataArray objectAtIndex:indexPath.row];
if (![object isKindOfClass:[NSString class]]) {
EaseMessageCell *cell = (EaseMessageCell *)[self.tableView cellForRowAtIndexPath:indexPath];
//////////////////////////解决转发问题的代码///////////////////////////////
EMImageMessageBody *imageBody = (EMImageMessageBody*)[cell.model.message body];
EMMessageBodyType ty = cell.model.bodyType;
if (ty == EMMessageBodyTypeImage) {
NSString *str = cell.model.message == nil ? cell.model.thumbnailFileURLPath : [imageBody localPath];
[[NSUserDefaults standardUserDefaults] setValue:str forKey:@"imgTosand"];
}
/////////////////////////解决转发问题的代码////////////////////////
[cell becomeFirstResponder];
self.menuIndexPath = indexPath;
[self showMenuViewController:cell.bubbleView andIndexPath:indexPath messageType:cell.model.bodyType];
}
return YES;
}
ContactListSelectViewController是取数据,那么存数据要在ChatViewController控制器做存数据的操作。消息类型写死为EMChatTypeChat,是因为,不论是从单聊界面转发,还是从群聊界面转发,都只能转发给个人,所以这里写死,目前没有问题。
如下图:

2893691-7beb949a62e8035f.png

ChatViewController.m
代码拷走直接用
#pragma mark - EMUserListViewControllerDelegate
- (void)userListViewController:(EaseUsersListViewController *)userListViewController
didSelectUserModel:(id<IUserModel>)userModel
{
if (!self.messageModel) {
return;
}
if (self.messageModel.bodyType == EMMessageBodyTypeText) {
EMMessage *message = [EaseSDKHelper sendTextMessage:self.messageModel.text to:userModel.buddy messageType:EMChatTypeChat messageExt:self.messageModel.message.ext];
__weak typeof(self) weakself = self;
[[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *aMessage, EMError *aError) {
if (!aError) {
// NSMutableArray *array = [NSMutableArray arrayWithArray:[self.navigationController viewControllers]];
UIViewController *chatController = nil;
#ifdef REDPACKET_AVALABLE
chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
chatController = [[ChatViewController alloc]
initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? [userModel.nickname copy] : [userModel.buddy copy];
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakself.navigationController setViewControllers:array animated:YES];
[weakself.navigationController popViewControllerAnimated:YES];
} else {
[self showHudInView:self.view hint:Localized(@"transpondFail")];
}
}];
} else if (self.messageModel.bodyType == EMMessageBodyTypeImage) {
[self showHudInView:self.view hint:Localized(@"transponding")];
__weak typeof(self) weakSelf = self;
NSString *localPath = [(EMImageMessageBody *)self.messageModel.message.body thumbnailLocalPath];
//////////////////////////解决转发问题的代码////////////////////////////
localPath = [[NSUserDefaults standardUserDefaults] valueForKey:@"imgTosand"];
//////////////////////////解决转发问题的代码//////////////////////////
UIImage *image = [UIImage imageWithContentsOfFile:localPath];
void (^block)() = ^(EMMessage *message){
EMImageMessageBody *imgBody = (EMImageMessageBody *)message.body;
NSString *from = [[EMClient sharedClient] currentUsername];
EMImageMessageBody *newBody = [[EMImageMessageBody alloc] initWithData:nil thumbnailData:[NSData dataWithContentsOfFile:imgBody.thumbnailLocalPath]];
newBody.thumbnailLocalPath = imgBody.thumbnailLocalPath;
newBody.thumbnailRemotePath = imgBody.thumbnailRemotePath;
newBody.remotePath = imgBody.remotePath;
EMMessage *newMsg = [[EMMessage alloc] initWithConversationID:userModel.buddy from:from to:userModel.buddy body:newBody ext:message.ext];
// newMsg.chatType = message.chatType;//此为环信代码
newMsg.chatType = EMChatTypeChat;//这里是我加的
[[EMClient sharedClient].chatManager sendMessage:newMsg progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}
[(EMImageMessageBody *)message.body setLocalPath:imgBody.localPath];
[[EMClient sharedClient].chatManager updateMessage:message completion:nil];

// NSMutableArray *array = [NSMutableArray arrayWithArray:[weakSelf.navigationController viewControllers]];

#ifdef REDPACKET_AVALABLE
RedPacketChatViewController *chatController = [[RedPacketChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#else
ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:userModel.buddy conversationType:EMConversationTypeChat];
#endif
chatController.title = userModel.nickname.length != 0 ? userModel.nickname : userModel.buddy;
// if ([array count] >= 3) {
// [array removeLastObject];
// [array removeLastObject];
// }
// [array addObject:chatController];
// [weakSelf.navigationController setViewControllers:array animated:YES];
[weakSelf.navigationController popViewControllerAnimated:YES];//转发完跳回去
}];
};

if (!image) {
[[EMClient sharedClient].chatManager downloadMessageThumbnail:self.messageModel.message progress:nil completion:^(EMMessage *message, EMError *error) {
if (error) {
[weakSelf showHudInView:self.view hint:Localized(@"transpondFail")];
[weakSelf performSelector:@selector(backAction) withObject:nil afterDelay:1];
return ;
}

block(message);
}];
} else {
block(self.messageModel.message);
}
}
}
上面一定要判断一下消息体类型,只有消息体为图片类型(EMMessageBodyTypeImage)才需要保存图片本地。如果不做判断的话,点击气泡马上崩掉。
个人感觉虽然能解决图片转发的问题,但并不是最好的解决办法,虽然对环信demo的代码改动最少。有更好的办法,欢迎在评论区交流。
(本人github:https://github.com/BHAreslee)(若转载,请告知本人并附上原文链接,谢谢)
4
评论

【开源项目】家聊 -- 一款基于环信开发专为老人打造的轻量级IM开源项目 IM 开源项目 家聊

lwk520136 发表了文章 • 2673 次浏览 • 2016-11-09 16:59 • 来自相关话题

 
写在前面的话:

    简单来说,这是一款做来给家里老人用的APP,核心组件就是采用的环信sdk,感谢环信做的这么棒的sdk!!!感兴趣或者看过项目的人希望给出宝贵意见与我探讨,文章末尾有作者联系方式(放后面是确定你在联系我之前知道了家聊)
 
初衷:
 
   很久之前想就教家里老人学习使用智能机,让他们能用App和家人交流沟通,但是发现市面上流行的社交软件对于他们来说学习成本太高。

   这个项目属于轻量级的IM项目,聊天形式只有文字、语音、图片、短视频、实时音视频。这个软件的定位和市面上大多数社交软件不一样,我希望去掉那些复杂的社交元素,专门做一款能适合老人快速上手智能机的软件。
 
   所以类似群聊、朋友圈那样的社交模块都没有做,力争每个功能的入口简单清晰,老人一眼就能看懂,所以产品逻辑不会特别复杂,想了解的同学可以clone下来运行看看(有时间我会打包个测试apk出来)。
 
项目特点:

  项目里的业务架构类似MVP,在环信官方的基础上加了一些自己的实现,代码阅读更加顺畅。无论是做开源项目还是工作上的项目,我个人更倾向于能用原生实现就用原生实现,类似现在流行的RX系列、注解框架等我都没有采用(这里不是说不要去学,新技术当然值得去学,但是至于要不要在项目中采用需要自己或团队考量)
 
  当然这么多做的前提是有把握做好,不要随随便便就崩溃,目的其实就是为了降低别人阅读或者接管代码时的学习成本,所以大家在看代码的时候应该不会有特别难理解的地方
 
主要功能:
聊天模块,包含文字聊天、语音聊天、发送图片、短视频、实时音频通话、实时视频通话。通讯录:可获取系统通讯录,和环信好友关系整合。拨号器:自定义的简单拨号盘,方便老人直接拨打电话
 
 
项目运行效果图:






























 
联系作者:
如果发现项目bug或者对项目有好的建议,欢迎提交issue,或者通过下面的联系方式联系我.

*QQ:505515031 746604151

*邮箱:505515031@qq.com

*微信:Vanish520136
github地址:https://github.com/Vanish136/FamilyChat
oschina地址:http://git.oschina.net/vanish136/FamilyChat 查看全部
 
写在前面的话:

    简单来说,这是一款做来给家里老人用的APP,核心组件就是采用的环信sdk,感谢环信做的这么棒的sdk!!!感兴趣或者看过项目的人希望给出宝贵意见与我探讨,文章末尾有作者联系方式(放后面是确定你在联系我之前知道了家聊)
 
初衷:
 
   很久之前想就教家里老人学习使用智能机,让他们能用App和家人交流沟通,但是发现市面上流行的社交软件对于他们来说学习成本太高。

   这个项目属于轻量级的IM项目,聊天形式只有文字、语音、图片、短视频、实时音视频。这个软件的定位和市面上大多数社交软件不一样,我希望去掉那些复杂的社交元素,专门做一款能适合老人快速上手智能机的软件。
 
   所以类似群聊、朋友圈那样的社交模块都没有做,力争每个功能的入口简单清晰,老人一眼就能看懂,所以产品逻辑不会特别复杂,想了解的同学可以clone下来运行看看(有时间我会打包个测试apk出来)。
 
项目特点:

  项目里的业务架构类似MVP,在环信官方的基础上加了一些自己的实现,代码阅读更加顺畅。无论是做开源项目还是工作上的项目,我个人更倾向于能用原生实现就用原生实现,类似现在流行的RX系列、注解框架等我都没有采用(这里不是说不要去学,新技术当然值得去学,但是至于要不要在项目中采用需要自己或团队考量)
 
  当然这么多做的前提是有把握做好,不要随随便便就崩溃,目的其实就是为了降低别人阅读或者接管代码时的学习成本,所以大家在看代码的时候应该不会有特别难理解的地方
 
主要功能:
  1. 聊天模块,包含文字聊天、语音聊天、发送图片、短视频、实时音频通话、实时视频通话。
  2. 通讯录:可获取系统通讯录,和环信好友关系整合。
  3. 拨号器:自定义的简单拨号盘,方便老人直接拨打电话

 
 
项目运行效果图:

FC01.png


FC02.png


FC03.png


FC04.png


FC05.png


FC06.png

 
联系作者:

如果发现项目bug或者对项目有好的建议,欢迎提交issue,或者通过下面的联系方式联系我.

*QQ:505515031 746604151

*邮箱:505515031@qq.com

*微信:Vanish520136


github地址:https://github.com/Vanish136/FamilyChat
oschina地址:http://git.oschina.net/vanish136/FamilyChat
0
评论

招人不易留人更难 创业团队要闯哪些关? 开发者 IM 环信

admin 发表了文章 • 1932 次浏览 • 2015-07-22 16:44 • 来自相关话题

嘉宾简介:马晓宇,环信CTO,18年的老程序员, 先后从事过 IC设计软件,短信网关、电信网管、中间件、手机操作系统和手机App的研发。从2004年开始从事开源软件的开发,参与了Apache,Eclipse,Symbian fundation等开源社区。在创办环信之前,先后在Symbian、Nokia、微软等公司工作。
公司简介:环信即时通讯云是移动即时通讯能力的云计算PaaS (Platform as a Service, 平台即服务) 平台服务商。环信将基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端SDK 包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。环信全面支持Android、iOS、Web等多种平台,在流量、电量、长连接、语音、位置、安全等能力做了极致的优化,让移动开发者摆脱繁重的移动IM通讯底层开发,极大限度地缩短产品开发周期,极短的时间内让App拥有移动IM能力。
现场速记:
马晓宇:大家好,我是环信的马晓宇 Johnson, 很高兴这个机会和大家交流。
18年的老程序员,先后做过IC软件,电信系统,中间件,手机系统等;重度开源参与者,从2004年从事开源软件开发,参与了Apache,Eclipse,Symbian Foundation 等社区。创办环信前,在Iona,Nokia,Symbian,Microsoft 等公司工作,2001到2004年在美国工作,见证了第一次的互联网泡沫。

个人是从何时开始的创业之旅,请分享下创业心得。

马晓宇:2013年初看到移动互联网的爆发,结合我们在服务器端的长期积累,开始做一个移动互联网的BaaS平台,到最后聚焦在IM云平台创立环信。我们做的是面向企业的SaaS,确切的说是toD,对企业的SaaS服务.

体会是两个:
一是市场巨大:这两年在环信平台上,我们见证了新兴移动互联网app的爆发式增长,有些客户已经开始准备IPO,更多的公司经过各轮融资得到了快速发展。20%+的app都有付费能力,另外,大量的传统企业,像国美,链家等也开始使用我们的saas 服务,市场”钱景”广阔。第二个体会是过程刺激。心脏不好的没法做企业SaaS。尤其是像我们的IM 云服务和移动客服产品,都是和客户的业务系统紧耦合的,是他们服务的关键一环。对稳定性要求比人力资源评测,后台数据分析等服务要求高的多。从上线以来,用户每月增长100%造成的容量压力;PaaS 服务商的宕机;DDoS攻击;DNS 域名污染,甚至线上运维错误操作。

介绍下环信目前的情况以及团队构成。在创业方向上环信是如何选择的呢?

马晓宇:环信平台2014年5月上线,目前服务2万多App,日活几千多万,每天消息超过1亿条。公司现有100人,其中研发团队50多人。分为:移动端,IM后台,运维,音视频,大数据,移动客服几个team。
起步是做移动互联网的BaaS(backend as service),在上面又做了企业IM产品,但发展不顺利,13年年底几个创始人闭门开会,决定聚焦在 IM 云服务。在即时通讯云发展起来之后,发现很多我们的客户都有移动客服的需求,去年年底开始开发移动客服saas 服务。

您怎么看移动即时通讯平台技术的现状及发展趋势?

马晓宇:两个趋势:融合通信和连接平台。即时通讯从只提供发送短信,图片到实时语音,实时视频。虽然现在还有牌照限制,最逐渐终会取代传统的移动网络。基于此技术的公共平台,现在是微信和Facebook主推的业务。不光是一个IM服务,而是用来连接生活的方方面面。

环信的创新产品及重点项目有哪些?产品竞争力体现在哪些方面?下一步重点发展方向是什么?

马晓宇:环信是第一个即时通讯云平台。但我们认为,接入IM,提供即时通讯服务只是第一步。我们在定义一个社交模型,通过定量定性分析,来帮助客户增加用户黏性,更好的做社交。同时我们也在重点推荐移动客服平台的开发。

我们是开放的即时通讯云平台,支持千万级并发。环信下一步重点在IM方面是提供增值服务,包括反垃圾,数据挖掘等。同时,我们也在重点开发移动客服云服务。

环信在开发移动即时通讯平台中经历过哪些经验教训?

马晓宇:最主要是经验教训是用户爆发性增长和平台容量的矛盾,和由此推动的平台架构快速演进和扩容。从去年6月上线以来,每月用户量环比增长100%,推动平台快速演进。一年时间,后台架构演化到第6个版本。

从教训角度来看,架构演化是无法一步到位的,一个比较实际的措施是压力测试。应该尽早的,经常性的做系统的压力测试,并且要保持比线上环境高几倍的压力。

请您谈谈环信与开源技术的渊源,环信技术中都涉及哪些开源技术?

马晓宇:环信的三个创始人都是开源社区的重度参与者,我们的平台也用到了大量优先的开源软件,从Kafka 消息队列,Storm 实时处理,到Spark 大数据挖掘。我们在开源方面的目标是在即时通信领域,结合我们的海量用户,打造一个开源的基础软件项目,并逐渐成为一个流行的开源项目,回馈技术社区。

技术团队是什么样的氛围?工作模式是怎样的?

马晓宇:总结一下就是压力、乐观、坦诚。创业团队事比人多,每个人都有足够的技术挑战。虽然遇到各种大大小小的困难,但大家每天工作气氛是乐观而幽默的。我们研发团队50多人,没有专门的管理人员,我是CTO,但每天主要大部分时间也是写程序。基本是工程师文化,没有KPI,讲究code wins。

举几个具体了例子:有些同事一般下午才出现在公司,有的同事喜欢跑步,一看天气好,就去奥森跑步去了,当然跑完再继续工作。

我们希望打造自组织的高效团队。下半年开始周三,周六work from home。希望到明年,team 更加成熟,实现有些同事期待在海南、普吉岛等地remote 办公,每年会北京开两次会。

在培养技术人才方面,环信有哪些举措?

马晓宇:我们有三个措施:
第一内部交流:再忙也要做每周的技术分享,而且要高质量的,充分准备的。这个对年轻工程师帮助很大;第二外部交流。我们自己主办 技术沙龙meetup. 也鼓励技术人员多参加各种会议和线下活动,和同行交流,和比自己更优秀的人交流,这个有助于核心工程师的技术提高;这块其实一直有人提醒我,小心优秀的工程师被挖走。但帮助他们更优秀,在社区更认同是我的责任,如果留不来了,也是我工作中的不足了。第三是梯队培养:对重点的苗子,要越级使用,给比较大的技术挑战和压力,同时由技术负责人对他重点给予相关的指导和帮助。对年轻工程师,我们允许失败,但不允许犯同一个错误。

现在创业公司大批量招人,在招聘过程中您遇到过怎样的瓶颈吗?如何找到优秀的技术人才,有什么好的建议?

马晓宇:招聘上我们走了一些弯路。

现在总结看,通过社招,通过猎头效果都不够好。团队本身就人手紧张,每周安排几个面试,但没有收获,比较浪费时间。我总结通过内部推荐、介绍和线下交流长期跟进最有效。

比如我们移动端团队,就是symbian,nokai 同事逐渐互相推荐来的。对出色的技术人才,要长期跟进。最近我们有两个都是跟进了1年多,有了机会,邀请加盟的。

另外,我们看好的候选人,都是优秀的技术人才,很容易得到 BAT的offer。这种情况下,就要靠工作内容、技术挑战打动他们,一起打造一个业界领先的SaaS平台。还有一个细节的地方是:JD中对要求和岗位责任描述要尽量清楚。每个岗位需要写一个专门的JD。

创业团队该如何留住人?(换血问题)

马晓宇:工作的吸引力主要有三点,不同工程师内心排序不同。
第一是待遇。兄弟们没日没夜的跟你干几年,工资要above market rate。即便没时间花,但给家里有个交待。期权更要多给,公司如果能成功,主要原因是团队每天的拼搏和贡献的积累,而不是因为创始人或投资人。第二是发展。员工在环信的这2,3年技术水平等能不能有比较大的发展,跟上甚至超过公司的发展速度。我们是要求技术团队每人都得有一个6个月的发展目标,根据此目标,我和 team lead 来具体看怎么安排相关工作,并经常给予指导。第三是快乐。个人觉得最重要。团队成员每天能不能高兴的来上班,enjoy 工作中的各种挑战。另外,我一直尽量创造轻松幽默的工作氛围,team内部提倡简单直接的沟通方式。

在公司发展过程中,“换血”也无法避免。

不少创业公司都有类似问题,起步阶段创始人和各个team技术比较强,但整个团队技术水平和经验比较欠缺。

环信2年前从车库咖啡起步,那时候还没有高大上的创业大街。水平不错的应聘者一来,一看是在一个咖啡馆办公,一般转身就走。再加上经费有限,那个阶段,我们只能招到大专毕业,甚至培训班毕业的员工。这样的团队结构,只能把产品做出来。

公司发展起来后,对人员标准的目标变成要能做出技术领先的产品,并且能服务世界各地的用户,能用英语和海外开发者流利交流。为了解决人才瓶颈,最近一年我们逐渐对开发团队进行了“换血”。比如移动端team,开发的主力变成了 计算机专业研究生毕业工作10多年的经验丰富的工程师为主力。这样的中间力量才能支撑我们今后2,3年的快速发展。

在团队逐渐调整过程中,早期的技术人员也没用流失,而是在更合适自身经验和能力的岗位成长。还是我们移动端的例子,核心的sdk core部分是几位10多年经验的Linux C++背景的同事负责,其他工程师负责在此基础上开发Android,iOS sdk 和App,也做的很出色。

创业团队如何创造条件来更好的协调工作和家庭?

马晓宇:环信团队平均年龄30多岁,很有几个40几岁的老程序员。大家都是上有老下有小,更好的协调工作和家庭是决定大家每天能不能来公司开心工作的关键。

我们的经验有几点

事业要获得家庭的理解和支持。让家里人理解我们做的事情,感受到整个团队的拼搏,也能看到我们未来3年的目标和可期待的收获。我多次给核心技术团队的家人们打电话,一打就是1,2个小时,主要是让他们理解外人开来一群酷爱编程的疯子每天在干的事情,汇报我们的发展和我们的目标。

切实关心团队的家庭。家庭遇到什么具体问题,我们能怎么帮助。比如父亲病重,可以安排回老家远程工作1,2个月,边工作边照顾家人;买房首付不够,我们帮助借些钱周转;虽然平时我们没时间照顾小孩,环信工作时间比较灵活,孩子开家长会什么的,倒是爸爸去的比较多。

推行work from home。我们是一周6天工作,但周六在家办公。下半年开始进一步推进周三也在家办公。一方面能节省路上的时间,另一方面能让大家在家的时间多一些,帮帮忙。

要招聘合适创业团队的员工。我们自己的经验看,女生有小孩的,不适合创业公司的开发岗位。但能在销售、市场、人事等岗位做得很好。不过这些都是对团队来说的。做为创始人,根本无法协调工作和家庭了。我每天晚上11,12点下班,除了周日,都见不到小孩。

互动环节:
 
最近腾讯云也推出了即时通讯产品,环信的产品和他们产品对比,有哪些不同

马晓宇:对,阿里去年年底开始搞openim,腾讯云最近也推出了类似服务,我们的区别主要开放和专注。他们的产品是云平台下面的一个小team 在搞,我们是一个整个公司focus 在上面。
 
请问一下没有KPI,那么你们怎么做绩效考核或者说激励的呢

马晓宇:我们每月会统计代码提交,但那是做为reference。team 每个人的效率和output,其实不需要领导打分,大家都知道。奖励是 team lead 给input,由管理层商量。
 
环信做为im平台,会切入移动办公类的工具或系统开发吗,类似阿里钉钉之类的。
 马晓宇:不会,我们只做平台,不做产品。我们希望借鉴开源团队的管理方式。每个人完成的feature,做的技术分享,checkin 的代码质量,其实team 都在看着。 查看全部
嘉宾简介:马晓宇,环信CTO,18年的老程序员, 先后从事过 IC设计软件,短信网关、电信网管、中间件、手机操作系统和手机App的研发。从2004年开始从事开源软件的开发,参与了Apache,Eclipse,Symbian fundation等开源社区。在创办环信之前,先后在Symbian、Nokia、微软等公司工作。
公司简介:环信即时通讯云是移动即时通讯能力的云计算PaaS (Platform as a Service, 平台即服务) 平台服务商。环信将基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端SDK 包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。环信全面支持Android、iOS、Web等多种平台,在流量、电量、长连接、语音、位置、安全等能力做了极致的优化,让移动开发者摆脱繁重的移动IM通讯底层开发,极大限度地缩短产品开发周期,极短的时间内让App拥有移动IM能力。
现场速记:
马晓宇:大家好,我是环信的马晓宇 Johnson, 很高兴这个机会和大家交流。
18年的老程序员,先后做过IC软件,电信系统,中间件,手机系统等;重度开源参与者,从2004年从事开源软件开发,参与了Apache,Eclipse,Symbian Foundation 等社区。创办环信前,在Iona,Nokia,Symbian,Microsoft 等公司工作,2001到2004年在美国工作,见证了第一次的互联网泡沫。

个人是从何时开始的创业之旅,请分享下创业心得。

马晓宇:2013年初看到移动互联网的爆发,结合我们在服务器端的长期积累,开始做一个移动互联网的BaaS平台,到最后聚焦在IM云平台创立环信。我们做的是面向企业的SaaS,确切的说是toD,对企业的SaaS服务.

体会是两个:
  • 一是市场巨大:这两年在环信平台上,我们见证了新兴移动互联网app的爆发式增长,有些客户已经开始准备IPO,更多的公司经过各轮融资得到了快速发展。20%+的app都有付费能力,另外,大量的传统企业,像国美,链家等也开始使用我们的saas 服务,市场”钱景”广阔。
  • 第二个体会是过程刺激。心脏不好的没法做企业SaaS。尤其是像我们的IM 云服务和移动客服产品,都是和客户的业务系统紧耦合的,是他们服务的关键一环。对稳定性要求比人力资源评测,后台数据分析等服务要求高的多。从上线以来,用户每月增长100%造成的容量压力;PaaS 服务商的宕机;DDoS攻击;DNS 域名污染,甚至线上运维错误操作。


介绍下环信目前的情况以及团队构成。在创业方向上环信是如何选择的呢?

马晓宇:环信平台2014年5月上线,目前服务2万多App,日活几千多万,每天消息超过1亿条。公司现有100人,其中研发团队50多人。分为:移动端,IM后台,运维,音视频,大数据,移动客服几个team。
起步是做移动互联网的BaaS(backend as service),在上面又做了企业IM产品,但发展不顺利,13年年底几个创始人闭门开会,决定聚焦在 IM 云服务。在即时通讯云发展起来之后,发现很多我们的客户都有移动客服的需求,去年年底开始开发移动客服saas 服务。

您怎么看移动即时通讯平台技术的现状及发展趋势?

马晓宇:两个趋势:融合通信和连接平台。即时通讯从只提供发送短信,图片到实时语音,实时视频。虽然现在还有牌照限制,最逐渐终会取代传统的移动网络。基于此技术的公共平台,现在是微信和Facebook主推的业务。不光是一个IM服务,而是用来连接生活的方方面面。

环信的创新产品及重点项目有哪些?产品竞争力体现在哪些方面?下一步重点发展方向是什么?

马晓宇:环信是第一个即时通讯云平台。但我们认为,接入IM,提供即时通讯服务只是第一步。我们在定义一个社交模型,通过定量定性分析,来帮助客户增加用户黏性,更好的做社交。同时我们也在重点推荐移动客服平台的开发。

我们是开放的即时通讯云平台,支持千万级并发。环信下一步重点在IM方面是提供增值服务,包括反垃圾,数据挖掘等。同时,我们也在重点开发移动客服云服务。

环信在开发移动即时通讯平台中经历过哪些经验教训?

马晓宇:最主要是经验教训是用户爆发性增长和平台容量的矛盾,和由此推动的平台架构快速演进和扩容。从去年6月上线以来,每月用户量环比增长100%,推动平台快速演进。一年时间,后台架构演化到第6个版本。

从教训角度来看,架构演化是无法一步到位的,一个比较实际的措施是压力测试。应该尽早的,经常性的做系统的压力测试,并且要保持比线上环境高几倍的压力。

请您谈谈环信与开源技术的渊源,环信技术中都涉及哪些开源技术?

马晓宇:环信的三个创始人都是开源社区的重度参与者,我们的平台也用到了大量优先的开源软件,从Kafka 消息队列,Storm 实时处理,到Spark 大数据挖掘。我们在开源方面的目标是在即时通信领域,结合我们的海量用户,打造一个开源的基础软件项目,并逐渐成为一个流行的开源项目,回馈技术社区。

技术团队是什么样的氛围?工作模式是怎样的?

马晓宇:总结一下就是压力、乐观、坦诚。创业团队事比人多,每个人都有足够的技术挑战。虽然遇到各种大大小小的困难,但大家每天工作气氛是乐观而幽默的。我们研发团队50多人,没有专门的管理人员,我是CTO,但每天主要大部分时间也是写程序。基本是工程师文化,没有KPI,讲究code wins。

举几个具体了例子:有些同事一般下午才出现在公司,有的同事喜欢跑步,一看天气好,就去奥森跑步去了,当然跑完再继续工作。

我们希望打造自组织的高效团队。下半年开始周三,周六work from home。希望到明年,team 更加成熟,实现有些同事期待在海南、普吉岛等地remote 办公,每年会北京开两次会。

在培养技术人才方面,环信有哪些举措?

马晓宇:我们有三个措施:
  • 第一内部交流:再忙也要做每周的技术分享,而且要高质量的,充分准备的。这个对年轻工程师帮助很大;
  • 第二外部交流。我们自己主办 技术沙龙meetup. 也鼓励技术人员多参加各种会议和线下活动,和同行交流,和比自己更优秀的人交流,这个有助于核心工程师的技术提高;这块其实一直有人提醒我,小心优秀的工程师被挖走。但帮助他们更优秀,在社区更认同是我的责任,如果留不来了,也是我工作中的不足了。
  • 第三是梯队培养:对重点的苗子,要越级使用,给比较大的技术挑战和压力,同时由技术负责人对他重点给予相关的指导和帮助。对年轻工程师,我们允许失败,但不允许犯同一个错误。


现在创业公司大批量招人,在招聘过程中您遇到过怎样的瓶颈吗?如何找到优秀的技术人才,有什么好的建议?

马晓宇:招聘上我们走了一些弯路。

现在总结看,通过社招,通过猎头效果都不够好。团队本身就人手紧张,每周安排几个面试,但没有收获,比较浪费时间。我总结通过内部推荐、介绍和线下交流长期跟进最有效。

比如我们移动端团队,就是symbian,nokai 同事逐渐互相推荐来的。对出色的技术人才,要长期跟进。最近我们有两个都是跟进了1年多,有了机会,邀请加盟的。

另外,我们看好的候选人,都是优秀的技术人才,很容易得到 BAT的offer。这种情况下,就要靠工作内容、技术挑战打动他们,一起打造一个业界领先的SaaS平台。还有一个细节的地方是:JD中对要求和岗位责任描述要尽量清楚。每个岗位需要写一个专门的JD。

创业团队该如何留住人?(换血问题)

马晓宇:工作的吸引力主要有三点,不同工程师内心排序不同。
  • 第一是待遇。兄弟们没日没夜的跟你干几年,工资要above market rate。即便没时间花,但给家里有个交待。期权更要多给,公司如果能成功,主要原因是团队每天的拼搏和贡献的积累,而不是因为创始人或投资人。
  • 第二是发展。员工在环信的这2,3年技术水平等能不能有比较大的发展,跟上甚至超过公司的发展速度。我们是要求技术团队每人都得有一个6个月的发展目标,根据此目标,我和 team lead 来具体看怎么安排相关工作,并经常给予指导。
  • 第三是快乐。个人觉得最重要。团队成员每天能不能高兴的来上班,enjoy 工作中的各种挑战。另外,我一直尽量创造轻松幽默的工作氛围,team内部提倡简单直接的沟通方式。


在公司发展过程中,“换血”也无法避免。

不少创业公司都有类似问题,起步阶段创始人和各个team技术比较强,但整个团队技术水平和经验比较欠缺。

环信2年前从车库咖啡起步,那时候还没有高大上的创业大街。水平不错的应聘者一来,一看是在一个咖啡馆办公,一般转身就走。再加上经费有限,那个阶段,我们只能招到大专毕业,甚至培训班毕业的员工。这样的团队结构,只能把产品做出来。

公司发展起来后,对人员标准的目标变成要能做出技术领先的产品,并且能服务世界各地的用户,能用英语和海外开发者流利交流。为了解决人才瓶颈,最近一年我们逐渐对开发团队进行了“换血”。比如移动端team,开发的主力变成了 计算机专业研究生毕业工作10多年的经验丰富的工程师为主力。这样的中间力量才能支撑我们今后2,3年的快速发展。

在团队逐渐调整过程中,早期的技术人员也没用流失,而是在更合适自身经验和能力的岗位成长。还是我们移动端的例子,核心的sdk core部分是几位10多年经验的Linux C++背景的同事负责,其他工程师负责在此基础上开发Android,iOS sdk 和App,也做的很出色。

创业团队如何创造条件来更好的协调工作和家庭?

马晓宇:环信团队平均年龄30多岁,很有几个40几岁的老程序员。大家都是上有老下有小,更好的协调工作和家庭是决定大家每天能不能来公司开心工作的关键。

我们的经验有几点

事业要获得家庭的理解和支持。让家里人理解我们做的事情,感受到整个团队的拼搏,也能看到我们未来3年的目标和可期待的收获。我多次给核心技术团队的家人们打电话,一打就是1,2个小时,主要是让他们理解外人开来一群酷爱编程的疯子每天在干的事情,汇报我们的发展和我们的目标。

切实关心团队的家庭。家庭遇到什么具体问题,我们能怎么帮助。比如父亲病重,可以安排回老家远程工作1,2个月,边工作边照顾家人;买房首付不够,我们帮助借些钱周转;虽然平时我们没时间照顾小孩,环信工作时间比较灵活,孩子开家长会什么的,倒是爸爸去的比较多。

推行work from home。我们是一周6天工作,但周六在家办公。下半年开始进一步推进周三也在家办公。一方面能节省路上的时间,另一方面能让大家在家的时间多一些,帮帮忙。

要招聘合适创业团队的员工。我们自己的经验看,女生有小孩的,不适合创业公司的开发岗位。但能在销售、市场、人事等岗位做得很好。不过这些都是对团队来说的。做为创始人,根本无法协调工作和家庭了。我每天晚上11,12点下班,除了周日,都见不到小孩。

互动环节:
 
  • 最近腾讯云也推出了即时通讯产品,环信的产品和他们产品对比,有哪些不同


马晓宇:对,阿里去年年底开始搞openim,腾讯云最近也推出了类似服务,我们的区别主要开放和专注。他们的产品是云平台下面的一个小team 在搞,我们是一个整个公司focus 在上面。
 
  • 请问一下没有KPI,那么你们怎么做绩效考核或者说激励的呢


马晓宇:我们每月会统计代码提交,但那是做为reference。team 每个人的效率和output,其实不需要领导打分,大家都知道。奖励是 team lead 给input,由管理层商量。
 
  • 环信做为im平台,会切入移动办公类的工具或系统开发吗,类似阿里钉钉之类的。

 马晓宇:不会,我们只做平台,不做产品。我们希望借鉴开源团队的管理方式。每个人完成的feature,做的技术分享,checkin 的代码质量,其实team 都在看着。
0
评论

环信CEO:“即时通讯云+移动客服”为App打造用户体验闭环 环信 移动开发 IM

oscar 发表了文章 • 1156 次浏览 • 2015-07-06 14:52 • 来自相关话题

 
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
 
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
 刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。

最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。

常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。

第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。

最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。

InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。

第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。

环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。

InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。

我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。

InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。

除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。

有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。

最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。

InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。

环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。

  查看全部

ban9.png

 
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
 
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
 
刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。
InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?
刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。

最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。

常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。

第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。

最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。

InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?
刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。

第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。

环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。

InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?
刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。

我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。

InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?
刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。

除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。

有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。

最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。

InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?
刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。
InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?
刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。

环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。

 
0
评论

实时网络音视频通讯qos的一种解决方案 IM 移动开发

oscar 发表了文章 • 1452 次浏览 • 2015-06-19 16:58 • 来自相关话题

一、前言
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。




发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。




当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。 查看全部
一、前言
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。
31.png

发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。
33.png

当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。
0
评论

IM客户端数据库加载过程优化 IM

oscar 发表了文章 • 941 次浏览 • 2015-06-19 16:40 • 来自相关话题

IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。




每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()  
FOR (conversation : conversationList) {  
    db.loadMessages(conversation);  
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠 查看全部
IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
1.png

每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()  
FOR (conversation : conversationList) {  
    db.loadMessages(conversation);  
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。

测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠
0
评论

环信SDK与Apple Watch的结合系列讲解(3) IM 移动开发 iOS Apple Watch

oscar 发表了文章 • 1573 次浏览 • 2015-06-19 14:59 • 来自相关话题

第3章主要介绍怎样在Watch App的页面上显示iPhone程序里的数据。主要操作的是“EMWatchOCDemo WatchKit Extension”这个文件夹,附源码EMWatchOCDemo。
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的




一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图




二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class




新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张




2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
         BOOL isLogined = NO;
 
         if ([replyInfo count] > 0) {
            isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
         }
 
          if (isLogined) {
              NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
             NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
             NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
             [self.dataSoure addObject:conversationInfo];
             [self.dataSoure addObject:friendInfo];
             [self.dataSoure addObject:groupInfo];
 
             NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
            [self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
             for (int i = 0; i < count; i++) {
                 RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
                 NSDictionary *dic = self.dataSoure[i];
                 NSString *title = dic[@"title"];
                 [rowController.titleLabel setText:title];
             }
        }
        else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
             [self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
             RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
             [rowController.titleLabel setText:@"登录"];
       }
  }];
 
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
    if ([userInfo count] > 0) {
        NSString *actionString = [userInfo objectForKey:@"action"];
 
        EaseMob *easemob = [EaseMob sharedInstance];
        if ([actionString isEqualToString:@"isLogined"]) {
            reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
        }
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
 
- (void)setRowTypes:(NSArray *)rowTypes;                                         // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
 
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
 
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
 
- (void)scrollToRowAtIndex:(NSInteger)index;
 
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context;  // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
 
  查看全部
第3章主要介绍怎样在Watch App的页面上显示iPhone程序里的数据。主要操作的是“EMWatchOCDemo WatchKit Extension”这个文件夹,附源码EMWatchOCDemo。
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的
41.png

一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图
42.png

二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class
43.png

新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张
44.png

2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
         BOOL isLogined = NO;
 
         if ([replyInfo count] > 0) {
            isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
         }
 
          if (isLogined) {
              NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
             NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
             NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
             [self.dataSoure addObject:conversationInfo];
             [self.dataSoure addObject:friendInfo];
             [self.dataSoure addObject:groupInfo];
 
             NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
            [self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
             for (int i = 0; i < count; i++) {
                 RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
                 NSDictionary *dic = self.dataSoure[i];
                 NSString *title = dic[@"title"];
                 [rowController.titleLabel setText:title];
             }
        }
        else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
             [self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
             RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
             [rowController.titleLabel setText:@"登录"];
       }
  }];
 
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
    if ([userInfo count] > 0) {
        NSString *actionString = [userInfo objectForKey:@"action"];
 
        EaseMob *easemob = [EaseMob sharedInstance];
        if ([actionString isEqualToString:@"isLogined"]) {
            reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
        }
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
 
- (void)setRowTypes:(NSArray *)rowTypes;                                         // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
 
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
 
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
 
- (void)scrollToRowAtIndex:(NSInteger)index;
 
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context;  // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
 
 
0
评论

环信SDK与Apple Watch的结合系列讲解(1) IM iOS 移动开发 Apple Watch

oscar 发表了文章 • 1538 次浏览 • 2015-06-19 14:38 • 来自相关话题

该系列是记录在apple watch上开发IM,用到了最近挺流行的环信IM SDK。
一、先来一段网上随处可查到的信息:

1、两种分辨率
1.65寸 312*390
1.5寸 272*340
2、开发环境
Xcode 6.2 or later
OS X 10.9.4 or later
Watchkit
3、三种展现类型
a>标准的watch app,拥有自身的界面和功能
b>Glances,纯信息浏览,样式也很固定。这一方式适合新闻、天气、股票、运动数据等信息。
c>Notification,用于显示用户的本地通知和远程通知,它包括 Short-Look 和 Long-Look 两种形式。
4、官方文档
https://developer.apple.com/li ... .html
5、怎么在工程中加入apple watch
关于这部分,网上的资料很多,在这外链一些不错的blog:
http://www.tuicool.com/articles/MFJFNny
http://blog.jobbole.com/79984/
 
二、WatchKit和环信SDK的结合

1、在工程中添加WatchKit扩展
你可以新建一个工程,在target上添加,也可以在已有的工程的target上添加,操作步骤是一样一样的。
为了记录的完整性,我新建了一个工程EMWatchOCDemo,看名字可以知道,这是一个ObjC写的工程。




2、EMWatchOCDemo是iphone的程序,先在这个target上接入环信SDK。
按照环信官网上的ios集成文档下载环信sdk,加入依赖库,配置好属性。我不准备实现实时语音,所以只用了lite.a。导入环信sdk之后的工程变成了这个样子




我比较习惯每个步骤都编译一下,如果编译通过,继续进行下边的步骤。
3、初始化环信SDK
环信SDK的初始化几乎都在AppDelegate.m中实现,如注册app,配置apns证书和昵称,进入前台和进入后台的操作,这些在环信官网的ios初始化文档中都有描述,不再赘述。
4、实现一些简单的功能
ConversationViewController 会话获取,展示。
FriendsViewController 好友获取,展示。
GroupsViewController 群组获取,展示。
HomeViewController是首页,Tab容器,实现了登录等操作。
DXEMIMHelper是IM的管理类,定义了全局的宏定义,包括默认登录账号KDEFAULT_USERNAME。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(2)
环信SDK与Apple Watch的结合系列讲解(3) 查看全部
该系列是记录在apple watch上开发IM,用到了最近挺流行的环信IM SDK。
一、先来一段网上随处可查到的信息:

1、两种分辨率
1.65寸 312*390
1.5寸 272*340
2、开发环境
Xcode 6.2 or later
OS X 10.9.4 or later
Watchkit
3、三种展现类型
a>标准的watch app,拥有自身的界面和功能
b>Glances,纯信息浏览,样式也很固定。这一方式适合新闻、天气、股票、运动数据等信息。
c>Notification,用于显示用户的本地通知和远程通知,它包括 Short-Look 和 Long-Look 两种形式。
4、官方文档
https://developer.apple.com/li ... .html
5、怎么在工程中加入apple watch
关于这部分,网上的资料很多,在这外链一些不错的blog:
http://www.tuicool.com/articles/MFJFNny
http://blog.jobbole.com/79984/
 
二、WatchKit和环信SDK的结合

1、在工程中添加WatchKit扩展
你可以新建一个工程,在target上添加,也可以在已有的工程的target上添加,操作步骤是一样一样的。
为了记录的完整性,我新建了一个工程EMWatchOCDemo,看名字可以知道,这是一个ObjC写的工程。
21.png

2、EMWatchOCDemo是iphone的程序,先在这个target上接入环信SDK。
按照环信官网上的ios集成文档下载环信sdk,加入依赖库,配置好属性。我不准备实现实时语音,所以只用了lite.a。导入环信sdk之后的工程变成了这个样子
22.png

我比较习惯每个步骤都编译一下,如果编译通过,继续进行下边的步骤。
3、初始化环信SDK
环信SDK的初始化几乎都在AppDelegate.m中实现,如注册app,配置apns证书和昵称,进入前台和进入后台的操作,这些在环信官网的ios初始化文档中都有描述,不再赘述。
4、实现一些简单的功能
ConversationViewController 会话获取,展示。
FriendsViewController 好友获取,展示。
GroupsViewController 群组获取,展示。
HomeViewController是首页,Tab容器,实现了登录等操作。
DXEMIMHelper是IM的管理类,定义了全局的宏定义,包括默认登录账号KDEFAULT_USERNAME。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(2)
环信SDK与Apple Watch的结合系列讲解(3)
0
评论

IM客户端数据库加载过程优化 IM

oscar1212 发表了文章 • 883 次浏览 • 2015-06-19 12:17 • 来自相关话题

IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。




数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
[datalist]
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions() 
FOR (conversation : conversationList) { 
db.loadMessages(conversation);
 }
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages()
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
1. 每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
2. 对于每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
[]一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
[]mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
[]SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者:李楠 查看全部
IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
11.png

数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
[datalist]
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions() 
FOR (conversation : conversationList) { 
db.loadMessages(conversation);
 }
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages()
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
1. 每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
2. 对于每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
 cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');  
  while (cursor.NEXT()) {  
       msg = msgFrom(cursor)  
IF (! msg belong TO conversation) {  
    // 消息不属于当前的会话,所以  
    conversation = NEW Conversation();  
    conversation.ADD(msg);  
    continue;  
}  
 
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {  
    conversation.ADD(msg);  
} ELSE {  
    // 消息个数已经超过会话消息限制  
    continue;  
}  
  }
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
[]一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
[]mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
[]SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1 ... 0379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog ... .html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者:李楠