增加换肤功能
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 | vinson:When will the new version of the SDK be released? | -- abstract1 | lynx:Plan for next
|
||||
* Monday, the specific time depends on the system test situation in these two days.. | -- abstract2 | vinson:Okay.
|
||||
*/
|
||||
|
||||
#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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user