增加换肤功能

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,17 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell_Minimalist.h>
#import "TUIFaceMessageCellData.h"
@interface TUIFaceMessageCell_Minimalist : TUIBubbleMessageCell_Minimalist
/**
* 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,97 @@
//
// FaceMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIFaceMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIFaceMessageCell_Minimalist ()
@end
@implementation TUIFaceMessageCell_Minimalist
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_face = [[UIImageView alloc] init];
_face.contentMode = UIViewContentModeScaleAspectFit;
[self.bubbleView addSubview:_face];
_face.mm_fill();
_face.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_face.backgroundColor = RGBA(236, 240, 246, 1);
}
return self;
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGFloat topMargin = 5;
CGFloat height = self.container.mm_h;
[self.face mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(kScale390(88));
make.centerX.mas_equalTo(self.container.mas_centerX);
make.top.mas_equalTo(topMargin);
make.width.mas_equalTo(kScale390(90));
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
- (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;
}
imageWidth += kScale390(30);
imageHeight += kScale390(30);
return CGSizeMake(imageWidth, imageHeight);
}
@end

View File

@@ -0,0 +1,39 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TUIBubbleMessageCell_Minimalist.h>
#import "TUIFileMessageCellData.h"
@interface TUIFileMessageCell_Minimalist : TUIBubbleMessageCell_Minimalist
/**
* 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 *fileImage;
/**
* Download icon
*/
@property(nonatomic, strong) UIImageView *downloadImage;
@property TUIFileMessageCellData *fileData;
- (void)fillWithData:(TUIFileMessageCellData *)data;
@end

View File

@@ -0,0 +1,332 @@
//
// TUIFileMessageCell_Minimalist.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIFileMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import "ReactiveObjC/ReactiveObjC.h"
#import "TUIMessageProgressManager.h"
@interface TUIFileMessageCell_Minimalist () <V2TIMSDKListener, TUIMessageProgressManagerDelegate>
@property(nonatomic, strong) UIView *fileContainer;
@property(nonatomic, strong) UIView *animateHighlightView;
@property(nonatomic, strong) UIView *progressView;
@end
@implementation TUIFileMessageCell_Minimalist
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self.bubbleView addSubview:self.fileContainer];
[self.fileContainer addSubview:self.progressView];
_fileImage = [[UIImageView alloc] init];
_fileImage.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"msg_file")];
_fileImage.contentMode = UIViewContentModeScaleAspectFit;
[self.fileContainer addSubview:_fileImage];
_fileName = [[UILabel alloc] init];
_fileName.font = [UIFont systemFontOfSize:14];
_fileName.textColor = [UIColor blackColor];
[self.fileContainer addSubview:_fileName];
_length = [[UILabel alloc] init];
_length.font = [UIFont systemFontOfSize:12];
_length.textColor = RGBA(122, 122, 122, 1);
[self.bubbleView addSubview:_length];
_downloadImage = [[UIImageView alloc] init];
_downloadImage.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath_Minimalist(@"file_download")];
_downloadImage.contentMode = UIViewContentModeScaleAspectFit;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(downloadClick)];
[_downloadImage addGestureRecognizer:tap];
[_downloadImage setUserInteractionEnabled:YES];
[self.contentView addSubview:_downloadImage];
[V2TIMManager.sharedInstance addIMSDKListener:self];
[TUIMessageProgressManager.shareManager addDelegate:self];
}
return self;
}
- (void)downloadClick {
_downloadImage.frame = CGRectZero;
if (self.delegate && [self.delegate respondsToSelector:@selector(onSelectMessage:)]) {
[self.delegate onSelectMessage:self];
}
}
- (void)fillWithData:(TUIFileMessageCellData *)data {
// set data
[super fillWithData:data];
self.fileData = data;
_fileName.text = data.fileName;
_length.text = [self formatLength:data.length];
@weakify(self);
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];
}
+ (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];
[self.fileContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(containerSize.width - kScale390(32));
make.height.mas_equalTo(48);
make.leading.mas_equalTo(kScale390(16));
make.top.mas_equalTo(self.bubbleView).mas_offset(8);
}];
CGFloat fileImageSize = 24;
[self.fileImage mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.container.mas_leading).mas_offset(kScale390(17));
make.top.mas_equalTo(self.container.mas_top).mas_offset(kScale390(12));
make.size.mas_equalTo(fileImageSize);
}];
[self.fileName mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.fileImage.mas_trailing).mas_offset(kScale390(8));
make.top.mas_equalTo(15);
make.trailing.mas_equalTo(self.container.mas_trailing).mas_offset(- kScale390(12));
make.height.mas_equalTo(17);
}];
[self.length mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.container).mas_offset(kScale390(22));
make.bottom.mas_equalTo(self.container.mas_bottom).mas_offset(- 11);
make.size.mas_equalTo(CGSizeMake(150, 14));
}];
if (!self.fileData.isLocalExist && !self.fileData.isDownloading) {
CGFloat downloadSize = 16;
[self.downloadImage mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.fileData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(self.bubbleView.mas_trailing).mas_offset(kScale390(8));
} else {
make.trailing.mas_equalTo(self.bubbleView.mas_leading).mas_offset(- kScale390(8));
}
make.centerY.mas_equalTo(self.length.mas_centerY);
make.height.width.mas_equalTo(downloadSize);
}];
} else {
_downloadImage.frame = CGRectZero;
}
[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];
}
#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 showProgressLoadingAnimation:progress];
}
- (void)updateDownloadProgress:(int)progress {
[self.indicator startAnimating];
self.progressView.hidden = YES;
self.length.text = [self formatLength:self.fileData.length];
if (!self.fileData.isLocalExist && !self.fileData.isDownloading) {
_downloadImage.hidden = NO;
} else {
_downloadImage.hidden = YES;
}
if (progress >= 100 || progress == 0) {
[self.indicator stopAnimating];
return;
}
[self showProgressLoadingAnimation:progress];
}
- (void)showProgressLoadingAnimation:(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.downloadImage.hidden = YES;
}
}];
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.isDownloading || (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];
}
return str;
}
- (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 = [UIColor whiteColor];
_fileContainer.layer.cornerRadius = 16;
_fileContainer.layer.masksToBounds = YES;
}
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.bubbleView.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.messageData.highlightKeyword) {
[self animate:0];
return;
}
[self animate:times];
}];
}];
}
- (UIView *)animateHighlightView {
if (_animateHighlightView == nil) {
_animateHighlightView = [[UIView alloc] init];
_animateHighlightView.backgroundColor = [UIColor orangeColor];
_animateHighlightView.layer.cornerRadius = 12;
_animateHighlightView.layer.masksToBounds = YES;
}
return _animateHighlightView;
}
#pragma mark - TUIMessageCellProtocol
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
CGSize size = CGSizeMake(kScale390(250), 90);
return size;
}
@end

View File

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

View File

@@ -0,0 +1,157 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIImageCollectionCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUITool.h>
@implementation TUIImageCollectionCell_Minimalist
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
}
return self;
}
- (void)setupViews {
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 addSubview:self.imageView];
self.imageView.mm_fill();
self.imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
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.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSelectMedia)];
[self addGestureRecognizer:tap];
}
- (void)onDownloadBtnClick {
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.imageView.image = nil;
//1.Read from cache
if ([self originImageFirst:data]) {
return;
}
if ([self largeImageSecond:data]) {
return;
}
if (data.thumbImage == nil) {
[data downloadImage:TImage_Type_Thumb];
}
if (data.thumbImage && data.largeImage == nil) {
[data downloadImage:TImage_Type_Large];
}
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.imageView.image = thumbImage;
}
}];
// largeImage
[[RACObserve(data, largeImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *largeImage) {
@strongify(self);
if (largeImage) {
self.imageView.image = largeImage;
}
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.downloadBtn.mm_width(31).mm_height(31).mm_right(16).mm_bottom(48);
}
- (BOOL)largeImageSecond:(TUIImageMessageCellData *)data {
@weakify(self);
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 fillOriginImageImageWithData:data];
}
return isExist;
}
- (void)fillOriginImageImageWithData:(TUIImageMessageCellData *)data {
@weakify(self);
// originImage
[[RACObserve(data, originImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *originImage) {
@strongify(self);
if (originImage) {
self.imageView.image = originImage;
}
}];
}
- (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;
}
}];
}
- (void)fillThumbImageWithData:(TUIImageMessageCellData *)data {
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.imageView.image = thumbImage;
}
}];
}
@end

View File

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

View File

@@ -0,0 +1,273 @@
//
// TUIImageMessageCell_Minimalist.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIImageMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIImageMessageCell_Minimalist ()
@property(nonatomic, strong) UIView *animateHighlightView;
@end
@implementation TUIImageMessageCell_Minimalist
- (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.msgTimeLabel.textColor = RGB(255, 255, 255);
[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.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;
// tell constraints they need updating
[self setNeedsUpdateConstraints];
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
[self layoutIfNeeded];
}
}];
if (data.direction == MsgDirectionIncoming) {
[[[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];
}];
}
}
- (void)makeConstraints {
[self.thumb mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(self.container);
make.width.mas_equalTo(self.container);
make.top.mas_equalTo(self.container);
make.leading.mas_equalTo(self.container);
}];
[self.progress mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.container);
}];
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[super updateConstraints];
CGFloat topMargin = 0;
CGFloat height = self.container.mm_h;
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.container).mas_offset(12);
make.size.mas_equalTo(CGSizeMake(150, 150));
make.centerX.mas_equalTo(self.container);
}];
}
else {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(height);
make.width.mas_equalTo(self.container.mas_width);
make.top.mas_equalTo(self.container).mas_offset(topMargin);
make.leading.mas_equalTo(self.container);
}];
}
[self.progress mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.container);
}];
[self.msgTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(38);
make.height.mas_equalTo(self.messageData.msgStatusSize.height);
make.bottom.mas_equalTo(self.container).mas_offset(-kScale390(9));
make.trailing.mas_equalTo(self.container).mas_offset(-kScale390(8));
}];
[self.msgStatusView mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(16);
make.height.mas_equalTo(self.messageData.msgStatusSize.height);
make.bottom.mas_equalTo(self.msgTimeLabel);
make.trailing.mas_equalTo(self.msgTimeLabel.mas_leading);
}];
[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);
}
}];
}
- (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 139.f;
}
+ (CGSize)getContentSize:(TUIMessageCellData *)data {
NSAssert([data isKindOfClass:TUIImageMessageCellData.class], @"data must be kind of TUIImageMessageCellData");
TUIImageMessageCellData *imageCellData = (TUIImageMessageCellData *)data;
BOOL hasRiskContent = imageCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
return CGSizeMake(150, 150);
}
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_Origin) {
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_Thumb) {
size = item.size;
}
}
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
return size;
}
CGFloat widthMax = kScale390(250);
CGFloat heightMax = kScale390(250);
if (size.height > size.width) {
size.width = size.width / size.height * heightMax;
size.height = heightMax;
} else {
size.height = size.height / size.width * widthMax;
size.width = widthMax;
}
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_Minimalist;
@protocol TUIJoinGroupMessageCellDelegate_Minimalist <NSObject>
@optional
- (void)didTapOnNameLabel:(TUIJoinGroupMessageCell_Minimalist *)cell;
- (void)didTapOnSecondNameLabel:(TUIJoinGroupMessageCell_Minimalist *)cell;
- (void)didTapOnRestNameLabel:(TUIJoinGroupMessageCell_Minimalist *)cell withIndex:(NSInteger)index;
@end
@interface TUIJoinGroupMessageCell_Minimalist : TUISystemMessageCell
@property TUIJoinGroupMessageCellData *joinData;
@property(nonatomic, weak) id<TUIJoinGroupMessageCellDelegate_Minimalist> joinGroupDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,135 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIJoinGroupMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIJoinGroupMessageCell_Minimalist () <UITextViewDelegate>
@property(nonatomic, strong) UITextView *textView;
@end
@implementation TUIJoinGroupMessageCell_Minimalist
- (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 : self.messageLabel.textColor,
NSBackgroundColorAttributeName : self.messageLabel.backgroundColor,
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;
[self setNeedsLayout];
}
+ (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];
}
- (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_Minimalist;
NS_ASSUME_NONNULL_BEGIN
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMediaCollectionCellDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIMediaCollectionCellDelegate_Minimalist <NSObject>
/**
* meida cell
*/
- (void)onCloseMedia:(TUIMediaCollectionCell_Minimalist *)cell;
@end
@interface TUIMediaCollectionCell_Minimalist : UICollectionViewCell
@property(nonatomic, strong) UIImageView *imageView;
@property(nonatomic, strong) UIButton *downloadBtn;
@property(nonatomic, weak) id<TUIMediaCollectionCellDelegate_Minimalist> delegate;
- (void)fillWithData:(TUIMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,24 @@
//
// TUIMediaCollectionCell.m
// TUIChat
//
// Created by xiangzhang on 2021/11/22.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMediaCollectionCell_Minimalist.h"
@implementation TUIMediaCollectionCell_Minimalist
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)fillWithData:(TUIMessageCellData *)data {
return;
}
@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_Minimalist
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIMenuCell_Minimalist : 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_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUIMenuCell_Minimalist
- (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,39 @@
// 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_Minimalist.h>
#import "TUIMergeMessageCellData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMergeMessageCell_Minimalist : TUIMessageCell_Minimalist
/**
* 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,326 @@
//
// TUIMergeMessageCell.m
// Pods
//
// Created by harvy on 2020/12/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMergeMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.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_Minimalist : 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_Minimalist
- (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;
_abstractName.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_Minimalist ()
@property(nonatomic, strong) CAShapeLayer *borderLayer;
@property(nonatomic, strong) TUIMergeMessageDetailRow_Minimalist *contentRowView1;
@property(nonatomic, strong) TUIMergeMessageDetailRow_Minimalist *contentRowView2;
@property(nonatomic, strong) TUIMergeMessageDetailRow_Minimalist *contentRowView3;
@end
@implementation TUIMergeMessageCell_Minimalist
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if ([super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[self setupViews];
}
return self;
}
- (void)setupViews {
self.container.backgroundColor = RGBA(249, 249, 249, 0.94);
_relayTitleLabel = [[UILabel alloc] init];
_relayTitleLabel.text = @"Chat history";
_relayTitleLabel.font = [UIFont systemFontOfSize:12];
_relayTitleLabel.textColor = RGBA(0, 0, 0, 0.8);
[self.container addSubview:_relayTitleLabel];
_contentRowView1 = [[TUIMergeMessageDetailRow_Minimalist alloc] init];
[self.container addSubview:_contentRowView1];
_contentRowView2 = [[TUIMergeMessageDetailRow_Minimalist alloc] init];
[self.container addSubview:_contentRowView2];
_contentRowView3 = [[TUIMergeMessageDetailRow_Minimalist 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 = RGBA(153, 153, 153, 1);
_bottomTipsLabel.font = [UIFont systemFontOfSize:10];
[self.container addSubview:_bottomTipsLabel];
}
+ (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.width.mas_lessThanOrEqualTo(self.container);
make.height.mas_equalTo(self.relayTitleLabel.font.lineHeight);
make.top.mas_equalTo(self.container).mas_offset(10);
}];
[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);
}];
}
- (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;
}
// 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: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 + mergeCellData.msgStatusSize.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 = senderRect.size.width;
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,15 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUIMediaCollectionCell_Minimalist.h"
#import "TUIVideoMessageCellData.h"
@interface TUIVideoCollectionCell_Minimalist : TUIMediaCollectionCell_Minimalist
- (void)fillWithData:(TUIVideoMessageCellData *)data;
- (void)stopVideoPlayAndSave;
@end

View File

@@ -0,0 +1,287 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIVideoCollectionCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import "ReactiveObjC/ReactiveObjC.h"
@import MediaPlayer;
@import AVFoundation;
@import AVKit;
@interface TUIVideoCollectionCell_Minimalist ()
@property(nonatomic, strong) UILabel *duration;
@property(nonatomic, strong) UILabel *playTime;
@property(nonatomic, strong) UISlider *playProcess;
@property(nonatomic, strong) UIButton *mainPlayBtn;
@property(nonatomic, strong) UIButton *playBtn;
@property(nonatomic, strong) UIButton *closeBtn;
@property(nonatomic, strong) AVPlayer *player;
@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_Minimalist
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
}
return self;
}
- (void)setupViews {
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 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.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;
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]) {
/**
* Undownloaded videos play using online url
*/
[self.videoData getVideoUrl:^(NSString *_Nonnull url) {
@strongify(self);
if (url) {
[self addPlayer:[NSURL URLWithString:url]];
}
}];
/**
* Download video asynchronously
*/
[self.videoData downloadVideo];
} 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, videoPath) filter:^BOOL(NSString *path) {
return [path length] > 0;
}] take:1] subscribeNext:^(NSString *path) {
@strongify(self);
self.videoPath = path;
if (self.isSaveVideo) {
[self saveVideo];
}
/**
* If it has not been played, or the playback is wrong, switch from online playback to local playback
*/
if (self.player.status == AVPlayerStatusFailed || self.player.status == AVPlayerStatusReadyToPlay) {
[self addPlayer:[NSURL fileURLWithPath:self.videoPath]];
}
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
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);
}
- (void)addPlayer:(NSURL *)url {
self.videoUrl = url;
if (!self.player) {
self.player = [AVPlayer playerWithURL:self.videoUrl];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
playerLayer.frame = self.bounds;
[self.layer insertSublayer: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.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 {
self.isPlay = NO;
[self.player pause];
self.mainPlayBtn.hidden = NO;
[self.playBtn setImage:TUIChatCommonBundleImage(@"video_play") forState:UIControlStateNormal];
}
#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)];
}
});
}];
}
@end

View File

@@ -0,0 +1,42 @@
// 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/TUIMessageCell.h>
#import <TIMCommon/TUIMessageCell_Minimalist.h>
#import "TUIVideoMessageCellData.h"
/**
* 【Module name】TUIVideoMessageCell_Minimalist
* 【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_Minimalist : TUIMessageCell_Minimalist
/**
* 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;
/**
* 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,288 @@
//
// TUIVideoMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVideoMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import "TUIMessageProgressManager.h"
#import "TUICircleLodingView.h"
@interface TUIVideoMessageCell_Minimalist () <TUIMessageProgressManagerDelegate>
@property(nonatomic, strong) UIView *animateHighlightView;
@property(nonatomic, strong) TUICircleLodingView *animateCircleView;
@end
@implementation TUIVideoMessageCell_Minimalist
- (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];
_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")];
[_thumb addSubview:_play];
_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;
self.msgTimeLabel.textColor = RGB(255, 255, 255);
[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.thumb.contentMode = UIViewContentModeScaleAspectFill;
self.play.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.play.hidden = YES;
self.indicator.hidden = YES;
self.animateCircleView.hidden = NO;
@weakify(self);
[[RACObserve(data, videoTranscodingProgress) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *x) {
@strongify(self);
double progress = [x doubleValue];
self.animateCircleView.progress = (progress *100);
}];
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;
}
else {
self.animateCircleView.hidden = YES;
}
@weakify(self);
[[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
@strongify(self);
if (thumbImage) {
self.thumb.image = thumbImage;
}
}];
if (data.direction == MsgDirectionIncoming) {
[[[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);
self.play.hidden = !self.progress.hidden;
}];
} else {
[[[RACObserve(data, uploadProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
@strongify(self);
self.play.hidden = !self.progress.hidden;
}];
}
// 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];
BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
if (hasRiskContent ) {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.container).mas_offset(12);
make.size.mas_equalTo(CGSizeMake(150, 150));
make.centerX.mas_equalTo(self.container);
}];
}
else {
[self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(self.container.mas_height);
make.width.mas_equalTo(self.container);
make.leading.mas_equalTo(self.container.mas_leading);
make.top.mas_equalTo(self.container);
}];
}
[self.play mas_remakeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(TVideoMessageCell_Play_Size);
make.center.mas_equalTo(self.thumb);
}];
[self.msgTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(38);
make.height.mas_equalTo(self.messageData.msgStatusSize.height);
make.bottom.mas_equalTo(self.container).mas_offset(-kScale390(9));
make.trailing.mas_equalTo(self.container).mas_offset(-kScale390(8));
}];
[self.msgStatusView mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(16);
make.height.mas_equalTo(self.messageData.msgStatusSize.height);
make.bottom.mas_equalTo(self.msgTimeLabel);
make.trailing.mas_equalTo(self.msgTimeLabel.mas_leading);
}];
[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;
BOOL hasRiskContent = videoCellData.innerMessage.hasRiskContent;
if (hasRiskContent) {
return CGSizeMake(150, 150);
}
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;
}
CGFloat widthMax = kScale390(250);
CGFloat heightMax = kScale390(250);
if (size.height > size.width) {
size.width = size.width / size.height * heightMax;
size.height = heightMax;
} else {
size.height = size.height / size.width * widthMax;
size.width = widthMax;
}
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_Minimalist.h>
#import "TUIVoiceMessageCellData.h"
@import AVFoundation;
/**
*
* 【Module name】 TUIVoiceMessageCell_Minimalist
* 【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_Minimalist : TUIBubbleMessageCell_Minimalist
@property(nonatomic, strong) UIImageView *voicePlay;
/**
* 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) NSMutableArray *voiceAnimations;
/**
* 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,226 @@
//
// TUIVoiceMessageCell.m
// UIKit
//
// Created by annidyfeng on 2019/5/30.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIVoiceMessageCell_Minimalist.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/TUICore.h>
@interface TUIVoiceMessageCell_Minimalist ()
@end
@implementation TUIVoiceMessageCell_Minimalist
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_voicePlay = [[UIImageView alloc] init];
_voicePlay.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath_Minimalist(@"voice_play")];
[self.bubbleView addSubview:_voicePlay];
self.voiceAnimations = [NSMutableArray array];
for (int i = 0; i < 6; ++i) {
UIImageView *animation = [[UIImageView alloc] init];
animation.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath_Minimalist(@"voice_play_animation")];
[self.bubbleView addSubview:animation];
[self.voiceAnimations addObject:animation];
}
_duration = [[UILabel alloc] init];
_duration.font = [UIFont boldSystemFontOfSize:14];
_duration.rtlAlignment = TUITextRTLAlignmentTrailing;
[self.bubbleView addSubview:_duration];
_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];
self.bottomContainer = [[UIView alloc] init];
[self.contentView addSubview:self.bottomContainer];
}
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_MinimalistExtensionID parentView:self.bottomContainer param:param];
}
- (void)fillWithData:(TUIVoiceMessageCellData *)data;
{
// set data
[super fillWithData:data];
self.voiceData = data;
if (data.duration > 0) {
_duration.text = [NSString stringWithFormat:@"%d:%.2d", (int)data.duration / 60, (int)data.duration % 60];
} else {
_duration.text = @"0:01";
}
self.bottomContainer.hidden = CGSizeEqualToSize(data.bottomContainerSize, CGSizeZero);
if (self.voiceData.innerMessage.localCustomInt == 0 && self.voiceData.direction == MsgDirectionIncoming) self.voiceReadPoint.hidden = NO;
@weakify(self);
[[RACObserve(data, isPlaying) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *x) {
@strongify(self);
if ([x boolValue]) {
[self startAnimating];
} else {
[self stopAnimating];
if (data.duration > 0) {
self.duration.text = [NSString stringWithFormat:@"%d:%.2d", (int)data.duration / 60, (int)data.duration % 60];
} else {
self.duration.text = @"0:01";
}
}
}];
[[RACObserve(data, currentTime) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *time) {
@strongify(self);
if (!data.isPlaying) {
return;
}
int min = (int)data.currentTime / 60;
int sec = (int)data.currentTime % 60;
NSString *forMatStr = [NSString stringWithFormat:@"%d:%.2d", min, sec];
self.duration.text = [NSString stringWithFormat:@"%d:%.2d", (int)data.currentTime / 60, (int)data.currentTime % 60];
}];
// 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.voicePlay mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(12);
make.leading.mas_equalTo(kScale390(16));
make.width.mas_equalTo(11);
make.height.mas_equalTo(13);
}];
CGFloat animationStartX = kScale390(35);
for (int i = 0; i < self.voiceAnimations.count; ++i) {
UIImageView *animation = self.voiceAnimations[i];
[animation mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.bubbleView).mas_offset(animationStartX + kScale390(25) * i);
make.top.mas_equalTo(self.bubbleView).mas_offset(self.voiceData.voiceTop);
make.width.height.mas_equalTo(_voiceData.voiceHeight);
}];
}
[self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.mas_greaterThanOrEqualTo(kScale390(34));
make.height.mas_greaterThanOrEqualTo(17);
make.top.mas_equalTo(self.voiceData.voiceTop + 2);
make.trailing.mas_equalTo(self.container).mas_offset(- kScale390(14));
}];
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));
}];
}
[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(self.messageData.messageContainerAppendSize.height + 6);
make.size.mas_equalTo(size);
}];
CGFloat repliesBtnTextWidth = self.messageModifyRepliesButton.frame.size.width;
if (!self.messageModifyRepliesButton.hidden) {
UIImageView *lastAvatarImageView = self.replyAvatarImageViews.lastObject;
[self.messageModifyRepliesButton mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.voiceData.direction == MsgDirectionIncoming) {
make.leading.mas_equalTo(lastAvatarImageView.mas_trailing);
} else {
make.trailing.mas_equalTo(self.container.mas_trailing);
}
make.top.mas_equalTo(self.bottomContainer.mas_bottom);
make.size.mas_equalTo(CGSizeMake(repliesBtnTextWidth + 10, 30));
}];
}
}
- (void)startAnimating {
_voicePlay.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath_Minimalist(@"voice_pause")];
}
- (void)stopAnimating {
_voicePlay.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath_Minimalist(@"voice_play")];
}
- (void)layoutSubviews {
[super layoutSubviews];
}
#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 {
NSAssert([data isKindOfClass:TUIVoiceMessageCellData.class], @"data must be kind of TUIVoiceMessageCellData");
TUIVoiceMessageCellData *voiceCellData = (TUIVoiceMessageCellData *)data;
return CGSizeMake((voiceCellData.voiceHeight + kScale390(5)) * 6 + kScale390(82),
voiceCellData.voiceHeight + voiceCellData.voiceTop * 3 + voiceCellData.msgStatusSize.height);
}
@end