This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
*
* This document declares the modules and components used to implement the message unit.
* The message unit (TUIMessageCell) is a general term for the bubble message/picture message/emoticon message/video message displayed in the chat view.
* The above messages are implemented by inheriting from this class or a subclass of this class. If you want to customize the message, you also need to
* implement it by inheriting from this class or a subclass of this class. The XXXX message unit (TUIXXXXMessageCell) is mainly responsible for displaying on
* the page and responding to user interaction events. For data processing and acquisition in the message unit, please refer to
* TUIChat\CellData\TUIXXXXMessageCellData.h according to the specific message unit
*
* The interaction callbacks provided by the TUIMessageCellDelegate protocol include: long press, resend, click on the message, click on the avatar, etc.
* The TUIMessageCell class stores message-related information, such as the sender's avatar, sender's nickname, and message content (supports various formats
* such as text, pictures, and videos). At the same time, TUIMessageeCell, as a parent class, provides basic properties and behavior templates for subclass
* messages.
*/
#import <UIKit/UIKit.h>
#import "TUIFitButton.h"
#import "TUIMessageCellData.h"
#import "TUISecurityStrikeView.h"
@class TUIMessageCell;
@protocol TUIMessageCellProtocol <NSObject>
@required
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width;
+ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data;
+ (CGSize)getContentSize:(TUIMessageCellData *)data;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMessageCellDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIMessageCellDelegate <NSObject>
/**
* Callback for long press message
* You can use this callback to implement secondary operations such as delete and recall (when the sender of the message long-presses his own message) on top
* of the long-pressed message.
*/
- (void)onLongPressMessage:(TUIMessageCell *)cell;
/**
* Callback for clicking retryView
* You can use this callback to implement: resend the message.
*/
- (void)onRetryMessage:(TUIMessageCell *)cell;
/**
* Callback for clicking message cell
* Usually:
* - Clicking on the sound message means playing voice
* - Clicking on the file message means opening the file
* - Clicking on the picture message means showing the larger image
* - Clicking on the video message means playing the video.
* Usually, it only provides a reference for the function implementation, and you can implement the delegate function according to your needs.
*/
- (void)onSelectMessage:(TUIMessageCell *)cell;
/**
* Callback for clicking avatar view of the messageCell
* You can use this callback to implement: in response to the user's click, jump to the detailed information interface of the corresponding user.
*/
- (void)onSelectMessageAvatar:(TUIMessageCell *)cell;
/**
* Callback for long pressing avatar view of messageCell
*/
- (void)onLongSelectMessageAvatar:(TUIMessageCell *)cell;
/**
* Callback for clicking read receipt label
*/
- (void)onSelectReadReceipt:(TUIMessageCellData *)cell;
/**
* Clicking the x-person reply button to jump to the multi-person reply details page
*/
- (void)onJumpToRepliesDetailPage:(TUIMessageCellData *)data;
- (void)onJumpToMessageInfoPage:(TUIMessageCellData *)data selectCell:(TUIMessageCell *)cell;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMessageCell
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIMessageCell : TUICommonTableViewCell <TUIMessageCellProtocol>
/**
* Icon that identifies the message selected
* In the multi-selection scenario, it is used to identify whether the message is selected
*/
@property(nonatomic, strong) UIImageView *selectedIcon;
/**
* Message selection view
* When multiple selection is activated, the view will be overlaid on this cell, and clicking on the view will trigger the check/uncheck of the message
*/
@property(nonatomic, strong) UIButton *selectedView;
/**
*
* The icon view of displays user's avatar
*/
@property(nonatomic, strong) UIImageView *avatarView;
/**
*
* The label of displays user's displayname
*/
@property(nonatomic, strong) UILabel *nameLabel;
/**
* Container view
* It wraps various views of MesageCell as the "bottom" of MessageCell, which is convenient for view management and layout.
*/
@property(nonatomic, strong) UIView *container;
/**
* Activity indicator
* A circling icon is provided while the message is being sent to indicate that the message is being sent.
*/
@property(nonatomic, strong) UIActivityIndicatorView *indicator;
/**
* Retry view, displayed after sending failed, click on this view to trigger onRetryMessage: callback.
*/
@property(nonatomic, strong) UIImageView *retryView;
/**
* security Strike View
*/
@property (nonatomic, strong) TUISecurityStrikeView * securityStrikeView;
/**
* Message reply details button
*/
@property(nonatomic, strong) TUIFitButton *messageModifyRepliesButton;
/**
* The message data class which stores the information required in the messageCell, including sender ID, sender avatar, message sending status, message bubble
* icon, etc. For details of messageData, please refer to: TUIChat\Cell\CellData\TUIMessageCellData.h
*/
@property(readonly) TUIMessageCellData *messageData;
/**
* A control that identifies whether a message has been read
*/
@property(nonatomic, strong) UILabel *readReceiptLabel;
/**
* The message time label control, which is not displayed by default, is located at the far right of the message cell
* In the message forwarding scenario, open the forwarded message list, and the time of the current message will be displayed on the far right of the message.
*/
@property(nonatomic, strong) UILabel *timeLabel;
/**
* Whether to disable the default selection behavior encapsulated in TUIKit, such as group live broadcast by default to create live room and other behaviors,
* default: NO
*/
@property(nonatomic, assign) BOOL disableDefaultSelectAction;
@property(nonatomic, weak) id<TUIMessageCellDelegate> delegate;
/**
*
* Whether the highlight flashing animation is in progress
*/
@property(nonatomic, assign) BOOL highlightAnimating;
- (void)fillWithData:(TUICommonCellData *)data;
/**
* Set the highlighting effect after matching the keyword, mainly used for jumping after message search, subclass rewriting
* The base class provides the default highlighting effect, and the subclass can implement it freely
*
* @param keyword Highlight keywords
*/
- (void)highlightWhenMatchKeyword:(NSString *)keyword;
/**
* Returns the view for highlighting
*/
- (UIView *)highlightAnimateView;
/**
* Update the content of the read label
*/
- (void)updateReadLabelText;
/// Preset bottom container in cell, which can be added custom view/viewControllers.
@property(nonatomic, strong) UIView *bottomContainer;
/// When bottom container is layout ready, notify it to add custom extensions.
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData;
/// Callback of SelectCell
@property(nonatomic, copy) TUIValueCallbck pluginMsgSelectCallback;
@end
@interface TUIMessageCell (TUILayoutConfiguration)
/**
* The color of the label that displays the recipient's nickname
* Used when the nickname needs to be displayed and the message direction is MsgDirectionIncoming
*/
@property(nonatomic, class) UIColor *incommingNameColor;
/**
*
* The font of the label that displays the recipient's nickname
* Used when the nickname needs to be displayed and the message direction is MsgDirectionIncoming
*
*/
@property(nonatomic, class) UIFont *incommingNameFont;
/**
* The color of the label showing the sender's nickname
* Used when the nickname needs to be displayed and the message direction is MsgDirectionOutgoing.
*/
@property(nonatomic, class) UIColor *outgoingNameColor;
/**
*
* The font of the label that displays the sender's nickname
* Used when the nickname needs to be displayed and the message direction is MsgDirectionOutgoing.
*/
@property(nonatomic, class) UIFont *outgoingNameFont;
@end

View File

@@ -0,0 +1,680 @@
//
// TUIMessageCell.m
// UIKit
//
// Created by kennethmiao on 2018/9/17.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/TUITool.h>
#import "NSString+TUIEmoji.h"
#import "TUISystemMessageCellData.h"
#import <TUICore/TUICore.h>
@interface TUIMessageCell () <CAAnimationDelegate>
@property(nonatomic, strong) TUIMessageCellData *messageData;
@end
@implementation TUIMessageCell
#pragma mark - Life cycle
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self setupSubViews];
[self setupRAC];
}
return self;
}
- (void)setupSubViews {
// head
_avatarView = [[UIImageView alloc] init];
_avatarView.contentMode = UIViewContentModeScaleAspectFill;
[self.contentView addSubview:_avatarView];
UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMessageAvatar:)];
[_avatarView addGestureRecognizer:tap1];
UILongPressGestureRecognizer *tap2 = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongSelectMessageAvatar:)];
[_avatarView addGestureRecognizer:tap2];
[_avatarView setUserInteractionEnabled:YES];
// nameLabel
_nameLabel = [[UILabel alloc] init];
_nameLabel.font = [self fontWithSize:13];
_nameLabel.textColor = [UIColor d_systemGrayColor];
[self.contentView addSubview:_nameLabel];
// container
_container = [[UIView alloc] init];
_container.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMessage:)];
tap.cancelsTouchesInView = NO;
[_container addGestureRecognizer:tap];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
[_container addGestureRecognizer:longPress];
[self.contentView addSubview:_container];
// indicator
_indicator = [[UIActivityIndicatorView alloc] init];
_indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
[_indicator sizeToFit];
[self.contentView addSubview:_indicator];
// error
_retryView = [[UIImageView alloc] init];
_retryView.userInteractionEnabled = YES;
UITapGestureRecognizer *resendTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onRetryMessage:)];
[_retryView addGestureRecognizer:resendTap];
[self.contentView addSubview:_retryView];
// messageModifyRepliesLabel
_messageModifyRepliesButton = [[TUIFitButton alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
_messageModifyRepliesButton.imageSize = CGSizeMake(12, 12);
[_messageModifyRepliesButton addTarget:self action:@selector(onJumpToRepliesDetailPage:) forControlEvents:UIControlEventTouchUpInside];
[_messageModifyRepliesButton.titleLabel setFont:[self fontWithSize:12]];
[_messageModifyRepliesButton setTitleColor:TIMCommonDynamicColor(@"chat_message_read_name_date_text_color", @"#999999") forState:UIControlStateNormal];
[_messageModifyRepliesButton setImage:TIMCommonBundleThemeImage(@"chat_messageReplyIcon_img", @"messageReplyIcon") forState:UIControlStateNormal];
[self.contentView addSubview:_messageModifyRepliesButton];
_readReceiptLabel = [[UILabel alloc] init];
_readReceiptLabel.hidden = YES;
_readReceiptLabel.font = [self fontWithSize:12];
_readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
_readReceiptLabel.lineBreakMode = NSLineBreakByCharWrapping;
UITapGestureRecognizer *showReadReceiptTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectReadReceipt:)];
[_readReceiptLabel addGestureRecognizer:showReadReceiptTap];
_readReceiptLabel.userInteractionEnabled = YES;
[self.contentView addSubview:_readReceiptLabel];
// selectedIcon
_selectedIcon = [[UIImageView alloc] init];
[self.contentView addSubview:_selectedIcon];
// selectedView
_selectedView = [UIButton buttonWithType:UIButtonTypeCustom];
_selectedView.backgroundColor = [UIColor clearColor];
[_selectedView addTarget:self action:@selector(onSelectMessage:) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:_selectedView];
// timeLabel
_timeLabel = [[UILabel alloc] init];
_timeLabel.textColor = [UIColor darkGrayColor];
_timeLabel.font = [self fontWithSize:11.0];
[self.contentView addSubview:_timeLabel];
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.backgroundColor = UIColor.clearColor;
self.contentView.backgroundColor = UIColor.clearColor;
[self makeConstraints];
}
- (void)makeConstraints {
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(_container.mas_leading).mas_offset(7);
make.top.mas_equalTo(self.avatarView.mas_top);
make.width.mas_equalTo(1);
make.height.mas_equalTo(20);
}];
[self.selectedIcon mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentView.mas_leading).mas_offset(3);
make.top.mas_equalTo(self.avatarView.mas_centerY).mas_offset(-10);
make.width.mas_equalTo(20);
make.height.mas_equalTo(20);
}];
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-10);
make.top.mas_equalTo(self.avatarView);
make.width.mas_greaterThanOrEqualTo(10);
make.height.mas_equalTo(10);
}];
[self.selectedView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.contentView);
}];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
TUIMessageCellLayout *cellLayout = self.messageData.cellLayout;
BOOL isInComing = (self.messageData.direction == MsgDirectionIncoming);
[self.nameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
if (isInComing) {
make.leading.mas_equalTo(_container.mas_leading).mas_offset(7);
make.trailing.mas_equalTo(self.contentView).mas_offset(-7);
} else {
make.leading.mas_equalTo(self.contentView).mas_offset(7);
make.trailing.mas_equalTo(self.container.mas_trailing);
}
if (self.messageData.showName) {
make.width.mas_greaterThanOrEqualTo(20);
make.height.mas_greaterThanOrEqualTo(20);
} else {
make.height.mas_equalTo(0);
}
make.top.mas_equalTo(self.avatarView.mas_top);
}];
[self.selectedIcon mas_updateConstraints:^(MASConstraintMaker *make) {
if (self.messageData.showCheckBox) {
make.width.mas_equalTo(20);
make.height.mas_equalTo(20);
} else {
make.size.mas_equalTo(CGSizeZero);
}
}];
[self.timeLabel sizeToFit];
[self.timeLabel mas_updateConstraints:^(MASConstraintMaker *make) {
if (self.messageData.showMessageTime) {
make.width.mas_equalTo(self.timeLabel.frame.size.width);
make.height.mas_equalTo(self.timeLabel.frame.size.height);
} else {
make.width.mas_equalTo(0);
make.height.mas_equalTo(0);
}
}];
CGSize csize = [self.class getContentSize:self.messageData];
CGFloat contentWidth = csize.width;
CGFloat contentHeight = csize.height;
if (!CGSizeEqualToSize(self.messageData.messageContainerAppendSize, CGSizeZero)) {
/**
* Taking the maximum width between the "emoji reply message" and the text content
*/
contentWidth = MAX(self.messageData.messageContainerAppendSize.width, csize.width);
/**
* Limit the maximum width to Screen_Width *0.25 * 3
*/
contentWidth = MIN(contentWidth, Screen_Width * 0.25 * 3);
contentHeight = csize.height + self.messageData.messageContainerAppendSize.height;
}
if (self.messageData.direction == MsgDirectionIncoming) {
self.avatarView.hidden = !self.messageData.showAvatar;
[self.avatarView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.messageData.showCheckBox) {
make.leading.mas_equalTo(self.selectedIcon.mas_trailing).mas_offset(cellLayout.avatarInsets.left);
} else {
make.leading.mas_equalTo(self.contentView.mas_leading).mas_offset(cellLayout.avatarInsets.left);
}
make.top.mas_equalTo(cellLayout.avatarInsets.top);
make.size.mas_equalTo(cellLayout.avatarSize);
}];
[self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.avatarView.mas_trailing).mas_offset(cellLayout.messageInsets.left);
make.top.mas_equalTo(self.nameLabel.mas_bottom).mas_offset(cellLayout.messageInsets.top);
make.width.mas_equalTo(contentWidth);
make.height.mas_equalTo(contentHeight);
}];
CGRect indicatorFrame = self.indicator.frame;
[self.indicator mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.container.mas_trailing).mas_offset(8);
make.centerY.mas_equalTo(self.container.mas_centerY);
make.size.mas_equalTo(indicatorFrame.size);
}];
self.retryView.frame = self.indicator.frame;
self.readReceiptLabel.hidden = YES;
} else {
if (!self.messageData.showAvatar) {
cellLayout.avatarSize = CGSizeZero;
}
[self.avatarView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-cellLayout.avatarInsets.right);
make.top.mas_equalTo(cellLayout.avatarInsets.top);
make.size.mas_equalTo(cellLayout.avatarSize);
}];
[self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.avatarView.mas_leading).mas_offset(-cellLayout.messageInsets.right);
make.top.mas_equalTo(self.nameLabel.mas_bottom).mas_offset(cellLayout.messageInsets.top);
make.width.mas_equalTo(contentWidth);
make.height.mas_equalTo(contentHeight);
}];
CGRect indicatorFrame = self.indicator.frame;
[self.indicator mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
make.centerY.mas_equalTo(self.container.mas_centerY);
make.size.mas_equalTo(indicatorFrame.size);
}];
self.retryView.frame = self.indicator.frame;
[self.readReceiptLabel sizeToFit];
[self.readReceiptLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(self.container.mas_bottom);
make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
make.size.mas_equalTo(self.readReceiptLabel.frame.size);
}];
}
if (!self.messageModifyRepliesButton.isHidden) {
self.messageModifyRepliesButton.mm_sizeToFit();
CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
[self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
if (isInComing) {
make.leading.mas_equalTo(self.container.mas_leading);
} else {
make.trailing.mas_equalTo(self.container.mas_trailing);
}
make.top.mas_equalTo(self.container.mas_bottom);
make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
}];
}
// according to apple super should be called at end of method
[super updateConstraints];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)setupRAC {
@weakify(self);
[RACObserve(self, readReceiptLabel.text) subscribeNext:^(id _Nullable x) {
@strongify(self);
if ([self shouldHighlightReadReceiptLabel]) {
self.readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_color", @"#147AFF");
} else {
self.readReceiptLabel.textColor = TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
}
}];
}
- (void)prepareForReuse {
[super prepareForReuse];
/**
* In the future, any UI problems caused by reuse can be solved by coding here.
*/
/**
* Once the message is reused, it means that a new message is about to appear, and the label content is changed to empty string.
*/
_readReceiptLabel.text = @"";
_readReceiptLabel.hidden = YES;
}
#pragma mark - Public
- (void)fillWithData:(TUIMessageCellData *)data {
[super fillWithData:data];
self.messageData = data;
[self loadAvatar:data];
if (self.messageData.showName) {
_nameLabel.hidden = NO;
} else {
_nameLabel.hidden = YES;
}
if (self.messageData.showCheckBox) {
_selectedIcon.hidden = NO;
_selectedView.hidden = NO;
} else {
_selectedIcon.hidden = YES;
_selectedView.hidden = YES;
}
if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
self.avatarView.layer.masksToBounds = YES;
self.avatarView.layer.cornerRadius = data.cellLayout.avatarSize.height / 2;
} else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
self.avatarView.layer.masksToBounds = YES;
self.avatarView.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
}
self.nameLabel.text = data.senderName;
if (data.direction == MsgDirectionIncoming) {
self.nameLabel.textColor = self.class.incommingNameColor;
self.nameLabel.font = self.class.incommingNameFont;
} else {
self.nameLabel.textColor = self.class.outgoingNameColor;
self.nameLabel.font = self.class.outgoingNameFont;
}
self.retryView.image = [UIImage imageNamed:TUIChatImagePath(@"msg_error")];
if (data.status == Msg_Status_Fail) {
[_indicator stopAnimating];
_readReceiptLabel.hidden = YES;
self.retryView.hidden = NO;
} else {
if (data.status == Msg_Status_Sending_2) {
[_indicator startAnimating];
_readReceiptLabel.hidden = YES;
} else if (data.status == Msg_Status_Succ) {
[_indicator stopAnimating];
/**
* The message is sent successfully, indicating that the indicator and error are no longer displayed on the label, and the read receipt label can be
* displayed.
*/
if (self.messageData.showReadReceipt && self.messageData.direction == MsgDirectionOutgoing && self.messageData.innerMessage.needReadReceipt &&
(self.messageData.innerMessage.userID || self.messageData.innerMessage.groupID) &&
![self.messageData isKindOfClass:TUISystemMessageCellData.class]) {
[self updateReadLabelText];
_readReceiptLabel.hidden = NO;
}
} else if (data.status == Msg_Status_Sending) {
[_indicator startAnimating];
_readReceiptLabel.hidden = YES;
}
self.retryView.hidden = YES;
}
self.messageModifyRepliesButton.hidden = !data.showMessageModifyReplies;
if (data.showMessageModifyReplies) {
NSString *title = [NSString stringWithFormat:@"%ld%@", data.messageModifyReplies.count, TIMCommonLocalizableString(TUIKitRepliesNum)];
[self.messageModifyRepliesButton setTitle:title forState:UIControlStateNormal];
[self.messageModifyRepliesButton sizeToFit];
[self.messageModifyRepliesButton setNeedsUpdateConstraints];
[self.messageModifyRepliesButton updateConstraintsIfNeeded];
[self.messageModifyRepliesButton layoutIfNeeded];
}
NSString *imageName = (data.showCheckBox && data.selected) ? TIMCommonImagePath(@"icon_select_selected") : TIMCommonImagePath(@"icon_select_normal");
self.selectedIcon.image = [UIImage imageNamed:imageName];
_timeLabel.text = [TUITool convertDateToStr:data.innerMessage.timestamp];
[_timeLabel sizeToFit];
_timeLabel.hidden = !data.showMessageTime;
/**
* Text highlighting - asynchronous operations are here to keep the order of execution consistent with subclasses
*/
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf highlightWhenMatchKeyword:data.highlightKeyword];
});
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)loadAvatar:(TUIMessageCellData *)data {
[self.avatarView setImage:DefaultAvatarImage];
@weakify(self);
[[[RACObserve(data, avatarUrl) takeUntil:self.rac_prepareForReuseSignal] ignore:nil] subscribeNext:^(NSURL *url) {
@strongify(self);
[self.avatarView sd_setImageWithURL:url placeholderImage:DefaultAvatarImage];
}];
if (data.isUseMsgReceiverAvatar) {
NSString *userId = @"";
if ([data.innerMessage.sender isEqualToString:V2TIMManager.sharedInstance.getLoginUser]) {
userId = data.innerMessage.userID;
} else {
userId = V2TIMManager.sharedInstance.getLoginUser;
}
[V2TIMManager.sharedInstance getUsersInfo:@[ userId?:@"" ]
succ:^(NSArray<V2TIMUserFullInfo *> *infoList) {
@strongify(self);
V2TIMUserFullInfo *info = infoList.firstObject;
if (info && [data isEqual:self.messageData]) {
data.avatarUrl = [NSURL URLWithString:info.faceURL];
[self.avatarView sd_setImageWithURL:data.avatarUrl placeholderImage:DefaultAvatarImage];
}
}
fail:^(int code, NSString *desc){
}];
}
}
- (void)highlightWhenMatchKeyword:(NSString *)keyword {
static NSString *const key = @"highlightAnimation";
if (keyword && keyword.length) {
if (self.highlightAnimating) {
return;
}
self.highlightAnimating = YES;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"backgroundColor"];
animation.repeatCount = 3;
animation.values = @[
(id)[[UIColor orangeColor] colorWithAlphaComponent:0.2].CGColor,
(id)[[UIColor orangeColor] colorWithAlphaComponent:0.5].CGColor,
(id)[[UIColor orangeColor] colorWithAlphaComponent:0.2].CGColor,
];
animation.duration = 0.5;
animation.removedOnCompletion = YES;
animation.delegate = self;
[self.highlightAnimateView.layer addAnimation:animation forKey:key];
} else {
[self.highlightAnimateView.layer removeAnimationForKey:key];
}
}
- (void)updateReadLabelText {
if (self.messageData.innerMessage.groupID.length > 0) {
// group message
NSString *text = TIMCommonLocalizableString(Unread);
if (self.messageData.messageReceipt == nil) {
// haven't received the message receipt yet
return;
}
NSInteger readCount = self.messageData.messageReceipt.readCount;
NSInteger unreadCount = self.messageData.messageReceipt.unreadCount;
if (unreadCount == 0) {
// show "All read"
text = TIMCommonLocalizableString(TUIKitMessageReadAllRead);
} else if (readCount > 0) {
// show "x read"
text = [NSString stringWithFormat:@"%ld %@", (long)readCount, TIMCommonLocalizableString(TUIKitMessageReadPartRead)];
}
self.readReceiptLabel.text = text;
} else {
// c2c message
BOOL isPeerRead = self.messageData.messageReceipt.isPeerRead;
NSString *text = isPeerRead ? TIMCommonLocalizableString(TUIKitMessageReadC2CRead) : TIMCommonLocalizableString(TUIKitMessageReadC2CUnRead);
self.readReceiptLabel.text = text;
}
[self.readReceiptLabel sizeToFit];
[self.readReceiptLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(self.container.mas_bottom);
make.trailing.mas_equalTo(self.container.mas_leading).mas_offset(-8);
make.size.mas_equalTo(self.readReceiptLabel.frame.size);
}];
self.readReceiptLabel.textColor = [self shouldHighlightReadReceiptLabel] ? TIMCommonDynamicColor(@"chat_message_read_status_text_color", @"#147AFF")
: TIMCommonDynamicColor(@"chat_message_read_status_text_gray_color", @"#BBBBBB");
}
- (UIView *)highlightAnimateView {
return self.container;
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
return 60.f;
}
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
CGFloat height = 0;
if (data.showName) height += kScale375(20);
if (data.showMessageModifyReplies) height += kScale375(22);
if (data.messageContainerAppendSize.height > 0) {
height += data.messageContainerAppendSize.height;
}
CGSize containerSize = [self getContentSize:data];
height += containerSize.height;
height += data.cellLayout.messageInsets.top;
height += data.cellLayout.messageInsets.bottom;
if (height < 55) height = 55;
return height;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
return CGSizeZero;
}
#pragma mark - Private
- (void)animationDidStart:(CAAnimation *)anim {
self.highlightAnimating = YES;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
self.highlightAnimating = NO;
}
#pragma mark-- Event
- (void)onLongPress:(UIGestureRecognizer *)recognizer {
if ([recognizer isKindOfClass:[UILongPressGestureRecognizer class]] && recognizer.state == UIGestureRecognizerStateBegan) {
if (_delegate && [_delegate respondsToSelector:@selector(onLongPressMessage:)]) {
[_delegate onLongPressMessage:self];
}
}
}
- (void)onRetryMessage:(UIGestureRecognizer *)recognizer {
if (_messageData.status == Msg_Status_Fail)
if (_delegate && [_delegate respondsToSelector:@selector(onRetryMessage:)]) {
[_delegate onRetryMessage:self];
}
}
- (void)onSelectMessage:(UIGestureRecognizer *)recognizer {
if (_delegate && [_delegate respondsToSelector:@selector(onSelectMessage:)]) {
[_delegate onSelectMessage:self];
}
}
- (void)onSelectMessageAvatar:(UIGestureRecognizer *)recognizer {
if (_delegate && [_delegate respondsToSelector:@selector(onSelectMessageAvatar:)]) {
[_delegate onSelectMessageAvatar:self];
}
}
- (void)onLongSelectMessageAvatar:(UIGestureRecognizer *)recognizer {
if (_delegate && [_delegate respondsToSelector:@selector(onLongSelectMessageAvatar:)]) {
[_delegate onLongSelectMessageAvatar:self];
}
}
- (void)onSelectReadReceipt:(UITapGestureRecognizer *)gesture {
if (![self shouldHighlightReadReceiptLabel]) {
return;
}
if (_delegate && [_delegate respondsToSelector:@selector(onSelectReadReceipt:)]) {
[_delegate onSelectReadReceipt:self.messageData];
}
}
- (void)onJumpToRepliesDetailPage:(UIButton *)btn {
NSLog(@"click onJumpToRepliesDetailPage");
NSLog(@"%@", self.messageData.messageModifyReplies);
if (_delegate && [_delegate respondsToSelector:@selector(onJumpToRepliesDetailPage:)]) {
[_delegate onJumpToRepliesDetailPage:self.messageData];
}
}
- (BOOL)shouldHighlightReadReceiptLabel {
if (self.messageData.innerMessage.groupID.length == 0) {
return ![self.readReceiptLabel.text isEqualToString:TIMCommonLocalizableString(TUIKitMessageReadC2CRead)];
} else {
return ![self.readReceiptLabel.text isEqualToString:TIMCommonLocalizableString(TUIKitMessageReadAllRead)];
}
}
- (UIFont *)fontWithSize:(CGFloat)size {
static NSCache *fontCache;
if (fontCache == nil) {
fontCache = [[NSCache alloc] init];
}
UIFont *font = [fontCache objectForKey:@(size)];
if (font == nil) {
font = [UIFont systemFontOfSize:size];
[fontCache setObject:font forKey:@(size)];
}
return font;
}
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
// Override by subclass.
}
@end
@implementation TUIMessageCell (TUILayoutConfiguration)
static UIColor *gOutgoingNameColor;
+ (UIColor *)outgoingNameColor {
if (!gOutgoingNameColor) {
gOutgoingNameColor = [UIColor d_systemGrayColor];
}
return gOutgoingNameColor;
}
+ (void)setOutgoingNameColor:(UIColor *)outgoingNameColor {
gOutgoingNameColor = outgoingNameColor;
}
static UIFont *gOutgoingNameFont;
+ (UIFont *)outgoingNameFont {
if (!gOutgoingNameFont) {
gOutgoingNameFont = [UIFont systemFontOfSize:14];
}
return gOutgoingNameFont;
}
+ (void)setOutgoingNameFont:(UIFont *)outgoingNameFont {
gOutgoingNameFont = outgoingNameFont;
}
static UIColor *gIncommingNameColor;
+ (UIColor *)incommingNameColor {
if (!gIncommingNameColor) {
gIncommingNameColor = [UIColor d_systemGrayColor];
}
return gIncommingNameColor;
}
+ (void)setIncommingNameColor:(UIColor *)incommingNameColor {
gIncommingNameColor = incommingNameColor;
}
static UIFont *gIncommingNameFont;
+ (UIFont *)incommingNameFont {
if (!gIncommingNameFont) {
gIncommingNameFont = [UIFont systemFontOfSize:14];
}
return gIncommingNameFont;
}
+ (void)setIncommingNameFont:(UIFont *)incommingNameFont {
gIncommingNameFont = incommingNameFont;
}
@end

View File

@@ -0,0 +1,20 @@
//
// TUISecurityStrikeView.h
// TIMCommon
//
// Created by wyl on 2023/10/11.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#define kTUISecurityStrikeViewTopLineMargin 14.5
#define kTUISecurityStrikeViewTopLineToBottom 28
@interface TUISecurityStrikeView : UIView
@property(nonatomic, strong) UIView * topLine;
@property(nonatomic, strong) UILabel * textLabel;
+ (UIImage *)changeImageColorWith:(UIColor *)color image:(UIImage *)image alpha:(CGFloat)alpha;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,73 @@
//
// TUISecurityStrikeView.m
// TIMCommon
//
// Created by wyl on 2023/10/11.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUISecurityStrikeView.h"
#import <TIMCommon/TIMDefine.h>
@implementation TUISecurityStrikeView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self){
[self setupView];
}
return self;
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)setupView {
self.topLine = [[UIView alloc] initWithFrame:CGRectZero];
self.topLine.backgroundColor = TUIDynamicColor(@"", TUIThemeModuleTIMCommon, @"#E5C7C7");
[self addSubview:self.topLine];
self.textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[self addSubview:self.textLabel];
self.textLabel.font = [UIFont systemFontOfSize:14];
self.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike);
self.textLabel.textColor = TUIDynamicColor(@"", TUIThemeModuleTIMCommon, @"#DA2222");
self.textLabel.numberOfLines = 0;
self.textLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.topLine mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(kTUISecurityStrikeViewTopLineMargin);
make.leading.mas_equalTo(10);
make.trailing.mas_equalTo(-10);
make.height.mas_equalTo(0.5);
}];
[self.textLabel sizeToFit];
[self.textLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(10);
make.bottom.mas_equalTo(-11);
make.width.mas_equalTo(self);
}];
}
+ (UIImage *)changeImageColorWith:(UIColor *)color image:(UIImage *)image alpha:(CGFloat)alpha {
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0, image.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetAlpha(context, alpha);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
CGContextClipToMask(context, rect, image.CGImage);
[color setFill];
CGContextFillRect(context, rect);
UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
@end

View File

@@ -0,0 +1,30 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This file declares the TUISystemMessageCell class, which is responsible for displaying system messages.
* The system message unit is responsible for displaying special messages from the system. Such messages are usually white on a gray background and centered.
*/
#import "TUIMessageCell.h"
#import "TUISystemMessageCellData.h"
/**
* 【Module name】 TUISystemMessageCell
* 【Function description】System message unit
* - It is used to display the system messages. Common system messages include: recall-message, group-member-change-message, group-created and
* group-diss-message, etc.
* - System messages are typically used to display notifications from apps that are sent by the system, not from any user.
*/
@interface TUISystemMessageCell : TUIMessageCell
/**
*
* The label of display system message content, such as "You recalled a message.".
*/
@property(readonly) UILabel *messageLabel;
@property(readonly) TUISystemMessageCellData *systemData;
- (void)fillWithData:(TUISystemMessageCellData *)data;
@end

View File

@@ -0,0 +1,107 @@
//
// TUISystemMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUISystemMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/NSString+TUIUtil.h>
@interface TUISystemMessageCell ()
@property(nonatomic, strong) UILabel *messageLabel;
@property TUISystemMessageCellData *systemData;
@end
@implementation TUISystemMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_messageLabel = [[UILabel alloc] init];
_messageLabel.textAlignment = NSTextAlignmentCenter;
_messageLabel.numberOfLines = 0;
_messageLabel.backgroundColor = [UIColor clearColor];
_messageLabel.layer.cornerRadius = 3;
[_messageLabel.layer setMasksToBounds:YES];
[self.container addSubview:_messageLabel];
self.backgroundColor = [UIColor clearColor];
self.contentView.backgroundColor = [UIColor clearColor];
}
return self;
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.container mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.contentView);
make.size.mas_equalTo(self.contentView);
}];
[self.messageLabel sizeToFit];
if(self.messageLabel.superview) {
[self.messageLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.container);
make.leading.trailing.mas_equalTo(self.container);
}];
}
}
- (void)fillWithData:(TUISystemMessageCellData *)data;
{
[super fillWithData:data];
self.systemData = data;
self.messageLabel.textColor = TUISystemMessageCellData.textColor ? : data.contentColor;
self.messageLabel.font = TUISystemMessageCellData.textFont ? : data.contentFont;
self.messageLabel.backgroundColor = TUISystemMessageCellData.textBackgroundColor ? : [UIColor clearColor];
self.messageLabel.attributedText = data.attributedString;
self.nameLabel.hidden = YES;
self.avatarView.hidden = YES;
self.retryView.hidden = YES;
[self.indicator stopAnimating];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
return 42.f;
}
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
return [self getContentSize:data].height + kScale375(16);
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUISystemMessageCellData.class], @"data must be kind of TUISystemMessageCellData");
TUISystemMessageCellData *systemCellData = (TUISystemMessageCellData *)data;
static CGSize maxSystemSize;
if (CGSizeEqualToSize(maxSystemSize, CGSizeZero)) {
maxSystemSize = CGSizeMake(Screen_Width, MAXFLOAT);
}
CGSize size = [systemCellData.attributedString.string textSizeIn:maxSystemSize font:systemCellData.contentFont];
size.height += 10;
size.width += 16;
return size;
}
@end