首次提交
This commit is contained in:
16
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIGroupCreatedCell.h
Normal file
16
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIGroupCreatedCell.h
Normal 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
|
||||
17
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIGroupCreatedCell.m
Normal file
17
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIGroupCreatedCell.m
Normal 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
|
||||
48
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIInputMoreCell.h
Normal file
48
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIInputMoreCell.h
Normal 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
|
||||
67
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIInputMoreCell.m
Normal file
67
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIInputMoreCell.m
Normal 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
|
||||
16
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIMemberCell.h
Normal file
16
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIMemberCell.h
Normal 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
|
||||
115
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIMemberCell.m
Normal file
115
TUIKit/TUIChat/UI_Classic/Cell/Base/TUIMemberCell.m
Normal 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
|
||||
75
TUIKit/TUIChat/UI_Classic/Cell/Base/TUITextMessageCell.h
Normal file
75
TUIKit/TUIChat/UI_Classic/Cell/Base/TUITextMessageCell.h
Normal 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
|
||||
339
TUIKit/TUIChat/UI_Classic/Cell/Base/TUITextMessageCell.m
Normal file
339
TUIKit/TUIChat/UI_Classic/Cell/Base/TUITextMessageCell.m
Normal 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
|
||||
20
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFaceMessageCell.h
Normal file
20
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFaceMessageCell.h
Normal 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
|
||||
100
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFaceMessageCell.m
Normal file
100
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFaceMessageCell.m
Normal 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
|
||||
36
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFileMessageCell.h
Normal file
36
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFileMessageCell.h
Normal 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
|
||||
425
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFileMessageCell.m
Normal file
425
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIFileMessageCell.m
Normal 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
|
||||
18
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageCollectionCell.h
Normal file
18
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageCollectionCell.h
Normal 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
|
||||
423
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageCollectionCell.m
Normal file
423
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageCollectionCell.m
Normal 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
|
||||
17
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageMessageCell.h
Normal file
17
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageMessageCell.h
Normal 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
|
||||
300
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageMessageCell.m
Normal file
300
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIImageMessageCell.m
Normal 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
|
||||
@@ -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
|
||||
145
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIJoinGroupMessageCell.m
Normal file
145
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIJoinGroupMessageCell.m
Normal 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
|
||||
39
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMediaCollectionCell.h
Normal file
39
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMediaCollectionCell.h
Normal 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
|
||||
31
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMediaCollectionCell.m
Normal file
31
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMediaCollectionCell.m
Normal 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
|
||||
19
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMenuCell.h
Normal file
19
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMenuCell.h
Normal 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
|
||||
46
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMenuCell.m
Normal file
46
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMenuCell.m
Normal 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
|
||||
41
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMergeMessageCell.h
Normal file
41
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMergeMessageCell.h
Normal 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
|
||||
* | vinson:When will the new version of the SDK be released? | -- abstract1
|
||||
* | lynx:Plan for next Monday, the specific time depends on the system test situation in these two days.. | -- abstract2
|
||||
* | vinson:Okay.
|
||||
*/
|
||||
|
||||
#import <TIMCommon/TUIMessageCell.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
|
||||
381
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMergeMessageCell.m
Normal file
381
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIMergeMessageCell.m
Normal 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
|
||||
16
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoCollectionCell.h
Normal file
16
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoCollectionCell.h
Normal 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
|
||||
532
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoCollectionCell.m
Normal file
532
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoCollectionCell.m
Normal 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
|
||||
49
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoMessageCell.h
Normal file
49
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoMessageCell.h
Normal 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
|
||||
419
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoMessageCell.m
Normal file
419
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVideoMessageCell.m
Normal 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
|
||||
48
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVoiceMessageCell.h
Normal file
48
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVoiceMessageCell.h
Normal 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
|
||||
230
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVoiceMessageCell.m
Normal file
230
TUIKit/TUIChat/UI_Classic/Cell/Chat/TUIVoiceMessageCell.m
Normal 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
|
||||
24
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIEvaluationCell.h
Normal file
24
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIEvaluationCell.h
Normal 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
|
||||
141
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIEvaluationCell.m
Normal file
141
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIEvaluationCell.m
Normal 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
|
||||
23
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUILinkCell.h
Normal file
23
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUILinkCell.h
Normal 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
|
||||
95
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUILinkCell.m
Normal file
95
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUILinkCell.m
Normal 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
|
||||
26
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIOrderCell.h
Normal file
26
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIOrderCell.h
Normal 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
|
||||
112
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIOrderCell.m
Normal file
112
TUIKit/TUIChat/UI_Classic/Cell/Custom/TUIOrderCell.m
Normal 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
|
||||
17
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIFileReplyQuoteView.h
Normal file
17
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIFileReplyQuoteView.h
Normal 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
|
||||
22
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIFileReplyQuoteView.m
Normal file
22
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIFileReplyQuoteView.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
488
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReferenceMessageCell.m
Normal file
488
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReferenceMessageCell.m
Normal 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
|
||||
38
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyMessageCell.h
Normal file
38
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyMessageCell.h
Normal 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
|
||||
484
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyMessageCell.m
Normal file
484
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyMessageCell.m
Normal 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
|
||||
23
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyQuoteView.h
Normal file
23
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyQuoteView.h
Normal 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
|
||||
22
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyQuoteView.m
Normal file
22
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUIReplyQuoteView.m
Normal 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
|
||||
19
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUITextReplyQuoteView.h
Normal file
19
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUITextReplyQuoteView.h
Normal 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
|
||||
72
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUITextReplyQuoteView.m
Normal file
72
TUIKit/TUIChat/UI_Classic/Cell/Reply/TUITextReplyQuoteView.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
121
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseChatViewController.h
Normal file
121
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseChatViewController.h
Normal 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
|
||||
1548
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseChatViewController.m
Normal file
1548
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseChatViewController.m
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
109
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseMessageController.h
Normal file
109
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseMessageController.h
Normal 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
|
||||
1931
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseMessageController.m
Normal file
1931
TUIKit/TUIChat/UI_Classic/Chat/TUIBaseMessageController.m
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
17
TUIKit/TUIChat/UI_Classic/Chat/TUIC2CChatViewController.h
Normal file
17
TUIKit/TUIChat/UI_Classic/Chat/TUIC2CChatViewController.h
Normal 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
|
||||
159
TUIKit/TUIChat/UI_Classic/Chat/TUIC2CChatViewController.m
Normal file
159
TUIKit/TUIChat/UI_Classic/Chat/TUIC2CChatViewController.m
Normal 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
|
||||
50
TUIKit/TUIChat/UI_Classic/Chat/TUIChatSmallTongueView.h
Normal file
50
TUIKit/TUIChat/UI_Classic/Chat/TUIChatSmallTongueView.h
Normal 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
|
||||
230
TUIKit/TUIChat/UI_Classic/Chat/TUIChatSmallTongueView.m
Normal file
230
TUIKit/TUIChat/UI_Classic/Chat/TUIChatSmallTongueView.m
Normal 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
|
||||
13
TUIKit/TUIChat/UI_Classic/Chat/TUIFileViewController.h
Normal file
13
TUIKit/TUIChat/UI_Classic/Chat/TUIFileViewController.h
Normal 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
|
||||
117
TUIKit/TUIChat/UI_Classic/Chat/TUIFileViewController.m
Normal file
117
TUIKit/TUIChat/UI_Classic/Chat/TUIFileViewController.m
Normal 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
|
||||
16
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupChatViewController.h
Normal file
16
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupChatViewController.h
Normal 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
|
||||
437
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupChatViewController.m
Normal file
437
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupChatViewController.m
Normal 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
|
||||
23
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoController.h
Normal file
23
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoController.h
Normal 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
|
||||
602
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoController.m
Normal file
602
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoController.m
Normal 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
|
||||
62
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoDataProvider.h
Normal file
62
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoDataProvider.h
Normal 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
|
||||
748
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoDataProvider.m
Normal file
748
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupInfoDataProvider.m
Normal 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
|
||||
19
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupNoticeController.h
Normal file
19
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupNoticeController.h
Normal 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
|
||||
114
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupNoticeController.m
Normal file
114
TUIKit/TUIChat/UI_Classic/Chat/TUIGroupNoticeController.m
Normal 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
|
||||
29
TUIKit/TUIChat/UI_Classic/Chat/TUIMediaView.h
Normal file
29
TUIKit/TUIChat/UI_Classic/Chat/TUIMediaView.h
Normal 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
|
||||
279
TUIKit/TUIChat/UI_Classic/Chat/TUIMediaView.m
Normal file
279
TUIKit/TUIChat/UI_Classic/Chat/TUIMediaView.m
Normal 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
|
||||
@@ -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
|
||||
599
TUIKit/TUIChat/UI_Classic/Chat/TUIMergeMessageListController.m
Normal file
599
TUIKit/TUIChat/UI_Classic/Chat/TUIMergeMessageListController.m
Normal 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
|
||||
106
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageCellConfig.h
Normal file
106
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageCellConfig.h
Normal 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
|
||||
251
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageCellConfig.m
Normal file
251
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageCellConfig.m
Normal 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
|
||||
30
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageController.h
Normal file
30
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageController.h
Normal 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
|
||||
587
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageController.m
Normal file
587
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageController.m
Normal 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
|
||||
67
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageMultiChooseView.h
Normal file
67
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageMultiChooseView.h
Normal 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
|
||||
159
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageMultiChooseView.m
Normal file
159
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageMultiChooseView.m
Normal 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
|
||||
@@ -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
|
||||
656
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageReadViewController.m
Normal file
656
TUIKit/TUIChat/UI_Classic/Chat/TUIMessageReadViewController.m
Normal 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
|
||||
@@ -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
|
||||
755
TUIKit/TUIChat/UI_Classic/Chat/TUIRepliesDetailViewController.m
Normal file
755
TUIKit/TUIChat/UI_Classic/Chat/TUIRepliesDetailViewController.m
Normal 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
|
||||
356
TUIKit/TUIChat/UI_Classic/Config/TUIChatConfig_Classic.h
Normal file
356
TUIKit/TUIChat/UI_Classic/Config/TUIChatConfig_Classic.h
Normal 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 message‘s 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
|
||||
591
TUIKit/TUIChat/UI_Classic/Config/TUIChatConfig_Classic.m
Normal file
591
TUIKit/TUIChat/UI_Classic/Config/TUIChatConfig_Classic.m
Normal 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
|
||||
15
TUIKit/TUIChat/UI_Classic/Header/TUIChat.h
Normal file
15
TUIKit/TUIChat/UI_Classic/Header/TUIChat.h
Normal 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 */
|
||||
177
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.h
Normal file
177
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.h
Normal 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
|
||||
696
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.m
Normal file
696
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.m
Normal 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
|
||||
153
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.h
Normal file
153
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.h
Normal 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
|
||||
730
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.m
Normal file
730
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.m
Normal 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
|
||||
61
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.h
Normal file
61
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.h
Normal 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
|
||||
123
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.m
Normal file
123
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.m
Normal 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
|
||||
61
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.h
Normal file
61
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.h
Normal 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
|
||||
171
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.m
Normal file
171
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.m
Normal 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
Reference in New Issue
Block a user