前言
公司业务需要,PC端,移动端都用到了第三方 网易云信 IM 来实现在线客服咨询。
在这当中难免遇到一些需求是网易云信没有提供,需要自行编码进行扩展的。写此篇文章的目的正是因业务需要,需要在网易云信的基础上进行消息类型的扩展。
此篇文章里的代码是基于 网易云信 NIM_PC_Demo_x86_x64_v4.6.0 版 进行修改的。
开发环境
根据网易云信官网 NIM Windows(PC) Demo导读 需要安装相应的环境
- Visual Studio 2013 Update5(必须使用Update5版本) vs版本下载见下图
我在安装完此环境后依然运行不起来(由于我是c++小白,环境搭建花了点时间),然后网上各种找原因,终于找到解决方案,安装 Windows SDK 8.1 ,然后终于才把下载过来的项目跑起来。(出现问题时忘记截图了,抱歉!)
业务需要
如下图所示的消息类型
标题是PC版,可想而知,肯定还有其他如 Android版,iOS版,Web版等,不可能此类型的消息(我称它为图文消息
)只支持PC,而在iOS,Android或Web端无法显示问题。以下附上其他版本扩展的链接
- 网易云信-新增自定义消息(Android版)
- 网易云信-新增自定义消息(iOS移动版)
- 网易云信-新增自定义消息(Web版)
- 网易云信-新增自定义消息(H5移动版)
正文
- 将demo运行起来后,首先我们要修改的就是将 appkey 改为自己的。
我这里说下VS里如何运行C++项目(可能不是标准的姿势)。
设置完成后,以后运行项目就可以直接按F5或者点击本地Windows调试器启动项目,不用每次都去点击 nim_demo右键->调试->启动新实例
如果你不嫌麻烦的话,也可以按以下方式启动
- 运行没有问题后,修改以下几个文件配置,将demo修改为自己所用。
- 修改
shared/util.h
中的DEMO_GLOBAL_APP_KEY
和DEMO_GLOBAL_TEST_APP_KEY
,填入自己的appKey
- 修改
修改后点击 停止调试
,然后在 启动新实例
,启动后再使用自己的云信帐号帐号登录
3.替换图片
替换主要与logo有关的图片
bin/themes/default/login/login.png
,
bin/themes/default/about/login.png
,
bin/themes/default/main/duoduan01.png
4.注释掉我们不需要的功能 (我这里只注释关键代码,以防哪天需要此功能时放开注释就好了)
- 登录页 去除 注册 和 免登录,体验匿名聊天室功能。
打开登录界面的布局文件 bin/themes/default/login/login.xml
,
找到name值为register_account的Button节点,添加属性 visible="false",将此按钮隐藏。
找到name值为anonymous_chatroom的Button节点,添加属性 visible="false",将此按钮隐藏。
上面两步操作完成后,点击运行新实例,发现帐号输入框无法获得焦点,解决办法是在底下的 VBox name
为enter_panel
的margin
属性上添加name
值为register_account
的Button
的高 15,修改前的margin
值为20,0,20,0
,修改后的margin
值为20,15,20,0
,目的是让注册按钮隐藏后,表单距顶部的高度不变。
//...
//...
//...
//...
//...
- 登录后的主界面 去除底部的 直播间 和 浏览器测试
打开登录界面的布局文件 bin/themes/default/main/main.xml
,注释掉以下节点
- 一对一单聊,聊天界面移除 语音,视频,白板,提醒消息
打开 tool_kits/ui_component/ui_kit/gui/session/session_box.cpp
,注释掉以下内容(大约在94行)
//...
void SessionBox::InitSessionBox()
{
//...
if (session_type_ == nim::kNIMSessionTypeP2P && !IsFileTransPhone())
{
// 将语音,视频,白板注释掉
//btn_audio->SetVisible(true);
//btn_video->SetVisible(true);
//btn_rts->SetVisible(true);
}
//...
}
//...
打开 bin/themes/default/session/session_box.xml
,找到 name
为 btn_tip
的 Button,添加属性visible="false"
,隐藏掉提醒消息。
5.新增自定义 图文链接消息的显示
- 创建自定义的消息类型,在
tool_kits/ui_component/ui_kit/gui/session/control/bubbles
目录下创建bubble_link.h
和bubble_link.cpp
文件
创建完成后从同级目录下的其他文件里复制代码过来,稍作修改,比如我复制的是 bubble_text.h
和 bubble_text.cpp
文件内容到我自己创建的文件中,复制后的代码如下:
bubble_link.h 内容如下
#pragma once
#include "bubble_item.h"
namespace nim_comp
{
/** @class MsgBubbleLink
* @brief 会话窗体中聊天框内的图文链接消息项
* @copyright (c) 2015, NetEase Inc. All rights reserved
* @author Andy
* @date 2018/1/8
*/
class MsgBubbleLink : public MsgBubbleItem
{
public:
/**
* 初始化控件内部指针
* @param[in] bubble_right 是否显示到右侧
* @return void 无返回值
*/
virtual void InitControl(bool bubble_right);
/**
* 初始化控件外观
* @param[in] msg 消息信息结构体
* @return void 无返回值
*/
virtual void InitInfo(const nim::IMMessage &msg);
/**
* 响应此消息项的单击消息,打开浏览器
*@param[in] param 被单击的按钮的相关信息
* @return bool 返回值true: 继续传递控件消息, false: 停止传递控件消息
*/
virtual bool OnClicked(ui::EventArgs* arg);
/**
* 响应此消息项的右击消息,弹出菜单
* @param[in] param 被单击的菜单项的相关信息
* @return bool 返回值true: 继续传递控件消息, false: 停止传递控件消息
*/
bool OnMenu(ui::EventArgs* arg);
private:
/**
* 设置图片资源的路径
* @return void 无返回值
*/
void InitResPath();
protected:
// 显示消息的box容器
ui::ButtonBox* msg_link_;
// 显示图片的box容器
ui::Box* image_box_;
// 标题
ui::RichEdit* title_;
// 图片
ui::Control* image_;
// 描述
ui::RichEdit* describe_;
// 图片的目录
std::wstring path_;
// 需要跳转的地址
std::wstring link_;
};
}
bubble_link.cpp 内容如下
#include "bubble_link.h"
using namespace ui;
namespace nim_comp
{
void MsgBubbleLink::InitControl(bool bubble_right)
{
__super::InitControl(bubble_right);
}
void MsgBubbleLink::InitInfo(const nim::IMMessage &msg)
{
__super::InitInfo(msg);
}
void MsgBubbleLink::InitResPath()
{
}
bool MsgBubbleLink::OnMenu(ui::EventArgs* arg)
{
return false;
}
bool MsgBubbleLink::OnClicked(ui::EventArgs* arg)
{
return true;
}
}
函数内的实现代码暂时没写,别急,我们继续往下走。
- 添加图文链接消息枚举类型
编辑文件tool_kits/ui_component/ui_kit/module/session/session_util.h
,新增CustomMsgType_Link = 5
表示图文链接消息,如下图所示
=5 可写可不写,不写默认是5,因为从1开始刚好在第5位,强列建议加上 = 5,我其他平台的图文链接消息type是5,所以为了统一消息类型 type
- 导入上面自己创建的头文件
编辑文件tool_kits/ui_component/ui_kit/gui/session/session_box.h
,在头部导入自己创建的bubble_link.h
这步完成后,项目需要重新生成下,方便在其他模块下能识别获取到你刚刚创建的 bubble_link
文件,如下图。
- 接下来在接收消息和发送消息处理的地方添加我们自定义的
bubble_link
。
编辑tool_kits/ui_component/ui_kit/gui/session/session_box.cpp
,在函数ShowMsg
添加如下代码,大约在409行
// 头部导入相关内容
#include
#include
#include
#pragma comment(lib,"urlmon.lib")
// 此处省略无关代码
MsgBubbleItem* SessionBox::ShowMsg(const nim::IMMessage &msg, bool first, bool show_time)
{
// 此处忽略部分代码
else if (msg.type_ == nim::kNIMMessageTypeCustom)
{
Json::Value json;
if (StringToJson(msg.attach_, json) && json.isObject())
{
// 此处忽略部分代码
// 添加此处 else if 代码
else if (sub_type == CustomMsgType_Link)
{
item = new MsgBubbleLink;
if (StringToJson(msg.attach_, json)
&& json.isObject()
&& json.isMember("data")
&& json["data"]["image_url"].asString() != "")
{
std::wstring image_dir = GetUserImagePath();
if (!nbase::FilePathIsExist(image_dir, true))
nbase::CreateDirectoryW(image_dir);
std::wstring image_path = image_dir + nbase::UTF8ToUTF16(msg.client_msg_id_);
// 将网络图片保存到本地
std::string url = json["data"]["image_url"].asString();
size_t len = url.length();//获取字符串长度
int nmlen = MultiByteToWideChar(CP_ACP, 0, url.c_str(), len + 1, NULL, 0);//如果函数运行成功,并且cchWideChar为零,
//返回值是接收到待转换字符串的缓冲区所需求的宽字符数大小。
wchar_t* buffer = new wchar_t[nmlen];
MultiByteToWideChar(CP_ACP, 0, url.c_str(), len + 1, buffer, nmlen);
HRESULT hr = URLDownloadToFile(NULL, buffer, image_path.c_str(), 0, NULL);
if (hr == S_OK)
{
printf("下载OK");
}
}
else
{
QLOG_ERR(L"There is not image_link property.");
}
}
}
}
}
编辑 tool_kits/ui_component/ui_kit/gui/session/msg_record.cpp
,在函数 ShowMsg
添加如下代码,大约在120行
void MsgRecordForm::ShowMsg(const nim::IMMessage &msg, bool first, bool show_time)
{
// ...
MsgBubbleItem* item = NULL;
if (msg.type_ == nim::kNIMMessageTypeText
|| IsNetCallMsg(msg.type_, msg.attach_))
{
//...
}
else if (msg.type_ == nim::kNIMMessageTypeCustom)
{
Json::Value json;
if (StringToJson(msg.attach_, json) && json.isObject())
{
//...
else if (sub_type == CustomMsgType_Rts)
{
//...
}
// 添加此处 else if 代码
else if (sub_type == CustomMsgType_Link)
{
item = new MsgBubbleLink;
}
}
}
//...
}
- 添加发送图文链接测试消息,用于开发测试(正式上线前注释掉此步骤的代码)
在发送文本消息时,如果消息是以custom::
开头的消息,就触发发送图文链接消息,如下所示。
编辑文件 tool_kits/ui_component/ui_kit/gui/session/session_box.cpp
,修改SendText
函数,添加如下代码
//...
void SessionBox::SendText( const std::string &text )
{
nim::IMMessage msg;
//...
if (msg.type_ != nim::kNIMMessageTypeRobot)
{
// 添加测试发送图文链接消息,当用户发送的文本内容是以 custom:: 开头时,触发如下业务
std::string prefix = "custom::";
if (strncmp(text.c_str(), prefix.c_str(), prefix.length()) == 0)
{
msg.type_ = nim::kNIMMessageTypeCustom;
// 消息内容体格式如下
/*
{
type: 5,
data: {
title: '消息标题',
link_url: '点击跳转链接',
image_url: '图片URL',
describe: '消息描述',
}
}
*/
Json::Value json;
Json::FastWriter writer;
json["type"] = CustomMsgType_Link;
json["data"]["title"] = Json::Value(text.substr(prefix.length()));
json["data"]["link_url"] = Json::Value("https://www.jianshu.com/u/bd57ade96e8a");
json["data"]["image_url"] = Json::Value("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2544269114,2104066965&fm=27&gp=0.jpg");
json["data"]["describe"] = Json::Value(text.substr(prefix.length()));
//msg.content_ = writer.write(json);
msg.attach_ = writer.write(json);
// 转json字符串
json_msg = msg.ToJsonString(true);
}
else
{
//...
}
}
else
{
//...
}
AddSendingMsg(msg);
nim::Talk::SendMsg(json_msg);
}
//...
点击发送后,会发现自己发送的内容是空白,那是因为我们还没做图文链接消息的显示代码,发送之后,我们在其他客户端查看消息。
- 发送图文链接消息后,接下来添加图文链接消息的显示
消息的显示都是由xml文件来控制布局的,打开文件目录bin/themes/default/session
(见下图)可以看到一些消息类型如视频消息
,文件消息
,猜拳消息
,图片消息
等都分成left
(接收消息的显示布局文件) 和right
(发送消息的显示布局文件)结尾的 xml 文件。
同样的,我们在这个目录下创建 link_left.xml
和 link_right.xml
文件,文件内容如下
link_left.xml
link_right.xml
其实两个文件内容几乎一样,只是 bkimage 和 margin 值稍微有点不一样。
接着我们在前面创建好的 bubble_link.cpp
中写业务逻辑代码控制布局文件的显示隐藏和赋值
bubble_link.cpp
#include "bubble_link.h"
#include "util/user.h"
#include
#include
#include
using namespace ui;
namespace nim_comp
{
void MsgBubbleLink::InitControl(bool bubble_right)
{
__super::InitControl(bubble_right);
msg_link_ = new ButtonBox;
if (bubble_right)
GlobalManager::FillBoxWithCache(msg_link_, L"session/link_right.xml");
else
GlobalManager::FillBoxWithCache(msg_link_, L"session/link_left.xml");
bubble_box_->Add(msg_link_);
image_box_ = (Box *)msg_link_->FindSubControl(L"image_box");
title_ = (RichEdit*)msg_link_->FindSubControl(L"title");
image_ = this->FindSubControl(L"image");
describe_ = (RichEdit*)msg_link_->FindSubControl(L"describe");
// 添加鼠标右键点击事件
msg_link_->AttachMenu(nbase::Bind(&MsgBubbleLink::OnMenu, this, std::placeholders::_1));
msg_link_->AttachClick(nbase::Bind(&MsgBubbleLink::OnClicked, this, std::placeholders::_1));
}
// 字符串转宽字符
std::wstring StringToWString(const std::string& str) {
int num = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
wchar_t *wide = new wchar_t[num];
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, wide, num);
std::wstring w_str(wide);
delete[] wide;
return w_str;
}
void MsgBubbleLink::InitInfo(const nim::IMMessage &msg)
{
__super::InitInfo(msg);
InitResPath();
Json::Value json;
if (StringToJson(msg.attach_, json) && json.isObject())
{
int sub_type = json["type"].asInt();
if (sub_type == CustomMsgType_Link && json["data"].isObject())
{
link_ = nbase::UTF8ToUTF16(json["data"]["link_url"].asString());
title_->SetText(StringToWString(json["data"]["title"].asString()));
// 是否有图片参数
bool hasImage = !json["data"]["image_url"].asString().empty();
if (hasImage)
{
image_box_->SetVisible(TRUE);
std::wstring imgeUri = path_ + nbase::UTF8ToUTF16(msg.client_msg_id_);
if (nbase::FilePathIsExist(imgeUri, false))
image_->SetBkImage(imgeUri);
else
image_->SetBkImage(L"image_def");
}
else
{
image_box_->SetVisible(FALSE);
}
if (json["data"]["describe"].asString().empty())
{
describe_->SetVisible(FALSE);
}
else
{
describe_->SetVisible(TRUE);
describe_->SetText(StringToWString(json["data"]["describe"].asString()));
UiRect rect = describe_->GetMargin();
LONG top = hasImage ? 202 : 42;
describe_->SetMargin(UiRect(rect.left, top, rect.right, rect.bottom));
}
}
}
QLOG_WAR(L"user type msg undefine, attach={0}") << msg.attach_;
}
void MsgBubbleLink::InitResPath()
{
std::wstring wpath = GetUserImagePath();
std::string path = nim::Talk::GetAttachmentPathFromMsg(msg_);
if (wpath.empty() || !nbase::FilePathIsExist(wpath, false))
{
path_ = nbase::UTF8ToUTF16(path);
std::wstring directory, filename;
nbase::FilePathApartDirectory(path_, directory);
nbase::FilePathApartFileName(path_, filename);
}
else
{
std::wstring directory, filename;
nbase::FilePathApartDirectory(nbase::UTF8ToUTF16(path), directory);
nbase::FilePathApartFileName(wpath, filename);
path_ = wpath;
}
}
bool MsgBubbleLink::OnMenu(ui::EventArgs* arg)
{
PopupMenu(false, true, false);
return false;
}
bool MsgBubbleLink::OnClicked(ui::EventArgs* arg)
{
const TCHAR szOperation[] = _T("open");
TCHAR szAddress[1024];
_tcscpy(szAddress, link_.c_str());
HINSTANCE hRslt = ShellExecute(NULL, szOperation,
szAddress, NULL, NULL, SW_SHOWNORMAL);
assert(hRslt > (HINSTANCE)HINSTANCE_ERROR);
return true;
}
}
- 上步操作完成后,我们就可以来测试发送图文链接消息了
根据我们上面的规则,发送文本消息以custom::
开头的消息就触发图文链接消息,当然也可以注释掉部分参数,下面列出我们上面在session_box.cpp
中写到的如下代码
Json::Value json;
Json::FastWriter writer;
json["type"] = CustomMsgType_Link;
// title 必须要有
json["data"]["title"] = Json::Value(text.substr(prefix.length()));
// link_url 跳转的链接,必须要有
json["data"]["link_url"] = Json::Value("https://www.jianshu.com/u/bd57ade96e8a");
// 图片链接地址,可选
json["data"]["image_url"] = Json::Value("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2544269114,2104066965&fm=27&gp=0.jpg");
// 商品描述,可选项
json["data"]["describe"] = Json::Value(text.substr(prefix.length()));
注释掉后,重新运行发送消息测试,经过测试,发送相关消息,最终显示如下。
点击消息可打开浏览器跳转相应的网页
- 最后在主界面消息列表的显示修改
首先打开文件目录 bin/lang
下的 zh_CN
目录 和 en_US
目录下的 gdstrings.ini
文件
使用编辑器打开这两个 gdstrings.ini
文件,分别添加一行
en_US/gdstrings.ini
STRID_SESSION_ITEM_MSG_TYPE_LINK = [Link]
zh_CN/gdstrings.ini
STRID_SESSION_ITEM_MSG_TYPE_LINK = [图文链接]
接着回到VS编辑器打开文件 tool_kits/ui_component/ui_kit/module/session/session_util.cpp
,在
GetCustomMsg
函数中添加如下 else if 代码块
std::wstring GetCustomMsg(const std::string &sender_accid, const std::string &msg_attach)
{
ui::MutiLanSupport* mls = ui::MutiLanSupport::GetInstance();
std::wstring show_text = mls->GetStringViaID(L"STRID_SESSION_ITEM_MSG_TYPE_CUSTOM_MSG");
Json::Value json;
if (StringToJson(msg_attach, json) && json.isObject())
{
//...
// 在最后添加如下判断,如果是 CustomMsgType_Link 类型消息
else if (sub_type == CustomMsgType_Link)
{
show_text = mls->GetStringViaID(L"STRID_SESSION_ITEM_MSG_TYPE_LINK");
}
}
return show_text;
}
完成后,我们再次运行,从消息列表可以看到。
- 上面有个bug,用户在线状态。
我另一个帐号明明是在线的,而消息列表却显示[离线]
这个地方我不管他是在线还是离线,我都返回空字符串,不显示用户在线状态,打开online_state_event_helper.cpp
在函数 GetOnlineState 中直接返回空字符串,大概在 140行,添加如下代码
std::wstring OnlineStateEventHelper::GetOnlineState(const nim::EventOnlineClientType& online_client_type, const EventMultiConfig& multi_config, bool is_simple)
{
return L""; // 在线离线状态有bug,所以直接返回空字符串
//...
}
- 替换logo图片
首先我们准备好一张图片,图片可以通过 https://www.ico.la/ 这个在线工具转成 128px * 128px 的 ico 文件,然后替换掉 nim_win_demo 目录下的 nim.ico 文件
替换掉之后,你可能看到的还是显示原来的图片,不要紧,先不管它。
在VS工具里修改 VS_VERSION_INFO 信息
修改完后保存,然后退出visual studio,接下来是打包发布过程
到此步基本完成了,最后是打包
打包过程我参考的是此文章,此文章有些步骤我是不需要操作的,有些步骤又没讲详细,所以下我也会列出自己打包是的步骤
- 首先使用管理员打开 VS 开发工具,如果不是以管理员身份打开,后面的打包步骤会失败。(这只针对Windows10权限问题)
VS自带的打包程序默认是没有安装的,如果有打包的需要,需要自己去下载一个安装程序。
如果没有安装 InstallShield Limited Edition Project,(按照 此文章 内的1,2,3步骤安装下 。
我这是已安装的
以下这个步骤,需要添加的文件和文件夹我们可以安装网易云信demo,然后打开安装目录,可以看到他依赖的所有文件和文件夹
尾篇
到此,云信PC端的扩展自定义消息已经完成。当然,这只是PC的显示正常了,其他如web,Android,iOS等客户端收到此类的消息,显示有问题,也是需要扩展调整的。此篇文章其他端的文章我会陆续更新,如果有需要的同学可以关注下。
以下附上其他版本扩展的链接
- 网易云信-新增自定义消息(iOS版)
- 网易云信-新增自定义消息(Android版)
- 网易云信-新增自定义消息(Web版)
- 网易云信-新增自定义消息(H5移动版)