自定义消息

TUIKit 默认实现了文本、图片、语音、视频、文件等基本消息类型的发送和展示,如果这些消息类型满足不了您的需求,您可以新增自定义消息类型。

基本消息类型

消息类型
显示效果图
文本类消息

图片类消息

语音类消息

视频类消息

文件类消息


自定义消息

如果基本消息类型不能满足您的需求,您可以根据实际业务需求自定义消息。下文以发送一条可跳转至浏览器的超文本作为自定义消息为例,帮助您快速了解实现流程。 TUIKit 内置的自定义消息样式如下图所示:

注意:
TUIKit 在 7.4.4643 版本重新设计了一套自定义消息注册机制,新旧方案变动较大,但可以支持不同的 UI 样式,建议您升级到 7.4.4643 版本。本文将以 7.4.4643 版本为例讲解。

展示自定义消息

您可以在 TUIMessageBaseDataProvideronRecvNewMessage 函数内接收自定义消息。 收到的自定义消息最终会以 Cell 的形式展示在消息列表中,Cell 绘制所需的数据我们称之为 CellData
下面我们分步骤讲解下如何展示自定义消息。

创建自定义 CellData

1. TUIChat/BaseCellData/Custom 文件夹下新建 TUILinkCellData,继承自TUIMessageCellData ,用于存储显示的文字和跳转的链接。示例代码如下:
Swift
Objective-C
public class TUILinkCellData: TUIBubbleMessageCellData {
var text: String = ""
var link: String = ""

// ...
}
@interface TUILinkCellData : TUIMessageCellData

@property NSString *text;
@property NSString *link;

@end
2. 重写父类的 getCellData: 方法。用于把 V2TIMMessage 转换成消息列表 Cell 的绘制数据 TUILinkCellData。示例代码如下:
Swift
Objective-C
public class TUILinkCellData: TUIBubbleMessageCellData {
override public class func getCellData(message: V2TIMMessage) -> TUIMessageCellData {
guard let data = message.customElem?.data,
let param = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
else {
return TUILinkCellData(direction: .incoming)
}
let cellData = TUILinkCellData(direction: message.isSelf ? .outgoing : .incoming)
cellData.msgID = message.msgID
cellData.text = param["text"] as? String ?? ""
cellData.link = param["link"] as? String ?? ""
cellData.avatarUrl = URL(string: message.faceURL ?? "")
return cellData
}
}
@implementation TUILinkCellData
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
TUILinkCellData *cellData = [[TUILinkCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
cellData.innerMessage = message;
cellData.msgID = message.msgID;
cellData.text = param[@"text"];
cellData.link = param[@"link"];
cellData.avatarUrl = [NSURL URLWithString:message.faceURL];
return cellData;
}
@end
3. 重写父类的 getDisplayString: 方法。用于把 V2TIMMessage 转换成会话列表 lastMsg 的展示文本信息。 会话列表 lastMsg 展示文本指的是当用户停留在会话列表,每个会话 cell 会显示当前会话最后一条消息。如下图所示:

示例代码如下:
Swift
Objective-C
public class TUILinkCellData: TUIBubbleMessageCellData {
override public class func getDisplayString(message: V2TIMMessage) -> String {
guard let data = message.customElem?.data,
let param = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
else {
return ""
}
return param["text"] as? String ?? ""
}
}
@implementation TUILinkCellData

+ (NSString *)getDisplayString:(V2TIMMessage *)message {
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
return param[@"text"];
}

@end

创建自定义 Cell

1. TUIChat/UI_Minimalist/Cell/Custom 文件夹下新建 TUILinkCell_Minimalist 文件,继承自 TUIBubbleMessageCell_Minimalist ,用于绘制 TUILinkCellData 数据。
示例代码如下:
Swift
Objective-C
class TUILinkCell_Minimalist: TUIBubbleMessageCell_Minimalist {
var myTextLabel: UILabel!
var myLinkLabel: UILabel!
var customData: TUILinkCellData?
override func fill(with data: TUICommonCellData) {
super.fill(with: data)
if let data = data as? TUILinkCellData {
customData = data
myTextLabel.text = data.text

setNeedsUpdateConstraints()
updateConstraintsIfNeeded()
layoutIfNeeded()
}
}
}
@interface TUILinkCell_Minimalist : TUIBubbleMessageCell_Minimalist
@property UILabel *myTextLabel; // Display text
@property UILabel *myLinkLabel; // Link redirection text
- (void)fillWithData:(TUILinkCellData *)data; // Draw UI
@end
2. 重写父类 initWithStyle:reuseIdentifier: 方法,创建 myTextLabelmyLinkLabel 文本展示对象,并添加至 container 容器。
示例代码如下:
Swift
Objective-C
class TUILinkCell_Minimalist: TUIBubbleMessageCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}

private func setupViews() {
myTextLabel = UILabel()
myTextLabel.numberOfLines = 0
myTextLabel.font = UIFont.systemFont(ofSize: 15)
myTextLabel.textAlignment = TUISwift.isRTL() ? .right : .left
myTextLabel.textColor = TUISwift.tuiChatDynamicColor("chat_link_message_title_color", defaultColor: "#000000")
container.addSubview(myTextLabel)

myLinkLabel = UILabel()
myLinkLabel.text = TUISwift.timCommonLocalizableString("TUIKitMoreLinkDetails")
myLinkLabel.font = UIFont.systemFont(ofSize: 15)
myLinkLabel.textAlignment = TUISwift.isRTL() ? .right : .left
myLinkLabel.textColor = TUISwift.tuiChatDynamicColor("chat_link_message_subtitle_color", defaultColor: "#0000FF")
container.addSubview(myLinkLabel)
}
}
@implementation TUILinkCell_Minimalist
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.myTextLabel = [[UILabel alloc] init];
[self.container addSubview:self.myTextLabel];
self.myLinkLabel = [[UILabel alloc] init];
self.myLinkLabel.text = @"View details >>";
[self.container addSubview:_myLinkLabel];
}
return self;
}
@end
3. 重写父类 fillWithData: 方法,在 TUILinkCell_Minimalist 中自定义展示 TUILinkCellData 数据。
示例代码如下:
Swift
Objective-C
class TUILinkCell_Minimalist: TUIBubbleMessageCell_Minimalist {
override func fill(with data: TUICommonCellData) {
super.fill(with: data)
if let data = data as? TUILinkCellData {
customData = data
myTextLabel.text = data.text

setNeedsUpdateConstraints()
updateConstraintsIfNeeded()
layoutIfNeeded()
}
}
}
@implementation TUILinkCell_Minimalist
// Draw the cell based on cellData
- (void)fillWithData:(TUILinkCellData *)data;
{
[super fillWithData:data];
self.myTextLabel.text = data.text;
}
@end
4. 重写父类 layoutSubviews 方法,自定义控件的布局。
示例代码如下:
Swift
Objective-C
override func layoutSubviews() {
super.layoutSubviews()
// Custimization
}
// Set the control coordinates
- (void)layoutSubviews
{
[super layoutSubviews];
self.myTextLabel.mm_top(10).mm_left(10).mm_flexToRight(10).mm_flexToBottom(50);
self.myLinkLabel.mm_sizeToFit().mm_left(10).mm_bottom(10);
}
@end
5. 重写父类的 getContentSize: 方法,用于计算 cellData 内容所占绘制区域的大小。
示例代码如下:
Swift
Objective-C
override class func getContentSize(_ data: TUIMessageCellData) -> CGSize {
guard let linkCellData = data as? TUILinkCellData else {
assertionFailure("data must be kind of TUILinkCellData")
return .zero
}

let textMaxWidth = 245
let font = UIFont.systemFont(ofSize: 15)
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font]
var size = CGSize(width: textMaxWidth, height: Int.max)

let rect = linkCellData.text.boundingRect(with: size,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: attributes,
context: nil)
size = CGSize(width: textMaxWidth + 15, height: Int(rect.size.height) + 56)
return size
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUILinkCellData.class], @"data must be kind of TUILinkCellData");
TUILinkCellData *linkCellData = (TUILinkCellData *)data;
CGFloat textMaxWidth = 245.f;
CGRect rect = [linkCellData.text boundingRectWithSize:CGSizeMake(textMaxWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]}
context:nil];
CGSize size = CGSizeMake(textMaxWidth + 15, rect.size.height + 56);
return size;
}

将您的自定义 Cell 和 CellData 注册进 TUIChat

注意:
每一种自定义消息都必须有唯一的 businessID,区分大小写,不可跟其他自定义消息的 businessID 重复。TUIChat 需要根据此 businessID 找到对应的自定义消息。
新增自定义消息的 businessID 也不能和 TUIKit 内置自定义消息的 businessID 重复。
方式一:当您采用 DevelopPods 源码集成时,并想要在组件内部直接修改需求,您可以在 TUIChat 组件内部按照以下操作直接进行修改。
在 TUIMessageCellConfig_Minimalist.m 的 registerExternalCustomMessageInfo 中注册您自己的自定义 Cell:
Swift
Objective-C
public class TUIMessageCellConfig_Minimalist: NSObject {
// ...
static func registerExternalCustomMessageInfo() {
/*
Insert your own custom message UI here, your businessID can not be same with built-in
Example:
registerCustomMessageCell(#your message cell#, messageCellData: #your message cell data#, forBusinessID: #your id#)
*/
}
}
@implementation TUIMessageCellConfig_Minimalist (CustomMessageRegister)
+ (void)registerExternalCustomMessageInfo {
// Insert your own custom message UI here, your businessID can not be same with built-in //
// Example:
[self registerCustomMessageCell:@"TUILinkCell_Minimalist" messageCellData:@"TUILinkCellData" forBusinessID:BussinessID_TextLink];
}

方式二:通过 Pod 集成 TUIChat。
在 App 初始化时,您也可以通过 TUIChatConfigregisterCustomMessage 函数里主动注册 cellcellData 信息。
示例代码如下:
Swift
Objective-C
// 自定义消息 businessID 不能重复
TUIChatConfig_Minimalist.shared.registerCustomMessage(businessID: "text_link", messageCellClassName: "TUIChat.TUILinkCell", messageCellDataClassName: "TUIChat.TUILinkCellData")

// 自定义消息 businessID(注意不能重复)
#define BussinessID_TextLink @"text_link"

/** 向 TUIChat 注册自定义消息。三个参数分别为
* @param businessID 自定义消息 businessID
* @param messagellClass 自定义消息 NSString 类型
* @param messageCellDataClassName 自定义消息 NSString 类型
*/
- (void)registerCustomMessageCell {
[TUIChatConfig.defaultConfig registerCustomMessage:BussinessID_TextLink
messageCellClassName:@"TUILinkCell"
messageCellDataClassName:@"TUILinkCellData"
];
}

发送自定义消息

如下图所示,自定义消息发送按钮主要由文本title和图片leftMark组成。您可以通过在 TUIChatDataProvidercustomInputMoreActionItemList 属性中新增 TUICustomActionSheetItem 对象来添加自定义按钮。
您可以通过设置TUICustomActionSheetItemtitleleftMark属性来自定义您想展示的文字和图片信息;如果您想调整按钮的展示顺序,可以设置 priority属性,其中priority值越大按钮越靠前;您也可以设置actionHandler来响应该按钮的点击事件,实现自己的业务逻辑。

示例代码如下:
Swift
Objective-C
class TUIChatDataProvider: TUIChatBaseDataProvider {
// ...

private func createCustomInputMoreActionItemList(model: TUIChatConversationModel) -> [TUICustomActionSheetItem] {
if self.customInputMoreActionItemList.isEmpty {
var arrayM: [TUICustomActionSheetItem] = []

let showCustom = TUIChatConfig.shared.enableWelcomeCustomMessage && model.enableWelcomeCustomMessage
if showCustom {
let link = TUICustomActionSheetItem(title: TUISwift.timCommonLocalizableString("TUIKitMoreLink"), leftMark: UIImage.safeImage(TUISwift.tuiChatImagePath_Minimalist("icon_more_custom"))) { [weak self] _ in
guard let self else { return }
let text = TUISwift.timCommonLocalizableString("TUIKitWelcome")
var homePageLink = TUITencentCloudHomePageEN
let language = TUIGlobalization.getPreferredLanguage() ?? ""
if language.contains("zh-") {
homePageLink = TUITencentCloudHomePageCN
}
do {
let param: [String: Any] = ["businessID": "text_link", "text": text, "link": homePageLink]
let data = try JSONSerialization.data(withJSONObject: param, options: [])
let message = TUIMessageDataProvider.getCustomMessageWithJsonData(data, desc: text, extensionInfo: text)
self.delegate?.dataProvider(self, sendMessage: message)
} catch {
print("[\(self)] Post Json Error")
}
}
link.priority = 100
arrayM.append(link)
}

if let items = model.customizedNewItemsInMoreMenu as? [TUICustomActionSheetItem], items.count > 0 {
arrayM.append(contentsOf: items)
}
self.customInputMoreActionItemList = arrayM
}
return self.customInputMoreActionItemList
}

// For Minimalist Edition.
func getInputMoreActionItemList(userID: String, groupID: String, conversationModel: TUIChatConversationModel, pushVC: UINavigationController?, actionController: TIMInputViewMoreActionProtocol) -> [TUICustomActionSheetItem] {
var result: [TUICustomActionSheetItem] = []
result.append(contentsOf: self.createBuiltInInputMoreActionItemList(model: conversationModel))
result.append(contentsOf: self.createCustomInputMoreActionItemList(model: conversationModel))

// ...
}
}
@implementation TUIChatDataProvider
- (NSArray<TUICustomActionSheetItem *> *)customInputMoreActionItemList {
if (_customInputMoreActionItemList == nil) {
NSMutableArray *arrayM = [NSMutableArray array];
if (TUIChatConfig.defaultConfig.enableWelcomeCustomMessage) {
__weak typeof(self) weakSelf = self;
TUICustomActionSheetItem *link =
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMoreLink)
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_custom")]
withActionHandler:^(UIAlertAction *_Nonnull action) {
link.priority = 100;
NSString *text = TIMCommonLocalizableString(TUIKitWelcome);
NSString *link = TUITencentCloudHomePageEN;
NSString *language = [TUIGlobalization tk_localizableLanguageKey];
if ([language containsString:@"zh-"]) {
link = TUITencentCloudHomePageCN;
}
NSError *error = nil;
NSDictionary *param = @{BussinessID : BussinessID_TextLink, @"text" : text, @"link" : link};
NSData *data = [NSJSONSerialization dataWithJSONObject:param options:0 error:&error];
if (error) {
NSLog(@"[%@] Post Json Error", [self class]);
return;
}
V2TIMMessage *message = [TUIMessageDataProvider getCustomMessageWithJsonData:data desc:text extension:text];
if ([weakSelf.delegate respondsToSelector:@selector(dataProvider:sendMessage:)]) {
[weakSelf.delegate dataProvider:weakSelf sendMessage:message];
}
}];
[arrayM addObject:link];
}
_customInputMoreActionItemList = [NSArray arrayWithArray:arrayM];
}
return _customInputMoreActionItemList;
}
@end