增加换肤功能

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,16 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This document declares that after the group is successfully created, the "xxx create group chat" message cell displayed when jumping to the message
* interface
*/
#import <TIMCommon/TUISystemMessageCell.h>
#import "TUIGroupCreatedCellData.h"
@interface TUIGroupCreatedCell : TUISystemMessageCell
- (void)fillWithData:(TUIGroupCreatedCellData *)data;
@end

View File

@@ -0,0 +1,17 @@
//
// MyCustomCell.m
// TUIKitDemo
//
// Created by annidyfeng on 2019/6/10.
// Copyright © 2019 Tencent. All rights reserved.
//
#import "TUIGroupCreatedCell.h"
@implementation TUIGroupCreatedCell
- (void)fillWithData:(TUIGroupCreatedCellData *)data {
[super fillWithData:data];
}
@end

View File

@@ -0,0 +1,48 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
*
* This document declares modules for implementing "more" units.
* More units, that is, the UI interface that appears after clicking the "+" in the lower right corner of the chat interface.
* At present, more units provide four multimedia sending functions of camera, video, picture, and file, and you can also customize it according to your needs.
*/
#import <UIKit/UIKit.h>
#import "TUIInputMoreCellData.h"
/////////////////////////////////////////////////////////////////////////////////
//
// TUIInputMoreCell
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIInputMoreCell : UICollectionViewCell
/**
* The icons corresponding to more cells are obtained from the image of TUIInputMoreCellData.
* The icons of each unit are different, which are used to visually represent the corresponding functions of the unit.
*/
@property(nonatomic, strong) UIImageView *image;
/**
* The label corresponding to more cells, the text content of which is obtained from the title of TUIInputMoreCellData.
* The names of each unit are different (such as camera, video, file, album, etc.), which are used to display the corresponding functions of the unit in text
* form below the icon.
*/
@property(nonatomic, strong) UILabel *title;
@property(nonatomic, strong) TUIInputMoreCellData *data;
/**
* 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;
- (void)fillWithData:(TUIInputMoreCellData *)data;
+ (CGSize)getSize;
@end

View File

@@ -0,0 +1,67 @@
//
// TMoreCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/22.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIInputMoreCell.h"
#import <TIMCommon/TIMDefine.h>
@implementation TUIInputMoreCell
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
}
return self;
}
- (void)setupViews {
_image = [[UIImageView alloc] init];
_image.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:_image];
_title = [[UILabel alloc] init];
[_title setFont:[UIFont systemFontOfSize:10]];
[_title setTextColor:[UIColor grayColor]];
_title.textAlignment = NSTextAlignmentCenter;
[self addSubview:_title];
}
- (void)fillWithData:(TUIInputMoreCellData *)data {
// set data
_data = data;
self.hidden = (data == nil) ? YES : NO;
_image.image = data.image;
[_title setText:data.title];
// update layout
CGSize menuSize = TMoreCell_Image_Size;
_image.frame = CGRectMake(0, 0, menuSize.width, menuSize.height);
_title.frame = CGRectMake(0, _image.frame.origin.y + _image.frame.size.height, _image.frame.size.width + 10, TMoreCell_Title_Height);
_title.center = CGPointMake(_image.center.x, _title.center.y);
}
+ (CGSize)getSize {
CGSize menuSize = TMoreCell_Image_Size;
return CGSizeMake(menuSize.width, menuSize.height + TMoreCell_Title_Height);
}
@end
@interface IUChatView : UIView
@property(nonatomic, strong) UIView *view;
@end
@implementation IUChatView
- (instancetype)init {
self = [super init];
if (self) {
self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
[self addSubview:self.view];
}
return self;
}
@end

View File

@@ -0,0 +1,16 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TIMCommonModel.h>
NS_ASSUME_NONNULL_BEGIN
@class TUIMemberCellData;
@interface TUIMemberCell : TUICommonTableViewCell
- (void)fillWithData:(TUIMemberCellData *)cellData;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,115 @@
//
// TUIMemberCell.m
// TUIChat
//
// Created by xia on 2022/3/11.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMemberCell.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMemberCellData.h"
@interface TUIMemberCell ()
@property(nonatomic, strong) UIImageView *avatarView;
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UILabel *detailLabel;
@property(nonatomic, strong) TUIMemberCellData *cellData;
@end
@implementation TUIMemberCell
#pragma mark - Life cycle
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.contentView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
self.avatarView = [[UIImageView alloc] initWithImage:DefaultAvatarImage];
[self.contentView addSubview:self.avatarView];
self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[self.contentView addSubview:self.titleLabel];
self.titleLabel.textColor = TIMCommonDynamicColor(@"form_title_color", @"#000000");
self.detailLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 20)];
[self.contentView addSubview:self.detailLabel];
self.detailLabel.rtlAlignment = TUITextRTLAlignmentTrailing;
self.detailLabel.textColor = TIMCommonDynamicColor(@"form_title_color", @"#000000");
[self setSelectionStyle:UITableViewCellSelectionStyleNone];
self.changeColorWhenTouched = YES;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
#pragma mark - Public
- (void)fillWithData:(TUIMemberCellData *)cellData {
[super fillWithData:cellData];
self.cellData = cellData;
self.titleLabel.text = cellData.title;
[self.avatarView sd_setImageWithURL:cellData.avatarUrL placeholderImage:DefaultAvatarImage];
self.detailLabel.hidden = cellData.detail.length == 0;
self.detailLabel.text = cellData.detail;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGFloat imgWidth = kScale390(34);
[self.avatarView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(imgWidth);
make.centerY.mas_equalTo(self.contentView.mas_centerY);
make.leading.mas_equalTo(kScale390(12));
}];
if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRounded) {
self.avatarView.layer.masksToBounds = YES;
self.avatarView.layer.cornerRadius = imgWidth / 2;
} else if ([TUIConfig defaultConfig].avatarType == TAvatarTypeRadiusCorner) {
self.avatarView.layer.masksToBounds = YES;
self.avatarView.layer.cornerRadius = [TUIConfig defaultConfig].avatarCornerRadius;
}
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(self.avatarView.mas_centerY);
make.leading.mas_equalTo(self.avatarView.mas_trailing).mas_offset(12);
make.height.mas_equalTo(20);
make.trailing.lessThanOrEqualTo(self.detailLabel.mas_leading).mas_offset(-5);
}];
[self.detailLabel sizeToFit];
[self.detailLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(self.avatarView.mas_centerY);
make.height.mas_equalTo(20);
make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-3);
make.width.mas_equalTo(self.detailLabel.frame.size.width);
}];
}
@end

View File

@@ -0,0 +1,75 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUITextView.h>
#import "TUIChatDefine.h"
#import "TUITextMessageCellData.h"
@class TUITextView;
typedef void (^TUIChatSelectAllContentCallback)(BOOL);
@interface TUITextMessageCell : TUIBubbleMessageCell <UITextViewDelegate>
/**
*
* TextView for display text message content
*/
@property(nonatomic, strong) TUITextView *textView;
/**
*
* Selected text content
*/
@property(nonatomic, strong) NSString *selectContent;
/**
*
* Callback for selected all text
*/
@property(nonatomic, strong) TUIChatSelectAllContentCallback selectAllContentContent;
/// Data for text message cell.
@property(nonatomic, strong) TUITextMessageCellData *textData;
@property(nonatomic, strong) UIImageView *voiceReadPoint;
- (void)fillWithData:(TUITextMessageCellData *)data;
@end
@interface TUITextMessageCell (TUILayoutConfiguration)
/**
*
* The color of label which displays the text message content.
* Used when the message direction is send.
*/
@property(nonatomic, class) UIColor *outgoingTextColor;
/**
*
* The font of label which displays the text message content.
* Used when the message direction is send.
*/
@property(nonatomic, class) UIFont *outgoingTextFont;
/**
*
* The color of label which displays the text message content.
* Used when the message direction is received.
*/
@property(nonatomic, class) UIColor *incommingTextColor;
/**
*
* The font of label which displays the text message content.
* Used when the message direction is received.
*/
@property(nonatomic, class) UIFont *incommingTextFont;
+ (void)setMaxTextSize:(CGSize)maxTextSz;
@end

View File

@@ -0,0 +1,339 @@
//
// TUITextMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUITextMessageCell.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIGlobalization.h>
#import "TUIFaceView.h"
#import <TUICore/UIColor+TUIHexColor.h>
#ifndef CGFLOAT_CEIL
#ifdef CGFLOAT_IS_DOUBLE
#define CGFLOAT_CEIL(value) ceil(value)
#else
#define CGFLOAT_CEIL(value) ceilf(value)
#endif
#endif
@interface TUITextMessageCell ()<TUITextViewDelegate>
@end
@implementation TUITextMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.textView = [[TUITextView alloc] init];
self.textView.backgroundColor = [UIColor clearColor];
self.textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
self.textView.textContainer.lineFragmentPadding = 0;
self.textView.scrollEnabled = NO;
self.textView.editable = NO;
self.textView.delegate = self;
self.textView.tuiTextViewDelegate = self;
self.bubbleView.userInteractionEnabled = YES;
[self.bubbleView addSubview:self.textView];
self.bottomContainer = [[UIView alloc] init];
[self.contentView addSubview:self.bottomContainer];
self.voiceReadPoint = [[UIImageView alloc] init];
self.voiceReadPoint.backgroundColor = [UIColor redColor];
self.voiceReadPoint.frame = CGRectMake(0, 0, 5, 5);
self.voiceReadPoint.hidden = YES;
[self.voiceReadPoint.layer setCornerRadius:self.voiceReadPoint.frame.size.width / 2];
[self.voiceReadPoint.layer setMasksToBounds:YES];
[self.bubbleView addSubview:self.voiceReadPoint];
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
for (UIView *view in self.bottomContainer.subviews) {
[view removeFromSuperview];
}
}
- (void)onLongPressTextViewMessage:(UITextView *)textView {
if (self.delegate && [self.delegate respondsToSelector:@selector(onLongPressMessage:)]) {
[self.delegate onLongPressMessage:self];
}
}
// Override
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.textData};
[TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
}
- (void)fillWithData:(TUITextMessageCellData *)data {
// set data
[super fillWithData:data];
self.textData = data;
self.selectContent = data.content;
self.voiceReadPoint.hidden = !data.showUnreadPoint;
self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
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.textView.font = textFont;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.bubbleView.mas_leading).mas_offset(self.textData.textOrigin.x);
make.top.mas_equalTo(self.bubbleView.mas_top).mas_offset(self.textData.textOrigin.y);
make.size.mas_equalTo(self.textData.textSize);
}];
if (self.voiceReadPoint.hidden == NO) {
[self.voiceReadPoint mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView);
make.leading.mas_equalTo(self.bubbleView.mas_trailing).mas_offset(1);
make.size.mas_equalTo(CGSizeMake(5, 5));
}];
}
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.textView.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
make.bottom.mas_equalTo(self.container).mas_offset(- self.messageData.messageContainerAppendSize.height);
}];
}
[self layoutBottomContainer];
}
- (void)layoutBottomContainer {
if (CGSizeEqualToSize(self.textData.bottomContainerSize, CGSizeZero)) {
return;
}
CGSize size = self.textData.bottomContainerSize;
[self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.textData.direction == MsgDirectionIncoming) {
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).offset(6);
make.size.mas_equalTo(size);
}];
CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
if (!self.messageModifyRepliesButton.hidden) {
[self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.textData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(self.container.mas_leading);
} else {
make.trailing.mas_equalTo(self.container.mas_trailing);
}
make.top.mas_equalTo(self.bottomContainer.mas_bottom);
make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
}];
}
}
- (void)textViewDidChangeSelection:(UITextView *)textView {
NSAttributedString *selectedString = [textView.attributedText attributedSubstringFromRange:textView.selectedRange];
if (self.selectAllContentContent && selectedString.length > 0) {
if (selectedString.length == textView.attributedText.length) {
self.selectAllContentContent(YES);
} else {
self.selectAllContentContent(NO);
}
}
if (selectedString.length > 0) {
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
[attributedString appendAttributedString:selectedString];
NSUInteger offsetLocation = 0;
for (NSDictionary *emojiLocation in self.textData.emojiLocations) {
NSValue *key = emojiLocation.allKeys.firstObject;
NSAttributedString *originStr = emojiLocation[key];
NSRange currentRange = [key rangeValue];
/**
* After each emoji is replaced, the length of the string will change, and the actual location of the emoji will also change accordingly.
*/
currentRange.location += offsetLocation;
if (currentRange.location >= textView.selectedRange.location) {
currentRange.location -= textView.selectedRange.location;
if (currentRange.location + currentRange.length <= attributedString.length) {
[attributedString replaceCharactersInRange:currentRange withAttributedString:originStr];
offsetLocation += originStr.length - currentRange.length;
}
}
}
self.selectContent = attributedString.string;
} else {
self.selectContent = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:@"kTUIChatPopMenuWillHideNotification" object:nil];
}
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
return 60.f;
}
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
CGFloat height = [super getHeight:data withWidth:width];
if (data.bottomContainerSize.height > 0) {
height += data.bottomContainerSize.height + kScale375(6);
}
return height;
}
static CGSize gMaxTextSize;
+ (void)setMaxTextSize:(CGSize)maxTextSz {
gMaxTextSize = maxTextSz;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUITextMessageCellData.class], @"data must be kind of TUITextMessageCellData");
TUITextMessageCellData *textCellData = (TUITextMessageCellData *)data;
NSAttributedString *attributeString = [textCellData getContentAttributedString:
data.direction == MsgDirectionIncoming ? self.incommingTextFont : self.outgoingTextFont];
if (CGSizeEqualToSize(gMaxTextSize, CGSizeZero)) {
gMaxTextSize = CGSizeMake(TTextMessageCell_Text_Width_Max, MAXFLOAT);
}
CGSize contentSize = [textCellData getContentAttributedStringSize:attributeString maxTextSize:gMaxTextSize];
textCellData.textSize = contentSize;
CGPoint textOrigin = CGPointMake(textCellData.cellLayout.bubbleInsets.left,
textCellData.cellLayout.bubbleInsets.top);
textCellData.textOrigin = textOrigin;
CGFloat height = contentSize.height;
CGFloat width = contentSize.width;
height += textCellData.cellLayout.bubbleInsets.top;
height += textCellData.cellLayout.bubbleInsets.bottom;
width += textCellData.cellLayout.bubbleInsets.left;
width += textCellData.cellLayout.bubbleInsets.right;
if (textCellData.direction == MsgDirectionIncoming) {
height = MAX(height, TUIBubbleMessageCell.incommingBubble.size.height);
} else {
height = MAX(height, TUIBubbleMessageCell.outgoingBubble.size.height);
}
BOOL hasRiskContent = textCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
width = MAX(width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
height += kTUISecurityStrikeViewTopLineMargin;
height += kTUISecurityStrikeViewTopLineToBottom;
}
CGSize size = CGSizeMake(width, height);
return size;
}
@end
@implementation TUITextMessageCell (TUILayoutConfiguration)
+ (void)initialize {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
}
static UIColor *gOutgoingTextColor;
+ (UIColor *)outgoingTextColor {
if (!gOutgoingTextColor) {
gOutgoingTextColor = TUIChatDynamicColor(@"chat_text_message_send_text_color", @"#000000");
}
return gOutgoingTextColor;
}
+ (void)setOutgoingTextColor:(UIColor *)outgoingTextColor {
gOutgoingTextColor = outgoingTextColor;
}
static UIFont *gOutgoingTextFont;
+ (UIFont *)outgoingTextFont {
if (!gOutgoingTextFont) {
gOutgoingTextFont = [UIFont systemFontOfSize:16];
}
return gOutgoingTextFont;
}
+ (void)setOutgoingTextFont:(UIFont *)outgoingTextFont {
gOutgoingTextFont = outgoingTextFont;
}
static UIColor *gIncommingTextColor;
+ (UIColor *)incommingTextColor {
if (!gIncommingTextColor) {
gIncommingTextColor = TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000");
}
return gIncommingTextColor;
}
+ (void)setIncommingTextColor:(UIColor *)incommingTextColor {
gIncommingTextColor = incommingTextColor;
}
static UIFont *gIncommingTextFont;
+ (UIFont *)incommingTextFont {
if (!gIncommingTextFont) {
gIncommingTextFont = [UIFont systemFontOfSize:16];
}
return gIncommingTextFont;
}
+ (void)setIncommingTextFont:(UIFont *)incommingTextFont {
gIncommingTextFont = incommingTextFont;
}
+ (void)onThemeChanged {
gOutgoingTextColor = nil;
gIncommingTextColor = nil;
}
@end

View File

@@ -0,0 +1,20 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUIMessageCell.h>
#import "TUIFaceMessageCellData.h"
@interface TUIFaceMessageCell : TUIBubbleMessageCell
/**
*
* Image view for the resource of emticon
*/
@property(nonatomic, strong) UIImageView *face;
@property TUIFaceMessageCellData *faceData;
- (void)fillWithData:(TUIFaceMessageCellData *)data;
@end

View File

@@ -0,0 +1,100 @@
//
// FaceMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIFaceMessageCell.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIFaceMessageCell ()
@end
@implementation TUIFaceMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_face = [[UIImageView alloc] init];
_face.contentMode = UIViewContentModeScaleAspectFit;
[self.container addSubview:_face];
_face.mm_fill();
_face.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGFloat topMargin = 0;
CGFloat height = self.container.mm_h;
if (self.messageData.messageContainerAppendSize.height > 0) {
topMargin = 10;
CGFloat tagViewTopPadding = 6;
height = self.container.mm_h - topMargin - self.messageData.messageContainerAppendSize.height - tagViewTopPadding;
self.bubbleView.hidden = NO;
} else {
self.bubbleView.hidden = YES;
}
[self.face mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(height);
make.centerX.mas_equalTo(self.container.mas_centerX);
make.top.mas_equalTo(topMargin);
make.width.mas_equalTo(self.container);
}];
}
- (void)fillWithData:(TUIFaceMessageCellData *)data {
// set data
[super fillWithData:data];
self.faceData = data;
UIImage *image = [[TUIImageCache sharedInstance] getFaceFromCache:data.path];
if (!image) {
image = [UIImage imageWithContentsOfFile:TUIChatFaceImagePath(@"ic_unknown_image")];
}
_face.image = image;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIFaceMessageCellData.class], @"data must be kind of TUIFaceMessageCellData");
TUIFaceMessageCellData *faceCellData = (TUIFaceMessageCellData *)data;
UIImage *image = [[TUIImageCache sharedInstance] getFaceFromCache:faceCellData.path];
if (!image) {
image = [UIImage imageWithContentsOfFile:TUIChatFaceImagePath(@"ic_unknown_image")];
}
CGFloat imageHeight = image.size.height;
CGFloat imageWidth = image.size.width;
if (imageHeight > TFaceMessageCell_Image_Height_Max) {
imageHeight = TFaceMessageCell_Image_Height_Max;
imageWidth = image.size.width / image.size.height * imageHeight;
}
if (imageWidth > TFaceMessageCell_Image_Width_Max) {
imageWidth = TFaceMessageCell_Image_Width_Max;
imageHeight = image.size.height / image.size.width * imageWidth;
}
return CGSizeMake(imageWidth, imageHeight);
}
@end

View File

@@ -0,0 +1,36 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUIMessageCell.h>
#import "TUIFileMessageCellData.h"
@interface TUIFileMessageCell : TUIMessageCell
/**
* File bubble view, used to wrap messages on the UI
*/
@property(nonatomic, strong) UIImageView *bubble;
/**
* Label for displaying filename
* As the main label of the file message, it displays the file information (including the suffix).
*/
@property(nonatomic, strong) UILabel *fileName;
/**
* Label for displaying file size
* As the secondary label of the file message, it further displays the secondary information of the file.
*/
@property(nonatomic, strong) UILabel *length;
/**
* File icon
*/
@property(nonatomic, strong) UIImageView *image;
@property TUIFileMessageCellData *fileData;
- (void)fillWithData:(TUIFileMessageCellData *)data;
@end

View File

@@ -0,0 +1,425 @@
//
// TFileMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIFileMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import "ReactiveObjC/ReactiveObjC.h"
#import "TUIMessageProgressManager.h"
#import <TUICore/TUICore.h>
@interface TUIFileMessageCell () <V2TIMSDKListener, TUIMessageProgressManagerDelegate>
@property(nonatomic, strong) CAShapeLayer *maskLayer;
@property(nonatomic, strong) CAShapeLayer *borderLayer;
@property(nonatomic, strong) UIView *progressView;
@property(nonatomic, strong) UIView *fileContainer;
@property(nonatomic, strong) UIView *animateHighlightView;
@end
@implementation TUIFileMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_bubble = [[UIImageView alloc] initWithFrame:self.container.bounds];
[self.container addSubview:_bubble];
_bubble.hidden = YES;
_bubble.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.securityStrikeView = [[TUISecurityStrikeView alloc] init];
[self.container addSubview:self.securityStrikeView];
[self.container addSubview:self.fileContainer];
self.fileContainer.backgroundColor = TUIChatDynamicColor(@"chat_file_message_bg_color", @"#FFFFFF");
[self.fileContainer addSubview:self.progressView];
_fileName = [[UILabel alloc] init];
_fileName.font = [UIFont boldSystemFontOfSize:15];
_fileName.textColor = TUIChatDynamicColor(@"chat_file_message_title_color", @"#000000");
[self.fileContainer addSubview:_fileName];
_length = [[UILabel alloc] init];
_length.font = [UIFont systemFontOfSize:12];
_length.textColor = TUIChatDynamicColor(@"chat_file_message_subtitle_color", @"#888888");
[self.fileContainer addSubview:_length];
_image = [[UIImageView alloc] init];
_image.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"msg_file_p")];
_image.contentMode = UIViewContentModeScaleAspectFit;
[self.fileContainer addSubview:_image];
[self.fileContainer.layer insertSublayer:self.borderLayer atIndex:0];
[self.fileContainer.layer setMask:self.maskLayer];
[V2TIMManager.sharedInstance addIMSDKListener:self];
[TUIMessageProgressManager.shareManager addDelegate:self];
}
return self;
}
- (void)fillWithData:(TUIFileMessageCellData *)data {
// set data
[super fillWithData:data];
self.fileData = data;
_fileName.text = data.fileName;
_length.text = [self formatLength:data.length];
_image.image = [[TUIImageCache sharedInstance] getResourceFromCache:[self getImagePathByCurrentFileType:data.fileName.pathExtension]];
@weakify(self);
[self prepareReactTagUI:self.container];
self.securityStrikeView.hidden = YES;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent) {
self.bubble.image = [self getErrorBubble];
self.securityStrikeView.hidden = NO;
self.readReceiptLabel.hidden = YES;
self.retryView.hidden = NO;
}
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
NSInteger uploadProgress = [TUIMessageProgressManager.shareManager uploadProgressForMessage:self.fileData.msgID];
NSInteger downloadProgress = [TUIMessageProgressManager.shareManager downloadProgressForMessage:self.fileData.msgID];
[self onUploadProgress:self.fileData.msgID progress:uploadProgress];
[self onDownloadProgress:self.fileData.msgID progress:downloadProgress];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
});
}
- (UIImage *)getErrorBubble {
if (self.messageData.direction == MsgDirectionIncoming) {
return TUIBubbleMessageCell.incommingErrorBubble;
} else {
return TUIBubbleMessageCell.outgoingErrorBubble;
}
}
#pragma mark - TUIMessageProgressManagerDelegate
- (void)onUploadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![msgID isEqualToString:self.fileData.msgID]) {
return;
}
self.fileData.uploadProgress = progress;
[self updateUploadProgress:(int)progress];
}
- (void)onDownloadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![msgID isEqualToString:self.fileData.msgID]) {
return;
}
self.fileData.downladProgress = progress;
[self updateDownloadProgress:(int)progress];
}
- (void)updateUploadProgress:(int)progress {
[self.indicator startAnimating];
self.progressView.hidden = YES;
self.length.text = [self formatLength:self.fileData.length];
NSLog(@"updateProgress:%ld,isLocalExist:%@,isDownloading:%@", (long)progress, self.fileData.isLocalExist ? @"YES" : @"NO",
self.fileData.isDownloading ? @"YES" : @"NO");
if (progress >= 100 || progress == 0) {
[self.indicator stopAnimating];
return;
}
[self showProgressLodingAnimation:progress];
}
- (void)updateDownloadProgress:(int)progress {
[self.indicator startAnimating];
self.progressView.hidden = YES;
self.length.text = [self formatLength:self.fileData.length];
if (progress >= 100 || progress == 0) {
[self.indicator stopAnimating];
return;
}
[self showProgressLodingAnimation:progress];
}
- (void)showProgressLodingAnimation:(NSInteger)progress {
self.progressView.hidden = NO;
NSLog(@"showProgressLodingAnimation:%ld", (long)progress);
[UIView animateWithDuration:0.25
animations:^{
[self.progressView mas_updateConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(self.fileContainer.mm_w * progress / 100.0);
}];
}
completion:^(BOOL finished) {
if (progress == 0 || progress >= 100) {
self.progressView.hidden = YES;
[self.indicator stopAnimating];
self.length.text = [self formatLength:self.fileData.length];
}
}];
self.length.text = [self formatLength:self.fileData.length];
}
- (NSString *)formatLength:(long)length {
/**
*
* Display file size by default
*/
double len = length;
NSArray *array = [NSArray arrayWithObjects:@"Bytes", @"K", @"M", @"G", @"T", nil];
int factor = 0;
while (len > 1024) {
len /= 1024;
factor++;
if (factor >= 4) {
break;
}
}
NSString *str = [NSString stringWithFormat:@"%4.2f%@", len, array[factor]];
/**
*
* Formatted display characters
*/
if (self.fileData.direction == MsgDirectionOutgoing) {
if (length == 0 && (self.fileData.status == Msg_Status_Sending || self.fileData.status == Msg_Status_Sending_2)) {
str = [NSString
stringWithFormat:@"%zd%%", self.fileData.direction == MsgDirectionIncoming ? self.fileData.downladProgress : self.fileData.uploadProgress];
}
} else {
if (!self.fileData.isLocalExist && !self.fileData.isDownloading) {
str = [NSString stringWithFormat:@"%@ %@", str, TIMCommonLocalizableString(TUIKitNotDownload)];
}
}
return str;
}
- (NSString *)getImagePathByCurrentFileType:(NSString *)pathExtension {
if (pathExtension.length > 0) {
if ([pathExtension hasSuffix:@"ppt"] || [pathExtension hasSuffix:@"key"] || [pathExtension hasSuffix:@"pdf"]) {
return TUIChatImagePath(@"msg_file_p");
}
}
return TUIChatImagePath(@"msg_file");
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGSize containerSize = [self.class getContentSize:self.fileData];
CGSize fileContainerSize = [self.class getFileContentSize:self.fileData];
[self.fileContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.container);
make.size.mas_equalTo(fileContainerSize);
}];
CGFloat imageHeight = fileContainerSize.height - 2 * TFileMessageCell_Margin;
CGFloat imageWidth = imageHeight;
[self.image mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.fileContainer.mas_leading).mas_offset(TFileMessageCell_Margin);
make.top.mas_equalTo(self.fileContainer.mas_top).mas_offset(TFileMessageCell_Margin);
make.size.mas_equalTo(CGSizeMake(imageWidth, imageHeight));
}];
CGFloat textWidth = fileContainerSize.width - 2 * TFileMessageCell_Margin - imageWidth;
CGSize nameSize = [_fileName sizeThatFits:fileContainerSize];
[self.fileName mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.image.mas_trailing).mas_offset(TFileMessageCell_Margin);
make.top.mas_equalTo(self.image);
make.size.mas_equalTo(CGSizeMake(textWidth, nameSize.height));
}];
CGSize lengthSize = [_length sizeThatFits:fileContainerSize];
[self.length mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.fileName);
make.top.mas_equalTo(self.fileName.mas_bottom).mas_offset(TFileMessageCell_Margin * 0.5);
make.size.mas_equalTo(CGSizeMake(textWidth, nameSize.height));
}];
if (self.messageData.messageContainerAppendSize.height > 0) {
[self.fileContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.container);
make.size.mas_equalTo(self.container);
}];
self.bubble.hidden = NO;
}
self.maskLayer.frame = self.fileContainer.bounds;
self.borderLayer.frame = self.fileContainer.bounds;
UIRectCorner corner = UIRectCornerBottomLeft | UIRectCornerBottomRight | UIRectCornerTopLeft;
if (self.fileData.direction == MsgDirectionIncoming) {
corner = UIRectCornerBottomLeft | UIRectCornerBottomRight | UIRectCornerTopRight;
}
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:self.fileContainer.bounds byRoundingCorners:corner cornerRadii:CGSizeMake(10, 10)];
self.maskLayer.path = bezierPath.CGPath;
self.borderLayer.path = bezierPath.CGPath;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.fileContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.container).mas_offset(13);
make.leading.mas_equalTo(12);
make.size.mas_equalTo(fileContainerSize);
}];
[self.bubble mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.size.mas_equalTo(self.container);
make.top.mas_equalTo(self.container);
}];
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.fileContainer.mas_bottom);
make.width.mas_equalTo(self.container);
make.bottom.mas_equalTo(self.container).mas_offset(-self.messageData.messageContainerAppendSize.height);
}];
self.bubble.hidden = NO;
}
else {
self.bubble.hidden = YES;
}
[self.progressView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(0);
make.width.mas_equalTo(self.progressView.mm_w ?: 1);
make.height.mas_equalTo(self.fileContainer.mm_h);
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (CAShapeLayer *)maskLayer {
if (_maskLayer == nil) {
_maskLayer = [CAShapeLayer layer];
}
return _maskLayer;
}
- (CAShapeLayer *)borderLayer {
if (_borderLayer == nil) {
_borderLayer = [CAShapeLayer layer];
_borderLayer.lineWidth = 0.5f;
_borderLayer.strokeColor = [UIColor colorWithRed:221 / 255.0 green:221 / 255.0 blue:221 / 255.0 alpha:1.0].CGColor;
_borderLayer.fillColor = [UIColor clearColor].CGColor;
}
return _borderLayer;
}
- (UIView *)progressView {
if (_progressView == nil) {
_progressView = [[UIView alloc] init];
_progressView.backgroundColor = [UIColor colorWithRed:208 / 255.0 green:228 / 255.0 blue:255 / 255.0 alpha:1 / 1.0];
}
return _progressView;
}
- (UIView *)fileContainer {
if (_fileContainer == nil) {
_fileContainer = [[UIView alloc] init];
_fileContainer.backgroundColor = TUIChatDynamicColor(@"chat_file_message_bg_color", @"#FFFFFF");
}
return _fileContainer;
}
- (void)onConnectSuccess {
[self fillWithData:self.fileData];
}
- (void)highlightWhenMatchKeyword:(NSString *)keyword {
if (keyword) {
if (self.highlightAnimating) {
return;
}
[self animate:3];
}
}
- (void)animate:(int)times {
times--;
if (times < 0) {
[self.animateHighlightView removeFromSuperview];
self.highlightAnimating = NO;
return;
}
self.highlightAnimating = YES;
self.animateHighlightView.frame = self.container.bounds;
self.animateHighlightView.alpha = 0.1;
[self.fileContainer addSubview:self.animateHighlightView];
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.5;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.1;
}
completion:^(BOOL finished) {
if (!self.messageData.highlightKeyword) {
[self animate:0];
return;
}
[self animate:times];
}];
}];
}
- (UIView *)animateHighlightView {
if (_animateHighlightView == nil) {
_animateHighlightView = [[UIView alloc] init];
_animateHighlightView.backgroundColor = [UIColor orangeColor];
}
return _animateHighlightView;
}
- (void)prepareReactTagUI:(UIView *)containerView {
NSDictionary *param = @{TUICore_TUIChatExtension_ChatMessageReactPreview_Delegate: self};
[TUICore raiseExtension:TUICore_TUIChatExtension_ChatMessageReactPreview_ClassicExtensionID parentView:containerView param:param];
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getFileContentSize:(TUIMessageCellData *)data {
BOOL hasRiskContent = data.innerMessage.hasRiskContent;
if (hasRiskContent) {
return CGSizeMake(237, 62);
}
return TFileMessageCell_Container_Size;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
CGSize size = [self.class getFileContentSize:data];
BOOL hasRiskContent = data.innerMessage.hasRiskContent;
if (hasRiskContent) {
CGFloat bubbleTopMargin = 12;
CGFloat bubbleBottomMargin = 12;
size.width = MAX(size.width, 261);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
size.height += bubbleTopMargin;
size.height += kTUISecurityStrikeViewTopLineMargin;
size.height += kTUISecurityStrikeViewTopLineToBottom;
size.height += bubbleBottomMargin;
}
return size;
}
@end

View File

@@ -0,0 +1,18 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUIImageMessageCellData.h"
#import "TUIMediaCollectionCell.h"
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMediaImageCell
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIImageCollectionCell : TUIMediaCollectionCell
- (void)fillWithData:(TUIImageMessageCellData *)data;
- (void)reloadAllView;
@end

View File

@@ -0,0 +1,423 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIImageCollectionCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUITool.h>
#import "TUICircleLodingView.h"
@interface TUIImageCollectionCellScrollView : UIScrollView <UIScrollViewDelegate>
@property(nonatomic, strong) UIView *containerView;
@property(assign, nonatomic) CGFloat imageNormalWidth;
@property(assign, nonatomic) CGFloat imageNormalHeight;
- (void)pictureZoomWithScale:(CGFloat)zoomScale;
@end
@implementation TUIImageCollectionCellScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.delegate = self;
self.minimumZoomScale = 0.1f;
self.maximumZoomScale = 2.0f;
_imageNormalHeight = frame.size.height;
_imageNormalWidth = frame.size.width;
self.containerView = [[UIView alloc] initWithFrame:frame];
[self addSubview:self.containerView];
if (@available(iOS 11.0, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
// Fallback on earlier versions
}
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
}
#pragma mark-- Help Methods
- (void)pictureZoomWithScale:(CGFloat)zoomScale {
CGFloat imageScaleWidth = zoomScale * self.imageNormalWidth;
CGFloat imageScaleHeight = zoomScale * self.imageNormalHeight;
CGFloat imageX = 0;
CGFloat imageY = 0;
if (imageScaleWidth < self.frame.size.width) {
imageX = floorf((self.frame.size.width - imageScaleWidth) / 2.0);
}
if (imageScaleHeight < self.frame.size.height) {
imageY = floorf((self.frame.size.height - imageScaleHeight) / 2.0);
}
self.containerView.frame = CGRectMake(imageX, imageY, imageScaleWidth, imageScaleHeight);
self.contentSize = CGSizeMake(imageScaleWidth, imageScaleHeight);
}
#pragma mark-- Setter
- (void)setImageNormalWidth:(CGFloat)imageNormalWidth {
_imageNormalWidth = imageNormalWidth;
self.containerView.frame = CGRectMake(0, 0, _imageNormalWidth, _imageNormalHeight);
self.containerView.center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
}
- (void)setImageNormalHeight:(CGFloat)imageNormalHeight {
_imageNormalHeight = imageNormalHeight;
self.containerView.frame = CGRectMake(0, 0, _imageNormalWidth, _imageNormalHeight);
self.containerView.center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
}
#pragma mark-- UIScrollViewDelegate
// Returns the view control that needs to be zoomed. During zooming
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.containerView;
}
// BeginZooming
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
NSLog(@"BeginZooming");
}
// EndZooming
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(@"EndZooming");
}
// zoom
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
CGFloat imageScaleWidth = scrollView.zoomScale * self.imageNormalWidth;
CGFloat imageScaleHeight = scrollView.zoomScale * self.imageNormalHeight;
CGFloat imageX = 0;
CGFloat imageY = 0;
if (imageScaleWidth < self.frame.size.width) {
imageX = floorf((self.frame.size.width - imageScaleWidth) / 2.0);
}
if (imageScaleHeight < self.frame.size.height) {
imageY = floorf((self.frame.size.height - imageScaleHeight) / 2.0);
}
self.containerView.frame = CGRectMake(imageX, imageY, imageScaleWidth, imageScaleHeight);
}
@end
@interface TUIImageCollectionCell ()
@property(nonatomic, strong) TUIImageCollectionCellScrollView *scrollView;
@property(nonatomic, strong) TUIImageMessageCellData *imgCellData;
@property(nonatomic, strong) UIButton *mainDownloadBtn;
@property(nonatomic, strong) TUICircleLodingView *animateCircleView;
@end
@implementation TUIImageCollectionCell
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self setupRotaionNotifications];
}
return self;
}
- (void)setupViews {
self.scrollView = [[TUIImageCollectionCellScrollView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
[self addSubview:self.scrollView];
self.imageView = [[UIImageView alloc] init];
self.imageView.layer.cornerRadius = 5.0;
[self.imageView.layer setMasksToBounds:YES];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.backgroundColor = [UIColor clearColor];
[self.scrollView.containerView addSubview:self.imageView];
self.imageView.mm_fill();
self.imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.mainDownloadBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.mainDownloadBtn.contentMode = UIViewContentModeScaleToFill;
[self.mainDownloadBtn setTitle:TIMCommonLocalizableString(TUIKitImageViewOrigin) forState:UIControlStateNormal];
self.mainDownloadBtn.backgroundColor = [UIColor grayColor];
[self.mainDownloadBtn.titleLabel setFont:[UIFont systemFontOfSize:14]];
self.mainDownloadBtn.layer.borderColor = [UIColor whiteColor].CGColor;
self.mainDownloadBtn.layer.cornerRadius = .16;
self.mainDownloadBtn.hidden = YES;
[self.mainDownloadBtn addTarget:self action:@selector(mainDownloadBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.mainDownloadBtn];
self.downloadBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.downloadBtn.contentMode = UIViewContentModeScaleToFill;
[self.downloadBtn setImage:TUIChatCommonBundleImage(@"download") forState:UIControlStateNormal];
[self.downloadBtn addTarget:self action:@selector(onSaveBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.downloadBtn];
self.animateCircleView = [[TUICircleLodingView alloc] initWithFrame:CGRectMake(0, 0, kScale390(40), kScale390(40))];
self.animateCircleView.hidden = YES;
self.animateCircleView.progress = 0;
[self addSubview:_animateCircleView];
self.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMedia)];
[self addGestureRecognizer:tap];
}
- (void)setupRotaionNotifications {
if (@available(iOS 16.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:TUIMessageMediaViewDeviceOrientationChangeNotification
object:nil];
} else {
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
}
- (void)mainDownloadBtnClick {
if (self.imgCellData.originImage == nil) {
[self.imgCellData downloadImage:TImage_Type_Origin];
}
}
- (void)onSaveBtnClick {
UIImage *image = self.imageView.image;
[[PHPhotoLibrary sharedPhotoLibrary]
performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
}
completionHandler:^(BOOL success, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitPictureSavedSuccess)];
} else {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitPictureSavedFailed)];
}
});
}];
}
- (void)onSelectMedia {
if (self.delegate && [self.delegate respondsToSelector:@selector(onCloseMedia:)]) {
[self.delegate onCloseMedia:self];
}
}
- (void)fillWithData:(TUIImageMessageCellData *)data;
{
[super fillWithData:data];
self.imgCellData = data;
self.imageView.image = nil;
BOOL hasRiskContent = data.innerMessage.hasRiskContent;
if (hasRiskContent ) {
self.imageView.image = TIMCommonBundleThemeImage(@"", @"icon_security_strike");
for (UIView *subview in self.subviews) {
if (subview != self.scrollView ){
subview.hidden = YES;
}
}
return;
}
//1.Read from cache
if ([self originImageFirst:data]) {
return;
}
if ([self largeImageSecond:data]) {
return;
}
//2. download image
if (data.thumbImage == nil) {
[data downloadImage:TImage_Type_Thumb];
}
if (data.thumbImage && data.largeImage == nil) {
self.animateCircleView.hidden = NO;
[data downloadImage:TImage_Type_Large];
}
[self fillThumbImageWithData:data];
[self fillLargeImageWithData:data];
[self fillOriginImageWithData:data];
}
- (BOOL)largeImageSecond:(TUIImageMessageCellData *)data {
BOOL isExist = NO;
NSString *path = [data getImagePath:TImage_Type_Large isExist:&isExist];
if (isExist) {
[data decodeImage:TImage_Type_Large];
[self fillLargeImageWithData:data];
}
return isExist;
}
- (BOOL)originImageFirst:(TUIImageMessageCellData *)data {
BOOL isExist = NO;
NSString *path = [data getImagePath:TImage_Type_Origin isExist:&isExist];
if (isExist) {
[data decodeImage:TImage_Type_Origin];
[self fillOriginImageWithData:data];
}
return isExist;
}
- (void)fillOriginImageWithData:(TUIImageMessageCellData *)data{
@weakify(self);
// originImage
[[RACObserve(data, originImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *originImage) {
@strongify(self);
if (originImage) {
self.imageView.image = originImage;
[self setNeedsLayout];
}
}];
[[[RACObserve(data, originProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
if (progress == 100) {
self.animateCircleView.progress = 99;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 100;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 0;
self.mainDownloadBtn.hidden = YES;
self.animateCircleView.hidden = YES;
[self.mainDownloadBtn setTitle:TIMCommonLocalizableString(TUIKitImageViewOrigin) forState:UIControlStateNormal];
});
});
} else if (progress > 1 && progress < 100) {
self.animateCircleView.progress = progress;
[self.mainDownloadBtn setTitle:[NSString stringWithFormat:@"%d%%", progress] forState:UIControlStateNormal];
self.animateCircleView.hidden = YES;
} else {
self.animateCircleView.progress = progress;
}
}];
}
- (void)fillLargeImageWithData:(TUIImageMessageCellData *)data {
@weakify(self);
// largeImage
[[RACObserve(data, largeImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *largeImage) {
@strongify(self);
if (largeImage) {
self.imageView.image = largeImage;
[self setNeedsLayout];
}
}];
[[[RACObserve(data, largeProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
if (progress == 100) {
self.animateCircleView.progress = 99;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 100;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 0;
self.mainDownloadBtn.hidden = NO;
self.animateCircleView.hidden = YES;
});
});
} else if (progress > 1 && progress < 100) {
self.animateCircleView.progress = progress;
self.mainDownloadBtn.hidden = YES;
self.animateCircleView.hidden = NO;
} else {
self.animateCircleView.progress = progress;
}
}];
}
- (void)fillThumbImageWithData:(TUIImageMessageCellData *)data {
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.imageView.image = thumbImage;
[self setNeedsLayout];
}
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self.mainDownloadBtn sizeToFit];
self.mainDownloadBtn.mm_width(self.mainDownloadBtn.mm_w + 10).mm_height(self.mainDownloadBtn.mm_h).mm__centerX(self.mm_w / 2).mm_bottom(48);
self.mainDownloadBtn.layer.cornerRadius = (self.mainDownloadBtn.mm_h * 0.5);
self.animateCircleView.tui_mm_center();
self.downloadBtn.mm_width(31).mm_height(31).mm_right(16).mm_bottom(48);
self.scrollView.mm_width(self.mm_w).mm_height(self.mm_h).mm__centerX(self.mm_w / 2).mm__centerY(self.mm_h / 2);
self.scrollView.imageNormalWidth = self.imageView.image.size.width;
self.scrollView.imageNormalHeight = self.imageView.image.size.height;
self.imageView.frame = CGRectMake(self.scrollView.bounds.origin.x,
self.scrollView.bounds.origin.y,
self.imageView.image.size.width,
self.imageView.image.size.height);
[self.imageView layoutIfNeeded];
[self adjustScale];
}
- (void)onDeviceOrientationChange:(NSNotification *)noti {
[self reloadAllView];
}
- (void)reloadAllView {
for (UIView *subview in self.subviews) {
if (subview) {
[UIView animateWithDuration:0.1 animations:^{
[subview removeFromSuperview];
}];
}
}
[self setupViews];
[self fillWithData:self.imgCellData];
}
#pragma mark - V2TIMAdvancedMsgListener
- (void)onRecvMessageModified:(V2TIMMessage *)msg {
V2TIMMessage *imMsg = msg;
if (imMsg == nil || ![imMsg isKindOfClass:V2TIMMessage.class]) {
return;
}
if ([self.imgCellData.innerMessage.msgID isEqualToString:imMsg.msgID]) {
BOOL hasRiskContent = imMsg.hasRiskContent;
if (hasRiskContent) {
self.imgCellData.innerMessage = imMsg;
[self showRiskAlert];
}
}
}
- (void)showRiskAlert {
UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil
message:TIMCommonLocalizableString(TUIKitPictureCheckRisk)
preferredStyle:UIAlertControllerStyleAlert];
__weak typeof(self) weakSelf = self;
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitVideoCheckRiskCancel)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *_Nonnull action) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf reloadAllView];
}]];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:ac animated:YES completion:nil];
}
- (void)adjustScale {
CGFloat scale = 1;
if (Screen_Width > self.imageView.image.size.width) {
scale = 1;
CGFloat scaleHeight = Screen_Height/ self.imageView.image.size.height;
scale = MIN(scale, scaleHeight);
}
else {
scale = Screen_Width/ self.imageView.image.size.width;
CGFloat scaleHeight = Screen_Height/ self.imageView.image.size.height;
scale = MIN(scale, scaleHeight);
}
self.scrollView.containerView.frame = CGRectMake(0, 0,MIN(Screen_Width,self.imageView.image.size.width), self.imageView.image.size.height);
[self.scrollView pictureZoomWithScale:scale];
}
@end

View File

@@ -0,0 +1,17 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUIMessageCell.h>
#import "TUIImageMessageCellData.h"
@interface TUIImageMessageCell : TUIBubbleMessageCell
@property(nonatomic, strong) UIImageView *thumb;
@property(nonatomic, strong) UILabel *progress;
@property TUIImageMessageCellData *imageData;
- (void)fillWithData:(TUIImageMessageCellData *)data;
@end

View File

@@ -0,0 +1,300 @@
//
// TUIImageMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIImageMessageCell.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIImageMessageCell ()
@property(nonatomic, strong) UIView *animateHighlightView;
@end
@implementation TUIImageMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_thumb = [[UIImageView alloc] init];
_thumb.layer.cornerRadius = 5.0;
[_thumb.layer setMasksToBounds:YES];
_thumb.contentMode = UIViewContentModeScaleAspectFit;
_thumb.backgroundColor = [UIColor clearColor];
[self.container addSubview:_thumb];
_progress = [[UILabel alloc] init];
_progress.textColor = [UIColor whiteColor];
_progress.font = [UIFont systemFontOfSize:15];
_progress.textAlignment = NSTextAlignmentCenter;
_progress.layer.cornerRadius = 5.0;
_progress.hidden = YES;
_progress.backgroundColor = TImageMessageCell_Progress_Color;
[_progress.layer setMasksToBounds:YES];
[self.container addSubview:_progress];
[self makeConstraints];
}
return self;
}
- (void)fillWithData:(TUIImageMessageCellData *)data;
{
// set data
[super fillWithData:data];
self.imageData = data;
_thumb.image = nil;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent) {
self.thumb.image = TIMCommonBundleThemeImage(@"", @"icon_security_strike");
self.securityStrikeView.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeImage);
self.progress.hidden = YES;
return;
}
if (data.thumbImage == nil) {
[data downloadImage:TImage_Type_Thumb];
}
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.thumb.image = thumbImage;
}
}];
[[[RACObserve(data, thumbProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
self.progress.text = [NSString stringWithFormat:@"%d%%", progress];
self.progress.hidden = (progress >= 100 || progress == 0);
}];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)makeConstraints {
[self.thumb mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(self.bubbleView);
make.width.mas_equalTo(self.bubbleView);
make.top.mas_equalTo(self.bubbleView);
make.leading.mas_equalTo(self.bubbleView);
}];
[self.progress mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.bubbleView);
}];
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
if (self.imageData.isSuperLongImage) {
self.thumb.contentMode = UIViewContentModeScaleToFill;
}
else {
self.thumb.contentMode = UIViewContentModeScaleAspectFit;
}
CGFloat topMargin = 0;
CGFloat height = self.bubbleView.mm_h;
if (self.messageData.messageContainerAppendSize.height > 0) {
topMargin = 10;
CGFloat tagViewTopMargin = 6;
height = self.bubbleView.mm_h - topMargin - self.messageData.messageContainerAppendSize.height - tagViewTopMargin;
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(height);
make.width.mas_equalTo(self.bubbleView.mas_width);
make.top.mas_equalTo(self.bubbleView).mas_offset(topMargin);
make.leading.mas_equalTo(self.bubbleView);
}];
} else {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.top);
make.bottom.mas_equalTo(self.bubbleView).mas_offset(-self.messageData.cellLayout.bubbleInsets.bottom);
make.leading.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.left);
make.trailing.mas_equalTo(self.bubbleView).mas_offset(-self.messageData.cellLayout.bubbleInsets.right);
}];
}
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView).mas_offset(12);
make.size.mas_equalTo(CGSizeMake(150, 150));
make.centerX.mas_equalTo(self.bubbleView);
}];
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.thumb.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
make.bottom.mas_equalTo(self.container).mas_offset(- self.messageData.messageContainerAppendSize.height);
}];
}
[self.progress mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.bubbleView);
}];
[self.selectedView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.contentView);
}];
[self.selectedIcon mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentView.mas_leading).mas_offset(3);
make.top.mas_equalTo(self.avatarView.mas_centerY).mas_offset(-10);
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_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.contentView.mas_trailing).mas_offset(-10);
make.top.mas_equalTo(self.avatarView);
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);
}
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)highlightWhenMatchKeyword:(NSString *)keyword {
if (keyword) {
if (self.highlightAnimating) {
return;
}
[self animate:3];
}
}
- (void)animate:(int)times {
times--;
if (times < 0) {
[self.animateHighlightView removeFromSuperview];
self.highlightAnimating = NO;
return;
}
self.highlightAnimating = YES;
self.animateHighlightView.frame = self.container.bounds;
self.animateHighlightView.alpha = 0.1;
[self.container addSubview:self.animateHighlightView];
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.5;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.1;
}
completion:^(BOOL finished) {
if (!self.imageData.highlightKeyword) {
[self animate:0];
return;
}
[self animate:times];
}];
}];
}
- (UIView *)animateHighlightView {
if (_animateHighlightView == nil) {
_animateHighlightView = [[UIView alloc] init];
_animateHighlightView.backgroundColor = [UIColor orangeColor];
}
return _animateHighlightView;
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getEstimatedHeight:(TUIMessageCellData *)data {
return 186.f;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIImageMessageCellData.class], @"data must be kind of TUIImageMessageCellData");
TUIImageMessageCellData *imageCellData = (TUIImageMessageCellData *)data;
CGSize size = CGSizeZero;
BOOL isDir = NO;
if (![imageCellData.path isEqualToString:@""] && [[NSFileManager defaultManager] fileExistsAtPath:imageCellData.path isDirectory:&isDir]) {
if (!isDir) {
size = [UIImage imageWithContentsOfFile:imageCellData.path].size;
}
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
for (TUIImageItem *item in imageCellData.items) {
if (item.type == TImage_Type_Thumb) {
size = item.size;
}
}
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
for (TUIImageItem *item in imageCellData.items) {
if (item.type == TImage_Type_Large) {
size = item.size;
}
}
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
for (TUIImageItem *item in imageCellData.items) {
if (item.type == TImage_Type_Origin) {
size = item.size;
}
}
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
return size;
}
if (size.height > size.width) {
if (size.height > 5* size.width) {
size.width = TImageMessageCell_Image_Width_Max;
size.height = TImageMessageCell_Image_Height_Max;
imageCellData.isSuperLongImage = YES;
}
else {
size.width = size.width / size.height * TImageMessageCell_Image_Height_Max;
size.height = TImageMessageCell_Image_Height_Max;
}
} else {
size.height = size.height / size.width * TImageMessageCell_Image_Width_Max;
size.width = TImageMessageCell_Image_Width_Max;
}
BOOL hasRiskContent = imageCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
CGFloat bubbleTopMargin = 12;
CGFloat bubbleBottomMargin = 12;
size.height = MAX(size.height, 150);// width must more than TIMCommonBundleThemeImage(@"", @"icon_security_strike");
size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
size.height += bubbleTopMargin;
size.height += kTUISecurityStrikeViewTopLineMargin;
size.height += kTUISecurityStrikeViewTopLineToBottom;
size.height += bubbleBottomMargin;
}
return size;
}
@end

View File

@@ -0,0 +1,30 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUISystemMessageCell.h>
#import "TUIJoinGroupMessageCellData.h"
NS_ASSUME_NONNULL_BEGIN
@class TUIJoinGroupMessageCell;
@protocol TUIJoinGroupMessageCellDelegate <NSObject>
@optional
- (void)didTapOnNameLabel:(TUIJoinGroupMessageCell *)cell;
- (void)didTapOnSecondNameLabel:(TUIJoinGroupMessageCell *)cell;
- (void)didTapOnRestNameLabel:(TUIJoinGroupMessageCell *)cell withIndex:(NSInteger)index;
@end
@interface TUIJoinGroupMessageCell : TUISystemMessageCell
@property TUIJoinGroupMessageCellData *joinData;
@property(nonatomic, weak) id<TUIJoinGroupMessageCellDelegate> joinGroupDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,145 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIJoinGroupMessageCell.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIJoinGroupMessageCell () <UITextViewDelegate>
@property(nonatomic, strong) UITextView *textView;
@end
@implementation TUIJoinGroupMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_textView = [[UITextView alloc] init];
_textView.editable = NO;
_textView.scrollEnabled = NO;
_textView.backgroundColor = [UIColor clearColor];
_textView.textColor = [UIColor d_systemGrayColor];
_textView.textContainerInset = UIEdgeInsetsMake(5, 0, 5, 0);
_textView.layer.cornerRadius = 3;
_textView.delegate = self;
_textView.textAlignment = NSTextAlignmentLeft;
[self.messageLabel removeFromSuperview];
[self.container addSubview:_textView];
_textView.delaysContentTouches = NO;
}
return self;
}
- (void)fillWithData:(TUIJoinGroupMessageCellData *)data;
{
[super fillWithData:data];
self.joinData = data;
self.nameLabel.hidden = YES;
self.avatarView.hidden = YES;
self.retryView.hidden = YES;
[self.indicator stopAnimating];
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", data.content]];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary *attributeDict = @{
NSFontAttributeName : self.messageLabel.font,
NSForegroundColorAttributeName : [UIColor d_systemGrayColor],
NSParagraphStyleAttributeName : paragraphStyle
};
[attributeString setAttributes:attributeDict range:NSMakeRange(0, attributeString.length)];
if (data.userNameList.count > 0) {
NSArray *nameRangeList = [self findRightRangeOfAllString:data.userNameList inText:attributeString.string];
int i = 0;
for (i = 0; i < nameRangeList.count; i++) {
NSString *nameRangeString = nameRangeList[i];
NSRange nameRange = NSRangeFromString(nameRangeString);
[attributeString addAttribute:NSLinkAttributeName value:[NSString stringWithFormat:@"%d", i] range:nameRange];
}
}
self.textView.attributedText = attributeString;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (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);
}];
if(self.textView.superview) {
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.container);
make.size.mas_equalTo(self.contentView);
}];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
self.container.tui_mm_center();
self.textView.mm_fill();
}
- (void)onSelectUserName:(NSInteger)index {
if (self.joinGroupDelegate && [self.joinGroupDelegate respondsToSelector:@selector(didTapOnRestNameLabel:withIndex:)])
[self.joinGroupDelegate didTapOnRestNameLabel:self withIndex:index];
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
NSArray *userNames = _joinData.userNameList;
NSURL *urlRecognizer = [[NSURL alloc] init];
for (int i = 0; i < userNames.count; i++) {
urlRecognizer = [NSURL URLWithString:[NSString stringWithFormat:@"%d", i]];
if ([URL isEqual:urlRecognizer]) {
[self onSelectUserName:i];
}
}
return NO;
}
/**
* To obtain the exact position of the nickname in the text content, the following properties are used: the storage order of userName in the array must be the
* same as the order in which the final text is displayed. For example: the text content is, "A invited B, C, D to join the group", then the storage order of
* the elements in userName must be ABCD. Therefore, the method of "searching from the beginning and searching in succession" is used. For example, find the
* first element A first, because of the characteristics of rangeOfString, it must find the A at the head position. After finding A at the head position, we
* remove A from the search range, and the search range becomes "B, C, D are invited to join the group", and then continue to search for the next element, which
* is B.
*/
- (NSMutableArray *)findRightRangeOfAllString:(NSMutableArray<NSString *> *)stringList inText:(NSString *)text {
NSMutableArray *rangeList = [NSMutableArray array];
NSUInteger beginLocation = 0;
NSEnumerator *enumer = [stringList objectEnumerator];
NSString *string = [NSString string];
while (string = [enumer nextObject]) {
NSRange newRange = NSMakeRange(beginLocation, text.length - beginLocation);
NSRange stringRange = [text rangeOfString:string options:NSLiteralSearch range:newRange];
if (stringRange.length > 0) {
[rangeList addObject:NSStringFromRange(stringRange)];
beginLocation = stringRange.location + stringRange.length;
}
}
return rangeList;
}
@end

View File

@@ -0,0 +1,39 @@
//
// TUIMediaCollectionCell.h
// TUIChat
//
// Created by xiangzhang on 2021/11/22.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Photos/Photos.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <UIKit/UIKit.h>
@class TUIMediaCollectionCell;
NS_ASSUME_NONNULL_BEGIN
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMediaCollectionCellDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIMediaCollectionCellDelegate <NSObject>
/**
* meida cell
*/
- (void)onCloseMedia:(TUIMediaCollectionCell *)cell;
@end
@interface TUIMediaCollectionCell : UICollectionViewCell
@property(nonatomic, strong) UIImageView *imageView;
@property(nonatomic, strong) UIButton *downloadBtn;
@property(nonatomic, weak) id<TUIMediaCollectionCellDelegate> delegate;
- (void)fillWithData:(TUIMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,31 @@
//
// TUIMediaCollectionCell.m
// TUIChat
//
// Created by xiangzhang on 2021/11/22.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMediaCollectionCell.h"
@interface TUIMediaCollectionCell()<V2TIMAdvancedMsgListener>
@end
@implementation TUIMediaCollectionCell
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
[self registerTUIKitNotification];
}
return self;
}
- (void)fillWithData:(TUIMessageCellData *)data {
return;
}
- (void)registerTUIKitNotification {
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
}
@end

View File

@@ -0,0 +1,19 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUIMenuCellData.h"
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMenuCell
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIMenuCell : UICollectionViewCell
@property(nonatomic, strong) UIImageView *menu;
- (void)setData:(TUIMenuCellData *)data;
@end

View File

@@ -0,0 +1,46 @@
//
// InputMenuCell.m
// UIKit
//
// Created by kennethmiao on 2018/9/20.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIMenuCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUIMenuCell
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self defaultLayout];
}
return self;
}
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_controller_bg_color", @"#EBF0F6");
_menu = [[UIImageView alloc] init];
_menu.backgroundColor = [UIColor clearColor];
[self addSubview:_menu];
}
- (void)defaultLayout {
}
- (void)setData:(TUIMenuCellData *)data {
// set data
_menu.image = [[TUIImageCache sharedInstance] getFaceFromCache:data.path];
if (data.isSelected) {
self.backgroundColor = TUIChatDynamicColor(@"chat_face_menu_select_color", @"#FFFFFF");
} else {
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
}
// update layout
CGSize size = self.frame.size;
_menu.frame = CGRectMake(TMenuCell_Margin, TMenuCell_Margin, size.width - 2 * TMenuCell_Margin, size.height - 2 * TMenuCell_Margin);
_menu.contentMode = UIViewContentModeScaleAspectFit;
}
@end

View File

@@ -0,0 +1,41 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This document declares the TUIMergeMessageCell class.
* When multiple messages are merged and forwarded, a merged-forward message will be displayed on the chat interface.
*
* When we receive a merged-forward message, it is usually displayed in the chat interface like this:
* | History of vinson and lynx | -- title
* | vinsonWhen will the new version of the SDK be released | -- abstract1
* | lynxPlan for next Monday, the specific time depends on the system test situation in these two days.. | -- abstract2
* | vinsonOkay.
*/
#import <TIMCommon/TUIMessageCell.h>
#import "TUIMergeMessageCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMergeMessageCell : TUIMessageCell
/**
* Title of merged-forward message
*/
@property(nonatomic, strong) UILabel *relayTitleLabel;
/**
* Horizontal dividing line
*/
@property(nonatomic, strong) UIView *separtorView;
/**
* bottom prompt
*/
@property(nonatomic, strong) UILabel *bottomTipsLabel;
@property(nonatomic, strong) TUIMergeMessageCellData *mergeData;
- (void)fillWithData:(TUIMergeMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,381 @@
//
// TUIMergeMessageCell.m
// Pods
//
// Created by harvy on 2020/12/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMergeMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.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
@interface TUIMergeMessageDetailRow : UIView
@property(nonatomic, strong) UILabel *abstractName;
@property(nonatomic, strong) UILabel *abstractBreak;
@property(nonatomic, strong) UILabel *abstractDetail;
@property(nonatomic, assign) CGFloat abstractNameLimitedWidth;
- (void)fillWithData:(NSAttributedString *)name detailContent:(NSAttributedString *)detailContent;
@end
@implementation TUIMergeMessageDetailRow
- (instancetype)init {
self = [super init];
if(self){
[self setupview];
}
return self;
}
- (void)setupview {
[self addSubview:self.abstractName];
[self addSubview:self.abstractBreak];
[self addSubview:self.abstractDetail];
}
- (UILabel *)abstractName {
if(!_abstractName) {
_abstractName = [[UILabel alloc] init];
_abstractName.numberOfLines = 1;
_abstractName.font = [UIFont systemFontOfSize:12.0];
_abstractName.textColor = [UIColor colorWithRed:187 / 255.0 green:187 / 255.0 blue:187 / 255.0 alpha:1 / 1.0];
_abstractName.textAlignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
}
return _abstractName;
}
- (UILabel *)abstractBreak {
if(!_abstractBreak) {
_abstractBreak = [[UILabel alloc] init];
_abstractBreak.text = @":";
_abstractBreak.font = [UIFont systemFontOfSize:12.0];
_abstractBreak.textColor = TUIChatDynamicColor(@"chat_merge_message_content_color", @"#d5d5d5");
}
return _abstractBreak;
}
- (UILabel *)abstractDetail {
if(!_abstractDetail) {
_abstractDetail = [[UILabel alloc] init];
_abstractDetail.numberOfLines = 0;
_abstractDetail.font = [UIFont systemFontOfSize:12.0];
_abstractDetail.textColor = TUIChatDynamicColor(@"chat_merge_message_content_color", @"#d5d5d5");
_abstractDetail.textAlignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
}
return _abstractDetail;
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.abstractName sizeToFit];
[self.abstractName mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.top.mas_equalTo(0);
make.trailing.mas_lessThanOrEqualTo(self.abstractBreak.mas_leading);
make.width.mas_equalTo(self.abstractNameLimitedWidth);
}];
[self.abstractBreak sizeToFit];
[self.abstractBreak mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.abstractName.mas_trailing);
make.top.mas_equalTo(self.abstractName);
make.width.mas_offset(self.abstractBreak.frame.size.width);
make.height.mas_offset(self.abstractBreak.frame.size.height);
}];
[self.abstractDetail sizeToFit];
[self.abstractDetail mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.abstractBreak.mas_trailing);
make.top.mas_equalTo(0);
make.trailing.mas_lessThanOrEqualTo(self.mas_trailing).mas_offset(-15);
make.bottom.mas_equalTo(self);
}];
}
- (void)fillWithData:(NSAttributedString *)name detailContent:(NSAttributedString *)detailContent {
self.abstractName.attributedText = name;
self.abstractDetail.attributedText = detailContent;
NSAttributedString * senderStr = [[NSAttributedString alloc] initWithString:self.abstractName.text];
CGRect senderRect = [senderStr boundingRectWithSize:CGSizeMake(70, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
self.abstractNameLimitedWidth = MIN(ceil(senderRect.size.width) + 2, 70);
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
@end
@interface TUIMergeMessageCell ()
@property(nonatomic, strong) CAShapeLayer *maskLayer;
@property(nonatomic, strong) CAShapeLayer *borderLayer;
@property(nonatomic, strong) TUIMergeMessageDetailRow *contentRowView1;
@property(nonatomic, strong) TUIMergeMessageDetailRow *contentRowView2;
@property(nonatomic, strong) TUIMergeMessageDetailRow *contentRowView3;
@end
@implementation TUIMergeMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if ([super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self setupViews];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
}
return self;
}
- (void)setupViews {
self.container.backgroundColor = TUIChatDynamicColor(@"chat_merge_message_bg_color", @"#FFFFFF");
_relayTitleLabel = [[UILabel alloc] init];
_relayTitleLabel.text = @"Chat history";
_relayTitleLabel.font = [UIFont systemFontOfSize:16];
_relayTitleLabel.textColor = TUIChatDynamicColor(@"chat_merge_message_title_color", @"#000000");
[self.container addSubview:_relayTitleLabel];
_contentRowView1 = [[TUIMergeMessageDetailRow alloc] init];
[self.container addSubview:_contentRowView1];
_contentRowView2 = [[TUIMergeMessageDetailRow alloc] init];
[self.container addSubview:_contentRowView2];
_contentRowView3 = [[TUIMergeMessageDetailRow alloc] init];
[self.container addSubview:_contentRowView3];
_separtorView = [[UIView alloc] init];
_separtorView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
[self.container addSubview:_separtorView];
_bottomTipsLabel = [[UILabel alloc] init];
_bottomTipsLabel.text = TIMCommonLocalizableString(TUIKitRelayChatHistory);
_bottomTipsLabel.textColor = TUIChatDynamicColor(@"chat_merge_message_content_color", @"#d5d5d5");
_bottomTipsLabel.font = [UIFont systemFontOfSize:9];
[self.container addSubview:_bottomTipsLabel];
[self.container.layer insertSublayer:self.borderLayer atIndex:0];
[self.container.layer setMask:self.maskLayer];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.relayTitleLabel sizeToFit];
[self.relayTitleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.container).mas_offset(10);
make.top.mas_equalTo(self.container).mas_offset(10);
make.trailing.mas_equalTo(self.container).mas_offset(-10);
make.height.mas_equalTo(self.relayTitleLabel.font.lineHeight);
}];
[self.contentRowView1 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.relayTitleLabel);
make.top.mas_equalTo(self.relayTitleLabel.mas_bottom).mas_offset(3);
make.trailing.mas_equalTo(self.container);
make.height.mas_equalTo(self.mergeData.abstractRow1Size.height);
}];
[self.contentRowView2 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.relayTitleLabel);
make.top.mas_equalTo(self.contentRowView1.mas_bottom).mas_offset(3);
make.trailing.mas_equalTo(self.container);
make.height.mas_equalTo(self.mergeData.abstractRow2Size.height);
}];
[self.contentRowView3 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.relayTitleLabel);
make.top.mas_equalTo(self.contentRowView2.mas_bottom).mas_offset(3);
make.trailing.mas_equalTo(self.container);
make.height.mas_equalTo(self.mergeData.abstractRow3Size.height);
}];
UIView *lastView = self.contentRowView1;
int count = self.mergeData.abstractSendDetailList.count;
if (count >= 3) {
lastView = self.contentRowView3;
}
else if (count == 2){
lastView = self.contentRowView2;
}
[self.separtorView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.container).mas_offset(10);
make.trailing.mas_equalTo(self.container).mas_offset(-10);
make.top.mas_equalTo(lastView.mas_bottom).mas_offset(3);
make.height.mas_equalTo(1);
}];
[self.bottomTipsLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.contentRowView1);
make.top.mas_equalTo(self.separtorView.mas_bottom).mas_offset(5);
make.width.mas_lessThanOrEqualTo(self.container);
make.height.mas_equalTo(self.bottomTipsLabel.font.lineHeight);
}];
self.maskLayer.frame = self.container.bounds;
self.borderLayer.frame = self.container.bounds;
UIRectCorner corner = UIRectCornerBottomLeft | UIRectCornerBottomRight | UIRectCornerTopLeft;
if (self.mergeData.direction == MsgDirectionIncoming) {
corner = UIRectCornerBottomLeft | UIRectCornerBottomRight | UIRectCornerTopRight;
}
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:self.container.bounds byRoundingCorners:corner cornerRadii:CGSizeMake(10, 10)];
self.maskLayer.path = bezierPath.CGPath;
self.borderLayer.path = bezierPath.CGPath;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)fillWithData:(TUIMergeMessageCellData *)data {
[super fillWithData:data];
self.mergeData = data;
self.relayTitleLabel.text = data.title;
int count = self.mergeData.abstractSendDetailList.count;
switch (count) {
case 1:
[self.contentRowView1 fillWithData:self.mergeData.abstractSendDetailList[0][@"sender"] detailContent:self.mergeData.abstractSendDetailList[0][@"detail"]];
self.contentRowView1.hidden = NO;
self.contentRowView2.hidden = YES;
self.contentRowView3.hidden = YES;
break;
case 2:
[self.contentRowView1 fillWithData:self.mergeData.abstractSendDetailList[0][@"sender"] detailContent:self.mergeData.abstractSendDetailList[0][@"detail"]];
[self.contentRowView2 fillWithData:self.mergeData.abstractSendDetailList[1][@"sender"] detailContent:self.mergeData.abstractSendDetailList[1][@"detail"]];
self.contentRowView1.hidden = NO;
self.contentRowView2.hidden = NO;
self.contentRowView3.hidden = YES;
break;
default:
[self.contentRowView1 fillWithData:self.mergeData.abstractSendDetailList[0][@"sender"] detailContent:self.mergeData.abstractSendDetailList[0][@"detail"]];
[self.contentRowView2 fillWithData:self.mergeData.abstractSendDetailList[1][@"sender"] detailContent:self.mergeData.abstractSendDetailList[1][@"detail"]];
[self.contentRowView3 fillWithData:self.mergeData.abstractSendDetailList[2][@"sender"] detailContent:self.mergeData.abstractSendDetailList[2][@"detail"]];
self.contentRowView1.hidden = NO;
self.contentRowView2.hidden = NO;
self.contentRowView3.hidden = NO;
break;
}
[self prepareReactTagUI:self.container];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (CAShapeLayer *)maskLayer {
if (_maskLayer == nil) {
_maskLayer = [CAShapeLayer layer];
}
return _maskLayer;
}
- (CAShapeLayer *)borderLayer {
if (_borderLayer == nil) {
_borderLayer = [CAShapeLayer layer];
_borderLayer.lineWidth = 1.0;
_borderLayer.strokeColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor;
_borderLayer.fillColor = [UIColor clearColor].CGColor;
}
return _borderLayer;
}
// MARK: ThemeChanged
- (void)applyBorderTheme {
if (_borderLayer) {
_borderLayer.strokeColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor;
}
}
- (void)onThemeChanged {
[self applyBorderTheme];
}
- (void)prepareReactTagUI:(UIView *)containerView {
NSDictionary *param = @{TUICore_TUIChatExtension_ChatMessageReactPreview_Delegate: self};
[TUICore raiseExtension:TUICore_TUIChatExtension_ChatMessageReactPreview_ClassicExtensionID parentView:containerView param:param];
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIMergeMessageCellData.class], @"data must be kind of TUIMergeMessageCellData");
TUIMergeMessageCellData *mergeCellData = (TUIMergeMessageCellData *)data;
mergeCellData.abstractRow1Size = [self.class caculate:mergeCellData index:0];
mergeCellData.abstractRow2Size = [self.class caculate:mergeCellData index:1];
mergeCellData.abstractRow3Size = [self.class caculate:mergeCellData index:2];
NSAttributedString *abstractAttributedString = [mergeCellData abstractAttributedString];
CGRect rect = [abstractAttributedString boundingRectWithSize:CGSizeMake(TMergeMessageCell_Width_Max - 20, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
CGSize size = CGSizeMake(CGFLOAT_CEIL(rect.size.width), CGFLOAT_CEIL(rect.size.height) - 10);
mergeCellData.abstractSize = size;
CGFloat height = mergeCellData.abstractRow1Size.height + mergeCellData.abstractRow2Size.height + mergeCellData.abstractRow3Size.height;
UIFont *titleFont = [UIFont systemFontOfSize:16];
height = (10 + titleFont.lineHeight + 3) + height + 1 + 5 + 20 + 5 +3;
return CGSizeMake(TMergeMessageCell_Width_Max, height);
}
+ (CGSize)caculate:(TUIMergeMessageCellData *)data index:(NSInteger)index {
NSArray<NSDictionary *> *abstractSendDetailList = data.abstractSendDetailList;
if (abstractSendDetailList.count <= index){
return CGSizeZero;
}
NSAttributedString * senderStr = abstractSendDetailList[index][@"sender"];
CGRect senderRect = [senderStr boundingRectWithSize:CGSizeMake(70, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
NSMutableAttributedString *abstr = [[NSMutableAttributedString alloc] initWithString:@""];
[abstr appendAttributedString:[[NSAttributedString alloc] initWithString:@":"]];
[abstr appendAttributedString:abstractSendDetailList[index][@"detail"]];
CGFloat senderWidth = MIN(CGFLOAT_CEIL(senderRect.size.width), 70);
CGRect rect = [abstr boundingRectWithSize:CGSizeMake(200 - 20 - senderWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
CGSize size = CGSizeMake(TMergeMessageCell_Width_Max,
MIN(TMergeMessageCell_Height_Max / 3.0, CGFLOAT_CEIL(rect.size.height)));
return size;
}
@end

View File

@@ -0,0 +1,16 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUIMediaCollectionCell.h"
#import "TUIVideoMessageCellData.h"
@interface TUIVideoCollectionCell : TUIMediaCollectionCell
- (void)fillWithData:(TUIVideoMessageCellData *)data;
- (void)stopVideoPlayAndSave;
- (void)reloadAllView;
@end

View File

@@ -0,0 +1,532 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIVideoCollectionCell.h"
#import <TIMCommon/TIMDefine.h>
#import "ReactiveObjC/ReactiveObjC.h"
#import "TUICircleLodingView.h"
@import MediaPlayer;
@import AVFoundation;
@import AVKit;
@interface TUIVideoCollectionCellScrollView : UIScrollView <UIScrollViewDelegate>
@property(nonatomic, strong) UIView *videoView;
@property(assign, nonatomic) CGFloat videoViewNormalWidth;
@property(assign, nonatomic) CGFloat videoViewNormalHeight;
- (void)pictureZoomWithScale:(CGFloat)zoomScale;
@end
@implementation TUIVideoCollectionCellScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.delegate = self;
self.minimumZoomScale = 1.0f;
self.maximumZoomScale = 2.0f;
_videoViewNormalHeight = frame.size.height;
_videoViewNormalWidth = frame.size.width;
self.videoView = [[UIView alloc] initWithFrame:frame];
[self addSubview:self.videoView];
if (@available(iOS 11.0, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
// Fallback on earlier versions
}
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
}
#pragma mark-- Help Methods
- (void)pictureZoomWithScale:(CGFloat)zoomScale {
CGFloat imageScaleWidth = zoomScale * self.videoViewNormalWidth;
CGFloat imageScaleHeight = zoomScale * self.videoViewNormalHeight;
CGFloat imageX = 0;
CGFloat imageY = 0;
if (imageScaleWidth < self.frame.size.width) {
imageX = floorf((self.frame.size.width - imageScaleWidth) / 2.0);
}
if (imageScaleHeight < self.frame.size.height) {
imageY = floorf((self.frame.size.height - imageScaleHeight) / 2.0);
}
self.videoView.frame = CGRectMake(imageX, imageY, imageScaleWidth, imageScaleHeight);
self.contentSize = CGSizeMake(imageScaleWidth, imageScaleHeight);
}
#pragma mark-- Setter
- (void)setVideoViewNormalWidth:(CGFloat)videoViewNormalWidth {
_videoViewNormalWidth = videoViewNormalWidth;
self.videoView.frame = CGRectMake(0, 0, _videoViewNormalWidth, _videoViewNormalHeight);
self.videoView.center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
}
- (void)setVideoViewNormalHeight:(CGFloat)videoViewNormalHeight {
_videoViewNormalHeight = videoViewNormalHeight;
self.videoView.frame = CGRectMake(0, 0, _videoViewNormalWidth, _videoViewNormalHeight);
self.videoView.center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
}
#pragma mark-- UIScrollViewDelegate
// Returns the view control that needs to be zoomed. During zooming
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.videoView;
}
// BeginZooming
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
NSLog(@"BeginZooming");
}
// EndZooming
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
NSLog(@"EndZooming");
}
// zoom
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
CGFloat imageScaleWidth = scrollView.zoomScale * self.videoViewNormalWidth;
CGFloat imageScaleHeight = scrollView.zoomScale * self.videoViewNormalHeight;
CGFloat imageX = 0;
CGFloat imageY = 0;
if (imageScaleWidth < self.frame.size.width) {
imageX = floorf((self.frame.size.width - imageScaleWidth) / 2.0);
}
if (imageScaleHeight < self.frame.size.height) {
imageY = floorf((self.frame.size.height - imageScaleHeight) / 2.0);
}
self.videoView.frame = CGRectMake(imageX, imageY, imageScaleWidth, imageScaleHeight);
}
@end
@interface TUIVideoCollectionCell ()
@property(nonatomic, strong) TUIVideoCollectionCellScrollView *scrollView;
@property(nonatomic, strong) UILabel *duration;
@property(nonatomic, strong) UILabel *playTime;
@property(nonatomic, strong) UISlider *playProcess;
@property(nonatomic, strong) UIButton *mainPlayBtn;
@property(nonatomic, strong) UIButton *mainDownloadBtn;
@property(nonatomic, strong) UIButton *playBtn;
@property(nonatomic, strong) UIButton *closeBtn;
@property(nonatomic, strong) TUICircleLodingView *animateCircleView;
@property(nonatomic, strong) AVPlayer *player;
@property(nonatomic, strong) AVPlayerLayer *playerLayer;
@property(nonatomic, strong) NSString *videoPath;
@property(nonatomic, strong) NSURL *videoUrl;
@property(nonatomic, assign) BOOL isPlay;
@property(nonatomic, assign) BOOL isSaveVideo;
@property(nonatomic, strong) TUIVideoMessageCellData *videoData;
@end
@implementation TUIVideoCollectionCell
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self setupRotaionNotifications];
}
return self;
}
- (void)setupRotaionNotifications {
if (@available(iOS 16.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:TUIMessageMediaViewDeviceOrientationChangeNotification
object:nil];
} else {
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
}
- (void)setupViews {
self.scrollView = [[TUIVideoCollectionCellScrollView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
[self addSubview:self.scrollView];
self.imageView = [[UIImageView alloc] init];
self.imageView.layer.cornerRadius = 5.0;
[self.imageView.layer setMasksToBounds:YES];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageView.backgroundColor = [UIColor clearColor];
[self.scrollView.videoView addSubview:self.imageView];
self.imageView.mm_fill();
self.imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.mainPlayBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.mainPlayBtn.contentMode = UIViewContentModeScaleToFill;
[self.mainPlayBtn setImage:TUIChatCommonBundleImage(@"video_play_big") forState:UIControlStateNormal];
[self.mainPlayBtn addTarget:self action:@selector(onPlayBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.mainPlayBtn];
self.mainDownloadBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.mainDownloadBtn.contentMode = UIViewContentModeScaleToFill;
[self.mainDownloadBtn setImage:[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"download")] forState:UIControlStateNormal];
self.mainDownloadBtn.hidden = YES;
[self.mainDownloadBtn addTarget:self action:@selector(mainDownloadBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.mainDownloadBtn];
self.animateCircleView = [[TUICircleLodingView alloc] initWithFrame:CGRectMake(0, 0, kScale390(40), kScale390(40))];
self.animateCircleView.progress = 0;
self.animateCircleView.hidden = YES;
[self addSubview:_animateCircleView];
self.playBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.playBtn.contentMode = UIViewContentModeScaleToFill;
[self.playBtn setImage:TUIChatCommonBundleImage(@"video_play") forState:UIControlStateNormal];
[self.playBtn addTarget:self action:@selector(onPlayBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.playBtn];
self.closeBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.closeBtn.contentMode = UIViewContentModeScaleToFill;
[self.closeBtn setImage:TUIChatCommonBundleImage(@"video_close") forState:UIControlStateNormal];
[self.closeBtn addTarget:self action:@selector(onCloseBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.closeBtn];
self.downloadBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.downloadBtn.contentMode = UIViewContentModeScaleToFill;
[self.downloadBtn setImage:TUIChatCommonBundleImage(@"download") forState:UIControlStateNormal];
[self.downloadBtn addTarget:self action:@selector(onDownloadBtnClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.downloadBtn];
self.playTime = [[UILabel alloc] init];
self.playTime.textColor = [UIColor whiteColor];
self.playTime.font = [UIFont systemFontOfSize:12];
self.playTime.text = @"00:00";
[self addSubview:self.playTime];
self.duration = [[UILabel alloc] init];
self.duration.textColor = [UIColor whiteColor];
self.duration.font = [UIFont systemFontOfSize:12];
self.duration.text = @"00:00";
[self addSubview:self.duration];
self.playProcess = [[UISlider alloc] init];
self.playProcess.minimumValue = 0;
self.playProcess.maximumValue = 1;
self.playProcess.minimumTrackTintColor = [UIColor whiteColor];
[self.playProcess addTarget:self action:@selector(onSliderValueChangedBegin:) forControlEvents:UIControlEventTouchDown];
[self.playProcess addTarget:self action:@selector(onSliderValueChanged:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.playProcess];
}
- (void)fillWithData:(TUIVideoMessageCellData *)data;
{
[super fillWithData:data];
self.videoData = data;
self.isSaveVideo = NO;
BOOL hasRiskContent = data.innerMessage.hasRiskContent;
if (hasRiskContent) {
self.imageView.image = TIMCommonBundleThemeImage(@"", @"icon_security_strike");
for (UIView *subview in self.subviews) {
if (subview != self.scrollView && subview != self.closeBtn){
subview.hidden = YES;
}
}
return;
}
CGFloat duration = data.videoItem.duration;
self.duration.text = [NSString stringWithFormat:@"%.2d:%.2d", (int)duration / 60, (int)duration % 60];
self.imageView.image = nil;
if (data.thumbImage == nil) {
[data downloadThumb];
}
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.imageView.image = thumbImage;
}
}];
if (![self.videoData isVideoExist]) {
self.mainDownloadBtn.hidden = NO;
self.mainPlayBtn.hidden = YES;
self.animateCircleView.hidden = YES;
} else {
/**
*
* Downloaded videos can be played directly using local video files
*/
self.videoPath = self.videoData.videoPath;
if (self.videoPath) {
[self addPlayer:[NSURL fileURLWithPath:self.videoPath]];
}
}
[[[RACObserve(self.videoData, videoProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(id _Nullable x) {
@strongify(self);
int progress = [x intValue];
if (progress == 100) {
self.animateCircleView.progress = 99;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 100;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.animateCircleView.progress = 0;
self.mainDownloadBtn.hidden = YES;
self.animateCircleView.hidden = YES;
self.mainPlayBtn.hidden = NO;
});
});
} else if (progress > 1 && progress < 100) {
self.animateCircleView.progress = progress;
self.mainDownloadBtn.hidden = YES;
self.animateCircleView.hidden = NO;
} else {
self.animateCircleView.progress = progress;
}
}];
[[[RACObserve(self.videoData, videoPath) filter:^BOOL(NSString *path) {
return [path length] > 0;
}] take:1] subscribeNext:^(NSString *path) {
@strongify(self);
self.videoPath = path;
if (self.isSaveVideo) {
[self saveVideo];
}
self.animateCircleView.hidden = YES;
[self addPlayer:[NSURL fileURLWithPath:self.videoPath]];
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self.mainDownloadBtn sizeToFit];
self.mainDownloadBtn.mm_width(65).mm_height(65).mm__centerX(self.mm_w / 2).mm__centerY(self.mm_h / 2);
self.mainDownloadBtn.layer.cornerRadius = (self.mainDownloadBtn.mm_h * 0.5);
self.animateCircleView.tui_mm_center();
self.mainPlayBtn.mm_width(65).mm_height(65).mm__centerX(self.mm_w / 2).mm__centerY(self.mm_h / 2);
self.closeBtn.mm_width(31).mm_height(31).mm_left(16).mm_bottom(47);
self.downloadBtn.mm_width(31).mm_height(31).mm_right(16).mm_bottom(48);
self.playBtn.mm_width(30).mm_height(30).mm_left(32).mm_bottom(108);
self.playTime.mm_width(40).mm_height(21).mm_left(self.playBtn.mm_maxX + 12).mm__centerY(self.playBtn.mm_centerY);
self.duration.mm_width(40).mm_height(21).mm_right(15).mm__centerY(self.playBtn.mm_centerY);
self.playProcess.mm_sizeToFit()
.mm_left(self.playTime.mm_maxX + 10)
.mm_flexToRight(self.duration.mm_r + self.duration.mm_w + 10)
.mm__centerY(self.playBtn.mm_centerY);
self.scrollView.mm_width(self.mm_w).mm_height(self.mm_h).mm__centerX(self.mm_w / 2).mm__centerY(self.mm_h / 2);
self.scrollView.videoViewNormalWidth = self.mm_w;
self.scrollView.videoViewNormalHeight = self.mm_h;
self.playerLayer.frame = self.scrollView.bounds;
[self.scrollView.videoView.layer layoutIfNeeded];
}
- (void)addPlayer:(NSURL *)url {
self.videoUrl = url;
if (!self.player) {
self.player = [AVPlayer playerWithURL:self.videoUrl];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.frame = self.scrollView.videoView.bounds;
[self.scrollView.videoView.layer insertSublayer:self.playerLayer atIndex:0];
@weakify(self);
[self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.05, 30)
queue:NULL
usingBlock:^(CMTime time) {
@strongify(self);
CGFloat curTime = CMTimeGetSeconds(self.player.currentItem.currentTime);
CGFloat duration = CMTimeGetSeconds(self.player.currentItem.duration);
CGFloat progress = curTime / duration;
[self.playProcess setValue:progress];
self.playTime.text = [NSString stringWithFormat:@"%.2d:%.2d", (int)curTime / 60, (int)curTime % 60];
}];
[self addPlayerItemObserver];
} else {
[self removePlayerItemObserver];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:self.videoUrl];
[self.player replaceCurrentItemWithPlayerItem:item];
[self addPlayerItemObserver];
}
}
- (void)addPlayerItemObserver {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onVideoPlayEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}
- (void)removePlayerItemObserver {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}
- (void)stopVideoPlayAndSave {
[self stopPlay];
self.isSaveVideo = NO;
[TUITool hideToast];
}
#pragma player event
- (void)onPlayBtnClick {
if (![self.videoData isVideoExist]) {
[self.videoData downloadVideo];
} else {
if (self.isPlay) {
[self stopPlay];
} else {
[self play];
}
}
}
- (void)onCloseBtnClick {
[self stopPlay];
if (self.delegate && [self.delegate respondsToSelector:@selector(onCloseMedia:)]) {
[self.delegate onCloseMedia:self];
}
}
- (void)onVideoPlayEnd {
if (1 == self.playProcess.value) {
[self.player seekToTime:CMTimeMakeWithSeconds(0, 30)];
[self stopPlay];
}
}
- (void)onSliderValueChangedBegin:(id)sender {
[self.player pause];
}
- (void)onSliderValueChanged:(id)sender {
UISlider *slider = (UISlider *)sender;
CGFloat curTime = CMTimeGetSeconds(self.player.currentItem.duration) * slider.value;
[self.player seekToTime:CMTimeMakeWithSeconds(curTime, 30)];
[self play];
}
- (void)play {
self.isPlay = YES;
[self.player play];
self.imageView.hidden = YES;
self.mainPlayBtn.hidden = YES;
[self.playBtn setImage:TUIChatCommonBundleImage(@"video_pause") forState:UIControlStateNormal];
}
- (void)stopPlay {
BOOL hasRiskContent = self.videoData.innerMessage.hasRiskContent;
self.isPlay = NO;
[self.player pause];
self.imageView.hidden = NO;
if (!hasRiskContent) {
self.mainPlayBtn.hidden = NO;
}
[self.playBtn setImage:TUIChatCommonBundleImage(@"video_play") forState:UIControlStateNormal];
}
- (void)mainDownloadBtnClick {
if (![self.videoData isVideoExist]) {
[self.videoData downloadVideo];
}
}
#pragma video save
- (void)onDownloadBtnClick {
if (![self.videoData isVideoExist]) {
self.isSaveVideo = YES;
[TUITool makeToast:TIMCommonLocalizableString(TUIKitVideoDownloading) duration:CGFLOAT_MAX];
} else {
[self saveVideo];
}
}
- (void)saveVideo {
[TUITool hideToast];
[[PHPhotoLibrary sharedPhotoLibrary]
performChanges:^{
PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:self.videoPath]];
request.creationDate = [NSDate date];
}
completionHandler:^(BOOL success, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitVideoSavedSuccess) duration:1];
} else {
[TUITool makeToastError:-1 msg:TIMCommonLocalizableString(TUIKitVideoSavedFailed)];
}
});
}];
}
- (void)onDeviceOrientationChange:(NSNotification *)noti {
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
[self reloadAllView];
}
- (void)reloadAllView {
if (self.player) {
[self stopPlay];
self.player = nil;
}
if (self.playerLayer) {
[self.playerLayer removeFromSuperlayer];
self.playerLayer = nil;
}
for (UIView *subview in self.scrollView.subviews) {
if (subview) {
[subview removeFromSuperview];
}
}
for (UIView *subview in self.subviews) {
if (subview) {
[subview removeFromSuperview];
}
}
[self setupViews];
if (self.videoData) {
[self fillWithData:self.videoData];
}
}
#pragma mark - TUIMessageProgressManagerDelegate
- (void)onUploadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![msgID isEqualToString:self.videoData.msgID]) {
return;
}
if (self.videoData.direction == MsgDirectionOutgoing) {
self.videoData.uploadProgress = progress;
}
}
#pragma mark - V2TIMAdvancedMsgListener
- (void)onRecvMessageModified:(V2TIMMessage *)msg {
V2TIMMessage *imMsg = msg;
if (imMsg == nil || ![imMsg isKindOfClass:V2TIMMessage.class]) {
return;
}
if ([self.videoData.innerMessage.msgID isEqualToString:imMsg.msgID]) {
BOOL hasRiskContent = msg.hasRiskContent;
if (hasRiskContent) {
self.videoData.innerMessage = imMsg;
[self showRiskAlert];
}
}
}
- (void)showRiskAlert {
if (self.player) {
[self stopPlay];
}
UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil
message:TIMCommonLocalizableString(TUIKitVideoCheckRisk)
preferredStyle:UIAlertControllerStyleAlert];
__weak typeof(self) weakSelf = self;
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitVideoCheckRiskCancel)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *_Nonnull action) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf reloadAllView];
}]];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:ac animated:YES completion:nil];
}
@end

View File

@@ -0,0 +1,49 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This document declares the TUIVideoMessageCell unit, which is responsible for the display of video messages.
* The video message unit, a unit displayed when sending and receiving video messages, can display the video cover, video duration, etc. to the user,
* and at the same time, can respond to user operations and provide an operation entry for video playback.
* When you tap the video message, you will enter the video playback interface.
*/
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUIMessageCell.h>
#import "TUIVideoMessageCellData.h"
/**
*
* 【Module name】TUIVideoMessageCell
* 【Function description】 Video message unit
* - The video message unit provides the function of extracting and displaying thumbnails of video messages, and can display the video length and video
* download/upload progress.
* - At the same time, the network acquisition and local acquisition of video messages (if it exists in the local cache) are integrated in the message unit.
*/
@interface TUIVideoMessageCell : TUIBubbleMessageCell
/**
*
* Video thumbnail
* Display the thumbnail of the video when it is not playing, so that users can get general information about the video without playing the video.
*/
@property(nonatomic, strong) UIImageView *thumb;
/**
* Label for displaying video duration
*/
@property(nonatomic, strong) UILabel *duration;
/**
* Play icon, that is, the "arrow icon" displayed in the UI.
*/
@property(nonatomic, strong) UIImageView *play;
/**
* Label for displaying video doadloading/uploading progress
*
*/
@property(nonatomic, strong) UILabel *progress;
@property TUIVideoMessageCellData *videoData;
- (void)fillWithData:(TUIVideoMessageCellData *)data;
@end

View File

@@ -0,0 +1,419 @@
//
// TUIVideoMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVideoMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import "TUICircleLodingView.h"
#import "TUIMessageProgressManager.h"
@interface TUIVideoMessageCell () <TUIMessageProgressManagerDelegate>
@property(nonatomic, strong) UIView *animateHighlightView;
@property(nonatomic, strong) TUICircleLodingView *animateCircleView;
@property(nonatomic, strong) UIImageView *downloadImage;
@end
@implementation TUIVideoMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_thumb = [[UIImageView alloc] init];
_thumb.layer.cornerRadius = 5.0;
[_thumb.layer setMasksToBounds:YES];
_thumb.contentMode = UIViewContentModeScaleAspectFill;
_thumb.backgroundColor = [UIColor clearColor];
[self.bubbleView addSubview:_thumb];
_thumb.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
CGSize playSize = TVideoMessageCell_Play_Size;
_play = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, playSize.width, playSize.height)];
_play.contentMode = UIViewContentModeScaleAspectFit;
_play.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"play_normal")];
_play.hidden = YES;
[_thumb addSubview:_play];
_downloadImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, playSize.width, playSize.height)];
_downloadImage.contentMode = UIViewContentModeScaleAspectFit;
_downloadImage.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"download")];
_downloadImage.hidden = YES;
[_thumb addSubview:_downloadImage];
_duration = [[UILabel alloc] init];
_duration.textColor = [UIColor whiteColor];
_duration.font = [UIFont systemFontOfSize:12];
[_thumb addSubview:_duration];
_animateCircleView = [[TUICircleLodingView alloc] initWithFrame:CGRectMake(0, 0, kScale390(40), kScale390(40))];
_animateCircleView.progress = 0;
[_thumb addSubview:_animateCircleView];
_progress = [[UILabel alloc] init];
_progress.textColor = [UIColor whiteColor];
_progress.font = [UIFont systemFontOfSize:15];
_progress.textAlignment = NSTextAlignmentCenter;
_progress.layer.cornerRadius = 5.0;
_progress.hidden = YES;
_progress.backgroundColor = TVideoMessageCell_Progress_Color;
[_progress.layer setMasksToBounds:YES];
[self.container addSubview:_progress];
_progress.mm_fill();
_progress.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[TUIMessageProgressManager.shareManager addDelegate:self];
}
return self;
}
- (void)fillWithData:(TUIVideoMessageCellData *)data;
{
// set data
[super fillWithData:data];
self.videoData = data;
_thumb.image = nil;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent) {
self.thumb.image = TIMCommonBundleThemeImage(@"", @"icon_security_strike");
self.securityStrikeView.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeImage);
self.duration.text = @"";
self.play.hidden = YES;
self.downloadImage.hidden = YES;
self.indicator.hidden = YES;
self.animateCircleView.hidden = YES;
return;
}
if (data.thumbImage == nil) {
[data downloadThumb];
}
if (data.isPlaceHolderCellData) {
//show placeHolder
_thumb.backgroundColor = [UIColor grayColor];
_animateCircleView.progress = (data.videoTranscodingProgress *100);
self.duration.text = @"";
self.play.hidden = YES;
self.downloadImage.hidden = YES;
self.indicator.hidden = YES;
self.animateCircleView.hidden = NO;
@weakify(self);
[[RACObserve(data, videoTranscodingProgress) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *x) {
// The transcoded animation can display up to 30% at maximum,
// and the upload progress increases from 30% to 100%.
@strongify(self);
double progress = [x doubleValue];
double factor = 0.3;
double resultProgress = (progress *100) * factor;
self.animateCircleView.progress = resultProgress;
}];
if (data.thumbImage) {
self.thumb.image = data.thumbImage;
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
return;
}
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.thumb.image = thumbImage;
}
}];
_duration.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)data.videoItem.duration / 60, (long)data.videoItem.duration % 60];
self.play.hidden = YES;
self.downloadImage.hidden = YES;
self.indicator.hidden = YES;
if (data.direction == MsgDirectionIncoming) {
[[[RACObserve(data, thumbProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
// Cover download progress callback
int progress = [x intValue];
self.progress.text = [NSString stringWithFormat:@"%d%%", progress];
self.progress.hidden = (progress >= 100 || progress == 0);
self.animateCircleView.progress = progress;
if (progress >= 100 || progress == 0) {
// The progress of cover download is called back and the download video icon is displayed when the cover progress is 100.
if ([data isVideoExist]) {
self.play.hidden = NO;
} else {
self.downloadImage.hidden = NO;
}
} else {
self.play.hidden = YES;
self.downloadImage.hidden = YES;
}
}];
// Video resource download progress callback
[[[RACObserve(data, videoProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
self.animateCircleView.progress = progress;
if (progress >= 100 || progress == 0) {
self.play.hidden = NO;
self.animateCircleView.hidden = YES;
} else {
self.play.hidden = YES;
self.downloadImage.hidden = YES;
self.animateCircleView.hidden = NO;
}
}];
} else {
if ([data isVideoExist]) {
[[[RACObserve(data, uploadProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
if (data.placeHolderCellData.videoTranscodingProgress > 0) {
progress = MAX(progress, 30);//the upload progress increases from 30% to 100%.
}
self.animateCircleView.progress = progress;
if (progress >= 100 || progress == 0) {
[self.indicator stopAnimating];
self.play.hidden = NO;
self.animateCircleView.hidden = YES;
} else {
[self.indicator startAnimating];
self.play.hidden = YES;
self.animateCircleView.hidden = NO;
}
}];
} else {
[[[RACObserve(data, thumbProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
// Cover download progress callback
int progress = [x intValue];
self.progress.text = [NSString stringWithFormat:@"%d%%", progress];
self.progress.hidden = (progress >= 100 || progress == 0);
self.animateCircleView.progress = progress;
if (progress >= 100 || progress == 0) {
// The download video icon is displayed when the cover progress reaches 100
if ([data isVideoExist]) {
self.play.hidden = NO;
} else {
self.downloadImage.hidden = NO;
}
} else {
self.play.hidden = YES;
self.downloadImage.hidden = YES;
}
}];
// Video resource download progress callback
[[[RACObserve(data, videoProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
self.animateCircleView.progress = progress;
if (progress >= 100 || progress == 0) {
self.play.hidden = NO;
self.animateCircleView.hidden = YES;
} else {
self.play.hidden = YES;
self.downloadImage.hidden = YES;
self.animateCircleView.hidden = NO;
}
}];
}
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
if (self.messageData.messageContainerAppendSize.height > 0) {
CGFloat topMargin = 10;
CGFloat tagViewTopMargin = 6;
CGFloat thumbHeight = self.bubbleView.mm_h - topMargin - self.messageData.messageContainerAppendSize.height - tagViewTopMargin;
CGSize size = [self.class getContentSize:self.messageData];
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(thumbHeight);
make.width.mas_equalTo(size.width);
make.centerX.mas_equalTo(self.bubbleView);
make.top.mas_equalTo(self.container).mas_offset(topMargin);
}];
[self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.thumb.mas_trailing).mas_offset(-2);
make.width.mas_greaterThanOrEqualTo(20);
make.height.mas_equalTo(20);
make.bottom.mas_equalTo(self.thumb.mas_bottom);
}];
} else {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.top);
make.bottom.mas_equalTo(self.bubbleView).mas_offset(- self.messageData.cellLayout.bubbleInsets.bottom);
make.leading.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.left);
make.trailing.mas_equalTo(self.bubbleView).mas_offset(- self.messageData.cellLayout.bubbleInsets.right);
}];
[self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.thumb.mas_trailing).mas_offset(-2);
make.width.mas_greaterThanOrEqualTo(20);
make.height.mas_equalTo(20);
make.bottom.mas_equalTo(self.thumb.mas_bottom);
}];
}
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView).mas_offset(12);
make.size.mas_equalTo(CGSizeMake(150, 150));
make.centerX.mas_equalTo(self.bubbleView);
}];
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.thumb.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
if(self.messageData.messageContainerAppendSize.height>0) {
make.bottom.mas_equalTo(self.container).mas_offset(-self.messageData.messageContainerAppendSize.height);
}
else {
make.bottom.mas_equalTo(self.container).mas_offset(-12);
}
}];
}
[self.play mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(TVideoMessageCell_Play_Size);
make.center.mas_equalTo(self.thumb);
}];
[self.downloadImage mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(TVideoMessageCell_Play_Size);
make.center.mas_equalTo(self.thumb);
}];
[self.animateCircleView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.thumb);
make.size.mas_equalTo(CGSizeMake(kScale390(40), kScale390(40)));
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)highlightWhenMatchKeyword:(NSString *)keyword {
if (keyword) {
if (self.highlightAnimating) {
return;
}
[self animate:3];
}
}
- (void)animate:(int)times {
times--;
if (times < 0) {
[self.animateHighlightView removeFromSuperview];
self.highlightAnimating = NO;
return;
}
self.highlightAnimating = YES;
self.animateHighlightView.frame = self.container.bounds;
self.animateHighlightView.alpha = 0.1;
[self.container addSubview:self.animateHighlightView];
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.5;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:0.25
animations:^{
self.animateHighlightView.alpha = 0.1;
}
completion:^(BOOL finished) {
if (!self.videoData.highlightKeyword) {
[self animate:0];
return;
}
[self animate:times];
}];
}];
}
- (UIView *)animateHighlightView {
if (_animateHighlightView == nil) {
_animateHighlightView = [[UIView alloc] init];
_animateHighlightView.backgroundColor = [UIColor orangeColor];
}
return _animateHighlightView;
}
#pragma mark - TUIMessageProgressManagerDelegate
- (void)onUploadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![msgID isEqualToString:self.videoData.msgID]) {
return;
}
if (self.videoData.direction == MsgDirectionOutgoing) {
self.videoData.uploadProgress = progress;
}
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIVideoMessageCellData.class], @"data must be kind of TUIVideoMessageCellData");
TUIVideoMessageCellData *videoCellData = (TUIVideoMessageCellData *)data;
CGSize size = CGSizeZero;
BOOL isDir = NO;
if (![videoCellData.snapshotPath isEqualToString:@""] && [[NSFileManager defaultManager] fileExistsAtPath:videoCellData.snapshotPath isDirectory:&isDir]) {
if (!isDir) {
size = [UIImage imageWithContentsOfFile:videoCellData.snapshotPath].size;
}
} else {
size = videoCellData.snapshotItem.size;
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
return size;
}
if (size.height > size.width) {
size.width = size.width / size.height * TVideoMessageCell_Image_Height_Max;
size.height = TVideoMessageCell_Image_Height_Max;
} else {
size.height = size.height / size.width * TVideoMessageCell_Image_Width_Max;
size.width = TVideoMessageCell_Image_Width_Max;
}
BOOL hasRiskContent = videoCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
CGFloat bubbleTopMargin = 12;
CGFloat bubbleBottomMargin = 12;
size.height = MAX(size.height, 150);// width must more than TIMCommonBundleThemeImage(@"", @"icon_security_strike");
size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
size.height += bubbleTopMargin;
size.height += kTUISecurityStrikeViewTopLineMargin;
size.height += kTUISecurityStrikeViewTopLineToBottom;
size.height += bubbleBottomMargin;
}
return size;
}
@end

View File

@@ -0,0 +1,48 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This file declares the TUIVoiceMessageCell class, which is responsible for implementing the display of voice messages.
* Voice messages, i.e. message units displayed after voice is sent/received. TUIKit displays it as a message with a "sound wave" icon in a bubble by default.
* The voice message unit is also responsible for responding to the user's operation and playing the corresponding audio information when the user clicks.
*/
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIVoiceMessageCellData.h"
@import AVFoundation;
/**
*
* 【Module name】 TUIVoiceMessageCell
* 【Function description】 Voice message unit
* - Voice messages, i.e. message units displayed after voice is sent/received. TUIKit displays it as a message with a "sound wave" icon in a bubble by
* default.
* - The voice message unit provides the display and playback functions of voice messages.
* - The TUIVoiceMessageCellData in the voice message unit integrates and calls the voice download and acquisition of the IM SDK, and handles the related
* business logic.
* - This class inherits from TUIBubbleMessageCell to implement bubble messages. You can implement custom bubbles by referring to this inheritance
* relationship.
*/
@interface TUIVoiceMessageCell : TUIBubbleMessageCell
/**
* Voice icon
* It is used to display the voice "sound wave" icon, and at the same time realize the animation effect of the voice when it is playing.
*/
@property(nonatomic, strong) UIImageView *voice;
/**
* Label for displays video duration
* Used to display the duration of the speech outside the bubble, the default value is an integer and the unit is seconds.
*/
@property(nonatomic, strong) UILabel *duration;
@property(nonatomic, strong) UIImageView *voiceReadPoint;
@property TUIVoiceMessageCellData *voiceData;
- (void)fillWithData:(TUIVoiceMessageCellData *)data;
@end

View File

@@ -0,0 +1,230 @@
//
// TUIVoiceMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceMessageCell.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/TUICore.h>
@implementation TUIVoiceMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_voice = [[UIImageView alloc] init];
_voice.animationDuration = 1;
[self.bubbleView addSubview:_voice];
_duration = [[UILabel alloc] init];
_duration.font = [UIFont boldSystemFontOfSize:12];
[self.bubbleView addSubview:_duration];
self.bottomContainer = [[UIView alloc] init];
[self.contentView addSubview:self.bottomContainer];
_voiceReadPoint = [[UIImageView alloc] init];
_voiceReadPoint.backgroundColor = [UIColor redColor];
_voiceReadPoint.frame = CGRectMake(0, 0, 5, 5);
_voiceReadPoint.hidden = YES;
[_voiceReadPoint.layer setCornerRadius:_voiceReadPoint.frame.size.width / 2];
[_voiceReadPoint.layer setMasksToBounds:YES];
[self.bubbleView addSubview:_voiceReadPoint];
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
for (UIView *view in self.bottomContainer.subviews) {
[view removeFromSuperview];
}
}
// Override
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.voiceData};
[TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
}
- (void)fillWithData:(TUIVoiceMessageCellData *)data {
// set data
[super fillWithData:data];
self.voiceData = data;
self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
if (data.duration > 0) {
_duration.text = [NSString stringWithFormat:@"%ld\"", (long)data.duration];
} else {
_duration.text = @"1\"";
}
_voice.image = data.voiceImage;
_voice.animationImages = data.voiceAnimationImages;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent) {
self.securityStrikeView.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeVoice);
}
if (self.voiceData.innerMessage.localCustomInt == 0 && self.voiceData.direction == MsgDirectionIncoming) self.voiceReadPoint.hidden = NO;
// animate
@weakify(self);
[[RACObserve(data, isPlaying) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *x) {
@strongify(self);
if ([x boolValue]) {
[self.voice startAnimating];
} else {
[self.voice stopAnimating];
}
}];
[self applyStyleFromDirection:data.direction];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)applyStyleFromDirection:(TMsgDirection)direction {
if (direction == MsgDirectionIncoming) {
_duration.rtlAlignment = TUITextRTLAlignmentLeading;
_duration.textColor = TUIChatDynamicColor(@"chat_voice_message_recv_duration_time_color", @"#000000");
} else {
_duration.rtlAlignment = TUITextRTLAlignmentTrailing;
_duration.textColor = TUIChatDynamicColor(@"chat_voice_message_send_duration_time_color", @"#000000");
}
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// This is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.voice sizeToFit];
[self.voice mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.voiceData.voiceTop);
make.width.height.mas_equalTo(_voiceData.voiceHeight);
if (self.voiceData.direction == MsgDirectionOutgoing) {
make.trailing.mas_equalTo(-self.voiceData.cellLayout.bubbleInsets.right);
} else {
make.leading.mas_equalTo(self.voiceData.cellLayout.bubbleInsets.left);
}
}];
[self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.mas_greaterThanOrEqualTo(10);
make.height.mas_greaterThanOrEqualTo(TVoiceMessageCell_Duration_Size.height);
make.centerY.mas_equalTo(self.voice);
if (self.voiceData.direction == MsgDirectionOutgoing) {
make.trailing.mas_equalTo(self.voice.mas_leading).mas_offset(-5);
} else {
make.leading.mas_equalTo(self.voice.mas_trailing).mas_offset(5);
}
}];
if (self.voiceData.direction == MsgDirectionOutgoing) {
self.voiceReadPoint.hidden = YES;
} else {
[self.voiceReadPoint mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView);
make.leading.mas_equalTo(self.bubbleView.mas_trailing).mas_offset(1);
make.size.mas_equalTo(CGSizeMake(5, 5));
}];
}
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent) {
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.voice.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
make.bottom.mas_equalTo(self.container).mas_offset(- self.messageData.messageContainerAppendSize.height);
}];
}
[self layoutBottomContainer];
}
- (void)layoutBottomContainer {
if (CGSizeEqualToSize(self.voiceData.bottomContainerSize, CGSizeZero)) {
return;
}
CGSize size = self.voiceData.bottomContainerSize;
[self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.voiceData.direction == MsgDirectionIncoming) {
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).offset(6);
make.size.mas_equalTo(size);
}];
CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
if (!self.messageModifyRepliesButton.hidden) {
[self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.voiceData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(self.container.mas_leading);
} else {
make.trailing.mas_equalTo(self.container.mas_trailing);
}
make.top.mas_equalTo(self.bottomContainer.mas_bottom);
make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
}];
}
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
CGFloat height = [super getHeight:data withWidth:width];
if (data.bottomContainerSize.height > 0) {
height += data.bottomContainerSize.height + kScale375(6);
}
return height;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
TUIVoiceMessageCellData *voiceCellData = (TUIVoiceMessageCellData *)data;
CGFloat bubbleWidth = TVoiceMessageCell_Back_Width_Min + voiceCellData.duration / TVoiceMessageCell_Max_Duration * Screen_Width;
if (bubbleWidth > TVoiceMessageCell_Back_Width_Max) {
bubbleWidth = TVoiceMessageCell_Back_Width_Max;
}
CGFloat bubbleHeight = TVoiceMessageCell_Duration_Size.height;
if (voiceCellData.direction == MsgDirectionIncoming) {
bubbleWidth = MAX(bubbleWidth, [TUIBubbleMessageCell incommingBubble].size.width);
bubbleHeight = voiceCellData.voiceImage.size.height + 2 * voiceCellData.voiceTop; //[TUIBubbleMessageCellData incommingBubble].size.height;
} else {
bubbleWidth = MAX(bubbleWidth, [TUIBubbleMessageCell outgoingBubble].size.width);
bubbleHeight = voiceCellData.voiceImage.size.height + 2 * voiceCellData.voiceTop; // [TUIBubbleMessageCellData outgoingBubble].size.height;
}
CGFloat width = bubbleWidth + TVoiceMessageCell_Duration_Size.width;
CGFloat height = bubbleHeight;
BOOL hasRiskContent = voiceCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
width = MAX(width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeVoice)
height += kTUISecurityStrikeViewTopLineMargin;
height += kTUISecurityStrikeViewTopLineToBottom;
}
return CGSizeMake(width, height);
}
@end

View File

@@ -0,0 +1,24 @@
//
// TUIEvaluationCell.h
// TUIChat
//
// Created by xia on 2022/6/10.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIEvaluationCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIEvaluationCell : TUIBubbleMessageCell
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UILabel *commentLabel;
@property(nonatomic, strong) NSMutableArray *starImageArray;
- (void)fillWithData:(TUIEvaluationCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,141 @@
//
// TUIEvaluationCell.m
// TUIChat
//
// Created by xia on 2022/6/10.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIEvaluationCell.h"
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUIEvaluationCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:15];
_titleLabel.numberOfLines = 1;
_titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_titleLabel.textColor = TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000");
[self.container addSubview:_titleLabel];
for (int i = 0; i < 5; i++) {
UIImageView *imageView = [[UIImageView alloc] init];
[imageView setImage:TUIChatBundleThemeImage(@"chat_custom_evaluation_message_img", @"message_custom_evaluation")];
[self.container addSubview:imageView];
[self.starImageArray addObject:imageView];
}
_commentLabel = [[UILabel alloc] init];
_commentLabel.font = [UIFont systemFontOfSize:15];
_commentLabel.numberOfLines = 0;
_commentLabel.textColor = TUIChatDynamicColor(@"chat_custom_evaluation_message_desc_color", @"#000000");
[self.container addSubview:_commentLabel];
}
return self;
}
- (void)fillWithData:(TUIEvaluationCellData *)data {
[super fillWithData:data];
self.titleLabel.text = data.desc;
self.commentLabel.text = data.comment;
// Configure all StarViews to avoid UI cache clutter
for (int i = 0; i < self.starImageArray.count; i++) {
UIImageView *starView = [self.starImageArray objectAtIndex:i];
starView.hidden = (i >= data.score);
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(10);
make.leading.mas_equalTo(10);
make.width.mas_equalTo(225);
make.height.mas_equalTo(18);
}];
UIImageView *leftView = nil;
for (UIImageView *starView in self.starImageArray) {
if (leftView == nil) {
[starView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(10);
make.top.mas_equalTo(self.titleLabel.mas_bottom).mas_offset(6);
make.width.mas_equalTo(30);
make.height.mas_equalTo(30);
}];
} else {
[starView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(leftView.mas_trailing);
make.top.mas_equalTo(self.titleLabel.mas_bottom).mas_offset(6);
make.width.mas_equalTo(30);
make.height.mas_equalTo(30);
}];
}
leftView = starView;
}
UIImageView *starView = self.starImageArray.firstObject;
self.commentLabel.hidden = self.commentLabel.text.length == 0;
if (self.commentLabel.text.length > 0) {
CGRect rect = [self.commentLabel.text boundingRectWithSize:CGSizeMake(225, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]}
context:nil];
CGSize size = CGSizeMake(225, ceilf(rect.size.height));
[self.commentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(starView.mas_bottom).mas_offset(6);
make.leading.mas_equalTo(10);
make.width.mas_equalTo(size.width);
make.height.mas_equalTo(size.height);
}];
}
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (NSMutableArray *)starImageArray {
if (!_starImageArray) {
_starImageArray = [[NSMutableArray alloc] init];
}
return _starImageArray;
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIEvaluationCellData.class], @"data must be kind of TUIEvaluationCellData");
TUIEvaluationCellData *evaluationCellData = (TUIEvaluationCellData *)data;
CGRect rect = [evaluationCellData.comment boundingRectWithSize:CGSizeMake(215, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]}
context:nil];
CGSize size = CGSizeMake(245, ceilf(rect.size.height));
size.height += evaluationCellData.comment.length > 0 ? 88 : 50;
return size;
}
@end

View File

@@ -0,0 +1,23 @@
//
// MyCustomCell.h
// TUIKitDemo
//
// Created by annidyfeng on 2019/6/10.
// Copyright © 2019 Tencent. All rights reserved.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUIMessageCell.h>
#import "TUILinkCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUILinkCell : TUIBubbleMessageCell
@property UILabel *myTextLabel;
@property UILabel *myLinkLabel;
@property TUILinkCellData *customData;
- (void)fillWithData:(TUILinkCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,95 @@
//
// MyCustomCell.m
// TUIKitDemo
//
// Created by annidyfeng on 2019/6/10.
// Copyright © 2019 Tencent. All rights reserved.
//
#import "TUILinkCell.h"
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUILinkCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_myTextLabel = [[UILabel alloc] init];
_myTextLabel.numberOfLines = 0;
_myTextLabel.font = [UIFont systemFontOfSize:15];
_myTextLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
_myTextLabel.textColor = TUIChatDynamicColor(@"chat_link_message_title_color", @"#000000");
[self.container addSubview:_myTextLabel];
_myLinkLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_myLinkLabel.text = TIMCommonLocalizableString(TUIKitMoreLinkDetails);
_myLinkLabel.font = [UIFont systemFontOfSize:15];
_myLinkLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
_myLinkLabel.textColor = TUIChatDynamicColor(@"chat_link_message_subtitle_color", @"#0000FF");
[self.container addSubview:_myLinkLabel];
}
return self;
}
- (void)fillWithData:(TUILinkCellData *)data;
{
[super fillWithData:data];
self.customData = data;
self.myTextLabel.text = data.text;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGRect rect = [self.myTextLabel.text boundingRectWithSize:CGSizeMake(245, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]}
context:nil];
[self.myTextLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(10);
make.leading.mas_equalTo(10);
make.width.mas_equalTo(245);
make.height.mas_equalTo(rect.size.height);
}];
[self.myLinkLabel sizeToFit];
[self.myLinkLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.myTextLabel.mas_bottom).mas_offset(15);
make.leading.mas_equalTo(10);
make.width.mas_equalTo(self.myLinkLabel.frame.size.width);
make.height.mas_equalTo(self.myLinkLabel.frame.size.height);
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
#pragma mark - TUIMessageCellProtocol
+ (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;
}
@end

View File

@@ -0,0 +1,26 @@
//
// TUIOrderCell.h
// TUIChat
//
// Created by xia on 2022/6/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIOrderCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIOrderCell : TUIBubbleMessageCell
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UILabel *descLabel;
@property(nonatomic, strong) UIImageView *iconView;
@property(nonatomic, strong) UILabel *priceLabel;
@property(nonatomic, strong) TUIOrderCellData *customData;
- (void)fillWithData:(TUIOrderCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,112 @@
//
// TUIOrderCell.m
// TUIChat
//
// Created by xia on 2022/6/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIOrderCell.h"
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUIOrderCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont boldSystemFontOfSize:12];
_titleLabel.textColor = TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000");
_titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
[self.container addSubview:_titleLabel];
_descLabel = [[UILabel alloc] init];
_descLabel.font = [UIFont systemFontOfSize:12];
_descLabel.numberOfLines = 1;
_descLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_descLabel.textColor = TUIChatDynamicColor(@"chat_custom_order_message_desc_color", @"#999999");
[self.container addSubview:_descLabel];
_priceLabel = [[UILabel alloc] init];
_priceLabel.font = [UIFont boldSystemFontOfSize:18];
_priceLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_priceLabel.textColor = TUIChatDynamicColor(@"chat_custom_order_message_price_color", @"#FF7201");
[self.container addSubview:_priceLabel];
_iconView = [[UIImageView alloc] init];
_iconView.layer.cornerRadius = 8.0;
_iconView.layer.masksToBounds = YES;
[self.container addSubview:_iconView];
}
return self;
}
- (void)fillWithData:(TUIOrderCellData *)data {
[super fillWithData:data];
self.customData = data;
self.titleLabel.text = data.title;
self.descLabel.text = data.desc;
self.priceLabel.text = data.price;
if (data.imageUrl == nil) {
[self.iconView setImage:TUIChatBundleThemeImage(@"chat_custom_order_message_img", @"message_custom_order")];
} else {
[self.iconView setImage:[UIImage sd_imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:data.imageUrl]]]];
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.iconView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(10);
make.leading.mas_equalTo(12);
make.width.mas_equalTo(60);
make.height.mas_equalTo(60);
}];
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(10);
make.leading.mas_equalTo(80);
make.width.mas_equalTo(150);
make.height.mas_equalTo(17);
}];
[self.descLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(30);
make.leading.mas_equalTo(80);
make.width.mas_equalTo(150);
make.height.mas_equalTo(17);
}];
[self.priceLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(49);
make.leading.mas_equalTo(80);
make.width.mas_equalTo(150);
make.height.mas_equalTo(25);
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
CGSize size = CGSizeMake(245, 80);
return size;
}
@end

View File

@@ -0,0 +1,17 @@
//
// TUIFileReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIFileReplyQuoteView : TUIVoiceReplyQuoteView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
//
// TUIFileReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIFileReplyQuoteView.h"
#import "TUIFileReplyQuoteViewData.h"
@implementation TUIFileReplyQuoteView
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
[super fillWithData:data];
if (![data isKindOfClass:TUIFileReplyQuoteViewData.class]) {
return;
}
self.textLabel.numberOfLines = 1;
self.textLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIImageReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIImageReplyQuoteView : TUIReplyQuoteView
@property(nonatomic, strong) UIImageView *imageView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,66 @@
//
// TUIImageReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIImageReplyQuoteView.h"
#import "TUIImageReplyQuoteViewData.h"
@implementation TUIImageReplyQuoteView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_imageView = [[UIImageView alloc] init];
_imageView.frame = CGRectMake(0, 0, 60, 60);
[self addSubview:_imageView];
}
return self;
}
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
[super fillWithData:data];
if (![data isKindOfClass:TUIImageReplyQuoteViewData.class]) {
return;
}
TUIImageReplyQuoteViewData *myData = (TUIImageReplyQuoteViewData *)data;
self.imageView.image = myData.image;
if (myData.image == nil && myData.imageStatus != TUIImageReplyQuoteStatusDownloading) {
[myData downloadImage];
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
TUIImageReplyQuoteViewData *myData = (TUIImageReplyQuoteViewData *)self.data;
[self.imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self);
make.top.mas_equalTo(self);
make.size.mas_equalTo(myData.imageSize);
}];
}
- (void)reset {
[super reset];
self.imageView.image = nil;
self.imageView.frame = CGRectMake(self.imageView.frame.origin.x, self.imageView.frame.origin.y, 60, 60);
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIMergeReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMergeReplyQuoteView : TUIReplyQuoteView
@property(nonatomic, strong) UILabel *titleLabel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,75 @@
//
// TUIMergeReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMergeReplyQuoteView.h"
#import <TUICore/TUIDarkModel.h>
#import <TUICore/UIView+TUILayout.h>
#import "TUIMergeReplyQuoteViewData.h"
@implementation TUIMergeReplyQuoteView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"title";
_titleLabel.font = [UIFont systemFontOfSize:10.0];
_titleLabel.textColor = [UIColor d_systemGrayColor];
_titleLabel.numberOfLines = 1;
[self addSubview:_titleLabel];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
[super fillWithData:data];
if (![data isKindOfClass:TUIMergeReplyQuoteViewData.class]) {
return;
}
TUIMergeReplyQuoteViewData *myData = (TUIMergeReplyQuoteViewData *)data;
self.titleLabel.text = myData.title;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.titleLabel sizeToFit];
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self);
make.top.mas_equalTo(self);
make.trailing.mas_equalTo(self.mas_trailing);
make.height.mas_equalTo(self.titleLabel.font.lineHeight);
}];
}
- (void)reset {
[super reset];
self.titleLabel.text = @"";
}
@end

View File

@@ -0,0 +1,43 @@
//
// TUIReferenceMessageCell.h
// TUIChat
//
// Created by wyl on 2022/5/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import "TUIChatDefine.h"
@class TUIReplyMessageCellData;
@class TUIReplyQuoteViewData;
@class TUIImageVideoReplyQuoteViewData;
@class TUIVoiceFileReplyQuoteViewData;
@class TUIMergeReplyQuoteViewData;
@class TUIReferenceMessageCellData;
@class TUITextView;
NS_ASSUME_NONNULL_BEGIN
@interface TUIReferenceMessageCell : TUIBubbleMessageCell
/**
*
* Border of quote view
*/
@property(nonatomic, strong) CALayer *quoteBorderLayer;
@property(nonatomic, strong) UIView *quoteView;
@property(nonatomic, strong) UILabel *senderLabel;
@property(nonatomic, strong) TUIReferenceMessageCellData *referenceData;
@property(nonatomic, strong) TUITextView *textView;
@property(nonatomic, strong) NSString *selectContent;
@property(nonatomic, strong) TUIReferenceSelectAllContentCallback selectAllContentContent;
- (void)fillWithData:(TUIReferenceMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,488 @@
//
// TUIReferenceMessageCell.m
// TUIChat
//
// Created by wyl on 2022/5/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReferenceMessageCell.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/UIView+TUILayout.h>
#import "TUIFileMessageCellData.h"
#import "TUIImageMessageCellData.h"
#import "TUILinkCellData.h"
#import "TUIMergeMessageCellData.h"
#import "TUIReplyMessageCell.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCellData.h"
#import "TUIVideoMessageCellData.h"
#import "TUIVoiceMessageCellData.h"
#import "TUIFileReplyQuoteView.h"
#import "TUIImageReplyQuoteView.h"
#import "TUIMergeReplyQuoteView.h"
#import "TUIReplyQuoteView.h"
#import "TUITextMessageCell.h"
#import "TUITextReplyQuoteView.h"
#import "TUIVideoReplyQuoteView.h"
#import "TUIVoiceReplyQuoteView.h"
#ifndef CGFLOAT_CEIL
#ifdef CGFLOAT_IS_DOUBLE
#define CGFLOAT_CEIL(value) ceil(value)
#else
#define CGFLOAT_CEIL(value) ceilf(value)
#endif
#endif
@interface TUIReferenceMessageCell () <UITextViewDelegate,TUITextViewDelegate>
@property(nonatomic, strong) TUIReplyQuoteView *currentOriginView;
@property(nonatomic, strong) NSMutableDictionary<NSString *, TUIReplyQuoteView *> *customOriginViewsCache;
@end
@implementation TUIReferenceMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self setupViews];
}
return self;
}
- (void)setupViews {
[self setupContentTextView];
[self.quoteView addSubview:self.senderLabel];
[self.contentView addSubview:self.quoteView];
self.bottomContainer = [[UIView alloc] init];
[self.contentView addSubview:self.bottomContainer];
}
- (void)setupContentTextView {
self.textView = [[TUITextView alloc] init];
self.textView.backgroundColor = [UIColor clearColor];
self.textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
self.textView.textContainer.lineFragmentPadding = 0;
self.textView.scrollEnabled = NO;
self.textView.editable = NO;
self.textView.delegate = self;
self.textView.tuiTextViewDelegate = self;
self.textView.font = [UIFont systemFontOfSize:16.0];
self.textView.textColor = TUIChatDynamicColor(@"chat_reference_message_content_text_color", @"#000000");
[self.bubbleView addSubview:self.textView];
}
- (void)onLongPressTextViewMessage:(UITextView *)textView {
if (self.delegate && [self.delegate respondsToSelector:@selector(onLongPressMessage:)]) {
[self.delegate onLongPressMessage:self];
}
}
- (void)fillWithData:(TUIReferenceMessageCellData *)data {
[super fillWithData:data];
self.referenceData = data;
self.senderLabel.text = [NSString stringWithFormat:@"%@:", data.sender];
self.selectContent = data.content;
self.textView.attributedText = [data.content getFormatEmojiStringWithFont:self.textView.font emojiLocations:self.referenceData.emojiLocations];
self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
@weakify(self);
[[RACObserve(data, originMessage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(V2TIMMessage *originMessage) {
@strongify(self);
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self updateUI:self.referenceData];
[self layoutBottomContainer];
}
// Override
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.referenceData};
[TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
}
- (void)updateUI:(TUIReferenceMessageCellData *)referenceData {
self.currentOriginView = [self getCustomOriginView:referenceData.originCellData];
[self hiddenAllCustomOriginViews:YES];
self.currentOriginView.hidden = NO;
referenceData.quoteData.supportForReply = NO;
referenceData.quoteData.showRevokedOriginMessage = referenceData.showRevokedOriginMessage;
[self.currentOriginView fillWithData:referenceData.quoteData];
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.bubbleView.mas_leading).mas_offset(self.referenceData.textOrigin.x);
make.top.mas_equalTo(self.bubbleView.mas_top).mas_offset(self.referenceData.textOrigin.y);
make.size.mas_equalTo(self.referenceData.textSize);
}];
if (referenceData.direction == MsgDirectionIncoming) {
self.textView.textColor = TUIChatDynamicColor(@"chat_reference_message_content_recv_text_color", @"#000000");
self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_recv_text_color", @"#888888");
self.quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_bg_color", @"#4444440c");
} else {
self.textView.textColor = TUIChatDynamicColor(@"chat_reference_message_content_text_color", @"#000000");
self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_text_color", @"#888888");
self.quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_bg_color", @"#4444440c");
}
if (referenceData.textColor) {
self.textView.textColor = referenceData.textColor;
}
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.textView.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
make.bottom.mas_equalTo(self.container);
}];
}
[self.senderLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.quoteView).mas_offset(6);
make.top.mas_equalTo(self.quoteView).mas_offset(8);
make.width.mas_equalTo(referenceData.senderSize.width);
make.height.mas_equalTo(referenceData.senderSize.height);
}];
BOOL hideSenderLabel = (referenceData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
!referenceData.showRevokedOriginMessage;
if (hideSenderLabel) {
self.senderLabel.hidden = YES;
} else {
self.senderLabel.hidden = NO;
}
[self.quoteView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.referenceData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(self.bubbleView);
}
else {
make.trailing.mas_equalTo(self.bubbleView);
}
make.top.mas_equalTo(self.container.mas_bottom).mas_offset(6);
make.size.mas_equalTo(self.referenceData.quoteSize);
}];
if (self.referenceData.showMessageModifyReplies) {
[self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.referenceData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(self.quoteView.mas_leading);
}
else {
make.trailing.mas_equalTo(self.quoteView.mas_trailing);
}
make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(3);
make.size.mas_equalTo(self.messageModifyRepliesButton.frame.size);
}];
}
[self.currentOriginView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (hideSenderLabel) {
make.leading.mas_equalTo(self.quoteView).mas_offset(6);
make.top.mas_equalTo(self.quoteView).mas_offset(8);
make.trailing.mas_lessThanOrEqualTo(self.quoteView.mas_trailing);
make.height.mas_equalTo(self.referenceData.quotePlaceholderSize);
}
else {
make.leading.mas_equalTo(self.senderLabel.mas_trailing).mas_offset(3);
make.top.mas_equalTo(self.senderLabel.mas_top).mas_offset(1);
make.trailing.mas_lessThanOrEqualTo(self.quoteView.mas_trailing);
make.height.mas_equalTo(self.referenceData.quotePlaceholderSize);
}
}];
}
- (TUIReplyQuoteView *)getCustomOriginView:(TUIMessageCellData *)originCellData {
NSString *reuseId = originCellData ? NSStringFromClass(originCellData.class) : NSStringFromClass(TUITextMessageCellData.class);
TUIReplyQuoteView *view = nil;
BOOL reuse = NO;
BOOL hasRiskContent = originCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
reuseId = @"hasRiskContent";
}
if ([self.customOriginViewsCache.allKeys containsObject:reuseId]) {
view = [self.customOriginViewsCache objectForKey:reuseId];
reuse = YES;
}
if (hasRiskContent && view == nil){
TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
view = quoteView;
}
if (view == nil) {
Class class = [originCellData getReplyQuoteViewClass];
if (class) {
view = [[class alloc] init];
}
}
if (view == nil) {
TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
view = quoteView;
}
if ([view isKindOfClass:[TUITextReplyQuoteView class]]) {
TUITextReplyQuoteView *quoteView = (TUITextReplyQuoteView *)view;
if (self.referenceData.direction == MsgDirectionIncoming) {
quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_recv_text_color", @"#888888");
} else {
quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_text_color", @"#888888");
}
} else if ([view isKindOfClass:[TUIMergeReplyQuoteView class]]) {
TUIMergeReplyQuoteView *quoteView = (TUIMergeReplyQuoteView *)view;
if (self.referenceData.direction == MsgDirectionIncoming) {
quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_recv_text_color", @"#888888");
} else {
quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_text_color", @"#888888");
}
}
if (!reuse) {
[self.customOriginViewsCache setObject:view forKey:reuseId];
[self.quoteView addSubview:view];
}
view.hidden = YES;
return view;
}
- (void)hiddenAllCustomOriginViews:(BOOL)hidden {
[self.customOriginViewsCache enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, TUIReplyQuoteView *_Nonnull obj, BOOL *_Nonnull stop) {
obj.hidden = hidden;
[obj reset];
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)layoutBottomContainer {
if (CGSizeEqualToSize(self.referenceData.bottomContainerSize, CGSizeZero)) {
return;
}
CGSize size = self.referenceData.bottomContainerSize;
CGFloat topMargin = self.bubbleView.mm_maxY + self.nameLabel.mm_h + 6;
[self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
if (!self.messageModifyRepliesButton.isHidden){
make.top.mas_equalTo(self.messageModifyRepliesButton.mas_bottom).mas_offset(8);
}
else {
make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(8);
}
make.size.mas_equalTo(size);
if (self.referenceData.direction == MsgDirectionOutgoing) {
make.trailing.mas_equalTo(self.container);
}
else {
make.leading.mas_equalTo(self.container);
}
}];
if (!self.quoteView.hidden) {
CGRect oldRect = self.quoteView.frame;
CGRect newRect = CGRectMake(oldRect.origin.x, CGRectGetMaxY(self.bottomContainer.frame) + 5, oldRect.size.width, oldRect.size.height);
self.quoteView.frame = newRect;
}
if (!self.messageModifyRepliesButton.hidden) {
CGRect oldRect = self.messageModifyRepliesButton.frame;
CGRect newRect = CGRectMake(oldRect.origin.x, CGRectGetMaxY(self.quoteView.frame), oldRect.size.width, oldRect.size.height);
self.messageModifyRepliesButton.frame = newRect;
}
}
- (UILabel *)senderLabel {
if (_senderLabel == nil) {
_senderLabel = [[UILabel alloc] init];
_senderLabel.text = @"harvy:";
_senderLabel.font = [UIFont systemFontOfSize:12.0];
_senderLabel.textColor = TUIChatDynamicColor(@"chat_reference_message_sender_text_color", @"#888888");
}
return _senderLabel;
}
- (UIView *)quoteView {
if (_quoteView == nil) {
_quoteView = [[UIView alloc] init];
_quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reference_message_quoteView_bg_color", @"#4444440c");
_quoteView.layer.cornerRadius = 4.0;
_quoteView.layer.masksToBounds = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(quoteViewOnTap)];
[_quoteView addGestureRecognizer:tap];
}
return _quoteView;
}
- (void)quoteViewOnTap {
if (self.delegate && [self.delegate respondsToSelector:@selector(onSelectMessage:)]) {
[self.delegate onSelectMessage:self];
}
}
- (NSMutableDictionary *)customOriginViewsCache {
if (_customOriginViewsCache == nil) {
_customOriginViewsCache = [[NSMutableDictionary alloc] init];
}
return _customOriginViewsCache;
}
- (void)textViewDidChangeSelection:(UITextView *)textView {
NSAttributedString *selectedString = [textView.attributedText attributedSubstringFromRange:textView.selectedRange];
if (self.selectAllContentContent && selectedString) {
if (selectedString.length == textView.attributedText.length) {
self.selectAllContentContent(YES);
} else {
self.selectAllContentContent(NO);
}
}
if (selectedString.length > 0) {
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
[attributedString appendAttributedString:selectedString];
NSUInteger offsetLocation = 0;
for (NSDictionary *emojiLocation in self.referenceData.emojiLocations) {
NSValue *key = emojiLocation.allKeys.firstObject;
NSAttributedString *originStr = emojiLocation[key];
NSRange currentRange = [key rangeValue];
currentRange.location += offsetLocation;
if (currentRange.location >= textView.selectedRange.location) {
currentRange.location -= textView.selectedRange.location;
if (currentRange.location + currentRange.length <= attributedString.length) {
[attributedString replaceCharactersInRange:currentRange withAttributedString:originStr];
offsetLocation += originStr.length - currentRange.length;
}
}
}
self.selectContent = attributedString.string;
} else {
self.selectContent = nil;
}
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
NSAssert([data isKindOfClass:TUIReferenceMessageCellData.class], @"data must be kind of TUIReferenceMessageCellData");
TUIReferenceMessageCellData *referenceCellData = (TUIReferenceMessageCellData *)data;
CGFloat cellHeight = [super getHeight:data withWidth:width];
cellHeight += referenceCellData.quoteSize.height + referenceCellData.bottomContainerSize.height;
cellHeight += kScale375(6);
return cellHeight;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIReferenceMessageCellData.class], @"data must be kind of TUIReferenceMessageCellData");
TUIReferenceMessageCellData *referenceCellData = (TUIReferenceMessageCellData *)data;
CGFloat quoteHeight = 0;
CGFloat quoteWidth = 0;
CGFloat quoteMaxWidth = TReplyQuoteView_Max_Width;
CGFloat quotePlaceHolderMarginWidth = 12;
// Calculate the size of label which displays the sender's displayname
CGSize senderSize = [@"0" sizeWithAttributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}];
CGRect senderRect = [[NSString stringWithFormat:@"%@:",referenceCellData.sender] boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
context:nil];
// Calculate the size of customize quote placeholder view
CGSize placeholderSize = [referenceCellData quotePlaceholderSizeWithType:referenceCellData.originMsgType data:referenceCellData.quoteData];
// Calculate the size of revoke string
CGRect messageRevokeRect = CGRectZero;
bool showRevokeStr = (referenceCellData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
!referenceCellData.showRevokedOriginMessage;
if (showRevokeStr) {
NSString *msgRevokeStr = TIMCommonLocalizableString(TUIKitReferenceOriginMessageRevoke);
messageRevokeRect = [msgRevokeStr boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
context:nil];
}
// Calculate the size of label which displays the content of replying the original message
NSAttributedString *attributeString = [referenceCellData.content getFormatEmojiStringWithFont:[UIFont systemFontOfSize:16.0] emojiLocations:nil];
CGRect replyContentRect = [attributeString boundingRectWithSize:CGSizeMake(TTextMessageCell_Text_Width_Max, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
CGSize size = CGSizeMake(CGFLOAT_CEIL(replyContentRect.size.width), CGFLOAT_CEIL(replyContentRect.size.height));
referenceCellData.textSize = size;
referenceCellData.textOrigin = CGPointMake(referenceCellData.cellLayout.bubbleInsets.left,
referenceCellData.cellLayout.bubbleInsets.top + [TUIBubbleMessageCell getBubbleTop:referenceCellData]);
size.height += referenceCellData.cellLayout.bubbleInsets.top + referenceCellData.cellLayout.bubbleInsets.bottom;
size.width += referenceCellData.cellLayout.bubbleInsets.left + referenceCellData.cellLayout.bubbleInsets.right;
if (referenceCellData.direction == MsgDirectionIncoming) {
size.height = MAX(size.height, TUIBubbleMessageCell.incommingBubble.size.height);
} else {
size.height = MAX(size.height, TUIBubbleMessageCell.outgoingBubble.size.height);
}
BOOL hasRiskContent = referenceCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
size.height += kTUISecurityStrikeViewTopLineMargin;
size.height += kTUISecurityStrikeViewTopLineToBottom;
}
quoteWidth = senderRect.size.width;
quoteWidth += placeholderSize.width;
quoteWidth += (quotePlaceHolderMarginWidth * 2);
if (showRevokeStr) {
quoteWidth = messageRevokeRect.size.width;
}
quoteHeight = MAX(senderRect.size.height, placeholderSize.height);
quoteHeight += (8 + 8);
referenceCellData.senderSize = CGSizeMake(senderRect.size.width, senderRect.size.height);
referenceCellData.quotePlaceholderSize = placeholderSize;
// self.replyContentSize = CGSizeMake(replyContentRect.size.width, replyContentRect.size.height);
referenceCellData.quoteSize = CGSizeMake(quoteWidth, quoteHeight);
return size;
}
@end

View File

@@ -0,0 +1,38 @@
//
// TUIReplyMessageCell.h
// TUIChat
//
// Created by harvy on 2021/11/11.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUITextView.h>
#import "TUIChatDefine.h"
@class TUIReplyMessageCellData;
@class TUIReplyQuoteViewData;
@class TUIImageVideoReplyQuoteViewData;
@class TUIVoiceFileReplyQuoteViewData;
@class TUIMergeReplyQuoteViewData;
NS_ASSUME_NONNULL_BEGIN
@interface TUIReplyMessageCell : TUIBubbleMessageCell
@property(nonatomic, strong) UIView *quoteBorderLine;
@property(nonatomic, strong) UIView *quoteView;
@property(nonatomic, strong) TUITextView *textView;
@property(nonatomic, strong) NSString *selectContent;
@property(nonatomic, strong) TUIReplySelectAllContentCallback selectAllContentContent;
@property(nonatomic, strong) UILabel *senderLabel;
@property(nonatomic, strong) TUIReplyMessageCellData *replyData;
- (void)fillWithData:(TUIReplyMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,484 @@
//
// TUIReplyMessageCell.m
// TUIChat
//
// Created by harvy on 2021/11/11.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyMessageCell.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/UIView+TUILayout.h>
#import "TUIFileMessageCellData.h"
#import "TUIImageMessageCellData.h"
#import "TUILinkCellData.h"
#import "TUIMergeMessageCellData.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCellData.h"
#import "TUIVideoMessageCellData.h"
#import "TUIVoiceMessageCellData.h"
#import "TUIFileReplyQuoteView.h"
#import "TUIImageReplyQuoteView.h"
#import "TUIMergeReplyQuoteView.h"
#import "TUIReplyQuoteView.h"
#import "TUITextReplyQuoteView.h"
#import "TUIVideoReplyQuoteView.h"
#import "TUIVoiceReplyQuoteView.h"
@interface TUIReplyMessageCell () <UITextViewDelegate,TUITextViewDelegate>
@property(nonatomic, strong) TUIReplyQuoteView *currentOriginView;
@property(nonatomic, strong) NSMutableDictionary<NSString *, TUIReplyQuoteView *> *customOriginViewsCache;
@end
@implementation TUIReplyMessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self setupViews];
}
return self;
}
- (void)setupViews {
[self.quoteView addSubview:self.senderLabel];
[self.quoteView addSubview:self.quoteBorderLine];
[self.bubbleView addSubview:self.quoteView];
[self.bubbleView addSubview:self.textView];
self.bottomContainer = [[UIView alloc] init];
[self.contentView addSubview:self.bottomContainer];
}
// Override
- (void)notifyBottomContainerReadyOfData:(TUIMessageCellData *)cellData {
NSDictionary *param = @{TUICore_TUIChatExtension_BottomContainer_CellData : self.replyData};
[TUICore raiseExtension:TUICore_TUIChatExtension_BottomContainer_ClassicExtensionID parentView:self.bottomContainer param:param];
}
- (void)fillWithData:(TUIReplyMessageCellData *)data {
[super fillWithData:data];
self.replyData = data;
self.senderLabel.text = [NSString stringWithFormat:@"%@:", data.sender];
self.textView.attributedText = [data.content getFormatEmojiStringWithFont:self.textView.font emojiLocations:self.replyData.emojiLocations];
self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
if (data.direction == MsgDirectionIncoming) {
self.textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_recv_text_color", @"#000000");
self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
self.quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_bg_color", @"#4444440c");
} else {
self.textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_text_color", @"#000000");
self.senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
self.quoteView.backgroundColor = [UIColor colorWithRed:68 / 255.0 green:68 / 255.0 blue:68 / 255.0 alpha:0.05];
}
@weakify(self);
[[RACObserve(data, originMessage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(V2TIMMessage *originMessage) {
@strongify(self);
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}];
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self updateUI:self.replyData];
[self layoutBottomContainer];
}
- (void)updateUI:(TUIReplyMessageCellData *)replyData {
self.currentOriginView = [self getCustomOriginView:replyData.originCellData];
[self hiddenAllCustomOriginViews:YES];
self.currentOriginView.hidden = NO;
replyData.quoteData.supportForReply = YES;
replyData.quoteData.showRevokedOriginMessage = replyData.showRevokedOriginMessage;
[self.currentOriginView fillWithData:replyData.quoteData];
[self.quoteView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.bubbleView).mas_offset(16);
make.top.mas_equalTo(12);
make.trailing.mas_lessThanOrEqualTo(self.bubbleView).mas_offset(-16);
make.width.mas_greaterThanOrEqualTo(self.senderLabel);
make.height.mas_equalTo(self.replyData.quoteSize.height);
}];
[self.quoteBorderLine mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.quoteView);
make.top.mas_equalTo(self.quoteView);
make.width.mas_equalTo(3);
make.bottom.mas_equalTo(self.quoteView);
}];
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.quoteView).mas_offset(4);
make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(12);
make.trailing.mas_lessThanOrEqualTo(self.quoteView).mas_offset(-4);;
make.bottom.mas_equalTo(self.bubbleView).mas_offset(-4);
}];
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.textView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.quoteView).mas_offset(4);
make.top.mas_equalTo(self.quoteView.mas_bottom).mas_offset(12);
make.trailing.mas_lessThanOrEqualTo(self.quoteView).mas_offset(-4);
make.size.mas_equalTo(self.replyData.replyContentSize);
}];
[self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.textView.mas_bottom);
make.width.mas_equalTo(self.bubbleView);
make.bottom.mas_equalTo(self.container);
}];
}
[self.senderLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.textView);
make.top.mas_equalTo(3);
make.size.mas_equalTo(self.replyData.senderSize);
}];
BOOL hideSenderLabel = (replyData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
!replyData.showRevokedOriginMessage;
if (hideSenderLabel) {
self.senderLabel.hidden = YES;
} else {
self.senderLabel.hidden = NO;
}
[self.currentOriginView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.senderLabel);
if (hideSenderLabel) {
make.centerY.mas_equalTo(self.quoteView);
} else {
make.top.mas_equalTo(self.senderLabel.mas_bottom).mas_offset(4);
}
// make.width.mas_greaterThanOrEqualTo(self.replyData.quotePlaceholderSize);
make.trailing.mas_lessThanOrEqualTo(self.quoteView.mas_trailing);
make.height.mas_equalTo(self.replyData.quotePlaceholderSize);
}];
}
- (TUIReplyQuoteView *)getCustomOriginView:(TUIMessageCellData *)originCellData {
NSString *reuseId = originCellData ? NSStringFromClass(originCellData.class) : NSStringFromClass(TUITextMessageCellData.class);
TUIReplyQuoteView *view = nil;
BOOL reuse = NO;
BOOL hasRiskContent = originCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
reuseId = @"hasRiskContent";
}
if ([self.customOriginViewsCache.allKeys containsObject:reuseId]) {
view = [self.customOriginViewsCache objectForKey:reuseId];
reuse = YES;
}
if (hasRiskContent && view == nil){
TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
view = quoteView;
}
if (view == nil) {
Class class = [originCellData getReplyQuoteViewClass];
if (class) {
view = [[class alloc] init];
}
}
if (view == nil) {
TUITextReplyQuoteView *quoteView = [[TUITextReplyQuoteView alloc] init];
view = quoteView;
}
if ([view isKindOfClass:[TUITextReplyQuoteView class]]) {
TUITextReplyQuoteView *quoteView = (TUITextReplyQuoteView *)view;
if (self.replyData.direction == MsgDirectionIncoming) {
quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
} else {
quoteView.textLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
}
} else if ([view isKindOfClass:[TUIMergeReplyQuoteView class]]) {
TUIMergeReplyQuoteView *quoteView = (TUIMergeReplyQuoteView *)view;
if (self.replyData.direction == MsgDirectionIncoming) {
quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_recv_text_color", @"#888888");
} else {
quoteView.titleLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_text_color", @"#888888");
}
}
if (!reuse) {
[self.customOriginViewsCache setObject:view forKey:reuseId];
[self.quoteView addSubview:view];
}
view.hidden = YES;
return view;
}
- (void)hiddenAllCustomOriginViews:(BOOL)hidden {
[self.customOriginViewsCache enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, TUIReplyQuoteView *_Nonnull obj, BOOL *_Nonnull stop) {
obj.hidden = hidden;
[obj reset];
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)layoutBottomContainer {
if (CGSizeEqualToSize(self.replyData.bottomContainerSize, CGSizeZero)) {
return;
}
CGSize size = self.replyData.bottomContainerSize;
CGFloat topMargin = self.bubbleView.mm_maxY + self.nameLabel.mm_h + 8;
[self.bottomContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.bubbleView.mas_bottom).mas_offset(8);
make.size.mas_equalTo(size);
if (self.replyData.direction == MsgDirectionOutgoing) {
make.trailing.mas_equalTo(self.container);
}
else {
make.leading.mas_equalTo(self.container);
}
}];
if (!self.messageModifyRepliesButton.hidden) {
CGRect oldRect = self.messageModifyRepliesButton.frame;
CGRect newRect = CGRectMake(oldRect.origin.x, CGRectGetMaxY(self.bottomContainer.frame), oldRect.size.width, oldRect.size.height);
self.messageModifyRepliesButton.frame = newRect;
}
}
- (UILabel *)senderLabel {
if (_senderLabel == nil) {
_senderLabel = [[UILabel alloc] init];
_senderLabel.text = @"harvy:";
_senderLabel.font = [UIFont boldSystemFontOfSize:12.0];
_senderLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_sender_text_color", @"#888888");
_senderLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
}
return _senderLabel;
}
- (UIView *)quoteView {
if (_quoteView == nil) {
_quoteView = [[UIView alloc] init];
_quoteView.backgroundColor = TUIChatDynamicColor(@"chat_reply_message_quoteView_bg_color", @"#4444440c");
}
return _quoteView;
}
- (UIView *)quoteBorderLine {
if (_quoteBorderLine == nil) {
_quoteBorderLine = [[UIView alloc] init];
_quoteBorderLine.backgroundColor = [UIColor colorWithRed:68 / 255.0 green:68 / 255.0 blue:68 / 255.0 alpha:0.1];
}
return _quoteBorderLine;
}
- (TUITextView *)textView {
if (_textView == nil) {
_textView = [[TUITextView alloc] init];
_textView.font = [UIFont systemFontOfSize:16.0];
_textView.textColor = TUIChatDynamicColor(@"chat_reply_message_content_text_color", @"#000000");
_textView.backgroundColor = [UIColor clearColor];
_textView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
_textView.textContainer.lineFragmentPadding = 0;
_textView.scrollEnabled = NO;
_textView.editable = NO;
_textView.delegate = self;
_textView.tuiTextViewDelegate = self;
_textView.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
}
return _textView;
}
- (void)onLongPressTextViewMessage:(UITextView *)textView {
if (self.delegate && [self.delegate respondsToSelector:@selector(onLongPressMessage:)]) {
[self.delegate onLongPressMessage:self];
}
}
- (NSMutableDictionary *)customOriginViewsCache {
if (_customOriginViewsCache == nil) {
_customOriginViewsCache = [[NSMutableDictionary alloc] init];
}
return _customOriginViewsCache;
}
- (void)textViewDidChangeSelection:(UITextView *)textView {
NSAttributedString *selectedString = [textView.attributedText attributedSubstringFromRange:textView.selectedRange];
if (self.selectAllContentContent && selectedString) {
if (selectedString.length == textView.attributedText.length) {
self.selectAllContentContent(YES);
} else {
self.selectAllContentContent(NO);
}
}
if (selectedString.length > 0) {
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
[attributedString appendAttributedString:selectedString];
NSUInteger offsetLocation = 0;
for (NSDictionary *emojiLocation in self.replyData.emojiLocations) {
NSValue *key = emojiLocation.allKeys.firstObject;
NSAttributedString *originStr = emojiLocation[key];
NSRange currentRange = [key rangeValue];
currentRange.location += offsetLocation;
if (currentRange.location >= textView.selectedRange.location) {
currentRange.location -= textView.selectedRange.location;
if (currentRange.location + currentRange.length <= attributedString.length) {
[attributedString replaceCharactersInRange:currentRange withAttributedString:originStr];
offsetLocation += originStr.length - currentRange.length;
}
}
}
self.selectContent = attributedString.string;
} else {
self.selectContent = nil;
}
}
#pragma mark - TUIMessageCellProtocol
+ (CGFloat)getHeight:(TUIMessageCellData *)data withWidth:(CGFloat)width {
NSAssert([data isKindOfClass:TUIReplyMessageCellData.class], @"data must be kind of TUIReplyMessageCellData");
TUIReplyMessageCellData *replyCellData = (TUIReplyMessageCellData *)data;
CGFloat height = [super getHeight:replyCellData withWidth:width];
if (replyCellData.bottomContainerSize.height > 0) {
height += replyCellData.bottomContainerSize.height + kScale375(6);
}
return height;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIReplyMessageCellData.class], @"data must be kind of TUIReplyMessageCellData");
TUIReplyMessageCellData *replyCellData = (TUIReplyMessageCellData *)data;
CGFloat height = 0;
CGFloat quoteHeight = 0;
CGFloat quoteWidth = 0;
CGFloat quoteMinWidth = 100;
CGFloat quoteMaxWidth = TReplyQuoteView_Max_Width;
CGFloat quotePlaceHolderMarginWidth = 12;
UIFont *font = [UIFont systemFontOfSize:16.0];
// Calculate the size of label which displays the sender's displyname
CGSize senderSize = [@"0" sizeWithAttributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}];
CGRect senderRect = [replyCellData.sender boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
context:nil];
// Calculate the size of revoke string
CGRect messageRevokeRect = CGRectZero;
BOOL showRevokeStr = (replyCellData.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
!replyCellData.showRevokedOriginMessage;
if (showRevokeStr) {
NSString *msgRevokeStr = TIMCommonLocalizableString(TUIKitRepliesOriginMessageRevoke);
messageRevokeRect = [msgRevokeStr boundingRectWithSize:CGSizeMake(quoteMaxWidth, senderSize.height)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:12.0]}
context:nil];
}
// Calculate the size of customize quote placeholder view
CGSize placeholderSize = [replyCellData quotePlaceholderSizeWithType:replyCellData.originMsgType data:replyCellData.quoteData];
// Calculate the size of label which displays the content of replying the original message
NSAttributedString *attributeString = [replyCellData.content getFormatEmojiStringWithFont:font emojiLocations:nil];
CGRect replyContentRect = [attributeString boundingRectWithSize:CGSizeMake(quoteMaxWidth, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
// Calculate the size of quote view base the content
quoteWidth = senderRect.size.width;
if (quoteWidth < placeholderSize.width) {
quoteWidth = placeholderSize.width;
}
if (quoteWidth < replyContentRect.size.width) {
quoteWidth = replyContentRect.size.width;
}
quoteWidth += quotePlaceHolderMarginWidth;
BOOL lineSpacingChecked = NO ;
if (quoteWidth > quoteMaxWidth) {
quoteWidth = quoteMaxWidth;
//line spacing
lineSpacingChecked = YES;
}
if (quoteWidth < quoteMinWidth) {
quoteWidth = quoteMinWidth;
}
if (showRevokeStr) {
quoteWidth = MAX(quoteWidth, messageRevokeRect.size.width);
}
quoteHeight = 3 + senderRect.size.height + 4 + placeholderSize.height + 6;
replyCellData.senderSize = CGSizeMake(quoteWidth, senderRect.size.height);
replyCellData.quotePlaceholderSize = placeholderSize;
replyCellData.replyContentSize = CGSizeMake(replyContentRect.size.width, replyContentRect.size.height);
replyCellData.quoteSize = CGSizeMake(quoteWidth, quoteHeight);
// cell
// Calculate the height of cell
height = 12 + quoteHeight + 12 + replyCellData.replyContentSize.height + 12;
CGRect replyContentRect2 = [attributeString boundingRectWithSize:CGSizeMake(MAXFLOAT, [font lineHeight])
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
context:nil];
// Determine whether the width of the last line exceeds the position of the message status. If it exceeds, the message status will be wrapped.
if (lineSpacingChecked) {
if ((int)replyContentRect2.size.width % (int)quoteWidth == 0 ||
(int)replyContentRect2.size.width % (int)quoteWidth + font.lineHeight > quoteWidth) {
height += font.lineHeight;
}
}
CGSize size = CGSizeMake(quoteWidth + TReplyQuoteView_Margin_Width, height);
BOOL hasRiskContent = replyCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
size.height += kTUISecurityStrikeViewTopLineMargin;
size.height += kTUISecurityStrikeViewTopLineToBottom;
}
return size;
}
@end

View File

@@ -0,0 +1,23 @@
//
// TUIReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
@class TUIReplyQuoteViewData;
NS_ASSUME_NONNULL_BEGIN
@interface TUIReplyQuoteView : UIView
@property(nonatomic, strong) TUIReplyQuoteViewData *data;
- (void)fillWithData:(TUIReplyQuoteViewData *)data;
- (void)reset;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
//
// TUIReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyQuoteView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import "TUIReplyQuoteViewData.h"
@implementation TUIReplyQuoteView
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
_data = data;
}
- (void)reset {
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUITextReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUITextReplyQuoteView : TUIReplyQuoteView
@property(nonatomic, strong) UILabel *textLabel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,72 @@
//
// TUITextReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUITextReplyQuoteView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TUICore/TUIThemeManager.h>
#import "TUITextReplyQuoteViewData.h"
@implementation TUITextReplyQuoteView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_textLabel = [[UILabel alloc] init];
_textLabel.font = [UIFont systemFontOfSize:10.0];
_textLabel.textColor = TUIChatDynamicColor(@"chat_reply_message_sender_text_color", @"888888");
_textLabel.numberOfLines = 2;
[self addSubview:_textLabel];
}
return self;
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.textLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
}
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
[super fillWithData:data];
if (![data isKindOfClass:TUITextReplyQuoteViewData.class]) {
return;
}
TUITextReplyQuoteViewData *myData = (TUITextReplyQuoteViewData *)data;
BOOL showRevokeStr = data.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED &&
!data.showRevokedOriginMessage;
if (showRevokeStr) {
NSString* revokeStr = data.supportForReply ?
TIMCommonLocalizableString(TUIKitRepliesOriginMessageRevoke) :
TIMCommonLocalizableString(TUIKitReferenceOriginMessageRevoke);
self.textLabel.attributedText = [revokeStr getFormatEmojiStringWithFont:self.textLabel.font emojiLocations:nil];
}
else {
self.textLabel.attributedText = [myData.text getFormatEmojiStringWithFont:self.textLabel.font emojiLocations:nil];
}
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
- (void)reset {
self.textLabel.text = @"";
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIVideoReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIImageReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIVideoReplyQuoteView : TUIImageReplyQuoteView
@property(nonatomic, strong) UIImageView *playView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,61 @@
//
// TUIVideoReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIDarkModel.h>
#import "TUIVideoReplyQuoteView.h"
#import "TUIVideoReplyQuoteViewData.h"
@implementation TUIVideoReplyQuoteView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_playView = [[UIImageView alloc] init];
_playView.image = TUIChatCommonBundleImage(@"play_normal");
_playView.frame = CGRectMake(0, 0, 30, 30);
[self addSubview:_playView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
TUIVideoReplyQuoteViewData *myData = (TUIVideoReplyQuoteViewData *)self.data;
[self.imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.top.mas_equalTo(self);
if (CGSizeEqualToSize(CGSizeZero, myData.imageSize)) {
make.size.mas_equalTo(CGSizeMake(60, 60));
}
else {
make.size.mas_equalTo(myData.imageSize);
}
}];
[self.playView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(30, 30));
make.center.mas_equalTo(self.imageView);
}];
}
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
//TUIImageReplyQuoteView deal Image
[super fillWithData:data];
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIVoiceReplyQuoteView.h
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUITextReplyQuoteView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIVoiceReplyQuoteView : TUITextReplyQuoteView
@property(nonatomic, strong) UIImageView *iconView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,79 @@
//
// TUIVoiceReplyQuoteView.m
// TUIChat
//
// Created by harvy on 2021/11/25.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceReplyQuoteView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIDarkModel.h>
#import "TUIVoiceReplyQuoteViewData.h"
@implementation TUIVoiceReplyQuoteView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_iconView = [[UIImageView alloc] init];
_iconView.image = TUIChatCommonBundleImage(@"message_voice_receiver_normal");
[self addSubview:_iconView];
self.textLabel.numberOfLines = 1;
self.textLabel.font = [UIFont systemFontOfSize:10.0];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (void)fillWithData:(TUIReplyQuoteViewData *)data {
[super fillWithData:data];
if (![data isKindOfClass:TUIVoiceReplyQuoteViewData.class]) {
return;
}
TUIVoiceReplyQuoteViewData *myData = (TUIVoiceReplyQuoteViewData *)data;
self.iconView.image = myData.icon;
self.textLabel.numberOfLines = 1;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
[self.iconView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self);
make.top.mas_equalTo(self);
make.width.mas_equalTo(15);
make.height.mas_equalTo(15);
}];
[self.textLabel sizeToFit];
[self.textLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.iconView.mas_trailing).mas_offset(3);
make.centerY.mas_equalTo(self.mas_centerY);
make.trailing.mas_equalTo(self.mas_trailing);
make.height.mas_equalTo(self.textLabel.font.lineHeight);
}];
}
@end