【社区精华|持续更新】收录本社区精华内容,手把手教学IM/RTC开发!

IM即时通讯admin 发表了文章 • 8 个评论 • 1184 次浏览 • 2020-12-07 14:41 • 来自相关话题

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。Android篇融云即时通讯SDK集成 — 通知检查融云 IM SDK 集成 —- 刷新会话界面... ...查看全部

本文收录了GeekOnline社区精华内容,希望帮助社区开发者学习IM+RTC知识,解答疑惑。赠人玫瑰,手有余香,如您有不错的内容需要收录,欢迎在在评论区投稿回复。

微信截图_20201207144054.png

Android篇

融云即时通讯SDK集成 — 通知检查

融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

Android 端如何添加自定义表情

解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

融云清空历史消息 Android 端

唠一唠融云的消息扩展功能

融云 IMkit 拦截或监听所有发送消息

融云如何把图片消息的图片上传到自己的文件服务器

唠一唠融云 VIVO push 无法跳转的解决方案

融云即时通讯SDK集成 — 定制UI(一) ——会话界面小改动

融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库

融云即时通讯SDK集成 — 定制UI(三) ——兼容Android Q

融云如何把图片消息的图片上传到自己的文件服务器

融云即时通讯SDK集成 — 华为推送的点击跳转处理

带你实现女朋友欲罢不能的 App

Flutter 集成融云 sdk

配置融云SDK的自签证书

自定义消息 包含 list 数组

关于融云聊天室KV 值的正确使用

融云 IM SDK 转 AndroidX

融云即时通讯SDK集成 — 国内厂商推送集成踩坑篇(Android平台)

在融云 IMkit 会话界面基础上添加消息已读未读

融云聊天室属性 kv

融云 ConversationListFragment 会话列表添加头部布局

融云即时通讯SDK集成 — FCM推送集成指南(Android平台)

融云集成之避坑指南-Android推送篇

融云IMKit 动态删除或添加plugin 的实现


iOS篇

iOS 基于实时音视频 SDK 实现屏幕共享功能——1

iOS 基于实时音视频 SDK 实现屏幕共享功能——2

iOS 基于实时音视频 SDK 实现屏幕共享功能——3

iOS 基于实时音视频 SDK 实现屏幕共享功能——4

如何隐藏融云输入框语音按钮

给融云的输入框上方加个功能按钮,怎么整?

融云 IM SDK 如何插入消息

集成融云 IMLib 时,如何实现一套类似于 IMKit 的用户信息管理机制

为融云聊天页面的输入框添加 Placeholder

30 分钟集成融云 IM 即时通讯

简单介绍融云 imkit 包含功能

融云的聊天页面在 iOS14 出现崩溃的解决办法

融云聊天页面长按消息后“翻译”功能的实现方法

使用融云 IM 点击最近聊天记录时跳转到 @ 自己的消息

如何设置融云用户信息

自定义融云会话列表 cell 选中背景

融云 IMKit 音频录制参数

融云会话页面刷新不及时问题

融云 Flutter IM SDK 解析

关于融云 SDK 在使用 p8 证书的坎坷~

融云 SDK 如何实现群组操作

如何利用融云 IMLib 来实现一个阅后即焚功能

干货分享——使用融云通讯能力库 IMLib 实现单群聊的阅读回执


Web篇

作为小白接融云 IM SDK 新路体验~

微信小程序集成融云 SDK (即时通讯) 集成必备条件

Web 端使用融云 SDK 集成实现滑动加载历史消息

融云IM SDK web 端集成 — 表情采坑篇

融云 Web SDK 如何实现表情的收发 ?

集成融云小程序 SDK 遇到的问题

使用融云 Web SDK 撤回消息

融云 RTC SDK 集成实现直播,趟坑之旅~~~

融云 Web SDK 删除历史消息

集成融云小程序 SDK 遇到的问题

Web 端集成融云 SDK 如何发送正确图片消息给移动端展示?

使用融云 IM SDK 实现 H5 直播聊天

WebRTC 实现实时音视频技术研究

融云发送语音消息

融云 CallLib 集成遇到的问题

结合融云 WebSDK 了解 WebSocket 基本原理

集成融云 Web 音视频通话踩坑之旅

SDK 兼容 JSON

融云 IM SDK 发送语音消息

集成融云 IM 问题总结

融云 Web SDK 如何实现只有一个设备登入

融云 Web 播放声音 — Flash 篇 (播放 AMR、WAV)

融云 IM 那些事儿

融云 AMR(Aduio) 播放 AMR 格式 Base64 码音频


社区福利

【领取见面礼】限量 100份 GeekOnline加油包!等你来拿

【有奖调研】Geek Online 2020 编程挑战赛参赛调研

【征稿活动】Geek Online 社区第一期投稿激励计划已启动!


GeekOnline编程挑战赛

Geek Online 2020 编程挑战赛官网

重磅!Geek Online 2020 编程挑战赛来了!

Geek Online 2020 编程挑战赛 GitHub 仓库

2 个月激烈角逐,15 支队伍突围决赛路演!Geek Online 2020 编程挑战赛完美收官!

一张图回顾 Geek Online 2020 编程挑战赛精彩瞬间!

“这些项目不是什么赚大钱的项目,但是它们足够有趣。”丨关于 Geek Online 2020 编程挑战赛,选手们如是说

融云 CTO 杨攀: Geek Online 2020 编程挑战赛 让开发者站上 C 位

【参赛攻略】你想了解的Geek Online 2020 编程挑战赛常见问题这里都有!

【融云集成常见问题整理】Geek Online 2020 编程挑战赛选手提问整理


求职招聘

【招聘】寻一枚熟悉融云IM的开发工程师,坐标合肥,待遇从优

和50万优质程序员一起成长——程序员客栈招聘

持续更新....

【融云集成常见问题整理】Geek Online 2020 编程挑战赛选手提问整理

GeekOnline编程挑战赛梅川酷子 发表了文章 • 0 个评论 • 953 次浏览 • 2020-12-02 18:40 • 来自相关话题

内容整理自Geek Online 2020 编程挑战赛群答疑内容,关于大赛请点击Geek Online 2020 编程挑战赛了解详情。如果您有IM/RTC开发,融云开发文档建议等技术问题欢迎留言讨论。问题 1: 下载SDK如何选择?各大版本有什么区别?问题 1... ...查看全部

内容整理自Geek Online 2020 编程挑战赛群答疑内容,关于大赛请点击Geek Online 2020 编程挑战赛了解详情。如果您有IM/RTC开发,融云开发文档建议等技术问题欢迎留言讨论。

问题 1: 下载SDK如何选择?各大版本有什么区别?

问题 1 答案:使用最新版本 4.0 SDK ,新版 SDK 包含很多新功能并且会将历史版本遗留 Bug 进行修复,所以极力推荐使用新版 SDK 4.0+ 集成开发,下载地址:https://www.rongcloud.cn/downloads

问题 2 :开发环境和生产环境有什么区别?

问题 2 答案:  开发环境功能免费使用,但用户数有 100 个限制,生产环境无用户数限制,但需要付费,咱们的参赛同学使用开发环境集成就好

问题 3 :参赛过程中开发产生的费用怎么办?(注:本条仅限于参赛期间选手的参赛作品)

问题 3 答案:开发环境功能均可免费体验,遇到特殊情况可在战队群里向融云同学处理

问题 4 :小程序开发有什么注意事项?

问题 4 答案:

 (1)需要在开发者后台小程序中开通,开通 30 分钟后生效

 (2)小程序发布上线需要优先设置合法域名:https://docs.rongcloud.cn/v4/views/rtc/call/noui/quick/mini.html

 (3)小程序特殊分类需要证书,例如社交小程序需要 ICP 证书,所以大家选择小程序分类时要提前注意是否需要证书

问题 5: 融云支持哪些平台?

问题 5 答案:支持 iOS、Android、Web、Flutter、uniapp、Electron,如果有 IoT 需求可以私信融云同学

问题 6:  如果遇到集成文档问题,怎么办?(也可以在本篇文章留言回复)

问题 6 答案:可直接在战队群里反馈至融云同学,欢迎大家对文档的改进提出宝贵建议,感谢

问题7:ios没上线 push怎么做?

问题7答案:在融云开发者后台 -> 应用 -> 开发环境可以设置

微信图片_20201202183130.png


问题8:融云的RTC集成必须要集成IM?

问题8答案:RTC SDK 依赖于 IM SDK,一定要连接 IM 后再进行 RTC 相关的集成

问题9:融云新版SDK4.0版本和2.0版本对比有哪些升级?具体有哪些优化和提升?

问题9答案:4.0 SDK 是融云基于近几年的经验积累和沉淀进行的重构版,包含对架构、连接、重连、弱网等使用场景做了特殊优化,除核心能力优化外 4.0 SDK 还在持续发布新功能,例如:聊天室 KV 存储、会话置顶免打扰等

问题10:我想问一下,rongrtc的storage改动如何监听似乎rongclient设置接受到消息的onreceived回调不会触发。而改动storage 时是有设置第三个消息参数的,Sdk的debug会打印storage_set的,可以拦截不?

问题10答案:如果设置了第三个参数,会触发接收方的 RongRTC 实例的 Message received 监听

问题11:融云有没有小程序版的IM集成SDK?

问题11答案:有的,开发者后台开通小程序后可以直接下载小程序 IM SDK,开通位置:https://developer.rongcloud.cn/miniprogram/index/

问题12:弱弱的问一句,融云可否实现微信群机器人?现在微商盛行,想用融云做个自动问答的机器人客服 」

问题12答案:可以的,融云支持将消息路由的能力,消息路由到自己服务器后可以对接三方图文识别厂商

问题13:融云有内嵌到app的H5版本客服机器人吗?

问题14:不单独提供客服的,但 IM SDK 支持 H5 的

感谢各位选手的参与,Geek Online 2020 编程挑战赛 完美收官,关于大赛:

 Geek Online 2020 编程挑战赛官网

2 个月激烈角逐,15 支队伍突围决赛路演!Geek Online 2020 编程挑战赛完美收官!

“这些项目不是什么赚大钱的项目,但是它们足够有趣。”丨关于 Geek Online 2020 编程挑战赛,选手们如是说

一张回顾 Geek Online 2020 编程挑战赛精彩瞬间!


关于IM/RTC开发,融云开发文档建议等技术问题欢迎留言讨论

融云即时通讯SDK集成 — 通知检查

IM即时通讯大兴 发表了文章 • 2 个评论 • 921 次浏览 • 2020-12-02 16:28 • 来自相关话题

背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地址:... ...查看全部

微信截图_20201202162632.png背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 集成完毕后, 也踩了不少坑. 所以这篇文章给大家总结下排查融云消息的本地通知和远端推送的办法. 希望可以帮助到正在看这篇文章的你.

什么是本地通知:

当我的App接入了融云的即时通讯sdk后, 便拥有了即时通讯的能力. sdk与融云服务器建立长连接, 当消息发出后, 先走到融云的服务器, 再转发给相应的用户. 这里移动端到服务端, 服务端到移动端, 走的通道都是长连接. 无论你的app是在前台还是在后台, 只要没有被杀死, 那么长连接是一直在的. 所以消息可以即时的发送到达给接收者. 融云把这种走长连接到达的消息, 在通知栏展示的通知叫做本地通知. 也就是消息是顺利发送到接收者端了, 逻辑可以走到消息接收监听那里. 融云sdk内部实现了消息到达后的本地通知, 也赋予了开发者自行实现消息到达后进行本地通知的权利.

本地通知的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

1.是否设置了 setOnReceiveMessageListener 监听, 并且 onReceived 方法返回的为 true。

 RongIM.setOnReceiveMessageListener(new RongIMClient.OnReceiveMessageListener() {
              @Override
              public boolean onReceived(Message message, 
                  return true;
              }
          });

如果 onReceived 方法返回值为 true 则是监听做了拦截, 则不会走通知逻辑。 2.发送的消息是否是自定义消息。 如果是自定义消息, 则请查看自定义消息的 MessageTag 的注解是否设置了 flag 的值为 MessageTag.ISCOUNTED 或 MessageTag.ISPERSISTED) 。 以下面为例。

@MessageTag(value = "RC:TxtMsg", flag = MessageTag.ISCOUNTED)
  @DestructionTag
  public class TextMessage extends MessageContent {
      ...
  }

如果没有设置其中之一,则不会走通知逻辑。

3.是否设置消息拦截器

设置代码如下:

/**
   * 设置接收消息时的拦截器
   *
   * @param messageInterceptor 拦截器
   */
  public void setMessageInterceptor(new RongIM.getInstance().MessageInterceptor() {
          @Override
          public boolean intercept(final Message message) {
                 return true;
          }
 });

如果设置了消息拦截器, 并且在 intercept() 方法中返回 true, 表示拦截此消息。 则不会走通知逻辑。

4.检查接收到的消息的 senderUserId 是否和当前用户的 id 相同。
如果 id 相同, 则不会走通知逻辑。 自己不可发给自己。

5.查看当前接收的消息是否是属于聊天室会话类型的
聊天室的消息目前不支持本地通知。

6.如果是自定义消息,请检查是否设置了自定义消息的 MessageProvider 。
如果没有,则不会走通知逻辑。

7.接收方本地是否有接收方的用户信息。 如果没有设置, 则不会走通知逻辑。

8.检查应用权限设置,看是否打开应用信任权限, 通知栏权限或者声音提示权限等。如果没有打开,请打开再试。

9.接收方没有发送方的用户信息,接收端不弹通知。用户信息通过用户信息提供者方式获取。
参考链接:https://docs.rongcloud.cn/v3/views/im/ui/guide/private/user/set/android.html

什么是远端推送:

集成了即时通讯的SDK, 我们的app不就能拥有像微信一样随时随地收到消息的即时通讯能力了? 说实话我一开始也是这么认为的. 可惜做开发也要按基本法来, Android平台回收app的这一关咱都过不了, app都给你杀死得透透的了你拿啥收消息呢? 咱又不是微信hhhh. 所以一番急赤白脸地阅读他家文档之后, 才发现app如果活着, 他融云能用自己的通道给你把消息推送到. 如果app被杀死了, 这个消息就在他家的服务端直接交给三方厂商了(也就是五大厂商蓝绿大厂华为小米FCM), 让这条消息走人家厂商的推送通道给送到你手机上.

远端推送的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

  1. 退出应用的时候,只能调用融云的 disconnect() 方法,而不是 logout()。这样退出后融云才会启动push进程。

  2. push 进程的名字不能更改,必须是默认的名字,既 io.rong.push.
    (2.6.0dev之后的版本,此进程名字可以修改)

  3. 通过 ddms 或者终端里敲入 adb shell ps|grep rong 查看终端里是否存在 io.rong.push 这个进程。

有些手机厂家做了特殊限制,不允许第三方后台进程启动,所以融云的后台进程起不来,导致收不到 push 消息。这种情况可以换个手机测试(如三星,大部分三星手机没有做权限限制)。另外大部分国产手机,有权限设置的菜单,比如小米,华为等,可以手动去安全中心,设置应用的自启动权限,后台运行权限等,就可以收到Push消息了。

不过 vivo 和 oppo 有些型号的手机,一旦应用退到后台,系统会很快把它杀死,这种没有办法解决。目前市面上所有推送都存在这个问题,除非系统把该应用加入白名单。

  1. push 进程存在,仍然收不到 push 消息。

如果你的应用有消息免打扰功能,那么请确认当前登录账号之前是否设置过消息免打扰。如果不太确定,那最好去你的应用设定里重新设置下消息免打扰时间。 这里要注意的是: 如果这个账号之前在别的手机上设置过消息免打扰,换一台手机登录或者卸载重装的时候,融云服务端记录的仍然是之前设置的消息免打扰状态,所以这种情况下是收不到push消息的。


融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

IM即时通讯大兴 发表了文章 • 0 个评论 • 946 次浏览 • 2020-12-02 16:28 • 来自相关话题

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行... ...查看全部

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.

微信截图_20201202162201.png

我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行刷新, 需要自己调用代码进行刷新, 但是, 融云没有提供对应的刷新界面的方法, 所以只能看代码.

刷新 ListView 是需要获取 Adapter , 然后使用 Adapter 进行刷新, 但是我们无法从 ConversationListFragment 中直接获取, 那怎么办呢?

我们知道, ListView 中有 getAdapter 的方法. 那我们能不能先获取 ListView 控件呢? 答案是可以的. 我们通过 findById 方法即可获取 ListView. 通过查找, 我们可以知道, 融云会话列表的 listView 的 id 为 R.id.rc_list.

所以代码如下.

ListView mList = (ListView)findViewById(R.id.rc_list);
HeaderViewListAdapter adapter = (HeaderViewListAdapter)mList.getAdapter();
ConversationListAdapter wrappedAdapter = (ConversationListAdapter)adapter.getWrappedAdapter();
if (adapter != null) {
    wrappedAdapter.notifyDataSetChanged();
}


融云如何把图片消息的图片上传到自己的文件服务器

IM即时通讯大神庵 发表了文章 • 0 个评论 • 842 次浏览 • 2020-12-02 16:16 • 来自相关话题

我们使用融云开发的项目, 但我们有一个需求是, 把图片不要上传到融云的服务器, 而是自己的服务器.于是就咨询了一下技术支持.被告知有一个接口方法完全可以满足我们的需求.       &nbs... ...查看全部

我们使用融云开发的项目, 但我们有一个需求是, 把图片不要上传到融云的服务器, 而是自己的服务器.于是就咨询了一下技术支持.

微信截图_20201202160709.png

被告知有一个接口方法完全可以满足我们的需求.

             ImageMessage imageMessage = ImageMessage.obtain(Uri.parse(FILEPATH), Uri.parse(FILEPATH));
        configSendMessage(imageMessage);
        Message message = Message.obtain(mTargetId,mConversationType,imageMessage);
        RongIM.getInstance().sendImageMessage(message, "pushcontent", "pushdata",
                new RongIMClient.SendImageMessageWithUploadListenerCallback() {
                    @Override
                    public void onAttached(Message message, RongIMClient.UploadImageStatusListener watcher) {
                            // 这里是自己上传图片的逻辑, 图片的路径可以通过 message 中进行获取. 
                            //watcher 这个参数主要是用于把自己的上传状态同步给 sdk. 这样我们就可以使用 sdk 内部的默认逻辑, 包括界面.
                    }
                    @Override
                    public void onError(Message message, RongIMClient.ErrorCode code) {
                    }
                    @Override
                    public void onSuccess(Message message) {
                    }
                    @Override
                    public void onProgress(Message message, int progress) {
                    }
                });

这样就可以继续使用 IMkit 的界面以及其他逻辑, 只是在 onAttached 中编写我们的上传逻辑并使用 watcher 上传的进度、成功或失败状态同步给 sdk 即可.


融云 IMkit 拦截或监听所有发送消息

IM即时通讯大神庵 发表了文章 • 0 个评论 • 797 次浏览 • 2020-12-02 16:16 • 来自相关话题

最近集成融云 IMkit 的 SDK, 有一个需求是要监听所有发出去的消息, 根据消息类型拦截或者进行修改.在官方文档上着了一遍, 都没有找到, 偶然在看 API 文档的时候看见了一个监听然后做了尝试, 是可以满足需求的, 所以再次记录一下.具体的方法是 Ro... ...查看全部

微信截图_20201202160910.png

最近集成融云 IMkit 的 SDK, 有一个需求是要监听所有发出去的消息, 根据消息类型拦截或者进行修改.

在官方文档上着了一遍, 都没有找到, 偶然在看 API 文档的时候看见了一个监听然后做了尝试, 是可以满足需求的, 所以再次记录一下.

具体的方法是 RongIM 类下的 setSendMessageListener 方法.

代码如下.

 /**
     * 设置发送消息的监听。
     *
     * @param listener 发送消息的监听。
     */
    RongIM.setSendMessageListener(new OnSendMessageListener() {
           @Override
         public Message onSend(Message message) {
                 // 发送消息之前会走此方法. message 为要发送的消息,
                 // 如果返回 null 的话, 就不会发送此消息了.
                 return message;
         }
         @Override
        public boolean onSent(Message message, SentMessageErrorCode sentMessageErrorCode) {
                 发送成功之后会走方法. 返回 true , 就会走 SDK 的后续逻辑. 返回 false 就拦截了.         return true;
             }
    }).


融云清空历史消息 Android 端

IM即时通讯大神庵 发表了文章 • 0 个评论 • 849 次浏览 • 2020-12-02 16:16 • 来自相关话题

融云清空历史消息 Android 端先调用获取历史消息。/** * 根据会话类型的目标 Id,回调方式获取N条历史消息记录。 * * @param conversationType 会话类型。不支持传入&nbs... ...查看全部

融云清空历史消息 Android 端

微信截图_20201202161404.png

  1. 先调用获取历史消息。

/**
* 根据会话类型的目标 Id,回调方式获取N条历史消息记录。
*
* @param conversationType 会话类型。不支持传入 ConversationType.CHATROOM。
* @param targetId         目标 Id。根据不同的 conversationType,可能是用户 Id、讨论组 Id、群组 Id。
* @param oldestMessageId  最后一条消息的 Id,获取此消息之前的 count 条消息,没有消息第一次调用应设置为:-1。
* @param count            要获取的消息数量。
* @param callback         获取历史消息记录的回调,按照时间顺序从新到旧排列。
*/
public void getHistoryMessages(Conversation.ConversationType conversationType, String targetId, int oldestMessageId, int count, RongIMClient.ResultCallback<List<Message>> callback) {
RongIMClient.getInstance().getHistoryMessages(conversationType, targetId, oldestMessageId, count, callback);
}
  1. 再调用删除的接口。

/**
* 删除指定的一条或者一组消息,回调方式获取是否删除成功。
*
* @param messageIds 要删除的消息 Id 数组。
* @param callback   是否删除成功的回调。
*/
public void deleteMessages(final int[] messageIds, final RongIMClient.ResultCallback<Boolean> callback) {
RongIMClient.getInstance().deleteMessages(messageIds, new RongIMClient.ResultCallback<Boolean>() {
@Override
public void onSuccess(Boolean bool) {
if (bool)
RongContext.getInstance().getEventBus().post(new Event.MessageDeleteEvent(messageIds));
if (callback != null)
callback.onSuccess(bool);
}
@Override
public void onError(RongIMClient.ErrorCode e) {
if (callback != null)
callback.onError(e);
}
});
}

清除远端消息调接口

  1. cleanRemote 传 true ,时间戳传入当前的时间戳。

/**

  • 删除指定时间戳之前的消息,可选择是否同时删除服务器端消息

  • 此方法从服务器端清除历史消息,但是必须先开通历史消息云存储功能。

  • 根据会话类型和 TargetId 清除某一会话指定时间戳之前的本地数据库消息(服务端历史消息),

  • 清除成功后只能从本地数据库(服务端)获取到该时间戳之后的历史消息。

*
* @param conversationType 会话类型。
* @param targetId         会话目标ID。
* @param recordTime       清除消息截止时间戳,【0 ~ 当前时间的 Unix 时间戳】。
* @param cleanRemote      是否删除服务器端消息
* @param callback         清除消息的回调。
*/
public void cleanHistoryMessages
  1. 如果界面没有刷新,重新进入看下是否全部删除了。

您先试下是否可以删除,如果可以删除,但是界面没有刷新刷新的话,您可以调用 ConversationFragment 的

getMessageAdapter
方法,然后

mListAdapter.removeAll();

mListAdapter.notifyDataSetChanged();
试下。


唠一唠融云的消息扩展功能

IM即时通讯大神庵 发表了文章 • 0 个评论 • 852 次浏览 • 2020-12-02 16:16 • 来自相关话题

在使用融云 SDK 的过程中,由于定制化程度过高,其中有一项业务的实现是需要实现“红包”功能,但是实现的流程比较复杂:自定义红包消息 MoneyMessage ,然后 A 用户发送给B 用户,B 用户在点击之后,同样发送一个自定义通知消息给 A ,用来修改A ... ...查看全部

微信截图_20201202161542.png

在使用融云 SDK 的过程中,由于定制化程度过高,其中有一项业务的实现是需要实现“红包”功能,但是实现的流程比较复杂:

  • 自定义红包消息 MoneyMessage ,然后 A 用户发送给B 用户,B 用户在点击之后,同样发送一个自定义通知消息给 A ,用来修改A 用户的界面。

上述流程看上去比较简单,但是实现起来就会比较复杂,并且并不灵活,于是跟融云技术反馈此问题之后,在从 4.0.3 版本开始支持单条消息扩展信息设置功能,用 消息扩展功能实现就会无比简单。

消息扩展功能的本质是在Message 类中添加了扩展功能;

 /**
     * 设置是否可以包含扩展信息
     *
     * @param canIncludeExpansion 是否可以包含扩展信息
     */
   public void setCanIncludeExpansion(boolean canIncludeExpansion);
  /**
  * 消息扩展信息列表
  *
 * @return 消息扩展信息列表
 */
public Map<String, String> getExpansion();
 /**
* 设置消息扩展信息列表
* 扩展信息只支持单聊和群组,其它会话类型不能设置扩展信息。*
* @param expansion 消息扩展信息列表
*/
 public void setExpansion(HashMap<String, String> expansion);

可以调用 RongIMClient.getInstance().updateMessageExpansion(); 来设置消息扩展属性;
可以调用 RongIMClient.getInstance().removeMessageExpansion();来删除消息扩展属性;
当然监听消息属性可以设置:

   RongIMClient.getInstance().setMessageExpansionListener(new RongIMClient.MessageExpansionListener() {
        @Override
        public void onMessageExpansionUpdate(Map<String, String> expansion, Message message) {
        }
        @Override
        public void onMessageExpansionRemove(List<String> keyArray, Message message) {
        }
    });

来实时监听消息属性的变化,这样可以动态实现红包点击接受的功能,或者礼物场景的实现

融云即时通讯SDK集成 — 定制UI(二) ——添加自定义表情库

IM即时通讯徐凤年 发表了文章 • 0 个评论 • 808 次浏览 • 2020-12-02 16:03 • 来自相关话题

背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地址:... ...查看全部

背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 我是使用的他们家的带UI的sdk,(他们家有带UI和不带UI的两种sdk, 不带UI的sdk就是只有即时通讯能力, 所有的UI都需要开发者自定实现, 带UI的sdk封装了一些基本的界面,例如会话列表, 和别人聊天的会话界面.)当然这些已经集成了UI的sdk并不能完全满足一个产品的需求, 所以这篇文章跟大家讲下如何添加一套自定义的表情包.

效果如下哈:

QQ20201112-0.jpg

虽然这里有点难看了哈哈, 不过是为了给大家展示方法嘛, 就不管那么多了. 可以看到底下除了默认的emoji的表情包, 还多了一个tab, 我是从QQ表情搞了一套, 直接就加在这里了. 一个可爱的小猪猪🐷哈哈哈.

添加步骤

需要改动的有这么几个类:

AndroidEmoji: 控制emoji图标资源, 编码, 以及相应展示的类
RongExtension: 会话界面除去聊天气泡与title bar的整个下方输入区域
IEmoticonTab: 表情tab
DefaultExtensionModule: 表情tab的上层控件.

对于AndroidEmoji这个类, 可以直接照抄, 把资源文件替换成自己准备好的图标, 以及编码/描述

public class ConversationListAdapter extends BaseAdapter<UIConversation> {
    private final static String TAG = "ConversationListAdapter";
    LayoutInflater mInflater;
    Context mContext;
    @Override
    public long getItemId(int position) {
        UIConversation conversation = getItem(position);
        if (conversation == null)
            return 0;
        return conversation.hashCode();
    }
    protected class ViewHolder {
        public View layout;
        public View leftImageLayout;
        public View rightImageLayout;
        public View leftUnReadView;
        public View rightUnReadView;
        public AsyncImageView leftImageView;
        public TextView unReadMsgCount;
        public ImageView unReadMsgCountIcon;
        public AsyncImageView rightImageView;
        public TextView unReadMsgCountRight;
        public ImageView unReadMsgCountRightIcon;
        public ProviderContainerView contentView;
    }
    public ConversationListAdapter(Context context) {
        super();
        mContext = context;
        mInflater = LayoutInflater.from(mContext);
    }
    public int findGatheredItem(Conversation.ConversationType type) {
        int index = getCount();
        int position = -1;
        while ((index-- > 0)) {
            UIConversation uiConversation = getItem(index);
            if (uiConversation.getConversationType().equals(type)) {
                position = index;
                break;
            }
        }
        return position;
    }
    public int findPosition(Conversation.ConversationType type, String targetId) {
        int index = getCount();
        int position = -1;
        while (index-- > 0) {
            if (getItem(index).getConversationType().equals(type)
                    && getItem(index).getConversationTargetId().equals(targetId)) {
                position = index;
                break;
            }
        }
        return position;
    }
    @Override
    protected View newView(Context context, int position, ViewGroup group) {
        View result = mInflater.inflate(R.layout.rc_item_conversation, null);
        ViewHolder holder = new ViewHolder();
        holder.layout = findViewById(result, R.id.rc_item_conversation);
        holder.leftImageLayout = findViewById(result, R.id.rc_item1);
        holder.rightImageLayout = findViewById(result, R.id.rc_item2);
        holder.leftUnReadView = findViewById(result, R.id.rc_unread_view_left);
        holder.rightUnReadView = findViewById(result, R.id.rc_unread_view_right);
        holder.leftImageView = findViewById(result, R.id.rc_left);
        holder.rightImageView = findViewById(result, R.id.rc_right);
        holder.contentView = findViewById(result, R.id.rc_content);
        holder.unReadMsgCount = findViewById(result, R.id.rc_unread_message);
        holder.unReadMsgCountRight = findViewById(result, R.id.rc_unread_message_right);
        holder.unReadMsgCountIcon = findViewById(result, R.id.rc_unread_message_icon);
        holder.unReadMsgCountRightIcon = findViewById(result, R.id.rc_unread_message_icon_right);
        result.setTag(holder);
        return result;
    }
    @Override
    protected void bindView(View v, int position, final UIConversation data) {
        ViewHolder holder = (ViewHolder) v.getTag();
        if (data == null) {
            return;
        }
        /*通过会话类型,获得对应的会话provider.ex: PrivateConversationProvider*/
        IContainerItemProvider provider = RongContext.getInstance().getConversationTemplate(data.getConversationType().getName());
        if (provider == null) {
            RLog.e(TAG, "provider is null");
            return;
        }
        View view = holder.contentView.inflate(provider);
        provider.bindView(view, position, data);
        //设置背景色
        if (data.isTop())
            holder.layout.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.rc_item_top_list_selector));
        else
            holder.layout.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.rc_item_list_selector));
        ConversationProviderTag tag = RongContext.getInstance().getConversationProviderTag(data.getConversationType().getName());
        int defaultId;
        if (data.getConversationType().equals(Conversation.ConversationType.GROUP)) {
            defaultId = R.drawable.rc_default_group_portrait;
        } else if (data.getConversationType().equals(Conversation.ConversationType.DISCUSSION)) {
            defaultId = R.drawable.rc_default_discussion_portrait;
        } else {
            defaultId = R.drawable.rc_default_portrait;
        }
        // 1:图像靠左显示。2:图像靠右显示。3:不显示图像。
        if (tag.portraitPosition() == 1) {
            holder.leftImageLayout.setVisibility(View.VISIBLE);
            holder.leftImageLayout.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnPortraitItemClick != null)
                        mOnPortraitItemClick.onPortraitItemClick(v, data);
                }
            });
            holder.leftImageLayout.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (mOnPortraitItemClick != null)
                        mOnPortraitItemClick.onPortraitItemLongClick(v, data);
                    return true;
                }
            });
            if (data.getConversationGatherState()) {
                holder.leftImageView.setAvatar(null, defaultId);
            } else {
                if (data.getIconUrl() != null) {
                    holder.leftImageView.setAvatar(data.getIconUrl().toString(), defaultId);
                } else {
                    holder.leftImageView.setAvatar(null, defaultId);
                }
            }
            if (data.getUnReadMessageCount() > 0) {
                holder.unReadMsgCountIcon.setVisibility(View.VISIBLE);
                setUnReadViewLayoutParams(holder.leftUnReadView, data.getUnReadType());
                if (data.getUnReadType().equals(UIConversation.UnreadRemindType.REMIND_WITH_COUNTING)) {
                    if (data.getUnReadMessageCount() > 99) {
                        holder.unReadMsgCount.setText(mContext.getResources().getString(R.string.rc_message_unread_count));
                    } else {
                        holder.unReadMsgCount.setText(Integer.toString(data.getUnReadMessageCount()));
                    }
                    holder.unReadMsgCount.setVisibility(View.VISIBLE);
                    holder.unReadMsgCountIcon.setImageResource(R.drawable.rc_unread_count_bg);
                } else {
                    holder.unReadMsgCount.setVisibility(View.GONE);
                    holder.unReadMsgCountIcon.setImageResource(R.drawable.rc_unread_remind_list_count);
                }
            } else {
                holder.unReadMsgCountIcon.setVisibility(View.GONE);
                holder.unReadMsgCount.setVisibility(View.GONE);
            }
            holder.rightImageLayout.setVisibility(View.GONE);
        } else if (tag.portraitPosition() == 2) {
            holder.rightImageLayout.setVisibility(View.VISIBLE);
            holder.rightImageLayout.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnPortraitItemClick != null)
                        mOnPortraitItemClick.onPortraitItemClick(v, data);
                }
            });
            holder.rightImageLayout.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (mOnPortraitItemClick != null)
                        mOnPortraitItemClick.onPortraitItemLongClick(v, data);
                    return true;
                }
            });
            if (data.getConversationGatherState()) {
                holder.rightImageView.setAvatar(null, defaultId);
            } else {
                if (data.getIconUrl() != null) {
                    holder.rightImageView.setAvatar(data.getIconUrl().toString(), defaultId);
                } else {
                    holder.rightImageView.setAvatar(null, defaultId);
                }
            }
            if (data.getUnReadMessageCount() > 0) {
                holder.unReadMsgCountRightIcon.setVisibility(View.VISIBLE);
                setUnReadViewLayoutParams(holder.rightUnReadView, data.getUnReadType());
                if (data.getUnReadType().equals(UIConversation.UnreadRemindType.REMIND_WITH_COUNTING)) {
                    holder.unReadMsgCount.setVisibility(View.VISIBLE);
                    if (data.getUnReadMessageCount() > 99) {
                        holder.unReadMsgCountRight.setText(mContext.getResources().getString(R.string.rc_message_unread_count));
                    } else {
                        holder.unReadMsgCountRight.setText(Integer.toString(data.getUnReadMessageCount()));
                    }
                    holder.unReadMsgCountRightIcon.setImageResource(R.drawable.rc_unread_count_bg);
                } else {
                    holder.unReadMsgCount.setVisibility(View.GONE);
                    holder.unReadMsgCountRightIcon.setImageResource(R.drawable.rc_unread_remind_without_count);
                }
            } else {
                holder.unReadMsgCountIcon.setVisibility(View.GONE);
                holder.unReadMsgCount.setVisibility(View.GONE);
            }
            holder.leftImageLayout.setVisibility(View.GONE);
        } else if (tag.portraitPosition() == 3) {
            holder.rightImageLayout.setVisibility(View.GONE);
            holder.leftImageLayout.setVisibility(View.GONE);
        } else {
            throw new IllegalArgumentException("the portrait position is wrong!");
        }
        MessageContent content = data.getMessageContent();
        if (content != null && content.isDestruct()) {
            RongIMClient.getInstance().getMessage(data.getLatestMessageId(), new RongIMClient.ResultCallback<Message>() {
                @Override
                public void onSuccess(Message message) {
                    if (message == null) {
                        EventBus.getDefault().post(new Event.MessageDeleteEvent(data.getLatestMessageId()));
                    }
                }
                @Override
                public void onError(RongIMClient.ErrorCode e) {
                }
            });
        }
    }
    protected void setUnReadViewLayoutParams(View view, UIConversation.UnreadRemindType type) {
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        Context context = view.getContext();
        if (type == UIConversation.UnreadRemindType.REMIND_WITH_COUNTING) {
            params.width = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_18);
            params.height = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_18);
            params.leftMargin = (int) mContext.getResources().getDimension(R.dimen.rc_dimen_size_44);
            params.topMargin = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_5);
        } else {
            params.width = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_9);
            params.height = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_9);
            params.leftMargin = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_50);
            params.topMargin = (int) context.getResources().getDimension(R.dimen.rc_dimen_size_7);
        }
        view.setLayoutParams(params);
    }
    private OnPortraitItemClick mOnPortraitItemClick;
    public interface OnPortraitItemClick {
        void onPortraitItemClick(View v, UIConversation data);
        boolean onPortraitItemLongClick(View v, UIConversation data);
    }
    public void setOnPortraitItemClick(OnPortraitItemClick onPortraitItemClick) {
        this.mOnPortraitItemClick = onPortraitItemClick;
    }
}

集成DefaultExtensionModule, 实现MyExtensionModule, 重写getEmoticonTabs()方法, 为每个图标设置监听.

@Override
    public List<IEmoticonTab> getEmoticonTabs() {
        List<IEmoticonTab> emoticonTabs =  super.getEmoticonTabs();
        MyEmoticonTab myEmoticonTab =new MyEmoticonTab();
        myEmoticonTab.setOnItemClickListener(new IEmojiItemClickListener() {
            @Override
            public void onEmojiClick(String emoji) {
                EditText editText = MyExtensionModule.this.mEditText;
                if (editText != null) {
                    int start = editText.getSelectionStart();
                    editText.getText().insert(start, emoji);
                }
            }
            @Override
            public void onDeleteClick() {
                EditText editText = MyExtensionModule.this.mEditText;
                if (editText != null) {
                    editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
                }
            }
        });
        emoticonTabs.add(myEmoticonTab);
        return emoticonTabs;
    }

继承RongExtension, 实现MyRongExtension, 然后获取到editText, 为其设置监听, 代码如下:

public class MyRongExtension extends RongExtension {
    EditText mEditText;
    public MyRongExtension(Context context) {
        super(context);
        tryAddTextChangedAction();
    }
    public MyRongExtension(Context context, AttributeSet attrs) {
        super(context, attrs);
        tryAddTextChangedAction();
    }
    private void tryAddTextChangedAction() {
        mEditText = getInputEditText();
        TextWatcher textWatcher = new TextWatcher() {
            private int start;
            private int count;
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                this.start = start;
                this.count = count;
            }
            @Override
            public void afterTextChanged(Editable s) {
                // 这块的检验规则是纯emoji的, 加其他表情的话直接给他去掉, 或者自己写规则。
//                if (QQEmoji.isQQEmoji(s.subSequence(start, start + count).toString())) {
                    mEditText.removeTextChangedListener(this);
                    String resultStr = QQEmoji.replaceEmojiWithText(s.toString());
                    mEditText.setText(QQEmoji.ensure(resultStr), TextView.BufferType.SPANNABLE);
                    mEditText.setSelection(mEditText.getText().length());
                    mEditText.addTextChangedListener(this);
//                }
            }
        };
        mEditText.addTextChangedListener(textWatcher);
    }
}

这样一来, 就大功告成啦. 添加好了一套属于自己的表情包!

融云即时通讯SDK集成 — 华为推送的点击跳转处理

IM即时通讯徐凤年 发表了文章 • 0 个评论 • 366 次浏览 • 2020-12-02 15:58 • 来自相关话题

1.背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地... ...查看全部

1.背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 基础的IM聊天, 群组聊天等功能集成在另一篇博客中已经讲过, 这里就先不说了. 之前的一篇文章已经讲过, 这篇文章重点讲下华为推送过来的通知栏点击事件, 这小小的一件事, 可耗费了不少的精力.

2. 融云家SDK接入厂商推送指南

这个他家讲的挺清楚了, 都近似手把手的教了, 那我也就不在这浪费键盘寿命了(偷个懒hhhhh).给他家官方文档往这儿一贴好了: https://docs.rongcloud.cn/v4/views/im/ui/guide/private/notify/push/mi.html. 大家注意一下他家华为厂商推送的文档分2.x和4.x哈, 对应的是他家旧版/新版SDK中接入的厂商的新/旧版本推送包.我这里之前是踩过坑的, 推荐大家还是用他家的4.x的sdk包集成, 里边集成的是华为新版本的推送包. 因为华为已经在逐渐弃用旧版本的推送包了, 所以不想以后麻烦再迭代的话, 还是直接上新版本比较好, 因为现在已经有接入旧版本包的app在某些低版本华为Rom上收不到推送了, 还是谨慎一点比较好.

3. 华为推送通知点击跳转自定义

这里我们所指的跳转是, 点击通知栏到达的华为推送通知后, 调起我们的app, 跳到相应的Activity的. 因为我们做的是即时通讯App, 所以点击别人发来的消息推送通知后, 当然是跳到和该用户聊天的会话页面, 但就是这样一个小业务, 实现起来还真有点麻烦. 所以这里也算是给大家踩踩坑啦.

首先是点击跳转的逻辑, 通过和融云技术支持联系以及查看他们sdk的代码, 得知他们的推送都是走了一个叫PushMessageReceiver的类, 开发者可以注册并继承该类, 重写onNotificationMessageClicked()方法, 显而易见这就是对通知消息的监听. 可这也是第一个坑, 华为因为平台的限制, 推送通知的点击事件根本不走这里. 准确的说是华为和Oppo都因为各自平台的限制, 不走这里. 所以当我兴致勃勃重写了这个方法准备一次通过的时候, 点击后直接走到了会话列表界面, 而非会话界面.

这是因为华为平台点击跳转是需要配一个intent, 在融云的后台, 如图所示:
QQ20201102-111109@2x.png

这里的intent即为通知栏点击跳转后的隐式启动相应activity的那个intent.

获取某Activity相应intent的代码, 可把intent给打到log中:

  1. Intent intent =newIntent(Intent.ACTION_VIEW,

  2.                Uri.parse("wonderfullpush://com.wonderfull.android.push/notification?action=your parameter"));

  3.        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

  4.        intent.setAction(Intent.ACTION_VIEW);

  5.        String intnetUri = intent.toUri(Intent.URI_INTENT_SCHEME);

  6.        Log.d("hwpush","intnetUri="+ intnetUri);

生成的intent大概是这样的格式:

  1. intent://com.wonderfull.android.push/notification?action=$action#Intent;scheme=wonderfullpush;action=android.intent.action.VIEW;launchFlags=0x10000000;end

不过这块还有坑, 还不能直接把装会话界面的activity的intent直接写在这里, 因为这个会话界面去的究竟是哪个对话者的界面, 相应userId的参数是传不到的. 在这里我尝试获取过intent里边的数据, 是空的.

和融云的技术支持团队确认后, 找到了解决方案. 只有intent走到入口类, 才能拿的到通知里携带的数据, 我app的闪屏页, 也就是SplashActivity, 在融云后台把自定义点击跳转intent配成SplashActivity的. 然后在这里把intent的数据拿到, 再一次调用融云的接口跳转到相应的会话界面就OK了.

等拿到这个intent的后, 又是来了一点点小麻烦. 根据融云的文档, 我在入口Activity(闪屏页, SplashActivity)这里接收到了intent, 从intent里边取相应的数据:

这个intent中给的数据包含了转义符:

{"rc":"{\"conversationType\":\"1\",\"targetId\":\"userid8\",\"sourceType\":\"0\",\"fromUserId\":\"userid8\",\"objectName\":\"RC:TxtMsg\",\"id\":\"BLCG-G8TC-U7E6-KV7P\",\"tId\":\"doctorid3\"}"}
String jsonStr = "{\"rc\":\"{\\\"conversationType\\\":\\\"1\\\",\\\"targetId\\\":\\\"userid8\\\",\\\"sourceType\\\":\\\"0\\\",\\\"fromUserId\\\":\\\"userid8\\\",\\\"objectName\\\":\\\"RC:TxtMsg\\\",\\\"id\\\":\\\"BLCG-G8TC-U7E6-KV7P\\\",\\\"tId\\\":\\\"doctorid3\\\"}\"}";        String fixStr1 = jsonStr.replace("\\", "");        String fixStr2 = fixStr1.replace("\"rc\":\"", "\"rc\":" );        String result = fixStr2.replace("\"}\"", "\"}");        Log.d(TAG, "result jsonStr: "+ result);        JSONObject jsonObject;        try {            jsonObject = new JSONObject(result);            String options = jsonObject.getString("rc");            JsonObject object = (JsonObject) new JsonParser().parse(options);            String targetId = object.getAsJsonObject().get("targetId").getAsString();            Log.d(TAG, "analyse json targetId: " + targetId);        } catch (JSONException e) {            e.printStackTrace();        }

我选择了自行处理字符串, 拿到了相应的targetId, 从我的闪屏页跳转到相应的会话界面去. 这样需求就搞定了.


友情链接