首次提交

This commit is contained in:
启星
2025-09-22 18:48:29 +08:00
parent 28ae935e93
commit ae9be0b58e
8941 changed files with 999209 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
//
// TUIBaseChatViewController+ProtectedAPI.h
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/6/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
#import "TUIBaseChatViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIBaseChatViewController () <TUIInputControllerDelegate, TUINotificationProtocol>
- (NSString *)forwardTitleWithMyName:(NSString *)nameStr;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,121 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* Tencent Cloud Communication Service Interface Component TUIKIT - Chat Interface Component
*
* This document mainly declares the components used to implement the chat interface, which supports two modes of 1v1 single chat and group chat, including:
* - Message display area: that is, the bubble display area.
* - Message input area: that is, the part that allows users to input message text, emoticons, pictures and videos.
*
* The TUIBaseChatViewController class is used to implement the general controller of the chat view, which is responsible for unified control of input, message
* controller, and more views. The classes and protocols declared in this file can effectively help you implement custom message formats.
*
*/
#import <TIMCommon/TIMCommonModel.h>
#import <UIKit/UIKit.h>
#import "TUIBaseMessageController.h"
#import "TUIChatConversationModel.h"
#import "TUIInputController.h"
@class TUIBaseChatViewController;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIBaseChatViewController
//
/////////////////////////////////////////////////////////////////////////////////
/**
*
* 【Module name】TUIBaseChatViewController
* 【Function description】Responsible for implementing the UI components of the chat interface, including the message display area and the message input area.
*
* The TUIBaseChatViewController class is used to implement the overall controller of the chat view, and is responsible for unified control of the chat message
* controller (TUIBaseMessageController), the information input controller (TUIInputController) and more views.
*
* The chat message controller is responsible for responding in the UI when you receive a new message or sending a message, and respond to your interactions on
* the message bubble, see: Section\Chat\TUIBaseMessageController.h The information input controller is responsible for receiving your input, providing you with
* the editing function of the input content and sending messages. For details, please refer to: Section\Chat\Input\TUIInputController.h This class contains the
* "more" view, that is, when you click the "+" button in the UI, more buttons can be displayed to satisfy your further operations. For details, please refer
* to: Section\Chat\TUIMoreView.h
*
* Q: How to implement custom messages?
* A: If you want to implement a message style that TUIKit does not support, such as adding a voting link to the message style, you can refer to the
* documentation: https://cloud.tencent.com/document/product/269/37067
*/
@interface TUIBaseChatViewController : UIViewController
@property(nonatomic, strong) TUIChatConversationModel *conversationData;
/**
* Highlight text
* In the search scenario, when highlightKeyword is not empty and matches @locateMessage, opening the chat session page will highlight the current cell
*/
@property(nonatomic, copy) NSString *highlightKeyword;
/**
* Locate message
* In the search scenario, when locateMessage is not empty, opening the chat session page will automatically scroll to here
*/
@property(nonatomic, strong) V2TIMMessage *locateMessage;
/**
* View for displaying unread count number
*/
@property TUIUnReadView *unRead;
/**
* TUIKit message controller
* It is responsible for the display of message bubbles, and at the same time, it is responsible for responding to the user's interaction with the message
* bubbles, such as: clicking on the avatar of the message sender, tapping the message, and long-pressing the message. For more information about the chat
* message controller, please refer to TUIChat\UI\Chat\TUIBaseMessageController.h
*/
@property TUIBaseMessageController *messageController;
/**
*
* The custom container view at the bottom, which is usually used to add plugin subviews.
*/
@property (nonatomic, strong) UIView *bottomContainerView;
/**
* TUIKit input controller
* Responsible for receiving user input, and displaying the "+" button, voice input button, emoticon button, etc.
* At the same time, TUIInputController integrates the message sending function, and you can directly use TUIInputController to collect and send message input.
* Please refer to TUIChat\UI\Input\TUIInputController.h for details of the message input controller
*/
@property TUIInputController *inputController;
/**
* Data group for more menu view data
* More menu views include: Capture, Picture, Video, File. See Section\Chat\TUIMoreView.h for details
*/
@property NSArray<TUIInputMoreCellData *> *moreMenus;
/**
* Need to scroll to the bottom
* It is usually used to push to another VC and need to scroll to the bottom after returning.
*/
@property(nonatomic, assign) BOOL needScrollToBottom;
- (void)sendMessage:(V2TIMMessage *)message;
- (void)sendMessage:(V2TIMMessage *)message placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
/**
* Add a custom view at the top of the chat interface. The view will stay at the top of the message list and will not slide up as the message list slides up.
* If not set, it will not be displayed by default.
*/
+ (void)setCustomTopView:(UIView *)view;
/**
* Get a custom view at the top of the chat interface.
*/
+ (UIView *)customTopView;
+ (UIView *)groupPinTopView;
+ (UIView *)topAreaBottomView;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
//
// TUIBaseMessageController+ProtectedAPI.h
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/7/8.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIBaseMessageController.h"
#import "TUIChatConversationModel.h"
#import "TUIMessageDataProvider.h"
@class TUIMessageCellData;
NS_ASSUME_NONNULL_BEGIN
@interface TUIBaseMessageController () <TUIMessageBaseDataProviderDataSource>
@property(nonatomic, strong) TUIMessageDataProvider *messageDataProvider;
@property(nonatomic, strong) TUIChatConversationModel *conversationData;
@property(nonatomic, strong) UIActivityIndicatorView *indicatorView;
- (void)onNewMessage:(NSNotification *)notification;
- (void)onJumpToRepliesDetailPage:(TUIMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,109 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This file declares the controller class used to implement the messaging logic
* The message controller is responsible for uniformly displaying the messages you send/receive, while providing response callbacks when you interact with
* those messages (tap/long-press, etc.). The message controller is also responsible for unified data processing of the messages you send into a data format
* that can be sent through the IM SDK and sent. That is to say, when you use this controller, you can save a lot of data processing work, so that you can
* access the IM SDK more quickly and conveniently.
*/
#import <TIMCommon/TUIMessageCell.h>
#import <UIKit/UIKit.h>
#import "TUIBaseMessageControllerDelegate.h"
#import "TUIChatConversationModel.h"
#import "TUIChatDefine.h"
NS_ASSUME_NONNULL_BEGIN
@class TUIConversationCellData;
@class TUIBaseMessageController;
@class TUIReplyMessageCell;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIBaseMessageController
//
/////////////////////////////////////////////////////////////////////////////////
/**
*
* 【Module name】TUIBaseMessageController
* 【Function description】The message controller is responsible for implementing a series of business logic such as receiving, sending, and displaying
* messages.
* - This class provides callback interfaces for interactive operations such as receiving/displaying new messages, showing/hiding menus, and clicking on
* message avatars.
* - At the same time, this class provides the sending function of image, video, and file information, and directly integrates and calls the IM SDK to realize
* the sending function.
*
*/
@interface TUIBaseMessageController : UITableViewController
+ (void)asyncGetDisplayString:(NSArray<V2TIMMessage *> *)messageList callback:(void(^)(NSDictionary<NSString *, NSString *> *))callback;
+ (nullable NSString *)getDisplayString:(V2TIMMessage *)message;
@property(nonatomic, weak) id<TUIBaseMessageControllerDelegate> delegate;
@property(nonatomic, assign) BOOL isInVC;
/**
* Whether a read receipt is required to send a message, the default is NO
*/
@property(nonatomic) BOOL isMsgNeedReadReceipt;
@property(nonatomic, copy) void (^groupRoleChanged)(V2TIMGroupMemberRole role);
@property(nonatomic, copy) void (^pinGroupMessageChanged)(NSArray *groupPinList);
- (void)sendMessage:(V2TIMMessage *)msg;
- (void)sendMessage:(V2TIMMessage *)msg placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
- (void)clearUImsg;
- (void)scrollToBottom:(BOOL)animate;
- (void)setConversation:(TUIChatConversationModel *)conversationData;
- (void)sendPlaceHolderUIMessage:(TUIMessageCellData *)cellData;
/**
*
* After enabling multi-selection mode, get the currently selected result
* Returns an empty array if multiple selection mode is off
*/
- (NSArray<TUIMessageCellData *> *)multiSelectedResult:(TUIMultiResultOption)option;
- (void)enableMultiSelectedMode:(BOOL)enable;
- (void)deleteMessages:(NSArray<TUIMessageCellData *> *)uiMsgs;
/**
* Conversation read report
*
*/
- (void)readReport;
/**
* Subclass implements click-to-reply messages
*/
- (void)showReplyMessage:(TUIReplyMessageCell *)cell;
- (void)willShowMediaMessage:(TUIMessageCell *)cell;
- (void)didCloseMediaMessage:(TUIMessageCell *)cell;
// Reload the specific cell UI.
- (void)reloadCellOfMessage:(NSString *)messageID;
- (void)reloadAndScrollToBottomOfMessage:(NSString *)messageID;
- (void)scrollCellToBottomOfMessage:(NSString *)messageID;
- (void)loadGroupInfo;
- (BOOL)isCurrentUserRoleSuperAdminInGroup;
- (BOOL)isCurrentMessagePin:(NSString *)msgID;
- (void)unPinGroupMessage:(V2TIMMessage *)innerMessage;
- (CGFloat)getHeightFromMessageCellData:(TUIMessageCellData *)cellData;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
@import ImSDK_Plus;
@class TUIBaseMessageController;
@class TUIMessageCellData;
@class TUIMessageCell;
NS_ASSUME_NONNULL_BEGIN
/////////////////////////////////////////////////////////////////////////////////
//
// TUIBaseMessageControllerDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIBaseMessageControllerDelegate <NSObject>
/**
* Callback for clicking controller
* You can use this callback to: reset the InputController, dismiss the keyboard.
*
* @param controller Delegator, Message Controller
*/
- (void)didTapInMessageController:(TUIBaseMessageController *)controller;
/**
* Callback after hide long press menu button
* You can customize the implementation of this delegate function according to your needs.
*
* @param controller Delegator, Message Controller
*/
- (void)didHideMenuInMessageController:(TUIBaseMessageController *)controller;
/**
* Callback before hide long press menu button
* You can customize the implementation of this delegate function according to your needs.
*
* @param controller Delegator, Message Controller
* @param view The view where the controller is located
*/
- (BOOL)messageController:(TUIBaseMessageController *)controller willShowMenuInCell:(UIView *)view;
/**
* Callback for receiving new message
* You can use this callback to initialize a new message based on the incoming data and perform a new message reminder.
*
* @param controller Delegator, Message Controller
* @param message Incoming new message
*
* @return Returns the new message unit that needs to be displayed.
*/
- (TUIMessageCellData *)messageController:(TUIBaseMessageController *)controller onNewMessage:(V2TIMMessage *)message;
/**
* Callback for displaying new message
* You can use this callback to initialize the message bubble based on the incoming data and display it
*
* @param controller Delegator, Message Controller
* @param data Data needed to display
*
* @return Returns the new message unit that needs to be displayed.。
*/
- (TUIMessageCell *)messageController:(TUIBaseMessageController *)controller onShowMessageData:(TUIMessageCellData *)data;
/**
* The callback the cell will be displayed with
*/
- (void)messageController:(TUIBaseMessageController *)controller willDisplayCell:(TUIMessageCell *)cell withData:(TUIMessageCellData *)cellData;
/**
* Callback for clicking avatar in the message cell
* You can use this callback to achieve: jump to the detailed information interface of the corresponding user.
* 1. First pull user information, if the user is a friend of the current user, initialize the corresponding friend information interface and jump.
* 2. If the user is not a friend of the current user, the corresponding interface for adding friends is initialized and a jump is performed.
*
*/
- (void)messageController:(TUIBaseMessageController *)controller onSelectMessageAvatar:(TUIMessageCell *)cell;
/**
* Callback for long pressing avatar in the message cell
*/
- (void)messageController:(TUIBaseMessageController *)controller onLongSelectMessageAvatar:(TUIMessageCell *)cell;
/**
* Callback for clicking message content in the message cell
*/
- (void)messageController:(TUIBaseMessageController *)controller onSelectMessageContent:(TUIMessageCell *)cell;
/**
* After long-pressing the message, the menu bar will pop up, and the callback after clicking the menu option
* menuType: The type of menu that was clicked. 0 - multiple choice, 1 - forwarding.
*/
- (void)messageController:(TUIBaseMessageController *)controller onSelectMessageMenu:(NSInteger)menuType withData:(TUIMessageCellData *)data;
/**
* Callback for about to reply to the message (usually triggered by long-pressing the message content and then clicking the reply button)
*/
- (void)messageController:(TUIBaseMessageController *)controller onRelyMessage:(TUIMessageCellData *)data;
/**
* Callback for quoting message (triggered by long-pressing the message content and then clicking the quote button)
*/
- (void)messageController:(TUIBaseMessageController *)controller onReferenceMessage:(TUIMessageCellData *)data;
/**
* Callback for re-editing message (usually for re-calling a message)
*/
- (void)messageController:(TUIBaseMessageController *)controller onReEditMessage:(TUIMessageCellData *)data;
/// Forward text.
- (void)messageController:(TUIBaseMessageController *)controller onForwardText:(NSString *)text;
/**
* Get the height of custom Tips (such as safety tips in Demo)
*/
- (CGFloat)getTopMarginByCustomView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,17 @@
//
// TUIC2CChatViewController.h
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/6/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIBaseChatViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIC2CChatViewController : TUIBaseChatViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,159 @@
//
// TUIC2CChatViewController.m
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/6/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIC2CChatViewController.h"
#import "TUIBaseChatViewController+ProtectedAPI.h"
#import "TUIChatConfig.h"
#import "TUICloudCustomDataTypeCenter.h"
#import "TUILinkCellData.h"
#import "TUIMessageController.h"
#import "TUIMessageDataProvider.h"
#define kC2CTypingTime 3000.0
@interface TUIC2CChatViewController ()
// If one sendTypingBaseCondation is satisfied, sendTypingBaseCondationInVC is used until the current session exits
@property(nonatomic, assign) BOOL sendTypingBaseCondationInVC;
@end
@implementation TUIC2CChatViewController
- (void)dealloc {
self.sendTypingBaseCondationInVC = NO;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.sendTypingBaseCondationInVC = NO;
self.view.backgroundColor = [UIColor clearColor];
// notify
NSDictionary *param = @{TUICore_TUIChatNotify_ChatVC_ViewDidLoadSubKey_UserID: self.conversationData.userID ? : @""};
[TUICore notifyEvent:TUICore_TUIChatNotify
subKey:TUICore_TUIChatNotify_ChatVC_ViewDidLoadSubKey
object:nil
param:param];
}
#pragma mark - Override Methods
- (void)inputControllerDidInputAt:(TUIInputController *)inputController {
[super inputControllerDidInputAt:inputController];
NSAttributedString *spaceString = [[NSAttributedString alloc]
initWithString:@"@"
attributes:@{NSFontAttributeName : kTUIInputNoramlFont, NSForegroundColorAttributeName : kTUIInputNormalTextColor}];
[self.inputController.inputBar addWordsToInputBar:spaceString];
}
- (NSString *)forwardTitleWithMyName:(NSString *)nameStr {
NSString *title = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitRelayChatHistoryForSomebodyFormat), self.conversationData.title, nameStr];
return rtlString(title);
}
- (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell {
[super inputController:inputController didSelectMoreCell:cell];
}
- (void)inputControllerBeginTyping:(TUIInputController *)inputController {
[super inputControllerBeginTyping:inputController];
[self sendTypingMsgByStatus:YES];
}
- (void)inputControllerEndTyping:(TUIInputController *)inputController {
[super inputControllerEndTyping:inputController];
[self sendTypingMsgByStatus:NO];
}
- (BOOL)sendTypingBaseCondation {
if (self.sendTypingBaseCondationInVC) {
return YES;
}
if ([self.messageController isKindOfClass:TUIMessageController.class]) {
TUIMessageController *vc = (TUIMessageController *)self.messageController;
NSDictionary *messageFeatureDic = (id)[vc.C2CIncomingLastMsg parseCloudCustomData:messageFeature];
if (messageFeatureDic && [messageFeatureDic isKindOfClass:[NSDictionary class]] && [messageFeatureDic.allKeys containsObject:@"needTyping"] &&
[messageFeatureDic.allKeys containsObject:@"version"]) {
BOOL needTyping = NO;
BOOL versionControl = NO;
BOOL timeControl = NO;
if ([messageFeatureDic[@"needTyping"] intValue] == 1) {
needTyping = YES;
}
if ([messageFeatureDic[@"version"] intValue] == 1) {
versionControl = YES;
}
CFTimeInterval current = [NSDate.new timeIntervalSince1970];
long currentTimeFloor = floor(current);
long otherSideTimeFloor = floor([vc.C2CIncomingLastMsg.timestamp timeIntervalSince1970]);
long interval = currentTimeFloor - otherSideTimeFloor;
if (interval <= kC2CTypingTime) {
timeControl = YES;
}
if (needTyping && versionControl && timeControl) {
self.sendTypingBaseCondationInVC = YES;
return YES;
}
}
}
return NO;
}
- (void)sendTypingMsgByStatus:(BOOL)editing {
// switch control
if (![TUIChatConfig defaultConfig].enableTypingStatus) {
return;
}
if (![self sendTypingBaseCondation]) {
return;
}
NSError *error = nil;
NSDictionary *param = @{
BussinessID : BussinessID_Typing,
@"typingStatus" : editing ? @1 : @0,
@"version" : @1,
@"userAction" : @14,
@"actionParam" : editing ? @"EIMAMSG_InputStatus_Ing" : @"EIMAMSG_InputStatus_End",
};
NSData *data = [NSJSONSerialization dataWithJSONObject:param options:0 error:&error];
V2TIMMessage *msg = [TUIMessageDataProvider getCustomMessageWithJsonData:data];
[msg setIsExcludedFromContentModeration:YES];
TUISendMessageAppendParams *appendParams = [[TUISendMessageAppendParams alloc] init];
appendParams.isSendPushInfo = NO;
appendParams.isOnlineUserOnly = YES;
appendParams.priority = V2TIM_PRIORITY_DEFAULT;
[TUIMessageDataProvider sendMessage:msg
toConversation:self.conversationData
appendParams:appendParams
Progress:^(uint32_t progress) {
}
SuccBlock:^{
NSLog(@"success");
}
FailBlock:^(int code, NSString *desc) {
NSLog(@"Fail");
}];
}
@end

View File

@@ -0,0 +1,50 @@
//
// TUIChatSmallTongue.h
// TUIChat
//
// Created by xiangzhang on 2022/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TUIChatDefine.h"
@class TUIChatSmallTongue;
NS_ASSUME_NONNULL_BEGIN
@protocol TUIChatSmallTongueViewDelegate <NSObject>
- (void)onChatSmallTongueClick:(TUIChatSmallTongue *)tongue;
@end
@interface TUIChatSmallTongueView : UIView
@property(nonatomic, weak) id<TUIChatSmallTongueViewDelegate> delegate;
- (void)setTongue:(TUIChatSmallTongue *)tongue;
@end
@interface TUIChatSmallTongue : NSObject
@property(nonatomic, assign) TUIChatSmallTongueType type;
@property(nonatomic, strong) UIView *parentView;
@property(nonatomic, assign) NSInteger unreadMsgCount;
@property(nonatomic, strong) NSString *atTipsStr;
@property(nonatomic, strong) NSArray *atMsgSeqs;
@end
@interface TUIChatSmallTongueManager : NSObject
+ (void)showTongue:(TUIChatSmallTongue *)tongue delegate:(id<TUIChatSmallTongueViewDelegate>)delegate;
+ (void)removeTongue:(TUIChatSmallTongueType)type;
+ (void)removeTongue;
+ (void)hideTongue:(BOOL)isHidden;
+ (void)adaptTongueBottomMargin:(CGFloat)margin;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,230 @@
//
// TUIChatSmallTongue.m
// TUIChat
//
// Created by xiangzhang on 2022/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatSmallTongueView.h"
#import <TUICore/NSString+TUIUtil.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIChatConfig.h"
#import <TIMCommon/TIMDefine.h>
#define TongueHeight 35.f
#define TongueImageWidth 12.f
#define TongueImageHeight 12.f
#define TongueLeftSpace 10.f
#define TongueMiddleSpace 5.f
#define TongueRightSpace 10.f
#define TongueFontSize 13
@interface TUIChatSmallTongueView ()
@property(nonatomic, strong) UIImageView *imageView;
@property(nonatomic, strong) UILabel *label;
@end
@implementation TUIChatSmallTongueView {
TUIChatSmallTongue *_tongue;
}
+ (void)load {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onThemeChanged:) name:TUIDidApplyingThemeChangedNotfication object:nil];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = TUIChatDynamicColor(@"chat_small_tongue_bg_color", @"#FFFFFF");
// border
self.layer.borderWidth = 0.2;
self.layer.borderColor = TUIChatDynamicColor(@"chat_small_tongue_line_color", @"#E5E5E5").CGColor;
self.layer.cornerRadius = 2;
self.layer.masksToBounds = YES;
// shadow
self.layer.shadowColor = RGBA(0, 0, 0, 0.15).CGColor;
self.layer.shadowOpacity = 1;
self.layer.shadowOffset = CGSizeMake(0, 0);
self.layer.shadowRadius = 2;
self.clipsToBounds = NO;
// tap
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap)];
[self addGestureRecognizer:tap];
}
return self;
}
- (void)onTap {
if (self.delegate && [self.delegate respondsToSelector:@selector(onChatSmallTongueClick:)]) {
[self.delegate onChatSmallTongueClick:_tongue];
}
}
- (void)setTongue:(TUIChatSmallTongue *)tongue {
_tongue = tongue;
if (!self.imageView) {
self.imageView = [[UIImageView alloc] init];
[self addSubview:self.imageView];
}
self.imageView.image = [TUIChatSmallTongueView getTongueImage:tongue];
if (!self.label) {
self.label = [[UILabel alloc] init];
self.label.font = [UIFont systemFontOfSize:TongueFontSize];
[self addSubview:self.label];
}
self.label.text = [TUIChatSmallTongueView getTongueText:tongue];
self.label.rtlAlignment = TUITextRTLAlignmentLeading;
self.label.textColor = TUIChatDynamicColor(@"chat_drop_down_color", @"#147AFF");
[self.imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(TongueImageWidth);
make.leading.mas_equalTo(TongueLeftSpace);
make.top.mas_equalTo(10);
}];
[self.label mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_lessThanOrEqualTo(self.mas_trailing).mas_offset(-TongueRightSpace);
make.height.mas_equalTo(TongueImageHeight);
make.leading.mas_equalTo(self.imageView.mas_trailing).mas_offset(TongueMiddleSpace);
make.top.mas_equalTo(10);
}];
}
+ (CGFloat)getTongueWidth:(TUIChatSmallTongue *)tongue {
NSString *tongueText = [self getTongueText:tongue];
CGSize titleSize = [tongueText boundingRectWithSize:CGSizeMake(MAXFLOAT, TongueHeight)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:TongueFontSize]}
context:nil]
.size;
CGFloat tongueWidth = TongueLeftSpace + TongueImageWidth + TongueMiddleSpace + ceil(titleSize.width) + TongueRightSpace;
return tongueWidth;
}
+ (NSString *)getTongueText:(TUIChatSmallTongue *)tongue {
static NSMutableDictionary *titleCacheFormat;
if (titleCacheFormat == nil) {
titleCacheFormat = [NSMutableDictionary dictionary];
[titleCacheFormat setObject:TIMCommonLocalizableString(TUIKitChatBackToLatestLocation) forKey:@(TUIChatSmallTongueType_ScrollToBoom)];
[titleCacheFormat setObject:TIMCommonLocalizableString(TUIKitChatNewMessages) forKey:@(TUIChatSmallTongueType_ReceiveNewMsg)];
}
if (tongue.type == TUIChatSmallTongueType_SomeoneAt) {
NSString *atMeStr = TIMCommonLocalizableString(TUIKitConversationTipsAtMe);
NSString *atAllStr = TIMCommonLocalizableString(TUIKitConversationTipsAtAll);
if ([tongue.atTipsStr tui_containsString:atMeStr]) {
atMeStr = [atMeStr stringByReplacingOccurrencesOfString:@"[" withString:@""];
atMeStr = [atMeStr stringByReplacingOccurrencesOfString:@"]" withString:@""];
[titleCacheFormat setObject:atMeStr forKey:@(TUIChatSmallTongueType_SomeoneAt)];
} else if ([tongue.atTipsStr tui_containsString:atAllStr]) {
atAllStr = [atAllStr stringByReplacingOccurrencesOfString:@"[" withString:@""];
atAllStr = [atAllStr stringByReplacingOccurrencesOfString:@"]" withString:@""];
[titleCacheFormat setObject:atAllStr forKey:@(TUIChatSmallTongueType_SomeoneAt)];
}
}
if (tongue.type == TUIChatSmallTongueType_ReceiveNewMsg) {
return [NSString stringWithFormat:[titleCacheFormat objectForKey:@(TUIChatSmallTongueType_ReceiveNewMsg)],
tongue.unreadMsgCount > 99 ? @"99+" : @(tongue.unreadMsgCount)];
} else {
return [titleCacheFormat objectForKey:@(tongue.type)];
}
}
static NSMutableDictionary *gImageCache;
+ (UIImage *)getTongueImage:(TUIChatSmallTongue *)tongue {
if (gImageCache == nil) {
gImageCache = [NSMutableDictionary dictionary];
[gImageCache setObject:TUIChatBundleThemeImage(@"chat_drop_down_img", @"drop_down") ?: UIImage.new forKey:@(TUIChatSmallTongueType_ScrollToBoom)];
[gImageCache setObject:TUIChatBundleThemeImage(@"chat_drop_down_img", @"drop_down") ?: UIImage.new forKey:@(TUIChatSmallTongueType_ReceiveNewMsg)];
[gImageCache setObject:TUIChatBundleThemeImage(@"chat_pull_up_img", @"pull_up") ?: UIImage.new forKey:@(TUIChatSmallTongueType_SomeoneAt)];
}
return [gImageCache objectForKey:@(tongue.type)];
}
+ (void)onThemeChanged:(NSNotification *)notice {
gImageCache = nil;
}
@end
@implementation TUIChatSmallTongue
- (instancetype)init {
self = [super init];
if (self) {
self.type = TUIChatSmallTongueType_None;
}
return self;
}
@end
static TUIChatSmallTongueView *gTongueView = nil;
static TUIChatSmallTongue *gTongue = nil;
static CGFloat gBottomMargin = 0;
@implementation TUIChatSmallTongueManager
+ (void)showTongue:(TUIChatSmallTongue *)tongue delegate:(id<TUIChatSmallTongueViewDelegate>)delegate {
if (tongue.type == gTongue.type
&& tongue.parentView == gTongue.parentView
&& tongue.unreadMsgCount == gTongue.unreadMsgCount
&& tongue.atMsgSeqs == gTongue.atMsgSeqs
&& !gTongueView.hidden) {
return;
}
gTongue = tongue;
if (!gTongueView) {
gTongueView = [[TUIChatSmallTongueView alloc] init];
} else {
[gTongueView removeFromSuperview];
}
CGFloat tongueWidth = [TUIChatSmallTongueView getTongueWidth:gTongue];
if(isRTL()) {
gTongueView.frame =
CGRectMake(16,
tongue.parentView.mm_h - Bottom_SafeHeight - TTextView_Height - 20 - TongueHeight - gBottomMargin,
tongueWidth, TongueHeight);
}
else {
gTongueView.frame =
CGRectMake(tongue.parentView.mm_w - tongueWidth - 16,
tongue.parentView.mm_h - Bottom_SafeHeight - TTextView_Height - 20 - TongueHeight - gBottomMargin,
tongueWidth, TongueHeight);
}
gTongueView.delegate = delegate;
[gTongueView setTongue:gTongue];
[tongue.parentView addSubview:gTongueView];
}
+ (void)removeTongue:(TUIChatSmallTongueType)type {
if (type != gTongue.type) {
return;
}
[self removeTongue];
}
+ (void)removeTongue {
gTongue = nil;
if (gTongueView) {
[gTongueView removeFromSuperview];
gTongueView = nil;
}
}
+ (void)hideTongue:(BOOL)isHidden {
if (gTongueView) {
gTongueView.hidden = isHidden;
}
}
+ (void)adaptTongueBottomMargin:(CGFloat)margin {
gBottomMargin = margin;
}
@end

View File

@@ -0,0 +1,13 @@
//
// FileViewController.h
// TUIKitDemo
//
// Created by kennethmiao on 2018/11/12.
// Copyright © 2018 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TUIFileMessageCell.h"
@interface TUIFileViewController : UIViewController
@property(nonatomic, strong) TUIFileMessageCellData *data;
@end

View File

@@ -0,0 +1,117 @@
//
// FileViewController.m
// TUIKitDemo
//
// Created by kennethmiao on 2018/11/12.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIFileViewController.h"
#import <QuickLook/QuickLook.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/UIView+TUILayout.h>
#import <TUICore/UIView+TUIToast.h>
#import "ReactiveObjC/ReactiveObjC.h"
@interface TUIFileViewController () <UIDocumentInteractionControllerDelegate>
@property(nonatomic, strong) UIImageView *image;
@property(nonatomic, strong) UILabel *name;
@property(nonatomic, strong) UILabel *progress;
@property(nonatomic, strong) UIButton *button;
@property(nonatomic, strong) UIDocumentInteractionController *document;
@end
@implementation TUIFileViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = TIMCommonLocalizableString(File);
titleLabel.font = [UIFont boldSystemFontOfSize:17.0];
titleLabel.textColor = TIMCommonDynamicColor(@"nav_title_text_color", @"#000000");
titleLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
[titleLabel sizeToFit];
self.navigationItem.titleView = titleLabel;
// left
UIImage *defaultImage = [UIImage imageNamed:TUIChatImagePath(@"back")];
UIButton *leftButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
UIImage *formatImage = [TIMCommonDynamicImage(@"nav_back_img", defaultImage) rtl_imageFlippedForRightToLeftLayoutDirection];
[leftButton addTarget:self action:@selector(onBack:) forControlEvents:UIControlEventTouchUpInside];
[leftButton setImage:formatImage forState:UIControlStateNormal];
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc] initWithCustomView:leftButton];
self.navigationItem.leftBarButtonItem = leftItem;
_image = [[UIImageView alloc] initWithFrame:CGRectMake((self.view.frame.size.width - 80) * 0.5, NavBar_Height + StatusBar_Height + 50, 80, 80)];
_image.contentMode = UIViewContentModeScaleAspectFit;
_image.image = [UIImage imageNamed:TUIChatImagePath(@"msg_file")];
[self.view addSubview:_image];
_name = [[UILabel alloc] initWithFrame:CGRectMake(0, _image.frame.origin.y + _image.frame.size.height + 20, self.view.frame.size.width, 40)];
_name.textColor = [UIColor blackColor];
_name.font = [UIFont systemFontOfSize:15];
_name.textAlignment = NSTextAlignmentCenter;
_name.text = _data.fileName;
[self.view addSubview:_name];
_button = [[UIButton alloc] initWithFrame:CGRectMake(100, _name.frame.origin.y + _name.frame.size.height + 20, self.view.frame.size.width - 200, 40)];
[_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_button.backgroundColor = [UIColor colorWithRed:44 / 255.0 green:145 / 255.0 blue:247 / 255.0 alpha:1.0];
_button.layer.cornerRadius = 5;
[_button.layer setMasksToBounds:YES];
[_button addTarget:self action:@selector(onOpen:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
@weakify(self);
[RACObserve(_data, downladProgress) subscribeNext:^(NSNumber *x) {
@strongify(self);
int progress = [x intValue];
if (progress < 100 && progress > 0) {
[self.button setTitle:[NSString stringWithFormat:TIMCommonLocalizableString(TUIKitDownloadProgressFormat), progress] forState:UIControlStateNormal];
} else {
[self.button setTitle:TIMCommonLocalizableString(TUIKitOpenWithOtherApp) forState:UIControlStateNormal];
}
}];
if ([_data isLocalExist]) {
[self.button setTitle:TIMCommonLocalizableString(TUIKitOpenWithOtherApp) forState:UIControlStateNormal];
} else {
[self.button setTitle:TIMCommonLocalizableString(Download) forState:UIControlStateNormal];
}
}
- (void)onOpen:(id)sender {
BOOL isExist = NO;
NSString *path = [_data getFilePath:&isExist];
if (isExist) {
NSURL *url = [NSURL fileURLWithPath:path];
_document = [UIDocumentInteractionController interactionControllerWithURL:url];
_document.delegate = self;
[_document presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
} else {
[_data downloadFile];
}
}
- (void)onBack:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
- (UIView *)documentInteractionControllerViewForPreview:(UIDocumentInteractionController *)controller {
return self.view;
}
- (CGRect)documentInteractionControllerRectForPreview:(UIDocumentInteractionController *)controller {
return self.view.frame;
}
- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller {
return self;
}
@end

View File

@@ -0,0 +1,16 @@
//
// TUIGroupChatViewController.h
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/6/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIBaseChatViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIGroupChatViewController : TUIBaseChatViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,437 @@
//
// TUIGroupChatViewController.m
// TXIMSDK_TUIKit_iOS
//
// Created by kayev on 2021/6/17.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "ReactiveObjC/ReactiveObjC.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/NSDictionary+TUISafe.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUILogin.h>
#import "TUIBaseChatViewController+ProtectedAPI.h"
#import "TUIGroupChatViewController.h"
#import "TUIGroupPendencyController.h"
#import "TUIGroupPendencyDataProvider.h"
#import "TUILinkCellData.h"
#import "TUIMessageDataProvider.h"
#import "TUITextMessageCellData.h"
#import "TUIChatFlexViewController.h"
#import "TUIMessageController.h"
#import "TUIGroupPinCell.h"
#import "TUIGroupPinPageViewController.h"
@interface TUIGroupChatViewController () <V2TIMGroupListener>
//@property (nonatomic, strong) UIButton *atBtn;
@property(nonatomic, strong) UIView *tipsView;
@property(nonatomic, strong) UILabel *pendencyLabel;
@property(nonatomic, strong) UIButton *pendencyBtn;
@property(nonatomic, strong) TUIGroupPendencyDataProvider *pendencyViewModel;
@property(nonatomic, strong) NSMutableArray<TUIUserModel *> *atUserList;
@property(nonatomic, assign) BOOL responseKeyboard;
@property(nonatomic, strong) TUIGroupPinCellView *oneGroupPinView;
@property(nonatomic, strong) NSArray *groupPinList;
@property(nonatomic, strong) TUIGroupPinPageViewController *pinPageVC;
@end
@implementation TUIGroupChatViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor clearColor];
[self setupTipsView];
[self setupGroupPinTips];
[[V2TIMManager sharedInstance] addGroupListener:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(refreshTipsView)
name:TUICore_TUIChatExtension_ChatViewTopArea_ChangedNotification
object:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self refreshTipsView];
}
- (void)dealloc {
[TUICore unRegisterEventByObject:self];
}
- (void)setupTipsView {
self.tipsView = [[UIView alloc] initWithFrame:CGRectZero];
self.tipsView.backgroundColor = RGB(246, 234, 190);
[self.view addSubview:self.tipsView];
self.tipsView.mm_height(24).mm_width(self.view.mm_w);
self.pendencyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[self.tipsView addSubview:self.pendencyLabel];
self.pendencyLabel.font = [UIFont systemFontOfSize:12];
self.pendencyBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[self.tipsView addSubview:self.pendencyBtn];
[self.pendencyBtn setTitle:TIMCommonLocalizableString(TUIKitChatPendencyTitle) forState:UIControlStateNormal];
[self.pendencyBtn.titleLabel setFont:[UIFont systemFontOfSize:12]];
[self.pendencyBtn addTarget:self action:@selector(openPendency:) forControlEvents:UIControlEventTouchUpInside];
[self.pendencyBtn sizeToFit];
self.tipsView.alpha = 0;
@weakify(self);
[RACObserve(self.pendencyViewModel, unReadCnt) subscribeNext:^(NSNumber *unReadCnt) {
@strongify(self);
if ([unReadCnt intValue]) {
self.pendencyLabel.text = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitChatPendencyRequestToJoinGroupFormat), unReadCnt];
[self.pendencyLabel sizeToFit];
CGFloat gap = (self.tipsView.mm_w - self.pendencyLabel.mm_w - self.pendencyBtn.mm_w - 8) / 2;
self.pendencyLabel.mm_left(gap).mm__centerY(self.tipsView.mm_h / 2);
self.pendencyBtn.mm_hstack(8);
self.tipsView.alpha = 1;
[self refreshTipsView];
} else {
self.tipsView.alpha = 0;
}
}];
[self getPendencyList];
}
- (void)refreshTipsView {
UIView *topView = [TUIGroupChatViewController topAreaBottomView];
CGRect transRect = [topView convertRect:topView.bounds toView:self.view];
self.tipsView.frame = CGRectMake(0, transRect.origin.y + transRect.size.height, self.tipsView.frame.size.width, self.tipsView.frame.size.height);
}
- (void)setupGroupPinTips {
self.oneGroupPinView = [[TUIGroupPinCellView alloc] init];
CGFloat margin = 0 ;
self.oneGroupPinView.frame = CGRectMake(0, margin, self.view.frame.size.width, 62);
UIView *topView = [TUIGroupChatViewController groupPinTopView];
for (UIView *subview in topView.subviews) {
[subview removeFromSuperview];
}
topView.frame = CGRectMake(0, 0, self.view.mm_w, 0);
[topView addSubview:self.oneGroupPinView];
@weakify(self);
self.oneGroupPinView.isFirstPage = YES;
self.oneGroupPinView.onClickCellView = ^(V2TIMMessage *originMessage) {
@strongify(self);
if (self.groupPinList.count >= 2) {
[self gotoDetailPopPinPage];
}
else {
[self jump2GroupPinHighlightLine:originMessage];
}
};
self.oneGroupPinView.onClickRemove = ^(V2TIMMessage *originMessage) {
@strongify(self);
[self.messageController unPinGroupMessage:originMessage];
};
self.messageController.pinGroupMessageChanged = ^(NSArray * _Nonnull groupPinList) {
@strongify(self);
if (groupPinList.count > 0 ) {
if (!self.oneGroupPinView.superview) {
[topView addSubview:self.oneGroupPinView];
}
TUIMessageCellData * cellData = [TUIMessageDataProvider getCellData:[groupPinList lastObject]];
[self.oneGroupPinView fillWithData:cellData];
if (groupPinList.count >= 2) {
[self.oneGroupPinView showMultiAnimation];
topView.frame = CGRectMake(0, 0, self.view.mm_w, self.oneGroupPinView.frame.size.height +20 + margin);
self.oneGroupPinView.removeButton.hidden = YES;
}
else {
[self.oneGroupPinView hiddenMultiAnimation];
topView.frame = CGRectMake(0, 0, self.view.mm_w, self.oneGroupPinView.frame.size.height + margin);
if ([self.messageController isCurrentUserRoleSuperAdminInGroup]) {
self.oneGroupPinView.removeButton.hidden = NO;
}
else {
self.oneGroupPinView.removeButton.hidden = YES;
}
}
}
else {
[self.oneGroupPinView removeFromSuperview];
topView.frame = CGRectMake(0, 0, self.view.mm_w, 0);
}
self.groupPinList = groupPinList;
[[NSNotificationCenter defaultCenter] postNotificationName:TUICore_TUIChatExtension_ChatViewTopArea_ChangedNotification object:nil];
if (self.pinPageVC) {
NSMutableArray *formatGroupPinList = [NSMutableArray arrayWithArray:groupPinList.reverseObjectEnumerator.allObjects];
self.pinPageVC.groupPinList = formatGroupPinList;
self.pinPageVC.canRemove = [self.messageController isCurrentUserRoleSuperAdminInGroup];
if (groupPinList.count > 0) {
[self reloadPopPinPage];
}
else {
[self.pinPageVC dismissViewControllerAnimated:NO completion:nil];
}
}
};
self.messageController.groupRoleChanged = ^(V2TIMGroupMemberRole role) {
@strongify(self);
self.messageController.pinGroupMessageChanged(self.groupPinList);
if (self.pinPageVC) {
self.pinPageVC.canRemove = [self.messageController isCurrentUserRoleSuperAdminInGroup];
[self.pinPageVC.tableview reloadData];
}
};
}
- (void)gotoDetailPopPinPage {
TUIGroupPinPageViewController *vc = [[TUIGroupPinPageViewController alloc] init];
self.pinPageVC = vc;
NSMutableArray *formatGroupPinList = [NSMutableArray arrayWithArray:self.groupPinList.reverseObjectEnumerator.allObjects];
vc.groupPinList = formatGroupPinList;
vc.canRemove = [self.messageController isCurrentUserRoleSuperAdminInGroup];
vc.view.frame = self.view.frame;
CGFloat cellHight = (62);
CGFloat maxOnePage = 4;
float height = (self.groupPinList.count) * cellHight;
height = MIN(cellHight * maxOnePage , height);
UIView *topView = [TUIGroupChatViewController groupPinTopView];
CGRect transRect = [topView convertRect:topView.bounds toView:[UIApplication sharedApplication].delegate.window];
vc.modalPresentationStyle = UIModalPresentationOverFullScreen;
@weakify(self);
[self presentViewController:vc animated:NO completion:^{
vc.tableview.frame = CGRectMake(0, CGRectGetMinY(transRect), self.view.frame.size.width, 60);
vc.customArrowView.frame = CGRectMake(0, CGRectGetMaxY(vc.tableview.frame), vc.tableview.frame.size.width, 0);
vc.bottomShadow.frame = CGRectMake(0, CGRectGetMaxY(vc.customArrowView.frame), vc.tableview.frame.size.width, 0);
[UIView animateWithDuration:0.3 animations:^{
vc.tableview.frame = CGRectMake(0, CGRectGetMinY(transRect), self.view.frame.size.width, height);
vc.customArrowView.frame = CGRectMake(0, CGRectGetMaxY(vc.tableview.frame), vc.tableview.frame.size.width, 40);
vc.bottomShadow.frame = CGRectMake(0, CGRectGetMaxY(vc.customArrowView.frame),
vc.tableview.frame.size.width,
self.view.frame.size.height);
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:vc.customArrowView.bounds
byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight)
cornerRadii:CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = vc.customArrowView.bounds;
maskLayer.path = maskPath.CGPath;
vc.customArrowView.layer.mask = maskLayer;
}];
}];
vc.onClickRemove = ^(V2TIMMessage *originMessage) {
@strongify(self);
[self.messageController unPinGroupMessage:originMessage];
};
vc.onClickCellView = ^(V2TIMMessage *originMessage) {
@strongify(self);
[self jump2GroupPinHighlightLine:originMessage];
};
}
- (void)jump2GroupPinHighlightLine:(V2TIMMessage *)originMessage {
TUIMessageController *msgVC = (TUIMessageController *)self.messageController;
NSString * originMsgID = originMessage.msgID;
[msgVC findMessages:@[originMsgID ?: @""] callback:^(BOOL success, NSString * _Nonnull desc, NSArray<V2TIMMessage *> * _Nonnull messages) {
if (success) {
V2TIMMessage *message = messages.firstObject;
if (message && message.status == V2TIM_MSG_STATUS_SEND_SUCC ) {
[msgVC locateAssignMessage:originMessage matchKeyWord:@""];
}
else {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
}
}
}];
}
- (void)reloadPopPinPage {
CGFloat cellHight = (62);
CGFloat maxOnePage = 4;
float height = (self.groupPinList.count) * cellHight;
height = MIN(cellHight * maxOnePage , height);
self.pinPageVC.tableview.frame = CGRectMake(0, self.pinPageVC.tableview.frame.origin.y, self.view.frame.size.width, height);
self.pinPageVC.customArrowView.frame = CGRectMake(0, CGRectGetMaxY(self.pinPageVC.tableview.frame), self.pinPageVC.tableview.frame.size.width, 40);
self.pinPageVC.bottomShadow.frame = CGRectMake(0, CGRectGetMaxY(self.pinPageVC.customArrowView.frame),
self.pinPageVC.tableview.frame.size.width,
self.view.frame.size.height);
[self.pinPageVC.tableview reloadData];
}
- (void)getPendencyList {
if (self.conversationData.groupID.length > 0) [self.pendencyViewModel loadData];
}
- (void)openPendency:(id)sender {
TUIGroupPendencyController *vc = [[TUIGroupPendencyController alloc] init];
@weakify(self);
vc.cellClickBlock = ^(TUIGroupPendencyCell *_Nonnull cell) {
if (cell.pendencyData.isRejectd || cell.pendencyData.isAccepted) {
// After selecting, you will no longer enter the details page.
return;
}
@strongify(self);
[[V2TIMManager sharedInstance] getUsersInfo:@[ cell.pendencyData.fromUser ]
succ:^(NSArray<V2TIMUserFullInfo *> *profiles) {
// Show user profile VC
NSDictionary *param = @{
TUICore_TUIContactObjectFactory_UserProfileController_UserProfile : profiles.firstObject,
TUICore_TUIContactObjectFactory_UserProfileController_PendencyData : cell.pendencyData,
TUICore_TUIContactObjectFactory_UserProfileController_ActionType : @(3)
};
[self.navigationController pushViewController:TUICore_TUIContactObjectFactory_UserProfileController_Classic
param:param
forResult:nil];
}
fail:nil];
};
vc.viewModel = self.pendencyViewModel;
[self.navigationController pushViewController:vc animated:YES];
}
- (void)setConversationData:(TUIChatConversationModel *)conversationData {
[super setConversationData:conversationData];
if (self.conversationData.groupID.length > 0) {
_pendencyViewModel = [TUIGroupPendencyDataProvider new];
_pendencyViewModel.groupId = conversationData.groupID;
}
self.atUserList = [NSMutableArray array];
}
#pragma mark - V2TIMGroupListener
- (void)onReceiveJoinApplication:(NSString *)groupID member:(V2TIMGroupMemberInfo *)member opReason:(NSString *)opReason {
[self getPendencyList];
}
- (void)onGroupInfoChanged:(NSString *)groupID changeInfoList:(NSArray<V2TIMGroupChangeInfo *> *)changeInfoList {
if (![groupID isEqualToString:self.conversationData.groupID]) {
return;
}
for (V2TIMGroupChangeInfo *changeInfo in changeInfoList) {
if (changeInfo.type == V2TIM_GROUP_INFO_CHANGE_TYPE_NAME) {
self.conversationData.title = changeInfo.value;
return;
}
}
}
#pragma mark - TUIInputControllerDelegate
- (void)inputController:(TUIInputController *)inputController didSendMessage:(V2TIMMessage *)msg {
/**
* If the text message has @ user, createTextAtMessage is required
*/
if (msg.elemType == V2TIM_ELEM_TYPE_TEXT) {
NSMutableArray *atUserList = [NSMutableArray array];
for (TUIUserModel *model in self.atUserList) {
if (model.userId) {
[atUserList addObject:model.userId];
}
}
if (atUserList.count > 0) {
NSData *cloudCustomData = msg.cloudCustomData;
msg = [[V2TIMManager sharedInstance] createTextAtMessage:msg.textElem.text atUserList:atUserList];
msg.cloudCustomData = cloudCustomData;
}
/**
* After the message is sent, the atUserList need to be reset
*/
[self.atUserList removeAllObjects];
}
[super inputController:inputController didSendMessage:msg];
}
- (void)inputControllerDidInputAt:(TUIInputController *)inputController {
[super inputControllerDidInputAt:inputController];
/**
* Input of @ character detected
*/
if (self.conversationData.groupID.length > 0) {
if ([self.navigationController.topViewController isKindOfClass:NSClassFromString(@"TUISelectGroupMemberViewController")]) {
return;
}
//When pushing a new VC, the keyboard needs to be hidden.
[self.inputController reset];
__weak typeof(self) weakSelf = self;
NSMutableDictionary *param = [NSMutableDictionary dictionary];
param[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_GroupID] = self.conversationData.groupID;
param[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Name] = TIMCommonLocalizableString(TUIKitAtSelectMemberTitle);
param[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_OptionalStyle] = @(1);
[self.navigationController
pushViewController:TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Classic
param:param
forResult:^(NSDictionary *_Nonnull param) {
NSArray<TUIUserModel *> *modelList = [param tui_objectForKey:TUICore_TUIContactObjectFactory_SelectGroupMemberVC_ResultUserList
asClass:NSArray.class];
NSMutableString *atText = [[NSMutableString alloc] init];
for (int i = 0; i < modelList.count; i++) {
TUIUserModel *model = modelList[i];
if (![model isKindOfClass:TUIUserModel.class]) {
NSAssert(NO, @"Error data-type in modelList");
continue;
}
[weakSelf.atUserList addObject:model];
[atText appendString:[NSString stringWithFormat:@"@%@ ", model.name]];
}
NSAttributedString *spaceString = [[NSAttributedString alloc]
initWithString:atText
attributes:@{NSFontAttributeName : kTUIInputNoramlFont, NSForegroundColorAttributeName : kTUIInputNormalTextColor}];
[weakSelf.inputController.inputBar addWordsToInputBar:spaceString ];
}];
}
}
- (void)inputController:(TUIInputController *)inputController didDeleteAt:(NSString *)atText {
[super inputController:inputController didDeleteAt:atText];
for (TUIUserModel *user in self.atUserList) {
if ([atText rangeOfString:user.name].location != NSNotFound) {
[self.atUserList removeObject:user];
break;
}
}
}
- (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell {
[super inputController:inputController didSelectMoreCell:cell];
}
#pragma mark - TUIBaseMessageControllerDelegate
- (void)messageController:(TUIBaseMessageController *)controller onLongSelectMessageAvatar:(TUIMessageCell *)cell {
if (!cell || !cell.messageData || !cell.messageData.identifier) {
return;
}
if ([cell.messageData.identifier isEqualToString:[TUILogin getUserID]]) {
return;
}
BOOL atUserExist = NO;
for (TUIUserModel *model in self.atUserList) {
if ([model.userId isEqualToString:cell.messageData.identifier]) {
atUserExist = YES;
break;
}
}
if (!atUserExist) {
TUIUserModel *user = [[TUIUserModel alloc] init];
user.userId = cell.messageData.identifier;
user.name = cell.messageData.senderName;
[self.atUserList addObject:user];
NSString *nameString = [NSString stringWithFormat:@"@%@ ", user.name];
UIFont *textFont = kTUIInputNoramlFont;
NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:nameString attributes:@{NSFontAttributeName : textFont}];
[self.inputController.inputBar addWordsToInputBar:spaceString ];
}
}
#pragma mark - Override Methods
- (NSString *)forwardTitleWithMyName:(NSString *)nameStr {
return TIMCommonLocalizableString(TUIKitRelayGroupChatHistory);
}
@end

View File

@@ -0,0 +1,23 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
@class TUIGroupInfoController;
@class TUIGroupMemberCellData;
@class V2TIMGroupInfo;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIGroupInfoController
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIGroupInfoController : UITableViewController
@property(nonatomic, strong) NSString *groupId;
- (void)updateData;
- (void)updateGroupInfo;
@end

View File

@@ -0,0 +1,602 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIGroupInfoController.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUILogin.h>
#import <TUICore/TUIThemeManager.h>
#import <TIMCommon/TIMGroupInfo+TUIDataProvider.h>
#import "TUIGroupInfoDataProvider.h"
#import <TIMCommon/TUICommonGroupInfoCellData.h>
#import "TUIGroupNoticeCell.h"
#import "TUIGroupNoticeController.h"
#define ADD_TAG @"-1"
#define DEL_TAG @"-2"
@interface TUIGroupInfoController () <TUIModifyViewDelegate,TUIProfileCardDelegate, TUIGroupInfoDataProviderDelegate>
@property(nonatomic, strong) TUIGroupInfoDataProvider *dataProvider;
@property(nonatomic, strong) TUINaviBarIndicatorView *titleView;
@property(nonatomic, strong) UIViewController *showContactSelectVC;
@end
@implementation TUIGroupInfoController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupViews];
self.dataProvider = [[TUIGroupInfoDataProvider alloc] initWithGroupID:self.groupId];
self.dataProvider.delegate = self;
[self.dataProvider loadData];
@weakify(self);
[RACObserve(self.dataProvider, dataList) subscribeNext:^(id _Nullable x) {
@strongify(self);
[self.tableView reloadData];
}];
_titleView = [[TUINaviBarIndicatorView alloc] init];
self.navigationItem.titleView = _titleView;
self.navigationItem.title = @"";
[_titleView setTitle:TIMCommonLocalizableString(ProfileDetails)];
}
- (void)setupViews {
self.tableView.tableFooterView = [[UIView alloc] init];
self.tableView.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
self.tableView.delaysContentTouches = NO;
if (@available(iOS 15.0, *)) {
self.tableView.sectionHeaderTopPadding = 0;
}
}
- (void)updateData {
[self.dataProvider loadData];
}
- (void)updateGroupInfo {
[self.dataProvider updateGroupInfo];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.dataProvider.dataList.count;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor clearColor];
return view;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 10;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSMutableArray *array = self.dataProvider.dataList[section];
return array.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSMutableArray *array = self.dataProvider.dataList[indexPath.section];
NSObject *data = array[indexPath.row];
if ([data isKindOfClass:[TUIProfileCardCellData class]]) {
return [(TUIProfileCardCellData *)data heightOfWidth:Screen_Width];
} else if ([data isKindOfClass:[TUIGroupMembersCellData class]]) {
return [(TUIGroupMembersCellData *)data heightOfWidth:Screen_Width];
} else if ([data isKindOfClass:[TUIButtonCellData class]]) {
return [(TUIButtonCellData *)data heightOfWidth:Screen_Width];
} else if ([data isKindOfClass:[TUICommonSwitchCellData class]]) {
return [(TUICommonSwitchCellData *)data heightOfWidth:Screen_Width];
} else if ([data isKindOfClass:TUIGroupNoticeCellData.class]) {
return 72.0;
}
return 44;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSMutableArray *array = self.dataProvider.dataList[indexPath.section];
NSObject *data = array[indexPath.row];
if ([data isKindOfClass:[TUIProfileCardCellData class]]) {
TUIProfileCardCell *cell = [tableView dequeueReusableCellWithIdentifier:TGroupCommonCell_ReuseId];
if (!cell) {
cell = [[TUIProfileCardCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TGroupCommonCell_ReuseId];
}
cell.delegate = self;
[cell fillWithData:(TUIProfileCardCellData *)data];
return cell;
} else if ([data isKindOfClass:[TUICommonTextCellData class]]) {
TUICommonTextCell *cell = [tableView dequeueReusableCellWithIdentifier:TKeyValueCell_ReuseId];
if (!cell) {
cell = [[TUICommonTextCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TKeyValueCell_ReuseId];
}
[cell fillWithData:(TUICommonTextCellData *)data];
return cell;
} else if ([data isKindOfClass:[TUIGroupMembersCellData class]]) {
TUICommonTableViewCell *cell = [[TUICommonTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TGroupMembersCell_ReuseId];
NSDictionary *param = @{
@"data": data ,
@"pushVC": self.navigationController,
@"groupID": self.groupId ? self.groupId : @"",
@"membersData": self.dataProvider.membersData ,
@"groupMembersCellData": self.dataProvider.groupMembersCellData,
};
[TUICore raiseExtension:TUICore_TUIChatExtension_GroupProfileMemberListExtension_ClassicExtensionID
parentView:cell
param:param];
return cell;
} else if ([data isKindOfClass:[TUICommonSwitchCellData class]]) {
TUICommonSwitchCell *cell = [tableView dequeueReusableCellWithIdentifier:TSwitchCell_ReuseId];
if (!cell) {
cell = [[TUICommonSwitchCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TSwitchCell_ReuseId];
}
[cell fillWithData:(TUICommonSwitchCellData *)data];
return cell;
} else if ([data isKindOfClass:[TUIButtonCellData class]]) {
TUIButtonCell *cell = [tableView dequeueReusableCellWithIdentifier:TButtonCell_ReuseId];
if (!cell) {
cell = [[TUIButtonCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TButtonCell_ReuseId];
}
[cell fillWithData:(TUIButtonCellData *)data];
return cell;
} else if ([data isKindOfClass:TUIGroupNoticeCellData.class]) {
TUIGroupNoticeCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TUIGroupNoticeCell"];
if (cell == nil) {
cell = [[TUIGroupNoticeCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"TUIGroupNoticeCell"];
}
cell.cellData = data;
return cell;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}
- (void)leftBarButtonClick:(UIButton *)sender {
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark TUIGroupInfoDataProviderDelegate
- (void)groupProfileExtensionButtonClick:(TUICommonTextCell *)cell {
TUIExtensionInfo *info = cell.data.tui_extValueObj;
if (info == nil || ![info isKindOfClass:TUIExtensionInfo.class] || info.onClicked == nil) {
return;
}
info.onClicked(@{});
}
- (void)didSelectMembers {
NSMutableDictionary *param = [NSMutableDictionary dictionary];
if (_groupId && _groupId.length >0) {
param[@"groupID"] = _groupId;
}
if (self.dataProvider.groupInfo) {
param[@"groupInfo"] = self.dataProvider.groupInfo;
}
UIViewController *vc = [TUICore createObject:TUICore_TUIContactObjectFactory
key:TUICore_TUIContactObjectFactory_GetGroupMemberVCMethod
param:param];
if (vc && [vc isKindOfClass:UIViewController.class]) {
[self.navigationController pushViewController:vc animated:YES];
}
}
- (void)didSelectAddOption:(TUICommonTextCell *)cell {
TUICommonTextCellData *data = cell.textData;
BOOL isApprove = [data.key isEqualToString:TIMCommonLocalizableString(TUIKitGroupProfileInviteType)];
__weak typeof(self) weakSelf = self;
UIAlertController *ac = [UIAlertController
alertControllerWithTitle:nil
message:isApprove ? TIMCommonLocalizableString(TUIKitGroupProfileInviteType) : TIMCommonLocalizableString(TUIKitGroupProfileJoinType)
preferredStyle:UIAlertControllerStyleActionSheet];
NSArray *actionList = @[
@{
@(V2TIM_GROUP_ADD_FORBID) : isApprove ? TIMCommonLocalizableString(TUIKitGroupProfileInviteDisable)
: TIMCommonLocalizableString(TUIKitGroupProfileJoinDisable)
},
@{@(V2TIM_GROUP_ADD_AUTH) : TIMCommonLocalizableString(TUIKitGroupProfileAdminApprove)},
@{@(V2TIM_GROUP_ADD_ANY) : TIMCommonLocalizableString(TUIKitGroupProfileAutoApproval)}
];
for (NSDictionary *map in actionList) {
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:map.allValues.firstObject
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
V2TIMGroupAddOpt opt = (V2TIMGroupAddOpt)[map.allKeys.firstObject intValue];
if (isApprove) {
[weakSelf.dataProvider setGroupApproveOpt:opt];
} else {
[weakSelf.dataProvider setGroupAddOpt:opt];
}
}]];
}
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Cancel) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:ac animated:YES completion:nil];
}
- (void)didSelectGroupNick:(TUICommonTextCell *)cell {
TUIModifyViewData *data = [[TUIModifyViewData alloc] init];
data.title = TIMCommonLocalizableString(TUIKitGroupProfileEditAlias);
data.content = self.dataProvider.selfInfo.nameCard;
data.desc = TIMCommonLocalizableString(TUIKitGroupProfileEditAliasDesc);
TUIModifyView *modify = [[TUIModifyView alloc] init];
modify.tag = 2;
modify.delegate = self;
[modify setData:data];
[modify showInWindow:self.view.window];
}
- (void)didSelectCommon {
UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
__weak typeof(self) weakSelf = self;
if ([self.dataProvider.groupInfo isPrivate] || [TUIGroupInfoDataProvider isMeOwner:self.dataProvider.groupInfo]) {
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitGroupProfileEditGroupName)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
TUIModifyViewData *data = [[TUIModifyViewData alloc] init];
data.title = TIMCommonLocalizableString(TUIKitGroupProfileEditGroupName);
data.content = weakSelf.dataProvider.groupInfo.groupName;
data.desc = TIMCommonLocalizableString(TUIKitGroupProfileEditGroupName);
TUIModifyView *modify = [[TUIModifyView alloc] init];
modify.tag = 0;
modify.delegate = weakSelf;
[modify setData:data];
[modify showInWindow:weakSelf.view.window];
}]];
}
if ([TUIGroupInfoDataProvider isMeOwner:self.dataProvider.groupInfo]) {
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitGroupProfileEditAnnouncement)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
TUIModifyViewData *data = [[TUIModifyViewData alloc] init];
data.title = TIMCommonLocalizableString(TUIKitGroupProfileEditAnnouncement);
TUIModifyView *modify = [[TUIModifyView alloc] init];
modify.tag = 1;
modify.delegate = weakSelf;
[modify setData:data];
[modify showInWindow:weakSelf.view.window];
}]];
}
if ([TUIGroupInfoDataProvider isMeOwner:self.dataProvider.groupInfo]) {
@weakify(self);
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitGroupProfileEditAvatar)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
@strongify(self);
TUISelectAvatarController *vc = [[TUISelectAvatarController alloc] init];
vc.selectAvatarType = TUISelectAvatarTypeGroupAvatar;
vc.profilFaceURL = self.dataProvider.groupInfo.faceURL;
[self.navigationController pushViewController:vc animated:YES];
vc.selectCallBack = ^(NSString *_Nonnull urlStr) {
if (urlStr.length > 0) {
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupId;
info.faceURL = urlStr;
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
[self updateGroupInfo];
}
fail:^(int code, NSString *msg) {
[TUITool makeToastError:code msg:msg];
}];
}
};
}]];
}
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Cancel) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:ac animated:YES completion:nil];
}
- (void)didSelectOnNotDisturb:(TUICommonSwitchCell *)cell {
V2TIMReceiveMessageOpt opt;
if (cell.switcher.on) {
opt = V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE;
} else {
opt = V2TIM_RECEIVE_MESSAGE;
}
@weakify(self);
[V2TIMManager.sharedInstance markConversation:@[ [NSString stringWithFormat:@"group_%@", self.groupId] ]
markType:@(V2TIM_CONVERSATION_MARK_TYPE_FOLD)
enableMark:NO
succ:nil
fail:nil];
[self.dataProvider setGroupReceiveMessageOpt:opt
Succ:^{
@strongify(self);
[self updateGroupInfo];
}
fail:^(int code, NSString *desc){
}];
}
- (void)didSelectOnTop:(TUICommonSwitchCell *)cell {
if (cell.switcher.on) {
[[TUIConversationPin sharedInstance] addTopConversation:[NSString stringWithFormat:@"group_%@", _groupId]
callback:^(BOOL success, NSString *_Nonnull errorMessage) {
if (success) {
return;
}
cell.switcher.on = !cell.switcher.isOn;
[TUITool makeToast:errorMessage];
}];
} else {
[[TUIConversationPin sharedInstance] removeTopConversation:[NSString stringWithFormat:@"group_%@", _groupId]
callback:^(BOOL success, NSString *_Nonnull errorMessage) {
if (success) {
return;
}
cell.switcher.on = !cell.switcher.isOn;
[TUITool makeToast:errorMessage];
}];
}
}
- (void)didDeleteGroup:(TUIButtonCell *)cell {
UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil
message:TIMCommonLocalizableString(TUIKitGroupProfileDeleteGroupTips)
preferredStyle:UIAlertControllerStyleActionSheet];
@weakify(self);
[ac tuitheme_addAction:[UIAlertAction
actionWithTitle:TIMCommonLocalizableString(Confirm)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *_Nonnull action) {
@strongify(self);
@weakify(self);
if ([self.dataProvider.groupInfo canDismissGroup]) {
[self.dataProvider
dismissGroup:^{
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
UIViewController *vc = [self findConversationListViewController];
[[TUIConversationPin sharedInstance]
removeTopConversation:[NSString stringWithFormat:@"group_%@", self.groupId]
callback:nil];
[V2TIMManager.sharedInstance markConversation:@[ [NSString stringWithFormat:@"group_%@", self.groupId] ]
markType:@(V2TIM_CONVERSATION_MARK_TYPE_FOLD)
enableMark:NO
succ:^(NSArray<V2TIMConversationOperationResult *> *result) {
[self.navigationController popToViewController:vc animated:YES];
}
fail:^(int code, NSString *desc) {
[self.navigationController popToViewController:vc animated:YES];
}];
});
}
fail:^(int code, NSString *msg) {
[TUITool makeToastError:code msg:msg];
}];
} else {
[self.dataProvider
quitGroup:^{
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
UIViewController *vc = [self findConversationListViewController];
[[TUIConversationPin sharedInstance]
removeTopConversation:[NSString stringWithFormat:@"group_%@", self.groupId]
callback:nil];
[V2TIMManager.sharedInstance markConversation:@[ [NSString stringWithFormat:@"group_%@", self.groupId] ]
markType:@(V2TIM_CONVERSATION_MARK_TYPE_FOLD)
enableMark:NO
succ:^(NSArray<V2TIMConversationOperationResult *> *result) {
[self.navigationController popToViewController:vc animated:YES];
}
fail:^(int code, NSString *desc) {
[self.navigationController popToViewController:vc animated:YES];
}];
});
}
fail:^(int code, NSString *msg) {
[TUITool makeToastError:code msg:msg];
}];
}
}]];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Cancel) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:ac animated:YES completion:nil];
}
- (void)didReportGroup:(TUIButtonCell *)cell {
NSURL *url = [NSURL URLWithString:@"https://cloud.tencent.com/act/event/report-platform"];
[TUITool openLinkWithURL:url];
}
- (UIViewController *)findConversationListViewController {
UIViewController *vc = self.navigationController.viewControllers[0];
for (UIViewController *vc in self.navigationController.viewControllers) {
if ([vc isKindOfClass:NSClassFromString(@"TUIFoldListViewController")]) {
return vc;
}
}
return vc;
}
- (void)didSelectOnFoldConversation:(TUICommonSwitchCell *)cell {
BOOL enableMark = NO;
if (cell.switcher.on) {
enableMark = YES;
}
@weakify(self);
[V2TIMManager.sharedInstance markConversation:@[ [NSString stringWithFormat:@"group_%@", self.groupId] ]
markType:@(V2TIM_CONVERSATION_MARK_TYPE_FOLD)
enableMark:enableMark
succ : ^(NSArray<V2TIMConversationOperationResult *> *result) {
cell.switchData.on = enableMark;
[[TUIConversationPin sharedInstance] removeTopConversation:[NSString stringWithFormat:@"group_%@", self.groupId]
callback:^(BOOL success, NSString *_Nonnull errorMessage) {
@strongify(self);
[self updateGroupInfo];
}];
} fail:nil];
}
- (void)didSelectOnChangeBackgroundImage:(TUICommonTextCell *)cell {
@weakify(self);
NSString *conversationID = [NSString stringWithFormat:@"group_%@", self.groupId];
TUISelectAvatarController *vc = [[TUISelectAvatarController alloc] init];
vc.selectAvatarType = TUISelectAvatarTypeConversationBackGroundCover;
vc.profilFaceURL = [self getBackgroundImageUrlByConversationID:conversationID];
[self.navigationController pushViewController:vc animated:YES];
vc.selectCallBack = ^(NSString *_Nonnull urlStr) {
@strongify(self);
[self appendBackgroundImage:urlStr conversationID:conversationID];
if (IS_NOT_EMPTY_NSSTRING(conversationID)) {
[TUICore notifyEvent:TUICore_TUIContactNotify
subKey:TUICore_TUIContactNotify_UpdateConversationBackgroundImageSubKey
object:self
param:@{TUICore_TUIContactNotify_UpdateConversationBackgroundImageSubKey_ConversationID : conversationID}];
}
};
}
- (NSString *)getBackgroundImageUrlByConversationID:(NSString *)targerConversationID {
if (targerConversationID.length == 0) {
return nil;
}
NSDictionary *dict = [NSUserDefaults.standardUserDefaults objectForKey:@"conversation_backgroundImage_map"];
if (dict == nil) {
dict = @{};
}
NSString *conversationID_UserID = [NSString stringWithFormat:@"%@_%@", targerConversationID, [TUILogin getUserID]];
if (![dict isKindOfClass:NSDictionary.class] || ![dict.allKeys containsObject:conversationID_UserID]) {
return nil;
}
return [dict objectForKey:conversationID_UserID];
}
- (void)appendBackgroundImage:(NSString *)imgUrl conversationID:(NSString *)conversationID {
if (conversationID.length == 0) {
return;
}
NSDictionary *dict = [NSUserDefaults.standardUserDefaults objectForKey:@"conversation_backgroundImage_map"];
if (dict == nil) {
dict = @{};
}
if (![dict isKindOfClass:NSDictionary.class]) {
return;
}
NSString *conversationID_UserID = [NSString stringWithFormat:@"%@_%@", conversationID, [TUILogin getUserID]];
NSMutableDictionary *originDataDict = [NSMutableDictionary dictionaryWithDictionary:dict];
if (imgUrl.length == 0) {
[originDataDict removeObjectForKey:conversationID_UserID];
} else {
[originDataDict setObject:imgUrl forKey:conversationID_UserID];
}
[NSUserDefaults.standardUserDefaults setObject:originDataDict forKey:@"conversation_backgroundImage_map"];
[NSUserDefaults.standardUserDefaults synchronize];
}
- (void)didClearAllHistory:(TUIButtonCell *)cell {
@weakify(self);
UIAlertController *ac = [UIAlertController alertControllerWithTitle:nil
message:TIMCommonLocalizableString(TUIKitClearAllChatHistoryTips)
preferredStyle:UIAlertControllerStyleAlert];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *_Nonnull action) {
@strongify(self);
[self.dataProvider
clearAllHistory:^{
[TUICore notifyEvent:TUICore_TUIConversationNotify
subKey:TUICore_TUIConversationNotify_ClearConversationUIHistorySubKey
object:self
param:nil];
[TUITool makeToast:@"success"];
}
fail:^(int code, NSString *desc) {
[TUITool makeToastError:code msg:desc];
}];
}]];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Cancel) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:ac animated:YES completion:nil];
}
- (UINavigationController *)pushNavigationController {
return self.navigationController;
}
- (void)didSelectGroupNotice {
TUIGroupNoticeController *vc = [[TUIGroupNoticeController alloc] init];
vc.groupID = self.groupId;
__weak typeof(self) weakSelf = self;
vc.onNoticeChanged = ^{
[weakSelf updateGroupInfo];
};
[self.navigationController pushViewController:vc animated:YES];
}
#pragma mark TUIProfileCardDelegate
- (void)didTapOnAvatar:(TUIProfileCardCell *)cell {
TUISelectAvatarController *vc = [[TUISelectAvatarController alloc] init];
vc.selectAvatarType = TUISelectAvatarTypeGroupAvatar;
vc.profilFaceURL = self.dataProvider.groupInfo.faceURL;
[self.navigationController pushViewController:vc animated:YES];
@weakify(self);
vc.selectCallBack = ^(NSString *_Nonnull urlStr) {
@strongify(self);
if (urlStr.length > 0) {
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupId;
info.faceURL = urlStr;
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
[self updateGroupInfo];
}
fail:^(int code, NSString *msg) {
[TUITool makeToastError:code msg:msg];
}];
}
};
}
#pragma mark TUIModifyViewDelegate
- (void)modifyView:(TUIModifyView *)modifyView didModiyContent:(NSString *)content {
if (modifyView.tag == 0) {
[self.dataProvider setGroupName:content];
} else if (modifyView.tag == 1) {
[self.dataProvider setGroupNotification:content];
} else if (modifyView.tag == 2) {
[self.dataProvider setGroupMemberNameCard:content];
}
}
@end
@interface IUGroupView : UIView
@property(nonatomic, strong) UIView *view;
@end
@implementation IUGroupView
- (instancetype)init {
self = [super init];
if (self) {
self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
[self addSubview:self.view];
}
return self;
}
@end

View File

@@ -0,0 +1,62 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
@import Foundation;
@import UIKit;
@import ImSDK_Plus;
@class TUICommonCellData;
@class TUICommonTextCell;
@class TUICommonSwitchCell;
@class TUIButtonCell;
@class TUIProfileCardCellData;
@class TUIProfileCardCell;
@class TUIGroupMemberCellData;
@class TUIGroupMembersCellData;
NS_ASSUME_NONNULL_BEGIN
@protocol TUIGroupInfoDataProviderDelegate <NSObject>
- (UINavigationController *)pushNavigationController;
- (void)didSelectMembers;
- (void)didSelectGroupNick:(TUICommonTextCell *)cell;
- (void)didSelectAddOption:(UITableViewCell *)cell;
- (void)didSelectCommon;
- (void)didSelectOnNotDisturb:(TUICommonSwitchCell *)cell;
- (void)didSelectOnTop:(TUICommonSwitchCell *)cell;
- (void)didSelectOnFoldConversation:(TUICommonSwitchCell *)cell;
- (void)didSelectOnChangeBackgroundImage:(TUICommonTextCell *)cell;
- (void)didDeleteGroup:(TUIButtonCell *)cell;
- (void)didClearAllHistory:(TUIButtonCell *)cell;
- (void)didSelectGroupNotice;
@end
@interface TUIGroupInfoDataProvider : NSObject
@property(nonatomic, weak) id<TUIGroupInfoDataProviderDelegate> delegate;
@property(nonatomic, strong) V2TIMGroupInfo *groupInfo;
@property(nonatomic, strong) NSMutableArray *dataList;
@property(nonatomic, strong) NSMutableArray<TUIGroupMemberCellData *> *membersData;
@property(nonatomic, strong) TUIGroupMembersCellData *groupMembersCellData;
@property(nonatomic, strong, readonly) V2TIMGroupMemberFullInfo *selfInfo;
@property(nonatomic, strong, readonly) TUIProfileCardCellData *profileCellData;
- (instancetype)initWithGroupID:(NSString *)groupID;
- (void)loadData;
- (void)updateGroupInfo;
- (void)setGroupAddOpt:(V2TIMGroupAddOpt)opt;
- (void)setGroupApproveOpt:(V2TIMGroupAddOpt)opt;
- (void)setGroupReceiveMessageOpt:(V2TIMReceiveMessageOpt)opt Succ:(V2TIMSucc)succ fail:(V2TIMFail)fail;
- (void)setGroupName:(NSString *)groupName;
- (void)setGroupNotification:(NSString *)notification;
- (void)setGroupMemberNameCard:(NSString *)nameCard;
- (void)dismissGroup:(V2TIMSucc)succ fail:(V2TIMFail)fail;
- (void)quitGroup:(V2TIMSucc)succ fail:(V2TIMFail)fail;
- (void)clearAllHistory:(V2TIMSucc)succ fail:(V2TIMFail)fail;
- (void)updateGroupAvatar:(NSString *)url succ:(V2TIMSucc)succ fail:(V2TIMFail)fail;
- (void)transferGroupOwner:(NSString *)groupID member:(NSString *)userID succ:(V2TIMSucc)succ fail:(V2TIMFail)fail;
+ (BOOL)isMeOwner:(V2TIMGroupInfo *)groupInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,748 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TIMGroupInfo+TUIDataProvider.h>
#import "TUIGroupInfoDataProvider.h"
#import <TIMCommon/TUICommonGroupInfoCellData.h>
#import "TUIGroupNoticeCell.h"
#import "TUIGroupConfig.h"
#import <TUICore/TUICore.h>
@interface TUIGroupInfoDataProvider () <V2TIMGroupListener>
@property(nonatomic, strong) TUICommonTextCellData *addOptionData;
@property(nonatomic, strong) TUICommonTextCellData *inviteOptionData;
@property(nonatomic, strong) TUICommonTextCellData *groupNickNameCellData;
@property(nonatomic, strong, readwrite) TUIProfileCardCellData *profileCellData;
@property(nonatomic, strong) V2TIMGroupMemberFullInfo *selfInfo;
@property(nonatomic, strong) NSString *groupID;
@property(nonatomic, assign) BOOL firstLoadData;
@end
@implementation TUIGroupInfoDataProvider
- (instancetype)initWithGroupID:(NSString *)groupID {
self = [super init];
if (self) {
self.groupID = groupID;
[[V2TIMManager sharedInstance] addGroupListener:self];
}
return self;
}
#pragma mark V2TIMGroupListener
- (void)onMemberEnter:(NSString *)groupID memberList:(NSArray<V2TIMGroupMemberInfo *> *)memberList {
[self loadData];
}
- (void)onMemberLeave:(NSString *)groupID member:(V2TIMGroupMemberInfo *)member {
[self loadData];
}
- (void)onMemberInvited:(NSString *)groupID opUser:(V2TIMGroupMemberInfo *)opUser memberList:(NSArray<V2TIMGroupMemberInfo *> *)memberList {
[self loadData];
}
- (void)onMemberKicked:(NSString *)groupID opUser:(V2TIMGroupMemberInfo *)opUser memberList:(NSArray<V2TIMGroupMemberInfo *> *)memberList {
[self loadData];
}
- (void)onGroupInfoChanged:(NSString *)groupID changeInfoList:(NSArray<V2TIMGroupChangeInfo *> *)changeInfoList {
if (![groupID isEqualToString:self.groupID]) {
return;
}
[self loadData];
}
- (void)loadData {
if (self.groupID.length == 0) {
[TUITool makeToastError:ERR_INVALID_PARAMETERS msg:@"invalid groupID"];
return;
}
self.firstLoadData = YES;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self getGroupInfo:^{
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self getGroupMembers:^{
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self getSelfInfoInGroup:^{
dispatch_group_leave(group);
}];
__weak typeof(self) weakSelf = self;
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[weakSelf setupData];
});
}
- (void)updateGroupInfo {
@weakify(self);
[[V2TIMManager sharedInstance] getGroupsInfo:@[ self.groupID ]
succ:^(NSArray<V2TIMGroupInfoResult *> *groupResultList) {
@strongify(self);
if (groupResultList.count == 1) {
self.groupInfo = groupResultList[0].info;
[self setupData];
}
}
fail:^(int code, NSString *msg) {
[self makeToastError:code msg:msg];
}];
}
- (void)makeToastError:(NSInteger)code msg:(NSString *)msg {
if (!self.firstLoadData) {
[TUITool makeToastError:code msg:msg];
}
}
- (void)transferGroupOwner:(NSString *)groupID member:(NSString *)userID succ:(V2TIMSucc)succ fail:(V2TIMFail)fail {
[V2TIMManager.sharedInstance transferGroupOwner:groupID
member:userID
succ:^{
succ();
}
fail:^(int code, NSString *desc) {
fail(code, desc);
}];
}
- (void)updateGroupAvatar:(NSString *)url succ:(V2TIMSucc)succ fail:(V2TIMFail)fail {
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupID;
info.faceURL = url;
[V2TIMManager.sharedInstance setGroupInfo:info succ:succ fail:fail];
}
- (void)getGroupInfo:(dispatch_block_t)callback {
__weak typeof(self) weakSelf = self;
[[V2TIMManager sharedInstance] getGroupsInfo:@[ self.groupID ]
succ:^(NSArray<V2TIMGroupInfoResult *> *groupResultList) {
weakSelf.groupInfo = groupResultList.firstObject.info;
if (callback) {
callback();
}
}
fail:^(int code, NSString *msg) {
if (callback) {
callback();
}
[self makeToastError:code msg:msg];
}];
}
- (void)getGroupMembers:(dispatch_block_t)callback {
__weak typeof(self) weakSelf = self;
[[V2TIMManager sharedInstance] getGroupMemberList:self.groupID
filter:V2TIM_GROUP_MEMBER_FILTER_ALL
nextSeq:0
succ:^(uint64_t nextSeq, NSArray<V2TIMGroupMemberFullInfo *> *memberList) {
NSMutableArray *membersData = [NSMutableArray array];
for (V2TIMGroupMemberFullInfo *fullInfo in memberList) {
TUIGroupMemberCellData *data = [[TUIGroupMemberCellData alloc] init];
data.identifier = fullInfo.userID;
data.name = fullInfo.userID;
data.avatarUrl = fullInfo.faceURL;
if (fullInfo.nameCard.length > 0) {
data.name = fullInfo.nameCard;
} else if (fullInfo.friendRemark.length > 0) {
data.name = fullInfo.friendRemark;
} else if (fullInfo.nickName.length > 0) {
data.name = fullInfo.nickName;
}
[membersData addObject:data];
}
weakSelf.membersData = membersData;
if (callback) {
callback();
}
}
fail:^(int code, NSString *msg) {
weakSelf.membersData = [NSMutableArray arrayWithCapacity:1];
[self makeToastError:code msg:msg];
if (callback) {
callback();
}
}];
}
- (void)getSelfInfoInGroup:(dispatch_block_t)callback {
NSString *loginUserID = [[V2TIMManager sharedInstance] getLoginUser];
if (loginUserID.length == 0) {
if (callback) {
callback();
}
return;
}
__weak typeof(self) weakSelf = self;
[[V2TIMManager sharedInstance] getGroupMembersInfo:self.groupID
memberList:@[ loginUserID ]
succ:^(NSArray<V2TIMGroupMemberFullInfo *> *memberList) {
for (V2TIMGroupMemberFullInfo *item in memberList) {
if ([item.userID isEqualToString:loginUserID]) {
weakSelf.selfInfo = item;
break;
}
}
if (callback) {
callback();
}
}
fail:^(int code, NSString *desc) {
[self makeToastError:code msg:desc];
if (callback) {
callback();
}
}];
}
- (void)setupData {
NSMutableArray *dataList = [NSMutableArray array];
if (self.groupInfo) {
NSMutableArray *commonArray = [NSMutableArray array];
TUIProfileCardCellData *commonData = [[TUIProfileCardCellData alloc] init];
commonData.avatarImage = DefaultGroupAvatarImageByGroupType(self.groupInfo.groupType);
commonData.avatarUrl = [NSURL URLWithString:self.groupInfo.faceURL];
commonData.name = self.groupInfo.groupName;
commonData.identifier = self.groupInfo.groupID;
commonData.signature = self.groupInfo.notification;
if ([TUIGroupInfoDataProvider isMeOwner:self.groupInfo] || [self.groupInfo isPrivate]) {
commonData.cselector = @selector(didSelectCommon);
commonData.showAccessory = YES;
}
self.profileCellData = commonData;
[commonArray addObject:commonData];
[dataList addObject:commonArray];
NSMutableArray *memberArray = [NSMutableArray array];
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Members]) {
TUICommonTextCellData *countData = [[TUICommonTextCellData alloc] init];
countData.key = TIMCommonLocalizableString(TUIKitGroupProfileMember);
countData.value = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitGroupProfileMemberCount), self.groupInfo.memberCount];
countData.cselector = @selector(didSelectMembers);
countData.showAccessory = YES;
[memberArray addObject:countData];
NSMutableArray *tmpArray = [self getShowMembers:self.membersData];
TUIGroupMembersCellData *membersData = [[TUIGroupMembersCellData alloc] init];
membersData.members = tmpArray;
[memberArray addObject:membersData];
self.groupMembersCellData = membersData;
[dataList addObject:memberArray];
}
// group info
NSMutableArray *groupInfoArray = [NSMutableArray array];
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Notice]) {
TUIGroupNoticeCellData *notice = [[TUIGroupNoticeCellData alloc] init];
notice.name = TIMCommonLocalizableString(TUIKitGroupNotice);
notice.desc = self.groupInfo.notification ?: TIMCommonLocalizableString(TUIKitGroupNoticeNull);
notice.target = self;
notice.selector = @selector(didSelectNotice);
[groupInfoArray addObject:notice];
}
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Members]) {
NSMutableDictionary *param = [NSMutableDictionary dictionary];
param[@"pushVC"] = self.delegate.pushNavigationController;
param[@"groupID"] = self.groupID.length > 0 ? self.groupID : @"";
NSArray<TUIExtensionInfo *> *extensionList = [TUICore getExtensionList:
TUICore_TUIChatExtension_GroupProfileSettingsItemExtension_ClassicExtensionID
param:param];
for (TUIExtensionInfo *info in extensionList) {
if (info.text && info.onClicked) {
TUICommonTextCellData *manageData = [[TUICommonTextCellData alloc] init];
manageData.key = info.text; //TIMCommonLocalizableString(TUIKitGroupProfileManage);
manageData.value = @"";
manageData.tui_extValueObj = info;
manageData.showAccessory = YES;
manageData.cselector = @selector(groupProfileExtensionButtonClick:);
if (info.weight == 100 ) {
if ([TUIGroupInfoDataProvider isMeOwner:self.groupInfo]) {
[groupInfoArray addObject:manageData];
}
}
else {
[groupInfoArray addObject:manageData];
}
}
}
}
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Manage]) {
TUICommonTextCellData *typeData = [[TUICommonTextCellData alloc] init];
typeData.key = TIMCommonLocalizableString(TUIKitGroupProfileType);
typeData.value = [TUIGroupInfoDataProvider getGroupTypeName:self.groupInfo];
[groupInfoArray addObject:typeData];
TUICommonTextCellData *addOptionData = [[TUICommonTextCellData alloc] init];
addOptionData.key = TIMCommonLocalizableString(TUIKitGroupProfileJoinType);
if ([TUIGroupInfoDataProvider isMeOwner:self.groupInfo]) {
addOptionData.cselector = @selector(didSelectAddOption:);
addOptionData.showAccessory = YES;
}
addOptionData.value = [TUIGroupInfoDataProvider getAddOption:self.groupInfo];
[groupInfoArray addObject:addOptionData];
self.addOptionData = addOptionData;
TUICommonTextCellData *inviteOptionData = [[TUICommonTextCellData alloc] init];
inviteOptionData.key = TIMCommonLocalizableString(TUIKitGroupProfileInviteType);
if ([TUIGroupInfoDataProvider isMeOwner:self.groupInfo]) {
inviteOptionData.cselector = @selector(didSelectAddOption:);
inviteOptionData.showAccessory = YES;
}
inviteOptionData.value = [TUIGroupInfoDataProvider getApproveOption:self.groupInfo];
[groupInfoArray addObject:inviteOptionData];
self.inviteOptionData = inviteOptionData;
[dataList addObject:groupInfoArray];
}
// personal info
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Alias]) {
TUICommonTextCellData *nickData = [[TUICommonTextCellData alloc] init];
nickData.key = TIMCommonLocalizableString(TUIKitGroupProfileAlias);
nickData.value = self.selfInfo.nameCard;
nickData.cselector = @selector(didSelectGroupNick:);
nickData.showAccessory = YES;
self.groupNickNameCellData = nickData;
[dataList addObject:@[ nickData ]];
}
TUICommonSwitchCellData *markFold = [[TUICommonSwitchCellData alloc] init];
TUICommonSwitchCellData *switchData = [[TUICommonSwitchCellData alloc] init];
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_MuteAndPin]) {
NSMutableArray *personalArray = [NSMutableArray array];
TUICommonSwitchCellData *messageSwitchData = [[TUICommonSwitchCellData alloc] init];
if (![self.groupInfo.groupType isEqualToString:GroupType_Meeting]) {
messageSwitchData.on = (self.groupInfo.recvOpt == V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE);
messageSwitchData.title = TIMCommonLocalizableString(TUIKitGroupProfileMessageDoNotDisturb);
messageSwitchData.cswitchSelector = @selector(didSelectOnNotDisturb:);
[personalArray addObject:messageSwitchData];
}
markFold.title = TIMCommonLocalizableString(TUIKitConversationMarkFold);
markFold.displaySeparatorLine = YES;
markFold.cswitchSelector = @selector(didSelectOnFoldConversation:);
if (messageSwitchData.on) {
[personalArray addObject:markFold];
}
switchData.title = TIMCommonLocalizableString(TUIKitGroupProfileStickyOnTop);
[personalArray addObject:switchData];
[dataList addObject:personalArray];
}
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Background]) {
TUICommonTextCellData *changeBackgroundImageItem = [[TUICommonTextCellData alloc] init];
changeBackgroundImageItem.key = TIMCommonLocalizableString(ProfileSetBackgroundImage);
changeBackgroundImageItem.cselector = @selector(didSelectOnChangeBackgroundImage:);
changeBackgroundImageItem.showAccessory = YES;
[dataList addObject:@[ changeBackgroundImageItem ]];
}
NSMutableArray *buttonArray = [NSMutableArray array];
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_ClearChatHistory]) {
TUIButtonCellData *clearHistory = [[TUIButtonCellData alloc] init];
clearHistory.title = TIMCommonLocalizableString(TUIKitClearAllChatHistory);
clearHistory.style = ButtonRedText;
clearHistory.cbuttonSelector = @selector(didClearAllHistory:);
[buttonArray addObject:clearHistory];
}
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_DeleteAndLeave]) {
TUIButtonCellData *quitButton = [[TUIButtonCellData alloc] init];
quitButton.title = TIMCommonLocalizableString(TUIKitGroupProfileDeleteAndExit);
quitButton.style = ButtonRedText;
quitButton.cbuttonSelector = @selector(didDeleteGroup:);
[buttonArray addObject:quitButton];
}
if ([self.class isMeSuper:self.groupInfo] &&
![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Transfer]) {
NSMutableDictionary *param = [NSMutableDictionary dictionary];
param[@"pushVC"] = self.delegate.pushNavigationController;
param[@"groupID"] = self.groupID.length > 0 ? self.groupID : @"";
void (^updateGroupInfoCallback)(void) = ^{
[self updateGroupInfo];
};
param[@"updateGroupInfoCallback"] = updateGroupInfoCallback;
NSArray<TUIExtensionInfo *> *extensionList = [TUICore getExtensionList:
TUICore_TUIChatExtension_GroupProfileBottomItemExtension_ClassicExtensionID
param:param];
for (TUIExtensionInfo *info in extensionList) {
if (info.text && info.onClicked) {
TUIButtonCellData *transferButton = [[TUIButtonCellData alloc] init];
transferButton.title = info.text;
transferButton.style = ButtonRedText;
transferButton.tui_extValueObj = info;
transferButton.cbuttonSelector = @selector(groupProfileExtensionButtonClick:);
[buttonArray addObject:transferButton];
}
}
}
if ([self.groupInfo canDismissGroup] &&
![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Dismiss]) {
TUIButtonCellData *deletebutton = [[TUIButtonCellData alloc] init];
deletebutton.title = TIMCommonLocalizableString(TUIKitGroupProfileDissolve);
deletebutton.style = ButtonRedText;
deletebutton.cbuttonSelector = @selector(didDeleteGroup:);
[buttonArray addObject:deletebutton];
}
if (![TUIGroupConfig.sharedConfig isItemHiddenInGroupConfig:TUIGroupConfigItem_Report]) {
TUIButtonCellData *reportButton = [[TUIButtonCellData alloc] init];
reportButton.title = TIMCommonLocalizableString(TUIKitGroupProfileReport);
reportButton.style = ButtonRedText;
reportButton.cbuttonSelector = @selector(didReportGroup:);
[buttonArray addObject:reportButton];
}
TUIButtonCellData *lastCellData = [buttonArray lastObject];
lastCellData.hideSeparatorLine = YES;
[dataList addObject:buttonArray];
#ifndef SDKPlaceTop
#define SDKPlaceTop
#endif
#ifdef SDKPlaceTop
@weakify(self);
[V2TIMManager.sharedInstance getConversation:[NSString stringWithFormat:@"group_%@", self.groupID]
succ:^(V2TIMConversation *conv) {
@strongify(self);
markFold.on = [self.class isMarkedByFoldType:conv.markList];
switchData.cswitchSelector = @selector(didSelectOnTop:);
switchData.on = conv.isPinned;
if (markFold.on) {
switchData.on = NO;
switchData.disableChecked = YES;
}
self.dataList = dataList;
}
fail:^(int code, NSString *desc) {
NSLog(@"");
}];
#else
if ([[[TUIConversationPin sharedInstance] topConversationList] containsObject:[NSString stringWithFormat:@"group_%@", self.groupID]]) {
switchData.on = YES;
}
#endif
}
}
- (void)groupProfileExtensionButtonClick:(TUICommonTextCell *)cell {
//Implemented through self.delegate, because of TUIButtonCellData cbuttonSelector mechanism
}
- (void)didSelectMembers {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectMembers)]) {
[self.delegate didSelectMembers];
}
}
- (void)didSelectGroupNick:(TUICommonTextCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectGroupNick:)]) {
[self.delegate didSelectGroupNick:cell];
}
}
- (void)didSelectAddOption:(UITableViewCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectAddOption:)]) {
[self.delegate didSelectAddOption:cell];
}
}
- (void)didSelectCommon {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectCommon)]) {
[self.delegate didSelectCommon];
}
}
- (void)didSelectOnNotDisturb:(TUICommonSwitchCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectOnNotDisturb:)]) {
[self.delegate didSelectOnNotDisturb:cell];
}
}
- (void)didSelectOnTop:(TUICommonSwitchCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectOnTop:)]) {
[self.delegate didSelectOnTop:cell];
}
}
- (void)didSelectOnFoldConversation:(TUICommonSwitchCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectOnFoldConversation:)]) {
[self.delegate didSelectOnFoldConversation:cell];
}
}
- (void)didSelectOnChangeBackgroundImage:(TUICommonTextCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectOnChangeBackgroundImage:)]) {
[self.delegate didSelectOnChangeBackgroundImage:cell];
}
}
- (void)didDeleteGroup:(TUIButtonCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didDeleteGroup:)]) {
[self.delegate didDeleteGroup:cell];
}
}
- (void)didClearAllHistory:(TUIButtonCell *)cell {
if (self.delegate && [self.delegate respondsToSelector:@selector(didClearAllHistory:)]) {
[self.delegate didClearAllHistory:cell];
}
}
- (void)didSelectNotice {
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelectGroupNotice)]) {
[self.delegate didSelectGroupNotice];
}
}
- (void)didReportGroup:(TUIButtonCell *)cell {
NSURL *url = [NSURL URLWithString:@"https://cloud.tencent.com/act/event/report-platform"];
[TUITool openLinkWithURL:url];
}
- (NSMutableArray *)getShowMembers:(NSMutableArray *)members {
int maxCount = TGroupMembersCell_Column_Count * TGroupMembersCell_Row_Count;
if ([self.groupInfo canInviteMember]) maxCount--;
if ([self.groupInfo canRemoveMember]) maxCount--;
NSMutableArray *tmpArray = [NSMutableArray array];
for (NSInteger i = 0; i < members.count && i < maxCount; ++i) {
[tmpArray addObject:members[i]];
}
if ([self.groupInfo canInviteMember]) {
TUIGroupMemberCellData *add = [[TUIGroupMemberCellData alloc] init];
add.avatarImage = TUIContactCommonBundleImage(@"add");
add.tag = 1;
[tmpArray addObject:add];
}
if ([self.groupInfo canRemoveMember]) {
TUIGroupMemberCellData *delete = [[TUIGroupMemberCellData alloc] init];
delete.avatarImage = TUIContactCommonBundleImage(@"delete");
delete.tag = 2;
[tmpArray addObject:delete];
}
return tmpArray;
}
- (void)setGroupAddOpt:(V2TIMGroupAddOpt)opt {
@weakify(self);
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupID;
info.groupAddOpt = opt;
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
@strongify(self);
self.groupInfo.groupAddOpt = opt;
self.addOptionData.value = [TUIGroupInfoDataProvider getAddOptionWithV2AddOpt:opt];
}
fail:^(int code, NSString *desc) {
[self makeToastError:code msg:desc];
}];
}
- (void)setGroupApproveOpt:(V2TIMGroupAddOpt)opt {
@weakify(self);
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupID;
info.groupApproveOpt = opt;
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
@strongify(self);
self.groupInfo.groupApproveOpt = opt;
self.inviteOptionData.value = [TUIGroupInfoDataProvider getApproveOption:self.groupInfo];
}
fail:^(int code, NSString *desc) {
[self makeToastError:code msg:desc];
}];
}
- (void)setGroupReceiveMessageOpt:(V2TIMReceiveMessageOpt)opt Succ:(V2TIMSucc)succ fail:(V2TIMFail)fail {
[[V2TIMManager sharedInstance] setGroupReceiveMessageOpt:self.groupID opt:opt succ:succ fail:fail];
}
- (void)setGroupName:(NSString *)groupName {
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupID;
info.groupName = groupName;
@weakify(self);
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
@strongify(self);
self.profileCellData.name = groupName;
}
fail:^(int code, NSString *msg) {
[self makeToastError:code msg:msg];
}];
}
- (void)setGroupNotification:(NSString *)notification {
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
info.groupID = self.groupID;
info.notification = notification;
@weakify(self);
[[V2TIMManager sharedInstance] setGroupInfo:info
succ:^{
@strongify(self);
self.profileCellData.signature = notification;
}
fail:^(int code, NSString *msg) {
[self makeToastError:code msg:msg];
}];
}
- (void)setGroupMemberNameCard:(NSString *)nameCard {
NSString *userID = [V2TIMManager sharedInstance].getLoginUser;
V2TIMGroupMemberFullInfo *info = [[V2TIMGroupMemberFullInfo alloc] init];
info.userID = userID;
info.nameCard = nameCard;
@weakify(self);
[[V2TIMManager sharedInstance] setGroupMemberInfo:self.groupID
info:info
succ:^{
@strongify(self);
self.groupNickNameCellData.value = nameCard;
self.selfInfo.nameCard = nameCard;
}
fail:^(int code, NSString *msg) {
[self makeToastError:code msg:msg];
}];
}
- (void)dismissGroup:(V2TIMSucc)succ fail:(V2TIMFail)fail {
[[V2TIMManager sharedInstance] dismissGroup:self.groupID succ:succ fail:fail];
}
- (void)quitGroup:(V2TIMSucc)succ fail:(V2TIMFail)fail {
[[V2TIMManager sharedInstance] quitGroup:self.groupID succ:succ fail:fail];
}
- (void)clearAllHistory:(V2TIMSucc)succ fail:(V2TIMFail)fail {
[V2TIMManager.sharedInstance clearGroupHistoryMessage:self.groupID succ:succ fail:fail];
}
+ (NSString *)getGroupTypeName:(V2TIMGroupInfo *)groupInfo {
if (groupInfo.groupType) {
if ([groupInfo.groupType isEqualToString:@"Work"]) {
return TIMCommonLocalizableString(TUIKitWorkGroup);
} else if ([groupInfo.groupType isEqualToString:@"Public"]) {
return TIMCommonLocalizableString(TUIKitPublicGroup);
} else if ([groupInfo.groupType isEqualToString:@"Meeting"]) {
return TIMCommonLocalizableString(TUIKitChatRoom);
} else if ([groupInfo.groupType isEqualToString:@"Community"]) {
return TIMCommonLocalizableString(TUIKitCommunity);
}
}
return @"";
}
+ (NSString *)getAddOption:(V2TIMGroupInfo *)groupInfo {
switch (groupInfo.groupAddOpt) {
case V2TIM_GROUP_ADD_FORBID:
return TIMCommonLocalizableString(TUIKitGroupProfileJoinDisable);
break;
case V2TIM_GROUP_ADD_AUTH:
return TIMCommonLocalizableString(TUIKitGroupProfileAdminApprove);
break;
case V2TIM_GROUP_ADD_ANY:
return TIMCommonLocalizableString(TUIKitGroupProfileAutoApproval);
break;
default:
break;
}
return @"";
}
+ (NSString *)getAddOptionWithV2AddOpt:(V2TIMGroupAddOpt)opt {
switch (opt) {
case V2TIM_GROUP_ADD_FORBID:
return TIMCommonLocalizableString(TUIKitGroupProfileJoinDisable);
break;
case V2TIM_GROUP_ADD_AUTH:
return TIMCommonLocalizableString(TUIKitGroupProfileAdminApprove);
break;
case V2TIM_GROUP_ADD_ANY:
return TIMCommonLocalizableString(TUIKitGroupProfileAutoApproval);
break;
default:
break;
}
return @"";
}
+ (NSString *)getApproveOption:(V2TIMGroupInfo *)groupInfo {
switch (groupInfo.groupApproveOpt) {
case V2TIM_GROUP_ADD_FORBID:
return TIMCommonLocalizableString(TUIKitGroupProfileInviteDisable);
break;
case V2TIM_GROUP_ADD_AUTH:
return TIMCommonLocalizableString(TUIKitGroupProfileAdminApprove);
break;
case V2TIM_GROUP_ADD_ANY:
return TIMCommonLocalizableString(TUIKitGroupProfileAutoApproval);
break;
default:
break;
}
return @"";
}
+ (BOOL)isMarkedByFoldType:(NSArray *)markList {
for (NSNumber *num in markList) {
if (num.unsignedLongValue == V2TIM_CONVERSATION_MARK_TYPE_FOLD) {
return YES;
}
}
return NO;
}
+ (BOOL)isMarkedByHideType:(NSArray *)markList {
for (NSNumber *num in markList) {
if (num.unsignedLongValue == V2TIM_CONVERSATION_MARK_TYPE_HIDE) {
return YES;
}
}
return NO;
}
+ (BOOL)isMeOwner:(V2TIMGroupInfo *)groupInfo {
return [groupInfo.owner isEqualToString:[[V2TIMManager sharedInstance] getLoginUser]] || (groupInfo.role == V2TIM_GROUP_MEMBER_ROLE_ADMIN);
}
+ (BOOL)isMeSuper:(V2TIMGroupInfo *)groupInfo {
return [groupInfo.owner isEqualToString:[[V2TIMManager sharedInstance] getLoginUser]] && (groupInfo.role == V2TIM_GROUP_MEMBER_ROLE_SUPER);
}
@end

View File

@@ -0,0 +1,19 @@
//
// TUIGroupNoticeController.h
// TUIGroup
//
// Created by harvy on 2022/1/11.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIGroupNoticeController : UIViewController
@property(nonatomic, copy) dispatch_block_t onNoticeChanged;
@property(nonatomic, copy) NSString *groupID;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,114 @@
//
// TUIGroupNoticeController.m
// TUIGroup
//
// Created by harvy on 2022/1/11.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIGroupNoticeController.h"
#import <TUICore/TUIThemeManager.h>
#import "TUIGroupNoticeDataProvider.h"
@interface TUIGroupNoticeController ()
@property(nonatomic, strong) UITextView *textView;
@property(nonatomic, weak) UIButton *rightButton;
@property(nonatomic, strong) TUIGroupNoticeDataProvider *dataProvider;
@end
@implementation TUIGroupNoticeController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
[self.view addSubview:self.textView];
UIButton *rightBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[rightBtn setTitleColor:TIMCommonDynamicColor(@"nav_title_text_color", @"#000000") forState:UIControlStateNormal];
[rightBtn setTitleColor:TIMCommonDynamicColor(@"nav_title_text_color", @"#000000") forState:UIControlStateSelected];
[rightBtn setTitle:TIMCommonLocalizableString(Edit) forState:UIControlStateNormal];
[rightBtn setTitle:TIMCommonLocalizableString(Done) forState:UIControlStateSelected];
[rightBtn sizeToFit];
[rightBtn addTarget:self action:@selector(onClickRight:) forControlEvents:UIControlEventTouchUpInside];
self.rightButton = rightBtn;
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:rightBtn];
self.rightButton.hidden = YES;
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = TIMCommonLocalizableString(TUIKitGroupNotice);
titleLabel.font = [UIFont boldSystemFontOfSize:17.0];
titleLabel.textColor = TIMCommonDynamicColor(@"nav_title_text_color", @"#000000");
[titleLabel sizeToFit];
self.navigationItem.titleView = titleLabel;
__weak typeof(self) weakSelf = self;
self.dataProvider.groupID = self.groupID;
[self.dataProvider getGroupInfo:^{
weakSelf.textView.text = weakSelf.dataProvider.groupInfo.notification;
weakSelf.textView.editable = NO;
weakSelf.rightButton.hidden = !weakSelf.dataProvider.canEditNotice;
}];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
self.textView.frame = self.view.bounds;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.dataProvider.canEditNotice && self.textView.text.length == 0) {
[self onClickRight:self.rightButton];
}
}
- (void)onClickRight:(UIButton *)button {
if (button.isSelected) {
self.textView.editable = NO;
[self.textView resignFirstResponder];
[self updateNotice];
} else {
self.textView.editable = YES;
[self.textView becomeFirstResponder];
}
button.selected = !button.isSelected;
}
- (void)updateNotice {
__weak typeof(self) weakSelf = self;
[self.dataProvider updateNotice:self.textView.text
callback:^(int code, NSString *desc) {
if (code != 0) {
[TUITool makeToastError:code msg:desc];
return;
}
if (weakSelf.onNoticeChanged) {
weakSelf.onNoticeChanged();
}
}];
}
- (UITextView *)textView {
if (_textView == nil) {
_textView = [[UITextView alloc] init];
_textView.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
_textView.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
_textView.textColor = TIMCommonDynamicColor(@"form_title_color", @"#000000");
_textView.font = [UIFont systemFontOfSize:17];
}
return _textView;
}
- (TUIGroupNoticeDataProvider *)dataProvider {
if (_dataProvider == nil) {
_dataProvider = [[TUIGroupNoticeDataProvider alloc] init];
}
return _dataProvider;
}
@end

View File

@@ -0,0 +1,29 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This file declares the logic used to realize the sliding display of pictures and videos
*/
#import <UIKit/UIKit.h>
@import ImSDK_Plus;
@interface TUIMediaView : UIView
@property(nonatomic, copy) dispatch_block_t onClose;
/**
* Setting thumb, for animating
*/
- (void)setThumb:(UIImageView *)thumb frame:(CGRect)frame;
/**
* Setting the current message that needs to be displayed. MediaView will automatically load the before and after messages of the current message for display,
* mainly used in the scene of the normal message list
*/
- (void)setCurMessage:(V2TIMMessage *)curMessage;
/**
* Setting the current and all messages to be displayed, mainly used in the scenario of merge-forward message lists
*/
- (void)setCurMessage:(V2TIMMessage *)curMessage allMessages:(NSArray *)messages;
@end

View File

@@ -0,0 +1,279 @@
//
// MenuView.m
// UIKit
//
// Created by kennethmiao on 2018/9/18.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIMediaView.h"
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUIMessageCell.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import "TUIChatConversationModel.h"
#import "TUIImageCollectionCell.h"
#import "TUIMessageMediaDataProvider.h"
#import "TUIVideoCollectionCell.h"
#define ANIMATION_TIME 0.2
@interface TUIMediaView () <UICollectionViewDelegate,
UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout,
UIScrollViewDelegate,
TUIMediaCollectionCellDelegate>
@property(strong, nonatomic) TUIMessageMediaDataProvider *dataProvider;
@property(strong, nonatomic) UICollectionView *menuCollectionView;
@property(strong, nonatomic) UIImage *saveBackgroundImage;
@property(strong, nonatomic) UIImage *saveShadowImage;
@property(strong, nonatomic) UIImageView *imageView;
@property(strong, nonatomic) UIImage *thumbImage;
@property(strong, nonatomic) UIView *coverView;
@property(strong, nonatomic) UIView *mediaView;
@property(assign, nonatomic) CGRect thumbFrame;
@property(assign, nonatomic) NSIndexPath *currentVisibleIndexPath;
@end
@implementation TUIMediaView {
V2TIMMessage *_curMessage;
}
- (instancetype)init {
self = [super init];
if (self) {
_currentVisibleIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
}
return self;
}
- (void)setupViews {
self.backgroundColor = [UIColor clearColor];
self.coverView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width * 3, Screen_Height * 3)];
self.coverView.backgroundColor = [UIColor blackColor];
[self addSubview:self.coverView];
self.mediaView = [[UIView alloc] initWithFrame:self.thumbFrame];
self.mediaView.backgroundColor = [UIColor clearColor];
[self addSubview:self.mediaView];
TUICollectionRTLFitFlowLayout *menuFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
menuFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
menuFlowLayout.minimumLineSpacing = 0;
menuFlowLayout.minimumInteritemSpacing = 0;
menuFlowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
self.menuCollectionView = [[UICollectionView alloc] initWithFrame:self.mediaView.bounds collectionViewLayout:menuFlowLayout];
self.menuCollectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.menuCollectionView registerClass:[TUIImageCollectionCell class] forCellWithReuseIdentifier:TImageMessageCell_ReuseId];
[self.menuCollectionView registerClass:[TUIVideoCollectionCell class] forCellWithReuseIdentifier:TVideoMessageCell_ReuseId];
self.menuCollectionView.pagingEnabled = YES;
self.menuCollectionView.delegate = self;
self.menuCollectionView.dataSource = self;
self.menuCollectionView.showsHorizontalScrollIndicator = NO;
self.menuCollectionView.showsVerticalScrollIndicator = NO;
self.menuCollectionView.alwaysBounceHorizontal = YES;
self.menuCollectionView.decelerationRate = UIScrollViewDecelerationRateFast;
self.menuCollectionView.backgroundColor = [UIColor clearColor];
self.menuCollectionView.hidden = YES;
[self.mediaView addSubview:self.menuCollectionView];
self.imageView = [[UIImageView alloc] initWithFrame:self.mediaView.bounds];
self.imageView.layer.cornerRadius = 5.0;
[self.imageView.layer setMasksToBounds:YES];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageView.backgroundColor = [UIColor clearColor];
self.imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.imageView.image = self.thumbImage;
[self.mediaView addSubview:self.imageView];
[UIView animateWithDuration:ANIMATION_TIME
animations:^{
self.mediaView.frame = self.bounds;
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ANIMATION_TIME * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.imageView removeFromSuperview];
self.menuCollectionView.hidden = NO;
});
[[NSNotificationCenter defaultCenter] postNotificationName:kEnableAllRotationOrientationNotification object:nil];
[self setupRotaionNotifications];
}
- (void)setupRotaionNotifications {
if (@available(iOS 16.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:TUIMessageMediaViewDeviceOrientationChangeNotification
object:nil];
} else {
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
}
- (void)setThumb:(UIImageView *)thumb frame:(CGRect)frame {
self.thumbImage = thumb.image;
self.thumbFrame = frame;
[self setupViews];
}
- (void)setCurMessage:(V2TIMMessage *)curMessage {
_curMessage = curMessage;
TUIChatConversationModel *model = [[TUIChatConversationModel alloc] init];
model.userID = _curMessage.userID;
model.groupID = _curMessage.groupID;
self.dataProvider = [[TUIMessageMediaDataProvider alloc] initWithConversationModel:model];
[self.dataProvider loadMediaWithMessage:_curMessage];
@weakify(self);
[RACObserve(self.dataProvider, medias) subscribeNext:^(NSArray *x) {
@strongify(self);
[self.menuCollectionView reloadData];
for (int i = 0; i < self.dataProvider.medias.count; i++) {
TUIMessageCellData *data = self.dataProvider.medias[i];
if ([data.innerMessage.msgID isEqualToString:self->_curMessage.msgID]) {
self.currentVisibleIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.menuCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]
atScrollPosition:(UICollectionViewScrollPositionLeft)animated:NO];
return;
}
}
}];
}
- (void)setCurMessage:(V2TIMMessage *)curMessage allMessages:(NSArray<V2TIMMessage *> *)allMessages {
_curMessage = curMessage;
NSMutableArray *medias = [NSMutableArray array];
for (V2TIMMessage *message in allMessages) {
TUIMessageCellData *data = [TUIMessageMediaDataProvider getMediaCellData:message];
if (data) {
[medias addObject:data];
}
}
self.dataProvider = [[TUIMessageMediaDataProvider alloc] initWithConversationModel:nil];
self.dataProvider.medias = medias;
[self.menuCollectionView reloadData];
for (int i = 0; i < self.dataProvider.medias.count; i++) {
TUIMessageCellData *data = self.dataProvider.medias[i];
if ([data.innerMessage.msgID isEqualToString:_curMessage.msgID]) {
[self.menuCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]
atScrollPosition:(UICollectionViewScrollPositionLeft)animated:NO];
return;
}
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.dataProvider.medias.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TUIMessageCellData *data = self.dataProvider.medias[indexPath.row];
TUIMediaCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:data.reuseId forIndexPath:indexPath];
if (cell) {
cell.delegate = self;
[cell fillWithData:data];
if ([cell isKindOfClass:[TUIVideoCollectionCell class]]) {
TUIVideoCollectionCell *videoCell = (TUIVideoCollectionCell *)cell;
[videoCell reloadAllView];
}
else if ([cell isKindOfClass:[TUIImageCollectionCell class]]) {
TUIImageCollectionCell *imageCell = (TUIImageCollectionCell *)cell;
[imageCell reloadAllView];
}
else {
//empty
}
}
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return collectionView.frame.size;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
[self.menuCollectionView.collectionViewLayout invalidateLayout];
}
- (CGPoint)collectionView:(UICollectionView *)collectionView targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
UICollectionViewLayoutAttributes *attrs = [collectionView layoutAttributesForItemAtIndexPath:self.currentVisibleIndexPath];
CGPoint newOriginForOldIndex = attrs.frame.origin;
return newOriginForOldIndex.x == 0 ? proposedContentOffset : newOriginForOldIndex;
}
#pragma mark TUIMediaCollectionCellDelegate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGPoint center = CGPointMake(scrollView.contentOffset.x + (scrollView.frame.size.width / 2), scrollView.frame.size.height / 2);
NSIndexPath *ip = [self.menuCollectionView indexPathForItemAtPoint:center];
if (ip) {
self.currentVisibleIndexPath = ip;
}
NSArray *indexPaths = [self.menuCollectionView indexPathsForVisibleItems];
NSIndexPath *indexPath = indexPaths.firstObject;
TUIMessageCellData *data = self.dataProvider.medias[indexPath.row];
_curMessage = data.innerMessage;
if (indexPath.row <= self.dataProvider.pageCount / 2) {
[self.dataProvider loadOlderMedia];
}
if (indexPath.row >= self.dataProvider.medias.count - self.dataProvider.pageCount / 2) {
[self.dataProvider loadNewerMedia];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
NSArray *indexPaths = [self.menuCollectionView indexPathsForVisibleItems];
NSIndexPath *indexPath = indexPaths.firstObject;
UICollectionViewCell *cell = [self.menuCollectionView cellForItemAtIndexPath:indexPath];
if ([cell isKindOfClass:[TUIVideoCollectionCell class]]) {
TUIVideoCollectionCell *videoCell = (TUIVideoCollectionCell *)cell;
[videoCell stopVideoPlayAndSave];
}
}
#pragma mark TUIMediaCollectionCellDelegate
- (void)onCloseMedia:(TUIMediaCollectionCell *)cell {
if (self.onClose) {
self.onClose();
}
if (self.superview) {
[self removeFromSuperview];
}
[[NSNotificationCenter defaultCenter] postNotificationName:kDisableAllRotationOrientationNotification object:nil];
}
- (void)applyRotaionFrame {
self.frame = CGRectMake(0, 0, Screen_Width, Screen_Height);
self.coverView.frame = CGRectMake(0, 0, Screen_Width * 3, Screen_Height * 3);
self.mediaView.frame = self.frame;
self.mediaView.center = CGPointMake(self.frame.size.width / 2.0, self.frame.size.height / 2.0);
self.menuCollectionView.frame = self.mediaView.frame;
self.menuCollectionView.center = CGPointMake(self.frame.size.width / 2.0, self.frame.size.height / 2.0);
[self.menuCollectionView setNeedsLayout];
self.imageView.frame = self.mediaView.frame;
self.imageView.center = CGPointMake(self.frame.size.width / 2.0, self.frame.size.height / 2.0);
[self.menuCollectionView
performBatchUpdates:^{
}
completion:^(BOOL finished) {
if (finished) {
[self.menuCollectionView scrollToItemAtIndexPath:self.currentVisibleIndexPath atScrollPosition:(UICollectionViewScrollPositionLeft)animated:NO];
}
}];
return;
}
- (void)onDeviceOrientationChange:(NSNotification *)noti {
[UIView performWithoutAnimation:^{
[self applyRotaionFrame];
}];
}
@end

View File

@@ -0,0 +1,27 @@
//
// TUIMergeMessageListController.h
// Pods
//
// Created by harvy on 2020/12/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <UIKit/UIKit.h>
#import "TUIBaseMessageControllerDelegate.h"
#import "TUIChatConversationModel.h"
#import "TUIMessageDataProvider.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMergeMessageListController : UITableViewController
@property(nonatomic, weak) id<TUIBaseMessageControllerDelegate> delegate;
@property(nonatomic, strong) V2TIMMergerElem *mergerElem;
@property(nonatomic, copy) dispatch_block_t willCloseCallback;
@property(nonatomic, strong) TUIChatConversationModel *conversationData;
@property(nonatomic, strong) TUIMessageDataProvider *parentPageDataProvider;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,599 @@
//
// TUIMergeMessageListController.m
// Pods
//
// Created by harvy on 2020/12/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMergeMessageListController.h"
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUISystemMessageCell.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIFaceMessageCell.h"
#import "TUIFileMessageCell.h"
#import "TUIFileViewController.h"
#import "TUIImageMessageCell.h"
#import "TUIJoinGroupMessageCell.h"
#import "TUILinkCell.h"
#import "TUIMediaView.h"
#import "TUIMergeMessageCell.h"
#import "TUIMessageDataProvider.h"
#import "TUIMessageSearchDataProvider.h"
#import "TUIReferenceMessageCell.h"
#import "TUIRepliesDetailViewController.h"
#import "TUIReplyMessageCell.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCell.h"
#import "TUIVideoMessageCell.h"
#import "TUIVoiceMessageCell.h"
#import "TUIMessageCellConfig.h"
#import "TUIChatConfig.h"
#define STR(x) @ #x
@interface TUIMergeMessageListController () <TUIMessageCellDelegate, TUIMessageBaseDataProviderDataSource,TUINotificationProtocol>
@property(nonatomic, strong) NSArray<V2TIMMessage *> *imMsgs;
@property(nonatomic, strong) NSMutableArray<TUIMessageCellData *> *uiMsgs;
@property(nonatomic, strong) NSMutableDictionary *stylesCache;
@property(nonatomic, strong) TUIMessageSearchDataProvider *msgDataProvider;
@property(nonatomic, strong) TUIMessageCellConfig *messageCellConfig;
@end
@implementation TUIMergeMessageListController
- (instancetype)init {
self = [super init];
if (self) {
[TUICore registerEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:self];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
_uiMsgs = [[NSMutableArray alloc] init];
[self loadMessages];
[self setupViews];
}
- (void)viewWillDisappear:(BOOL)animated {
[self updateCellStyle:YES];
if (self.willCloseCallback) {
self.willCloseCallback();
}
}
- (void)updateCellStyle:(BOOL)recover {
if (recover) {
TUIMessageCellLayout.incommingMessageLayout.avatarInsets = UIEdgeInsetsFromString(self.stylesCache[STR(incomingAvatarInsets)]);
TUIMessageCellLayout.incommingTextMessageLayout.avatarInsets = UIEdgeInsetsFromString(self.stylesCache[STR(incomingAvatarInsets)]);
TUIMessageCellLayout.incommingVoiceMessageLayout.avatarInsets = UIEdgeInsetsFromString(self.stylesCache[STR(incomingAvatarInsets)]);
TUIMessageCellLayout.incommingMessageLayout.messageInsets = UIEdgeInsetsFromString(self.stylesCache[STR(incommingMessageInsets)]);
[TUITextMessageCell setOutgoingTextColor:self.stylesCache[STR(outgoingTextColor)]];
[TUITextMessageCell setIncommingTextColor:self.stylesCache[STR(incomingTextColor)]];
return;
}
UIEdgeInsets incomingAvatarInsets = TUIMessageCellLayout.incommingTextMessageLayout.avatarInsets;
TUIMessageCellLayout.incommingMessageLayout.avatarInsets = UIEdgeInsetsMake(0, 10, 0, 10);
TUIMessageCellLayout.incommingTextMessageLayout.avatarInsets = UIEdgeInsetsMake(0, 10, 0, 10);
TUIMessageCellLayout.incommingVoiceMessageLayout.avatarInsets = UIEdgeInsetsMake(0, 10, 0, 10);
[self.stylesCache setObject:NSStringFromUIEdgeInsets(incomingAvatarInsets) forKey:STR(incomingAvatarInsets)];
UIEdgeInsets incommingMessageInsets = TUIMessageCellLayout.incommingMessageLayout.messageInsets;
TUIMessageCellLayout.incommingMessageLayout.messageInsets = UIEdgeInsetsMake(5, 5, 0, 0);
[self.stylesCache setObject:NSStringFromUIEdgeInsets(incommingMessageInsets) forKey:STR(incommingMessageInsets)];
UIColor *outgoingTextColor = [TUITextMessageCell outgoingTextColor];
[TUITextMessageCell setOutgoingTextColor:TUIChatDynamicColor(@"chat_text_message_send_text_color", @"#000000")];
[self.stylesCache setObject:outgoingTextColor forKey:STR(outgoingTextColor)];
UIColor *incomingTextColor = [TUITextMessageCell incommingTextColor];
[TUITextMessageCell setIncommingTextColor:TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000")];
[self.stylesCache setObject:incomingTextColor forKey:STR(incomingTextColor)];
}
- (void)loadMessages {
@weakify(self);
[self.mergerElem
downloadMergerMessage:^(NSArray<V2TIMMessage *> *msgs) {
@strongify(self);
self.imMsgs = msgs;
[self updateCellStyle:NO];
[self getMessages:self.imMsgs];
}
fail:^(int code, NSString *desc) {
[self updateCellStyle:NO];
}];
}
- (void)getMessages:(NSArray *)msgs {
NSMutableArray *uiMsgs = [self transUIMsgFromIMMsg:msgs];
@weakify(self);
[self.msgDataProvider preProcessMessage:uiMsgs
callback:^{
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (uiMsgs.count != 0) {
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, uiMsgs.count)];
[self.uiMsgs insertObjects:uiMsgs atIndexes:indexSet];
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
}
});
}];
}
- (NSMutableArray *)transUIMsgFromIMMsg:(NSArray *)msgs {
NSMutableArray *uiMsgs = [NSMutableArray array];
for (NSInteger k = 0; k < msgs.count; k++) {
V2TIMMessage *msg = msgs[k];
if ([self.delegate respondsToSelector:@selector(messageController:onNewMessage:)]) {
TUIMessageCellData *data = [self.delegate messageController:nil onNewMessage:msg];
if (data) {
TUIMessageCellLayout *layout = TUIMessageCellLayout.incommingMessageLayout;
if ([data isKindOfClass:TUITextMessageCellData.class] || [data isKindOfClass:TUIReferenceMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
} else if ([data isKindOfClass:TUIVoiceMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingVoiceMessageLayout;
}
data.cellLayout = layout;
data.direction = MsgDirectionIncoming;
data.innerMessage = msg;
data.showName = YES;
[uiMsgs addObject:data];
continue;
}
}
TUIMessageCellData *data = [TUIMessageDataProvider getCellData:msg];
if (!data) {
continue;
}
TUIMessageCellLayout *layout = TUIMessageCellLayout.incommingMessageLayout;
if ([data isKindOfClass:TUITextMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
} else if ([data isKindOfClass:TUIReplyMessageCellData.class] || [data isKindOfClass:TUIReferenceMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
TUIReferenceMessageCellData *textData = (TUIReferenceMessageCellData *)data;
textData.textColor = TUIChatDynamicColor(@"chat_text_message_receive_text_color", @"#000000");
textData.showRevokedOriginMessage = YES;
} else if ([data isKindOfClass:TUIVoiceMessageCellData.class]) {
TUIVoiceMessageCellData *voiceData = (TUIVoiceMessageCellData *)data;
voiceData.cellLayout = [TUIMessageCellLayout incommingVoiceMessageLayout];
voiceData.voiceImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_receiver_normal")];
voiceData.voiceAnimationImages =
[NSArray arrayWithObjects:[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_receiver_playing_1")],
[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_receiver_playing_2")],
[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_receiver_playing_3")], nil];
voiceData.voiceTop = 10;
msg.localCustomInt = 1;
layout = TUIMessageCellLayout.incommingVoiceMessageLayout;
}
data.cellLayout = layout;
data.direction = MsgDirectionIncoming;
data.innerMessage = msg;
data.showName = YES;
[uiMsgs addObject:data];
}
return uiMsgs;
}
- (void)setupViews {
self.title = TIMCommonLocalizableString(TUIKitRelayChatHistory);
self.tableView.scrollsToTop = NO;
self.tableView.estimatedRowHeight = 0;
[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
self.tableView.backgroundColor = TUIChatDynamicColor(@"chat_controller_bg_color", @"#FFFFFF");
self.tableView.contentInset = UIEdgeInsetsMake(5, 0, 0, 0);
[self.messageCellConfig bindTableView:self.tableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _uiMsgs.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
static CGFloat screenWidth = 0;
if (screenWidth == 0) {
screenWidth = Screen_Width;
}
if (indexPath.row < _uiMsgs.count) {
TUIMessageCellData *cellData = _uiMsgs[indexPath.row];
CGFloat height = [self.messageCellConfig getHeightFromMessageCellData:cellData];
return height;
} else {
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TUIMessageCellData *data = _uiMsgs[indexPath.row];
data.showMessageTime = YES;
data.showCheckBox = NO;
TUIMessageCell *cell = nil;
if ([self.delegate respondsToSelector:@selector(messageController:onShowMessageData:)]) {
cell = [self.delegate messageController:nil onShowMessageData:data];
if (cell) {
cell.delegate = self;
return cell;
}
}
cell = [tableView dequeueReusableCellWithIdentifier:data.reuseId forIndexPath:indexPath];
cell.delegate = self;
[cell fillWithData:_uiMsgs[indexPath.row]];
return cell;
}
#pragma mark - TUIMessageCellDelegate
- (void)onSelectMessage:(TUIMessageCell *)cell {
if (TUIChatConfig.defaultConfig.eventConfig.chatEventListener &&
[TUIChatConfig.defaultConfig.eventConfig.chatEventListener respondsToSelector:@selector(onMessageClicked:messageCellData:)]) {
BOOL result = [TUIChatConfig.defaultConfig.eventConfig.chatEventListener onMessageClicked:cell messageCellData:cell.messageData];
if (result) {
return;
}
}
if ([cell isKindOfClass:[TUIImageMessageCell class]]) {
[self showImageMessage:(TUIImageMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIVoiceMessageCell class]]) {
[self playVoiceMessage:(TUIVoiceMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIVideoMessageCell class]]) {
[self showVideoMessage:(TUIVideoMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIFileMessageCell class]]) {
[self showFileMessage:(TUIFileMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIMergeMessageCell class]]) {
TUIMergeMessageListController *mergeVc = [[TUIMergeMessageListController alloc] init];
mergeVc.mergerElem = [(TUIMergeMessageCell *)cell mergeData].mergerElem;
mergeVc.delegate = self.delegate;
[self.navigationController pushViewController:mergeVc animated:YES];
}
if ([cell isKindOfClass:[TUILinkCell class]]) {
[self showLinkMessage:(TUILinkCell *)cell];
}
if ([cell isKindOfClass:[TUIReplyMessageCell class]]) {
[self showReplyMessage:(TUIReplyMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIReferenceMessageCell class]]) {
[self showReplyMessage:(TUIReplyMessageCell *)cell];
}
if ([self.delegate respondsToSelector:@selector(messageController:onSelectMessageContent:)]) {
[self.delegate messageController:nil onSelectMessageContent:cell];
}
}
- (BOOL)checkIfMessageExistsInLocal:(V2TIMMessage *)locateMessage {
NSInteger index = 0;
for (TUIMessageCellData *uiMsg in self.uiMsgs) {
if ([uiMsg.innerMessage.msgID isEqualToString:locateMessage.msgID]) {
return YES;
}
index++;
}
if (index == self.uiMsgs.count) {
return NO;
}
return NO;
}
- (void)scrollToLocateMessage:(V2TIMMessage *)locateMessage matchKeyword:(NSString *)msgAbstract {
CGFloat offsetY = 0;
NSInteger index = 0;
for (TUIMessageCellData *uiMsg in self.uiMsgs) {
if ([uiMsg.innerMessage.msgID isEqualToString:locateMessage.msgID]) {
break;
}
offsetY += [uiMsg heightOfWidth:Screen_Width];
index++;
}
if (index == self.uiMsgs.count) {
return;
}
offsetY -= self.tableView.frame.size.height / 2.0;
if (offsetY <= TMessageController_Header_Height) {
offsetY = TMessageController_Header_Height + 0.1;
}
if (offsetY > TMessageController_Header_Height) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]
atScrollPosition:UITableViewScrollPositionMiddle
animated:YES];
}
[self highlightKeyword:msgAbstract locateMessage:locateMessage];
}
- (void)highlightKeyword:(NSString *)keyword locateMessage:(V2TIMMessage *)locateMessage {
TUIMessageCellData *cellData = nil;
for (TUIMessageCellData *tmp in self.uiMsgs) {
if ([tmp.msgID isEqualToString:locateMessage.msgID]) {
cellData = tmp;
break;
}
}
if (cellData == nil) {
return;
}
CGFloat time = 0.5;
UITableViewRowAnimation animation = UITableViewRowAnimationFade;
if ([cellData isKindOfClass:TUITextMessageCellData.class]) {
time = 2;
animation = UITableViewRowAnimationNone;
}
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.uiMsgs indexOfObject:cellData] inSection:0];
cellData.highlightKeyword = keyword;
TUIMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell fillWithData:cellData];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.uiMsgs indexOfObject:cellData] inSection:0];
cellData.highlightKeyword = nil;
TUIMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell fillWithData:cellData];
});
}
- (void)showReplyMessage:(TUIReplyMessageCell *)cell {
[UIApplication.sharedApplication.keyWindow endEditing:YES];
NSString *originMsgID = @"";
NSString *msgAbstract = @"";
if ([cell isKindOfClass:TUIReplyMessageCell.class]) {
TUIReplyMessageCell *acell = (TUIReplyMessageCell *)cell;
TUIReplyMessageCellData *cellData = acell.replyData;
originMsgID = cellData.messageRootID;
msgAbstract = cellData.msgAbstract;
} else if ([cell isKindOfClass:TUIReferenceMessageCell.class]) {
TUIReferenceMessageCell *acell = (TUIReferenceMessageCell *)cell;
TUIReferenceMessageCellData *cellData = acell.referenceData;
originMsgID = cellData.originMsgID;
msgAbstract = cellData.msgAbstract;
}
TUIMessageCellData *originMemoryMessageData = nil;
for (TUIMessageCellData *uiMsg in self.uiMsgs) {
if ([uiMsg.innerMessage.msgID isEqualToString:originMsgID]) {
originMemoryMessageData = uiMsg;
break;
}
}
if (originMemoryMessageData && [cell isKindOfClass:TUIReplyMessageCell.class]) {
[self onJumpToRepliesDetailPage:originMemoryMessageData];
}
else {
[(TUIMessageSearchDataProvider *)self.msgDataProvider
findMessages:@[ originMsgID ?: @"" ]
callback:^(BOOL success, NSString *_Nonnull desc, NSArray<V2TIMMessage *> *_Nonnull msgs) {
if (!success) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
V2TIMMessage *message = msgs.firstObject;
if (message == nil) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
if (message.status == V2TIM_MSG_STATUS_HAS_DELETED || message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
if ([cell isKindOfClass:TUIReplyMessageCell.class]) {
[self jumpDetailPageByMessage:message];
} else if ([cell isKindOfClass:TUIReferenceMessageCell.class]) {
BOOL existInLocal = [self checkIfMessageExistsInLocal:message];
if (!existInLocal) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
[self scrollToLocateMessage:message matchKeyword:msgAbstract];
}
}];
}
}
- (void)jumpDetailPageByMessage:(V2TIMMessage *)message {
NSMutableArray *uiMsgs = [self transUIMsgFromIMMsg:@[ message ]];
[self.msgDataProvider preProcessMessage:uiMsgs
callback:^{
for (TUIMessageCellData *cellData in uiMsgs) {
if ([cellData.innerMessage.msgID isEqual:message.msgID]) {
[self onJumpToRepliesDetailPage:cellData];
return;
}
}
}];
}
- (void)onJumpToRepliesDetailPage:(TUIMessageCellData *)data {
TUIRepliesDetailViewController *repliesDetailVC = [[TUIRepliesDetailViewController alloc] initWithCellData:data conversationData:self.conversationData];
repliesDetailVC.delegate = self.delegate;
[self.navigationController pushViewController:repliesDetailVC animated:YES];
repliesDetailVC.parentPageDataProvider = self.parentPageDataProvider;
__weak typeof(self) weakSelf = self;
repliesDetailVC.willCloseCallback = ^() {
[weakSelf.tableView reloadData];
};
}
- (void)showImageMessage:(TUIImageMessageCell *)cell {
CGRect frame = [cell.thumb convertRect:cell.thumb.bounds toView:[UIApplication sharedApplication].delegate.window];
TUIMediaView *mediaView = [[TUIMediaView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, Screen_Height)];
[mediaView setThumb:cell.thumb frame:frame];
[mediaView setCurMessage:cell.messageData.innerMessage allMessages:self.imMsgs];
[[UIApplication sharedApplication].keyWindow addSubview:mediaView];
}
- (void)playVoiceMessage:(TUIVoiceMessageCell *)cell {
for (NSInteger index = 0; index < _uiMsgs.count; ++index) {
if (![_uiMsgs[index] isKindOfClass:[TUIVoiceMessageCellData class]]) {
continue;
}
TUIVoiceMessageCellData *uiMsg = (TUIVoiceMessageCellData *)_uiMsgs[index];
if (uiMsg == cell.voiceData) {
[uiMsg playVoiceMessage];
cell.voiceReadPoint.hidden = YES;
} else {
[uiMsg stopVoiceMessage];
}
}
}
- (void)showVideoMessage:(TUIVideoMessageCell *)cell {
CGRect frame = [cell.thumb convertRect:cell.thumb.bounds toView:[UIApplication sharedApplication].delegate.window];
TUIMediaView *mediaView = [[TUIMediaView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, Screen_Height)];
[mediaView setThumb:cell.thumb frame:frame];
[mediaView setCurMessage:cell.messageData.innerMessage allMessages:self.imMsgs];
[[UIApplication sharedApplication].keyWindow addSubview:mediaView];
}
- (void)showFileMessage:(TUIFileMessageCell *)cell {
TUIFileViewController *file = [[TUIFileViewController alloc] init];
file.data = [cell fileData];
[self.navigationController pushViewController:file animated:YES];
}
- (void)showLinkMessage:(TUILinkCell *)cell {
TUILinkCellData *cellData = cell.customData;
if (cellData.link) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:cellData.link]];
}
}
- (NSMutableDictionary *)stylesCache {
if (_stylesCache == nil) {
_stylesCache = [NSMutableDictionary dictionary];
}
return _stylesCache;
}
- (UIImage *)bubbleImage {
CGRect rect = CGRectMake(0, 0, 100, 40);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]);
CGContextFillRect(context, rect);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
- (TUIMessageSearchDataProvider *)msgDataProvider {
if (_msgDataProvider == nil) {
_msgDataProvider = [[TUIMessageSearchDataProvider alloc] init];
_msgDataProvider.dataSource = self;
}
return _msgDataProvider;
}
- (TUIMessageCellConfig *)messageCellConfig {
if (_messageCellConfig == nil) {
_messageCellConfig = [[TUIMessageCellConfig alloc] init];
}
return _messageCellConfig;
}
#pragma mark - TUIMessageBaseDataProviderDataSource
- (void)dataProviderDataSourceWillChange:(TUIMessageBaseDataProvider *)dataProvider {
// do nothing
}
- (void)dataProviderDataSourceChange:(TUIMessageBaseDataProvider *)dataProvider
withType:(TUIMessageBaseDataProviderDataSourceChangeType)type
atIndex:(NSUInteger)index
animation:(BOOL)animation {
// do nothing
}
- (void)dataProviderDataSourceDidChange:(TUIMessageBaseDataProvider *)dataProvider {
[self.tableView reloadData];
}
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider onRemoveHeightCache:(TUIMessageCellData *)cellData {
if (cellData) {
[self.messageCellConfig removeHeightCacheOfMessageCellData:cellData];
}
}
#pragma mark - TUINotificationProtocol
- (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(id)anObject param:(NSDictionary *)param {
if ([key isEqualToString:TUICore_TUIPluginNotify] && [subKey isEqualToString:TUICore_TUIPluginNotify_DidChangePluginViewSubKey]) {
TUIMessageCellData *data = param[TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data];
[self.messageCellConfig removeHeightCacheOfMessageCellData:data];
[self reloadAndScrollToBottomOfMessage:data.innerMessage.msgID section:0];
}
}
- (void)reloadAndScrollToBottomOfMessage:(NSString *)messageID section:(NSInteger)section {
// Dispatch the task to RunLoop to ensure that they are executed after the UITableView refresh is complete.
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadCellOfMessage:messageID section:section];
dispatch_async(dispatch_get_main_queue(), ^{
[self scrollCellToBottomOfMessage:messageID section:section];
});
});
}
- (void)reloadCellOfMessage:(NSString *)messageID section:(NSInteger)section {
NSIndexPath *indexPath = [self indexPathOfMessage:messageID section:section];
// Disable animation when loading to avoid cell jumping.
if (indexPath == nil) {
return;
}
[UIView performWithoutAnimation:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationNone];
});
}];
}
- (void)scrollCellToBottomOfMessage:(NSString *)messageID section:(NSInteger)section {
NSIndexPath *indexPath = [self indexPathOfMessage:messageID section:section];
// Scroll the tableView only if the bottom of the cell is invisible.
CGRect cellRect = [self.tableView rectForRowAtIndexPath:indexPath];
CGRect tableViewRect = self.tableView.bounds;
BOOL isBottomInvisible = cellRect.origin.y < CGRectGetMaxY(tableViewRect) && CGRectGetMaxY(cellRect) > CGRectGetMaxY(tableViewRect);
if (isBottomInvisible) {
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
- (NSIndexPath *)indexPathOfMessage:(NSString *)messageID section:(NSInteger)section {
for (int i = 0; i < self.uiMsgs.count; i++) {
TUIMessageCellData *data = self.uiMsgs[i];
if ([data.innerMessage.msgID isEqualToString:messageID]) {
return [NSIndexPath indexPathForRow:i inSection:section];
}
}
return nil;
}
@end

View File

@@ -0,0 +1,106 @@
// Created by Tencent on 2023/07/20.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
@class TUIMessageCellData;
typedef NSString * TUICellDataClassName;
typedef NSString * TUICellClassName;
typedef NSString * TUIBusinessID;
NS_ASSUME_NONNULL_BEGIN
@interface TUIMessageCellConfig : NSObject
/**
* 1.
* Bind the message tableView, and TUIMessageCellConfig will perform built-in and customized cell bindings for the tableView internally.
*/
- (void)bindTableView:(UITableView *)tableView;
/**
* 2.
* Bind cell and cellData to the tableView, and the binding can be called externally.
*/
- (void)bindMessageCellClass:(Class)cellClass cellDataClass:(Class)cellDataClass reuseID:(NSString *)reuseID;
@end
@interface TUIMessageCellConfig (MessageCellWidth)
+ (void)setMaxTextSize:(CGSize)maxTextSz;
@end
@interface TUIMessageCellConfig (MessageCellHeight)
/**
* Get key of cache
*/
- (NSString *)getHeightCacheKey:(TUIMessageCellData *)msg;
/**
* Get the height, and the cache will be queried first.
*
*/
- (CGFloat)getHeightFromMessageCellData:(TUIMessageCellData *)cellData;
/**
* Get the estimated height, and the cache will be queried first.
*/
- (CGFloat)getEstimatedHeightFromMessageCellData:(TUIMessageCellData *)cellData;
/**
* Remove cache
*/
- (void)removeHeightCacheOfMessageCellData:(TUIMessageCellData *)cellData;
@end
@interface TUIMessageCellConfig (CustomMessageRegister)
/**
* Register custom message
*
* @param messageCellName cell
* @param messageCellDataName cellData
* @param businessID
*/
+ (void)registerCustomMessageCell:(TUICellClassName)messageCellName
messageCellData:(TUICellDataClassName)messageCellDataName
forBusinessID:(TUIBusinessID)businessID;
/**
* Register custom message This is for internal use of TUIKit plugin, please use registerCustomMessageCell:messageCellData:forBusinessID: instead.
*
* @param messageCellName (custom message cell name)
* @param messageCellDataName (custom message cell data)
* @param businessID (custom message businessID)
* @param isPlugin (From Plugin)
*/
+ (void)registerCustomMessageCell:(TUICellClassName)messageCellName
messageCellData:(TUICellDataClassName)messageCellDataName
forBusinessID:(TUIBusinessID)businessID
isPlugin:(BOOL)isPlugin;
/**
* Get all custom message UI with enumeration
*/
+ (void)enumerateCustomMessageInfo:(void(^)(NSString *messageCellName,
NSString *messageCellDataName,
NSString *businessID,
BOOL isPlugin))callback;
/**
* Get the class of custom message cellData based on businessID
*/
+ (nullable Class)getCustomMessageCellDataClass:(NSString *)businessID;
/**
* Whether the custom message cell data is defined in plugin
*/
+ (BOOL)isPluginCustomMessageCellData:(TUIMessageCellData *)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,251 @@
// Created by Tencent on 2023/07/20.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMessageCellConfig.h"
#import <TUICore/TUIThemeManager.h>
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <TIMCommon/TUISystemMessageCell.h>
#import <TIMCommon/TUISystemMessageCellData.h>
#import "TUIMessageDataProvider.h"
#import "TUIFaceMessageCell.h"
#import "TUIFaceMessageCellData.h"
#import "TUIFileMessageCell.h"
#import "TUIFileMessageCellData.h"
#import "TUIImageMessageCell.h"
#import "TUIImageMessageCellData.h"
#import "TUIJoinGroupMessageCell.h"
#import "TUIJoinGroupMessageCellData.h"
#import "TUIMergeMessageCell.h"
#import "TUIMergeMessageCellData.h"
#import "TUIReferenceMessageCell.h"
#import "TUIReplyMessageCell.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCell.h"
#import "TUITextMessageCellData.h"
#import "TUIVideoMessageCell.h"
#import "TUIVideoMessageCellData.h"
#import "TUIVoiceMessageCell.h"
#import "TUIVoiceMessageCellData.h"
#define kIsCustomMessageFromPlugin @"kIsCustomMessageFromPlugin"
typedef Class<TUIMessageCellProtocol> CellClass;
typedef NSString * MessageID;
typedef NSNumber * HeightNumber;
static NSMutableDictionary *gCustomMessageInfoMap = nil;
@interface TUIMessageCellConfig ()
@property(nonatomic, weak) UITableView *tableView;
@property(nonatomic, strong) NSMutableDictionary<TUICellDataClassName, CellClass> *cellClassMaps;
@property(nonatomic, strong) NSMutableDictionary<MessageID, HeightNumber> *heightCacheMaps;
@end
@implementation TUIMessageCellConfig (MessageCellWidth)
+ (void)setMaxTextSize:(CGSize)maxTextSz {
[TUITextMessageCell setMaxTextSize:maxTextSz];
}
@end
#pragma mark - Custom Message Register
@implementation TUIMessageCellConfig (CustomMessageRegister)
+ (NSMutableDictionary *)getCustomMessageInfoMap {
if (gCustomMessageInfoMap == nil) {
gCustomMessageInfoMap = [NSMutableDictionary dictionary];
}
return gCustomMessageInfoMap;
}
+ (void)registerBuiltInCustomMessageInfo {
[self registerCustomMessageCell:@"TUILinkCell" messageCellData:@"TUILinkCellData" forBusinessID:BussinessID_TextLink];
[self registerCustomMessageCell:@"TUIGroupCreatedCell" messageCellData:@"TUIGroupCreatedCellData" forBusinessID:BussinessID_GroupCreate];
[self registerCustomMessageCell:@"TUIEvaluationCell" messageCellData:@"TUIEvaluationCellData" forBusinessID:BussinessID_Evaluation];
[self registerCustomMessageCell:@"TUIOrderCell" messageCellData:@"TUIOrderCellData" forBusinessID:BussinessID_Order];
[self registerCustomMessageCell:@"TUIMessageCell" messageCellData:@"TUITypingStatusCellData" forBusinessID:BussinessID_Typing];
[self registerCustomMessageCell:@"TUISystemMessageCell" messageCellData:@"TUILocalTipsCellData" forBusinessID:@"local_tips"];
}
+ (void)registerExternalCustomMessageInfo {
// Insert your own custom message UI here, your businessID can not be same with built-in
//
// Example:
// [self registerCustomMessageCell:#your message cell# messageCellData:#your message cell data# forBusinessID:#your id#];
//
// ...
}
+ (void)registerCustomMessageCell:(TUICellClassName)messageCellName
messageCellData:(TUICellDataClassName)messageCellDataName
forBusinessID:(TUIBusinessID)businessID {
[self registerCustomMessageCell:messageCellName messageCellData:messageCellDataName forBusinessID:businessID isPlugin:NO];
}
+ (void)registerCustomMessageCell:(TUICellClassName)messageCellName
messageCellData:(TUICellDataClassName)messageCellDataName
forBusinessID:(TUIBusinessID)businessID
isPlugin:(BOOL)isPlugin {
NSAssert(messageCellName.length > 0, @"message cell name can not be nil");
NSAssert(messageCellDataName.length > 0, @"message cell data name can not be nil");
NSAssert(businessID.length > 0, @"businessID can not be nil");
NSAssert([[self getCustomMessageInfoMap] objectForKey:businessID] == nil, @"businessID can not be same with the exists");
NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[BussinessID] = businessID;
info[TMessageCell_Name] = messageCellName;
info[TMessageCell_Data_Name] = messageCellDataName;
info[kIsCustomMessageFromPlugin] = @(isPlugin);
[[self getCustomMessageInfoMap] setObject:info forKey:businessID];
}
+ (void)enumerateCustomMessageInfo:(void(^)(NSString *messageCellName,
NSString *messageCellDataName,
NSString *businessID,
BOOL isPlugin))callback {
if (callback == nil) {
return;
}
[[self getCustomMessageInfoMap] enumerateKeysAndObjectsUsingBlock:^(TUIBusinessID key, NSDictionary *info, BOOL * _Nonnull stop) {
NSString *businessID = info[BussinessID];
NSString *messageCellName = info[TMessageCell_Name];
NSString *messageCellDataName = info[TMessageCell_Data_Name];
NSNumber *isPlugin = info[kIsCustomMessageFromPlugin];
if (businessID && messageCellName && messageCellDataName && isPlugin) {
callback(messageCellName, messageCellDataName, businessID, isPlugin);
}
}];
}
+ (nullable Class)getCustomMessageCellDataClass:(NSString *)businessID {
NSDictionary *info = [[self getCustomMessageInfoMap] objectForKey:businessID];
if (info == nil) {
return nil;
}
NSString *messageCellDataName = info[TMessageCell_Data_Name];
Class messageCellDataClass = NSClassFromString(messageCellDataName);
return messageCellDataClass;
}
+ (BOOL)isPluginCustomMessageCellData:(TUIMessageCellData *)data {
__block BOOL flag = NO;
[[self getCustomMessageInfoMap] enumerateKeysAndObjectsUsingBlock:^(TUIBusinessID key, NSDictionary *info, BOOL * _Nonnull stop) {
NSString *businessID = info[BussinessID];
NSNumber *isPlugin = info[kIsCustomMessageFromPlugin];
if (businessID && isPlugin && isPlugin.boolValue && [data.reuseId isEqualToString:businessID]) {
flag = YES;
*stop = YES;
}
}];
return flag;
}
@end
#pragma mark - Cell Message cell height
@implementation TUIMessageCellConfig (MessageCellHeight)
- (NSString *)getHeightCacheKey:(TUIMessageCellData *)msg {
return msg.msgID.length == 0 ? [NSString stringWithFormat:@"%p", msg] : msg.msgID;
}
- (CGFloat)getHeightFromMessageCellData:(TUIMessageCellData *)cellData {
static CGFloat screenWidth = 0;
if (screenWidth == 0) {
screenWidth = Screen_Width;
}
NSString *key = [self getHeightCacheKey:cellData];
CGFloat height = [[self.heightCacheMaps objectForKey:key] floatValue];
if (height == 0) {
CellClass cellClass = [self.cellClassMaps objectForKey:NSStringFromClass(cellData.class)];
if ([cellClass respondsToSelector:@selector(getHeight:withWidth:)]) {
height = [cellClass getHeight:cellData withWidth:screenWidth];
[self.heightCacheMaps setObject:@(height) forKey:key];
}
}
return height;
}
- (CGFloat)getEstimatedHeightFromMessageCellData:(TUIMessageCellData *)cellData {
NSString *key = [self getHeightCacheKey:cellData];
CGFloat height = [[self.heightCacheMaps objectForKey:key] floatValue];
return height > 0 ? height : UITableViewAutomaticDimension;;
}
- (void)removeHeightCacheOfMessageCellData:(TUIMessageCellData *)cellData {
NSString *key = [self getHeightCacheKey:cellData];
[self.heightCacheMaps removeObjectForKey:key];
}
@end
#pragma mark - TUIMessageTable
@implementation TUIMessageCellConfig
#pragma mark - Life Cycle
+ (void)load {
[self registerBuiltInCustomMessageInfo];
[self registerExternalCustomMessageInfo];
}
#pragma mark - UI
- (void)bindTableView:(UITableView *)tableView {
self.tableView = tableView;
[self bindMessageCellClass:TUITextMessageCell.class cellDataClass:TUITextMessageCellData.class reuseID:TTextMessageCell_ReuseId];
[self bindMessageCellClass:TUIVoiceMessageCell.class cellDataClass:TUIVoiceMessageCellData.class reuseID:TVoiceMessageCell_ReuseId];
[self bindMessageCellClass:TUIImageMessageCell.class cellDataClass:TUIImageMessageCellData.class reuseID:TImageMessageCell_ReuseId];
[self bindMessageCellClass:TUISystemMessageCell.class cellDataClass:TUISystemMessageCellData.class reuseID:TSystemMessageCell_ReuseId];
[self bindMessageCellClass:TUIFaceMessageCell.class cellDataClass:TUIFaceMessageCellData.class reuseID:TFaceMessageCell_ReuseId];
[self bindMessageCellClass:TUIVideoMessageCell.class cellDataClass:TUIVideoMessageCellData.class reuseID:TVideoMessageCell_ReuseId];
[self bindMessageCellClass:TUIFileMessageCell.class cellDataClass:TUIFileMessageCellData.class reuseID:TFileMessageCell_ReuseId];
[self bindMessageCellClass:TUIJoinGroupMessageCell.class cellDataClass:TUIJoinGroupMessageCellData.class reuseID:TJoinGroupMessageCell_ReuseId];
[self bindMessageCellClass:TUIMergeMessageCell.class cellDataClass:TUIMergeMessageCellData.class reuseID:TMergeMessageCell_ReuserId];
[self bindMessageCellClass:TUIReplyMessageCell.class cellDataClass:TUIReplyMessageCellData.class reuseID:TReplyMessageCell_ReuseId];
[self bindMessageCellClass:TUIReferenceMessageCell.class cellDataClass:TUIReferenceMessageCellData.class reuseID:TUIReferenceMessageCell_ReuseId];
__weak typeof(self) weakSelf = self;
[self.class enumerateCustomMessageInfo:^(NSString * _Nonnull messageCellName,
NSString * _Nonnull messageCellDataName,
NSString * _Nonnull businessID,
BOOL isPlugin) {
Class cellClass = NSClassFromString(messageCellName);
Class cellDataClass = NSClassFromString(messageCellDataName);
[weakSelf bindMessageCellClass:cellClass cellDataClass:cellDataClass reuseID:businessID];
}];
}
- (void)bindMessageCellClass:(Class)cellClass cellDataClass:(Class)cellDataClass reuseID:(NSString *)reuseID {
NSAssert(cellClass != nil, @"The UITableViewCell can not be nil");
NSAssert(cellDataClass != nil, @"The cell data class can not be nil");
NSAssert(reuseID.length > 0, @"The reuse identifier can not be nil");
[self.tableView registerClass:cellClass forCellReuseIdentifier:reuseID];
[self.cellClassMaps setObject:cellClass forKey:NSStringFromClass(cellDataClass)];
}
#pragma mark - Lazy && Read-write operate
- (NSMutableDictionary<TUICellDataClassName,CellClass> *)cellClassMaps {
if (_cellClassMaps == nil) {
_cellClassMaps = [NSMutableDictionary dictionary];
}
return _cellClassMaps;
}
- (NSMutableDictionary<MessageID,HeightNumber> *)heightCacheMaps {
if (_heightCacheMaps == nil) {
_heightCacheMaps = [NSMutableDictionary dictionary];
}
return _heightCacheMaps;
}
@end

View File

@@ -0,0 +1,30 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <TIMCommon/TIMDefine.h>
#import "TUIBaseMessageController.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMessageController : TUIBaseMessageController
/**
* Highlight text
* In the search scenario, when highlightKeyword is not empty and matches @locateMessage, opening the chat session page will highlight the current cell
*/
@property(nonatomic, copy) NSString *hightlightKeyword;
/**
* Locate message
* In the search scenario, when locateMessage is not empty, opening the chat session page will automatically scroll to here
*/
@property(nonatomic, strong) V2TIMMessage *locateMessage;
@property(nonatomic, strong) V2TIMMessage *C2CIncomingLastMsg;
- (void)locateAssignMessage:(V2TIMMessage *)message matchKeyWord:(NSString *)keyword;
- (void)findMessages:(NSArray<NSString *> *)msgIDs callback:(void (^)(BOOL success, NSString *desc, NSArray<V2TIMMessage *> *messages))callback;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,587 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMessageController.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/UIView+TUILayout.h>
#import "TUIBaseMessageController+ProtectedAPI.h"
#import "TUIChatConfig.h"
#import "TUIChatModifyMessageHelper.h"
#import "TUIChatSmallTongueView.h"
#import "TUIMessageSearchDataProvider.h"
#import "TUIReferenceMessageCell.h"
#import "TUIReplyMessageCell.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCell.h"
#define MSG_GET_COUNT 20
@interface TUIMessageController () <TUIChatSmallTongueViewDelegate>
@property(nonatomic, strong) UIActivityIndicatorView *bottomIndicatorView;
@property(nonatomic, assign) uint64_t locateGroupMessageSeq;
@property(nonatomic, strong) TUIChatSmallTongueView *tongueView;
@property(nonatomic, strong) NSMutableArray *receiveMsgs;
@property(nonatomic, weak) UIImageView *backgroudView;
@end
@implementation TUIMessageController
#pragma mark - Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.bottomIndicatorView =
[[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, TMessageController_Header_Height)];
self.bottomIndicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
self.tableView.tableFooterView = self.bottomIndicatorView;
self.tableView.backgroundColor = UIColor.clearColor;
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillShow) name:UIKeyboardWillShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onBottomMarginChanged:)
name:TUIKitNotification_onMessageVCBottomMarginChanged object:nil];
self.receiveMsgs = [NSMutableArray array];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.conversationData.atMsgSeqs.count > 0) {
TUIChatSmallTongue *tongue = [[TUIChatSmallTongue alloc] init];
tongue.type = TUIChatSmallTongueType_SomeoneAt;
tongue.parentView = self.view.superview;
tongue.atTipsStr = self.conversationData.atTipsStr;
tongue.atMsgSeqs = [self.conversationData.atMsgSeqs copy];
[TUIChatSmallTongueManager showTongue:tongue delegate:self];
}
});
}
- (void)dealloc {
[TUIChatSmallTongueManager removeTongue];
[NSNotificationCenter.defaultCenter removeObserver:self];
NSLog(@"%s dealloc", __FUNCTION__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[TUIChatSmallTongueManager hideTongue:NO];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[TUIChatSmallTongueManager hideTongue:YES];
}
#pragma mark - Notification
- (void)keyboardWillShow {
if (![self messageSearchDataProvider].isNewerNoMoreMsg) {
[[self messageSearchDataProvider] removeAllSearchData];
[self.tableView reloadData];
[self loadMessages:YES];
}
}
- (void)onBottomMarginChanged:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
if ([userInfo.allKeys containsObject:TUIKitNotification_onMessageVCBottomMarginChanged_Margin] &&
[userInfo[TUIKitNotification_onMessageVCBottomMarginChanged_Margin] isKindOfClass:NSNumber.class]) {
float margin = [userInfo[TUIKitNotification_onMessageVCBottomMarginChanged_Margin] floatValue];
[TUIChatSmallTongueManager adaptTongueBottomMargin:margin];
}
}
#pragma mark - Overrider
- (void)willShowMediaMessage:(TUIMessageCell *)cell {
[TUIChatSmallTongueManager hideTongue:YES];
}
- (void)didCloseMediaMessage:(TUIMessageCell *)cell {
[TUIChatSmallTongueManager hideTongue:NO];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[super scrollViewDidScroll:scrollView];
if (scrollView.contentOffset.y <= TMessageController_Header_Height
&& (scrollView.isTracking || scrollView.isDecelerating)
&& ![self messageSearchDataProvider].isOlderNoMoreMsg
&& !self.indicatorView.isAnimating) {
// Display pull-to-refresh icon
[self.indicatorView startAnimating];
} else if ([self isScrollToBottomIndicatorViewY:scrollView]) {
if ((scrollView.isTracking || scrollView.isDecelerating)
&& ![self messageSearchDataProvider].isNewerNoMoreMsg
&& !self.bottomIndicatorView.isAnimating) {
// Display pull-to-refresh icon
[self.bottomIndicatorView startAnimating];
}
/**
* Remove the "back to the latest position", "xxx new message" bottom-banner-tips
*/
if (self.isInVC) {
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_ScrollToBoom];
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_ReceiveNewMsg];
}
} else if (self.isInVC && 0 == self.receiveMsgs.count
&& self.tableView.contentSize.height - self.tableView.contentOffset.y >= Screen_Height * 2.0) {
CGPoint point = [scrollView.panGestureRecognizer translationInView:scrollView];
/**
* When swiping, add a "back to last position" bottom-banner-tips
*/
if (point.y > 0) {
TUIChatSmallTongue *tongue = [[TUIChatSmallTongue alloc] init];
tongue.type = TUIChatSmallTongueType_ScrollToBoom;
tongue.parentView = self.view.superview;
[TUIChatSmallTongueManager showTongue:tongue delegate:self];
}
} else if (self.isInVC && self.tableView.contentSize.height - self.tableView.contentOffset.y >= 20) {
/**
* Remove the "someone @ me" bottom-banner-tips
*/
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_SomeoneAt];
} else {
if (self.indicatorView.isAnimating) {
[self.indicatorView stopAnimating];
}
if (self.bottomIndicatorView.isAnimating) {
[self.bottomIndicatorView stopAnimating];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[super scrollViewDidEndDecelerating:scrollView];
if (scrollView.contentOffset.y <= TMessageController_Header_Height && ![self messageSearchDataProvider].isOlderNoMoreMsg) {
/**
* Pull old news
*/
[self loadMessages:YES];
} else if ([self isScrollToBottomIndicatorViewY:scrollView] && ![self messageSearchDataProvider].isNewerNoMoreMsg) {
/**
* Load latese message
*/
[self loadMessages:NO];
}
}
- (BOOL)isScrollToBottomIndicatorViewY:(UIScrollView *)scrollView {
/**
* +2 pixels when scrolling to critical point
*/
return (scrollView.contentOffset.y + self.tableView.mm_h + 2) > (scrollView.contentSize.height - self.indicatorView.mm_h);
}
#pragma mark - Getters & Setters
- (void)setConversation:(TUIChatConversationModel *)conversationData {
self.conversationData = conversationData;
self.messageDataProvider = [[TUIMessageSearchDataProvider alloc] initWithConversationModel:self.conversationData];
self.messageDataProvider.dataSource = self;
if (self.locateMessage) {
[self loadAndScrollToLocateMessages:NO isHighlight:YES];
} else {
[[self messageSearchDataProvider] removeAllSearchData];
[self loadMessages:YES];
}
[self loadGroupInfo];
}
#pragma mark - Private Methods
- (TUIMessageSearchDataProvider *)messageSearchDataProvider {
return (TUIMessageSearchDataProvider *)self.messageDataProvider;
}
- (void)loadAndScrollToLocateMessages:(BOOL)scrollToBoom isHighlight:(BOOL)isHighlight{
if (!self.locateMessage && self.locateGroupMessageSeq == 0) {
return;
}
@weakify(self);
[[self messageSearchDataProvider]
loadMessageWithSearchMsg:self.locateMessage
SearchMsgSeq:self.locateGroupMessageSeq
ConversationInfo:self.conversationData
SucceedBlock:^(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, NSArray<TUIMessageCellData *> *_Nonnull newMsgs) {
@strongify(self);
[self.indicatorView stopAnimating];
[self.bottomIndicatorView stopAnimating];
self.indicatorView.mm_h = 0;
self.bottomIndicatorView.mm_h = 0;
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
dispatch_async(dispatch_get_main_queue(), ^{
[self scrollToLocateMessage:scrollToBoom];
if (isHighlight) {
[self highlightKeyword];
}
});
}
FailBlock:^(int code, NSString *desc){}];
}
- (void)scrollToLocateMessage:(BOOL)scrollToBoom {
/**
* First find the coordinate offset of locateMsg
*/
CGFloat offsetY = 0;
NSInteger index = 0;
for (TUIMessageCellData *uiMsg in [self messageSearchDataProvider].uiMsgs) {
if ([self isLocateMessage:uiMsg]) {
break;
}
offsetY += [self getHeightFromMessageCellData:uiMsg];
index++;
}
/**
*
* The locateMsg not found
*/
if (index == [self messageSearchDataProvider].uiMsgs.count) {
return;
}
/**
* tableview
* Offset half the height of the tableview
*/
offsetY -= self.tableView.frame.size.height / 2.0;
if (offsetY <= TMessageController_Header_Height) {
offsetY = TMessageController_Header_Height + 0.1;
}
if (offsetY > TMessageController_Header_Height) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]
atScrollPosition:UITableViewScrollPositionMiddle
animated:YES];
}
}
- (void)highlightKeyword {
TUIMessageCellData *cellData = nil;
for (TUIMessageCellData *tmp in [self messageSearchDataProvider].uiMsgs) {
if ([self isLocateMessage:tmp]) {
cellData = tmp;
break;
}
}
if (cellData == nil || cellData.innerMessage.elemType == V2TIM_ELEM_TYPE_GROUP_TIPS) {
return;
}
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[self messageDataProvider].uiMsgs indexOfObject:cellData] inSection:0];
cellData.highlightKeyword = self.hightlightKeyword.length ? self.hightlightKeyword : @"hightlight";
TUIMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell fillWithData:cellData];
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[self messageDataProvider].uiMsgs indexOfObject:cellData] inSection:0];
cellData.highlightKeyword = nil;
TUIMessageCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[cell fillWithData:cellData];
});
});
}
- (BOOL)isLocateMessage:(TUIMessageCellData *)uiMsg {
if (self.locateMessage) {
if ([uiMsg.innerMessage.msgID isEqualToString:self.locateMessage.msgID]) {
return YES;
}
} else {
if (self.conversationData.groupID.length > 0 && uiMsg.innerMessage && uiMsg.innerMessage.seq == self.locateGroupMessageSeq) {
return YES;
}
}
return NO;
}
- (void)loadMessages:(BOOL)order {
if ([self messageSearchDataProvider].isLoadingData) {
return;
}
if (order && [self messageSearchDataProvider].isOlderNoMoreMsg) {
[self.indicatorView stopAnimating];
return;
}
if (!order && [self messageSearchDataProvider].isNewerNoMoreMsg) {
[self.bottomIndicatorView stopAnimating];
return;
}
@weakify(self);
[[self messageSearchDataProvider]
loadMessageWithIsRequestOlderMsg:order
ConversationInfo:self.conversationData
SucceedBlock:^(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, BOOL isFirstLoad, NSArray<TUIMessageCellData *> *_Nonnull newUIMsgs) {
@strongify(self);
[self.indicatorView stopAnimating];
[self.bottomIndicatorView stopAnimating];
if (isOlderNoMoreMsg) {
self.indicatorView.mm_h = 0;
} else {
self.indicatorView.mm_h = TMessageController_Header_Height;
}
if (isNewerNoMoreMsg) {
self.bottomIndicatorView.mm_h = 0;
} else {
self.bottomIndicatorView.mm_h = TMessageController_Header_Height;
}
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[newUIMsgs enumerateObjectsWithOptions:NSEnumerationReverse
usingBlock:^(TUIMessageCellData *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
if (obj.direction == MsgDirectionIncoming) {
self.C2CIncomingLastMsg = obj.innerMessage;
*stop = YES;
}
}];
if (isFirstLoad) {
[self scrollToBottom:NO];
} else {
if (order) {
NSInteger index = 0;
if (newUIMsgs.count > 0) {
index = newUIMsgs.count - 1;
}
if (self.messageDataProvider.uiMsgs.count > 0) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
}
}
}
}
FailBlock:^(int code, NSString *desc){
}];
}
- (void)showReplyMessage:(TUIReplyMessageCell *)cell {
NSString *originMsgID = @"";
NSString *msgAbstract = @"";
if ([cell isKindOfClass:TUIReplyMessageCell.class]) {
TUIReplyMessageCell *acell = (TUIReplyMessageCell *)cell;
TUIReplyMessageCellData *cellData = acell.replyData;
originMsgID = cellData.messageRootID;
msgAbstract = cellData.msgAbstract;
} else if ([cell isKindOfClass:TUIReferenceMessageCell.class]) {
TUIReferenceMessageCell *acell = (TUIReferenceMessageCell *)cell;
TUIReferenceMessageCellData *cellData = acell.referenceData;
originMsgID = cellData.originMsgID;
msgAbstract = cellData.msgAbstract;
}
@weakify(self);
[(TUIMessageSearchDataProvider *)self.messageDataProvider
findMessages:@[ originMsgID ?: @"" ]
callback:^(BOOL success, NSString *_Nonnull desc, NSArray<V2TIMMessage *> *_Nonnull msgs) {
@strongify(self);
if (!success) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
V2TIMMessage *message = msgs.firstObject;
if (message == nil) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
if (message.status == V2TIM_MSG_STATUS_HAS_DELETED || message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitReplyMessageNotFoundOriginMessage)];
return;
}
BOOL hasRiskContent = message.hasRiskContent;
if ([cell isKindOfClass:TUIReplyMessageCell.class]) {
if (hasRiskContent) {
return;
}
[self jumpDetailPageByMessage:message];
} else if ([cell isKindOfClass:TUIReferenceMessageCell.class]) {
[self locateAssignMessage:message matchKeyWord:msgAbstract];
}
}];
}
- (void)jumpDetailPageByMessage:(V2TIMMessage *)message {
NSMutableArray *uiMsgs = [self.messageDataProvider transUIMsgFromIMMsg:@[ message ]];
if (uiMsgs.count == 0) {
return;
}
[self.messageDataProvider preProcessMessage:uiMsgs
callback:^{
for (TUIMessageCellData *cellData in uiMsgs) {
if ([cellData.innerMessage.msgID isEqual:message.msgID]) {
[self onJumpToRepliesDetailPage:cellData];
return;
}
}
}];
}
- (void)locateAssignMessage:(V2TIMMessage *)message matchKeyWord:(NSString *)keyword {
if (message == nil) {
return;
}
self.locateMessage = message;
self.hightlightKeyword = keyword;
BOOL memoryExist = NO;
for (TUIMessageCellData *cellData in self.messageDataProvider.uiMsgs) {
if ([cellData.innerMessage.msgID isEqual:message.msgID]) {
memoryExist = YES;
break;
}
}
if (memoryExist) {
[self scrollToLocateMessage:NO];
[self highlightKeyword];
return;
}
TUIMessageSearchDataProvider *provider = (TUIMessageSearchDataProvider *)self.messageDataProvider;
provider.isNewerNoMoreMsg = NO;
provider.isOlderNoMoreMsg = NO;
[self loadAndScrollToLocateMessages:NO isHighlight:YES];
}
- (void)findMessages:(NSArray<NSString *> *)msgIDs callback:(void (^)(BOOL success, NSString *desc, NSArray<V2TIMMessage *> *messages))callback {
TUIMessageSearchDataProvider *provider = (TUIMessageSearchDataProvider *)self.messageDataProvider;
if (provider) {
[provider findMessages:msgIDs callback:callback];
}
}
#pragma mark - TUIMessageBaseDataProviderDataSource
- (void)dataProvider:(TUIMessageDataProvider *)dataProvider ReceiveNewUIMsg:(TUIMessageCellData *)uiMsg {
[super dataProvider:dataProvider ReceiveNewUIMsg:uiMsg];
/**
* When viewing historical messages, if you scroll more than two screens, after receiving a new message, add a "xxx new message" bottom-banner-tips
*/
if (self.tableView.contentSize.height - self.tableView.contentOffset.y >= Screen_Height * 2.0) {
[self.receiveMsgs addObject:uiMsg];
TUIChatSmallTongue *tongue = [[TUIChatSmallTongue alloc] init];
tongue.type = TUIChatSmallTongueType_ReceiveNewMsg;
tongue.parentView = self.view.superview;
tongue.unreadMsgCount = self.receiveMsgs.count;
[TUIChatSmallTongueManager showTongue:tongue delegate:self];
}
if (self.isInVC) {
self.C2CIncomingLastMsg = uiMsg.innerMessage;
}
}
- (void)dataProvider:(TUIMessageDataProvider *)dataProvider ReceiveRevokeUIMsg:(TUIMessageCellData *)uiMsg {
/**
* Recalled messages need to be removed from "xxx new messages" bottom-banner-tips
*/
[super dataProvider:dataProvider ReceiveRevokeUIMsg:uiMsg];
if ([self.receiveMsgs containsObject:uiMsg]) {
[self.receiveMsgs removeObject:uiMsg];
TUIChatSmallTongue *tongue = [[TUIChatSmallTongue alloc] init];
tongue.type = TUIChatSmallTongueType_ReceiveNewMsg;
tongue.parentView = self.view.superview;
tongue.unreadMsgCount = self.receiveMsgs.count;
if (tongue.unreadMsgCount != 0) {
[TUIChatSmallTongueManager showTongue:tongue delegate:self];
} else {
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_ReceiveNewMsg];
}
}
/*
* When the retracted message is a "reply" type of message, go to the root message to delete the currently retracted message.
*/
if ([uiMsg isKindOfClass:TUIReplyMessageCellData.class]) {
TUIReplyMessageCellData *cellData = (TUIReplyMessageCellData *)uiMsg;
NSString *messageRootID = @"";
NSString *revokeMsgID = @"";
messageRootID = cellData.messageRootID;
revokeMsgID = cellData.msgID;
[(TUIMessageSearchDataProvider *)self.messageDataProvider
findMessages:@[ messageRootID ?: @"" ]
callback:^(BOOL success, NSString *_Nonnull desc, NSArray<V2TIMMessage *> *_Nonnull msgs) {
if (success) {
V2TIMMessage *message = msgs.firstObject;
[[TUIChatModifyMessageHelper defaultHelper] modifyMessage:message revokeMsgID:revokeMsgID];
}
}];
}
/*
The message whose reference is withdrawn should not expose the original message content, so it is necessary to traverse all reference messages and reply messages and replace the original message content with "TIMCommonLocalizableString(TUIKitRepliesOriginMessageRevoke)"
*/
for (TUIMessageCellData * cellData in self.messageDataProvider.uiMsgs) {
if ([cellData isKindOfClass:TUIReplyMessageCellData.class]) {
TUIReplyMessageCellData *replyMessageData = (TUIReplyMessageCellData *)cellData;
if ([replyMessageData.originMessage.msgID isEqualToString:uiMsg.msgID]) {
[self.messageDataProvider processQuoteMessage:@[replyMessageData]];
}
}
}
}
#pragma mark - TUIChatSmallTongueViewDelegate
- (void)onChatSmallTongueClick:(TUIChatSmallTongue *)tongue {
switch (tongue.type) {
case TUIChatSmallTongueType_ScrollToBoom: {
@weakify(self)
[self.messageDataProvider getLastMessage:YES succ:^(V2TIMMessage * _Nonnull message) {
@strongify(self)
if (!message) return;
self.locateMessage = message;
for (TUIMessageCellData *cellData in self.messageDataProvider.uiMsgs) {
if ([self isLocateMessage:cellData]) {
[self scrollToLocateMessage:YES];
return;
}
}
[self loadAndScrollToLocateMessages:YES isHighlight:NO];
} fail:^(int code, NSString *desc) {
NSLog(@"getLastMessage failed");
}];
} break;
case TUIChatSmallTongueType_ReceiveNewMsg: {
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_ReceiveNewMsg];
TUIMessageCellData *cellData = self.receiveMsgs.firstObject;
if (cellData) {
self.locateMessage = cellData.innerMessage;
[self scrollToLocateMessage:YES];
[self highlightKeyword];
}
[self.receiveMsgs removeAllObjects];
} break;
case TUIChatSmallTongueType_SomeoneAt: {
[TUIChatSmallTongueManager removeTongue:TUIChatSmallTongueType_SomeoneAt];
[self.conversationData.atMsgSeqs removeAllObjects];
self.locateGroupMessageSeq = [tongue.atMsgSeqs.firstObject integerValue];
for (TUIMessageCellData *cellData in self.messageDataProvider.uiMsgs) {
if ([self isLocateMessage:cellData]) {
[self scrollToLocateMessage:YES];
[self highlightKeyword];
return;
}
}
[self loadAndScrollToLocateMessages:YES isHighlight:YES];
} break;
default:
break;
}
}
@end

View File

@@ -0,0 +1,67 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class TUIMessageMultiChooseView;
@protocol TUIMessageMultiChooseViewDelegate <NSObject>
/**
* Callback when the cancel button on the multi-select message panel is clicked
*/
- (void)messageMultiChooseViewOnCancelClicked:(TUIMessageMultiChooseView *)multiChooseView;
/**
* Callback for when the forward button on the multi-select message panel is clicked
*/
- (void)messageMultiChooseViewOnRelayClicked:(TUIMessageMultiChooseView *)multiChooseView;
/**
* Callback for when the delete button on the multi-select message panel is clicked
*/
- (void)messageMultiChooseViewOnDeleteClicked:(TUIMessageMultiChooseView *)multiChooseView;
@end
@interface TUIMessageMultiChooseView : UIView
@property(nonatomic, weak) id<TUIMessageMultiChooseViewDelegate> delegate;
#pragma mark - Top toolbar
/**
* The top toolbar, showing shortcut operations such as cancel
*/
@property(nonatomic, strong) UIView *toolView;
/**
* Top toolbar element: Cancel button
*/
@property(nonatomic, strong) UIButton *cancelButton;
/**
* Top toolbar element: title
*/
@property(nonatomic, strong) UILabel *titleLabel;
#pragma mark - Bottom menu bar
/**
* The bottom menu bar, shows the operation menu after multiple selection messages, such as forwarding, deleting, etc.
*/
@property(nonatomic, strong) UIView *menuView;
/**
* Bottom menu bar element: Forward button
*/
@property(nonatomic, strong) UIButton *relayButton;
/**
* Bottom menu bar element: Delete button
*/
@property(nonatomic, strong) UIButton *deleteButton;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,159 @@
//
// TUIMessageMultiChooseView.m
// Pods
//
// Created by harvy on 2020/11/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMessageMultiChooseView.h"
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/UIView+TUILayout.h>
#import <TIMCommon/TIMDefine.h>
@interface TUIMessageMultiChooseView ()
@property(nonatomic, strong) CALayer *separtorLayer;
@end
@implementation TUIMessageMultiChooseView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupViews];
}
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
return nil;
} else {
return hitView;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat toolHeight = 44;
CGFloat menuHeight = 54;
CGFloat centerTopOffset = 0;
CGFloat centerBottomOffset = 0;
if (@available(iOS 11.0, *)) {
toolHeight += self.safeAreaInsets.top;
menuHeight += self.safeAreaInsets.bottom;
centerTopOffset = self.safeAreaInsets.top;
centerBottomOffset = self.safeAreaInsets.bottom;
}
self.toolView.frame = CGRectMake(0, 0, self.bounds.size.width, toolHeight);
self.menuView.frame = CGRectMake(0, self.bounds.size.height - menuHeight, self.bounds.size.width, menuHeight);
self.separtorLayer.frame = CGRectMake(0, 0, self.menuView.mm_w, 1);
// toolView
{
CGFloat centerY = 0.5 * (self.toolView.bounds.size.height - self.cancelButton.bounds.size.height);
self.cancelButton.frame = CGRectMake(10, centerY += 0.5 * centerTopOffset, self.cancelButton.bounds.size.width, self.cancelButton.bounds.size.height);
[self.titleLabel sizeToFit];
self.titleLabel.center = self.toolView.center;
CGRect titleRect = self.self.titleLabel.frame;
titleRect.origin.y += 0.5 * centerTopOffset;
self.titleLabel.frame = titleRect;
}
// menuView
{
NSInteger count = self.menuView.subviews.count;
CGFloat width = self.menuView.bounds.size.width / count;
CGFloat height = (self.menuView.bounds.size.height - centerBottomOffset);
for (int i = 0; i < self.menuView.subviews.count; i++) {
UIView *sub = self.menuView.subviews[i];
CGFloat centerY = (self.menuView.bounds.size.height - height) * 0.5;
sub.frame = CGRectMake(i * width, centerY -= 0.5 * centerBottomOffset, width, height);
}
}
if(isRTL()) {
for (UIView *subview in self.toolView.subviews) {
[subview resetFrameToFitRTL];
}
for (UIView *subview in self.menuView.subviews) {
[subview resetFrameToFitRTL];
}
}
}
#pragma mark - Views
- (void)setupViews {
_toolView = [[UIView alloc] init];
_toolView.backgroundColor = TIMCommonDynamicColor(@"head_bg_gradient_start_color", @"#EBF0F6");
[self addSubview:_toolView];
_menuView = [[UIView alloc] init];
_menuView.backgroundColor = TUIChatDynamicColor(@"chat_controller_bg_color", @"#FFFFFF");
[_menuView.layer addSublayer:self.separtorLayer];
[self addSubview:_menuView];
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_cancelButton setTitle:TIMCommonLocalizableString(Cancel) forState:UIControlStateNormal];
[_cancelButton setTitleColor:TIMCommonDynamicColor(@"form_title_color", @"#000000") forState:UIControlStateNormal];
_cancelButton.titleLabel.font = [UIFont systemFontOfSize:15.0];
[_cancelButton addTarget:self action:@selector(onCancel:) forControlEvents:UIControlEventTouchUpInside];
[_cancelButton sizeToFit];
[_toolView addSubview:_cancelButton];
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"";
_titleLabel.font = [UIFont boldSystemFontOfSize:17.0];
_titleLabel.textColor = TIMCommonDynamicColor(@"form_title_color", @"#000000");
[_toolView addSubview:_titleLabel];
_relayButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_relayButton setTitle:TIMCommonLocalizableString(Forward) forState:UIControlStateNormal];
_relayButton.titleLabel.font = [UIFont systemFontOfSize:14.0];
[_relayButton setTitleColor:TIMCommonDynamicColor(@"form_title_color", @"#000000") forState:UIControlStateNormal];
[_relayButton addTarget:self action:@selector(onRelay:) forControlEvents:UIControlEventTouchUpInside];
[_menuView addSubview:_relayButton];
_deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_deleteButton setTitle:TIMCommonLocalizableString(Delete) forState:UIControlStateNormal];
_deleteButton.titleLabel.font = [UIFont systemFontOfSize:14.0];
[_deleteButton setTitleColor:TIMCommonDynamicColor(@"form_title_color", @"#000000") forState:UIControlStateNormal];
[_deleteButton addTarget:self action:@selector(onDelete:) forControlEvents:UIControlEventTouchUpInside];
[_menuView addSubview:_deleteButton];
}
#pragma mark - Action
- (void)onCancel:(UIButton *)cancelButton {
if (self.delegate && [self.delegate respondsToSelector:@selector(messageMultiChooseViewOnCancelClicked:)]) {
[self.delegate messageMultiChooseViewOnCancelClicked:self];
}
}
- (void)onRelay:(UIButton *)cancelButton {
if (self.delegate && [self.delegate respondsToSelector:@selector(messageMultiChooseViewOnRelayClicked:)]) {
[self.delegate messageMultiChooseViewOnRelayClicked:self];
}
}
- (void)onDelete:(UIButton *)cancelButton {
if (self.delegate && [self.delegate respondsToSelector:@selector(messageMultiChooseViewOnDeleteClicked:)]) {
[self.delegate messageMultiChooseViewOnDeleteClicked:self];
}
}
- (CALayer *)separtorLayer {
if (_separtorLayer == nil) {
_separtorLayer = [CALayer layer];
_separtorLayer.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor;
}
return _separtorLayer;
}
@end

View File

@@ -0,0 +1,47 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
*
* This file mainly declares the controller class that jumps to the read member list after clicking the read label of the group chat message
*
* The TUIMessageReadSelectView class defines a tab-like view in the read list. Currently only used in TUIMessageReadViewController.
* TUIMessageReadSelectViewDelegate Callback for clicked view event. Currently implemented by TUIMessageReadViewController to switch between read and unread
* lists.
*
* The TUIMessageReadViewController class implements the UI and logic for the read member list.
* TUIMessageReadViewControllerDelegate callback click member list cell event.
*/
#import <TIMCommon/TUIMessageCellData.h>
#import <UIKit/UIKit.h>
#import "TUIChatDefine.h"
NS_ASSUME_NONNULL_BEGIN
@class TUIMessageReadSelectView;
@protocol TUIMessageReadSelectViewDelegate <NSObject>
@optional
- (void)messageReadSelectView:(TUIMessageReadSelectView *)view didSelectItemTag:(TUIMessageReadViewTag)tag;
@end
@interface TUIMessageReadSelectView : UIView
@property(nonatomic, weak) id<TUIMessageReadSelectViewDelegate> delegate;
@property(nonatomic, assign) BOOL selected;
- (instancetype)initWithTitle:(NSString *)title viewTag:(TUIMessageReadViewTag)tag selected:(BOOL)selected;
@end
@class TUIMessageDataProvider;
@interface TUIMessageReadViewController : UIViewController
@property(copy, nonatomic) void (^viewWillDismissHandler)(void);
- (instancetype)initWithCellData:(TUIMessageCellData *)data
dataProvider:(TUIMessageDataProvider *)dataProvider
showReadStatusDisable:(BOOL)showReadStatusDisable
c2cReceiverName:(NSString *)name
c2cReceiverAvatar:(NSString *)avatarUrl;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,656 @@
//
// TUIMessageReadViewController.m
// TUIChat
//
// Created by xia on 2022/3/10.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMessageReadViewController.h"
#import <TUICore/TUICore.h>
#import <TUICore/TUIThemeManager.h>
#import <TUICore/TUITool.h>
#import <TUICore/UIColor+TUIHexColor.h>
#import "TUIImageMessageCellData.h"
#import "TUIMemberCell.h"
#import "TUIMemberCellData.h"
#import "TUIMessageDataProvider.h"
#import "TUIVideoMessageCellData.h"
@interface TUIMessageReadSelectView ()
@property(nonatomic, copy) NSString *title;
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UIView *bottomLine;
@end
@implementation TUIMessageReadSelectView
#pragma mark - Life cycle
- (void)layoutSubviews {
[super layoutSubviews];
[self layoutViews];
}
#pragma mark - Public
- (instancetype)initWithTitle:(NSString *)title viewTag:(TUIMessageReadViewTag)tag selected:(BOOL)selected {
self = [super init];
if (self) {
self.title = title;
self.tag = tag;
[self setupViews];
[self setupGesture];
[self setupRAC];
self.selected = selected;
}
return self;
}
#pragma mark - Private
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_controller_bg_color", @"#FFFFFF");
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.text = self.title;
self.titleLabel.font = [UIFont systemFontOfSize:16];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.titleLabel];
self.bottomLine = [[UIView alloc] init];
[self addSubview:self.bottomLine];
}
- (void)layoutViews {
self.titleLabel.mm_sizeToFit().mm_height(24).mm__centerX(self.mm_w / 2.0).mm__centerY(self.mm_h / 2.0);
self.bottomLine.mm_width(self.titleLabel.mm_w).mm_height(5 / 2.0).mm_top(self.titleLabel.mm_maxY + 8 / 2.0).mm__centerX(self.titleLabel.mm_centerX);
}
- (void)setupRAC {
@weakify(self);
[RACObserve(self, selected) subscribeNext:^(id _Nullable x) {
@strongify(self);
[self updateColorBySelected:self.selected];
}];
}
- (void)updateColorBySelected:(BOOL)selected {
UIColor *color = selected ? TUIChatDynamicColor(@"chat_message_read_status_tab_color", @"#147AFF")
: TIMCommonDynamicColor(@"chat_message_read_status_tab_unselect_color", @"#444444");
self.titleLabel.textColor = color;
self.bottomLine.hidden = !selected;
self.bottomLine.backgroundColor = color;
}
- (void)setupGesture {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapped:)];
[self addGestureRecognizer:tap];
}
#pragma mark - Event
- (void)onTapped:(UIGestureRecognizer *)gesture {
if ([self.delegate respondsToSelector:@selector(messageReadSelectView:didSelectItemTag:)]) {
[self.delegate messageReadSelectView:self didSelectItemTag:self.tag];
}
}
@end
@interface TUIMessageReadViewController () <UITableViewDelegate, UITableViewDataSource, TUIMessageReadSelectViewDelegate>
@property(nonatomic, strong) TUIMessageCellData *cellData;
@property(nonatomic, assign) BOOL showReadStatusDisable;
@property(nonatomic, assign) TUIMessageReadViewTag selectedViewTag;
@property(nonatomic, strong) TUIMessageDataProvider *dataProvider;
@property(nonatomic, strong) UIView *messageBackView;
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) UIActivityIndicatorView *indicatorView;
@property(nonatomic, strong) UILabel *contentLabel;
@property(nonatomic, strong) NSMutableDictionary *selectViewsDict;
@property(nonatomic, strong) NSMutableArray *readMembers;
@property(nonatomic, strong) NSMutableArray *unreadMembers;
@property(nonatomic, assign) NSUInteger readSeq;
@property(nonatomic, assign) NSUInteger unreadSeq;
@property(nonatomic, copy) NSString *c2cReceiverName;
@property(nonatomic, copy) NSString *c2cReceiverAvatarUrl;
@end
@implementation TUIMessageReadViewController
#pragma mark - Life cycle
- (void)viewDidLoad {
[super viewDidLoad];
if ([self isGroupMessageRead]) {
self.selectedViewTag = TUIMessageReadViewTagRead;
[self loadMembers];
} else {
self.selectedViewTag = TUIMessageReadViewTagC2C;
}
[self setupViews];
// Do any additional setup after loading the view.
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self layoutViews];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (_viewWillDismissHandler) {
_viewWillDismissHandler();
}
}
- (void)dealloc {
NSLog(@"%s dealloc", __FUNCTION__);
}
#pragma mark - Public
- (instancetype)initWithCellData:(TUIMessageCellData *)data
dataProvider:(TUIMessageDataProvider *)dataProvider
showReadStatusDisable:(BOOL)showReadStatusDisable
c2cReceiverName:(NSString *)name
c2cReceiverAvatar:(NSString *)avatarUrl {
self = [super init];
if (self) {
self.cellData = data;
self.dataProvider = dataProvider;
self.showReadStatusDisable = showReadStatusDisable;
self.c2cReceiverName = name;
self.c2cReceiverAvatarUrl = avatarUrl;
}
return self;
}
#pragma mark - Private
- (void)setupViews {
self.view.backgroundColor = TIMCommonDynamicColor(@"controller_bg_color", @"#F2F3F5");
[self setupTitleView];
[self setupMessageView];
if ([self isGroupMessageRead]) {
[self setupSelectView];
}
[self setupTableView];
}
- (void)layoutViews {
id content = [self content];
CGFloat messageBackViewHeight = 69 ;
if ([content isKindOfClass:UIImage.class]) {
messageBackViewHeight = 87;
}
[self.messageBackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.view.mas_safeAreaLayoutGuideTop);
make.leading.mas_equalTo(0);
make.height.mas_equalTo(messageBackViewHeight);
make.width.mas_equalTo(self.view);
}];
// content label may not exist when content is not text
if (self.contentLabel) {
[self.contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(16);
make.top.mas_equalTo(33);
make.height.mas_equalTo(24);
make.trailing.mas_equalTo(-16);
}];
}
UIView *readView = [self layoutSelectView:self.selectViewsDict[@(TUIMessageReadViewTagRead)] leftView:nil];
UIView *unreadView = [self layoutSelectView:self.selectViewsDict[@(TUIMessageReadViewTagUnread)] leftView:readView];
if (self.showReadStatusDisable) {
[self layoutSelectView:self.selectViewsDict[@(TUIMessageReadViewTagReadDisable)] leftView:unreadView];
}
self.tableView.mm_top(self.messageBackView.mm_maxY + 10 + (self.selectViewsDict.count > 0 ? 48 : 0))
.mm_left(0)
.mm_width(self.view.mm_w)
.mm_height(self.view.mm_h - _tableView.mm_y);
}
- (UIView *)layoutSelectView:(UIView *)view leftView:(UIView *)leftView {
NSInteger count = self.selectViewsDict.count;
if (count == 0) {
return nil;
}
[view mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(self.view).multipliedBy(1.0/count);
make.height.mas_equalTo(48);
if (leftView) {
make.leading.mas_equalTo(leftView.mas_trailing);
}
else {
make.leading.mas_equalTo(0);
}
make.top.mas_equalTo(self.messageBackView.mas_bottom).mas_offset(10);
}];
return view;
}
- (void)setupTitleView {
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = TIMCommonLocalizableString(TUIKitMessageReadDetail);
titleLabel.font = [UIFont systemFontOfSize:18.0];
titleLabel.textColor = TIMCommonDynamicColor(@"nav_title_text_color", @"#000000");
[titleLabel sizeToFit];
self.navigationItem.titleView = titleLabel;
}
- (void)setupMessageView {
UIView *messageBackView = [[UIView alloc] init];
messageBackView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
[self.view addSubview:messageBackView];
self.messageBackView = messageBackView;
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.text = self.cellData.senderName;
nameLabel.font = [UIFont systemFontOfSize:12.0];
nameLabel.textColor = TUIChatDynamicColor(@"chat_message_read_name_date_text_color", @"#999999");
nameLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
[messageBackView addSubview:nameLabel];
[nameLabel sizeToFit];
[nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(12);
make.leading.mas_equalTo(16);
make.width.mas_equalTo(nameLabel.frame.size.width);
make.height.mas_equalTo(nameLabel.frame.size.height);
}];
UILabel *dateLabel = [[UILabel alloc] init];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"MM-dd HH:mm"];
NSString *dateString = [formatter stringFromDate:self.cellData.innerMessage.timestamp];
dateLabel.text = dateString;
dateLabel.font = [UIFont systemFontOfSize:12];
dateLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
dateLabel.textColor = TUIChatDynamicColor(@"chat_message_read_name_date_text_color", @"#999999");
[messageBackView addSubview:dateLabel];
[dateLabel sizeToFit];
[dateLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(nameLabel);
make.leading.mas_equalTo(nameLabel.mas_trailing).mas_offset(12);
make.width.mas_equalTo(dateLabel.frame.size.width);
make.height.mas_equalTo(dateLabel.frame.size.height);
}];
id content = [self content];
if ([content isKindOfClass:NSString.class]) {
UILabel *contentLabel = [[UILabel alloc] init];
contentLabel.text = content;
contentLabel.font = [UIFont systemFontOfSize:16];
contentLabel.textColor = TUIChatDynamicColor(@"chat_input_text_color", @"#111111");
contentLabel.lineBreakMode = NSLineBreakByTruncatingTail;
contentLabel.textAlignment = isRTL()?NSTextAlignmentRight:NSTextAlignmentLeft;
self.contentLabel = contentLabel;
[messageBackView addSubview:contentLabel];
} else if ([content isKindOfClass:UIImage.class]) {
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = content;
[messageBackView addSubview:imageView];
[imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(33);
make.leading.mas_equalTo(16);
make.width.mas_equalTo([self scaledSizeOfImage:content].width);
make.height.mas_equalTo([self scaledSizeOfImage:content].height);
}];
}
}
- (void)setupSelectView {
NSMutableDictionary *dataDict = [self selectViewsData];
NSInteger count = dataDict.count;
NSMutableArray * selectViewsArray = [NSMutableArray arrayWithCapacity:2];
UIView *tmp = nil;
for (NSNumber *tag in dataDict) {
NSDictionary *data = dataDict[tag];
TUIMessageReadSelectView *selectView = [[TUIMessageReadSelectView alloc] initWithTitle:data[@"title"]
viewTag:[data[@"tag"] integerValue]
selected:[data[@"selected"] boolValue]];
selectView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
selectView.delegate = self;
[self.view addSubview:selectView];
[selectViewsArray addObject:selectView];
[self.selectViewsDict setObject:selectView forKey:data[@"tag"]];
selectView.mm_width(self.view.mm_w / count)
.mm_height(48)
.mm_left(tmp == nil ? 0 : tmp.mm_x + tmp.mm_w)
.mm_top(self.messageBackView.mm_y + self.messageBackView.mm_h + 10);
tmp = selectView;
}
}
- (void)setupTableView {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.contentInset = UIEdgeInsetsMake(0, 0, 8, 0);
_tableView.rowHeight = 56;
[_tableView setBackgroundColor:TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF")];
[self.view addSubview:_tableView];
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
[_tableView setTableFooterView:view];
_tableView.separatorStyle = [self isGroupMessageRead] ? UITableViewCellSeparatorStyleSingleLine : UITableViewCellSeparatorStyleNone;
_tableView.separatorInset = UIEdgeInsetsMake(0, 58, 0, 0);
[_tableView registerClass:[TUIMemberCell class] forCellReuseIdentifier:kMemberCellReuseId];
self.indicatorView.frame = CGRectMake(0, 0, self.view.bounds.size.width, TMessageController_Header_Height);
self.tableView.tableFooterView = self.indicatorView;
}
- (void)loadMembers {
[self getReadMembersWithCompletion:^(int code, NSString *desc, NSArray *members, BOOL isFinished) {
if (code != 0) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageReadGetReadMembersFail)];
NSLog(@"get read members failed, code: %d, desc: %@", code, desc);
return;
}
[self.tableView reloadData];
}];
[self getUnreadMembersWithCompletion:^(int code, NSString *desc, NSArray *members, BOOL isFinished) {
if (code != 0) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageReadGetUnreadMembersFail)];
NSLog(@"get unread members failed, code: %d, desc: %@", code, desc);
return;
}
[self.tableView reloadData];
}];
}
- (void)getReadMembersWithCompletion:(void (^)(int code, NSString *desc, NSArray *members, BOOL isFinished))completion {
@weakify(self);
[TUIMessageDataProvider getReadMembersOfMessage:self.cellData.innerMessage
filter:V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ
nextSeq:self.readSeq
completion:^(int code, NSString *_Nonnull desc, NSArray *_Nonnull members, NSUInteger nextSeq, BOOL isFinished) {
@strongify(self);
if (code != 0) {
if (completion) {
completion(code, desc, nil, NO);
}
return;
}
[self.readMembers addObjectsFromArray:members];
self.readSeq = isFinished ? -1 : nextSeq;
if (completion) {
completion(code, desc, members, isFinished);
}
}];
}
- (void)getUnreadMembersWithCompletion:(void (^)(int code, NSString *desc, NSArray *members, BOOL isFinished))completion {
@weakify(self);
[TUIMessageDataProvider getReadMembersOfMessage:self.cellData.innerMessage
filter:V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_UNREAD
nextSeq:self.unreadSeq
completion:^(int code, NSString *_Nonnull desc, NSArray *_Nonnull members, NSUInteger nextSeq, BOOL isFinished) {
@strongify(self);
if (code != 0) {
if (completion) {
completion(code, desc, nil, NO);
}
return;
}
[self.unreadMembers addObjectsFromArray:members];
self.unreadSeq = isFinished ? -1 : nextSeq;
if (completion) {
completion(code, desc, members, isFinished);
}
}];
}
- (void)getUserOrFriendProfileVCWithUserID:(NSString *)userID SuccBlock:(void (^)(UIViewController *vc))succ failBlock:(nullable V2TIMFail)fail {
NSDictionary *param = @{
TUICore_TUIContactObjectFactory_GetUserOrFriendProfileVCMethod_UserIDKey: userID ? : @"",
TUICore_TUIContactObjectFactory_GetUserOrFriendProfileVCMethod_SuccKey: succ ? : ^(UIViewController *vc){},
TUICore_TUIContactObjectFactory_GetUserOrFriendProfileVCMethod_FailKey: fail ? : ^(int code, NSString * desc){}
};
[TUICore createObject:TUICore_TUIContactObjectFactory key:TUICore_TUIContactObjectFactory_GetUserOrFriendProfileVCMethod param:param];
}
#pragma mark - UITableViewDataSource & UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self isGroupMessageRead]) {
if (indexPath.row >= [self members].count) {
return;
}
V2TIMGroupMemberInfo *member = [self members][indexPath.row];
[self getUserOrFriendProfileVCWithUserID:member.userID
SuccBlock:^(UIViewController *vc) {
[self.navigationController pushViewController:vc animated:YES];
}
failBlock:nil];
} else {
[self getUserOrFriendProfileVCWithUserID:self.cellData.innerMessage.userID
SuccBlock:^(UIViewController *vc) {
[self.navigationController pushViewController:vc animated:YES];
}
failBlock:nil];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self isGroupMessageRead] ? [self members].count : 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TUIMemberCell *cell = [tableView dequeueReusableCellWithIdentifier:kMemberCellReuseId forIndexPath:indexPath];
cell.changeColorWhenTouched = YES;
TUIMemberCellData *data;
if ([self isGroupMessageRead]) {
if (indexPath.row >= [self members].count) {
return nil;
}
V2TIMGroupMemberInfo *member = [self members][indexPath.row];
data = [[TUIMemberCellData alloc] initWithUserID:member.userID
nickName:member.nickName
friendRemark:member.friendRemark
nameCard:member.nameCard
avatarUrl:member.faceURL
detail:nil];
} else {
NSString *detail = nil;
BOOL isPeerRead = self.cellData.messageReceipt.isPeerRead;
detail = isPeerRead ? TIMCommonLocalizableString(TUIKitMessageReadC2CRead) : TIMCommonLocalizableString(TUIKitMessageReadC2CUnReadDetail);
data = [[TUIMemberCellData alloc] initWithUserID:self.cellData.innerMessage.userID
nickName:nil
friendRemark:self.c2cReceiverName
nameCard:nil
avatarUrl:self.c2cReceiverAvatarUrl
detail:detail];
}
[cell fillWithData:data];
return cell;
}
#pragma mark - UIScrollView
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y <= 0 || scrollView.contentOffset.y < scrollView.bounds.origin.y) {
return;
}
if (self.indicatorView.isAnimating) {
return;
}
[self.indicatorView startAnimating];
@weakify(self);
switch (self.selectedViewTag) {
case TUIMessageReadViewTagRead: {
[self getReadMembersWithCompletion:^(int code, NSString *desc, NSArray *members, BOOL isFinished) {
@strongify(self);
[self.indicatorView stopAnimating];
[self refreshTableView];
if (members != nil && members.count == 0) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageReadNoMoreData)];
[self.tableView setContentOffset:CGPointMake(0, scrollView.contentOffset.y - TMessageController_Header_Height) animated:YES];
}
}];
break;
}
case TUIMessageReadViewTagUnread: {
[self getUnreadMembersWithCompletion:^(int code, NSString *desc, NSArray *members, BOOL isFinished) {
@strongify(self);
[self.indicatorView stopAnimating];
[self refreshTableView];
if (members != nil && members.count == 0) {
[TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageReadNoMoreData)];
[self.tableView setContentOffset:CGPointMake(0, scrollView.contentOffset.y - TMessageController_Header_Height) animated:YES];
}
}];
break;
}
case TUIMessageReadViewTagReadDisable: {
break;
}
default: {
break;
}
}
}
- (void)refreshTableView {
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
}
#pragma mark - TUIMessageReadSelectViewDelegate
- (void)messageReadSelectView:(TUIMessageReadSelectView *)view didSelectItemTag:(TUIMessageReadViewTag)tag {
for (TUIMessageReadSelectView *view in self.selectViewsDict.allValues) {
view.selected = view.tag == tag;
}
self.selectedViewTag = tag;
[self.tableView reloadData];
}
#pragma mark - Getter
- (id)content {
V2TIMMessage *msg = self.cellData.innerMessage;
NSMutableString *content = [NSMutableString stringWithString:[TUIMessageDataProvider getDisplayString:msg]];
switch (msg.elemType) {
case V2TIM_ELEM_TYPE_IMAGE: {
TUIImageMessageCellData *data = (TUIImageMessageCellData *)self.cellData;
return data.thumbImage;
}
case V2TIM_ELEM_TYPE_VIDEO: {
TUIVideoMessageCellData *data = (TUIVideoMessageCellData *)self.cellData;
return data.thumbImage;
}
case V2TIM_ELEM_TYPE_FILE: {
[content appendString:msg.fileElem.filename];
break;
}
case V2TIM_ELEM_TYPE_CUSTOM: {
break;
}
default: {
break;
}
}
return content;
}
- (CGSize)scaledSizeOfImage:(UIImage *)image {
NSSet *portraitOrientations =
[NSSet setWithArray:@[ @(UIImageOrientationLeft), @(UIImageOrientationRight), @(UIImageOrientationLeftMirrored), @(UIImageOrientationRightMirrored) ]];
UIImageOrientation orientation = image.imageOrientation;
CGFloat width = CGImageGetWidth(image.CGImage);
CGFloat height = CGImageGetHeight(image.CGImage);
// Height is fixed at 42.0, and width is proportionally scaled.
if ([portraitOrientations containsObject:@(orientation)]) {
// UIImage is stored in memory in a fixed size, like 1280 * 720.
// So we should adapt its size manually according to the direction.
return CGSizeMake(42.0 * height / width, 42.0);
} else {
return CGSizeMake(42.0 * width / height, 42.0);
}
}
- (NSMutableDictionary *)selectViewsData {
NSMutableDictionary *readViews = [NSMutableDictionary dictionaryWithDictionary:@{
@(TUIMessageReadViewTagRead) : @{
@"tag" : @(TUIMessageReadViewTagRead),
@"title" :
[NSString stringWithFormat:@"%ld %@", (long)self.cellData.messageReceipt.readCount, TIMCommonLocalizableString(TUIKitMessageReadPartRead)],
@"selected" : @(YES)
},
@(TUIMessageReadViewTagUnread) : @{
@"tag" : @(TUIMessageReadViewTagUnread),
@"title" :
[NSString stringWithFormat:@"%ld %@", (long)self.cellData.messageReceipt.unreadCount, TIMCommonLocalizableString(TUIKitMessageReadPartUnread)],
@"selected" : @(NO)
},
}];
if (self.showReadStatusDisable) {
[readViews
setObject:@{@"tag" : @(TUIMessageReadViewTagReadDisable), @"title" : TIMCommonLocalizableString(TUIKitMessageReadPartDisable), @"selected" : @(NO)}
forKey:@(TUIMessageReadViewTagReadDisable)];
}
return readViews;
}
- (NSArray *)members {
switch (self.selectedViewTag) {
case TUIMessageReadViewTagRead: {
return self.readMembers;
}
case TUIMessageReadViewTagUnread: {
return self.unreadMembers;
}
case TUIMessageReadViewTagReadDisable: {
return @[];
}
default: {
return @[];
}
}
}
- (NSMutableDictionary *)selectViewsDict {
if (!_selectViewsDict) {
_selectViewsDict = [[NSMutableDictionary alloc] init];
}
return _selectViewsDict;
}
- (NSMutableArray *)readMembers {
if (!_readMembers) {
_readMembers = [[NSMutableArray alloc] init];
}
return _readMembers;
}
- (NSMutableArray *)unreadMembers {
if (!_unreadMembers) {
_unreadMembers = [[NSMutableArray alloc] init];
}
return _unreadMembers;
}
- (UIActivityIndicatorView *)indicatorView {
if (_indicatorView == nil) {
_indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_indicatorView.hidesWhenStopped = YES;
}
return _indicatorView;
}
- (BOOL)isGroupMessageRead {
return self.cellData.innerMessage.groupID.length > 0;
}
@end

View File

@@ -0,0 +1,31 @@
//
// TUIRepliesDetailViewController.h
// TUIChat
//
// Created by wyl on 2022/4/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <UIKit/UIKit.h>
#import "TUIBaseMessageControllerDelegate.h"
#import "TUIChatConversationModel.h"
#import "TUIInputController.h"
@class TUIMessageDataProvider;
NS_ASSUME_NONNULL_BEGIN
@interface TUIRepliesDetailViewController : UIViewController
- (instancetype)initWithCellData:(TUIMessageCellData *)data conversationData:(TUIChatConversationModel *)conversationData;
@property(nonatomic, weak) id<TUIBaseMessageControllerDelegate> delegate;
@property(nonatomic, strong) V2TIMMergerElem *mergerElem;
@property(nonatomic, copy) dispatch_block_t willCloseCallback;
@property(nonatomic, strong) TUIInputController *inputController;
@property(nonatomic, strong) TUIMessageDataProvider *parentPageDataProvider;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,755 @@
//
// TUIRepliesDetailViewController.m
// TUIChat
//
// Created by wyl on 2022/4/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIRepliesDetailViewController.h"
#import <TIMCommon/TUIMessageCell.h>
#import "TUIChatDataProvider.h"
#import "TUIMessageDataProvider.h"
#import "TUITextMessageCell.h"
#import "TUITextMessageCellData.h"
#import "TUIChatConfig.h"
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUISystemMessageCell.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIFaceMessageCell.h"
#import "TUIFileMessageCell.h"
#import "TUIFileViewController.h"
#import "TUIImageMessageCell.h"
#import "TUIJoinGroupMessageCell.h"
#import "TUILinkCell.h"
#import "TUIMediaView.h"
#import "TUIMergeMessageCell.h"
#import "TUIMergeMessageListController.h"
#import "TUIMessageDataProvider.h"
#import "TUIReferenceMessageCell.h"
#import "TUIReplyMessageCell.h"
#import "TUIReplyMessageCellData.h"
#import "TUITextMessageCell.h"
#import "TUIVideoMessageCell.h"
#import "TUIVoiceMessageCell.h"
#import "TUIMessageCellConfig.h"
@interface TUIRepliesDetailViewController () <TUIInputControllerDelegate,
UITableViewDelegate,
UITableViewDataSource,
TUIMessageBaseDataProviderDataSource,
TUIMessageCellDelegate,
TUINotificationProtocol,
V2TIMAdvancedMsgListener>
@property(nonatomic, strong) TUIMessageCellData *cellData;
@property(nonatomic, strong) TUIMessageDataProvider *msgDataProvider;
@property(nonatomic, strong) UIView *headerView;
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) NSArray<V2TIMMessage *> *imMsgs;
@property(nonatomic, strong) NSMutableArray<TUIMessageCellData *> *uiMsgs;
@property(nonatomic, assign) BOOL responseKeyboard;
@property(nonatomic, strong) TUIChatConversationModel *conversationData;
@property(nonatomic, strong) TUIMessageCellConfig *messageCellConfig;
@property(nonatomic, strong) TUIMessageCellLayout *originCellLayout;
@property TMsgDirection direction;
@property(nonatomic, assign) BOOL showName;
@property(nonatomic, assign) BOOL showMessageTime;
@property(nonatomic) BOOL isMsgNeedReadReceipt;
@end
@implementation TUIRepliesDetailViewController
- (instancetype)initWithCellData:(TUIMessageCellData *)data conversationData:(TUIChatConversationModel *)conversationData {
self = [super init];
self.cellData = data;
[self setConversation:conversationData];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupViews];
[self setupInputViewController];
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
[TUICore registerEvent:TUICore_TUIPluginNotify
subKey:TUICore_TUIPluginNotify_DidChangePluginViewSubKey
object:self];
}
- (void)viewWillAppear:(BOOL)animated {
[self updateRootMsg];
[self applyData];
[self updateTableViewConstraint];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.responseKeyboard = YES;
self.isMsgNeedReadReceipt = YES;
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.responseKeyboard = NO;
[self revertRootMsg];
if (self.willCloseCallback) {
self.willCloseCallback();
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if (self.inputController.status == Input_Status_Input || self.inputController.status == Input_Status_Input_Keyboard) {
CGPoint offset = self.tableView.contentOffset;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.responseKeyboard = YES;
[UIApplication.sharedApplication.keyWindow endEditing:YES];
[strongSelf inputController:strongSelf.inputController didChangeHeight:CGRectGetMaxY(strongSelf.inputController.inputBar.frame) + Bottom_SafeHeight];
[strongSelf.tableView setContentOffset:offset];
});
}
}
- (void)dealloc {
NSLog(@"%s dealloc", __FUNCTION__);
[TUICore unRegisterEventByObject:self];
}
- (void)applyData {
NSArray *messageModifyReplies = self.cellData.messageModifyReplies;
NSMutableArray *msgIDArray = [NSMutableArray array];
if (messageModifyReplies.count > 0) {
for (NSDictionary *dic in messageModifyReplies) {
if (dic) {
NSString *messageID = dic[@"messageID"];
if (IS_NOT_EMPTY_NSSTRING(messageID)) {
[msgIDArray addObject:messageID];
}
}
}
}
// When the only reply is retracted, go back to the previous controller
if (msgIDArray.count <= 0) {
[self.navigationController popViewControllerAnimated:YES];
}
__weak typeof(self) weakSelf = self;
[TUIChatDataProvider findMessages:msgIDArray
callback:^(BOOL succ, NSString *_Nonnull error_message, NSArray *_Nonnull msgs) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (succ) {
if (msgs.count > 0) {
strongSelf.imMsgs = msgs;
strongSelf.uiMsgs = [self transUIMsgFromIMMsg:msgs];
dispatch_async(dispatch_get_main_queue(), ^{
if (strongSelf.uiMsgs.count != 0) {
[strongSelf.tableView reloadData];
[strongSelf.tableView layoutIfNeeded];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[strongSelf scrollToBottom:NO];
});
}
});
}
}
}];
}
- (void)updateTableViewConstraint {
CGFloat textViewHeight = TUIChatConfig.defaultConfig.enableMainPageInputBar? TTextView_Height:0;
CGFloat height = textViewHeight + Bottom_SafeHeight;
CGRect msgFrame = self.tableView.frame;
msgFrame.size.height = self.view.frame.size.height - height;
self.tableView.frame = msgFrame;
}
- (void)setupViews {
self.title = TIMCommonLocalizableString(TUIKitRepliesDetailTitle);
self.view.backgroundColor = TUIChatDynamicColor(@"chat_controller_bg_color", @"#FFFFFF");
self.tableView.scrollsToTop = NO;
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
self.tableView.contentInset = UIEdgeInsetsMake(5, 0, 0, 0);
[self.messageCellConfig bindTableView:self.tableView];
}
- (void)setupInputViewController {
_inputController = [[TUIInputController alloc] init];
_inputController.delegate = self;
_inputController.view.frame =
CGRectMake(0, self.view.frame.size.height - TTextView_Height - Bottom_SafeHeight, self.view.frame.size.width, TTextView_Height + Bottom_SafeHeight);
_inputController.view.autoresizingMask = UIViewAutoresizingFlexibleTopMargin;
_inputController.inputBar.isFromReplyPage = YES;
[self addChildViewController:_inputController];
[self.view addSubview:_inputController.view];
TUIFaceGroup *group = TIMConfig.defaultConfig.faceGroups[0];
[_inputController.faceSegementScrollView setItems:(id) @[ group ] delegate:(id)_inputController];
TUIMenuCellData *data = [[TUIMenuCellData alloc] init];
data.path = group.menuPath;
data.isSelected = YES;
[_inputController.menuView setData:(id) @[ data ]];
_inputController.view.hidden = !TUIChatConfig.defaultConfig.enableMainPageInputBar;
CGFloat margin = 0;
CGFloat padding = 10;
_inputController.inputBar.inputTextView.frame =
CGRectMake(margin, _inputController.inputBar.inputTextView.frame.origin.y,
_inputController.inputBar.frame.size.width - _inputController.inputBar.faceButton.frame.size.width - margin * 2 - padding,
_inputController.inputBar.inputTextView.frame.size.height);
_inputController.inputBar.faceButton.frame =
CGRectMake(_inputController.inputBar.frame.size.width - _inputController.inputBar.faceButton.frame.size.width - margin,
_inputController.inputBar.faceButton.frame.origin.y, _inputController.inputBar.faceButton.frame.size.width,
_inputController.inputBar.faceButton.frame.size.height);
if (_inputController.inputBar.micButton) {
_inputController.inputBar.micButton.alpha = 0;
}
if (_inputController.inputBar.moreButton) {
_inputController.inputBar.moreButton.alpha = 0;
}
[_inputController.inputBar defaultLayout];
}
- (void)updateRootMsg {
self.originCellLayout = self.cellData.cellLayout;
self.direction = self.cellData.direction;
self.showName = self.cellData.showName;
self.showMessageTime = self.cellData.showMessageTime;
TUIMessageCellData *data = self.cellData;
TUIMessageCellLayout *layout = TUIMessageCellLayout.incommingMessageLayout;
if ([data isKindOfClass:TUITextMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
}
if ([data isKindOfClass:TUIReferenceMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
}
if ([data isKindOfClass:TUIVoiceMessageCellData.class]) {
layout = [TUIMessageCellLayout incommingVoiceMessageLayout];
}
self.cellData.cellLayout = layout;
self.cellData.direction = MsgDirectionIncoming;
self.cellData.showName = YES;
self.cellData.showMessageModifyReplies = NO;
self.cellData.showMessageTime = YES;
}
- (void)revertRootMsg {
self.cellData.cellLayout = self.originCellLayout;
self.cellData.direction = self.direction;
self.cellData.showName = self.showName;
self.cellData.showMessageModifyReplies = YES;
self.cellData.showMessageTime = self.showMessageTime;
}
- (NSMutableArray *)transUIMsgFromIMMsg:(NSArray *)msgs {
NSMutableArray *uiMsgs = [NSMutableArray array];
for (NSInteger k = 0; k < msgs.count; k++) {
V2TIMMessage *msg = msgs[k];
TUIMessageCellData *data = [TUITextMessageCellData getCellData:msg];
TUIMessageCellLayout *layout = TUIMessageCellLayout.incommingMessageLayout;
if ([data isKindOfClass:TUITextMessageCellData.class]) {
layout = TUIMessageCellLayout.incommingTextMessageLayout;
}
data.cellLayout = layout;
data.direction = MsgDirectionIncoming;
data.showName = YES;
if (data) {
data.innerMessage = msg;
[uiMsgs addObject:data];
}
}
NSArray *sortedArray = [uiMsgs sortedArrayUsingComparator:^NSComparisonResult(TUIMessageCellData *obj1, TUIMessageCellData *obj2) {
if ([obj1.innerMessage.timestamp timeIntervalSince1970] == [obj2.innerMessage.timestamp timeIntervalSince1970]) {
return obj1.innerMessage.seq > obj2.innerMessage.seq;
} else {
return [obj1.innerMessage.timestamp compare:obj2.innerMessage.timestamp];
}
}];
uiMsgs = [NSMutableArray arrayWithArray:sortedArray];
return uiMsgs;
}
#pragma mark - tableView
- (UITableView *)tableView {
if (!_tableView) {
CGRect rect = self.view.bounds;
_tableView = [[UITableView alloc] initWithFrame:rect style:UITableViewStylePlain];
if (@available(iOS 15.0, *)) {
_tableView.sectionHeaderTopPadding = 0;
}
[self.view addSubview:_tableView];
}
return _tableView;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section == 1) {
return 20;
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if (section == 1) {
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor clearColor];
return view;
}
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (section == 0) {
return 0.5;
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
if (section == 0) {
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, 0.5)];
line.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
return line;
}
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 0) {
return 1;
}
return _uiMsgs.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
return [self.messageCellConfig getHeightFromMessageCellData:self.cellData];
} else {
if (indexPath.row < self.uiMsgs.count) {
TUIMessageCellData *cellData = self.uiMsgs[indexPath.row];
return [self.messageCellConfig getHeightFromMessageCellData:cellData];
} else {
return 0;
}
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
TUIMessageCell *cell = nil;
TUIMessageCellData *data = self.cellData;
cell = [tableView dequeueReusableCellWithIdentifier:data.reuseId forIndexPath:indexPath];
cell.delegate = self;
[cell fillWithData:data];
[cell notifyBottomContainerReadyOfData:nil];
return cell;
}
TUIMessageCellData *data = _uiMsgs[indexPath.row];
data.showMessageTime = YES;
data.showCheckBox = NO;
TUIMessageCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:data.reuseId forIndexPath:indexPath];
[cell fillWithData:_uiMsgs[indexPath.row]];
cell.delegate = self;
[cell notifyBottomContainerReadyOfData:nil];
if ([cell isKindOfClass:TUIBubbleMessageCell.class]) {
TUIBubbleMessageCell *bubbleCell = (TUIBubbleMessageCell *)cell;
if (bubbleCell.bubbleView) {
bubbleCell.bubbleView.image = nil;
}
}
return cell;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[self.inputController reset];
}
#pragma mark - TUIInputControllerDelegate
- (void)inputController:(TUIInputController *)inputController didChangeHeight:(CGFloat)height {
if (!self.responseKeyboard) {
return;
}
if (self.inputController.replyData == nil) {
[self onRelyMessage:self.cellData];
}
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect msgFrame = self.tableView.frame;
msgFrame.size.height = self.view.frame.size.height - height;
self.tableView.frame = msgFrame;
CGRect inputFrame = self.inputController.view.frame;
inputFrame.origin.y = msgFrame.origin.y + msgFrame.size.height;
inputFrame.size.height = height;
self.inputController.view.frame = inputFrame;
[self scrollToBottom:NO];
}
completion:nil];
}
- (void)inputController:(TUIInputController *)inputController didSendMessage:(V2TIMMessage *)msg {
[self sendMessage:msg];
}
- (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell {
cell.disableDefaultSelectAction = NO;
if (cell.disableDefaultSelectAction) {
return;
}
}
- (void)sendMessage:(V2TIMMessage *)message {
TUIMessageCellData *cellData = nil;
if (!cellData) {
cellData = [TUIMessageDataProvider getCellData:message];
}
if (cellData) {
cellData.innerMessage.needReadReceipt = self.isMsgNeedReadReceipt;
[self sendUIMessage:cellData];
}
}
- (void)sendUIMessage:(TUIMessageCellData *)cellData {
@weakify(self);
[self.parentPageDataProvider sendUIMsg:cellData
toConversation:self.conversationData
willSendBlock:^(BOOL isReSend, TUIMessageCellData *_Nonnull dateUIMsg) {
@strongify(self);
int delay = 1;
if ([cellData isKindOfClass:[TUIImageMessageCellData class]]) {
delay = 0;
}
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
if (cellData.status == Msg_Status_Sending) {
[self changeMsg:cellData status:Msg_Status_Sending_2];
}
});
}
SuccBlock:^{
@strongify(self);
[self changeMsg:cellData status:Msg_Status_Succ];
[self scrollToBottom:YES];
}
FailBlock:^(int code, NSString *desc) {
@strongify(self);
[TUITool makeToastError:code msg:desc];
[self changeMsg:cellData status:Msg_Status_Fail];
}];
}
- (void)scrollToBottom:(BOOL)animated {
if (self.uiMsgs.count > 0) {
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.uiMsgs.count - 1 inSection:1]
atScrollPosition:UITableViewScrollPositionBottom
animated:animated];
}
}
- (void)changeMsg:(TUIMessageCellData *)msg status:(TMsgStatus)status {
msg.status = status;
NSInteger index = [self.uiMsgs indexOfObject:msg];
if ([self.tableView numberOfRowsInSection:0] > index) {
TUIMessageCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
[cell fillWithData:msg];
} else {
NSLog(@"lack of cell");
}
[[NSNotificationCenter defaultCenter] postNotificationName:@"kTUINotifyMessageStatusChanged"
object:nil
userInfo:@{
@"msg" : msg,
@"status" : [NSNumber numberWithUnsignedInteger:status],
@"msgSender" : self,
}];
}
#pragma mark - Message reply
- (void)onRelyMessage:(nonnull TUIMessageCellData *)data {
NSString *desc = @"";
desc = [self replyReferenceMessageDesc:data];
TUIReplyPreviewData *replyData = [[TUIReplyPreviewData alloc] init];
replyData.msgID = data.msgID;
replyData.msgAbstract = desc;
replyData.sender = data.senderName;
replyData.type = (NSInteger)data.innerMessage.elemType;
replyData.originMessage = data.innerMessage;
self.inputController.replyData = replyData;
}
- (NSString *)replyReferenceMessageDesc:(TUIMessageCellData *)data {
NSString *desc = @"";
if (data.innerMessage.elemType == V2TIM_ELEM_TYPE_FILE) {
desc = data.innerMessage.fileElem.filename;
} else if (data.innerMessage.elemType == V2TIM_ELEM_TYPE_MERGER) {
desc = data.innerMessage.mergerElem.title;
} else if (data.innerMessage.elemType == V2TIM_ELEM_TYPE_CUSTOM) {
desc = [TUIMessageDataProvider getDisplayString:data.innerMessage];
} else if (data.innerMessage.elemType == V2TIM_ELEM_TYPE_TEXT) {
desc = data.innerMessage.textElem.text;
}
return desc;
}
#pragma mark - TUIMessageCellDelegate
- (void)onSelectMessage:(TUIMessageCell *)cell {
if (TUIChatConfig.defaultConfig.eventConfig.chatEventListener &&
[TUIChatConfig.defaultConfig.eventConfig.chatEventListener respondsToSelector:@selector(onMessageClicked:messageCellData:)]) {
BOOL result = [TUIChatConfig.defaultConfig.eventConfig.chatEventListener onMessageClicked:cell messageCellData:cell.messageData];
if (result) {
return;
}
}
if ([cell isKindOfClass:[TUIImageMessageCell class]]) {
[self showImageMessage:(TUIImageMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIVoiceMessageCell class]]) {
[self playVoiceMessage:(TUIVoiceMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIVideoMessageCell class]]) {
[self showVideoMessage:(TUIVideoMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIFileMessageCell class]]) {
[self showFileMessage:(TUIFileMessageCell *)cell];
}
if ([cell isKindOfClass:[TUIMergeMessageCell class]]) {
TUIMergeMessageListController *mergeVc = [[TUIMergeMessageListController alloc] init];
mergeVc.mergerElem = [(TUIMergeMessageCell *)cell mergeData].mergerElem;
mergeVc.delegate = self.delegate;
[self.navigationController pushViewController:mergeVc animated:YES];
}
if ([cell isKindOfClass:[TUILinkCell class]]) {
[self showLinkMessage:(TUILinkCell *)cell];
}
// if ([cell isKindOfClass:[TUIReplyMessageCell class]]) {
// [self showReplyMessage:(TUIReplyMessageCell *)cell];
// }
// if ([cell isKindOfClass:[TUIReferenceMessageCell class]]) {
// [self showReplyMessage:(TUIReplyMessageCell *)cell];
// }
if ([self.delegate respondsToSelector:@selector(messageController:onSelectMessageContent:)]) {
[self.delegate messageController:nil onSelectMessageContent:cell];
}
}
#pragma mark - V2TIMAdvancedMsgListener
- (void)onRecvNewMessage:(V2TIMMessage *)msg {
V2TIMMessage *imMsg = msg;
if (imMsg == nil || ![imMsg isKindOfClass:V2TIMMessage.class]) {
return;
}
if ([imMsg.msgID isEqualToString:self.cellData.msgID] ) {
TUIMessageCellData *cellData = [TUIMessageDataProvider getCellData:imMsg];
self.cellData.messageModifyReplies = cellData.messageModifyReplies;
[self applyData];
}
}
- (void)onRecvMessageModified:(V2TIMMessage *)msg {
V2TIMMessage *imMsg = msg;
if (imMsg == nil || ![imMsg isKindOfClass:V2TIMMessage.class]) {
return;
}
if ([imMsg.msgID isEqualToString:self.cellData.msgID] ) {
TUIMessageCellData *cellData = [TUIMessageDataProvider getCellData:imMsg];
self.cellData.messageModifyReplies = cellData.messageModifyReplies;
[self applyData];
}
}
#pragma mark - dataProviderDataChange
- (void)dataProviderDataSourceWillChange:(TUIMessageDataProvider *)dataProvider {
}
- (void)dataProviderDataSourceChange:(TUIMessageDataProvider *)dataProvider
withType:(TUIMessageBaseDataProviderDataSourceChangeType)type
atIndex:(NSUInteger)index
animation:(BOOL)animation {
}
- (void)dataProviderDataSourceDidChange:(TUIMessageDataProvider *)dataProvider {
}
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider onRemoveHeightCache:(TUIMessageCellData *)cellData {
if (cellData) {
[self.messageCellConfig removeHeightCacheOfMessageCellData:cellData];
}
}
#pragma mark - action
- (void)showImageMessage:(TUIImageMessageCell *)cell {
CGRect frame = [cell.thumb convertRect:cell.thumb.bounds toView:[UIApplication sharedApplication].delegate.window];
TUIMediaView *mediaView = [[TUIMediaView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, Screen_Height)];
[mediaView setThumb:cell.thumb frame:frame];
[mediaView setCurMessage:cell.messageData.innerMessage allMessages:@[ self.cellData.innerMessage ]];
[[UIApplication sharedApplication].keyWindow addSubview:mediaView];
}
- (void)playVoiceMessage:(TUIVoiceMessageCell *)cell {
TUIVoiceMessageCellData *uiMsg = (TUIVoiceMessageCellData *)self.cellData;
if (uiMsg == cell.voiceData) {
[uiMsg playVoiceMessage];
cell.voiceReadPoint.hidden = YES;
} else {
[uiMsg stopVoiceMessage];
}
}
- (void)showVideoMessage:(TUIVideoMessageCell *)cell {
CGRect frame = [cell.thumb convertRect:cell.thumb.bounds toView:[UIApplication sharedApplication].delegate.window];
TUIMediaView *mediaView = [[TUIMediaView alloc] initWithFrame:CGRectMake(0, 0, Screen_Width, Screen_Height)];
[mediaView setThumb:cell.thumb frame:frame];
[mediaView setCurMessage:cell.messageData.innerMessage allMessages:@[ self.cellData.innerMessage ]];
__weak typeof(self) weakSelf = self;
mediaView.onClose = ^{
[weakSelf.tableView reloadData];
};
[[UIApplication sharedApplication].keyWindow addSubview:mediaView];
}
- (void)showFileMessage:(TUIFileMessageCell *)cell {
TUIFileViewController *file = [[TUIFileViewController alloc] init];
file.data = [cell fileData];
[self.navigationController pushViewController:file animated:YES];
}
- (void)showLinkMessage:(TUILinkCell *)cell {
TUILinkCellData *cellData = cell.customData;
if (cellData.link) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:cellData.link]];
}
}
- (void)setConversation:(TUIChatConversationModel *)conversationData {
self.conversationData = conversationData;
if (!self.msgDataProvider) {
self.msgDataProvider = [[TUIMessageDataProvider alloc] initWithConversationModel:conversationData];
self.msgDataProvider.dataSource = self;
}
[self loadMessage];
}
- (void)loadMessage {
if (self.msgDataProvider.isLoadingData || self.msgDataProvider.isNoMoreMsg) {
return;
}
[self.msgDataProvider
loadMessageSucceedBlock:^(BOOL isFirstLoad, BOOL isNoMoreMsg, NSArray<TUIMessageCellData *> *_Nonnull newMsgs) {
}
FailBlock:^(int code, NSString *desc) {
[TUITool makeToastError:code msg:desc];
}];
}
#pragma mark - TUINotificationProtocol
- (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(id)anObject param:(NSDictionary *)param {
if ([key isEqualToString:TUICore_TUIPluginNotify] &&
[subKey isEqualToString:TUICore_TUIPluginNotify_DidChangePluginViewSubKey]) {
TUIMessageCellData *data = param[TUICore_TUIPluginNotify_DidChangePluginViewSubKey_Data];
NSInteger section = 1;
if ([data.msgID isEqualToString:self.cellData.msgID] ) {
//root section
section = 0;
}
[self.messageCellConfig removeHeightCacheOfMessageCellData:data];
[self reloadAndScrollToBottomOfMessage:data.innerMessage.msgID section:section];
}
}
- (void)reloadAndScrollToBottomOfMessage:(NSString *)messageID section:(NSInteger)section {
// Dispatch the task to RunLoop to ensure that they are executed after the UITableView refresh is complete.
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadCellOfMessage:messageID section:section];
dispatch_async(dispatch_get_main_queue(), ^{
[self scrollCellToBottomOfMessage:messageID section:section];
});
});
}
- (void)reloadCellOfMessage:(NSString *)messageID section:(NSInteger)section {
NSIndexPath *indexPath = [self indexPathOfMessage:messageID section:section];
// Disable animation when loading to avoid cell jumping.
if (indexPath == nil) {
return;
}
[UIView performWithoutAnimation:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationNone];
});
}];
}
- (void)scrollCellToBottomOfMessage:(NSString *)messageID section:(NSInteger)section {
NSIndexPath *indexPath = [self indexPathOfMessage:messageID section:section];
// Scroll the tableView only if the bottom of the cell is invisible.
CGRect cellRect = [self.tableView rectForRowAtIndexPath:indexPath];
CGRect tableViewRect = self.tableView.bounds;
BOOL isBottomInvisible = cellRect.origin.y < CGRectGetMaxY(tableViewRect) && CGRectGetMaxY(cellRect) > CGRectGetMaxY(tableViewRect);
if (isBottomInvisible) {
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
- (NSIndexPath *)indexPathOfMessage:(NSString *)messageID section:(NSInteger)section {
if (section == 0) {
return [NSIndexPath indexPathForRow:0 inSection:section];
} else {
for (int i = 0; i < self.uiMsgs.count; i++) {
TUIMessageCellData *data = self.uiMsgs[i];
if ([data.innerMessage.msgID isEqualToString:messageID]) {
return [NSIndexPath indexPathForRow:i inSection:section];
}
}
}
return nil;
}
- (TUIMessageCellConfig *)messageCellConfig {
if (_messageCellConfig == nil) {
_messageCellConfig = [[TUIMessageCellConfig alloc] init];
}
return _messageCellConfig;
}
@end

View File

@@ -0,0 +1,356 @@
//
// TUIChatConfig_Classic.h
// TUIChat
//
// Created by Tencent on 2024/7/16.
// Copyright © 2024 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
#import "UIAlertController+TUICustomStyle.h"
#import "TUIChatShortcutMenuView.h"
#import "TUIInputMoreCellData.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TUIMessageCellLayout.h>
#import "TUIChatConfig.h"
NS_ASSUME_NONNULL_BEGIN
@class TUIMessageCellData;
typedef NS_ENUM(NSInteger, TUIAvatarStyle_Classic) {
TUIAvatarStyleRectangle,
TUIAvatarStyleCircle,
TUIAvatarStyleRoundedRectangle,
};
typedef NS_OPTIONS(NSInteger, TUIChatItemWhenLongPressMessage_Classic) {
TUIChatItemWhenLongPressMessage_Classic_None = 0,
TUIChatItemWhenLongPressMessage_Classic_Reply = 1 << 0,
TUIChatItemWhenLongPressMessage_Classic_EmojiReaction = 1 << 1,
TUIChatItemWhenLongPressMessage_Classic_Quote = 1 << 2,
TUIChatItemWhenLongPressMessage_Classic_Pin = 1 << 3,
TUIChatItemWhenLongPressMessage_Classic_Recall = 1 << 4,
TUIChatItemWhenLongPressMessage_Classic_Translate = 1 << 5,
TUIChatItemWhenLongPressMessage_Classic_Convert = 1 << 6,
TUIChatItemWhenLongPressMessage_Classic_Forward = 1 << 7,
TUIChatItemWhenLongPressMessage_Classic_Select = 1 << 8,
TUIChatItemWhenLongPressMessage_Classic_Copy = 1 << 9,
TUIChatItemWhenLongPressMessage_Classic_Delete = 1 << 10,
};
@protocol TUIChatConfigDelegate_Classic <NSObject>
/**
* Tells the delegate a user's avatar in the chat list is clicked.
* Returning YES indicates this event has been intercepted, and Chat will not process it further.
* Returning NO indicates this event is not intercepted, and Chat will continue to process it.
*/
- (BOOL)onUserAvatarClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
/**
* Tells the delegate a user's avatar in the chat list is long pressed.
* Returning YES indicates that this event has been intercepted, and Chat will not process it further.
* Returning NO indicates that this event is not intercepted, and Chat will continue to process it.
*/
- (BOOL)onUserAvatarLongPressed:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
/**
* Tells the delegate a message in the chat list is clicked.
* Returning YES indicates that this event has been intercepted, and Chat will not process it further.
* Returning NO indicates that this event is not intercepted, and Chat will continue to process it.
*/
- (BOOL)onMessageClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
/**
* Tells the delegate a message in the chat list is long pressed.
* Returning YES indicates that this event has been intercepted, and Chat will not process it further.
* Returning NO indicates that this event is not intercepted, and Chat will continue to process it.
*/
- (BOOL)onMessageLongPressed:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
@end
@interface TUIChatConfig_Classic : NSObject
+ (TUIChatConfig_Classic *)sharedConfig;
/**
* The object that acts as the delegate of the TUIChatMessageConfig_Minimalist.
*/
@property (nonatomic, weak) id<TUIChatConfigDelegate_Classic> delegate;
/**
* Customize the backgroud color of message list interface.
* This configuration takes effect in all message list interfaces.
*/
@property (nonatomic, strong) UIColor *backgroudColor;
/**
* Customize the backgroud image of message list interface.
* This configuration takes effect in all message list interfaces.
*/
@property (nonatomic, strong) UIImage *backgroudImage;
/**
* Customize the style of avatar.
* The default value is TUIAvatarStyleCircle.
* This configuration takes effect in all avatars.
*/
@property (nonatomic, assign) TUIAvatarStyle_Classic avatarStyle;
/**
* Customize the corner radius of the avatar.
* This configuration takes effect in all avatars.
*/
@property (nonatomic, assign) CGFloat avatarCornerRadius;
/**
* Display the group avatar in the nine-square grid style.
* The default value is YES.
* This configuration takes effect in all groups.
*/
@property (nonatomic, assign) BOOL enableGroupGridAvatar;
/**
* Default avatar image.
* This configuration takes effect in all avatars.
*/
@property (nonatomic, strong) UIImage *defaultAvatarImage;
/**
* Enable the display "Alice is typing..." on one-to-one chat interface.
* The default value is YES.
* This configuration takes effect in all one-to-one chat message list interfaces.
*/
@property (nonatomic, assign) BOOL enableTypingIndicator;
/**
* When sending a message, set this flag to require message read receipt.
* The default value is NO.
* This configuration takes effect in all chat message list interfaces.
*/
@property (nonatomic, assign) BOOL isMessageReadReceiptNeeded;
/**
* Hide the "Video Call" button in the message list header.
* The default value is NO.
*/
@property (nonatomic, assign) BOOL hideVideoCallButton;
/**
* Hide the "Audio Call" button in the message list header.
* The default value is NO.
*/
@property (nonatomic, assign) BOOL hideAudioCallButton;
/**
* Turn on audio and video call floating windows,
* The default value is YES.
*/
@property (nonatomic, assign) BOOL enableFloatWindowForCall;
/**
* Enable multi-terminal login function for audio and video calls
* The default value is NO.
*/
@property (nonatomic, assign) BOOL enableMultiDeviceForCall;
/**
* Set this parameter when the sender sends a message, and the receiver will not update the unread count after receiving the message.
* The default value is NO.
*/
@property (nonatomic, assign) BOOL isExcludedFromUnreadCount;
/**
* Set this parameter when the sender sends a message, and the receiver will not update the last message of the conversation after receiving the message.
* The default value is NO.
*/
@property (nonatomic, assign) BOOL isExcludedFromLastMessage;
/**
* Time interval within which a message can be recalled after being sent.
* The default value is 120 seconds.
* If you want to adjust this configuration, please modify the setting on Chat Console synchronously: https://trtc.io/document/34419?platform=web&product=chat&menulabel=uikit#message-recall-settings
*/
@property (nonatomic, assign) NSUInteger timeIntervalForAllowedMessageRecall;
/**
* Maximum audio recording duration, no more than 60s.
* The default value is 60 seconds.
*/
@property (nonatomic, assign) CGFloat maxAudioRecordDuration;
/**
* Maximum video recording duration, no more than 15s.
* The default value is 15 seconds.
*/
@property (nonatomic, assign) CGFloat maxVideoRecordDuration;
/**
* Enable custom ringtone.
* This config takes effect only for Android devices.
*/
@property (nonatomic, assign) BOOL enableAndroidCustomRing;
/**
* Hide the items in the pop-up menu when user presses the message.
*/
+ (void)hideItemsWhenLongPressMessage:(TUIChatItemWhenLongPressMessage_Classic)items;
/**
* Call this method to use speakers instead of handsets by default when playing voice messages.
*/
+ (void)setPlayingSoundMessageViaSpeakerByDefault;
/**
* Add a custom view at the top of the chat interface.
* This view will be displayed at the top of the message list and will not slide up.
*/
+ (void)setCustomTopView:(UIView *)view;
/**
* Register custom message.
* - Parameters:
* - businessID: Customized messages businessID, which is unique.
* - cellName: Customized message's MessagCell class name.
* - cellDataName: Customized message's MessagCellData class name.
*/
- (void)registerCustomMessage:(NSString *)businessID
messageCellClassName:(NSString *)cellName
messageCellDataClassName:(NSString *)cellDataName;
@end
@interface TUIChatConfig_Classic (MessageStyle)
/**
* The color of send text message.
*/
@property(nonatomic, assign) UIColor *sendTextMessageColor;
/**
* The font of send text message.
*/
@property(nonatomic, assign) UIFont *sendTextMessageFont;
/**
* The color of receive text message.
*/
@property(nonatomic, assign) UIColor *receiveTextMessageColor;
/**
* The font of receive text message.
*/
@property(nonatomic, assign) UIFont *receiveTextMessageFont;
/**
* The text color of system message.
*/
@property (nonatomic, strong) UIColor *systemMessageTextColor;
/**
* The font of system message.
*/
@property (nonatomic, strong) UIFont *systemMessageTextFont;
/**
* The background color of system message.
*/
@property (nonatomic, strong) UIColor *systemMessageBackgroundColor;
/**
* The font of user's nickname of received messages.
*/
@property (nonatomic, strong) UIFont *receiveNicknameFont;
/**
* The color of user's nickname of received messages.
*/
@property (nonatomic, strong) UIColor *receiveNicknameColor;
@end
@interface TUIChatConfig_Classic (MessageLayout)
/**
* Text message cell layout of my sent message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *sendTextMessageLayout;
/**
* Text message cell layout of my received message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *receiveTextMessageLayout;
/**
* Image message cell layout of my sent message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *sendImageMessageLayout;
/**
* Image message cell layout of my received message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *receiveImageMessageLayout;
/**
* Voice message cell layout of my sent message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *sendVoiceMessageLayout;
/**
* Voice message cell layout of my received message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *receiveVoiceMessageLayout;
/**
* Video message cell layout of my sent message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *sendVideoMessageLayout;
/**
* Video message cell layout of my received message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *receiveVideoMessageLayout;
/**
* Other message cell layout of my sent message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *sendMessageLayout;
/**
* Other message cell layout of my received message.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *receiveMessageLayout;
/**
* System message cell layout.
*/
@property(nonatomic, assign, readonly) TUIMessageCellLayout *systemMessageLayout;
@end
@interface TUIChatConfig_Classic (MessageBubble)
/**
* Enable the message display in the bubble style.
* The default value is YES.
*/
@property(nonatomic, assign) BOOL enableMessageBubbleStyle;
/**
* Set the background image of the sent message bubble in consecutive message.
*/
@property (nonatomic, strong) UIImage *sendBubbleBackgroundImage;
/**
* Set the background image of the sent message bubble in highlight status.
*/
@property (nonatomic, strong) UIImage *sendHighlightBubbleBackgroundImage;
/**
* Set the light background image when the sent message bubble needs to flicker.
*/
@property (nonatomic, strong) UIImage *sendAnimateLightBubbleBackgroundImage;
/**
* Set the dark background image when the sent message bubble needs to flicker.
*/
@property (nonatomic, strong) UIImage *sendAnimateDarkBubbleBackgroundImage;
/**
* Set the background image of the sent error message bubble.
*/
@property (nonatomic, strong) UIImage *sendErrorBubbleBackgroundImage;
/**
* Set the background image of the received message bubble in consecutive message.
*/
@property (nonatomic, strong) UIImage *receiveBubbleBackgroundImage;
/**
* Set the background image of the received message bubble in highlight status.
*/
@property (nonatomic, strong) UIImage *receiveHighlightBubbleBackgroundImage;
/**
* Set the light background image when the received message bubble needs to flicker.
*/
@property (nonatomic, strong) UIImage *receiveAnimateLightBubbleBackgroundImage;
/**
* Set the dark background image when the received message bubble needs to flicker.
*/
@property (nonatomic, strong) UIImage *receiveAnimateDarkBubbleBackgroundImage;
/**
* Set the background image of the received error message bubble.
*/
@property (nonatomic, strong) UIImage *receiveErrorBubbleBackgroundImage;
@end
@interface TUIChatConfig_Classic (InputBar)
/**
* DataSource for inputBar.
*/
@property (nonatomic, weak) id<TUIChatInputBarConfigDataSource> inputBarDataSource;
/**
* DataSource for shortcutView above inputBar.
*/
@property (nonatomic, weak) id<TUIChatShortcutViewDataSource> shortcutViewDataSource;
/**
* Show the input bar in the message list interface.
* The default value is YES.
*/
@property(nonatomic, assign) BOOL showInputBar;
/**
* Hide items in more menu.
*/
+ (void)hideItemsInMoreMenu:(TUIChatInputBarMoreMenuItem)items;
/**
* Add sticker group.
*/
- (void)addStickerGroup:(TUIFaceGroup *)group;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,591 @@
//
// TUIChatConfig_Classic.m
// TUIChat
//
// Created by Tencent on 2024/7/16.
// Copyright © 2024 Tencent. All rights reserved.
#import "TUIChatConfig_Classic.h"
#import <TUICore/TUIConfig.h>
#import <TIMCommon/TUIBubbleMessageCell.h>
#import <TIMCommon/TUISystemMessageCellData.h>
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TIMConfig.h>
#import <TIMCommon/TIMCommonMediator.h>
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
#import "TUIBaseChatViewController.h"
#import "TUITextMessageCell.h"
#import "TUIEmojiConfig.h"
#import "TUIChatConversationModel.h"
#import "TUIVoiceMessageCellData.h"
@interface TUIChatConfig_Classic()<TUIChatEventListener>
@end
@implementation TUIChatConfig_Classic
+ (TUIChatConfig_Classic *)sharedConfig {
static dispatch_once_t onceToken;
static TUIChatConfig_Classic *config;
dispatch_once(&onceToken, ^{
config = [[TUIChatConfig_Classic alloc] init];
});
return config;
}
- (instancetype)init {
self = [super init];
if (self) {
TUIChatConfig.defaultConfig.eventConfig.chatEventListener = self;
}
return self;
}
- (void)setEnableTypingIndicator:(BOOL)enable {
[TUIChatConfig defaultConfig].enableTypingStatus = enable;
}
- (BOOL)enableTypingIndicator {
return [TUIChatConfig defaultConfig].enableTypingStatus;
}
- (void)setBackgroudColor:(UIColor *)backgroudColor {
[TUIChatConfig defaultConfig].backgroudColor = backgroudColor;
}
- (UIColor *)backgroudColor {
return [TUIChatConfig defaultConfig].backgroudColor;
}
- (void)setBackgroudImage:(UIImage *)backgroudImage {
[TUIChatConfig defaultConfig].backgroudImage = backgroudImage;
}
- (UIImage *)backgroudImage {
return [TUIChatConfig defaultConfig].backgroudImage;
}
- (void)setAvatarStyle:(TUIAvatarStyle_Classic)avatarStyle {
[TUIConfig defaultConfig].avatarType = (TUIKitAvatarType)avatarStyle;
}
- (TUIAvatarStyle_Classic)avatarStyle {
return (TUIAvatarStyle_Classic)[TUIConfig defaultConfig].avatarType;
}
- (void)setAvatarCornerRadius:(CGFloat)avatarCornerRadius {
[TUIConfig defaultConfig].avatarCornerRadius = avatarCornerRadius;
}
- (CGFloat)avatarCornerRadius {
return [TUIConfig defaultConfig].avatarCornerRadius;
}
- (void)setDefaultAvatarImage:(UIImage *)defaultAvatarImage {
[TUIConfig defaultConfig].defaultAvatarImage = defaultAvatarImage;
}
- (UIImage *)defaultAvatarImage {
return [TUIConfig defaultConfig].defaultAvatarImage;
}
- (void)setEnableGroupGridAvatar:(BOOL)enableGroupGridAvatar {
[TUIConfig defaultConfig].enableGroupGridAvatar = enableGroupGridAvatar;
}
- (BOOL)enableGroupGridAvatar {
return [TUIConfig defaultConfig].enableGroupGridAvatar;
}
- (void)setIsMessageReadReceiptNeeded:(BOOL)isMessageReadReceiptNeeded {
[TUIChatConfig defaultConfig].msgNeedReadReceipt = isMessageReadReceiptNeeded;
}
- (BOOL)isMessageReadReceiptNeeded {
return [TUIChatConfig defaultConfig].msgNeedReadReceipt;
}
- (void)setTimeIntervalForAllowedMessageRecall:(NSUInteger)timeIntervalForAllowedMessageRecall {
[TUIChatConfig defaultConfig].timeIntervalForMessageRecall = timeIntervalForAllowedMessageRecall;
}
- (NSUInteger)timeIntervalForAllowedMessageRecall {
return [TUIChatConfig defaultConfig].timeIntervalForMessageRecall;
}
- (void)setEnableFloatWindowForCall:(BOOL)enableFloatWindowForCall {
[TUIChatConfig defaultConfig].enableFloatWindowForCall = enableFloatWindowForCall;
}
- (BOOL)enableFloatWindowForCall {
return [TUIChatConfig defaultConfig].enableFloatWindowForCall;
}
- (void)setEnableMultiDeviceForCall:(BOOL)enableMultiDeviceForCall {
[TUIChatConfig defaultConfig].enableMultiDeviceForCall = enableMultiDeviceForCall;
}
- (BOOL)enableMultiDeviceForCall {
return [TUIChatConfig defaultConfig].enableMultiDeviceForCall;
}
- (void)setHideVideoCallButton:(BOOL)hideVideoCallButton {
[TUIChatConfig defaultConfig].enableVideoCall = !hideVideoCallButton;
}
- (void)setEnableAndroidCustomRing:(BOOL)enableAndroidCustomRing {
[TUIConfig defaultConfig].enableCustomRing = enableAndroidCustomRing;
}
- (BOOL)enableAndroidCustomRing {
return [TUIConfig defaultConfig].enableCustomRing;
}
- (BOOL)hideVideoCallButton {
return ![TUIChatConfig defaultConfig].enableVideoCall;
}
- (void)setHideAudioCallButton:(BOOL)hideAudioCallButton {
[TUIChatConfig defaultConfig].enableAudioCall = !hideAudioCallButton;
}
- (BOOL)hideAudioCallButton {
return ![TUIChatConfig defaultConfig].enableAudioCall;
}
+ (void)hideItemsWhenLongPressMessage:(TUIChatItemWhenLongPressMessage_Classic)items {
[TUIChatConfig defaultConfig].enablePopMenuReplyAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Reply);
[TUIChatConfig defaultConfig].enablePopMenuEmojiReactAction = !(items & TUIChatItemWhenLongPressMessage_Classic_EmojiReaction);
[TUIChatConfig defaultConfig].enablePopMenuReferenceAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Quote);
[TUIChatConfig defaultConfig].enablePopMenuPinAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Pin);
[TUIChatConfig defaultConfig].enablePopMenuRecallAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Recall);
[TUIChatConfig defaultConfig].enablePopMenuTranslateAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Translate);
[TUIChatConfig defaultConfig].enablePopMenuConvertAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Convert);
[TUIChatConfig defaultConfig].enablePopMenuForwardAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Forward);
[TUIChatConfig defaultConfig].enablePopMenuSelectAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Select);
[TUIChatConfig defaultConfig].enablePopMenuCopyAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Copy);
[TUIChatConfig defaultConfig].enablePopMenuDeleteAction = !(items & TUIChatItemWhenLongPressMessage_Classic_Delete);
}
- (void)setIsExcludedFromUnreadCount:(BOOL)isExcludedFromUnreadCount {
[TUIConfig defaultConfig].isExcludedFromUnreadCount = isExcludedFromUnreadCount;
}
- (BOOL)isExcludedFromUnreadCount {
return [TUIConfig defaultConfig].isExcludedFromUnreadCount;
}
- (void)setIsExcludedFromLastMessage:(BOOL)isExcludedFromLastMessage {
[TUIConfig defaultConfig].isExcludedFromLastMessage = isExcludedFromLastMessage;
}
- (BOOL)isExcludedFromLastMessage {
return [TUIConfig defaultConfig].isExcludedFromLastMessage;
}
- (void)setMaxAudioRecordDuration:(CGFloat)maxAudioRecordDuration {
[TUIChatConfig defaultConfig].maxAudioRecordDuration = maxAudioRecordDuration;
}
- (CGFloat)maxAudioRecordDuration {
return [TUIChatConfig defaultConfig].maxAudioRecordDuration;
}
- (void)setMaxVideoRecordDuration:(CGFloat)maxVideoRecordDuration {
[TUIChatConfig defaultConfig].maxVideoRecordDuration = maxVideoRecordDuration;
}
- (CGFloat)maxVideoRecordDuration {
return [TUIChatConfig defaultConfig].maxVideoRecordDuration;
}
+ (void)setPlayingSoundMessageViaSpeakerByDefault {
if ([TUIVoiceMessageCellData getAudioplaybackStyle] == TUIVoiceAudioPlaybackStyleHandset) {
[TUIVoiceMessageCellData changeAudioPlaybackStyle];
}
}
+ (void)setCustomTopView:(UIView *)view {
[TUIBaseChatViewController setCustomTopView:view];
}
- (void)registerCustomMessage:(NSString *)businessID
messageCellClassName:(NSString *)cellName
messageCellDataClassName:(NSString *)cellDataName {
[[TUIChatConfig defaultConfig] registerCustomMessage:businessID
messageCellClassName:cellName
messageCellDataClassName:cellDataName];
}
#pragma mark - TUIChatEventListener
- (BOOL)onUserIconClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata {
if ([self.delegate respondsToSelector:@selector(onUserAvatarClicked:messageCellData:)]) {
return [self.delegate onUserAvatarClicked:view messageCellData:celldata];
}
return NO;
}
- (BOOL)onUserIconLongClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata {
if ([self.delegate respondsToSelector:@selector(onUserAvatarLongPressed:messageCellData:)]) {
return [self.delegate onUserAvatarLongPressed:view messageCellData:celldata];
}
return NO;
}
- (BOOL)onMessageClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata {
if ([self.delegate respondsToSelector:@selector(onMessageClicked:messageCellData:)]) {
return [self.delegate onMessageClicked:view messageCellData:celldata];
}
return NO;
}
- (BOOL)onMessageLongClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata {
if ([self.delegate respondsToSelector:@selector(onMessageLongPressed:messageCellData:)]) {
return [self.delegate onMessageLongPressed:view messageCellData:celldata];
}
return NO;
}
@end
@implementation TUIChatConfig_Classic (MessageStyle)
- (void)setSendNicknameFont:(UIFont *)sendNicknameFont {
TUIMessageCell.outgoingNameFont = sendNicknameFont;
}
- (UIFont *)sendNicknameFont {
return TUIMessageCell.outgoingNameFont;
}
- (void)setReceiveNicknameFont:(UIFont *)receiveNicknameFont {
TUIMessageCell.incommingNameFont = receiveNicknameFont;
}
- (UIFont *)receiveNicknameFont {
return TUIMessageCell.incommingNameFont;
}
- (void)setSendNicknameColor:(UIColor *)sendNicknameColor {
TUIMessageCell.outgoingNameColor = sendNicknameColor;
}
- (UIColor *)sendNicknameColor {
return TUIMessageCell.outgoingNameColor;
}
- (void)setReceiveNicknameColor:(UIColor *)receiveNicknameColor {
TUIMessageCell.incommingNameColor = receiveNicknameColor;
}
- (UIColor *)receiveNicknameColor {
return TUIMessageCell.incommingNameColor;
}
- (void)setSendTextMessageFont:(UIFont *)sendTextMessageFont {
TUITextMessageCell.outgoingTextFont = sendTextMessageFont;
}
- (UIFont *)sendTextMessageFont {
return TUITextMessageCell.outgoingTextFont;
}
- (void)setReceiveTextMessageFont:(UIFont *)receiveTextMessageFont {
TUITextMessageCell.incommingTextFont = receiveTextMessageFont;
}
- (UIFont *)receiveTextMessageFont {
return TUITextMessageCell.incommingTextFont;
}
- (void)setSendTextMessageColor:(UIColor *)sendTextMessageColor {
TUITextMessageCell.outgoingTextColor = sendTextMessageColor;
}
- (UIColor *)sendTextMessageColor {
return TUITextMessageCell.outgoingTextColor;
}
- (void)setReceiveTextMessageColor:(UIColor *)receiveTextMessageColor {
TUITextMessageCell.incommingTextColor = receiveTextMessageColor;
}
- (UIColor *)receiveTextMessageColor {
return TUITextMessageCell.incommingTextColor;
}
@end
typedef NS_ENUM(NSInteger, UIMessageCellLayoutType) {
UIMessageCellLayoutTypeText,
UIMessageCellLayoutTypeImage,
UIMessageCellLayoutTypeVideo,
UIMessageCellLayoutTypeVoice,
UIMessageCellLayoutTypeOther,
UIMessageCellLayoutTypeSystem
};
@implementation TUIChatConfig_Classic (MessageLayout)
- (TUIMessageCellLayout *)sendTextMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeText isSender:YES];
}
- (TUIMessageCellLayout *)receiveTextMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeText isSender:NO];
}
- (TUIMessageCellLayout *)sendImageMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeImage isSender:YES];
}
- (TUIMessageCellLayout *)receiveImageMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeImage isSender:NO];
}
- (TUIMessageCellLayout *)sendVoiceMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeVoice isSender:YES];
}
- (TUIMessageCellLayout *)receiveVoiceMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeVoice isSender:NO];
}
- (TUIMessageCellLayout *)sendVideoMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeVideo isSender:YES];
}
- (TUIMessageCellLayout *)receiveVideoMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeVideo isSender:NO];
}
- (TUIMessageCellLayout *)sendMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeOther isSender:YES];
}
- (TUIMessageCellLayout *)receiveMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeOther isSender:NO];
}
- (TUIMessageCellLayout *)systemMessageLayout {
return [self getMessageLayoutOfType:UIMessageCellLayoutTypeSystem isSender:NO];
}
- (TUIMessageCellLayout *)getMessageLayoutOfType:(UIMessageCellLayoutType)type isSender:(BOOL)isSender {
TUIMessageCellLayout *innerLayout = nil;
switch (type) {
case UIMessageCellLayoutTypeText: {
innerLayout = isSender ? [TUIMessageCellLayout outgoingTextMessageLayout] : [TUIMessageCellLayout incommingTextMessageLayout];
break;
}
case UIMessageCellLayoutTypeImage: {
innerLayout = isSender ? [TUIMessageCellLayout outgoingImageMessageLayout] : [TUIMessageCellLayout incommingImageMessageLayout];
break;
}
case UIMessageCellLayoutTypeVideo: {
innerLayout = isSender ? [TUIMessageCellLayout outgoingVideoMessageLayout] : [TUIMessageCellLayout incommingVideoMessageLayout];
break;
}
case UIMessageCellLayoutTypeVoice: {
innerLayout = isSender ? [TUIMessageCellLayout outgoingVoiceMessageLayout] : [TUIMessageCellLayout incommingVoiceMessageLayout];
break;
}
case UIMessageCellLayoutTypeOther: {
innerLayout = isSender ? [TUIMessageCellLayout outgoingMessageLayout] : [TUIMessageCellLayout incommingMessageLayout];
break;
}
case UIMessageCellLayoutTypeSystem: {
innerLayout = [TUIMessageCellLayout systemMessageLayout];
break;
}
}
return innerLayout;
}
- (void)setSystemMessageBackgroundColor:(UIColor *)systemMessageBackgroundColor {
TUISystemMessageCellData.textBackgroundColor = systemMessageBackgroundColor;
}
- (UIColor *)systemMessageBackgroundColor {
return TUISystemMessageCellData.textBackgroundColor;
}
- (void)setSystemMessageTextFont:(UIFont *)systemMessageTextFont {
TUISystemMessageCellData.textFont = systemMessageTextFont;
}
- (UIFont *)systemMessageTextFont {
return TUISystemMessageCellData.textFont;
}
- (void)setSystemMessageTextColor:(UIColor *)systemMessageTextColor {
TUISystemMessageCellData.textColor = systemMessageTextColor;
}
- (UIColor *)systemMessageTextColor {
return TUISystemMessageCellData.textColor;
}
- (void)setReceiveNicknameFont:(UIFont *)receiveNicknameFont {
TUIMessageCell.incommingNameFont = receiveNicknameFont;
}
- (UIFont *)receiveNicknameFont {
return TUIMessageCell.incommingNameFont;
}
- (void)setReceiveNicknameColor:(UIColor *)receiveNicknameColor {
TUIMessageCell.incommingNameColor = receiveNicknameColor;
}
- (UIColor *)receiveNicknameColor {
return TUIMessageCell.incommingNameColor;
}
@end
@implementation TUIChatConfig_Classic (MessageBubble)
- (void)setEnableMessageBubbleStyle:(BOOL)enableMessageBubbleStyle {
[TIMConfig defaultConfig].enableMessageBubble = enableMessageBubbleStyle;
}
- (BOOL)enableMessageBubbleStyle {
return [TIMConfig defaultConfig].enableMessageBubble;
}
- (void)setSendBubbleBackgroundImage:(UIImage *)sendBubbleBackgroundImage {
[TUIBubbleMessageCell setOutgoingBubble:sendBubbleBackgroundImage];
}
- (UIImage *)sendBubbleBackgroundImage {
return [TUIBubbleMessageCell outgoingBubble];
}
- (void)setSendHighlightBubbleBackgroundImage:(UIImage *)sendHighlightBackgroundImage {
[TUIBubbleMessageCell setOutgoingHighlightedBubble:sendHighlightBackgroundImage];
}
- (UIImage *)sendHighlightBubbleBackgroundImage {
return [TUIBubbleMessageCell outgoingHighlightedBubble];
}
- (void)setSendAnimateLightBubbleBackgroundImage:(UIImage *)sendAnimateLightBackgroundImage {
[TUIBubbleMessageCell setOutgoingAnimatedHighlightedAlpha20:sendAnimateLightBackgroundImage];
}
- (UIImage *)sendAnimateLightBubbleBackgroundImage {
return [TUIBubbleMessageCell outgoingAnimatedHighlightedAlpha20];
}
- (void)setSendAnimateDarkBubbleBackgroundImage:(UIImage *)sendAnimateDarkBackgroundImage {
[TUIBubbleMessageCell setOutgoingAnimatedHighlightedAlpha50:sendAnimateDarkBackgroundImage];
}
- (UIImage *)sendAnimateDarkBubbleBackgroundImage {
return [TUIBubbleMessageCell outgoingAnimatedHighlightedAlpha50];
}
- (void)setSendErrorBubbleBackgroundImage:(UIImage *)sendErrorBubbleBackgroundImage {
[TUIBubbleMessageCell setOutgoingErrorBubble:sendErrorBubbleBackgroundImage];
}
- (UIImage *)sendErrorBubbleBackgroundImage {
return [TUIBubbleMessageCell outgoingErrorBubble];
}
- (void)setReceiveBubbleBackgroundImage:(UIImage *)receiveBubbleBackgroundImage {
[TUIBubbleMessageCell setIncommingBubble:receiveBubbleBackgroundImage];
}
- (UIImage *)receiveBubbleBackgroundImage {
return [TUIBubbleMessageCell incommingBubble];
}
- (void)setReceiveHighlightBubbleBackgroundImage:(UIImage *)receiveHighlightBubbleBackgroundImage {
[TUIBubbleMessageCell setIncommingHighlightedBubble:receiveHighlightBubbleBackgroundImage];
}
- (UIImage *)receiveHighlightBubbleBackgroundImage {
return [TUIBubbleMessageCell incommingHighlightedBubble];
}
- (void)setReceiveAnimateLightBubbleBackgroundImage:(UIImage *)receiveAnimateLightBubbleBackgroundImage {
[TUIBubbleMessageCell setIncommingAnimatedHighlightedAlpha20:receiveAnimateLightBubbleBackgroundImage];
}
- (UIImage *)receiveAnimateLightBubbleBackgroundImage {
return [TUIBubbleMessageCell incommingAnimatedHighlightedAlpha20];
}
- (void)setReceiveAnimateDarkBubbleBackgroundImage:(UIImage *)receiveAnimateDarkBubbleBackgroundImage {
[TUIBubbleMessageCell setIncommingAnimatedHighlightedAlpha50:receiveAnimateDarkBubbleBackgroundImage];
}
- (UIImage *)receiveAnimateDarkBubbleBackgroundImage {
return [TUIBubbleMessageCell incommingAnimatedHighlightedAlpha50];
}
- (void)setReceiveErrorBubbleBackgroundImage:(UIImage *)receiveErrorBubbleBackgroundImage {
[TUIBubbleMessageCell setIncommingErrorBubble:receiveErrorBubbleBackgroundImage];
}
- (UIImage *)receiveErrorBubbleBackgroundImage {
return [TUIBubbleMessageCell incommingErrorBubble];
}
@end
@implementation TUIChatConfig_Classic (InputBar)
- (id<TUIChatInputBarConfigDataSource>)setinputBarDataSource {
return [TUIChatConfig defaultConfig].inputBarDataSource;
}
- (void)setInputBarDataSource:(id<TUIChatInputBarConfigDataSource>)inputBarDataSource {
[TUIChatConfig defaultConfig].inputBarDataSource = inputBarDataSource;
}
- (id<TUIChatShortcutViewDataSource>)shortcutViewDataSource {
return [TUIChatConfig defaultConfig].shortcutViewDataSource;
}
- (void)setShortcutViewDataSource:(id<TUIChatShortcutViewDataSource>)shortcutViewDataSource {
[TUIChatConfig defaultConfig].shortcutViewDataSource = shortcutViewDataSource;
}
- (void)setShowInputBar:(BOOL)showInputBar {
[TUIChatConfig defaultConfig].enableMainPageInputBar = showInputBar;
}
- (BOOL)showInputBar {
return ![TUIChatConfig defaultConfig].enableMainPageInputBar;
}
+ (void)hideItemsInMoreMenu:(TUIChatInputBarMoreMenuItem)items {
[TUIChatConfig defaultConfig].enableWelcomeCustomMessage = !(items & TUIChatInputBarMoreMenuItem_CustomMessage);
[TUIChatConfig defaultConfig].showRecordVideoButton = !(items & TUIChatInputBarMoreMenuItem_RecordVideo);
[TUIChatConfig defaultConfig].showTakePhotoButton = !(items & TUIChatInputBarMoreMenuItem_TakePhoto);
[TUIChatConfig defaultConfig].showAlbumButton = !(items & TUIChatInputBarMoreMenuItem_Album);
[TUIChatConfig defaultConfig].showFileButton = !(items & TUIChatInputBarMoreMenuItem_File);
[TUIChatConfig defaultConfig].showRoomButton = !(items & TUIChatInputBarMoreMenuItem_Room);
[TUIChatConfig defaultConfig].showPollButton = !(items & TUIChatInputBarMoreMenuItem_Poll);
[TUIChatConfig defaultConfig].showGroupNoteButton = !(items & TUIChatInputBarMoreMenuItem_GroupNote);
[TUIChatConfig defaultConfig].enableVideoCall = !(items & TUIChatInputBarMoreMenuItem_VideoCall);
[TUIChatConfig defaultConfig].enableAudioCall = !(items & TUIChatInputBarMoreMenuItem_AudioCall);
}
- (void)addStickerGroup:(TUIFaceGroup *)group {
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
[service appendFaceGroup:group];
}
@end

View File

@@ -0,0 +1,15 @@
//
// TUIChat.h
// Pods
//
// Created by harvy on 2022/6/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#ifndef TUIChat_h
#define TUIChat_h
#import "TUIC2CChatViewController.h"
#import "TUIChatConfig.h"
#import "TUIGroupChatViewController.h"
#endif /* TUIChat_h */

View File

@@ -0,0 +1,177 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This file declares the TUIInputBarDelegate protocol and the TUIInputBar class.
* TUI input bar, a UI component used to detect and obtain user input.
* TUIInputBar, the UI component at the bottom of the chat message. Includes text input box, emoji button, voice button, and "+" button ("More" button)
* TUIInputBarDelegate provides callbacks for various situations of the input bar, including the callback for the emoticon of clicking the input bar, the
* "more" view, and the voice button. And callbacks to send message, send voice, change input height.
*/
#import <TIMCommon/TIMCommonModel.h>
#import <TUICore/TUIThemeManager.h>
#import <UIKit/UIKit.h>
#import "TUIResponderTextView.h"
#define kTUIInputNoramlFont [UIFont systemFontOfSize:16.0]
#define kTUIInputNormalTextColor TUIChatDynamicColor(@"chat_input_text_color", @"#000000")
@class TUIInputBar;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIInputBarDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIInputBarDelegate <NSObject>
/**
* Callback after clicking the emoji button - "smiley" button.
* You can use this callback to achieve: After clicking the emoticon button, the corresponding emoticon view is displayed.
*/
- (void)inputBarDidTouchFace:(TUIInputBar *)textView;
/**
* Callback after more button - "+" is clicked.
* You can use this callback to achieve: corresponding user's click operation to display more corresponding views.
*/
- (void)inputBarDidTouchMore:(TUIInputBar *)textView;
/**
* Callback after clicking the voice button - "Sound Wave" icon .
* You can use this callback to display the corresponding operation prompt view and start voice recording
*/
- (void)inputBarDidTouchVoice:(TUIInputBar *)textView;
/**
* Callback when input bar height changes
* This callback is fired when the InputBar height changes when you click the voice button, emoji button, "+" button, or call out/retract the keyboard
* You can use this callback to achieve: UI layout adjustment when InputBar height changes through this callback function.
* In the default implementation of TUIKit, this callback function further calls the didChangeHeight delegate in TUIInputController to adjust the height of the
* UI layout after processing the appearance of the expression view and more views.
*/
- (void)inputBar:(TUIInputBar *)textView didChangeInputHeight:(CGFloat)offset;
/**
* Callback when sending a text message.
* This callback is fired when you send a text message through the InputBar (click the send button from the keyboard).
* You can use this callback to get the content of the InputBar and send the message.
* In the default implementation of TUIKit, this callback further calls the didSendMessage delegate in TUIInputController for further logical processing of
* message sending after processing the appearance of the expression view and more views.
*/
- (void)inputBar:(TUIInputBar *)textView didSendText:(NSString *)text;
/**
* Callback when sending voice
* This callback is triggered when you long press and release the voice button.
* You can use this callback to process the recorded voice information and send the voice message.
* In the default implementation of TUIKit, this callback function further calls the didSendMessage delegate in TUIInputController for further logical
* processing of message sending after processing the appearance of the expression view and more views.
*/
- (void)inputBar:(TUIInputBar *)textView didSendVoice:(NSString *)path;
/**
* Callback after entering text containing the @ character
*/
- (void)inputBarDidInputAt:(TUIInputBar *)textView;
/**
* Callback after removing text containing @ characters (e.g. removing @xxx)
*/
- (void)inputBar:(TUIInputBar *)textView didDeleteAt:(NSString *)text;
/**
* Callback after keyboard button click
* After clicking the emoticon button, the "smiley face" icon at the corresponding position will become the "keyboard" icon, which is the keyboard button at
* this time. You can use this callback to: hide the currently displayed emoticon view or more views, and open the keyboard.
*/
- (void)inputBarDidTouchKeyboard:(TUIInputBar *)textView;
/**
* Callback after clicking delete button on keyboard
*/
- (void)inputBarDidDeleteBackward:(TUIInputBar *)textView;
- (void)inputTextViewShouldBeginTyping:(UITextView *)textView;
- (void)inputTextViewShouldEndTyping:(UITextView *)textView;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIInputBar
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIInputBar : UIView
/**
* Separtor
*/
@property(nonatomic, strong) UIView *lineView;
/**
* Voice button
* Switch to voice input state after clicking
*/
@property(nonatomic, strong) UIButton *micButton;
/**
* Keyboard button
* Switch to keyboard input state after clicking
*/
@property(nonatomic, strong) UIButton *keyboardButton;
/**
* Input view
*/
@property(nonatomic, strong) TUIResponderTextView *inputTextView;
/**
* Emoticon button
* Switch to emoji input state after clicking
*/
@property(nonatomic, strong) UIButton *faceButton;
/**
* More button
* A button that, when clicked, opens up more menu options
*/
@property(nonatomic, strong) UIButton *moreButton;
/**
* Record button, long press the button to start recording
*/
@property(nonatomic, strong) UIButton *recordButton;
@property(nonatomic, weak) id<TUIInputBarDelegate> delegate;
@property(nonatomic, copy) void (^inputBarTextChanged)(UITextView * textview);
@property(nonatomic, assign) BOOL isFromReplyPage;
- (void)defaultLayout;
/**
* Add emoticon text
* Used to input emoticon text in the current text input box
*
* @param emoji The string representation of the emoticon to be entered.
*/
- (void)addEmoji:(TUIFaceCellData *)emoji;
- (void)backDelete;
- (void)clearInput;
- (NSString *)getInput;
- (void)updateTextViewFrame;
- (void)changeToKeyboard;
- (void)addDraftToInputBar:(NSAttributedString *)draft;
- (void)addWordsToInputBar:(NSAttributedString *)words;
@end

View File

@@ -0,0 +1,696 @@
//
// TUIInputBar.m
// UIKit
//
// Created by kennethmiao on 2018/9/18.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIInputBar.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUITool.h>
#import "TUIRecordView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/NSTimer+TUISafe.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/UIView+TUILayout.h>
#import "ReactiveObjC/ReactiveObjC.h"
#import "TUIAudioRecorder.h"
#import "TUIChatConfig.h"
@interface TUIInputBar () <UITextViewDelegate, TUIAudioRecorderDelegate>
@property(nonatomic, strong) TUIRecordView *recordView;
@property(nonatomic, strong) NSDate *recordStartTime;
@property(nonatomic, strong) TUIAudioRecorder *recorder;
@property(nonatomic, assign) BOOL isFocusOn;
@property(nonatomic, strong) NSTimer *sendTypingStatusTimer;
@property(nonatomic, assign) BOOL allowSendTypingStatusByChangeWord;
@end
@implementation TUIInputBar
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self defaultLayout];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
}
return self;
}
- (void)dealloc {
if (_sendTypingStatusTimer) {
[_sendTypingStatusTimer invalidate];
_sendTypingStatusTimer = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
_lineView = [[UIView alloc] init];
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#FFFFFF");
[self addSubview:_lineView];
_micButton = [[UIButton alloc] init];
[_micButton addTarget:self action:@selector(onMicButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoice_img", @"ToolViewInputVoice") forState:UIControlStateNormal];
[_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoiceHL_img", @"ToolViewInputVoiceHL") forState:UIControlStateHighlighted];
[self addSubview:_micButton];
_faceButton = [[UIButton alloc] init];
[_faceButton addTarget:self action:@selector(onFaceEmojiButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotion_img", @"ToolViewEmotion") forState:UIControlStateNormal];
[_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotionHL_img", @"ToolViewEmotionHL") forState:UIControlStateHighlighted];
[self addSubview:_faceButton];
_keyboardButton = [[UIButton alloc] init];
[_keyboardButton addTarget:self action:@selector(onKeyboardButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboard_img", @"ToolViewKeyboard") forState:UIControlStateNormal];
[_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboardHL_img", @"ToolViewKeyboardHL") forState:UIControlStateHighlighted];
_keyboardButton.hidden = YES;
[self addSubview:_keyboardButton];
_moreButton = [[UIButton alloc] init];
[_moreButton addTarget:self action:@selector(onMoreButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtn_Black_img", @"TypeSelectorBtn_Black") forState:UIControlStateNormal];
[_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtnHL_Black_img", @"TypeSelectorBtnHL_Black") forState:UIControlStateHighlighted];
[self addSubview:_moreButton];
_recordButton = [[UIButton alloc] init];
[_recordButton.titleLabel setFont:[UIFont systemFontOfSize:15.0f]];
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDown:) forControlEvents:UIControlEventTouchDown];
[_recordButton addTarget:self action:@selector(onRecordButtonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[_recordButton addTarget:self action:@selector(onRecordButtonTouchCancel:) forControlEvents:UIControlEventTouchUpOutside | UIControlEventTouchCancel];
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDragExit:) forControlEvents:UIControlEventTouchDragExit];
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDragEnter:) forControlEvents:UIControlEventTouchDragEnter];
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
[_recordButton setTitleColor:TUIChatDynamicColor(@"chat_input_text_color", @"#000000") forState:UIControlStateNormal];
_recordButton.hidden = YES;
[self addSubview:_recordButton];
_inputTextView = [[TUIResponderTextView alloc] init];
_inputTextView.delegate = self;
[_inputTextView setFont:kTUIInputNoramlFont];
_inputTextView.backgroundColor = TUIChatDynamicColor(@"chat_input_bg_color", @"#FFFFFF");
_inputTextView.textColor = TUIChatDynamicColor(@"chat_input_text_color", @"#000000");
_inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
[_inputTextView setReturnKeyType:UIReturnKeySend];
[self addSubview:_inputTextView];
[self applyBorderTheme];
}
- (void)onThemeChanged {
[self applyBorderTheme];
}
- (void)applyBorderTheme {
if (_recordButton) {
[_recordButton.layer setMasksToBounds:YES];
[_recordButton.layer setCornerRadius:4.0f];
[_recordButton.layer setBorderWidth:1.0f];
[_recordButton.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
}
if (_inputTextView) {
[_inputTextView.layer setMasksToBounds:YES];
[_inputTextView.layer setCornerRadius:4.0f];
[_inputTextView.layer setBorderWidth:0.5f];
[_inputTextView.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
}
}
- (void)defaultLayout {
[_lineView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
make.width.mas_equalTo(self);
make.height.mas_equalTo(TLine_Heigh);
}];
CGSize buttonSize = TTextView_Button_Size;
CGFloat buttonOriginY = (TTextView_Height - buttonSize.height) * 0.5;
[_micButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.mas_leading);
make.centerY.mas_equalTo(self);
make.size.mas_equalTo(buttonSize);
}];
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(_micButton);
}];
[_moreButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(0);
make.size.mas_equalTo(buttonSize);
make.centerY.mas_equalTo(self);
}];
[_faceButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.trailing.mas_equalTo(_moreButton.mas_leading).mas_offset(- TTextView_Margin);
make.size.mas_equalTo(buttonSize);
make.centerY.mas_equalTo(self);
}];
[_recordButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
make.height.mas_equalTo(TTextView_TextView_Height_Min);
make.centerY.mas_equalTo(self);
}];
[_inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (self.isFromReplyPage) {
make.leading.mas_equalTo(self.mas_leading).mas_offset(10);
}
else {
make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
}
make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
make.height.mas_equalTo(TTextView_TextView_Height_Min);
make.centerY.mas_equalTo(self);
}];
}
- (void)layoutButton:(CGFloat)height {
CGRect frame = self.frame;
CGFloat offset = height - frame.size.height;
frame.size.height = height;
self.frame = frame;
CGSize buttonSize = TTextView_Button_Size;
CGFloat bottomMargin = (TTextView_Height - buttonSize.height) * 0.5;
CGFloat originY = frame.size.height - buttonSize.height - bottomMargin;
CGRect faceFrame = _faceButton.frame;
faceFrame.origin.y = originY;
_faceButton.frame = faceFrame;
CGRect moreFrame = _moreButton.frame;
moreFrame.origin.y = originY;
_moreButton.frame = moreFrame;
CGRect voiceFrame = _micButton.frame;
voiceFrame.origin.y = originY;
_micButton.frame = voiceFrame;
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(_faceButton);
}];
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didChangeInputHeight:)]) {
[_delegate inputBar:self didChangeInputHeight:offset];
}
}
#pragma mark - Event response
- (void)onMicButtonClicked:(UIButton *)sender {
_recordButton.hidden = NO;
_inputTextView.hidden = YES;
_micButton.hidden = YES;
_keyboardButton.hidden = NO;
_faceButton.hidden = NO;
[_inputTextView resignFirstResponder];
[self layoutButton:TTextView_Height];
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
[_delegate inputBarDidTouchVoice:self];
}
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(_micButton);
}];
}
- (void)onKeyboardButtonClicked:(UIButton *)sender {
_micButton.hidden = NO;
_keyboardButton.hidden = YES;
_recordButton.hidden = YES;
_inputTextView.hidden = NO;
_faceButton.hidden = NO;
[self layoutButton:_inputTextView.frame.size.height + 2 * TTextView_Margin];
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchKeyboard:)]) {
[_delegate inputBarDidTouchKeyboard:self];
}
}
- (void)onFaceEmojiButtonClicked:(UIButton *)sender {
_micButton.hidden = NO;
_faceButton.hidden = YES;
_keyboardButton.hidden = NO;
_recordButton.hidden = YES;
_inputTextView.hidden = NO;
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchFace:)]) {
[_delegate inputBarDidTouchFace:self];
}
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(_faceButton);
}];
}
- (void)onMoreButtonClicked:(UIButton *)sender {
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
[_delegate inputBarDidTouchMore:self];
}
}
- (void)onRecordButtonTouchDown:(UIButton *)sender {
[self.recorder record];
}
- (void)onRecordButtonTouchUpInside:(UIButton *)sender {
self.recordButton.backgroundColor = [UIColor clearColor];
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:self.recordStartTime];
@weakify(self);
if (interval < 1) {
[self.recordView setStatus:Record_Status_TooShort];
[self.recorder cancel];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
[self.recordView removeFromSuperview];
self.recordView = nil;
});
} else if (interval > MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration)) {
[self.recordView setStatus:Record_Status_TooLong];
[self.recorder cancel];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
[self.recordView removeFromSuperview];
self.recordView = nil;
});
} else {
/// TUICallKit may need some time to stop all services, so remove UI immediately then stop the recorder.
if (_recordView) {
[self.recordView removeFromSuperview];
self.recordView = nil;
}
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
@strongify(self);
dispatch_async(main_queue, ^{
[self.recorder stop];
NSString *path = self.recorder.recordedFilePath;
if (path) {
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
[self.delegate inputBar:self didSendVoice:path];
}
}
});
});
}
}
- (void)onRecordButtonTouchCancel:(UIButton *)sender {
[self.recordView removeFromSuperview];
self.recordView = nil;
self.recordButton.backgroundColor = [UIColor clearColor];
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
[self.recorder cancel];
}
- (void)onRecordButtonTouchDragExit:(UIButton *)sender {
[self.recordView setStatus:Record_Status_Cancel];
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToCancel) forState:UIControlStateNormal];
}
- (void)onRecordButtonTouchDragEnter:(UIButton *)sender {
[self.recordView setStatus:Record_Status_Recording];
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
}
- (void)showHapticFeedback {
if (@available(iOS 10.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator prepare];
[generator impactOccurred];
});
} else {
// Fallback on earlier versions
}
}
#pragma mark - Text input
#pragma mark-- UITextViewDelegate
- (void)textViewDidBeginEditing:(UITextView *)textView {
self.keyboardButton.hidden = YES;
self.micButton.hidden = NO;
self.faceButton.hidden = NO;
self.isFocusOn = YES;
self.allowSendTypingStatusByChangeWord = YES;
__weak typeof(self) weakSelf = self;
self.sendTypingStatusTimer = [NSTimer tui_scheduledTimerWithTimeInterval:4
repeats:YES
block:^(NSTimer *_Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.allowSendTypingStatusByChangeWord = YES;
}];
if (self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
[_delegate inputTextViewShouldBeginTyping:textView];
}
}
}
- (void)textViewDidEndEditing:(UITextView *)textView {
self.isFocusOn = NO;
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
[_delegate inputTextViewShouldEndTyping:textView];
}
}
- (void)textViewDidChange:(UITextView *)textView {
if (self.allowSendTypingStatusByChangeWord && self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
self.allowSendTypingStatusByChangeWord = NO;
[_delegate inputTextViewShouldBeginTyping:textView];
}
}
if (self.isFocusOn && [textView.textStorage tui_getPlainString].length == 0) {
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
[_delegate inputTextViewShouldEndTyping:textView];
}
}
if (self.inputBarTextChanged) {
self.inputBarTextChanged(_inputTextView);
}
CGSize size = [_inputTextView sizeThatFits:CGSizeMake(_inputTextView.frame.size.width, TTextView_TextView_Height_Max)];
CGFloat oldHeight = _inputTextView.frame.size.height;
CGFloat newHeight = size.height;
if (newHeight > TTextView_TextView_Height_Max) {
newHeight = TTextView_TextView_Height_Max;
}
if (newHeight < TTextView_TextView_Height_Min) {
newHeight = TTextView_TextView_Height_Min;
}
if (oldHeight == newHeight) {
return;
}
__weak typeof(self) ws = self;
[UIView animateWithDuration:0.3
animations:^{
[ws.inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(ws.micButton.mas_trailing).mas_offset(10);
make.trailing.mas_equalTo(ws.faceButton.mas_leading).mas_offset(-10);
make.height.mas_equalTo(newHeight);
make.centerY.mas_equalTo(self);
}];
[ws layoutButton:newHeight + 2 * TTextView_Margin];
}];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text tui_containsString:@"["] && [text tui_containsString:@"]"]) {
NSRange selectedRange = textView.selectedRange;
if (selectedRange.length > 0) {
[textView.textStorage deleteCharactersInRange:selectedRange];
}
NSMutableAttributedString *textChange = [text getAdvancedFormatEmojiStringWithFont:kTUIInputNoramlFont
textColor:kTUIInputNormalTextColor
emojiLocations:nil];
[textView.textStorage insertAttributedString:textChange atIndex:textView.textStorage.length];
dispatch_async(dispatch_get_main_queue(), ^{
self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
});
return NO;
}
if ([text isEqualToString:@"\n"]) {
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendText:)]) {
NSString *sp = [[textView.textStorage tui_getPlainString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (sp.length == 0) {
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputBlankMessageTitle)
message:nil
preferredStyle:UIAlertControllerStyleAlert];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
[self.mm_viewController presentViewController:ac animated:YES completion:nil];
} else {
[_delegate inputBar:self didSendText:[textView.textStorage tui_getPlainString]];
[self clearInput];
}
}
return NO;
} else if ([text isEqualToString:@""]) {
if (textView.textStorage.length > range.location) {
// Delete the @ message like @xxx at one time
NSAttributedString *lastAttributedStr = [textView.textStorage attributedSubstringFromRange:NSMakeRange(range.location, 1)];
NSString *lastStr = [lastAttributedStr tui_getPlainString];
if (lastStr && lastStr.length > 0 && [lastStr characterAtIndex:0] == ' ') {
NSUInteger location = range.location;
NSUInteger length = range.length;
// corresponds to ascii code
int at = 64;
// (space) ascii
// Space (space) corresponding ascii code
int space = 32;
while (location != 0) {
location--;
length++;
// Convert characters to ascii code, copy to int, avoid out of bounds
int c = (int)[[[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, 1)] tui_getPlainString] characterAtIndex:0];
if (c == at) {
NSString *atText = [[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, length)] tui_getPlainString];
UIFont *textFont = kTUIInputNoramlFont;
NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:@"" attributes:@{NSFontAttributeName : textFont}];
[textView.textStorage replaceCharactersInRange:NSMakeRange(location, length) withAttributedString:spaceString];
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didDeleteAt:)]) {
[self.delegate inputBar:self didDeleteAt:atText];
}
return NO;
} else if (c == space) {
// Avoid "@nickname Hello, nice to meet you (space) "" Press del after a space to over-delete to @
break;
}
}
}
}
}
// Monitor the input of @ character, including full-width/half-width
else if ([text isEqualToString:@"@"] || [text isEqualToString:@""]) {
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidInputAt:)]) {
[self.delegate inputBarDidInputAt:self];
}
return NO;
}
return YES;
}
- (void)onDeleteBackward:(TUIResponderTextView *)textView {
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidDeleteBackward:)]) {
[self.delegate inputBarDidDeleteBackward:self];
}
}
- (void)clearInput {
[_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(0, _inputTextView.textStorage.length)];
[self textViewDidChange:_inputTextView];
}
- (NSString *)getInput {
return [_inputTextView.textStorage tui_getPlainString];
}
- (void)addEmoji:(TUIFaceCellData *)emoji {
// Create emoji attachment
TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
emojiTextAttachment.faceCellData = emoji;
NSString *localizableFaceName = emoji.name;
// Set tag and image
emojiTextAttachment.emojiTag = localizableFaceName;
emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:emoji.path];
// Set emoji size
emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
NSAttributedString *str = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
NSRange selectedRange = _inputTextView.selectedRange;
if (selectedRange.length > 0) {
[_inputTextView.textStorage deleteCharactersInRange:selectedRange];
}
// Insert emoji image
[_inputTextView.textStorage insertAttributedString:str atIndex:_inputTextView.selectedRange.location];
_inputTextView.selectedRange = NSMakeRange(_inputTextView.selectedRange.location + 1, 0);
[self resetTextStyle];
if (_inputTextView.contentSize.height > TTextView_TextView_Height_Max) {
float offset = _inputTextView.contentSize.height - _inputTextView.frame.size.height;
[_inputTextView scrollRectToVisible:CGRectMake(0, offset, _inputTextView.frame.size.width, _inputTextView.frame.size.height) animated:YES];
}
[self textViewDidChange:_inputTextView];
}
- (void)resetTextStyle {
// After changing text selection, should reset style.
NSRange wholeRange = NSMakeRange(0, _inputTextView.textStorage.length);
[_inputTextView.textStorage removeAttribute:NSFontAttributeName range:wholeRange];
[_inputTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:wholeRange];
[_inputTextView.textStorage addAttribute:NSForegroundColorAttributeName value:kTUIInputNormalTextColor range:wholeRange];
[_inputTextView.textStorage addAttribute:NSFontAttributeName value:kTUIInputNoramlFont range:wholeRange];
[_inputTextView setFont:kTUIInputNoramlFont];
_inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
// In iOS 15.0 and later, you need set styles again as belows
_inputTextView.textColor = kTUIInputNormalTextColor;
_inputTextView.font = kTUIInputNoramlFont;
}
- (void)backDelete {
if (_inputTextView.textStorage.length > 0) {
[_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(_inputTextView.textStorage.length - 1, 1)];
[self textViewDidChange:_inputTextView];
}
}
- (void)updateTextViewFrame {
[self textViewDidChange:[UITextView new]];
}
- (void)changeToKeyboard {
[self onKeyboardButtonClicked:self.keyboardButton];
}
- (void)addDraftToInputBar:(NSAttributedString *)draft {
[self addWordsToInputBar:draft];
}
- (void)addWordsToInputBar:(NSAttributedString *)words {
NSRange selectedRange = self.inputTextView.selectedRange;
if (selectedRange.length > 0) {
[self.inputTextView.textStorage deleteCharactersInRange:selectedRange];
}
// Insert draft
[self.inputTextView.textStorage insertAttributedString:words atIndex:self.inputTextView.selectedRange.location];
self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
[self resetTextStyle];
[self updateTextViewFrame];
}
#pragma mark - TUIAudioRecorderDelegate
- (void)audioRecorder:(TUIAudioRecorder *)recorder didCheckPermission:(BOOL)isGranted isFirstTime:(BOOL)isFirstTime {
if (isFirstTime) {
if (!isGranted) {
[self showRequestMicAuthorizationAlert];
}
return;
}
[self updateViewsToRecordingStatus];
}
- (void)showRequestMicAuthorizationAlert {
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicTitle)
message:TIMCommonLocalizableString(TUIKitInputNoMicTips)
preferredStyle:UIAlertControllerStyleAlert];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateLater) style:UIAlertActionStyleCancel handler:nil]];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateEnable)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
UIApplication *app = [UIApplication sharedApplication];
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
if ([app canOpenURL:settingsURL]) {
[app openURL:settingsURL];
}
}]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.mm_viewController presentViewController:ac animated:YES completion:nil];
});
}
- (void)updateViewsToRecordingStatus {
[self.window addSubview:self.recordView];
[self.recordView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(self.window);
make.width.height.mas_equalTo(self.window);
}];
self.recordStartTime = [NSDate date];
[self.recordView setStatus:Record_Status_Recording];
self.recordButton.backgroundColor = [UIColor lightGrayColor];
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
[self showHapticFeedback];
}
- (void)audioRecorder:(TUIAudioRecorder *)recorder didPowerChanged:(float)power {
if (!self.recordView.hidden) {
[self.recordView setPower:power];
}
}
- (void)audioRecorder:(TUIAudioRecorder *)recorder didRecordTimeChanged:(NSTimeInterval)time {
float uiMaxDuration = MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration);
float realMaxDuration = uiMaxDuration + 0.7;
NSInteger seconds = uiMaxDuration - time;
self.recordView.timeLabel.text = [[NSString alloc] initWithFormat:@"%ld\"", (long)seconds + 1];
if (time >= (uiMaxDuration - 4) && time <= uiMaxDuration) {
NSInteger seconds = uiMaxDuration - time;
/**
* The long type is cast here to eliminate compiler warnings.
* Here +1 is to round up and optimize the time logic.
*/
self.recordView.title.text = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitInputWillFinishRecordInSeconds), (long)seconds + 1];
} else if (time > realMaxDuration) {
[self.recorder stop];
NSString *path = self.recorder.recordedFilePath;
[self.recordView setStatus:Record_Status_TooLong];
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
[self.recordView removeFromSuperview];
self.recordView = nil;
});
if (path) {
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
[_delegate inputBar:self didSendVoice:path];
}
}
}
}
#pragma mark - Getter
- (TUIAudioRecorder *)recorder {
if (!_recorder) {
_recorder = [[TUIAudioRecorder alloc] init];
_recorder.delegate = self;
}
return _recorder;
}
- (TUIRecordView *)recordView {
if (!_recordView) {
_recordView = [[TUIRecordView alloc] init];
_recordView.frame = self.frame;
}
return _recordView;
}
@end

View File

@@ -0,0 +1,153 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This document declares the relevant components to implement the input area.
* The input area includes the emoticon view input area (TUIFaceView+TUIMoreView), the "more" functional area (TUIMoreView) and the text input area
* (TUIInputBar). This file contains the TUIInputControllerDelegate protocol and the TInputController class. In the input bar (TUIInputBar), button response
* callbacks for expressions, voices, and more views are provided. In this class, the InputBar is actually combined with the above three views to realize the
* display and switching logic of each view.
*/
#import <TIMCommon/TUIMessageCell.h>
#import <UIKit/UIKit.h>
#import "TUIChatDefine.h"
#import "TUIFaceView.h"
#import "TUIInputBar.h"
#import "TUIMenuView.h"
#import "TUIMoreView.h"
#import "TUIReplyPreviewBar.h"
#import "TUIFaceSegementScrollView.h"
@class TUIInputController;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIInputControllerDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIInputControllerDelegate <NSObject>
/**
* Callback when the current InputController height changes.
* You can use this callback to adjust the UI layout of each component in the controller according to the changed height.
*/
- (void)inputController:(TUIInputController *)inputController didChangeHeight:(CGFloat)height;
/**
* Callback when the current InputController sends a message.
*/
- (void)inputController:(TUIInputController *)inputController didSendMessage:(V2TIMMessage *)msg;
/**
* Callback for clicking a more item
* You can use this callback to achieve: according to the clicked cell type, do the next step. For example, select pictures, select files, etc.
* At the same time, the implementation of this delegate contains the following code:
* <pre>
* - (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell {
* ……
* ……
* if(_delegate && [_delegate respondsToSelector:@selector(chatController:onSelectMoreCell:)]){
* [_delegate chatController:self onSelectMoreCell:cell];
* }
* }
* </pre>
* The above code can help you to customize the "more" unit.
* For more information you can refer to the comments in TUIChat\UI\Chat\TUIBaseChatController.h
*/
- (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell;
/**
* Callback when @ character is entered
*/
- (void)inputControllerDidInputAt:(TUIInputController *)inputController;
/**
* Callback when there are @xxx characters removed
*/
- (void)inputController:(TUIInputController *)inputController didDeleteAt:(NSString *)atText;
- (void)inputControllerBeginTyping:(TUIInputController *)inputController;
- (void)inputControllerEndTyping:(TUIInputController *)inputController;
- (void)inputControllerDidClickMore:(TUIInputController *)inputController;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIInputControllerDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIInputController : UIViewController
/**
* A preview view above the input box for message reply scenarios
*/
@property(nonatomic, strong) TUIReplyPreviewBar *replyPreviewBar;
/**
* The preview view below the input box, with the message reference scene
*
*/
@property(nonatomic, strong) TUIReferencePreviewBar *referencePreviewBar;
/**
* Message currently being replied to
*/
@property(nonatomic, strong) TUIReplyPreviewData *replyData;
@property(nonatomic, strong) TUIReferencePreviewData *referenceData;
/**
* Input bar
* The input bar contains a series of interactive components such as text input box, voice button, "more" button, emoticon button, etc., and provides
* corresponding callbacks for these components.
*/
@property(nonatomic, strong) TUIInputBar *inputBar;
/**
* Emoticon view
* The emoticon view generally appears after clicking the "Smiley" button. Responsible for displaying each expression group and the expressions within the
* group.
*
*/
//@property(nonatomic, strong) TUIFaceView *faceView;
@property(nonatomic, strong) TUIFaceSegementScrollView *faceSegementScrollView;
/**
* Menu view
* The menu view is located below the emoticon view and is responsible for providing the emoticon grouping unit and the send button.
*/
@property(nonatomic, strong) TUIMenuView *menuView;
/**
* More view
* More views generally appear after clicking the "More" button ("+" button), and are responsible for displaying each more unit, such as shooting, video, file,
* album, etc.
*/
@property(nonatomic, strong) TUIMoreView *moreView;
@property(nonatomic, weak) id<TUIInputControllerDelegate> delegate;
/**
* Reset the current input controller.
* If there is currently an emoji view or a "more" view being displayed, collapse the corresponding view and set the current status to Input_Status_Input.
* That is, no matter what state the current InputController is in, reset it to its initialized state.
*/
- (void)reset;
/**
* Show/hide preview bar of message reply input box
*/
- (void)showReplyPreview:(TUIReplyPreviewData *)data;
- (void)showReferencePreview:(TUIReferencePreviewData *)data;
- (void)exitReplyAndReference:(void (^__nullable)(void))finishedCallback;
/**
* Current input box state
*/
@property(nonatomic, assign, readonly) InputStatus status;
@end

View File

@@ -0,0 +1,730 @@
//
// TInputController.m
// UIKit
//
// Created by kennethmiao on 2018/9/18.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIInputController.h"
#import <AVFoundation/AVFoundation.h>
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIChatDataProvider.h"
#import "TUIChatModifyMessageHelper.h"
#import "TUICloudCustomDataTypeCenter.h"
#import "TUIFaceMessageCell.h"
#import "TUIInputMoreCell.h"
#import "TUIMenuCell.h"
#import "TUIMenuCellData.h"
#import "TUIMessageDataProvider.h"
#import "TUITextMessageCell.h"
#import "TUIVoiceMessageCell.h"
#import <TIMCommon/TIMCommonMediator.h>
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
@interface TUIInputController () <TUIInputBarDelegate, TUIMenuViewDelegate, TUIFaceViewDelegate, TUIMoreViewDelegate>
@property(nonatomic, assign) InputStatus status;
@property(nonatomic, assign) CGRect keyboardFrame;
@property(nonatomic, copy) void (^modifyRootReplyMsgBlock)(TUIMessageCellData *);
@end
@implementation TUIInputController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupViews];
_inputBar.frame = CGRectMake(16, CGRectGetMaxY(self.replyPreviewBar.frame), self.view.frame.size.width - 32, TTextView_Height);
[_inputBar setNeedsLayout];
_menuView.frame = CGRectMake(16, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width - 32, TMenuView_Menu_Height);
[_menuView setNeedsLayout];
_faceSegementScrollView.frame = CGRectMake(0, _menuView.frame.origin.y + _menuView.frame.size.height, self.view.frame.size.width, TFaceView_Height);
[_faceSegementScrollView setNeedsLayout];
_moreView.frame = CGRectMake(0, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width, _moreView.frame.size.height);
[_moreView setNeedsLayout];
}
- (void)viewWillAppear:(BOOL)animated {
[_inputBar setNeedsLayout];
[_menuView setNeedsLayout];
[_faceSegementScrollView setNeedsLayout];
[_moreView setNeedsLayout];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMessageStatusChanged:) name:@"kTUINotifyMessageStatusChanged" object:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
for (UIGestureRecognizer *gesture in self.view.window.gestureRecognizers) {
NSLog(@"gesture = %@", gesture);
gesture.delaysTouchesBegan = NO;
NSLog(@"delaysTouchesBegan = %@", gesture.delaysTouchesBegan ? @"YES" : @"NO");
NSLog(@"delaysTouchesEnded = %@", gesture.delaysTouchesEnded ? @"YES" : @"NO");
}
self.navigationController.interactivePopGestureRecognizer.delaysTouchesBegan = NO;
}
- (void)viewDidDisappear:(BOOL)animated {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setupViews {
self.view.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
_status = Input_Status_Input;
_inputBar = [[TUIInputBar alloc] initWithFrame:CGRectZero];
_inputBar.delegate = self;
[self.view addSubview:_inputBar];
}
- (void)keyboardWillHide:(NSNotification *)notification {
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
CGFloat inputContainerBottom = [self getInputContainerBottom];
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
}
if (_status == Input_Status_Input_Keyboard) {
_status = Input_Status_Input;
}
}
- (void)keyboardWillShow:(NSNotification *)notification {
if (_status == Input_Status_Input_Face) {
[self hideFaceAnimation];
} else if (_status == Input_Status_Input_More) {
[self hideMoreAnimation];
} else {
//[self hideFaceAnimation:NO];
//[self hideMoreAnimation:NO];
}
_status = Input_Status_Input_Keyboard;
}
- (void)keyboardWillChangeFrame:(NSNotification *)notification {
CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
CGFloat inputContainerBottom = [self getInputContainerBottom];
[_delegate inputController:self didChangeHeight:keyboardFrame.size.height + inputContainerBottom];
}
self.keyboardFrame = keyboardFrame;
}
- (void)hideFaceAnimation {
self.faceSegementScrollView.hidden = NO;
self.faceSegementScrollView.alpha = 1.0;
self.menuView.hidden = NO;
self.menuView.alpha = 1.0;
__weak typeof(self) ws = self;
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
ws.faceSegementScrollView.alpha = 0.0;
ws.menuView.alpha = 0.0;
}
completion:^(BOOL finished) {
ws.faceSegementScrollView.hidden = YES;
ws.faceSegementScrollView.alpha = 1.0;
ws.menuView.hidden = YES;
ws.menuView.alpha = 1.0;
[ws.menuView removeFromSuperview];
[ws.faceSegementScrollView removeFromSuperview];
}];
}
- (void)showFaceAnimation {
[self.view addSubview:self.faceSegementScrollView];
[self.view addSubview:self.menuView];
__weak typeof(self) ws = self;
[self.faceSegementScrollView updateRecentView];
[self.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:(self.inputBar.inputTextView.text.length > 0)?YES:NO];
self.faceSegementScrollView.onScrollCallback = ^(NSInteger indexPage) {
[ws.menuView scrollToMenuIndex:indexPage];
};
self.inputBar.inputBarTextChanged = ^(UITextView *textview) {
if(textview.text.length > 0) {
[ws.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:YES];
}
else {
[ws.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:NO];
}
};
self.faceSegementScrollView.hidden = NO;
CGRect frame = self.menuView.frame;
frame.origin.y = self.view.window.frame.size.height;
self.menuView.frame = frame;
self.menuView.hidden = NO;
frame = self.faceSegementScrollView.frame;
frame.origin.y = self.menuView.frame.origin.y + self.menuView.frame.size.height;
self.faceSegementScrollView.frame = frame;
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect newFrame = ws.menuView.frame;
newFrame.origin.y = CGRectGetMaxY(ws.inputBar.frame); // ws.inputBar.frame.origin.y + ws.inputBar.frame.size.height;
ws.menuView.frame = newFrame;
newFrame = ws.faceSegementScrollView.frame;
newFrame.origin.y = ws.menuView.frame.origin.y + ws.menuView.frame.size.height;
ws.faceSegementScrollView.frame = newFrame;
}
completion:nil];
}
- (void)hideMoreAnimation {
self.moreView.hidden = NO;
self.moreView.alpha = 1.0;
__weak typeof(self) ws = self;
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
ws.moreView.alpha = 0.0;
}
completion:^(BOOL finished) {
ws.moreView.hidden = YES;
ws.moreView.alpha = 1.0;
[ws.moreView removeFromSuperview];
}];
}
- (void)showMoreAnimation {
[self.view addSubview:self.moreView];
self.moreView.hidden = NO;
CGRect frame = self.moreView.frame;
frame.origin.y = self.view.window.frame.size.height;
self.moreView.frame = frame;
__weak typeof(self) ws = self;
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect newFrame = ws.moreView.frame;
newFrame.origin.y = ws.inputBar.frame.origin.y + ws.inputBar.frame.size.height;
ws.moreView.frame = newFrame;
}
completion:nil];
}
- (void)inputBarDidTouchVoice:(TUIInputBar *)textView {
if (_status == Input_Status_Input_Talk) {
return;
}
[_inputBar.inputTextView resignFirstResponder];
[self hideFaceAnimation];
[self hideMoreAnimation];
_status = Input_Status_Input_Talk;
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
CGFloat inputContainerBottom = [self getInputContainerBottom];
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
}
}
- (void)inputBarDidTouchMore:(TUIInputBar *)textView {
if (_status == Input_Status_Input_More) {
return;
}
if (_status == Input_Status_Input_Face) {
[self hideFaceAnimation];
}
[_inputBar.inputTextView resignFirstResponder];
[self showMoreAnimation];
_status = Input_Status_Input_More;
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[_delegate inputController:self didChangeHeight:CGRectGetMaxY(_inputBar.frame) + self.moreView.frame.size.height + Bottom_SafeHeight];
}
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerDidClickMore:)]) {
[_delegate inputControllerDidClickMore:self];
}
}
- (void)inputBarDidTouchFace:(TUIInputBar *)textView {
if ([TIMConfig defaultConfig].faceGroups.count == 0) {
return;
}
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (_status == Input_Status_Input_More) {
[self hideMoreAnimation];
}
[_inputBar.inputTextView resignFirstResponder];
_status = Input_Status_Input_Face;
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[_delegate inputController:self
didChangeHeight:CGRectGetMaxY(_inputBar.frame) + self.faceSegementScrollView.frame.size.height + self.menuView.frame.size.height ];
}
[self showFaceAnimation];
}
- (void)inputBarDidTouchKeyboard:(TUIInputBar *)textView {
if (_status == Input_Status_Input_More) {
[self hideMoreAnimation];
}
if (_status == Input_Status_Input_Face) {
[self hideFaceAnimation];
}
_status = Input_Status_Input_Keyboard;
[_inputBar.inputTextView becomeFirstResponder];
}
- (void)inputBar:(TUIInputBar *)textView didChangeInputHeight:(CGFloat)offset {
if (_status == Input_Status_Input_Face) {
[self showFaceAnimation];
} else if (_status == Input_Status_Input_More) {
[self showMoreAnimation];
}
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[_delegate inputController:self didChangeHeight:self.view.frame.size.height + offset];
if (_referencePreviewBar) {
CGRect referencePreviewBarFrame = _referencePreviewBar.frame;
_referencePreviewBar.frame = CGRectMake(referencePreviewBarFrame.origin.x, referencePreviewBarFrame.origin.y + offset,
referencePreviewBarFrame.size.width, referencePreviewBarFrame.size.height);
}
}
}
- (void)inputBar:(TUIInputBar *)textView didSendText:(NSString *)text {
/**
* Emoticon internationalization --> restore to actual Chinese key
*/
NSString *content = [text getInternationalStringWithfaceContent];
V2TIMMessage *message = [[V2TIMManager sharedInstance] createTextMessage:content];
[self appendReplyDataIfNeeded:message];
[self appendReferenceDataIfNeeded:message];
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
[_delegate inputController:self didSendMessage:message];
}
}
- (void)inputMessageStatusChanged:(NSNotification *)noti {
NSDictionary *userInfo = noti.userInfo;
TUIMessageCellData *msg = userInfo[@"msg"];
long status = [userInfo[@"status"] intValue];
if ([msg isKindOfClass:TUIMessageCellData.class] && status == Msg_Status_Succ) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.modifyRootReplyMsgBlock) {
self.modifyRootReplyMsgBlock(msg);
}
});
}
}
- (void)appendReplyDataIfNeeded:(V2TIMMessage *)message {
if (self.replyData) {
V2TIMMessage *parentMsg = self.replyData.originMessage;
NSMutableDictionary *simpleReply = [NSMutableDictionary dictionary];
[simpleReply addEntriesFromDictionary:@{
@"messageID" : self.replyData.msgID ?: @"",
@"messageAbstract" : [self.replyData.msgAbstract ?: @"" getInternationalStringWithfaceContent],
@"messageSender" : self.replyData.sender ?: @"",
@"messageType" : @(self.replyData.type),
@"messageTime" : @(self.replyData.originMessage.timestamp ? [self.replyData.originMessage.timestamp timeIntervalSince1970] : 0),
@"messageSequence" : @(self.replyData.originMessage.seq),
@"version" : @(kMessageReplyVersion),
}];
NSMutableDictionary *cloudResultDic = [[NSMutableDictionary alloc] initWithCapacity:5];
if (parentMsg.cloudCustomData) {
NSDictionary *originDic = [TUITool jsonData2Dictionary:parentMsg.cloudCustomData];
if (originDic && [originDic isKindOfClass:[NSDictionary class]]) {
[cloudResultDic addEntriesFromDictionary:originDic];
}
/**
* Accept the data in the parent, but cannot save messageReplies\messageReact, because the root message topic creator has this field.
* messageReplies\messageReact cannot be stored in the new message currently sent
*/
[cloudResultDic removeObjectForKey:@"messageReplies"];
[cloudResultDic removeObjectForKey:@"messageReact"];
}
NSString *messageParentReply = cloudResultDic[@"messageReply"];
NSString *messageRootID = [messageParentReply valueForKey:@"messageRootID"];
if (self.replyData.messageRootID.length > 0) {
messageRootID = self.replyData.messageRootID;
}
if (!IS_NOT_EMPTY_NSSTRING(messageRootID)) {
/**
* If the original message does not have a messageRootID, you need to use the msgID of the current original message as root
*/
if (IS_NOT_EMPTY_NSSTRING(parentMsg.msgID)) {
messageRootID = parentMsg.msgID;
}
}
[simpleReply setObject:messageRootID forKey:@"messageRootID"];
[cloudResultDic setObject:simpleReply forKey:@"messageReply"];
NSData *data = [TUITool dictionary2JsonData:cloudResultDic];
if (data) {
message.cloudCustomData = data;
}
[self exitReplyAndReference:nil];
__weak typeof(self) weakSelf = self;
self.modifyRootReplyMsgBlock = ^(TUIMessageCellData *cellData) {
__strong typeof(self) strongSelf = weakSelf;
[strongSelf modifyRootReplyMsgByID:messageRootID currentMsg:cellData];
strongSelf.modifyRootReplyMsgBlock = nil;
};
}
}
- (void)modifyRootReplyMsgByID:(NSString *)messageRootID currentMsg:(TUIMessageCellData *)messageCellData {
NSDictionary *simpleCurrentContent = @{
@"messageID" : messageCellData.innerMessage.msgID ?: @"",
@"messageAbstract" : [messageCellData.innerMessage.textElem.text ?: @"" getInternationalStringWithfaceContent],
@"messageSender" : messageCellData.senderName ? : @"",
@"messageType" : @(messageCellData.innerMessage.elemType),
@"messageTime" : @(messageCellData.innerMessage.timestamp ? [messageCellData.innerMessage.timestamp timeIntervalSince1970] : 0),
@"messageSequence" : @(messageCellData.innerMessage.seq),
@"version" : @(kMessageReplyVersion)
};
if (messageRootID) {
[TUIChatDataProvider findMessages:@[ messageRootID ]
callback:^(BOOL succ, NSString *_Nonnull error_message, NSArray *_Nonnull msgs) {
if (succ) {
if (msgs.count > 0) {
V2TIMMessage *rootMsg = msgs.firstObject;
[[TUIChatModifyMessageHelper defaultHelper] modifyMessage:rootMsg simpleCurrentContent:simpleCurrentContent];
}
}
}];
}
}
- (void)appendReferenceDataIfNeeded:(V2TIMMessage *)message {
if (self.referenceData) {
NSDictionary *dict = @{
@"messageReply" : @{
@"messageID" : self.referenceData.msgID ?: @"",
@"messageAbstract" : [self.referenceData.msgAbstract ?: @"" getInternationalStringWithfaceContent],
@"messageSender" : self.referenceData.sender ?: @"",
@"messageType" : @(self.referenceData.type),
@"messageTime" : @(self.referenceData.originMessage.timestamp ? [self.referenceData.originMessage.timestamp timeIntervalSince1970] : 0),
@"messageSequence" : @(self.referenceData.originMessage.seq),
@"version" : @(kMessageReplyVersion)
}
};
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error];
if (error == nil) {
message.cloudCustomData = data;
}
[self exitReplyAndReference:nil];
}
}
- (void)inputBar:(TUIInputBar *)textView didSendVoice:(NSString *)path {
NSURL *url = [NSURL fileURLWithPath:path];
AVURLAsset *audioAsset = [AVURLAsset URLAssetWithURL:url options:nil];
float duration = (float)CMTimeGetSeconds(audioAsset.duration);
int formatDuration = duration > 59 ? 60 : duration + 1 ;
V2TIMMessage *message = [[V2TIMManager sharedInstance] createSoundMessage:path duration:formatDuration];
if (message && _delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
[_delegate inputController:self didSendMessage:message];
}
}
- (void)inputBarDidInputAt:(TUIInputBar *)textView {
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerDidInputAt:)]) {
[_delegate inputControllerDidInputAt:self];
}
}
- (void)inputBar:(TUIInputBar *)textView didDeleteAt:(NSString *)atText {
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didDeleteAt:)]) {
[_delegate inputController:self didDeleteAt:atText];
}
}
- (void)inputBarDidDeleteBackward:(TUIInputBar *)textView {
if (textView.inputTextView.text.length == 0) {
[self exitReplyAndReference:nil];
}
}
- (void)inputTextViewShouldBeginTyping:(UITextView *)textView {
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerBeginTyping:)]) {
[_delegate inputControllerBeginTyping:self];
}
}
- (void)inputTextViewShouldEndTyping:(UITextView *)textView {
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerEndTyping:)]) {
[_delegate inputControllerEndTyping:self];
}
}
- (void)reset {
if (_status == Input_Status_Input) {
return;
} else if (_status == Input_Status_Input_More) {
[self hideMoreAnimation];
} else if (_status == Input_Status_Input_Face) {
[self hideFaceAnimation];
}
_status = Input_Status_Input;
[_inputBar.inputTextView resignFirstResponder];
[TUICore notifyEvent:TUICore_TUIChatNotify
subKey:TUICore_TUIChatNotify_KeyboardWillHideSubKey
object:nil
param:nil];
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
CGFloat inputContainerBottom = [self getInputContainerBottom];
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
}
}
- (void)showReferencePreview:(TUIReferencePreviewData *)data {
self.referenceData = data;
[self.referencePreviewBar removeFromSuperview];
[self.view addSubview:self.referencePreviewBar];
self.inputBar.lineView.hidden = YES;
self.referencePreviewBar.previewReferenceData = data;
self.inputBar.mm_y = 0;
self.referencePreviewBar.frame = CGRectMake(0, 0, self.view.bounds.size.width, TMenuView_Menu_Height);
self.referencePreviewBar.mm_y = CGRectGetMaxY(self.inputBar.frame);
// Set the default position to solve the UI confusion when the keyboard does not become the first responder
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + Bottom_SafeHeight + TMenuView_Menu_Height];
}
if (self.status == Input_Status_Input_Keyboard) {
CGFloat keyboradHeight = self.keyboardFrame.size.height;
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.referencePreviewBar.frame) + keyboradHeight];
}
} else if (self.status == Input_Status_Input_Face || self.status == Input_Status_Input_Talk) {
[self.inputBar changeToKeyboard];
} else {
[self.inputBar.inputTextView becomeFirstResponder];
}
}
- (void)showReplyPreview:(TUIReplyPreviewData *)data {
self.replyData = data;
[self.replyPreviewBar removeFromSuperview];
[self.view addSubview:self.replyPreviewBar];
self.inputBar.lineView.hidden = YES;
self.replyPreviewBar.previewData = data;
self.replyPreviewBar.frame = CGRectMake(0, 0, self.view.bounds.size.width, TMenuView_Menu_Height);
self.inputBar.mm_y = CGRectGetMaxY(self.replyPreviewBar.frame);
// Set the default position to solve the UI confusion when the keyboard does not become the first responder
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + Bottom_SafeHeight];
}
if (self.status == Input_Status_Input_Keyboard) {
CGFloat keyboradHeight = self.keyboardFrame.size.height;
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + keyboradHeight];
}
} else if (self.status == Input_Status_Input_Face || self.status == Input_Status_Input_Talk) {
[self.inputBar changeToKeyboard];
} else {
[self.inputBar.inputTextView becomeFirstResponder];
}
}
- (void)exitReplyAndReference:(void (^__nullable)(void))finishedCallback {
if (self.replyData == nil && self.referenceData == nil) {
if (finishedCallback) {
finishedCallback();
}
return;
}
self.replyData = nil;
self.referenceData = nil;
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.25
animations:^{
weakSelf.replyPreviewBar.hidden = YES;
weakSelf.referencePreviewBar.hidden = YES;
weakSelf.inputBar.mm_y = 0;
if (weakSelf.status == Input_Status_Input_Keyboard) {
CGFloat keyboradHeight = weakSelf.keyboardFrame.size.height;
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[weakSelf.delegate inputController:weakSelf didChangeHeight:CGRectGetMaxY(weakSelf.inputBar.frame) + keyboradHeight];
}
} else {
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
[weakSelf.delegate inputController:weakSelf didChangeHeight:CGRectGetMaxY(weakSelf.inputBar.frame) + Bottom_SafeHeight];
}
}
}
completion:^(BOOL finished) {
[weakSelf.replyPreviewBar removeFromSuperview];
[weakSelf.referencePreviewBar removeFromSuperview];
weakSelf.replyPreviewBar = nil;
weakSelf.referencePreviewBar = nil;
[weakSelf hideFaceAnimation];
weakSelf.inputBar.lineView.hidden = NO;
if (finishedCallback) {
finishedCallback();
}
}];
}
- (void)menuView:(TUIMenuView *)menuView didSelectItemAtIndex:(NSInteger)index {
[self.faceSegementScrollView setPageIndex:index];
}
- (void)menuViewDidSendMessage:(TUIMenuView *)menuView {
NSString *text = [_inputBar getInput];
if ([text isEqualToString:@""]) {
return;
}
/**
* Emoticon internationalization --> restore to actual Chinese key
*/
NSString *content = [text getInternationalStringWithfaceContent];
[_inputBar clearInput];
V2TIMMessage *message = [[V2TIMManager sharedInstance] createTextMessage:content];
[self appendReplyDataIfNeeded:message];
[self appendReferenceDataIfNeeded:message];
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
[_delegate inputController:self didSendMessage:message];
}
}
- (void)faceView:(TUIFaceView *)faceView scrollToFaceGroupIndex:(NSInteger)index {
[self.menuView scrollToMenuIndex:index];
}
- (void)faceViewDidBackDelete:(TUIFaceView *)faceView {
[_inputBar backDelete];
}
- (void)faceViewClickSendMessageBtn {
[self menuViewDidSendMessage:self.menuView];
}
- (void)faceView:(TUIFaceView *)faceView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
TUIFaceGroup *group = faceView.faceGroups[indexPath.section];
TUIFaceCellData *face = group.faces[indexPath.row];
if (group.isNeedAddInInputBar) {
[_inputBar addEmoji:face];
[self updateRecentMenuQueue:face.name];
} else {
if (face.name) {
V2TIMMessage *message = [[V2TIMManager sharedInstance] createFaceMessage:group.groupIndex data:[face.name dataUsingEncoding:NSUTF8StringEncoding]];
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
[_delegate inputController:self didSendMessage:message];
}
}
}
}
- (void)updateRecentMenuQueue:(NSString *)faceName {
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
return [service updateRecentMenuQueue:faceName];
}
#pragma mark - more view delegate
- (void)moreView:(TUIMoreView *)moreView didSelectMoreCell:(TUIInputMoreCell *)cell {
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSelectMoreCell:)]) {
[_delegate inputController:self didSelectMoreCell:cell];
}
}
#pragma mark - lazy load
- (TUIFaceSegementScrollView *)faceSegementScrollView {
if(!_faceSegementScrollView) {
_faceSegementScrollView = [[TUIFaceSegementScrollView alloc]
initWithFrame:CGRectMake(0,
_inputBar.frame.origin.y + _inputBar.frame.size.height,
self.view.frame.size.width,
TFaceView_Height)];
[_faceSegementScrollView setItems:[TIMConfig defaultConfig].faceGroups delegate:self];
}
return _faceSegementScrollView;
}
- (TUIMoreView *)moreView {
if (!_moreView) {
_moreView =
[[TUIMoreView alloc] initWithFrame:CGRectMake(0,
_inputBar.frame.origin.y + _inputBar.frame.size.height,
_faceSegementScrollView.frame.size.width,
0)];
_moreView.delegate = self;
}
return _moreView;
}
- (TUIMenuView *)menuView {
if (!_menuView) {
_menuView = [[TUIMenuView alloc]
initWithFrame:CGRectMake(16, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width - 32, TMenuView_Menu_Height)];
_menuView.delegate = self;
TIMConfig *config = [TIMConfig defaultConfig];
NSMutableArray *menus = [NSMutableArray array];
for (NSInteger i = 0; i < config.faceGroups.count; ++i) {
TUIFaceGroup *group = config.faceGroups[i];
TUIMenuCellData *data = [[TUIMenuCellData alloc] init];
data.path = group.menuPath;
data.isSelected = NO;
if (i == 0) {
data.isSelected = YES;
}
[menus addObject:data];
}
[_menuView setData:menus];
}
return _menuView;
}
- (TUIReplyPreviewBar *)replyPreviewBar {
if (_replyPreviewBar == nil) {
_replyPreviewBar = [[TUIReplyPreviewBar alloc] init];
__weak typeof(self) weakSelf = self;
_replyPreviewBar.onClose = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf exitReplyAndReference:nil];
};
}
return _replyPreviewBar;
}
- (TUIReferencePreviewBar *)referencePreviewBar {
if (_referencePreviewBar == nil) {
_referencePreviewBar = [[TUIReferencePreviewBar alloc] init];
__weak typeof(self) weakSelf = self;
_referencePreviewBar.onClose = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf exitReplyAndReference:nil];
};
}
return _referencePreviewBar;
}
- (CGFloat)getInputContainerBottom {
CGFloat inputHeight = CGRectGetMaxY(_inputBar.frame);
if (_referencePreviewBar) {
inputHeight = CGRectGetMaxY(_referencePreviewBar.frame);
}
return inputHeight;
}
@end

View File

@@ -0,0 +1,61 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This file declares the components used to implement the emoji menu view.
* The emoji menu view, the bright white view at the bottom of the emoji view, is responsible for displaying individual emoji groups and their thumbnails, and
* providing a "Send" button.
*
* The TUIMenuViewDelegate protocol provides the emoticon menu view with event callbacks for sending messages and cell selection.
* The TUIMenuView class, the "ontology" of the emoticon menu view, is responsible for displaying it in the form of a view in the UI, and at the same time
* serving as a "container" for each component. You can switch between different groups of emoticons or send emoticons through the emoticon menu view.
*/
#import <UIKit/UIKit.h>
#import "TUIMenuCellData.h"
@class TUIMenuView;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMenuViewDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIMenuViewDelegate <NSObject>
/**
* Callback after clicking on a specific menuCell
* You can use this callback to achieve: in response to the user's click, switch to the corresponding emoticon group view according to the menuCell selected by
* the user.
*/
- (void)menuView:(TUIMenuView *)menuView didSelectItemAtIndex:(NSInteger)index;
/**
* Callback after click of send button on menuView
* You can send the content of the current input box (TUIInputBar) through this callback
* In the default implementation of TUIKit, the delegate call chain is menuView -> inputController -> messageController.
* Call the sendMessage function in the above classes respectively, so that the functions are reasonably layered and the code reuse rate is improved.
*/
- (void)menuViewDidSendMessage:(TUIMenuView *)menuView;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMenuView
//
/////////////////////////////////////////////////////////////////////////////////
///
@interface TUIMenuView : UIView
@property(nonatomic, strong) UICollectionView *menuCollectionView;
@property(nonatomic, strong) UICollectionViewFlowLayout *menuFlowLayout;
@property(nonatomic, weak) id<TUIMenuViewDelegate> delegate;
- (void)scrollToMenuIndex:(NSInteger)index;
- (void)setData:(NSMutableArray<TUIMenuCellData *> *)data;
@end

View File

@@ -0,0 +1,123 @@
//
// MenuView.m
// UIKit
//
// Created by kennethmiao on 2018/9/18.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIMenuView.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIGlobalization.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMenuCell.h"
@interface TUIMenuView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property(nonatomic, strong) NSMutableArray<TUIMenuCellData *> *data;
@end
@implementation TUIMenuView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
}
return self;
}
- (void)setData:(NSMutableArray<TUIMenuCellData *> *)data {
_data = data;
[_menuCollectionView reloadData];
[self defaultLayout];
[_menuCollectionView layoutIfNeeded];
[_menuCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] animated:NO scrollPosition:UICollectionViewScrollPositionNone];
}
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
_menuFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
_menuFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_menuFlowLayout.minimumLineSpacing = 0;
_menuFlowLayout.minimumInteritemSpacing = 0;
//_menuFlowLayout.headerReferenceSize = CGSizeMake(TMenuView_Margin, 1);
_menuCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_menuFlowLayout];
[_menuCollectionView registerClass:[TUIMenuCell class] forCellWithReuseIdentifier:TMenuCell_ReuseId];
[_menuCollectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:TMenuCell_Line_ReuseId];
_menuCollectionView.collectionViewLayout = _menuFlowLayout;
_menuCollectionView.delegate = self;
_menuCollectionView.dataSource = self;
_menuCollectionView.showsHorizontalScrollIndicator = NO;
_menuCollectionView.showsVerticalScrollIndicator = NO;
_menuCollectionView.backgroundColor = self.backgroundColor;
_menuCollectionView.alwaysBounceHorizontal = YES;
_menuCollectionView.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
[self addSubview:_menuCollectionView];
}
- (void)defaultLayout {
[_menuCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(0);
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(0);
make.height.mas_equalTo(40);
make.centerY.mas_equalTo(self);
}];
}
- (void)sendUpInside:(UIButton *)sender {
if (_delegate && [_delegate respondsToSelector:@selector(menuViewDidSendMessage:)]) {
[_delegate menuViewDidSendMessage:self];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _data.count * 2;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 == 0) {
TUIMenuCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMenuCell_ReuseId forIndexPath:indexPath];
[cell setData:_data[indexPath.row / 2]];
return cell;
} else {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMenuCell_Line_ReuseId forIndexPath:indexPath];
cell.backgroundColor = [UIColor clearColor];
return cell;
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 != 0) {
return;
}
for (NSInteger i = 0; i < _data.count; ++i) {
TUIMenuCellData *data = _data[i];
data.isSelected = (i == indexPath.row / 2);
}
[_menuCollectionView reloadData];
if (_delegate && [_delegate respondsToSelector:@selector(menuView:didSelectItemAtIndex:)]) {
[_delegate menuView:self didSelectItemAtIndex:indexPath.row / 2];
}
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 == 0) {
CGFloat wh = collectionView.frame.size.height;
return CGSizeMake(wh, wh);
} else {
return CGSizeMake(TLine_Heigh, collectionView.frame.size.height);
}
}
- (void)scrollToMenuIndex:(NSInteger)index {
for (NSInteger i = 0; i < _data.count; ++i) {
TUIMenuCellData *data = _data[i];
data.isSelected = (i == index);
}
[_menuCollectionView reloadData];
}
@end

View File

@@ -0,0 +1,61 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This file declares the components used to implement the "more" view.
* More view, the window that appears when you click the "+" button in the lower right corner of the chat window.
* More view are usually responsible for providing some additional important functions, such as sending pictures, taking pictures and sending, sending videos,
* sending files, etc. Currently TUIKit implements and provides the above four functions. If the above 4 functions cannot meet your functional requirements, you
* can also add your custom unit in this view.
*
* TUIMoreView provides an entry for sending multimedia messages such as videos, pictures, and files based on the existing text messaging.
* The TUIMoreViewDelegate protocol provides callbacks for more views in response to user actions.
*/
#import <UIKit/UIKit.h>
#import "TUIInputMoreCell.h"
@class TUIMoreView;
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMoreViewDelegate
//
/////////////////////////////////////////////////////////////////////////////////
@protocol TUIMoreViewDelegate <NSObject>
- (void)moreView:(TUIMoreView *)moreView didSelectMoreCell:(TUIInputMoreCell *)cell;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIMoreView
//
/////////////////////////////////////////////////////////////////////////////////
/**
* 【Module name】 TUIMoreView
* 【Function description】More view are displayed after clicking the "+" on the far right of the input box.
* This view can provide you with functional extensions on the current page. for example:
* 1. Camera. Call the system camera to take a photo and send
* 2. Photo. Select a picture from the system album and send it.
* 3. Video. Select a video from the system gallery and send it.
* 4. File. Select file from system files and send.
*/
@interface TUIMoreView : UIView
@property(nonatomic, strong) UIView *lineView;
@property(nonatomic, strong) UICollectionView *moreCollectionView;
@property(nonatomic, strong) UICollectionViewFlowLayout *moreFlowLayout;
@property(nonatomic, strong) UIPageControl *pageControl;
@property(nonatomic, weak) id<TUIMoreViewDelegate> delegate;
- (void)setData:(NSArray *)data;
@end

View File

@@ -0,0 +1,171 @@
//
// TUIMoreView.m
// UIKit
//
// Created by kennethmiao on 2018/9/21.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIMoreView.h"
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIInputMoreCell.h"
@interface TUIMoreView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property(nonatomic, strong) NSArray *data;
@property(nonatomic, strong) NSMutableDictionary *itemIndexs;
@property(nonatomic, assign) NSInteger sectionCount;
@property(nonatomic, assign) NSInteger itemsInSection;
@property(nonatomic, assign) NSInteger rowCount;
@end
@implementation TUIMoreView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupViews];
[self defaultLayout];
}
return self;
}
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
_moreFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
_moreFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_moreFlowLayout.minimumLineSpacing = 0;
_moreFlowLayout.minimumInteritemSpacing = 0;
_moreFlowLayout.sectionInset = UIEdgeInsetsMake(0, TMoreView_Section_Padding, 0, TMoreView_Section_Padding);
_moreCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_moreFlowLayout];
[_moreCollectionView registerClass:[TUIInputMoreCell class] forCellWithReuseIdentifier:TMoreCell_ReuseId];
_moreCollectionView.collectionViewLayout = _moreFlowLayout;
_moreCollectionView.pagingEnabled = YES;
_moreCollectionView.delegate = self;
_moreCollectionView.dataSource = self;
_moreCollectionView.showsHorizontalScrollIndicator = NO;
_moreCollectionView.showsVerticalScrollIndicator = NO;
_moreCollectionView.backgroundColor = self.backgroundColor;
_moreCollectionView.alwaysBounceHorizontal = YES;
[self addSubview:_moreCollectionView];
_lineView = [[UIView alloc] init];
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
[self addSubview:_lineView];
_pageControl = [[UIPageControl alloc] init];
_pageControl.currentPageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_current_color", @"#7D7D7D");
_pageControl.pageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_color", @"#DEDEDE");
[self addSubview:_pageControl];
}
- (void)defaultLayout {
CGSize cellSize = [TUIInputMoreCell getSize];
CGFloat collectionHeight = cellSize.height * _rowCount + TMoreView_Margin * (_rowCount - 1);
_lineView.frame = CGRectMake(0, 0, self.frame.size.width, TLine_Heigh);
_moreCollectionView.frame =
CGRectMake(0, _lineView.frame.origin.y + _lineView.frame.size.height + TMoreView_Margin, self.frame.size.width, collectionHeight);
if (_sectionCount > 1) {
_pageControl.frame =
CGRectMake(0, _moreCollectionView.frame.origin.y + _moreCollectionView.frame.size.height, self.frame.size.width, TMoreView_Page_Height);
_pageControl.hidden = NO;
} else {
_pageControl.hidden = YES;
}
if (_rowCount > 1) {
_moreFlowLayout.minimumInteritemSpacing = (_moreCollectionView.frame.size.height - cellSize.height * _rowCount) / (_rowCount - 1);
}
CGFloat margin = TMoreView_Section_Padding;
CGFloat spacing = (_moreCollectionView.frame.size.width - cellSize.width * TMoreView_Column_Count - 2 * margin) / (TMoreView_Column_Count - 1);
_moreFlowLayout.minimumLineSpacing = spacing;
_moreFlowLayout.sectionInset = UIEdgeInsetsMake(0, margin, 0, margin);
CGFloat height = _moreCollectionView.frame.origin.y + _moreCollectionView.frame.size.height + TMoreView_Margin;
if (_sectionCount > 1) {
height = _pageControl.frame.origin.y + _pageControl.frame.size.height;
}
CGRect frame = self.frame;
frame.size.height = height;
self.frame = frame;
}
- (void)setData:(NSArray *)data {
_data = data;
if (_data.count > TMoreView_Column_Count) {
_rowCount = 2;
} else {
_rowCount = 1;
}
_itemsInSection = TMoreView_Column_Count * _rowCount;
_sectionCount = ceil(_data.count * 1.0 / _itemsInSection);
_pageControl.numberOfPages = _sectionCount;
_itemIndexs = [NSMutableDictionary dictionary];
for (NSInteger curSection = 0; curSection < _sectionCount; ++curSection) {
for (NSInteger itemIndex = 0; itemIndex < _itemsInSection; ++itemIndex) {
// transpose line/row
NSInteger row = itemIndex % _rowCount;
NSInteger column = itemIndex / _rowCount;
NSInteger reIndex = TMoreView_Column_Count * row + column + curSection * _itemsInSection;
[_itemIndexs setObject:@(reIndex) forKey:[NSIndexPath indexPathForRow:itemIndex inSection:curSection]];
}
}
[_moreCollectionView reloadData];
[self defaultLayout];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return _sectionCount;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _itemsInSection;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TUIInputMoreCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMoreCell_ReuseId forIndexPath:indexPath];
TUIInputMoreCellData *data;
NSNumber *index = _itemIndexs[indexPath];
if (index.integerValue >= _data.count) {
data = nil;
} else {
data = _data[index.integerValue];
}
[cell fillWithData:data];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
if (_delegate && [_delegate respondsToSelector:@selector(moreView:didSelectMoreCell:)]) {
if ([cell isKindOfClass:[TUIInputMoreCell class]]) {
[_delegate moreView:self didSelectMoreCell:(TUIInputMoreCell *)cell];
;
}
}
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return [TUIInputMoreCell getSize];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat contentOffset = scrollView.contentOffset.x;
float page = contentOffset / scrollView.frame.size.width;
if ((int)(page * 10) % 10 == 0) {
_pageControl.currentPage = page;
}
}
@end

Some files were not shown because too many files have changed in this diff Show More