网易云信-新增自定义消息(iOS版)

96
醉生夢死
0.2 2018.01.04 09:58* 字数 1092

前言

公司业务需要,PC端,移动端都用到了第三方 网易云信 IM 来实现在线客服咨询。
在这当中难免遇到一些需求是网易云信没有提供,需要自行编码进行扩展的。写此篇文章的目的正是因业务需要,需要在网易云信的基础上进行消息类型的扩展。

此篇文章里的代码是基于 网易云信 NIM_iOS_Demo_v4.5.0 版 进行修改的

如下图所示的消息类型

带图片和文字,并且可点击的消息类型,(注意收到的消息和发送的消息文本颜色不一样)

标题是iOS版,可想而知,肯定还有其他如 Android版,Web版等,不可能此类型的消息(我称它为图文消息)只支持iOS,而在Android或Web端无法显示问题。以下附上其他版本扩展的链接


正文

  1. 下载demo后,双击 NIMDemo/NIM.xcworkspace 打开项目,然后运行,确保下载下来的demo能正确运行起来。

  2. 运行没有问题后,修改以下几个文件配置,将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];
}

修改上述代码后,重新运行,即可使用自己的账号密码登录了。

  1. 添加测试发送图文链接的按钮,点击即发送图文链接消息

编辑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 协议

创建Cocoa Touch Class文件 NTESLinkAttachment,命名规则尽量遵循云信命名规则

创建完成后,添加响应的属性值 标题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:@"https://www.jianshu.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
  1. 添加显示自定义的图文消息
    上面第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等客户端收到此类的消息,显示有问题,也是需要扩展调整的。此篇文章其他端的文章我会陆续更新,如果有需要的同学可以关注下。

以下附上其他版本扩展的链接

iOS
Web note ad 1