增加换肤功能

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,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