增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

@@ -0,0 +1,35 @@
//
// TUIBotBranchCell.h
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIBotBranchCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIBotBranchItemCell : UITableViewCell
@property (nonatomic, assign) BranchMsgSubType subType;
@property (nonatomic, strong) UIImageView *topLine;
@property (nonatomic, strong) UILabel *numberLabel;
@property (nonatomic, strong) UILabel *contentLabel;
@property (nonatomic, strong) UIImageView *arrowView;
@end
@interface TUIBotBranchCell : TUIBubbleMessageCell
@property (nonatomic, strong) UIImageView *headerBkView;
@property (nonatomic, strong) UIImageView *headerDotView;
@property (nonatomic, strong) UILabel *headerLabel;
@property (nonatomic, strong) UIButton *headerRefreshBtn;
@property (nonatomic, strong) UIImageView *headerRefreshView;
@property (nonatomic, strong) UITableView *itemsTableView;
- (void)fillWithData:(TUIBotBranchCellData *)data;
@property (nonatomic, strong) TUIBotBranchCellData *customData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,339 @@
//
// TUIBotBranchCell.m
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import "TUIBotBranchCell.h"
#import "TUICustomerServicePluginDataProvider.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TIMRTLUtil.h>
#import <TUICore/TUICore.h>
@implementation TUIBotBranchItemCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.contentView.backgroundColor = [UIColor clearColor];
_topLine = [[UIImageView alloc] init];
[_topLine setImage:TUICustomerServicePluginBundleThemeImage(@"bot_branch_cell_dotted_line_img", @"branch_cell_dotted_line")];
[self.contentView addSubview:_topLine];
_numberLabel = [[UILabel alloc] init];
_numberLabel.font = [UIFont systemFontOfSize:17];
_numberLabel.numberOfLines = 0;
_numberLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_numberLabel.textColor = TUICustomerServicePluginDynamicColor(@"bot_branch_cell_number_text_color", @"#006EFF");
[self.contentView addSubview:_numberLabel];
_contentLabel = [[UILabel alloc] init];
_contentLabel.font = [UIFont systemFontOfSize:14];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_contentLabel.textColor = TUICustomerServicePluginDynamicColor(@"bot_branch_cell_content_text_color", @"#333333");
[self.contentView addSubview:_contentLabel];
_arrowView = [[UIImageView alloc] init];
UIImage *arrowImage = TUICustomerServicePluginBundleThemeImage(@"bot_branch_cell_arrow_img", @"branch_cell_arrow");
[_arrowView setImage:[arrowImage rtl_imageFlippedForRightToLeftLayoutDirection]];
[self.contentView addSubview:_arrowView];
}
return self;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.topLine mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUIBotBranchCellMargin);
make.top.mas_equalTo(0);
make.width.mas_equalTo(self.mm_w - TUIBotBranchCellMargin * 2);
make.height.mas_equalTo(0.5);
}];
if (BranchMsgSubType_Welcome == self.subType) {
CGFloat height = [TUICustomerServicePluginDataProvider calcBranchCellHeightOfContent:self.contentLabel.text];
[self.numberLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.contentView.mas_centerY);
make.width.mas_equalTo(20);
make.height.mas_equalTo(20);
}];
[self.contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.numberLabel.mas_trailing).offset(TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.contentView.mas_centerY);
make.width.mas_equalTo(self.mm_w - TUIBotBranchCellMargin * 4 - kScale375(16) - 6);
make.height.mas_equalTo(height);
}];
} else {
[self.contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUIBotBranchCellMargin);
make.top.mas_equalTo(0);
make.width.mas_equalTo(self.mm_w - TUIBotBranchCellMargin * 2 - kScale375(16) - 6);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcBranchCellHeightOfContent:self.contentLabel.text]);
}];
}
[self.arrowView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentLabel.mas_trailing).offset(6);
make.centerY.mas_equalTo(self.contentView);
make.width.mas_equalTo(kScale375(16));
make.height.mas_equalTo(kScale375(16));
}];
}
@end
#define BranchItemCellMaxCountPerPage 4
@interface TUIBotBranchCell() <UITableViewDelegate, UITableViewDataSource>
@end
@implementation TUIBotBranchCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_headerBkView = [[UIImageView alloc] init];
UIImage *headerBkImage = TUICustomerServicePluginBundleThemeImage(@"bot_branch_cell_head_bk_img", @"branch_cell_head_bk");
[_headerBkView setImage:[headerBkImage rtl_imageFlippedForRightToLeftLayoutDirection]];
[self.container addSubview:_headerBkView];
_headerDotView = [[UIImageView alloc] init];
[_headerDotView setBackgroundColor:TUICustomerServicePluginDynamicColor(@"bot_branch_cell_header_dot_color", @"#FFFFFF")];
_headerDotView.layer.cornerRadius = kScale375(8) / 2;
_headerDotView.layer.masksToBounds = YES;
[self.container addSubview:_headerDotView];
_headerLabel = [[UILabel alloc] init];
_headerLabel.font = [UIFont systemFontOfSize:14];
_headerLabel.numberOfLines = 0;
_headerLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"bot_branch_cell_header_text_color_1", @"#FFFFFF");
[self.container addSubview:_headerLabel];
_headerRefreshBtn = [UIButton buttonWithType:UIButtonTypeCustom];
_headerRefreshBtn.backgroundColor = [UIColor clearColor];
[_headerRefreshBtn setTitle:TIMCommonLocalizableString(TUIChatBotChangeQuestion) forState:UIControlStateNormal];
[_headerRefreshBtn.titleLabel setFont:[UIFont systemFontOfSize:14]];
[_headerRefreshBtn setTitleColor:TUICustomerServicePluginDynamicColor(@"bot_branch_cell_refresh_btn_color", @"#006EFF") forState:UIControlStateNormal];
[_headerRefreshBtn addTarget:self action:@selector(onRefresh) forControlEvents:UIControlEventTouchUpInside];
[self.container addSubview:_headerRefreshBtn];
_headerRefreshView = [[UIImageView alloc] init];
[_headerRefreshView setImage:TUICustomerServicePluginBundleThemeImage(@"bot_branch_cell_refresh_img", @"branch_cell_refresh")];
UITapGestureRecognizer *tag = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onRefresh)];
[_headerRefreshView addGestureRecognizer:tag];
_headerRefreshView.userInteractionEnabled = YES;
[self.container addSubview:_headerRefreshView];
_itemsTableView = [[UITableView alloc] init];
_itemsTableView.tableFooterView = [[UIView alloc] init];
// _itemsTableView.backgroundColor = TUICoreDynamicColor(@"customer_service_brance_bg_color", @"#FFFFFF");
_itemsTableView.backgroundColor = [UIColor clearColor];
_itemsTableView.delegate = self;
_itemsTableView.dataSource = self;
_itemsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_itemsTableView registerClass:[TUIBotBranchItemCell class] forCellReuseIdentifier:@"item_cell"];
[self.container addSubview:_itemsTableView];
}
return self;
}
- (void)onRefresh {
if (!self.customData || 0 == self.customData.items.count) {
return;
}
NSUInteger pageCount = self.customData.items.count / BranchItemCellMaxCountPerPage;
if (self.customData.items.count % BranchItemCellMaxCountPerPage > 0) {
pageCount++;
}
NSUInteger oldPageIndex = self.customData.pageIndex;
if (self.customData.pageIndex < pageCount - 1) {
self.customData.pageIndex++;
} else {
self.customData.pageIndex = 0;
}
if (self.customData.pageIndex != oldPageIndex) {
NSUInteger location = self.customData.pageIndex * BranchItemCellMaxCountPerPage;
NSUInteger length = MIN(self.customData.items.count - location, BranchItemCellMaxCountPerPage);
self.customData.pageItems = [self.customData.items subarrayWithRange:NSMakeRange(location, length)];
[self notifyCellSizeChanged];
}
}
- (void)notifyCellSizeChanged {
NSDictionary *param = @{TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey_Message : self.customData.innerMessage};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey
object:nil
param:param];
}
- (void)fillWithData:(TUIBotBranchCellData *)data {
[super fillWithData:data];
self.customData = data;
self.headerLabel.text = data.header;
if (BranchMsgSubType_Welcome == data.subType) {
self.headerDotView.hidden = NO;
self.headerRefreshBtn.hidden = NO;
self.headerRefreshView.hidden = NO;
self.headerBkView.hidden = NO;
self.headerLabel.font = [UIFont systemFontOfSize:14];
self.headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"bot_branch_cell_header_text_color_1", @"#FFFFFF");
} else {
self.headerDotView.hidden = YES;
self.headerRefreshBtn.hidden = YES;
self.headerRefreshView.hidden = YES;
self.headerBkView.hidden = YES;
self.headerLabel.font = [UIFont boldSystemFontOfSize:14];
self.headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"bot_branch_cell_header_text_color_2", @"#000000");
}
[self.itemsTableView reloadData];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
// Override, the size of bubble content.
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIBotBranchCellData.class],
@"data must be a kind of TUIBotBranchCellData");
TUIBotBranchCellData *branchCellData = (TUIBotBranchCellData *)data;
if (!branchCellData.pageItems) {
if (BranchMsgSubType_Welcome == branchCellData.subType) {
branchCellData.pageItems = [branchCellData.items subarrayWithRange:
NSMakeRange(0, MIN(branchCellData.items.count, BranchItemCellMaxCountPerPage))];
} else {
branchCellData.pageItems = branchCellData.items;
}
}
return [TUICustomerServicePluginDataProvider calcBranchCellSize:branchCellData.header items:branchCellData.pageItems];
}
- (void)updateConstraints {
[super updateConstraints];
CGFloat cellHeight = [TUICustomerServicePluginDataProvider calcBranchCellSize:self.customData.header
items:self.customData.pageItems].height;
CGSize tableViewSize = [TUICustomerServicePluginDataProvider calcBranchCellSizeOfTableView:self.customData.pageItems];
CGSize headerLabelSize = [TUICustomerServicePluginDataProvider calcBranchCellSizeOfHeader:self.customData.header];
CGSize headerSize = CGSizeMake(self.container.mm_w, headerLabelSize.height + TUIBotBranchCellMargin + TUIBotBranchCellInnerMargin);
self.container
.mm_width(TUIBotBranchCellWidth)
.mm_height(cellHeight);
if (BranchMsgSubType_Welcome == self.customData.subType) {
[self.headerBkView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(0);
make.width.mas_equalTo(headerSize.width);
make.height.mas_equalTo(headerSize.height);
}];
[self.headerDotView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.headerBkView);
make.width.mas_equalTo(TUIBotBranchCellInnerMargin);
make.height.mas_equalTo(TUIBotBranchCellInnerMargin);
}];
[self.headerRefreshView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.container.mas_trailing).offset(-TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.headerBkView);
make.width.mas_equalTo(12);
make.height.mas_equalTo(16);
}];
[self.headerRefreshBtn sizeToFit];
[self.headerRefreshBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.headerRefreshView.mas_leading).offset(-TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.headerBkView);
make.size.mas_equalTo(self.headerRefreshBtn.frame.size);
}];
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.headerDotView.mas_trailing).offset(TUIBotBranchCellMargin);
make.trailing.mas_equalTo(self.headerRefreshBtn.mas_leading).offset(-TUIBotBranchCellMargin);
make.centerY.mas_equalTo(self.headerBkView);
make.height.mas_equalTo(headerLabelSize.height);
}];
} else {
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUIBotBranchCellMargin);
make.top.mas_equalTo(TUIBotBranchCellMargin);
make.width.mas_equalTo(headerLabelSize.width);
make.height.mas_equalTo(headerLabelSize.height);
}];
}
[self.itemsTableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(headerSize.height);
make.width.mas_equalTo(tableViewSize.width);
make.height.mas_equalTo(tableViewSize.height);
}];
}
#pragma mark - UITableViewDelegate & UITableViewDataSource
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [TUICustomerServicePluginDataProvider calcBranchCellHeightOfTableView:self.customData.pageItems row:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.pageItems.count <= indexPath.row) {
return;
}
[tableView deselectRowAtIndexPath:indexPath animated:NO];
NSString *content = self.customData.pageItems[indexPath.row];
[TUICustomerServicePluginDataProvider sendTextMessage:content];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.customData.pageItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.pageItems.count <= indexPath.row) {
return nil;
}
TUIBotBranchItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"item_cell" forIndexPath:indexPath];
cell.subType = self.customData.subType;
if (BranchMsgSubType_Welcome == self.customData.subType) {
cell.numberLabel.hidden = NO;
cell.numberLabel.text = @(indexPath.row + 1).stringValue;
} else {
cell.numberLabel.hidden = YES;
}
cell.contentLabel.text = self.customData.pageItems[indexPath.row];
// tell constraints they need updating
[cell setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[cell updateConstraintsIfNeeded];
[cell layoutIfNeeded];
return cell;
}
@end

View File

@@ -0,0 +1,28 @@
//
// TUIBotBranchCellData.h
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, BranchMsgSubType) {
BranchMsgSubType_Welcome = 0,
BranchMsgSubType_Clarify = 1,
};
@interface TUIBotBranchCellData : TUIBubbleMessageCellData
@property (nonatomic, assign) BranchMsgSubType subType;
@property (nonatomic, copy) NSString *header;
@property (nonatomic, strong) NSMutableArray *items;
@property (nonatomic, assign) NSUInteger pageIndex;
@property (nonatomic, strong) NSArray *pageItems;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,63 @@
//
// TUIBotBranchCellData.m
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import "TUIBotBranchCellData.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@implementation TUIBotBranchCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUIBotBranchCellData *cellData = [[TUIBotBranchCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSString *subType = param[@"subtype"];
if ([subType isEqualToString:@"welcome_msg"]) {
cellData.subType = BranchMsgSubType_Welcome;
} else if ([subType isEqualToString:@"clarify_msg"]) {
cellData.subType = BranchMsgSubType_Clarify;
}
NSDictionary *content = param[@"content"];
cellData.header = content[@"title"];
NSArray *items = content[@"items"];
for (NSDictionary *item in items) {
[cellData.items addObject:item[@"content"]];
}
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUIBotBranchCellData *cellData = [[TUIBotBranchCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSDictionary *content = param[@"content"];
return content[@"title"];
}
// Override
- (BOOL)canForward {
return NO;
}
- (NSMutableArray *)items {
if (!_items) {
_items = [[NSMutableArray alloc] init];
}
return _items;
}
@end

View File

@@ -0,0 +1,12 @@
// Created by lynx on 2024/3/1.
// Copyright © 2024 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIBotRichTextCellData.h"
@interface TUIBotRichTextCell : TUIBubbleMessageCell
@property TUIBotRichTextCellData *webViewData;
- (void)fillWithData:(TUIBotRichTextCellData *)data;
@end

View File

@@ -0,0 +1,254 @@
//
// TUIBotRichTextCell.m
// TUICustomerServicePlugin
//
// Created by lynx on 2024/3/1.
// Copyright © 2024 Tencent. All rights reserved.
//
#import "TUIBotRichTextCell.h"
#import <TUIChat/TUITextMessageCell.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
#import <WebKit/WebKit.h>
@interface TUIBotRichTextCell ()<WKNavigationDelegate>
@property(nonatomic, strong) WKWebView *webView;
@property(nonatomic, strong) UIColor *webViewBkColor;
@property(nonatomic, strong) UIColor *webViewTextColor;
@end
@implementation TUIBotRichTextCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self.container addSubview:self.webView];
self.webViewTextColor = [TUITextMessageCell incommingTextColor];
}
return self;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
self.webViewBkColor = nil;
[self.webView evaluateJavaScript:[NSString stringWithFormat:@"document.body.style.backgroundColor=\"%@\"",
[self hexStringFromColor:self.webViewBkColor]] completionHandler:nil];
[self.webView evaluateJavaScript:[NSString stringWithFormat:@"document.getElementById('content').style.color=\"%@\"",
[self hexStringFromColor:self.webViewTextColor]] completionHandler:nil];
}
- (WKWebView *)webView {
if (!_webView) {
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
NSString *script = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUserScript = [[WKUserScript alloc] initWithSource:script injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUserController = [[WKUserContentController alloc] init];
[wkUserController addUserScript: wkUserScript];
config.userContentController = wkUserController;
_webView = [[WKWebView alloc]initWithFrame:self.bounds configuration:config];
_webView.navigationDelegate = self;
_webView.backgroundColor = [UIColor clearColor];
[_webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
_webView.scrollView.scrollEnabled = NO;
NSString *bundlePath = TUIBundlePath(TUICustomerServicePluginBundle,TUICustomerServicePluginBundle_Key_Class);
NSString *path = [bundlePath stringByAppendingPathComponent:@"markdown.html"];
if (path) {
NSURL *url = [NSURL fileURLWithPath:path];
NSURLRequest *requset = [NSURLRequest requestWithURL:url];
[_webView loadRequest:requset];
}
}
return _webView;
}
- (UIColor *)webViewBkColor {
if (!_webViewBkColor && self.bubbleView.image) {
UIImage *image = [self.bubbleView.image.imageAsset imageWithTraitCollection:[UITraitCollection currentTraitCollection]];
_webViewBkColor = [self colorFromImage:image point:CGPointMake(trunc(10), trunc(10))];
}
return _webViewBkColor;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.webView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(10);
make.top.mas_equalTo(2);
make.width.mas_equalTo(self.container.mm_w - 10 * 2);
make.height.mas_equalTo(self.container.mm_h - 2 * 2);
}];
}
- (void)fillWithData:(TUIBotRichTextCellData *)data {
// set data
[super fillWithData:data];
self.webViewData = data;
[self loadMarkdownContent];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)loadMarkdownContent {
NSString *content = [self getMarkdownContentWithMarkdowString:self.webViewData.content];
NSString *js = [NSString stringWithFormat:@"javascript:parseMarkdown(\"%@\",true)", content];
[self.webView evaluateJavaScript:js completionHandler:nil];
// Set webview background color
[self.webView evaluateJavaScript:[NSString stringWithFormat:@"document.body.style.backgroundColor=\"%@\"",
[self hexStringFromColor:self.webViewBkColor]] completionHandler:nil];
// Set webview text color
[self.webView evaluateJavaScript:[NSString stringWithFormat:@"document.getElementById('content').style.color=\"%@\"",
[self hexStringFromColor:self.webViewTextColor]] completionHandler:nil];
// Webview disables sliding and zooming
NSString *injectionJSString = @"var script = document.createElement('meta');script.name = 'viewport';"
"script.content=\"width=device-width, user-scalable=no\";"
"document.getElementsByTagName('head')[0].appendChild(script);";
[self.webView evaluateJavaScript:injectionJSString completionHandler:nil];
}
- (NSString *)getMarkdownContentWithMarkdowString:(NSString *)markdown {
markdown = [markdown stringByReplacingOccurrencesOfString:@"\r"withString:@""];
markdown = [markdown stringByReplacingOccurrencesOfString:@"\n"withString:@"\\n"];
markdown = [markdown stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
markdown = [markdown stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
markdown = [markdown stringByReplacingOccurrencesOfString:@"<br>" withString:@"\\n"];
return markdown;
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
if ([data isKindOfClass:[TUIBotRichTextCellData class]]) {
return CGSizeMake(TRichTextMessageCell_Width_Max, [(TUIBotRichTextCellData *)data cellHeight]);
}
return CGSizeZero;
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[self loadMarkdownContent];
[self updateCellSize];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:@"contentSize"]) {
[self limitUpdateCellSize];
}
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (navigationAction.navigationType == WKNavigationTypeLinkActivated) {
if ([[UIApplication sharedApplication] canOpenURL:navigationAction.request.URL]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:nil];
}
decisionHandler(WKNavigationActionPolicyCancel);
} else {
decisionHandler (WKNavigationActionPolicyAllow);
}
}
- (void)limitUpdateCellSize {
uint64_t curTs = [[NSDate date] timeIntervalSince1970];
uint64_t interval = 1; // s
if (curTs - self.webViewData.lastUpdateTs >= interval && self.webViewData.lastUpdateTs) {
self.webViewData.lastUpdateTs = curTs;
[self updateCellSize];
} else {
if (self.webViewData.delayUpdate) {
return;
}
self.webViewData.delayUpdate = YES;
@weakify(self)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self)
[self updateCellSize];
self.webViewData.delayUpdate = NO;
});
}
}
- (void)updateCellSize {
@weakify(self);
[self.webView evaluateJavaScript:@"document.body.scrollWidth" completionHandler:^(id _Nullable result,NSError *_Nullable error) {
CGFloat scrollWidth= [result doubleValue];
[self.webView evaluateJavaScript:@"document.body.scrollHeight"completionHandler:^(id _Nullable result,NSError*_Nullable error) {
@strongify(self)
CGFloat scrollHeight = [result doubleValue];
CGFloat ratio = CGRectGetWidth(self.webView.frame) /scrollWidth;
CGFloat webHeight = scrollHeight * ratio;
if (self.webViewData.cellHeight != webHeight) {
self.webViewData.cellHeight = webHeight;
[self notifyCellSizeChanged];
}
}];
}];
}
- (void)notifyCellSizeChanged {
NSDictionary *param = @{TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey_Message : self.webViewData.innerMessage};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey
object:nil
param:param];
}
#pragma mark Util
- (UIColor *)colorFromImage:(UIImage *)image point:(CGPoint)point {
CGImageRef cgImage = image.CGImage;
NSUInteger width = image.size.width;
NSUInteger height = image.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
int bytesPerPixel = 4;
int bytesPerRow = bytesPerPixel * 1;
NSUInteger bitsPerComponent = 8;
unsigned char pixelData[4] = { 0, 0, 0, 0 };
CGContextRef context = CGBitmapContextCreate(pixelData,
1,
1,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextSetBlendMode(context, kCGBlendModeCopy);
// Draw the pixel we are interested in onto the bitmap context
CGContextTranslateCTM(context, -point.x, point.y-(CGFloat)height);
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
CGContextRelease(context);
// Convert color values [0..255] to floats [0.0..1.0]
CGFloat red = (CGFloat)pixelData[0] / 255.0f;
CGFloat green = (CGFloat)pixelData[1] / 255.0f;
CGFloat blue = (CGFloat)pixelData[2] / 255.0f;
CGFloat alpha = (CGFloat)pixelData[3] / 255.0f;
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
- (NSString *)hexStringFromColor:(UIColor *)color {
if (!color) {
return @"";
}
const CGFloat *components = CGColorGetComponents(color.CGColor);
CGFloat r = components[0];
CGFloat g = components[1];
CGFloat b = components[2];
return [NSString stringWithFormat:@"#%02lX%02lX%02lX",
lroundf(r * 255),
lroundf(g * 255),
lroundf(b * 255)];
}
@end

View File

@@ -0,0 +1,15 @@
// Created by lynx on 2024/3/1.
// Copyright © 2024 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIBotRichTextCellData : TUIBubbleMessageCellData
@property(nonatomic, strong) NSString *content;
@property(nonatomic, assign) CGFloat cellHeight;
@property(nonatomic, assign) CGFloat lastUpdateTs;
@property(nonatomic, assign) BOOL delayUpdate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,33 @@
//
// TUIBotRichTextCellData.m
// TUICustomerServicePlugin
//
// Created by lynx on 2024/3/1.
// Copyright © 2024 Tencent. All rights reserved.
//
#import "TUIBotRichTextCellData.h"
#import <TIMCommon/TIMDefine.h>
@implementation TUIBotRichTextCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUIBotRichTextCellData *cellData = [[TUIBotRichTextCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
cellData.content = param[@"content"];
cellData.reuseId = TRichTextMessageCell_ReuserId;
cellData.status = Msg_Status_Init;
cellData.cellHeight = TRichTextMessageCell_Height_Default;
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
return @"Rich Text";
}
@end

View File

@@ -0,0 +1,17 @@
//
// TUIChatBotStreamTextCell.h
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import <TUIChat/TUITextMessageCell.h>
#import "TUIBotStreamTextCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIBotStreamTextCell : TUITextMessageCell
- (void)fillWithData:(TUIBotStreamTextCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,81 @@
//
// TUIChatBotStreamTextCell.m
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import "TUIBotStreamTextCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
@implementation TUIBotStreamTextCell
- (void)fillWithData:(TUIBotStreamTextCellData *)data {
[super fillWithData:data];
// 线 Push
if (Msg_Source_OnlinePush == data.source) {
data.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
NSTimeInterval period = 0.05;
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, period * NSEC_PER_SEC);
dispatch_source_set_timer(data.timer, start, period * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(data.timer, ^{
if (data.displayedContentLength == data.contentString.length) {
[self stopTimer:data.timer];
return;
}
data.displayedContentLength++;
if (self.textView.attributedText.length > 1 &&
[self getAttributeStringRect:self.textView.attributedText].size.height >
[self getAttributeStringRect:[self.textView.attributedText attributedSubstringFromRange:
NSMakeRange(0, self.textView.attributedText.length - 1)]].size.height) {
[self stopTimer:data.timer];
[self notifyCellSizeChanged];
} else {
UIColor *textColor = self.class.incommingTextColor;
UIFont *textFont = self.class.incommingTextFont;
if (data.direction == MsgDirectionIncoming) {
textColor = self.class.incommingTextColor;
textFont = self.class.incommingTextFont;
} else {
textColor = self.class.outgoingTextColor;
textFont = self.class.outgoingTextFont;
}
self.textView.attributedText = [data getContentAttributedString:textFont];
self.textView.textColor = textColor;
[self updateCellConstraints];
}
});
dispatch_resume(data.timer);
}
}
- (CGRect)getAttributeStringRect:(NSAttributedString *)attributeString {
return [attributeString boundingRectWithSize:CGSizeMake(TTextMessageCell_Text_Width_Max, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil];
}
- (void)stopTimer:(dispatch_source_t)timer {
if (timer) {
dispatch_source_cancel(timer);
timer = nil;
}
}
- (void)notifyCellSizeChanged {
NSDictionary *param = @{TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey_Message : self.textData.innerMessage};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey
object:nil
param:param];
}
- (void)updateCellConstraints {
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIBotStreamTextCellData.h
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import <TUIChat/TUITextMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIBotStreamTextCellData : TUITextMessageCellData
@property(nonatomic, strong) dispatch_source_t timer;
@property(nonatomic, strong) UIFont *contentFont;
@property(nonatomic, strong) NSAttributedString *contentString;
@property(nonatomic, assign) NSInteger displayedContentLength;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,87 @@
//
// TUIBotStreamTextCellData.m
// TUICustomerServicePlugin
//
// Created by lynx on 2023/10/30.
//
#import "TUIBotStreamTextCellData.h"
#import <TUICore/TUICore.h>
#ifndef CGFLOAT_CEIL
#ifdef CGFLOAT_IS_DOUBLE
#define CGFLOAT_CEIL(value) ceil(value)
#else
#define CGFLOAT_CEIL(value) ceilf(value)
#endif
#endif
@implementation TUIBotStreamTextCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUIBotStreamTextCellData *cellData = [[TUIBotStreamTextCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
cellData.content = [self getDisplayString:message];
cellData.displayedContentLength = 0;
cellData.reuseId = TTextMessageCell_ReuseId;
cellData.status = Msg_Status_Init;
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return @"";
}
NSMutableString *displyString = [NSMutableString string];
NSArray *chunks = param[@"chunks"];
for (NSString *chunk in chunks) {
[displyString appendString:chunk];
}
return [displyString copy];
}
- (BOOL)customReloadCellWithNewMsg:(V2TIMMessage *)newMessage {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:newMessage.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return YES;
}
NSString *newContent = [self.class getDisplayString:newMessage];
if (newContent.length <= self.content.length) {
return YES;
}
self.innerMessage = newMessage;
self.content = [self.class getDisplayString:newMessage];
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:nil
param:@{TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data : self}];
return YES;
}
- (NSAttributedString *)getContentAttributedString:(UIFont *)textFont {
self.contentFont = textFont;
self.contentString = [super getContentAttributedString:textFont];
if (Msg_Source_OnlinePush == self.source) {
if (self.displayedContentLength < self.contentString.length) {
return [self.contentString attributedSubstringFromRange:NSMakeRange(0, self.displayedContentLength + 1)];
}
}
return self.contentString;
}
- (CGSize)getContentAttributedStringSize:(NSAttributedString *)attributeString maxTextSize:(CGSize)maxTextSize {
CGSize size = [super getContentAttributedStringSize:attributeString maxTextSize:maxTextSize];
if (size.height > CGFLOAT_CEIL(self.contentFont.lineHeight)) {
return CGSizeMake(TTextMessageCell_Text_Width_Max, size.height);
} else {
return size;
}
}
@end

View File

@@ -0,0 +1,33 @@
//
// TUICustomerServicePluginBranchCell.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUICustomerServicePluginBranchCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginBranchItemCell : UITableViewCell
@property (nonatomic, strong) UIImageView *topLine;
@property (nonatomic, strong) UILabel *contentLabel;
@property (nonatomic, strong) UIImageView *arrowView;
@end
@interface TUICustomerServicePluginBranchCell : TUIBubbleMessageCell
@property (nonatomic, strong) UILabel *headerLabel;
//@property (nonatomic, strong) NSMutableArray *itemButtons;
@property (nonatomic, strong) UITableView *itemsTableView;
- (void)fillWithData:(TUICustomerServicePluginBranchCellData *)data;
@property (nonatomic, strong) TUICustomerServicePluginBranchCellData *customData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,188 @@
//
// TUICustomerServicePluginBranchCell.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginBranchCell.h"
#import "TUICustomerServicePluginDataProvider.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
#import <TIMCommon/TIMDefine.h>
@implementation TUICustomerServicePluginBranchItemCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.contentView.backgroundColor = [UIColor clearColor];
_topLine = [[UIImageView alloc] init];
[_topLine setImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_branch_dotted_line_img", @"dotted_line")];
[self.contentView addSubview:_topLine];
_contentLabel = [[UILabel alloc] init];
_contentLabel.font = [UIFont systemFontOfSize:16];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_contentLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_branch_content_text_color", @"#368DFF");
[self.contentView addSubview:_contentLabel];
_arrowView = [[UIImageView alloc] init];
[_arrowView setImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_branch_arrow_img", @"arrow")];
[self.contentView addSubview:_arrowView];
}
return self;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.topLine mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(0);
make.width.mas_equalTo(self.mm_w - TUICustomerServicePluginBranchCellMargin * 2);
make.height.mas_equalTo(0.5);
}];
[self.contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(0);
make.width.mas_equalTo(self.mm_w - TUICustomerServicePluginBranchCellMargin * 2 - 5 - 6);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcBranchCellHeightOfContent:self.contentLabel.text]);
}];
[self.arrowView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentLabel.mas_trailing).offset(6);
make.top.mas_equalTo((self.mm_h - self.arrowView.mm_h) / 2.0);
make.width.mas_equalTo(5);
make.height.mas_equalTo(9);
}];
}
@end
@interface TUICustomerServicePluginBranchCell() <UITableViewDelegate, UITableViewDataSource>
@end
@implementation TUICustomerServicePluginBranchCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_headerLabel = [[UILabel alloc] init];
_headerLabel.font = [UIFont systemFontOfSize:16];
_headerLabel.numberOfLines = 0;
_headerLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_branch_header_text_color", @"#000000");
[self.container addSubview:_headerLabel];
_itemsTableView = [[UITableView alloc] init];
_itemsTableView.tableFooterView = [[UIView alloc] init];
// _itemsTableView.backgroundColor = TUICoreDynamicColor(@"customer_service_brance_bg_color", @"#FFFFFF");
_itemsTableView.backgroundColor = [UIColor clearColor];
_itemsTableView.delegate = self;
_itemsTableView.dataSource = self;
_itemsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_itemsTableView registerClass:[TUICustomerServicePluginBranchItemCell class] forCellReuseIdentifier:@"item_cell"];
[self.container addSubview:_itemsTableView];
}
return self;
}
- (void)fillWithData:(TUICustomerServicePluginBranchCellData *)data {
[super fillWithData:data];
self.customData = data;
self.headerLabel.text = data.header;
[self.itemsTableView reloadData];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
// Override, the size of bubble content.
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUICustomerServicePluginBranchCellData.class],
@"data must be a kind of TUICustomerServicePluginBranchCellData");
TUICustomerServicePluginBranchCellData *branchCellData = (TUICustomerServicePluginBranchCellData *)data;
return [TUICustomerServicePluginDataProvider calcBranchCellSize:branchCellData.header
items:branchCellData.items];
}
- (void)updateConstraints {
[super updateConstraints];
CGFloat cellHeight = [TUICustomerServicePluginDataProvider calcBranchCellSize:self.customData.header
items:self.customData.items].height;
CGSize tableViewSize = [TUICustomerServicePluginDataProvider calcBranchCellSizeOfTableView:self.customData.items];
CGSize headerSize= [TUICustomerServicePluginDataProvider calcBranchCellSizeOfHeader:self.customData.header];
self.container
.mm_width(TUICustomerServicePluginBranchCellWidth)
.mm_height(cellHeight);
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.width.mas_equalTo(headerSize.width);
make.height.mas_equalTo(headerSize.height);
}];
[self.itemsTableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(self.headerLabel.mas_bottom).offset(TUICustomerServicePluginBranchCellInnerMargin);
make.width.mas_equalTo(tableViewSize.width);
make.height.mas_equalTo(tableViewSize.height);
}];
}
#pragma mark - UITableViewDelegate & UITableViewDataSource
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [TUICustomerServicePluginDataProvider calcBranchCellHeightOfTableView:self.customData.items row:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.items.count <= indexPath.row) {
return;
}
if (self.customData.selected) {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
return;
}
NSString *content = self.customData.items[indexPath.row];
[TUICustomerServicePluginDataProvider sendTextMessage:content];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.customData.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.items.count <= indexPath.row) {
return nil;
}
NSString *content = self.customData.items[indexPath.row];
TUICustomerServicePluginBranchItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"item_cell" forIndexPath:indexPath];
cell.contentLabel.text = content;
// tell constraints they need updating
[cell setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[cell updateConstraintsIfNeeded];
[cell layoutIfNeeded];
return cell;
}
@end

View File

@@ -0,0 +1,22 @@
//
// TUICustomerServicePluginBranchCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginBranchCellData : TUIBubbleMessageCellData
@property (nonatomic, copy) NSString *header;
@property (nonatomic, strong) NSMutableArray *items;
@property (nonatomic, copy) NSString *selectedContent;
@property (nonatomic, assign) BOOL selected;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,52 @@
//
// TUICustomerServicePluginBranchCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginBranchCellData.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@implementation TUICustomerServicePluginBranchCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUICustomerServicePluginBranchCellData *cellData = [[TUICustomerServicePluginBranchCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSDictionary *content = param[@"content"];
cellData.header = content[@"header"];
NSArray *items = content[@"items"];
for (NSDictionary *item in items) {
[cellData.items addObject:item[@"content"]];
}
if ([content.allKeys containsObject:@"selected"]) {
NSDictionary *selected = content[@"selected"];
cellData.selectedContent = selected[@"content"];
cellData.selected = YES;
}
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
return TIMCommonLocalizableString(TUICustomerServiceBranchMessage);
}
// Override
- (BOOL)canForward {
return NO;
}
- (NSMutableArray *)items {
if (!_items) {
_items = [[NSMutableArray alloc] init];
}
return _items;
}
@end

View File

@@ -0,0 +1,25 @@
//
// TUICustomerServicePluginCardCell.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUICustomerServicePluginCardCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginCardCell : TUIBubbleMessageCell
@property (nonatomic, strong) UILabel *headerLabel;
@property (nonatomic, strong) UILabel *descLabel;
@property (nonatomic, strong) UIImageView *picView;
- (void)fillWithData:(TUICustomerServicePluginCardCellData *)data;
@property (nonatomic, strong) TUICustomerServicePluginCardCellData *customData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
//
// TUICustomerServicePluginCardCell.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginCardCell.h"
#import "TUICustomerServicePluginDataProvider.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@interface TUICustomerServicePluginCardCell()
@end
@implementation TUICustomerServicePluginCardCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_headerLabel = [[UILabel alloc] init];
_headerLabel.font = [UIFont systemFontOfSize:12];
_headerLabel.numberOfLines = 3;
_headerLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_card_header_text_color", @"#000000");
[self.container addSubview:_headerLabel];
_descLabel = [[UILabel alloc] init];
_descLabel.font = [UIFont systemFontOfSize:16];
_descLabel.numberOfLines = 1;
_descLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_descLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_card_desc_text_color", @"#FF6C2E");
[self.container addSubview:_descLabel];
_picView = [[UIImageView alloc] init];
_picView.layer.cornerRadius = 8;
_picView.layer.masksToBounds = YES;
[self.container addSubview:_picView];
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapped:)];
[self.container addGestureRecognizer:recognizer];
}
return self;
}
- (void)onTapped:(UITapGestureRecognizer *)recognizer {
NSURL *url = [NSURL URLWithString:self.customData.jumpURL ? : @""];
[TUITool openLinkWithURL:url];
}
- (void)fillWithData:(TUICustomerServicePluginCardCellData *)data {
[super fillWithData:data];
self.customData = data;
self.headerLabel.text = data.header;
self.descLabel.text = data.desc;
[self.picView sd_setImageWithURL:[NSURL URLWithString:data.picURL] placeholderImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_card_pic_placeholder_img", @"card_pic_placeholder")];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
// Override, the size of bubble content.
+ (CGSize)getContentSize:(TUICustomerServicePluginCardCellData *)data {
return CGSizeMake(TUICustomerServicePluginCardBubbleWidth, 112);
}
- (void)updateConstraints {
[super updateConstraints];
[self.picView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(14);
make.top.mas_equalTo(13);
make.width.mas_equalTo(86);
make.height.mas_equalTo(86);
}];
CGSize headerSize = [TUICustomerServicePluginDataProvider calcCardHeaderSize:self.customData.header];
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.picView.mas_trailing).offset(12);
make.top.mas_equalTo(13);
make.width.mas_equalTo(headerSize.width);
make.height.mas_equalTo(headerSize.height);
}];
[self.descLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.picView.mas_trailing).offset(12);
make.bottom.mas_equalTo(self.container.mas_bottom).mas_offset(-13);
make.width.mas_equalTo(headerSize.width);
make.height.mas_equalTo(22);
}];
}
@end

View File

@@ -0,0 +1,22 @@
//
// TUICustomerServicePluginCardCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginCardCellData : TUIBubbleMessageCellData
@property (nonatomic, copy) NSString *header;
@property (nonatomic, copy) NSString *desc;
@property (nonatomic, copy) NSString *picURL;
@property (nonatomic, copy) NSString *jumpURL;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,38 @@
//
// TUICustomerServicePluginCardCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginCardCellData.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@implementation TUICustomerServicePluginCardCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUICustomerServicePluginCardCellData *cellData = [[TUICustomerServicePluginCardCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSDictionary *content = param[@"content"];
cellData.header = content[@"header"];
cellData.desc = content[@"desc"];
cellData.picURL = content[@"pic"];
cellData.jumpURL = content[@"url"];
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
return TIMCommonLocalizableString(TUICustomerServiceCardMessage);
}
// Override
- (BOOL)canForward {
return NO;
}
@end

View File

@@ -0,0 +1,32 @@
//
// TUICustomerServicePluginCollectionCell.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUICustomerServicePluginCollectionCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginCollectionItemCell : UITableViewCell
@property (nonatomic, strong) UILabel *contentLabel;
@end
@interface TUICustomerServicePluginCollectionCell : TUIBubbleMessageCell
@property (nonatomic, strong) UILabel *headerLabel;
@property (nonatomic, strong) UITableView *itemsTableView;
@property (nonatomic, strong) UITextField *inputTextField;
@property (nonatomic, strong) UIButton *confirmButton;
- (void)fillWithData:(TUICustomerServicePluginCollectionCellData *)data;
@property (nonatomic, strong) TUICustomerServicePluginCollectionCellData *customData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,272 @@
//
// TUICustomerServicePluginCollectionCell.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginCollectionCell.h"
#import "TUICustomerServicePluginDataProvider.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
#import <TUICore/TUICore.h>
@implementation TUICustomerServicePluginCollectionItemCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.contentView.backgroundColor = [UIColor clearColor];
_contentLabel = [[UILabel alloc] init];
_contentLabel.font = [UIFont systemFontOfSize:16];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_contentLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_collection_content_text_color", @"#368DFF");
[self.contentView addSubview:_contentLabel];
}
return self;
}
- (void)updateConstraints {
[super updateConstraints];
[self.contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(0);
make.width.mas_equalTo(TUICustomerServicePluginBranchCellWidth - TUICustomerServicePluginBranchCellMargin * 2);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcCollectionCellHeightOfContent:self.contentLabel.text]);
}];
}
@end
@interface TUICustomerServicePluginCollectionCell() <UITableViewDelegate, UITableViewDataSource, TUINotificationProtocol>
@end
@implementation TUICustomerServicePluginCollectionCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self setupHeaderLabel];
[self setupListCollectionViews];
[self setupInputCollectionViews];
[TUICore registerEvent:TUICore_TUIChatNotify
subKey:TUICore_TUIChatNotify_KeyboardWillHideSubKey
object:self];
}
return self;
}
- (void)setupHeaderLabel {
_headerLabel = [[UILabel alloc] init];
_headerLabel.font = [UIFont systemFontOfSize:16];
_headerLabel.numberOfLines = 0;
_headerLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_collection_header_text_color", @"#000000");
_headerLabel.backgroundColor = [UIColor clearColor];
[self.container addSubview:_headerLabel];
}
- (void)setupListCollectionViews {
_itemsTableView = [[UITableView alloc] init];
_itemsTableView.tableFooterView = [[UIView alloc] init];
// _itemsTableView.backgroundColor = TUICoreDynamicColor(@"customer_service_brance_bg_color", @"#FFFFFF");
_itemsTableView.backgroundColor = [UIColor clearColor];
_itemsTableView.delegate = self;
_itemsTableView.dataSource = self;
_itemsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_itemsTableView registerClass:[TUICustomerServicePluginCollectionItemCell class] forCellReuseIdentifier:@"item_cell"];
_itemsTableView.hidden = YES;
[self.container addSubview:_itemsTableView];
}
- (void)setupInputCollectionViews {
_inputTextField = [[UITextField alloc] init];
_inputTextField.backgroundColor = TUICustomerServicePluginDynamicColor(@"customer_service_collection_textfield_bg_color", @"#FFFFFF");
_inputTextField.hidden = YES;
_inputTextField.font = [UIFont systemFontOfSize:16];
[self.container addSubview:_inputTextField];
UIView *blankView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 6, 20)];
blankView.backgroundColor = [UIColor clearColor];
_inputTextField.leftViewMode = UITextFieldViewModeAlways;
_inputTextField.leftView = blankView;
_confirmButton = [UIButton new];
_confirmButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_confirmButton.titleLabel.font = [UIFont systemFontOfSize:16];
[_confirmButton setTitleColor:TUICustomerServicePluginDynamicColor(@"customer_service_collection_textfield_text_color", @"#000000")
forState:UIControlStateNormal];
[_confirmButton setImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_collection_submit_img", @"submit") forState:UIControlStateNormal];
[_confirmButton addTarget:self action:@selector(onConfirmButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
_confirmButton.hidden = YES;
[self.container addSubview:_confirmButton];
}
- (void)hideListCollectionViews {
self.itemsTableView.hidden = YES;
self.inputTextField.hidden = NO;
self.confirmButton.hidden = NO;
}
- (void)hideInputCollectionViews {
self.itemsTableView.hidden = NO;
self.inputTextField.hidden = YES;
self.confirmButton.hidden = YES;
}
- (void)onConfirmButtonClicked:(UIButton *)sender {
self.inputTextField.userInteractionEnabled = NO;
NSString *content = self.inputTextField.text;
[TUICustomerServicePluginDataProvider sendTextMessage:content];
}
- (void)fillWithData:(TUICustomerServicePluginCollectionCellData *)data {
[super fillWithData:data];
self.customData = data;
self.headerLabel.text = data.header;
if (data.type == 1) {
[self hideInputCollectionViews];
[self.itemsTableView reloadData];
} else {
[self hideListCollectionViews];
self.inputTextField.text = self.customData.selectedContent;
BOOL canFill = self.customData.selectedContent.length == 0;
self.inputTextField.enabled = canFill;
self.confirmButton.enabled = canFill;
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
// Override, the size of bubble content.
+ (CGSize)getContentSize:(TUICustomerServicePluginCollectionCellData *)data {
if (data.type == 1) {
return [TUICustomerServicePluginDataProvider calcCollectionCellSize:data.header items:data.items];
} else {
return [TUICustomerServicePluginDataProvider calcCollectionInputCellSize:data.header];
}
}
- (void)updateConstraints {
[super updateConstraints];
if (self.customData.type == 1) {
CGFloat cellHeight = [TUICustomerServicePluginDataProvider calcBranchCellSize:self.customData.header
items:self.customData.items].height;
CGSize tableViewSize = [TUICustomerServicePluginDataProvider calcCollectionCellSizeOfTableView:self.customData.items];
CGSize headerSize= [TUICustomerServicePluginDataProvider calcCollectionCellSizeOfHeader:self.customData.header];
self.container
.mm_width(TUICustomerServicePluginBranchCellWidth)
.mm_height(cellHeight);
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.width.mas_equalTo(headerSize.width);
make.height.mas_equalTo(headerSize.height);
}];
[self.itemsTableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(self.headerLabel.mas_bottom).offset(TUICustomerServicePluginBranchCellInnerMargin);
make.width.mas_equalTo(tableViewSize.width);
make.height.mas_equalTo(tableViewSize.height);
}];
} else {
self.container
.mm_width(TUICustomerServicePluginInputCellWidth)
.mm_height([TUICustomerServicePluginDataProvider calcCollectionInputCellSize:self.customData.header].height);
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.width.mas_equalTo([TUICustomerServicePluginDataProvider calcCollectionInputCellSizeOfHeader:self.customData.header].width);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcCollectionInputCellSizeOfHeader:self.customData.header].height);
}];
[self.inputTextField mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(TUICustomerServicePluginBranchCellMargin);
make.top.mas_equalTo(self.headerLabel.mas_bottom).offset(6);
make.width.mas_equalTo(TUICustomerServicePluginInputCellWidth - TUICustomerServicePluginBranchCellMargin * 2 - 40);
make.height.mas_equalTo(36);
}];
[self setupInputTextFieldCorners];
[self.confirmButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.inputTextField.mas_trailing);
make.top.mas_equalTo(self.headerLabel.mas_bottom).offset(6);
make.width.mas_equalTo(40);
make.height.mas_equalTo(36);
}];
}
}
- (void)setupInputTextFieldCorners {
UIRectCorner corners = UIRectCornerTopLeft | UIRectCornerBottomLeft;
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:self.inputTextField.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(8, 8)];
CAShapeLayer *layer = [[CAShapeLayer alloc] init];
layer.path = bezierPath.CGPath;
self.inputTextField.layer.mask = layer;
}
#pragma mark - UITableViewDelegate & UITableViewDataSource
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [TUICustomerServicePluginDataProvider calcCollectionCellHeightOfTableView:self.customData.items row:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.items.count <= indexPath.row) {
return;
}
NSString *content = self.customData.items[indexPath.row];
[TUICustomerServicePluginDataProvider sendTextMessage:content];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.customData.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.customData.items.count <= indexPath.row) {
return nil;
}
NSString *content = self.customData.items[indexPath.row];
TUICustomerServicePluginCollectionItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"item_cell" forIndexPath:indexPath];
cell.contentLabel.text = content;
// tell constraints they need updating
[cell setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[cell updateConstraintsIfNeeded];
[cell layoutIfNeeded];
return cell;
}
#pragma mark - TUINotificationProtocol
- (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(nullable id)anObject param:(nullable NSDictionary *)param {
if ([key isEqualToString:TUICore_TUIChatNotify] &&
[subKey isEqualToString:TUICore_TUIChatNotify_KeyboardWillHideSubKey]) {
[self.inputTextField resignFirstResponder];
}
}
@end

View File

@@ -0,0 +1,22 @@
//
// TUICustomerServicePluginCollectionCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginCollectionCellData : TUIBubbleMessageCellData
@property (nonatomic, copy) NSString *header;
@property (nonatomic, strong) NSMutableArray *items;
@property (nonatomic, copy) NSString *selectedContent;
@property (nonatomic, assign) NSInteger type;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,50 @@
//
// TUICustomerServicePluginCollectionCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginCollectionCellData.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@implementation TUICustomerServicePluginCollectionCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUICustomerServicePluginCollectionCellData *cellData = [[TUICustomerServicePluginCollectionCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSDictionary *content = param[@"content"];
cellData.header = content[@"header"];
NSArray *items = content[@"items"];
for (NSDictionary *item in items) {
[cellData.items addObject:item[@"content"]];
}
cellData.type = [content[@"type"] integerValue];
NSDictionary *selected = content[@"selected"];
cellData.selectedContent = selected[@"content"];
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
return TIMCommonLocalizableString(TUICustomerServiceCollectInfomation);
}
// Override
- (BOOL)canForward {
return NO;
}
- (NSMutableArray *)items {
if (!_items) {
_items = [[NSMutableArray alloc] init];
}
return _items;
}
@end

View File

@@ -0,0 +1,33 @@
//
// TUICustomerServicePluginEvaluationCell.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import "TUICustomerServicePluginEvaluationCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginEvaluationCell : TUIMessageCell
@property (nonatomic, strong) UIView *backView;
@property (nonatomic, strong) UILabel *topLabel;
@property (nonatomic, strong) UILabel *bottomLabel;
@property (nonatomic, strong) UILabel *headerLabel;
@property (nonatomic, strong) UIButton *submitButton;
@property (nonatomic, strong) NSMutableArray *scoreButtonArray;
@property (nonatomic, strong) TUICustomerServicePluginEvaluationCellData *customData;
- (void)fillWithData:(TUICustomerServicePluginEvaluationCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,314 @@
//
// TUICustomerServicePluginEvaluationCell.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginEvaluationCell.h"
#import "TUICustomerServicePluginDataProvider.h"
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
#import <TUICore/TUICore.h>
@interface TUICustomerServicePluginEvaluationCell()
@property (nonatomic, strong) UIButton *selectedButton;
@end
@implementation TUICustomerServicePluginEvaluationCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_backView = [UIView new];
_backView.backgroundColor = TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_bg_color", @"#FBFBFB");
_backView.layer.cornerRadius = 10;
_backView.layer.masksToBounds = YES;
[self.container addSubview:_backView];
_topLabel = [[UILabel alloc] init];
_topLabel.font = [UIFont systemFontOfSize:12];
_topLabel.numberOfLines = 1;
_topLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_topLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_top_text_color", @"#9A9A9A");
_topLabel.text = @"感谢您使用我们的服务,请对此次服务进行评价!";
_topLabel.textAlignment = NSTextAlignmentCenter;
[self.container addSubview:_topLabel];
_headerLabel = [[UILabel alloc] init];
_headerLabel.font = [UIFont systemFontOfSize:12];
_headerLabel.numberOfLines = 0;
_headerLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_headerLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_header_text_color", @"#1C1C1C");
[self.backView addSubview:_headerLabel];
_submitButton = [UIButton new];
[_submitButton setTitle:TIMCommonLocalizableString(TUICustomerServiceSubmitEvaluation) forState:UIControlStateNormal];
_submitButton.layer.cornerRadius = 4;
_submitButton.layer.masksToBounds = YES;
_submitButton.titleLabel.font = [UIFont systemFontOfSize:12];
[_submitButton setTitleColor:TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_button_text_color", @"#FFFFFF")
forState:UIControlStateNormal];
[_submitButton addTarget:self action:@selector(onSubmitButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
_submitButton.backgroundColor = TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_button_bg_color", @"#2F80ED");
[self.backView addSubview:_submitButton];
_bottomLabel = [[UILabel alloc] init];
_bottomLabel.font = [UIFont systemFontOfSize:12];
_bottomLabel.numberOfLines = 0;
_bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_bottomLabel.textColor = TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_bottom_text_color", @"#9A9A9A");
_bottomLabel.hidden = YES;
_bottomLabel.textAlignment = NSTextAlignmentCenter;
[self.container addSubview:_bottomLabel];
}
return self;
}
- (void)onSubmitButtonClicked:(UIButton *)sender {
NSString *itemID = [NSString stringWithFormat:@"%ld", (long)self.selectedButton.tag];
NSDictionary *dict = @{@"menuSelected": @{@"id": itemID,
@"content": self.customData.itemDict[itemID] ? : @"",
@"sessionId": self.customData.sessionID ? : @""},
@"src": BussinessID_Src_CustomerService_EvaluationSelected};
NSData *data = [TUITool dictionary2JsonData:dict];
[TUICustomerServicePluginDataProvider sendCustomMessageWithoutUpdateUI:data];
}
- (void)onScoreButtonClicked:(UIButton *)sender {
if (self.customData.isExpired) {
return;
}
self.selectedButton = sender;
for (int i = 0; i < self.scoreButtonArray.count; i++) {
UIButton *button = self.scoreButtonArray[i];
if (button.tag <= sender.tag) {
[self updateScoreButton:button index:i selected:YES];
} else {
[self updateScoreButton:button index:i selected:NO];
}
}
// The submit button can only be clicked after selecting
self.submitButton.enabled = YES;
self.submitButton.alpha = 1.0;
}
- (void)notifyCellSizeChanged {
NSDictionary *param = @{TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey_Message : self.customData.innerMessage};
[TUICore notifyEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_PluginViewSizeChangedSubKey
object:nil
param:param];
}
- (void)prepareForReuse {
[super prepareForReuse];
for (UIButton *button in self.scoreButtonArray) {
[button removeFromSuperview];
}
[self.scoreButtonArray removeAllObjects];
}
- (void)fillWithData:(TUICustomerServicePluginEvaluationCellData *)data {
[super fillWithData:data];
BOOL isCellDataChanged = [self isCellDataChanged:self.customData newData:data];
self.customData = data;
self.headerLabel.text = data.header;
self.bottomLabel.text = data.tail;
if (data.isSelected) {
for (int i = 0; i < data.totalScore; i++) {
NSDictionary *item = data.items[i];
UIButton *scoreButton = [[UIButton alloc] init];
scoreButton.tag = [item[@"id"] integerValue];
scoreButton.enabled = NO;
scoreButton.alpha = 0.8;
scoreButton.titleLabel.font = [UIFont systemFontOfSize:12];
if (i <= data.score) {
[self updateScoreButton:scoreButton index:i selected:YES];
} else {
[self updateScoreButton:scoreButton index:i selected:NO];
}
[self.backView addSubview:scoreButton];
[self.scoreButtonArray addObject:scoreButton];
}
self.submitButton.enabled = NO;
self.submitButton.alpha = 0.8;
self.bottomLabel.hidden = NO;
if (isCellDataChanged) {
[self notifyCellSizeChanged];
}
} else {
for (int i = 0; i < data.totalScore; i++) {
NSDictionary *item = data.items[i];
UIButton *scoreButton = [[UIButton alloc] init];
[scoreButton addTarget:self
action:@selector(onScoreButtonClicked:)
forControlEvents:UIControlEventTouchUpInside];
scoreButton.tag = [item[@"id"] integerValue];
scoreButton.enabled = !self.customData.isExpired;
scoreButton.alpha = 1.0;
scoreButton.titleLabel.font = [UIFont systemFontOfSize:12];
[self updateScoreButton:scoreButton index:i selected:NO];
[self.backView addSubview:scoreButton];
[self.scoreButtonArray addObject:scoreButton];
}
self.submitButton.enabled = NO;
self.submitButton.alpha = 0.8;
self.bottomLabel.hidden = YES;
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)updateScoreButton:(UIButton *)button index:(int)i selected:(BOOL)selected {
if (self.customData.type == 1) {
// star
if (selected) {
[button setBackgroundImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_evaluation_star_selected_img", @"star_selected") forState:UIControlStateNormal];
} else {
[button setBackgroundImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_evaluation_star_unselected_img", @"star_unselected") forState:UIControlStateNormal];
}
} else if (self.customData.type == 2) {
// number
if (selected) {
[button setBackgroundImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_evaluation_number_selected_img", @"number_selected") forState:UIControlStateNormal];
[button setTitle:[NSString stringWithFormat:@"%d", i + 1] forState:UIControlStateNormal];
[button setTitleColor:TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_button_selected_bg_color", @"#FFFFFF") forState:UIControlStateNormal];
} else {
[button setBackgroundImage:TUICustomerServicePluginBundleThemeImage(@"customer_service_evaluation_number_unselected_img", @"number_unselected") forState:UIControlStateNormal];
[button setTitle:[NSString stringWithFormat:@"%d", i + 1] forState:UIControlStateNormal];
[button setTitleColor:TUICustomerServicePluginDynamicColor(@"customer_service_evaluation_button_unselected_bg_color", @"#006EFF") forState:UIControlStateNormal];
}
}
}
// Override, the height of the cell
+ (CGFloat)getHeight:(TUICustomerServicePluginEvaluationCellData *)data withWidth:(CGFloat)width {
return [self getContentSize:data].height;
}
// Override, the size of bubble content.
+ (CGSize)getContentSize:(TUICustomerServicePluginEvaluationCellData *)data {
return [TUICustomerServicePluginDataProvider calcEvaluationCellSize:data.header
tail:data.tail
score:data.totalScore
selected:data.isSelected];
}
- (void)updateConstraints {
[super updateConstraints];
self.container.mm_fill();
[self.topLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self);
make.top.mas_equalTo(0);
make.width.mas_equalTo(270);
make.height.mas_equalTo(20);
}];
[self.backView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self);
make.top.mas_equalTo(self.topLabel.mas_bottom).offset(20);
make.width.mas_equalTo(TUICustomerServicePluginEvaluationBubbleWidth);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcEvaluationBubbleSize:self.customData.header
score:self.customData.totalScore].height);
}];
[self.headerLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.backView);
make.top.mas_equalTo(20);
make.width.mas_equalTo([TUICustomerServicePluginDataProvider calcEvaluationBubbleHeaderSize:self.customData.header].width);
make.height.mas_equalTo([TUICustomerServicePluginDataProvider calcEvaluationBubbleHeaderSize:self.customData.header].height);
}];
UIImageView *leftView = nil;
UIImageView *upView = nil;
float leftMargin = (self.backView.mm_w - 24 * 5 - 15 * 4) / 2.0;
for (int i = 0; i < self.scoreButtonArray.count; i++) {
UIImageView *scoreView = self.scoreButtonArray[i];
if (i < 5) {
// First row.
[scoreView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (leftView == nil) {
make.leading.mas_equalTo(leftMargin);
} else {
make.leading.mas_equalTo(leftView.mas_trailing).offset(15);
}
make.top.mas_equalTo(self.headerLabel.mas_bottom).offset(12);
make.width.mas_equalTo(24);
make.height.mas_equalTo(24);
}];
leftView = scoreView;
upView = scoreView;
} else {
// Second row if exist.
if (i == 5) {
leftView = nil;
}
[scoreView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (leftView == nil) {
make.leading.mas_equalTo(leftMargin);
} else {
make.leading.mas_equalTo(leftView.mas_trailing).offset(15);
}
make.top.mas_equalTo(upView.mas_bottom).offset(12);
make.width.mas_equalTo(24);
make.height.mas_equalTo(24);
}];
leftView = scoreView;
}
}
UIImageView *scoreView = self.scoreButtonArray.count <= 5 ? self.scoreButtonArray.firstObject : self.scoreButtonArray.lastObject;
if (scoreView) {
[self.submitButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.backView);
make.top.mas_equalTo(scoreView.mas_bottom).offset(12);
make.width.mas_equalTo(140);
make.height.mas_equalTo(30);
}];
}
if (self.customData.isSelected) {
CGSize tailSize = [TUICustomerServicePluginDataProvider calcEvaluationBubbleTailSize:self.customData.tail];
[self.bottomLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self);
make.top.mas_equalTo(self.backView.mas_bottom).offset(20);
make.width.mas_equalTo(tailSize.width);
make.height.mas_equalTo(tailSize.height);
}];
}
}
- (NSMutableArray *)scoreButtonArray {
if (!_scoreButtonArray) {
_scoreButtonArray = [[NSMutableArray alloc] init];
}
return _scoreButtonArray;
}
- (BOOL)isCellDataChanged:(TUICustomerServicePluginEvaluationCellData *)oldData
newData:(TUICustomerServicePluginEvaluationCellData *)newData {
if (oldData && newData && oldData.score == newData.score && oldData.isSelected == newData.isSelected) {
return NO;
}
return YES;
}
@end

View File

@@ -0,0 +1,32 @@
//
// TUICustomerServicePluginEvaluationCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginEvaluationCellData : TUIMessageCellData
@property (nonatomic, copy) NSString *header;
@property (nonatomic, copy) NSString *tail;
@property (nonatomic, assign) NSInteger totalScore;
@property (nonatomic, assign) NSInteger score;
@property (nonatomic, assign) NSInteger type; // 1:star, 2:number
@property (nonatomic, assign) BOOL isSelected;
@property (nonatomic, copy) NSString *sessionID;
@property (nonatomic, assign) NSTimeInterval expireTime;
@property (nonatomic, assign) BOOL isExpired;
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, strong) NSMutableDictionary *itemDict;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,85 @@
//
// TUICustomerServicePluginEvaluationCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginEvaluationCellData.h"
#import <TIMCommon/TUIMessageCellLayout.h>
#import "TUICustomerServicePluginDataProvider+CalculateSize.h"
@implementation TUICustomerServicePluginEvaluationCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUICustomerServicePluginEvaluationCellData *cellData = [[TUICustomerServicePluginEvaluationCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
NSDictionary *content = param[@"menuContent"];
cellData.type = [content[@"type"] integerValue];
cellData.header = content[@"head"];
cellData.tail = content[@"tail"];
if ([content.allKeys containsObject:@"selected"]) {
NSDictionary *selected = content[@"selected"];
cellData.isSelected = selected.count > 0;
NSInteger selectedID = [selected[@"id"] integerValue];
cellData.score = selectedID < 100 ? selectedID : selectedID - 100;
} else {
cellData.isSelected = NO;
}
cellData.sessionID = content[@"sessionId"];
cellData.expireTime = [content[@"expireTime"] unsignedIntegerValue];
NSArray *items = content[@"menu"];
cellData.totalScore = items.count;
cellData.items = items;
for (NSDictionary *item in items) {
[cellData.itemDict setValue:item[@"content"] forKey:item[@"id"]];
}
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
return TIMCommonLocalizableString(TUICustomerServiceSatisfactionEvaluation);
}
- (instancetype)initWithDirection:(TMsgDirection)direction
{
self = [super initWithDirection:direction];
if (self) {
self.showAvatar = NO;
}
return self;
}
// Override
- (BOOL)canForward {
return NO;
}
- (BOOL)canLongPress {
return NO;
}
- (NSMutableDictionary *)itemDict {
if (!_itemDict) {
_itemDict = [[NSMutableDictionary alloc] init];
}
return _itemDict;
}
- (BOOL)isExpired {
NSTimeInterval cur = [[NSDate date] timeIntervalSince1970];
return cur > self.expireTime;
}
@end

View File

@@ -0,0 +1,16 @@
//
// TUICustomerServicePluginInvisibleCell.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginInvisibleCell : TUIBubbleMessageCell
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,19 @@
//
// TUICustomerServicePluginInvisibleCell.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginInvisibleCell.h"
@implementation TUICustomerServicePluginInvisibleCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
}
return self;
}
@end

View File

@@ -0,0 +1,18 @@
//
// TUICustomerServicePluginInvisibleCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIBubbleMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginInvisibleCellData : TUIBubbleMessageCellData
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,51 @@
//
// TUICustomerServicePluginInvisibleCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/5/30.
//
#import "TUICustomerServicePluginInvisibleCellData.h"
#import "TUICustomerServicePluginPrivateConfig.h"
@implementation TUICustomerServicePluginInvisibleCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
TUICustomerServicePluginInvisibleCellData *cellData = [[TUICustomerServicePluginInvisibleCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
if ([param[@"src"] isEqualToString: BussinessID_Src_CustomerService_EvaluationRule]) {
NSDictionary *content = param[@"content"];
NSInteger menuSendRuleFlag = [content[@"menuSendRuleFlag"] integerValue];
[TUICustomerServicePluginPrivateConfig sharedInstance].canEvaluate = menuSendRuleFlag >> 2;
}
return cellData;
}
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data
options:NSJSONReadingAllowFragments error:nil];
if (param == nil) {
return nil;
}
if ([param[@"src"] isEqualToString: BussinessID_Src_CustomerService_Timeout]) {
return TIMCommonLocalizableString(TUICustomerServiceTimeout);
} else if ([param[@"src"] isEqualToString: BussinessID_Src_CustomerService_End]) {
return TIMCommonLocalizableString(TUICustomerServiceEnd);
}
return nil;
}
// Override
- (BOOL)shouldHide {
return YES;
}
@end

View File

@@ -0,0 +1,17 @@
//
// TUICustomerServicePluginTypingCellData.h
// TUICustomerServicePlugin
//
// Created by xia on 2023/6/16.
//
#import <TUIChat/TUIChat.h>
#import <TUIChat/TUITypingStatusCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomerServicePluginTypingCellData : TUITypingStatusCellData
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,24 @@
//
// TUICustomerServicePluginTypingCellData.m
// TUICustomerServicePlugin
//
// Created by xia on 2023/6/16.
//
#import "TUICustomerServicePluginTypingCellData.h"
@implementation TUICustomerServicePluginTypingCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
TUITypingStatusCellData *cellData = [[TUITypingStatusCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.msgID = message.msgID;
if ([param[@"src"] isEqualToString: BussinessID_Src_CustomerService_Typing]) {
cellData.typingStatus = 1;
}
return cellData;
}
@end