前言
公司業務需要,PC端,移動端都用到了第三方 網易云信 IM 來實現在線客服咨詢。
在這當中難免遇到一些需求是網易云信沒有提供,需要自行編碼進行擴展的。寫此篇文章的目的正是因業務需要,需要在網易云信的基礎上進行消息類型的擴展。
此篇文章里的代碼是基于 網易云信 NIM_iOS_Demo_v4.5.0 版 進行修改的
如下圖所示的消息類型
標題是iOS版,可想而知,肯定還有其他如 Android版,Web版等,不可能此類型的消息(我稱它為圖文消息
)只支持iOS,而在Android或Web端無法顯示問題。以下附上其他版本擴展的鏈接
正文
下載demo后,雙擊 NIMDemo/NIM.xcworkspace 打開項目,然后運行,確保下載下來的demo能正確運行起來。
-
運行沒有問題后,修改以下幾個文件配置,將demo修改為自己所用。
- 修改
Classes/Util/NTESDemoConfig.m
中的_appKey,填入自己的appKey
- 修改
- (instancetype)init
{
if (self = [super init])
{
_appKey = @"填入自己的appKey";
_apiURL = @"https://app.netease.im/api";
_apnsCername = @"ENTERPRISE";
_pkCername = @"DEMO_PUSH_KIT";
_redPacketConfig = [[NTESRedPacketConfig alloc] init];
}
return self;
}
- 修改
- (NSString *)tokenByPassword
{
//demo直接使用username作為account,md5(password)作為token
//接入應用開發需要根據自己的實際情況來獲取 account和token
//return [[NIMSDK sharedSDK] isUsingDemoAppKey] ? [self MD5String] : self;
return [self MD5String];
}
修改上述代碼后,重新運行,即可使用自己的賬號密碼登錄了。
- 添加測試發送圖文鏈接的按鈕,點擊即發送圖文鏈接消息
編輯NTESCellLayoutConfig.m
文件,在init
函數中 _types 增加一條
- (instancetype)init
{
if (self = [super init])
{
_types = @[
@"NTESJanKenPonAttachment",
@"NTESSnapchatAttachment",
@"NTESChartletAttachment",
@"NTESWhiteboardAttachment",
@"NTESRedPacketAttachment",
@"NTESRedPacketTipAttachment",
// 添加圖文鏈接消息
@"NTESLinkAttachment"
];
_sessionCustomconfig = [[NTESSessionCustomContentConfig alloc] init];
_chatroomTextConfig = [[NTESChatroomTextContentConfig alloc] init];
_chatroomRobotConfig = [[NTESChatroomRobotContentConfig alloc] init];
}
return self;
}
編輯 NTESCustomAttachmentDecoder.m
文件,checkAttachment
函數中添加如下代碼
//頭部導入
#import "NTESLinkAttachment.h"
//...
- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content
{
id<NIMCustomAttachment> attachment = nil;
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
if ([dict isKindOfClass:[NSDictionary class]])
{
NSInteger type = [dict jsonInteger:CMType];
NSDictionary *data = [dict jsonDict:CMData];
switch (type) {
//...
// 添加圖文鏈接 case
case CustomMessageTypeLink:
{
attachment = [[NTESLinkAttachment alloc] init];
((NTESLinkAttachment *)attachment).title = [data jsonString:CMLinkPacketTitle];
((NTESLinkAttachment *)attachment).linkUrl = [data jsonString:CMLinkPacketLinkUrl];
((NTESLinkAttachment *)attachment).imageUrl = [data jsonString:CMLinkPacketImageUrl];
((NTESLinkAttachment *)attachment).describe = [data jsonString:CMLinkPacketDescribe];
}
break;
default:
break;
}
attachment = [self checkAttachment:attachment] ? attachment : nil;
}
}
return attachment;
}
- (BOOL)checkAttachment:(id<NIMCustomAttachment>)attachment
{
// ... 省略前面的 if else if 塊
// 添加如下代碼
else if ([attachment isKindOfClass:[NTESLinkAttachment class]])
{
check = YES;
}
return check;
}
編輯NTESSessionConfig.m
文件,在mediaItems
函數中添加如下代碼
//...
// 添加圖文鏈接測試按鈕,此處的 onTapMediaItemLinkPacket 在
NTESSessionViewController.m 中添加
NIMMediaItem *linkPacket = [NIMMediaItem item:@"onTapMediaItemLinkPacket:"
normalImage:[UIImage imageNamed:@"icon_redpacket_normal"]
selectedImage:[UIImage imageNamed:@"icon_redpacket_pressed"]
title:@"圖文鏈接"];
//...
if (isMe)
{
items = @[janKenPon,fileTrans,tip];
}
else if(_session.sessionType == NIMSessionTypeTeam)
{
// 在群組消息里添加
items = @[janKenPon,teamMeeting,fileTrans,tip,redPacket,linkPacket];
}
else
{
// 添加圖文鏈接測試按鈕
items = @[janKenPon,audioChat,videoChat,fileTrans,snapChat,whiteBoard,tip,redPacket,linkPacket];
}
在 Classes/Sections/Session/Object/Attach
目錄下創建 NTESLinkAttachment
文件,繼承 NSObject
類,實現 NIMCustomAttachment
,NTESCustomAttachmentInfo
協議
創建完成后,添加響應的屬性值 標題title
,跳轉的鏈接linkUrl
,圖片imageUrl
,描述describe
。
NTESLinkAttachment.h
文件內容如下
#import <Foundation/Foundation.h>
#import "NTESCustomAttachmentDefines.h"
@interface NTESLinkAttachment : NSObject<NIMCustomAttachment,NTESCustomAttachmentInfo>
// 標題
@property (nonatomic, copy) NSString *title;
// 點擊跳轉的鏈接地址
@property (nonatomic, copy) NSString *linkUrl;
// 圖片
@property (nonatomic, copy) NSString *imageUrl;
// 描述
@property (nonatomic, copy) NSString *describe;
@end
NTESLinkAttachment.m
文件內容如下
復制之后,會有報錯如
NTESSessionLinkContentView.h
找不到,和CMLinkPacket***
未定義等相關錯誤,先別急,后面會講到,如果看不順眼可以先注釋掉,回頭再過來放開注釋也行。(ps:本人非iOS開發,所以代碼部分不做詳細講解)
#import "NTESLinkAttachment.h"
#import "NTESSessionLinkContentView.h"
@implementation NTESLinkAttachment
- (NSString *)encodeAttachment
{
NSDictionary *dict = @{
CMType : @(CustomMessageTypeRedPacket),
CMData : @{
CMLinkPacketTitle : self.title,
CMLinkPacketLinkUrl : self.linkUrl,
CMLinkPacketImageUrl : self.imageUrl,
CMLinkPacketDescribe : self.describe
}
};
NSData *data = [NSJSONSerialization dataWithJSONObject:dict
options:0
error:nil];
NSString *content = nil;
if (data) {
content = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
}
return content;
}
- (NSString *)cellContent:(NIMMessage *)message{
return @"NTESSessionLinkContentView";
}
- (CGSize)contentSize:(NIMMessage *)message cellWidth:(CGFloat)width{
CGFloat w = 240.0f;
CGFloat h = 40.0f;
CGFloat padding = 3.0f * 3;
if (self.imageUrl != nil) {
h += 140.f;
}
if (self.describe != nil) {
UIFont *font = [UIFont systemFontOfSize:12.0];
CGFloat height = [NTESSessionLinkContentView getHeightByWidth:w - padding title:self.describe font:font];
h += height + padding;
}
return CGSizeMake(w, h);
}
- (UIEdgeInsets)contentViewInsets:(NIMMessage *)message
{
CGFloat bubblePaddingForImage = 3.f;
CGFloat bubbleArrowWidthForImage = 5.f;
if (message.isOutgoingMsg) {
return UIEdgeInsetsMake(bubblePaddingForImage,bubblePaddingForImage,bubblePaddingForImage,bubblePaddingForImage + bubbleArrowWidthForImage);
}else{
return UIEdgeInsetsMake(bubblePaddingForImage,bubblePaddingForImage + bubbleArrowWidthForImage, bubblePaddingForImage,bubblePaddingForImage);
}
}
- (BOOL)canBeRevoked
{
return YES;
}
- (BOOL)canBeForwarded
{
return YES;
}
@end
現在再來補充上面缺失的部分。
在 NTESCustomAttachmentDefines.h
文件中定義如下四個字段。打開這個文件可以看到這個里面還定義了一些其他消息需要用到的字段,所以遵循人家的游戲規則,也在此處定義。
//...省略
typedef NS_ENUM(NSInteger,NTESCustomMessageType){
CustomMessageTypeJanKenPon = 1, //剪子石頭布
CustomMessageTypeSnapchat = 2, //閱后即焚
CustomMessageTypeChartlet = 3, //貼圖表情
CustomMessageTypeWhiteboard = 4, //白板會話
// (由于我其他平臺圖文消息type是5,剛好我們業務不需要發紅包功能,這里我只好把5變成我的圖文消息,把紅包類型的消息去除)
CustomMessageTypeRedPacket = 5, //紅包消息
CustomMessageTypeRedPacketTip = 6, //紅包提示消息
};
//...省略
//紅包
#define CMRedPacketTitle @"title" //紅包標題
#define CMRedPacketContent @"content" //紅包內容
#define CMRedPacketId @"redPacketId" //紅包ID
//紅包詳情
#define CMRedPacketSendId @"sendPacketId"
#define CMRedPacketOpenId @"openPacketId"
#define CMRedPacketDone @"isGetDone"
// 添加此處四個字段用于圖文鏈接消息使用
#define CMLinkPacketTitle @"title" //標題
#define CMLinkPacketLinkUrl @"link_url" //跳轉鏈接
#define CMLinkPacketImageUrl @"image_url" //圖片鏈接
#define CMLinkPacketDescribe @"describe" //描述
//...省略
在Classes/Sections/Session/View/SessionCell/SessionContentView
目錄下創建Cocoach Touch Class
文件 NIMSessionMessageContentView
,此文件主要用來做圖文鏈接消息的顯示。
NIMSessionMessageContentView.h
文件內容如下
#import "NIMSessionMessageContentView.h"
static NSString *const NIMDemoEventNameLinkingPacket = @"NIMDemoEventNameLinkingPacket";
@interface NTESSessionLinkContentView : NIMSessionMessageContentView
// 根據寬度,字體和文本內容獲取高度
+ (CGFloat)getHeightByWidth:(CGFloat)width title:(NSString *)title font:(UIFont *)font;
@end
NIMSessionMessageContentView.m
文件內容如下
#import "NTESSessionLinkContentView.h"
#import "UIView+NTES.h"
#import "NTESLinkAttachment.h"
#import "NTESSessionUtil.h"
#import "UIImageView+WebCache.h"
CGFloat titleHeight = 40.f; // title高度
CGFloat imageHeight = 120.f;// 圖片高度
@interface NTESSessionLinkContentView()
// 圖文鏈接消息附件
@property (nonatomic,strong) NTESLinkAttachment *attachment;
@property (nonatomic,strong) UILabel *titleLabel;
@property (nonatomic,strong) UIImageView *imageView;
@property (nonatomic,strong) UILabel *describeLabel;
@end
@implementation NTESSessionLinkContentView
- (instancetype)initSessionMessageContentView{
self = [super initSessionMessageContentView];
if (self) {
self.opaque = YES;
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_describeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
}
return self;
}
- (void)refresh:(NIMMessageModel *)data
{
[super refresh:data];
NIMCustomObject *customObject = (NIMCustomObject*)data.message.messageObject;
id attach = customObject.attachment;
if ([attach isKindOfClass:[NTESLinkAttachment class]]) {
self.attachment = (NTESLinkAttachment *)attach;
self.titleLabel.text = self.attachment.title;
[self addSubview:_titleLabel];
if (self.attachment.imageUrl != nil) {
NSURL *url = [NSURL URLWithString:self.attachment.imageUrl];
// 默認圖片 default_image,記得在 Images.xcassets 中添加
[self.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"default_image"]];
[self.imageView sizeToFit];
[self addSubview:_imageView];
}
if (self.attachment.describe != nil) {
self.describeLabel.text = self.attachment.describe;
[self addSubview:_describeLabel];
}
}
}
- (void)layoutSubviews{
[super layoutSubviews];
BOOL outgoing = self.model.message.isOutgoingMsg;
UIEdgeInsets contentInsets = self.model.contentViewInsets;
CGSize contentSize = [self.model contentSize:self.superview.width];
CGFloat padding = 15;
self.titleLabel.frame = CGRectMake(padding, contentInsets.left, contentSize.width - padding, titleHeight);
self.titleLabel.font = [UIFont systemFontOfSize:14.0];
self.titleLabel.numberOfLines = 1;
// 詳情描述距離
CGFloat describeY = titleHeight;
if (self.attachment != nil && self.attachment.imageUrl != nil) {
self.imageView.frame = CGRectMake(
contentInsets.left + contentInsets.right,
titleHeight + contentInsets.top + 5,
contentSize.width - (contentInsets.left + contentInsets.right), imageHeight);
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
[self setBorderWithImageView:self.imageView top:TRUE left:FALSE bottom:TRUE right:FALSE borderColor:[UIColor lightGrayColor] borderWidth:0.3f];
describeY += imageHeight + contentInsets.top * 3 + 5 ;
}
if (self.attachment != nil && self.attachment.describe != nil) {
UIFont *font = [UIFont systemFontOfSize:12.0];
self.describeLabel.font = font;
self.describeLabel.numberOfLines = 3;
CGFloat height = [NTESSessionLinkContentView getHeightByWidth:self.describeLabel.frame.size.width title:self.attachment.describe font:font];
self.describeLabel.frame = CGRectMake(padding, describeY, contentSize.width - padding, height + padding);
}
// 發出去的消息
if (outgoing)
{
self.titleLabel.textColor = [UIColor whiteColor];
self.describeLabel.textColor = [UIColor whiteColor];
}
else
{
self.titleLabel.textColor = [UIColor blackColor];
self.describeLabel.textColor = [UIColor grayColor];
}
}
// 根據寬動態獲取高度
+ (CGFloat)getHeightByWidth:(CGFloat)width title:(NSString *)title font:(UIFont *)font
{
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, 0)];
label.text = title;
label.font = font;
label.numberOfLines = 0;
[label sizeToFit];
CGFloat height = label.frame.size.height;
return height;
}
// 設置元素邊框
-(void)setBorderWithImageView:(UIImageView *) imageView top:(BOOL)top left:(BOOL)left bottom:(BOOL)bottom right:(BOOL)right borderColor:(UIColor *)color borderWidth:(CGFloat)width
{
// 垂直內邊距
CGFloat verticalPadding = 5.0f;
if (top)
{
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, -verticalPadding, imageView.frame.size.width, width);
layer.backgroundColor = color.CGColor;
[imageView.layer addSublayer:layer];
}
if (left)
{
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, width, imageView.frame.size.height);
layer.backgroundColor = color.CGColor;
[imageView.layer addSublayer:layer];
}
if (bottom)
{
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, imageView.frame.size.height - width + verticalPadding, imageView.frame.size.width, width);
layer.backgroundColor = color.CGColor;
[imageView.layer addSublayer:layer];
}
if (right)
{
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(imageView.frame.size.width - width, 0, width, imageView.frame.size.height);
layer.backgroundColor = color.CGColor;
[imageView.layer addSublayer:layer];
}
}
- (void)onTouchUpInside:(id)sender
{
if ([self.delegate respondsToSelector:@selector(onCatchEvent:)]) {
NIMKitEvent *event = [[NIMKitEvent alloc] init];
event.eventName = NIMDemoEventNameLinkingPacket;
event.messageModel = self.model;
event.data = self;
[self.delegate onCatchEvent:event];
}
}
@end
接下來我們添加圖文按鈕的點擊事件處理。
打開文件 NTESSessionViewController.m
, 編輯函數 onTapCell
在 if else if 代碼塊后面添加如下代碼
// 頭部需導入
#import "NTESLinkAttachment.h"
#import "NTESSessionLinkContentView.h"
#import "NTESWebViewController.h"
// ...
// 添加圖文鏈接消息點擊事件
else if ([eventName isEqualToString:NIMDemoEventNameLinkingPacket]) {
NIMCustomObject *object = event.messageModel.message.messageObject;
NTESLinkAttachment *attachment = (NTESLinkAttachment *)object.attachment;
[self onOpenWebView:attachment];
handled = YES;
}
// ....
// 添加上面調用的 onOpenWebView 函數
- (void)onOpenWebView:(NTESLinkAttachment *)attachment {
// NTESWebViewController 是點擊顯示的圖文消息后要跳轉的頁面,在構造函數添加跳轉時傳入 linkUrl
NTESWebViewController *vc = [[NTESWebViewController alloc] initWithUrl:attachment.linkUrl];
// 設置title
if (attachment && attachment.title != nil) {
vc.title = attachment.title;
}
[self.navigationController pushViewController:vc animated:YES];
}
//...
#pragma mark - 圖文鏈接
- (void)onTapMediaItemLinkPacket:(NIMMediaItem *)item
{
// 此處模擬測試數據
NTESLinkAttachment *attachment = [[NTESLinkAttachment alloc] init];
[attachment setTitle:@"暖冬季歡樂送"];
[attachment setLinkUrl:@"http://www.lxweimin.com/u/bd57ade96e8a"];
[attachment setImageUrl:@"https://www.baidu.com/img/bd_logo1.png"];
[attachment setDescribe:@"家具滿1000元減100元再返100元現金券!點擊查看詳情!"];
NIMMessage *message = [NTESSessionMsgConverter msgWithLink:attachment];
[self sendMessage:message];
}
//...
在目錄 Classes/Sections/Session/ViewController
添加上面使用到的 NTESWebViewController
,用來顯示點擊后的網頁
NTESWebViewController.h
內容如下
#import <UIKit/UIKit.h>
@interface NTESWebViewController : UIViewController<UIWebViewDelegate>
{
UIWebView *webView;
}
- (instancetype)initWithUrl:(NSString *)url;
@end
NTESWebViewController.m
內容如下
#import "NTESWebViewController.h"
@interface NTESWebViewController ()<UINavigationControllerDelegate>
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
@property (nonatomic, strong) NSString *url;
@end
@implementation NTESWebViewController
- (instancetype)initWithUrl:(NSString *)url
{
self = [super init];
if (self)
{
_url = url;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// app 尺寸,去掉狀態欄
CGRect mainScreen = [UIScreen mainScreen].applicationFrame;
// 1.創建webview,并設置大小
webView = [[UIWebView alloc] initWithFrame:CGRectMake(mainScreen.origin.x, mainScreen.origin.y, mainScreen.size.width, mainScreen.size.height)];
// 2.創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url]];
// 3.加載網頁
[webView loadRequest:request];
// 4.將webview添加到界面
[self.view addSubview:webView];
[webView setDelegate:self];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
// 創建UIActivityIndicatorView背底半透明View
CGRect mainScreen = [UIScreen mainScreen].applicationFrame;
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(mainScreen.origin.x, mainScreen.origin.y, mainScreen.size.width, mainScreen.size.height)];
[view setTag:108];
[view setBackgroundColor:[UIColor whiteColor]];
[self.view addSubview:view];
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 40.0f)];
[self.activityIndicator setCenter:view.center];
[self.activityIndicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
[view addSubview:self.activityIndicator];
[self.activityIndicator startAnimating];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self.activityIndicator stopAnimating];
UIView *view = (UIView *)[self.view viewWithTag:108];
[view removeFromSuperview];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
[self.activityIndicator stopAnimating];
UIView *view = (UIView *)[self.view viewWithTag:108];
[view removeFromSuperview];
}
@end
- 添加顯示自定義的圖文消息
上面第3個步驟其實已經做了大部分自定義的圖文鏈接消息的顯示工作了,此處添加圖文鏈接消息的轉換代碼,
編輯NTESSessionMsgConverter.h
頭文件
// ...
@class NTESLinkAttachment
@interface NTESSessionMsgConverter : NSObject
// ...
// 添加鏈接消息
+ (NIMMessage *)msgWithLink:(NTESLinkAttachment *)attachment;
@end
在實現文件NTESSessionMsgConverter.m
添加以下代碼
//...
#import "NTESLinkAttachment.h"
@implementation NTESSessionMsgConverter
//...
+ (NIMMessage *)msgWithLink:(NTESLinkAttachment *)attachment
{
NIMMessage *message = [[NIMMessage alloc] init];
NIMCustomObject *customObject = [[NIMCustomObject alloc] init];
customObject.attachment = attachment;
message.messageObject = customObject;
message.apnsContent = @"發來了鏈接信息";
return message;
}
@end
5.修改消息列表中,顯示的縮略文字
編輯 NTESSessionListViewController.m
, 在contentForRecentSession
中添加一條邏輯判斷
// ...
#import "NTESLinkAttachment.h"
// ...
- (NSAttributedString *)contentForRecentSession:(NIMRecentSession *)recent{
//...
else if ([object.attachment isKindOfClass:[NTESLinkAttachment class]]) {
text = @"[圖文鏈接]";
} else {
text = @"[未知消息]";
}
//...
}
// ...
尾篇
到此,云信iOS端的擴展自定義消息已經完成。當然,這只是iOS的顯示正常了,其他如web,Android,pc等客戶端收到此類的消息,顯示有問題,也是需要擴展調整的。此篇文章其他端的文章我會陸續更新,如果有需要的同學可以關注下。
以下附上其他版本擴展的鏈接