提交
This commit is contained in:
17
TUIKit/TUIChat/BaseCell/TUIChatPopActionsView.h
Normal file
17
TUIKit/TUIChat/BaseCell/TUIChatPopActionsView.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TUIChatPopActionsView.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/6/13.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIChatPopActionsView : UIView
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
28
TUIKit/TUIChat/BaseCell/TUIChatPopActionsView.m
Normal file
28
TUIKit/TUIChat/BaseCell/TUIChatPopActionsView.m
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// TUIChatPopActionsView.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/6/13.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatPopActionsView.h"
|
||||
|
||||
@implementation TUIChatPopActionsView
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
[self updateCorner];
|
||||
}
|
||||
- (void)updateCorner {
|
||||
UIRectCorner corner = UIRectCornerBottomLeft | UIRectCornerBottomRight;
|
||||
CGRect containerBounds = self.bounds;
|
||||
CGRect bounds = CGRectMake(containerBounds.origin.x, containerBounds.origin.y - 1, containerBounds.size.width, containerBounds.size.height);
|
||||
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:corner cornerRadii:CGSizeMake(5, 5)];
|
||||
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
|
||||
maskLayer.frame = self.bounds;
|
||||
maskLayer.path = maskPath.CGPath;
|
||||
self.layer.mask = maskLayer;
|
||||
}
|
||||
@end
|
||||
56
TUIKit/TUIChat/BaseCell/TUIChatPopMenu.h
Normal file
56
TUIKit/TUIChat/BaseCell/TUIChatPopMenu.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// TUIChatPopMenu.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/30.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIChatConfig.h"
|
||||
#import "TUIChatPopMenuDefine.h"
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCell.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^TUIChatPopMenuActionCallback)(void);
|
||||
|
||||
@interface TUIChatPopMenuAction : NSObject
|
||||
|
||||
@property(nonatomic, copy) NSString *title;
|
||||
@property(nonatomic, strong) UIImage *image;
|
||||
@property(nonatomic, copy) TUIChatPopMenuActionCallback callback;
|
||||
|
||||
/**
|
||||
* The higher the weight, the more prominent it is: audioPlayback 11000 Copy 10000, Forward 9000, Multiple Choice 8000, Quote 7000, Reply 5000, Withdraw 4000, Delete 3000.
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger weight;
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title image:(UIImage *)image weight:(NSInteger)weight callback:(TUIChatPopMenuActionCallback)callback;
|
||||
@end
|
||||
|
||||
typedef void (^TUIChatPopMenuHideCallback)(void);
|
||||
@interface TUIChatPopMenu : UIView
|
||||
@property(nonatomic, copy) TUIChatPopMenuHideCallback hideCallback;
|
||||
@property(nonatomic, copy) void (^reactClickCallback)(NSString *faceName);
|
||||
@property(nonatomic, weak) TUIMessageCellData *targetCellData;
|
||||
@property(nonatomic, weak) TUIMessageCell *targetCell;
|
||||
/**
|
||||
* TUIChatPopMenu has no emojiView by default. If you need a chatPopMenu with emojiView, use this initializer.
|
||||
*/
|
||||
- (instancetype)initWithEmojiView:(BOOL)hasEmojiView frame:(CGRect)frame;
|
||||
|
||||
@property(nonatomic, strong, readonly) UIView *emojiContainerView;
|
||||
@property(nonatomic, strong, readonly) UIView *containerView;
|
||||
|
||||
- (void)addAction:(TUIChatPopMenuAction *)action;
|
||||
- (void)removeAllAction;
|
||||
- (void)setArrawPosition:(CGPoint)point adjustHeight:(CGFloat)adjustHeight;
|
||||
- (void)showInView:(UIView *__nullable)window;
|
||||
- (void)layoutSubview;
|
||||
- (void)hideWithAnimation;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
555
TUIKit/TUIChat/BaseCell/TUIChatPopMenu.m
Normal file
555
TUIKit/TUIChat/BaseCell/TUIChatPopMenu.m
Normal file
@@ -0,0 +1,555 @@
|
||||
//
|
||||
// TUIChatPopMenu.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/30.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatPopMenu.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIChatPopActionsView.h"
|
||||
#import <TIMCommon/TIMCommonMediator.h>
|
||||
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import "TUIFaceView.h"
|
||||
|
||||
#define maxColumns 5
|
||||
#define kContainerInsets UIEdgeInsetsMake(3, 0, 3, 0)
|
||||
#define kActionWidth 54
|
||||
#define kActionHeight 65
|
||||
#define kActionMargin 5
|
||||
#define kSepartorHeight 0.5
|
||||
#define kSepartorLRMargin 10
|
||||
#define kArrowSize CGSizeMake(15, 10)
|
||||
#define kEmojiHeight 44
|
||||
|
||||
@implementation TUIChatPopMenuAction
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title image:(UIImage *)image weight:(NSInteger)weight callback:(TUIChatPopMenuActionCallback)callback {
|
||||
if (self = [super init]) {
|
||||
self.title = title;
|
||||
self.image = image;
|
||||
self.weight = weight;
|
||||
self.callback = callback;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIChatPopMenu () <UIGestureRecognizerDelegate,V2TIMAdvancedMsgListener>
|
||||
|
||||
/**
|
||||
* emojiRecent view and emoji secondary page view
|
||||
*/
|
||||
@property(nonatomic, strong) UIView *emojiContainerView;
|
||||
|
||||
@property(nonatomic, strong) UIView *containerView;
|
||||
|
||||
@property(nonatomic, strong) NSMutableArray *actions;
|
||||
|
||||
@property(nonatomic, assign) CGPoint arrawPoint;
|
||||
|
||||
@property(nonatomic, assign) CGFloat adjustHeight;
|
||||
|
||||
@property(nonatomic, strong) NSMutableDictionary *actionCallback;
|
||||
|
||||
@property(nonatomic, strong) CAShapeLayer *arrowLayer;
|
||||
|
||||
@property(nonatomic, assign) CGFloat emojiHeight;
|
||||
|
||||
@property(nonatomic, strong) TUIChatPopActionsView *actionsView;
|
||||
|
||||
@property(nonatomic, assign) BOOL hasEmojiView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatPopMenu
|
||||
|
||||
- (void)addAction:(TUIChatPopMenuAction *)action {
|
||||
if (action) {
|
||||
[self.actions addObject:action];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllAction {
|
||||
[self.actions removeAllObjects];
|
||||
}
|
||||
|
||||
- (void)setArrawPosition:(CGPoint)point adjustHeight:(CGFloat)adjustHeight {
|
||||
point = CGPointMake(point.x, point.y - NavBar_Height);
|
||||
self.arrawPoint = point;
|
||||
self.adjustHeight = adjustHeight;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEmojiView:(BOOL)hasEmojiView frame:(CGRect)frame {
|
||||
self.hasEmojiView = hasEmojiView;
|
||||
return [self initWithFrame:frame];
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
|
||||
tap.delegate = self;
|
||||
pan.delegate = self;
|
||||
[self addGestureRecognizer:tap];
|
||||
[self addGestureRecognizer:pan];
|
||||
if ([self isAddEmojiView]) {
|
||||
self.emojiHeight = kEmojiHeight;
|
||||
}
|
||||
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(hideWithAnimation) name:@"kTUIChatPopMenuWillHideNotification" object:nil];
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(hideWithAnimation) name:UIKeyboardWillChangeFrameNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
|
||||
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isAddEmojiView {
|
||||
return self.hasEmojiView && [TUIChatConfig defaultConfig].enablePopMenuEmojiReactAction;
|
||||
}
|
||||
|
||||
- (void)onTap:(UIGestureRecognizer *)tap {
|
||||
[self hideWithAnimation];
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
||||
if ([touch.view isDescendantOfView:self.emojiContainerView]) {
|
||||
return NO;
|
||||
}
|
||||
if ([touch.view isDescendantOfView:self.containerView]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (@available(iOS 17.0, *)) {
|
||||
CGPoint touchPoint = [touch locationInView:touch.view.nextResponder];
|
||||
CGRect frame = self.targetCell.frame;
|
||||
if (CGRectContainsPoint(frame, touchPoint)) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
if (@available(iOS 17.0, *)) {
|
||||
CGPoint touchPoint = [self.superview convertPoint:point fromView:self];
|
||||
CGRect frame = self.targetCell.frame;
|
||||
CGRect containerFrame = [self.superview convertRect:self.targetCell.container.frame fromView:self.targetCell];
|
||||
// CGRect popFrame1 = [self.superview convertRect:self.emojiContainerView.frame fromView:self];
|
||||
CGRect popFrame2 = [self.superview convertRect:self.containerView.frame fromView:self];
|
||||
if ( CGRectContainsPoint(popFrame2, touchPoint)) {
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
[self.superview convertRect:self.targetCell.container.frame fromView:self.targetCell];
|
||||
|
||||
if (CGRectContainsPoint(frame, touchPoint)) {
|
||||
if ([self.targetCell respondsToSelector:@selector(textView)]) {
|
||||
UITextView *textView = [self.targetCell valueForKey:@"textView"];
|
||||
if (CGRectContainsPoint(containerFrame,touchPoint)) {
|
||||
if (textView && [textView isKindOfClass:UITextView.class] && !textView.isSelectable) {
|
||||
[textView selectAll:self];
|
||||
}
|
||||
return textView;
|
||||
}else {
|
||||
if (textView && [textView isKindOfClass:UITextView.class]) {
|
||||
[textView selectAll:nil];
|
||||
[self hideWithAnimation];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[self hideWithAnimation];
|
||||
}
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
else {
|
||||
return [super hitTest:point withEvent:event];
|
||||
}
|
||||
}
|
||||
- (void)hideWithAnimation {
|
||||
[UIView animateWithDuration:0.3
|
||||
animations:^{
|
||||
self.alpha = 0;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
if (self.hideCallback) {
|
||||
self.hideCallback();
|
||||
}
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)hideByClickButton:(UIButton *)button callback:(void (^__nullable)(void))callback {
|
||||
[UIView animateWithDuration:0.3
|
||||
animations:^{
|
||||
self.alpha = 0;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
if (self.hideCallback) {
|
||||
self.hideCallback();
|
||||
}
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}];
|
||||
}
|
||||
- (void)showInView:(UIView *)window {
|
||||
if (window == nil) {
|
||||
window = UIApplication.sharedApplication.keyWindow;
|
||||
}
|
||||
|
||||
self.frame = window.bounds;
|
||||
[window addSubview:self];
|
||||
|
||||
[self layoutSubview];
|
||||
}
|
||||
|
||||
- (void)layoutSubview {
|
||||
self.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
self.layer.shadowRadius = 5;
|
||||
self.layer.shadowOpacity = 0.5;
|
||||
|
||||
[self updateActionByRank];
|
||||
|
||||
if ([self isAddEmojiView]) {
|
||||
[self prepareEmojiView];
|
||||
}
|
||||
|
||||
[self prepareContainerView];
|
||||
|
||||
if ([self isAddEmojiView]) {
|
||||
[self setupEmojiSubView];
|
||||
}
|
||||
|
||||
[self setupContainerPosition];
|
||||
|
||||
[self updateLayout];
|
||||
|
||||
if (isRTL()) {
|
||||
[self fitRTLViews];
|
||||
}
|
||||
|
||||
}
|
||||
- (void)fitRTLViews {
|
||||
if (self.actionsView) {
|
||||
for (UIView *subview in self.actionsView.subviews) {
|
||||
if ([subview respondsToSelector:@selector(resetFrameToFitRTL)]) {
|
||||
[subview resetFrameToFitRTL];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)updateActionByRank {
|
||||
NSArray *ageSortResultArray = [self.actions sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
|
||||
TUIChatPopMenuAction *per1 = obj1;
|
||||
TUIChatPopMenuAction *per2 = obj2;
|
||||
return per1.weight > per2.weight ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
NSMutableArray *filterArray = [NSMutableArray arrayWithArray:ageSortResultArray];
|
||||
|
||||
self.actions = [NSMutableArray arrayWithArray:ageSortResultArray];
|
||||
}
|
||||
- (void)setupContainerPosition {
|
||||
/**
|
||||
* Calculate the coordinates and correct them, the default arrow points down
|
||||
*/
|
||||
CGFloat minTopBottomMargin = (Is_IPhoneX ? (100) : (0.0));
|
||||
CGFloat minLeftRightMargin = 50;
|
||||
CGFloat containerW = self.containerView.bounds.size.width;
|
||||
CGFloat containerH = self.containerView.bounds.size.height;
|
||||
CGFloat upContainerY = self.arrawPoint.y + self.adjustHeight + kArrowSize.height; // The containerY value when arrow points up
|
||||
|
||||
/**
|
||||
* The default arrow points down
|
||||
*/
|
||||
CGFloat containerX = self.arrawPoint.x - 0.5 * containerW;
|
||||
CGFloat containerY = self.arrawPoint.y - kArrowSize.height - containerH - StatusBar_Height - self.emojiHeight;
|
||||
BOOL top = NO; // The direction of arrow, here is down
|
||||
CGFloat arrawX = 0.5 * containerW;
|
||||
CGFloat arrawY = kArrowSize.height + containerH - 1.5;
|
||||
|
||||
/**
|
||||
* Corrected vertical coordinates
|
||||
*/
|
||||
if (containerY < minTopBottomMargin) {
|
||||
/**
|
||||
* At this time, the container is too high, and it is planned to adjust the direction of the arrow to upward.
|
||||
*/
|
||||
if (upContainerY + containerH + minTopBottomMargin > self.superview.bounds.size.height) {
|
||||
/**
|
||||
* After adjusting the upward arrow direction, it will cause the entire container to exceed the screen. At this time, the adjustment strategy is
|
||||
* changed to: keep the arrow direction downward and move self.arrawPoint
|
||||
*/
|
||||
top = NO;
|
||||
self.arrawPoint = CGPointMake(self.arrawPoint.x, self.arrawPoint.y - containerY);
|
||||
containerY = self.arrawPoint.y - kArrowSize.height - containerH;
|
||||
|
||||
} else {
|
||||
/**
|
||||
* Adjust the direction of the arrow to meet the requirements
|
||||
*/
|
||||
top = YES;
|
||||
self.arrawPoint = CGPointMake(self.arrawPoint.x, self.arrawPoint.y + self.adjustHeight - StatusBar_Height - 5);
|
||||
arrawY = -kArrowSize.height;
|
||||
containerY = self.arrawPoint.y + kArrowSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Corrected horizontal coordinates
|
||||
*/
|
||||
if (containerX < minLeftRightMargin) {
|
||||
/**
|
||||
* At this time, the container is too close to the left side of the screen and needs to move to the right
|
||||
*/
|
||||
CGFloat offset = (minLeftRightMargin - containerX);
|
||||
arrawX = arrawX - offset;
|
||||
containerX = containerX + offset;
|
||||
if (arrawX < 20) {
|
||||
arrawX = 20;
|
||||
}
|
||||
|
||||
} else if (containerX + containerW + minLeftRightMargin > self.bounds.size.width) {
|
||||
/**
|
||||
* At this time, the container is too close to the right side of the screen and needs to be moved to the left
|
||||
*/
|
||||
CGFloat offset = containerX + containerW + minLeftRightMargin - self.bounds.size.width;
|
||||
arrawX = arrawX + offset;
|
||||
containerX = containerX - offset;
|
||||
if (arrawX > containerW - 20) {
|
||||
arrawX = containerW - 20;
|
||||
}
|
||||
}
|
||||
|
||||
self.emojiContainerView.frame = CGRectMake(containerX, containerY, containerW, MAX(self.emojiHeight + containerH, 200));
|
||||
self.containerView.frame = CGRectMake(containerX, containerY + self.emojiHeight, containerW, containerH);
|
||||
|
||||
/**
|
||||
* Drawing arrow
|
||||
*/
|
||||
self.arrowLayer = [[CAShapeLayer alloc] init];
|
||||
self.arrowLayer.path = [self arrawPath:CGPointMake(arrawX, arrawY) directionTop:top].CGPath;
|
||||
self.arrowLayer.fillColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF").CGColor;
|
||||
if (top) {
|
||||
if (self.emojiContainerView) {
|
||||
[self.emojiContainerView.layer addSublayer:self.arrowLayer];
|
||||
} else {
|
||||
[self.containerView.layer addSublayer:self.arrowLayer];
|
||||
}
|
||||
} else {
|
||||
[self.containerView.layer addSublayer:self.arrowLayer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)prepareEmojiView {
|
||||
if (self.emojiContainerView) {
|
||||
[self.emojiContainerView removeFromSuperview];
|
||||
self.emojiContainerView = nil;
|
||||
}
|
||||
|
||||
self.emojiContainerView = [[UIView alloc] init];
|
||||
[self addSubview:self.emojiContainerView];
|
||||
}
|
||||
- (void)prepareContainerView {
|
||||
if (self.containerView) {
|
||||
[self.containerView removeFromSuperview];
|
||||
self.containerView = nil;
|
||||
}
|
||||
self.containerView = [[UIView alloc] init];
|
||||
[self addSubview:self.containerView];
|
||||
|
||||
self.actionsView = [[TUIChatPopActionsView alloc] init];
|
||||
self.actionsView.backgroundColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF");
|
||||
[self.containerView addSubview:self.actionsView];
|
||||
|
||||
int i = 0;
|
||||
for (TUIChatPopMenuAction *action in self.actions) {
|
||||
UIButton *actionButton = [self buttonWithAction:action tag:[self.actions indexOfObject:action]];
|
||||
[self.actionsView addSubview:actionButton];
|
||||
i++;
|
||||
if (i == maxColumns && i < self.actions.count) {
|
||||
UIView *separtorView = [[UIView alloc] init];
|
||||
separtorView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#39393B");
|
||||
separtorView.hidden = YES;
|
||||
[self.actionsView addSubview:separtorView];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculating the size of container
|
||||
*/
|
||||
int rows = (self.actions.count % maxColumns == 0) ? (int)self.actions.count / maxColumns : (int)(self.actions.count / maxColumns) + 1;
|
||||
int columns = self.actions.count < maxColumns ? (int)self.actions.count : maxColumns;
|
||||
if ([self isAddEmojiView]) {
|
||||
columns = maxColumns;
|
||||
}
|
||||
CGFloat width = kActionWidth * columns + kActionMargin * (columns + 1) + kContainerInsets.left + kContainerInsets.right;
|
||||
CGFloat height = kActionHeight * rows + (rows - 1) * kSepartorHeight + kContainerInsets.top + kContainerInsets.bottom;
|
||||
|
||||
self.emojiContainerView.frame = CGRectMake(0, 0, width, self.emojiHeight + height);
|
||||
self.containerView.frame = CGRectMake(0, self.emojiHeight, width, height);
|
||||
}
|
||||
|
||||
- (void)setupEmojiSubView {
|
||||
[self setupEmojiRecentView];
|
||||
[self setupEmojiAdvanceView];
|
||||
}
|
||||
- (void)setupEmojiRecentView {
|
||||
NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
|
||||
BOOL isRaiseEmojiExtensionSuccess = [TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactRecentView_ClassicExtensionID
|
||||
parentView:self.emojiContainerView
|
||||
param:param];
|
||||
if (!isRaiseEmojiExtensionSuccess) {
|
||||
self.emojiHeight = 0;
|
||||
}
|
||||
}
|
||||
- (void)setupEmojiAdvanceView {
|
||||
NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
|
||||
[TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactDetailView_ClassicExtensionID parentView:self.emojiContainerView param:param];
|
||||
}
|
||||
|
||||
- (void)updateLayout {
|
||||
|
||||
self.actionsView.frame = CGRectMake(0, -0.5, self.containerView.frame.size.width, self.containerView.frame.size.height);
|
||||
|
||||
int columns = self.actions.count < maxColumns ? (int)self.actions.count : maxColumns;
|
||||
CGFloat containerWidth = kActionWidth * columns + kActionMargin * (columns + 1) + kContainerInsets.left + kContainerInsets.right;
|
||||
|
||||
int i = 0;
|
||||
int currentRow = 0;
|
||||
int currentColumn = 0;
|
||||
for (UIView *subView in self.actionsView.subviews) {
|
||||
if ([subView isKindOfClass:UIButton.class]) {
|
||||
currentRow = i / maxColumns;
|
||||
currentColumn = i % maxColumns;
|
||||
|
||||
CGFloat x = kContainerInsets.left + (currentColumn + 1) * kActionMargin + currentColumn * kActionWidth;
|
||||
CGFloat y = kContainerInsets.top + currentRow * kActionHeight + currentRow * kSepartorHeight;
|
||||
subView.frame = CGRectMake(x, y, kActionWidth, kActionHeight);
|
||||
|
||||
i++;
|
||||
} else {
|
||||
CGFloat y = (currentRow + 1) * kActionHeight + kContainerInsets.top;
|
||||
CGFloat width = containerWidth - 2 * kSepartorLRMargin - kContainerInsets.left - kContainerInsets.right;
|
||||
subView.frame = CGRectMake(kSepartorLRMargin, y, width, kSepartorHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIBezierPath *)arrawPath:(CGPoint)point directionTop:(BOOL)top {
|
||||
CGSize arrowSize = kArrowSize;
|
||||
UIBezierPath *arrowPath = [[UIBezierPath alloc] init];
|
||||
[arrowPath moveToPoint:point];
|
||||
if (top) {
|
||||
[arrowPath addLineToPoint:CGPointMake(point.x + arrowSize.width * 0.5, point.y + arrowSize.height)];
|
||||
[arrowPath addLineToPoint:CGPointMake(point.x - arrowSize.width * 0.5, point.y + arrowSize.height)];
|
||||
} else {
|
||||
[arrowPath addLineToPoint:CGPointMake(point.x + arrowSize.width * 0.5, point.y - arrowSize.height)];
|
||||
[arrowPath addLineToPoint:CGPointMake(point.x - arrowSize.width * 0.5, point.y - arrowSize.height)];
|
||||
}
|
||||
[arrowPath closePath];
|
||||
return arrowPath;
|
||||
}
|
||||
|
||||
- (UIButton *)buttonWithAction:(TUIChatPopMenuAction *)action tag:(NSInteger)tag {
|
||||
UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[actionButton setTitleColor:TUIChatDynamicColor(@"chat_pop_menu_text_color", @"#444444")
|
||||
forState:UIControlStateNormal];
|
||||
actionButton.titleLabel.font = [UIFont systemFontOfSize:10.0];
|
||||
actionButton.titleLabel.numberOfLines = 2;
|
||||
actionButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[actionButton setTitle:action.title forState:UIControlStateNormal];
|
||||
[actionButton setImage:action.image forState:UIControlStateNormal];
|
||||
actionButton.contentMode = UIViewContentModeScaleAspectFit;
|
||||
|
||||
[actionButton addTarget:self action:@selector(buttonHighlightedEnter:) forControlEvents:UIControlEventTouchDown];
|
||||
[actionButton addTarget:self action:@selector(buttonHighlightedEnter:) forControlEvents:UIControlEventTouchDragEnter];
|
||||
[actionButton addTarget:self action:@selector(buttonHighlightedExit:) forControlEvents:UIControlEventTouchDragExit];
|
||||
[actionButton addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
actionButton.tag = tag;
|
||||
|
||||
CGSize imageSize = CGSizeMake(20, 20);
|
||||
CGSize titleSize = actionButton.titleLabel.frame.size;
|
||||
CGSize textSize = [actionButton.titleLabel.text sizeWithAttributes:@{NSFontAttributeName : actionButton.titleLabel.font}];
|
||||
CGSize frameSize = CGSizeMake(ceilf(textSize.width), ceilf(textSize.height));
|
||||
if (titleSize.width + 0.5 < frameSize.width) {
|
||||
titleSize.width = frameSize.width;
|
||||
}
|
||||
titleSize.width = MIN(titleSize.width, 48);
|
||||
CGFloat totalHeight = (imageSize.height + titleSize.height + 8);
|
||||
actionButton.imageEdgeInsets = UIEdgeInsetsMake(-(totalHeight - imageSize.height), 0.0, 0.0, -titleSize.width);
|
||||
actionButton.titleEdgeInsets = UIEdgeInsetsMake(0, -imageSize.width, -(totalHeight - titleSize.height), 0);
|
||||
|
||||
[self.actionCallback setObject:action.callback forKey:@(tag)];
|
||||
|
||||
return actionButton;
|
||||
}
|
||||
|
||||
- (void)buttonHighlightedEnter:(UIButton *)sender {
|
||||
sender.backgroundColor = TUIChatDynamicColor(@"", @"#006EFF19");
|
||||
}
|
||||
- (void)buttonHighlightedExit:(UIButton *)sender {
|
||||
sender.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
- (void)onClick:(UIButton *)button {
|
||||
if (![self.actionCallback.allKeys containsObject:@(button.tag)]) {
|
||||
[self hideWithAnimation];
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self hideByClickButton:button
|
||||
callback:^() {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
TUIChatPopMenuActionCallback callback = [strongSelf.actionCallback objectForKey:@(button.tag)];
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSMutableArray *)actions {
|
||||
if (_actions == nil) {
|
||||
_actions = [NSMutableArray array];
|
||||
}
|
||||
return _actions;
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)actionCallback {
|
||||
if (_actionCallback == nil) {
|
||||
_actionCallback = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return _actionCallback;
|
||||
}
|
||||
|
||||
// MARK: V2TIMAdvancedMsgListener
|
||||
- (void)onRecvMessageRevoked:(NSString *)msgID operateUser:(V2TIMUserFullInfo *)operateUser reason:(NSString *)reason {
|
||||
if ([msgID isEqualToString:self.targetCellData.msgID]) {
|
||||
[self hideWithAnimation];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ThemeChanged
|
||||
- (void)applyBorderTheme {
|
||||
if (_arrowLayer) {
|
||||
_arrowLayer.fillColor = TUIChatDynamicColor(@"chat_pop_menu_bg_color", @"#FFFFFF").CGColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onThemeChanged {
|
||||
[self applyBorderTheme];
|
||||
}
|
||||
@end
|
||||
18
TUIKit/TUIChat/BaseCell/TUIChatPopMenuDefine.h
Normal file
18
TUIKit/TUIChat/BaseCell/TUIChatPopMenuDefine.h
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// TUIChatPopMenuDefine.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by cologne on 2023/11/22.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#define TChatEmojiView_Margin 10
|
||||
#define TChatEmojiView_MarginTopBottom 17
|
||||
|
||||
#define TChatEmojiView_Padding 20
|
||||
#define TChatEmojiView_Page_Height 30
|
||||
#define TChatEmojiView_CollectionOffsetY 8
|
||||
#define TChatEmojiView_CollectionHeight 107
|
||||
|
||||
41
TUIKit/TUIChat/BaseCell/TUIChatShortcutMenuView.h
Normal file
41
TUIKit/TUIChat/BaseCell/TUIChatShortcutMenuView.h
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// TUIChatShortcutMenuView.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2023/6/29.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIChatShortcutMenuCellData : NSObject
|
||||
@property (nonatomic, strong) NSString *text;
|
||||
@property (nonatomic, assign) SEL cselector;
|
||||
@property (nonatomic, strong) id target;
|
||||
|
||||
@property (nonatomic, strong) UIColor *textColor;
|
||||
@property (nonatomic, strong) UIColor *backgroundColor;
|
||||
@property (nonatomic, strong) UIFont *textFont;
|
||||
@property (nonatomic, strong) UIColor *borderColor;
|
||||
@property (nonatomic, assign) CGFloat borderWidth;
|
||||
@property (nonatomic, assign) CGFloat cornerRadius;
|
||||
|
||||
- (CGSize)calcSize;
|
||||
@end
|
||||
|
||||
|
||||
@interface TUIChatShortcutMenuCell : UICollectionViewCell
|
||||
@property (nonatomic, strong) UIButton *button;
|
||||
@property (nonatomic, strong) TUIChatShortcutMenuCellData *cellData;
|
||||
@end
|
||||
|
||||
|
||||
@interface TUIChatShortcutMenuView : UIView
|
||||
@property (nonatomic, assign) CGFloat viewHeight;
|
||||
@property (nonatomic, assign) CGFloat itemHorizontalSpacing;
|
||||
- (instancetype)initWithDataSource:(NSArray *)source;
|
||||
- (void)updateFrame;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
182
TUIKit/TUIChat/BaseCell/TUIChatShortcutMenuView.m
Normal file
182
TUIKit/TUIChat/BaseCell/TUIChatShortcutMenuView.m
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// TUIChatShortcutMenuView.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2023/6/29.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIChatShortcutMenuView.h"
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIDefine.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@implementation TUIChatShortcutMenuCellData
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.textColor = [UIColor tui_colorWithHex:@"#8F959E"];
|
||||
self.textFont = [UIFont systemFontOfSize:14];
|
||||
self.backgroundColor = [UIColor tui_colorWithHex:@"#F6F7F9"];
|
||||
self.cornerRadius = 16;
|
||||
self.borderColor = [UIColor tui_colorWithHex:@"#C5CBD4"];
|
||||
self.borderWidth = 1.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)calcSize {
|
||||
return [self calcMenuCellButtonSize:self.text];
|
||||
}
|
||||
|
||||
- (CGSize)calcMenuCellButtonSize:(NSString *)title {
|
||||
CGFloat margin = 28;
|
||||
CGRect rect = [title boundingRectWithSize:CGSizeMake(MAXFLOAT, 32)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
attributes:@{ NSFontAttributeName : self.textFont}
|
||||
context:nil];
|
||||
return CGSizeMake(rect.size.width + margin, 32);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface TUIChatShortcutMenuCell()
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatShortcutMenuCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.button = [UIButton new];
|
||||
[self addSubview:self.button];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)fillWithData:(TUIChatShortcutMenuCellData *)cellData {
|
||||
self.cellData = cellData;
|
||||
[self.button setTitle:cellData.text forState:UIControlStateNormal];
|
||||
[self.button addTarget:cellData.target action:cellData.cselector forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
self.button.layer.cornerRadius = self.cellData.cornerRadius;
|
||||
self.button.titleLabel.font = self.cellData.textFont;
|
||||
self.button.backgroundColor = self.cellData.backgroundColor;
|
||||
[self.button setTitleColor:self.cellData.textColor forState:UIControlStateNormal];
|
||||
self.button.layer.borderWidth = self.cellData.borderWidth;
|
||||
self.button.layer.borderColor = self.cellData.borderColor.CGColor;
|
||||
|
||||
[self updateConstraints];
|
||||
// tell constraints they need updating
|
||||
[self setNeedsUpdateConstraints];
|
||||
// update constraints now so we can animate the change
|
||||
[self updateConstraintsIfNeeded];
|
||||
[self layoutIfNeeded];
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
[super updateConstraints];
|
||||
|
||||
CGSize size = [self.cellData calcSize];
|
||||
[self.button mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(12);
|
||||
make.centerY.mas_equalTo(self.mas_centerY);
|
||||
make.width.mas_equalTo(size.width);
|
||||
make.height.mas_equalTo(size.height);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIChatShortcutMenuView() <UICollectionViewDelegate, UICollectionViewDataSource>
|
||||
|
||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||
@property (nonatomic, strong) NSMutableArray *dataSource;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatShortcutMenuView
|
||||
|
||||
- (instancetype)initWithDataSource:(NSArray *)source {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.dataSource = [source mutableCopy];
|
||||
self.backgroundColor = [UIColor tui_colorWithHex:@"#EBF0F6"];
|
||||
[self addSubview:self.collectionView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
- (void)updateFrame {
|
||||
self.mm_left(0).mm_top(0).mm_width(Screen_Width).mm_height(self.viewHeight > 0 ? self.viewHeight : 46);
|
||||
self.collectionView.mm_fill();
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
- (UICollectionView *)collectionView {
|
||||
if (!_collectionView) {
|
||||
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
||||
[layout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
|
||||
_collectionView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:layout];
|
||||
_collectionView.delegate = self;
|
||||
_collectionView.dataSource = self;
|
||||
_collectionView.scrollEnabled = YES;
|
||||
_collectionView.backgroundColor = [UIColor clearColor];
|
||||
_collectionView.showsHorizontalScrollIndicator = NO;
|
||||
[_collectionView registerClass:[TUIChatShortcutMenuCell class] forCellWithReuseIdentifier:@"menuCell"];
|
||||
}
|
||||
return _collectionView;
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionViewDataSource & Delegate
|
||||
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView
|
||||
numberOfItemsInSection:(NSInteger)section {
|
||||
return self.dataSource.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
|
||||
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIChatShortcutMenuCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"menuCell" forIndexPath:indexPath];
|
||||
TUIChatShortcutMenuCellData *cellData = self.dataSource[indexPath.row];
|
||||
[cell fillWithData:cellData];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIChatShortcutMenuCellData *cellData = self.dataSource[indexPath.row];
|
||||
return CGSizeMake([cellData calcSize].width + 12, [cellData calcSize].height);
|
||||
}
|
||||
|
||||
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
insetForSectionAtIndex:(NSInteger)section {
|
||||
return UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (CGFloat)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout*)collectionViewLayout
|
||||
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView
|
||||
shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
25
TUIKit/TUIChat/BaseCell/TUIFaceSegementScrollView.h
Normal file
25
TUIKit/TUIChat/BaseCell/TUIFaceSegementScrollView.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// TUIFaceSegementScrollView.h
|
||||
// TUIEmojiPlugin
|
||||
//
|
||||
// Created by wyl on 2023/11/15.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIFaceView.h"
|
||||
#import "TUIFaceVerticalView.h"
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class TUIFaceGroup;
|
||||
|
||||
@interface TUIFaceSegementScrollView : UIView
|
||||
@property(nonatomic, copy) void(^onScrollCallback)(NSInteger indexPage);
|
||||
@property(strong, nonatomic) UIScrollView *pageScrollView;
|
||||
- (void)setItems:(NSArray<TUIFaceGroup *> *)items delegate:(id <TUIFaceVerticalViewDelegate>) delegate;
|
||||
- (void)updateContainerView;
|
||||
- (void)setPageIndex:(NSInteger)index;
|
||||
- (void)setAllFloatCtrlViewAllowSendSwitch:(BOOL)isAllow;
|
||||
- (void)updateRecentView;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
129
TUIKit/TUIChat/BaseCell/TUIFaceSegementScrollView.m
Normal file
129
TUIKit/TUIChat/BaseCell/TUIFaceSegementScrollView.m
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// TUIFaceSegementScrollView.m
|
||||
// TUIEmojiPlugin
|
||||
//
|
||||
// Created by wyl on 2023/11/15.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFaceSegementScrollView.h"
|
||||
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TUIFitButton.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import <TIMCommon/TIMCommonMediator.h>
|
||||
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
|
||||
|
||||
@interface TUIFaceSegementScrollView () <UIScrollViewDelegate>
|
||||
|
||||
@property(nonatomic, strong) NSArray<TUIFaceGroup *> *items;
|
||||
|
||||
@property(nonatomic, strong) NSMutableArray<TUIFaceVerticalView*> * viewArray;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIFaceSegementScrollView
|
||||
|
||||
|
||||
- (void)setItems:(NSArray<TUIFaceGroup *> *)items delegate:(id <TUIFaceViewDelegate>) delegate {
|
||||
_items = items;
|
||||
for (UIView *view in self.pageScrollView.subviews) {
|
||||
if (view) {
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
[self.viewArray removeAllObjects];
|
||||
|
||||
for (int i = 0; i < items.count; i++) {
|
||||
TUIFaceVerticalView* faceView = [[TUIFaceVerticalView alloc] initWithFrame:CGRectMake(0,
|
||||
0, self.frame.size.width, self.pageScrollView.frame.size.height)];
|
||||
TUIFaceGroup *indexGroup = items[i];
|
||||
if (indexGroup.recentGroup) {
|
||||
[faceView setData:[NSMutableArray arrayWithArray:@[indexGroup.recentGroup,indexGroup]]];
|
||||
}
|
||||
else {
|
||||
[faceView setData:[NSMutableArray arrayWithArray:@[indexGroup]]];
|
||||
}
|
||||
|
||||
faceView.frame = CGRectMake(i * self.frame.size.width, 0, self.frame.size.width, self.pageScrollView.frame.size.height);
|
||||
faceView.delegate = delegate;
|
||||
[self.pageScrollView addSubview:faceView];
|
||||
[self.viewArray addObject:faceView];
|
||||
}
|
||||
self.pageScrollView.contentSize = CGSizeMake(self.viewArray.count * self.frame.size.width, self.pageScrollView.frame.size.height);
|
||||
if (isRTL()) {
|
||||
_pageScrollView.transform = CGAffineTransformMakeRotation(M_PI);
|
||||
NSArray *subViews = _pageScrollView.subviews;
|
||||
for (UIView *subView in subViews) {
|
||||
subView.transform = CGAffineTransformMakeRotation(M_PI);
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)updateContainerView {
|
||||
self.pageScrollView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
|
||||
|
||||
for (int i = 0; i < self.viewArray.count; i++) {
|
||||
TUIFaceVerticalView* view = self.viewArray[i];
|
||||
view.frame = CGRectMake(i * self.pageScrollView.frame.size.width, 0, self.pageScrollView.frame.size.width, self.pageScrollView.frame.size.height);
|
||||
}
|
||||
|
||||
self.pageScrollView.contentSize = CGSizeMake(self.viewArray.count * self.frame.size.width, self.pageScrollView.frame.size.height);
|
||||
}
|
||||
|
||||
|
||||
- (UIScrollView *)pageScrollView {
|
||||
if (_pageScrollView == nil) {
|
||||
_pageScrollView = [[UIScrollView alloc]
|
||||
initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
|
||||
_pageScrollView.backgroundColor = [UIColor clearColor];
|
||||
_pageScrollView.delegate = self;
|
||||
_pageScrollView.showsVerticalScrollIndicator = NO;
|
||||
_pageScrollView.showsHorizontalScrollIndicator = NO;
|
||||
_pageScrollView.bounces = NO;
|
||||
_pageScrollView.scrollsToTop = NO;
|
||||
_pageScrollView.pagingEnabled = YES;
|
||||
[self addSubview:_pageScrollView];
|
||||
}
|
||||
return _pageScrollView;
|
||||
}
|
||||
- (NSMutableArray<TUIFaceVerticalView *> *)viewArray {
|
||||
if(!_viewArray) {
|
||||
_viewArray = [NSMutableArray arrayWithCapacity:3];
|
||||
}
|
||||
return _viewArray;
|
||||
}
|
||||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||
if (scrollView == _pageScrollView) {
|
||||
int p = _pageScrollView.contentOffset.x / self.frame.size.width;
|
||||
if (self.onScrollCallback){
|
||||
self.onScrollCallback(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPageIndex:(NSInteger)index {
|
||||
CGPoint p = CGPointMake(_pageScrollView.frame.size.width * index,0);
|
||||
[_pageScrollView setContentOffset:p animated:NO];
|
||||
}
|
||||
|
||||
- (void)setAllFloatCtrlViewAllowSendSwitch:(BOOL)isAllow {
|
||||
for (TUIFaceVerticalView *view in self.viewArray) {
|
||||
[view setFloatCtrlViewAllowSendSwitch:isAllow];
|
||||
}
|
||||
}
|
||||
- (void)updateRecentView {
|
||||
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
|
||||
TUIFaceVerticalView* faceView = self.viewArray[0];
|
||||
TUIFaceGroup *indexGroup = self.items[0];
|
||||
indexGroup.recentGroup = [service getChatPopMenuRecentQueue];
|
||||
indexGroup.recentGroup.rowCount = 1;
|
||||
indexGroup.recentGroup.itemCountPerRow = 8;
|
||||
indexGroup.recentGroup.groupName = TIMCommonLocalizableString(TUIChatFaceGroupRecentEmojiName);
|
||||
if (indexGroup.isNeedAddInInputBar && indexGroup.recentGroup) {
|
||||
[faceView setData:[NSMutableArray arrayWithArray:@[indexGroup.recentGroup,indexGroup]]];
|
||||
}
|
||||
else {
|
||||
[faceView setData:[NSMutableArray arrayWithArray:@[indexGroup]]];
|
||||
}
|
||||
}
|
||||
@end
|
||||
71
TUIKit/TUIChat/BaseCell/TUIFaceVerticalView.h
Normal file
71
TUIKit/TUIChat/BaseCell/TUIFaceVerticalView.h
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// TUIFaceVerticalView.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2023/11/16.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIFaceView.h"
|
||||
@class TUIFaceVerticalView;
|
||||
@protocol TUIFaceVerticalViewDelegate <TUIFaceViewDelegate>
|
||||
- (void)faceViewClickSendMessageBtn;
|
||||
@end
|
||||
|
||||
@interface TUIFaceVerticalView : UIView
|
||||
|
||||
/**
|
||||
* Line view
|
||||
* The separtor which distinguish emoticons from other views
|
||||
*/
|
||||
@property(nonatomic, strong) UIView *lineView;
|
||||
|
||||
/**
|
||||
* The collectionView of emoticon view
|
||||
* Contains multiple lines of expressions, and cooperates with faceFlowLayout for flexible and unified view layout.
|
||||
*/
|
||||
@property(nonatomic, strong) UICollectionView *faceCollectionView;
|
||||
|
||||
/**
|
||||
* The flow layout of @faceCollectionView
|
||||
* Cooperating with faceCollectionView to make the expression view more beautiful. Supported setting layout direction, line spacing, cell spacing, etc.
|
||||
*/
|
||||
@property(nonatomic, strong) UICollectionViewFlowLayout *faceFlowLayout;
|
||||
|
||||
/**
|
||||
*
|
||||
* The data of @faceView
|
||||
* The object stored in this NSMutableArray is TUIFaceGroup, that is, the expression group.
|
||||
*/
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *faceGroups;
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *sectionIndexInGroup;
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *groupIndexInSection;
|
||||
@property(nonatomic, strong, readonly) NSMutableDictionary *itemIndexs;
|
||||
@property(nonatomic, strong, readonly) UIView *floatCtrlView;
|
||||
|
||||
/**
|
||||
*
|
||||
* Delegate variable, delegated
|
||||
* Need to implement the functionality required in the @TUIFaceVerticalViewDelegate protocol.
|
||||
*/
|
||||
@property(nonatomic, weak) id<TUIFaceVerticalViewDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Swipe to the specified expression group.
|
||||
* Switch the emoticon group according to the subscript of the emoticon group clicked by the user.
|
||||
*
|
||||
* @param index The index of the destination group, starting from 0.
|
||||
*/
|
||||
- (void)scrollToFaceGroupIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
* Setting data
|
||||
* Used to initialize TUIFaceView or update data in faceView when needed.
|
||||
*
|
||||
* @param data The data that needs to be set (TUIFaceGroup). The object stored in this NSMutableArray is TUIFaceGroup, that is, the emoticon group.
|
||||
*/
|
||||
- (void)setData:(NSMutableArray *)data;
|
||||
|
||||
- (void)setFloatCtrlViewAllowSendSwitch:(BOOL)isAllow;
|
||||
@end
|
||||
410
TUIKit/TUIChat/BaseCell/TUIFaceVerticalView.m
Normal file
410
TUIKit/TUIChat/BaseCell/TUIFaceVerticalView.m
Normal file
@@ -0,0 +1,410 @@
|
||||
//
|
||||
// TUIFaceVerticalView.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2023/11/16.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFaceVerticalView.h"
|
||||
|
||||
@interface TUIFaceVerticalView () <UICollectionViewDelegate,
|
||||
UICollectionViewDataSource,
|
||||
UICollectionViewDelegateFlowLayout,
|
||||
UIPopoverPresentationControllerDelegate>
|
||||
@property(nonatomic, strong) NSMutableArray *sectionIndexInGroup;
|
||||
@property(nonatomic, strong) NSMutableArray *groupIndexInSection;
|
||||
@property(nonatomic, strong) NSMutableDictionary *itemIndexs;
|
||||
@property(nonatomic, assign) NSInteger sectionCount;
|
||||
@property(nonatomic, assign) NSInteger curGroupIndex;
|
||||
|
||||
@property(nonatomic, strong) UIView *floatCtrlView;
|
||||
@property(nonatomic, strong) UIButton *sendButton;
|
||||
@property(nonatomic, strong) UIButton *deleteButton;
|
||||
|
||||
//preview
|
||||
@property (nonatomic, strong) UIImageView *dispalyView;
|
||||
@property (nonatomic, strong) UIImageView *dispalyImage;
|
||||
@property (nonatomic, assign) BOOL hasPreViewShow;
|
||||
@end
|
||||
|
||||
@implementation TUIFaceVerticalView
|
||||
|
||||
- (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");
|
||||
|
||||
_faceFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
|
||||
_faceFlowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
||||
_faceFlowLayout.minimumLineSpacing = TFaceView_Margin;
|
||||
_faceFlowLayout.minimumInteritemSpacing = TFaceView_Margin;
|
||||
_faceFlowLayout.sectionInset = UIEdgeInsetsMake(0, TFaceView_Page_Padding, 0, TFaceView_Page_Padding);
|
||||
|
||||
_faceCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_faceFlowLayout];
|
||||
[_faceCollectionView registerClass:[TUIFaceCell class] forCellWithReuseIdentifier:TFaceCell_ReuseId];
|
||||
[_faceCollectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"headerView"];
|
||||
_faceCollectionView.collectionViewLayout = _faceFlowLayout;
|
||||
_faceCollectionView.pagingEnabled = NO;
|
||||
_faceCollectionView.delegate = self;
|
||||
_faceCollectionView.dataSource = self;
|
||||
_faceCollectionView.showsHorizontalScrollIndicator = NO;
|
||||
_faceCollectionView.showsVerticalScrollIndicator = NO;
|
||||
_faceCollectionView.backgroundColor = self.backgroundColor;
|
||||
_faceCollectionView.alwaysBounceVertical = YES;
|
||||
[self addSubview:_faceCollectionView];
|
||||
|
||||
_lineView = [[UIView alloc] init];
|
||||
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
|
||||
[self addSubview:_lineView];
|
||||
|
||||
[self setupfloatCtrlView];
|
||||
}
|
||||
- (void)setupfloatCtrlView {
|
||||
_floatCtrlView = [[UIView alloc] init];
|
||||
[self addSubview:_floatCtrlView];
|
||||
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[self.sendButton setTitle:TIMCommonLocalizableString(Send)forState:UIControlStateNormal];
|
||||
_sendButton.titleLabel.font = [UIFont systemFontOfSize:16];
|
||||
[self.sendButton addTarget:self action:@selector(didSelectSendButton:) forControlEvents:UIControlEventTouchUpInside];
|
||||
self.sendButton.backgroundColor = TIMCommonDynamicColor(@"", @"#0069F6");
|
||||
self.sendButton.layer.cornerRadius = 2;
|
||||
[_floatCtrlView addSubview:self.sendButton];
|
||||
|
||||
_deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_deleteButton setImage:[UIImage imageWithContentsOfFile:TUIChatFaceImagePath(@"del_normal")] forState:UIControlStateNormal];
|
||||
[_deleteButton setImageEdgeInsets:UIEdgeInsetsMake(5, 5, 5, 5)];
|
||||
_deleteButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_deleteButton.layer.cornerRadius = 2;
|
||||
[_deleteButton addTarget:self action:@selector(didSelectDeleteButton:) forControlEvents:UIControlEventTouchUpInside];
|
||||
_deleteButton.backgroundColor = [UIColor whiteColor];
|
||||
[_floatCtrlView addSubview:_deleteButton];
|
||||
}
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
[self defaultLayout];
|
||||
}
|
||||
|
||||
- (void)defaultLayout {
|
||||
_lineView.frame = CGRectMake(0, 0, self.frame.size.width, TLine_Heigh);
|
||||
[_faceCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(self);
|
||||
}];
|
||||
|
||||
[_floatCtrlView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(-16);
|
||||
make.bottom.mas_equalTo(self.mas_bottom).mas_offset(20);
|
||||
make.height.mas_equalTo(88);
|
||||
make.leading.mas_equalTo(self.deleteButton.mas_leading);
|
||||
}];
|
||||
|
||||
[self.sendButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(self.floatCtrlView.mas_trailing);
|
||||
make.top.mas_equalTo(self.floatCtrlView);
|
||||
make.height.mas_equalTo(30);
|
||||
make.width.mas_equalTo(50);
|
||||
}];
|
||||
[self.deleteButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(self.sendButton.mas_leading).mas_offset(-10);
|
||||
make.top.mas_equalTo(self.floatCtrlView);
|
||||
make.height.mas_equalTo(30);
|
||||
make.width.mas_equalTo(50);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setData:(NSMutableArray *)data {
|
||||
_faceGroups = data;
|
||||
[self defaultLayout];
|
||||
|
||||
_sectionIndexInGroup = [NSMutableArray array];
|
||||
_groupIndexInSection = [NSMutableArray array];
|
||||
_itemIndexs = [NSMutableDictionary dictionary];
|
||||
|
||||
NSInteger sectionIndex = 0;
|
||||
for (NSInteger groupIndex = 0; groupIndex < _faceGroups.count; ++groupIndex) {
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
[_sectionIndexInGroup addObject:@(sectionIndex)];
|
||||
int itemCount = group.faces.count;
|
||||
int sectionCount = ceil(group.faces.count * 1.0 / itemCount);
|
||||
for (int sectionIndex = 0; sectionIndex < sectionCount; ++sectionIndex) {
|
||||
[_groupIndexInSection addObject:@(groupIndex)];
|
||||
}
|
||||
sectionIndex += sectionCount;
|
||||
}
|
||||
_sectionCount = sectionIndex;
|
||||
|
||||
for (NSInteger curSection = 0; curSection < _sectionCount; ++curSection) {
|
||||
NSNumber *groupIndex = _groupIndexInSection[curSection];
|
||||
NSNumber *groupSectionIndex = _sectionIndexInGroup[groupIndex.integerValue];
|
||||
TUIFaceGroup *face = _faceGroups[groupIndex.integerValue];
|
||||
NSInteger itemCount = face.faces.count;
|
||||
NSInteger groupSection = curSection - groupSectionIndex.integerValue;
|
||||
for (NSInteger itemIndex = 0; itemIndex < itemCount; ++itemIndex) {
|
||||
// transpose line/row
|
||||
NSInteger reIndex = itemIndex;
|
||||
[_itemIndexs setObject:@(reIndex) forKey:[NSIndexPath indexPathForRow:itemIndex inSection:curSection]];
|
||||
}
|
||||
}
|
||||
|
||||
_curGroupIndex = 0;
|
||||
|
||||
[_faceCollectionView reloadData];
|
||||
|
||||
TUIFaceGroup *group = _faceGroups[0];
|
||||
if (!group.isNeedAddInInputBar) {
|
||||
_floatCtrlView.hidden = YES;
|
||||
}
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self adjustEmotionsAlpha];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
||||
return _sectionCount;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
int groupIndex = [_groupIndexInSection[section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
return group.faces.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIFaceCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TFaceCell_ReuseId forIndexPath:indexPath];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
__weak typeof(cell) weakCell = cell;
|
||||
cell.longPressCallback = ^(UILongPressGestureRecognizer * _Nonnull recognizer) {
|
||||
if (weakSelf.hasPreViewShow) {
|
||||
return;
|
||||
}
|
||||
[self showDisplayView:0 display_y:0 targetView:[UIApplication sharedApplication].keyWindow.rootViewController.view faceCell:weakCell];
|
||||
};
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
int itemCount = group.faces.count;
|
||||
|
||||
NSNumber *index = [_itemIndexs objectForKey:indexPath];
|
||||
if (index.integerValue < group.faces.count) {
|
||||
TUIFaceCellData *data = group.faces[index.integerValue];
|
||||
[cell setData:data];
|
||||
} else {
|
||||
[cell setData:nil];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *faces = _faceGroups[groupIndex];
|
||||
NSNumber *index = [_itemIndexs objectForKey:indexPath];
|
||||
if (index.integerValue < faces.faces.count) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceView:didSelectItemAtIndexPath:)]) {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index.integerValue inSection:groupIndex];
|
||||
[_delegate faceView:self didSelectItemAtIndexPath:indexPath];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
CGFloat width = (self.frame.size.width - TFaceView_Page_Padding * 2 - TFaceView_Margin * (group.itemCountPerRow - 1)) / group.itemCountPerRow;
|
||||
CGFloat height = width + TFaceView_Margin;
|
||||
return CGSizeMake(width, height);
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
referenceSizeForHeaderInSection:(NSInteger)section {
|
||||
TUIFaceGroup *group = _faceGroups[section];
|
||||
if (group.groupName.length > 0) {
|
||||
return CGSizeMake(self.frame.size.width, 20);
|
||||
}
|
||||
else {
|
||||
return CGSizeMake(self.frame.size.width, 0);
|
||||
}
|
||||
}
|
||||
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
|
||||
viewForSupplementaryElementOfKind:(NSString *)kind
|
||||
atIndexPath:(NSIndexPath *)indexPath {
|
||||
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader
|
||||
withReuseIdentifier:@"headerView"
|
||||
forIndexPath:indexPath];
|
||||
|
||||
for (UIView *view in headerView.subviews) {
|
||||
if (view) {
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
UILabel *view = [[UILabel alloc]initWithFrame:CGRectMake(TFaceView_Page_Padding, 0, self.frame.size.width, 17)];
|
||||
view.font = [UIFont systemFontOfSize:12];
|
||||
view.textColor = TIMCommonDynamicColor(@"", @"#444444");
|
||||
[headerView addSubview:view];
|
||||
TUIFaceGroup *group = _faceGroups[indexPath.section];
|
||||
if (group.groupName.length > 0 ){
|
||||
view.text = group.groupName;
|
||||
}
|
||||
return headerView;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
NSInteger curSection = round(scrollView.contentOffset.x / scrollView.frame.size.width);
|
||||
if (curSection >= _groupIndexInSection.count) {
|
||||
return;
|
||||
}
|
||||
NSNumber *groupIndex = _groupIndexInSection[curSection];
|
||||
NSNumber *startSection = _sectionIndexInGroup[groupIndex.integerValue];
|
||||
if (_curGroupIndex != groupIndex.integerValue) {
|
||||
_curGroupIndex = groupIndex.integerValue;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceView:scrollToFaceGroupIndex:)]) {
|
||||
[_delegate faceView:self scrollToFaceGroupIndex:_curGroupIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollView == self.faceCollectionView) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self adjustEmotionsAlpha];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)scrollToFaceGroupIndex:(NSInteger)index {
|
||||
if (index > _sectionIndexInGroup.count) {
|
||||
return;
|
||||
}
|
||||
NSNumber *start = _sectionIndexInGroup[index];
|
||||
NSInteger curSection = ceil(_faceCollectionView.contentOffset.x / _faceCollectionView.frame.size.width);
|
||||
if (curSection > start.integerValue && curSection < start.integerValue ) {
|
||||
return;
|
||||
}
|
||||
CGRect rect =
|
||||
CGRectMake(start.integerValue * _faceCollectionView.frame.size.width, 0, _faceCollectionView.frame.size.width, _faceCollectionView.frame.size.height);
|
||||
[_faceCollectionView scrollRectToVisible:rect animated:NO];
|
||||
[self scrollViewDidScroll:_faceCollectionView];
|
||||
}
|
||||
#pragma mark - floatCtrlView
|
||||
- (void)adjustEmotionsAlpha {
|
||||
if (self.floatCtrlView.isHidden) {
|
||||
return;
|
||||
}
|
||||
CGRect buttonGruopRect = self.floatCtrlView.frame;
|
||||
CGRect floatingRect = [self.faceCollectionView convertRect:buttonGruopRect fromView:self];
|
||||
for (UICollectionViewCell *visibleCell in self.faceCollectionView.visibleCells) {
|
||||
CGRect cellInCollection = [self.faceCollectionView convertRect:visibleCell.frame toView:self.faceCollectionView];
|
||||
BOOL ischongdie = CGRectIntersectsRect(floatingRect,cellInCollection);
|
||||
if(ischongdie){
|
||||
CGPoint emojiCenterPoint = CGPointMake(CGRectGetMidX(cellInCollection), CGRectGetMidY(cellInCollection));
|
||||
BOOL containsHalf = CGRectContainsPoint(floatingRect,emojiCenterPoint);
|
||||
if (containsHalf) {
|
||||
visibleCell.alpha = 0;
|
||||
} else {
|
||||
visibleCell.alpha = 0.5;
|
||||
}
|
||||
}
|
||||
else {
|
||||
visibleCell.alpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFloatCtrlViewAllowSendSwitch:(BOOL)isAllow {
|
||||
if (isAllow) {
|
||||
self.deleteButton.enabled = YES;
|
||||
self.sendButton.enabled = YES;
|
||||
self.deleteButton.alpha = 1;
|
||||
self.sendButton.alpha = 1;
|
||||
}
|
||||
else {
|
||||
self.deleteButton.enabled = NO;
|
||||
self.sendButton.enabled = NO;
|
||||
self.deleteButton.alpha = 0.5;
|
||||
self.sendButton.alpha = 0.5;
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self adjustEmotionsAlpha];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (void)didSelectSendButton:(id)btn {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceViewClickSendMessageBtn)]) {
|
||||
[_delegate faceViewClickSendMessageBtn];
|
||||
}
|
||||
}
|
||||
- (void)didSelectDeleteButton:(id)btn {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceViewDidBackDelete:)]) {
|
||||
[_delegate faceViewDidBackDelete:(id)self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showDisplayView:(CGFloat)display_x
|
||||
display_y:(CGFloat)display_y
|
||||
targetView:(UIView *)targetView
|
||||
faceCell: (TUIFaceCell *)faceCell{
|
||||
|
||||
self.hasPreViewShow = YES;
|
||||
|
||||
UIViewController *contentViewController = [[UIViewController alloc] init];
|
||||
[contentViewController.view addSubview:self.dispalyView];
|
||||
self.dispalyImage.image = (faceCell.gifImage?:faceCell.face.image);
|
||||
CGFloat dispalyImagex = 5;
|
||||
CGFloat dispalyImagey = 5;
|
||||
CGFloat dispalyImagew = MIN(faceCell.face.image.size.width, 150) ;
|
||||
CGFloat dispalyImagewh = MIN(faceCell.face.image.size.height, 150);
|
||||
|
||||
self.dispalyView.frame = CGRectMake(display_x, display_y, dispalyImagew + 10, dispalyImagewh + 10);
|
||||
_dispalyImage.frame = CGRectMake(dispalyImagex, dispalyImagey, dispalyImagewh, dispalyImagewh);
|
||||
|
||||
contentViewController.view.backgroundColor = [UIColor clearColor
|
||||
];
|
||||
contentViewController.preferredContentSize = CGSizeMake(self.dispalyView.frame.size.width, self.dispalyView.frame.size.height);
|
||||
contentViewController.modalPresentationStyle = UIModalPresentationPopover;
|
||||
|
||||
UIPopoverPresentationController *popoverController = contentViewController.popoverPresentationController;
|
||||
|
||||
popoverController.sourceView = self;
|
||||
popoverController.sourceRect = CGRectMake(0, -10, self.frame.size.width, 0);
|
||||
popoverController.permittedArrowDirections = UIPopoverArrowDirectionDown;
|
||||
popoverController.delegate = self;
|
||||
popoverController.canOverlapSourceViewRect = NO;
|
||||
[self.mm_viewController presentViewController:contentViewController animated:YES completion:nil];
|
||||
popoverController.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
}
|
||||
- (UIImageView *)dispalyView {
|
||||
if (!_dispalyView) {
|
||||
_dispalyView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
|
||||
_dispalyView.contentMode = UIViewContentModeScaleToFill;
|
||||
_dispalyView.backgroundColor = [UIColor whiteColor];
|
||||
[_dispalyView addSubview:self.dispalyImage];
|
||||
}
|
||||
return _dispalyView;
|
||||
|
||||
}
|
||||
- (UIImageView *)dispalyImage {
|
||||
if (!_dispalyImage) {
|
||||
_dispalyImage = [[UIImageView alloc] init];
|
||||
}
|
||||
return _dispalyImage;
|
||||
}
|
||||
#pragma mark - UIPopoverPresentationControllerDelegate
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
|
||||
self.hasPreViewShow = NO;
|
||||
}
|
||||
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
|
||||
return UIModalPresentationNone;
|
||||
}
|
||||
@end
|
||||
135
TUIKit/TUIChat/BaseCell/TUIFaceView.h
Normal file
135
TUIKit/TUIChat/BaseCell/TUIFaceView.h
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
* 【Module Description】
|
||||
* - This file declares the TUIFaceViewDelegate protocol and two classes, TUIFaceGroup and TUIFaceView.
|
||||
* - This file is used to implement the emoticon browsing view in the chat window, that is, the interface opened by clicking the smiley face icon in the
|
||||
* default state.
|
||||
* - Through this view, you can view and use all your emoticons, browse between different emoji groups, and further select and send emoticon message.
|
||||
* - This view has integrated the editing function of string-type expressions (such as [Smile]).
|
||||
*
|
||||
* 【Function description】
|
||||
* - TUIFaceView: Emoji view, which displays the emoticons of each group, and provides functions for selecting and deleting emoticons.
|
||||
* - TUIFaceGroup: Emoticons group. Including the initialization of the emoticons group, the positioning of a single emoticon, etc.
|
||||
*/
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class TUIFaceView;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIFaceViewDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol TUIFaceViewDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* The callback after sliding to the specified emoticons group.
|
||||
* - You can use this callback to respond to the swipe operation, and then update the information of the emoticon view to display the emoticons in the new
|
||||
* emoticon group.
|
||||
*
|
||||
* @param faceView Delegator, emoticon view. Usually, the expression view has one and only one.
|
||||
* @param index The index of the emoji group to which slide.
|
||||
*/
|
||||
- (void)faceView:(TUIFaceView *)faceView scrollToFaceGroupIndex:(NSInteger)index;
|
||||
|
||||
|
||||
/**
|
||||
* The Callback after selecting a specific emoticon (index positioning).
|
||||
* You can use this callback to achieve:
|
||||
* - When a string type emoticon (such as [smile]) is clicked, add the emoticon to the input bar.
|
||||
* - When clicking on another type of emoji, send that emoji directly.
|
||||
*
|
||||
* @param faceView Delegator, emoticon view. Usually, the expression view has one and only one.
|
||||
* @param indexPath Index path, used to locate expressions. index.section: the group where the expression is located; index.row: the row where the expression
|
||||
* is located.
|
||||
*/
|
||||
- (void)faceView:(TUIFaceView *)faceView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
|
||||
|
||||
/**
|
||||
* The action callback after clicking the delete button in the emoji view.
|
||||
* You can use this callback to delete the entire emoji string in the inputBar, for example, for "[smile]", delete the square brackets and the content between
|
||||
* the brackets directly, instead of only deleting the rightmost "]".
|
||||
*
|
||||
* @param faceView Delegator, emoticon view. Usually, the expression view has one and only one.
|
||||
*/
|
||||
- (void)faceViewDidBackDelete:(TUIFaceView *)faceView;
|
||||
@end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIFaceView
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 【Module name】TUIFaceView
|
||||
* 【Function description】It is used to realize the emoticon browsing view in the chat window, that is, the interface opened by clicking the smiley face icon
|
||||
* in the default state.
|
||||
* - Through this view, you can view all available emoticons, support emoticon grouping, select and send emoticons.
|
||||
* - This view has integrated the editing functions of string-type emoticons (such as [smile]), and the selection and sending functions of image-type
|
||||
* emoticons.
|
||||
*/
|
||||
@interface TUIFaceView : UIView
|
||||
|
||||
/**
|
||||
* Line view
|
||||
* The separtor which distinguish emoticons from other views
|
||||
*/
|
||||
@property(nonatomic, strong) UIView *lineView;
|
||||
|
||||
/**
|
||||
* The collectionView of emoticon view
|
||||
* Contains multiple lines of expressions, and cooperates with faceFlowLayout for flexible and unified view layout.
|
||||
*/
|
||||
@property(nonatomic, strong) UICollectionView *faceCollectionView;
|
||||
|
||||
/**
|
||||
* The flow layout of @faceCollectionView
|
||||
* Cooperating with faceCollectionView to make the expression view more beautiful. Supported setting layout direction, line spacing, cell spacing, etc.
|
||||
*/
|
||||
@property(nonatomic, strong) UICollectionViewFlowLayout *faceFlowLayout;
|
||||
|
||||
/**
|
||||
* Page control
|
||||
* It is used to realize multi-page browsing of emoticons, can slide to switch emoticon pages, and display the total number of pages and the current number of
|
||||
* pages in the form of small dots below the emoticon page.
|
||||
*/
|
||||
@property(nonatomic, strong) UIPageControl *pageControl;
|
||||
|
||||
/**
|
||||
* The data of @faceView
|
||||
* The object stored in this NSMutableArray is TUIFaceGroup, that is, the expression group.
|
||||
*/
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *faceGroups;
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *sectionIndexInGroup;
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *pageCountInGroup;
|
||||
@property(nonatomic, strong, readonly) NSMutableArray *groupIndexInSection;
|
||||
@property(nonatomic, strong, readonly) NSMutableDictionary *itemIndexs;
|
||||
|
||||
/**
|
||||
* Delegate variable, delegated
|
||||
* Need to implement the functionality required in the @TUIFaceViewDelegate protocol.
|
||||
*/
|
||||
@property(nonatomic, weak) id<TUIFaceViewDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Swipe to the specified expression group.
|
||||
* Switch the emoticon group according to the subscript of the emoticon group clicked by the user.
|
||||
*
|
||||
* @param index The index of the destination group, starting from 0.
|
||||
*/
|
||||
- (void)scrollToFaceGroupIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
* Setting data
|
||||
* Used to initialize TUIFaceView or update data in faceView when needed.
|
||||
*
|
||||
* @param data The data that needs to be set (TUIFaceGroup). The object stored in this NSMutableArray is TUIFaceGroup, that is, the emoticon group.
|
||||
*/
|
||||
- (void)setData:(NSMutableArray *)data;
|
||||
@end
|
||||
217
TUIKit/TUIChat/BaseCell/TUIFaceView.m
Normal file
217
TUIKit/TUIChat/BaseCell/TUIFaceView.m
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// TUIFaceView.m
|
||||
// UIKit
|
||||
//
|
||||
// Created by kennethmiao on 2018/9/18.
|
||||
// Copyright © 2018 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFaceView.h"
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
|
||||
@interface TUIFaceView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
||||
@property(nonatomic, strong) NSMutableArray *sectionIndexInGroup;
|
||||
@property(nonatomic, strong) NSMutableArray *pageCountInGroup;
|
||||
@property(nonatomic, strong) NSMutableArray *groupIndexInSection;
|
||||
@property(nonatomic, strong) NSMutableDictionary *itemIndexs;
|
||||
@property(nonatomic, assign) NSInteger sectionCount;
|
||||
@property(nonatomic, assign) NSInteger curGroupIndex;
|
||||
@end
|
||||
|
||||
@implementation TUIFaceView
|
||||
|
||||
- (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");
|
||||
|
||||
_faceFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
|
||||
_faceFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
_faceFlowLayout.minimumLineSpacing = TFaceView_Margin;
|
||||
_faceFlowLayout.minimumInteritemSpacing = TFaceView_Margin;
|
||||
_faceFlowLayout.sectionInset = UIEdgeInsetsMake(0, TFaceView_Page_Padding, 0, TFaceView_Page_Padding);
|
||||
|
||||
_faceCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_faceFlowLayout];
|
||||
[_faceCollectionView registerClass:[TUIFaceCell class] forCellWithReuseIdentifier:TFaceCell_ReuseId];
|
||||
_faceCollectionView.collectionViewLayout = _faceFlowLayout;
|
||||
_faceCollectionView.pagingEnabled = YES;
|
||||
_faceCollectionView.delegate = self;
|
||||
_faceCollectionView.dataSource = self;
|
||||
_faceCollectionView.showsHorizontalScrollIndicator = NO;
|
||||
_faceCollectionView.showsVerticalScrollIndicator = NO;
|
||||
_faceCollectionView.backgroundColor = self.backgroundColor;
|
||||
_faceCollectionView.alwaysBounceHorizontal = YES;
|
||||
[self addSubview:_faceCollectionView];
|
||||
|
||||
_lineView = [[UIView alloc] init];
|
||||
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
|
||||
|
||||
_pageControl = [[UIPageControl alloc] init];
|
||||
_pageControl.currentPageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_current_color", @"#7D7D7D");
|
||||
_pageControl.pageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_color", @"#DEDEDE");
|
||||
_pageControl.userInteractionEnabled = NO;
|
||||
[self addSubview:_pageControl];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
[self defaultLayout];
|
||||
}
|
||||
|
||||
- (void)defaultLayout {
|
||||
_lineView.frame = CGRectMake(0, 0, self.frame.size.width, TLine_Heigh);
|
||||
_pageControl.frame = CGRectMake(0, self.frame.size.height - TFaceView_Page_Height, self.frame.size.width, TFaceView_Page_Height);
|
||||
_faceCollectionView.frame = CGRectMake(0, _lineView.frame.origin.y + _lineView.frame.size.height + TFaceView_Margin, self.frame.size.width,
|
||||
self.frame.size.height - _pageControl.frame.size.height - _lineView.frame.size.height - 2 * TFaceView_Margin);
|
||||
}
|
||||
|
||||
- (void)setData:(NSMutableArray *)data {
|
||||
_faceGroups = data;
|
||||
[self defaultLayout];
|
||||
|
||||
_sectionIndexInGroup = [NSMutableArray array];
|
||||
_groupIndexInSection = [NSMutableArray array];
|
||||
_itemIndexs = [NSMutableDictionary dictionary];
|
||||
_pageCountInGroup = [NSMutableArray array];
|
||||
|
||||
NSInteger sectionIndex = 0;
|
||||
for (NSInteger groupIndex = 0; groupIndex < _faceGroups.count; ++groupIndex) {
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
[_sectionIndexInGroup addObject:@(sectionIndex)];
|
||||
int itemCount = group.rowCount * group.itemCountPerRow;
|
||||
int sectionCount = ceil(group.faces.count * 1.0 / (itemCount - (group.needBackDelete ? 1 : 0)));
|
||||
[_pageCountInGroup addObject:@(sectionCount)];
|
||||
for (int sectionIndex = 0; sectionIndex < sectionCount; ++sectionIndex) {
|
||||
[_groupIndexInSection addObject:@(groupIndex)];
|
||||
}
|
||||
sectionIndex += sectionCount;
|
||||
}
|
||||
_sectionCount = sectionIndex;
|
||||
|
||||
for (NSInteger curSection = 0; curSection < _sectionCount; ++curSection) {
|
||||
NSNumber *groupIndex = _groupIndexInSection[curSection];
|
||||
NSNumber *groupSectionIndex = _sectionIndexInGroup[groupIndex.integerValue];
|
||||
TUIFaceGroup *face = _faceGroups[groupIndex.integerValue];
|
||||
NSInteger itemCount = face.rowCount * face.itemCountPerRow - face.needBackDelete;
|
||||
NSInteger groupSection = curSection - groupSectionIndex.integerValue;
|
||||
for (NSInteger itemIndex = 0; itemIndex < itemCount; ++itemIndex) {
|
||||
// transpose line/row
|
||||
NSInteger row = itemIndex % face.rowCount;
|
||||
NSInteger column = itemIndex / face.rowCount;
|
||||
NSInteger reIndex = face.itemCountPerRow * row + column + groupSection * itemCount;
|
||||
[_itemIndexs setObject:@(reIndex) forKey:[NSIndexPath indexPathForRow:itemIndex inSection:curSection]];
|
||||
}
|
||||
}
|
||||
|
||||
_curGroupIndex = 0;
|
||||
if (_pageCountInGroup.count != 0) {
|
||||
_pageControl.numberOfPages = [_pageCountInGroup[0] intValue];
|
||||
}
|
||||
[_faceCollectionView reloadData];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
||||
return _sectionCount;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
int groupIndex = [_groupIndexInSection[section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
return group.rowCount * group.itemCountPerRow;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIFaceCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TFaceCell_ReuseId forIndexPath:indexPath];
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
int itemCount = group.rowCount * group.itemCountPerRow;
|
||||
if (indexPath.row == itemCount - 1 && group.needBackDelete) {
|
||||
TUIFaceCellData *data = [[TUIFaceCellData alloc] init];
|
||||
data.path = TUIChatFaceImagePath(@"del_normal");
|
||||
[cell setData:data];
|
||||
cell.face.image = [cell.face.image rtl_imageFlippedForRightToLeftLayoutDirection];
|
||||
} else {
|
||||
NSNumber *index = [_itemIndexs objectForKey:indexPath];
|
||||
if (index.integerValue < group.faces.count) {
|
||||
TUIFaceCellData *data = group.faces[index.integerValue];
|
||||
[cell setData:data];
|
||||
} else {
|
||||
[cell setData:nil];
|
||||
}
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *faces = _faceGroups[groupIndex];
|
||||
int itemCount = faces.rowCount * faces.itemCountPerRow;
|
||||
if (indexPath.row == itemCount - 1 && faces.needBackDelete) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceViewDidBackDelete:)]) {
|
||||
[_delegate faceViewDidBackDelete:self];
|
||||
}
|
||||
} else {
|
||||
NSNumber *index = [_itemIndexs objectForKey:indexPath];
|
||||
if (index.integerValue < faces.faces.count) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceView:didSelectItemAtIndexPath:)]) {
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index.integerValue inSection:groupIndex];
|
||||
[_delegate faceView:self didSelectItemAtIndexPath:indexPath];
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
int groupIndex = [_groupIndexInSection[indexPath.section] intValue];
|
||||
TUIFaceGroup *group = _faceGroups[groupIndex];
|
||||
CGFloat width = (self.frame.size.width - TFaceView_Page_Padding * 2 - TFaceView_Margin * (group.itemCountPerRow - 1)) / group.itemCountPerRow;
|
||||
CGFloat height = (collectionView.frame.size.height - TFaceView_Margin * (group.rowCount - 1)) / group.rowCount;
|
||||
return CGSizeMake(width, height);
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
NSInteger curSection = round(scrollView.contentOffset.x / scrollView.frame.size.width);
|
||||
if (curSection >= _groupIndexInSection.count) {
|
||||
return;
|
||||
}
|
||||
NSNumber *groupIndex = _groupIndexInSection[curSection];
|
||||
NSNumber *startSection = _sectionIndexInGroup[groupIndex.integerValue];
|
||||
NSNumber *pageCount = _pageCountInGroup[groupIndex.integerValue];
|
||||
if (_curGroupIndex != groupIndex.integerValue) {
|
||||
_curGroupIndex = groupIndex.integerValue;
|
||||
_pageControl.numberOfPages = pageCount.integerValue;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(faceView:scrollToFaceGroupIndex:)]) {
|
||||
[_delegate faceView:self scrollToFaceGroupIndex:_curGroupIndex];
|
||||
}
|
||||
}
|
||||
_pageControl.currentPage = curSection - startSection.integerValue;
|
||||
}
|
||||
|
||||
- (void)scrollToFaceGroupIndex:(NSInteger)index {
|
||||
if (index > _sectionIndexInGroup.count) {
|
||||
return;
|
||||
}
|
||||
NSNumber *start = _sectionIndexInGroup[index];
|
||||
NSNumber *count = _pageCountInGroup[index];
|
||||
NSInteger curSection = ceil(_faceCollectionView.contentOffset.x / _faceCollectionView.frame.size.width);
|
||||
if (curSection > start.integerValue && curSection < start.integerValue + count.integerValue) {
|
||||
return;
|
||||
}
|
||||
CGRect rect =
|
||||
CGRectMake(start.integerValue * _faceCollectionView.frame.size.width, 0, _faceCollectionView.frame.size.width, _faceCollectionView.frame.size.height);
|
||||
[_faceCollectionView scrollRectToVisible:rect animated:NO];
|
||||
[self scrollViewDidScroll:_faceCollectionView];
|
||||
}
|
||||
@end
|
||||
24
TUIKit/TUIChat/BaseCell/TUIGroupNoticeCell.h
Normal file
24
TUIKit/TUIChat/BaseCell/TUIGroupNoticeCell.h
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// TUIGroupNoticeCell.h
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIGroupNoticeCellData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIGroupNoticeCell : UITableViewCell
|
||||
|
||||
@property(nonatomic, strong) UILabel *nameLabel;
|
||||
@property(nonatomic, strong) UILabel *descLabel;
|
||||
@property(nonatomic, strong) UIImageView *iconView;
|
||||
|
||||
@property(nonatomic, strong) TUIGroupNoticeCellData *cellData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
109
TUIKit/TUIChat/BaseCell/TUIGroupNoticeCell.m
Normal file
109
TUIKit/TUIChat/BaseCell/TUIGroupNoticeCell.m
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// TUIGroupNoticeCell.m
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupNoticeCell.h"
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@implementation TUIGroupNoticeCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
[self setupViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupViews {
|
||||
self.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
|
||||
self.contentView.backgroundColor = TIMCommonDynamicColor(@"form_bg_color", @"#FFFFFF");
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
[self.contentView addSubview:self.nameLabel];
|
||||
[self.contentView addSubview:self.descLabel];
|
||||
|
||||
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
|
||||
tapRecognizer.delegate = self;
|
||||
tapRecognizer.cancelsTouchesInView = NO;
|
||||
[self.contentView addGestureRecognizer:tapRecognizer];
|
||||
}
|
||||
|
||||
- (void)tapGesture:(UIGestureRecognizer *)gesture {
|
||||
if (self.cellData.selector && self.cellData.target) {
|
||||
if ([self.cellData.target respondsToSelector:self.cellData.selector]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[self.cellData.target performSelector:self.cellData.selector];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCellData:(TUIGroupNoticeCellData *)cellData {
|
||||
_cellData = cellData;
|
||||
|
||||
self.nameLabel.text = cellData.name;
|
||||
self.descLabel.text = cellData.desc;
|
||||
// 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.nameLabel sizeToFit];
|
||||
[self.nameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(20);
|
||||
make.top.mas_equalTo(12);
|
||||
make.trailing.mas_lessThanOrEqualTo(self.contentView).mas_offset(-20);
|
||||
make.size.mas_equalTo(self.nameLabel.frame.size);
|
||||
}];
|
||||
[self.descLabel sizeToFit];
|
||||
[self.descLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.nameLabel);
|
||||
make.top.mas_equalTo(self.nameLabel.mas_bottom).mas_offset(4);
|
||||
make.trailing.mas_lessThanOrEqualTo(self.contentView).mas_offset(-30);
|
||||
make.size.mas_equalTo(self.descLabel.frame.size);
|
||||
}];
|
||||
}
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
- (UILabel *)nameLabel {
|
||||
if (_nameLabel == nil) {
|
||||
_nameLabel = [[UILabel alloc] init];
|
||||
_nameLabel.text = @"";
|
||||
_nameLabel.textColor = TIMCommonDynamicColor(@"form_key_text_color", @"#888888");
|
||||
_nameLabel.font = [UIFont systemFontOfSize:16.0];
|
||||
}
|
||||
return _nameLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)descLabel {
|
||||
if (_descLabel == nil) {
|
||||
_descLabel = [[UILabel alloc] init];
|
||||
_descLabel.text = @"neirong";
|
||||
_descLabel.textColor = TIMCommonDynamicColor(@"form_subtitle_color", @"#BBBBBB");
|
||||
_descLabel.font = [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
return _descLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
16
TUIKit/TUIChat/BaseCellData/Base/TUIGroupCreatedCellData.h
Normal file
16
TUIKit/TUIChat/BaseCellData/Base/TUIGroupCreatedCellData.h
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
* This file declares the data source for TUIGroupCreatedCell
|
||||
*/
|
||||
|
||||
#import <TIMCommon/TUISystemMessageCellData.h>
|
||||
|
||||
@interface TUIGroupCreatedCellData : TUISystemMessageCellData
|
||||
|
||||
@property(nonatomic, copy) NSString *opUser;
|
||||
@property(nonatomic, strong) NSNumber *cmd;
|
||||
|
||||
@end
|
||||
87
TUIKit/TUIChat/BaseCellData/Base/TUIGroupCreatedCellData.m
Normal file
87
TUIKit/TUIChat/BaseCellData/Base/TUIGroupCreatedCellData.m
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// TUIGroupCreatedCellData.m
|
||||
// TUIKitDemo
|
||||
//
|
||||
// Created by annidyfeng on 2019/6/10.
|
||||
// Copyright © 2019 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupCreatedCellData.h"
|
||||
#import <TUICore/NSString+TUIUtil.h>
|
||||
|
||||
@implementation TUIGroupCreatedCellData
|
||||
|
||||
+ (TUIGroupCreatedCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
TUIGroupCreatedCellData *cellData = [[TUIGroupCreatedCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
cellData.innerMessage = message;
|
||||
cellData.msgID = message.msgID;
|
||||
cellData.content = param[@"content"];
|
||||
cellData.opUser = [self.class getOpUserName:message]?:param[@"opUser"];
|
||||
cellData.cmd = param[@"cmd"];
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (NSString *)getOpUserName:(V2TIMMessage *)info {
|
||||
NSString *opUser;
|
||||
if (info.nameCard.length > 0) {
|
||||
opUser = info.nameCard;
|
||||
} else if (info.nickName.length > 0) {
|
||||
opUser = info.nickName;
|
||||
} else {
|
||||
opUser = info.userID;
|
||||
}
|
||||
return opUser;
|
||||
}
|
||||
- (NSMutableAttributedString *)attributedString {
|
||||
NSString *localizableContent = self.content;
|
||||
if (self.cmd && [self.cmd isKindOfClass:NSNumber.class]) {
|
||||
NSInteger command = [self.cmd integerValue];
|
||||
if (command == 1) {
|
||||
localizableContent = TIMCommonLocalizableString(TUICommunityCreateTipsMessage);
|
||||
} else {
|
||||
localizableContent = TIMCommonLocalizableString(TUIGroupCreateTipsMessage);
|
||||
}
|
||||
}
|
||||
NSString *str = [NSString stringWithFormat:@"\"%@\" %@", self.opUser, localizableContent];
|
||||
str = rtlString(str);
|
||||
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:str];
|
||||
NSDictionary *attributeDict = @{NSForegroundColorAttributeName : [UIColor d_systemGrayColor]};
|
||||
[attributeString setAttributes:attributeDict range:NSMakeRange(0, attributeString.length)];
|
||||
return attributeString;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)msg {
|
||||
if (msg.customElem == nil || msg.customElem.data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *param = [TUITool jsonData2Dictionary:msg.customElem.data];
|
||||
if (param == nil || ![param isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *businessID = param[@"businessID"];
|
||||
if (![businessID isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
if (![businessID isEqualToString:BussinessID_GroupCreate] && ![param.allKeys containsObject:BussinessID_GroupCreate]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *localizableContent = param[@"content"];
|
||||
NSNumber *cmd = param[@"cmd"];
|
||||
if (cmd && [cmd isKindOfClass:NSNumber.class]) {
|
||||
NSInteger command = [cmd integerValue];
|
||||
if (command == 1) {
|
||||
localizableContent = TIMCommonLocalizableString(TUICommunityCreateTipsMessage);
|
||||
} else {
|
||||
localizableContent = TIMCommonLocalizableString(TUIGroupCreateTipsMessage);
|
||||
}
|
||||
}
|
||||
NSString * opUser = [self.class getOpUserName:msg]?:param[@"opUser"];
|
||||
NSString *str = [NSString stringWithFormat:@"\"%@\" %@", opUser, localizableContent];
|
||||
return rtlString(str);
|
||||
}
|
||||
|
||||
@end
|
||||
58
TUIKit/TUIChat/BaseCellData/Base/TUIInputMoreCellData.h
Normal file
58
TUIKit/TUIChat/BaseCellData/Base/TUIInputMoreCellData.h
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
// 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, several units that appear after clicking the "+" in the lower right corner of the chat interface.
|
||||
* - At present, "More Units" provides four multimedia sending functions of shooting, video, picture and file, and you can also customize it.
|
||||
* - TUIInputMoreCellData is responsible for storing the information needed for a series of "more" cells.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@import UIKit;
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^TUIInputMoreCallback)(NSDictionary *param);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIInputMoreCellData
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 【Module name】TUIInputMoreCellData
|
||||
* 【Function description】"More units" data source
|
||||
* - "More Units" is responsible for displaying in "More Views", showing the user the functionality contained in "More Views". At the same time, it serves as
|
||||
* the entrance of each function and responds to user interaction events.
|
||||
* - The data source is responsible for storing the information needed for a series of "more units".
|
||||
*/
|
||||
@interface TUIInputMoreCellData : NSObject
|
||||
|
||||
/**
|
||||
* Image for single unit
|
||||
* The icons of each unit are different, which are used to visually represent the function corresponding to the unit
|
||||
*/
|
||||
@property(nonatomic, strong) UIImage *image;
|
||||
|
||||
/**
|
||||
* Name for single unit
|
||||
* The names of each unit are different (such as Photo, Video, File, Album, etc.), which are used to display the corresponding functions of the unit in text
|
||||
* form below the icon.
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *title;
|
||||
|
||||
/**
|
||||
* Callback for clicked
|
||||
*/
|
||||
@property(nonatomic, copy) TUIInputMoreCallback onClicked;
|
||||
|
||||
/**
|
||||
* Prioriy for displaying in more menu list
|
||||
* The larger the value, the higher the front in list
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger priority;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
9
TUIKit/TUIChat/BaseCellData/Base/TUIInputMoreCellData.m
Normal file
9
TUIKit/TUIChat/BaseCellData/Base/TUIInputMoreCellData.m
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIInputMoreCellData.h"
|
||||
|
||||
@implementation TUIInputMoreCellData
|
||||
|
||||
@end
|
||||
39
TUIKit/TUIChat/BaseCellData/Base/TUIMemberCellData.h
Normal file
39
TUIKit/TUIChat/BaseCellData/Base/TUIMemberCellData.h
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/******************************************************************************
|
||||
*
|
||||
* This file declares the TUIMemberCellData class.
|
||||
* It provides a data source for the TUIMemberCell class, which is mainly used in the message read member list interface, etc.
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMemberDescribeCellData : TUICommonCellData
|
||||
|
||||
@property(nonatomic, copy) NSString *title;
|
||||
@property(nonatomic, copy) UIImage *icon;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface TUIMemberCellData : TUICommonCellData
|
||||
|
||||
@property(nonatomic, copy) NSString *title; // member's display name
|
||||
@property(nonatomic, copy) NSURL *avatarUrL; // member's avatar image url
|
||||
@property(nonatomic, copy) NSString *detail; // optional, used to display more info
|
||||
@property(nonatomic, copy) NSString *userID;
|
||||
|
||||
- (instancetype)initWithUserID:(nonnull NSString *)userID
|
||||
nickName:(nullable NSString *)nickName
|
||||
friendRemark:(nullable NSString *)friendRemark
|
||||
nameCard:(nullable NSString *)nameCard
|
||||
avatarUrl:(nonnull NSString *)avatarUrl
|
||||
detail:(nullable NSString *)detail;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
47
TUIKit/TUIChat/BaseCellData/Base/TUIMemberCellData.m
Normal file
47
TUIKit/TUIChat/BaseCellData/Base/TUIMemberCellData.m
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// TUIMemberCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by xia on 2022/3/14.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMemberCellData.h"
|
||||
|
||||
@implementation TUIMemberDescribeCellData
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIMemberCellData
|
||||
|
||||
- (instancetype)initWithUserID:(nonnull NSString *)userID
|
||||
nickName:(nullable NSString *)nickName
|
||||
friendRemark:(nullable NSString *)friendRemark
|
||||
nameCard:(nullable NSString *)nameCard
|
||||
avatarUrl:(nonnull NSString *)avatarUrl
|
||||
detail:(nullable NSString *)detail {
|
||||
self = [super init];
|
||||
|
||||
if (userID.length > 0) {
|
||||
_userID = userID;
|
||||
}
|
||||
|
||||
if (avatarUrl.length > 0) {
|
||||
_avatarUrL = [NSURL URLWithString:avatarUrl];
|
||||
}
|
||||
_detail = detail;
|
||||
|
||||
if (nameCard.length > 0) {
|
||||
_title = nameCard;
|
||||
} else if (friendRemark.length > 0) {
|
||||
_title = friendRemark;
|
||||
} else if (nickName.length > 0) {
|
||||
_title = nickName;
|
||||
} else {
|
||||
_title = userID;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
67
TUIKit/TUIChat/BaseCellData/Base/TUITextMessageCellData.h
Normal file
67
TUIKit/TUIChat/BaseCellData/Base/TUITextMessageCellData.h
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* This file declares the TUITextMessageCellData class.
|
||||
* This class inherits from TUIBubbleMessageCellData and is used to store a series of data and information required by the text message unit.
|
||||
*/
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* 【Module name】 TUITextMessageCellData
|
||||
* 【Function description】The datasource of text message unit.
|
||||
* - Text message unit, which is the most common message unit in most message sending and receiving situations.
|
||||
* - The text message unit data source provides a series of required data and information for the text message unit.
|
||||
*/
|
||||
@interface TUITextMessageCellData : TUIBubbleMessageCellData
|
||||
|
||||
/**
|
||||
* Content of text message
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *content;
|
||||
|
||||
@property(nonatomic, assign) BOOL isAudioCall;
|
||||
@property(nonatomic, assign) BOOL isVideoCall;
|
||||
@property(nonatomic, assign) BOOL isCaller;
|
||||
@property(nonatomic, assign) BOOL showUnreadPoint;
|
||||
|
||||
/**
|
||||
*
|
||||
* Mutable strings.
|
||||
* After the text message receives the content string, it is necessary to convert the string expression (such as [smile]) that may exist in the string into a
|
||||
* picture expression. This string is responsible for storing the converted result of the above process.
|
||||
*
|
||||
*/
|
||||
- (NSAttributedString *)getContentAttributedString:(UIFont *)textFont;
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the display size of content string
|
||||
*/
|
||||
- (CGSize)getContentAttributedStringSize:(NSAttributedString *)attributeString maxTextSize:(CGSize)maxTextSize;
|
||||
|
||||
/**
|
||||
* NSValue (NSRange) stores the converted string of emoji at the position of attributedString.
|
||||
* NSAttributedString stores the string before emoji conversion, such as "[呲牙]".
|
||||
* When the text is selected and copied, it is necessary to find the original string of emoji.
|
||||
*/
|
||||
@property(nonatomic, strong) NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *emojiLocations;
|
||||
|
||||
/**
|
||||
* The size of the label which displays the text message content.
|
||||
* Position the text message with the @textOrigin.
|
||||
*/
|
||||
@property(nonatomic, assign) CGSize textSize;
|
||||
|
||||
/**
|
||||
* The origin of label which displays the text message content.
|
||||
|
||||
*/
|
||||
@property(nonatomic, assign) CGPoint textOrigin;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
124
TUIKit/TUIChat/BaseCellData/Base/TUITextMessageCellData.m
Normal file
124
TUIKit/TUIChat/BaseCellData/Base/TUITextMessageCellData.m
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// TUITextMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITextMessageCellData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
|
||||
#ifndef CGFLOAT_CEIL
|
||||
#ifdef CGFLOAT_IS_DOUBLE
|
||||
#define CGFLOAT_CEIL(value) ceil(value)
|
||||
#else
|
||||
#define CGFLOAT_CEIL(value) ceilf(value)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@interface TUITextMessageCellData ()
|
||||
|
||||
@property(nonatomic, assign) CGSize size;
|
||||
@property(nonatomic, assign) CGFloat containerWidth;
|
||||
|
||||
@property(nonatomic, strong) NSMutableAttributedString *attributedString;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUITextMessageCellData
|
||||
{
|
||||
NSString *_content;
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
TUITextMessageCellData *textData = [[TUITextMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
textData.content = message.textElem.text;
|
||||
textData.reuseId = TTextMessageCell_ReuseId;
|
||||
textData.status = Msg_Status_Init;
|
||||
return textData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
NSString *content = message.textElem.text;
|
||||
return content.getLocalizableStringWithFaceContent;
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUITextReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUITextReplyQuoteView");
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
if (direction == MsgDirectionIncoming) {
|
||||
self.cellLayout = [TUIMessageCellLayout incommingTextMessageLayout];
|
||||
} else {
|
||||
self.cellLayout = [TUIMessageCellLayout outgoingTextMessageLayout];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setContent:(NSString *)content {
|
||||
if (![_content isEqualToString:content]) {
|
||||
_content = content;
|
||||
_attributedString = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)content {
|
||||
return _content;
|
||||
}
|
||||
|
||||
- (NSAttributedString *)getContentAttributedString:(UIFont *)textFont {
|
||||
if (!_attributedString) {
|
||||
_emojiLocations = [NSMutableArray array];
|
||||
_attributedString = [self.content getFormatEmojiStringWithFont:textFont emojiLocations:_emojiLocations];
|
||||
if (self.isAudioCall || self.isVideoCall) {
|
||||
NSTextAttachment *attchment = [[NSTextAttachment alloc] init];
|
||||
UIImage *image = nil;
|
||||
if (self.isAudioCall) {
|
||||
image = TUIChatCommonBundleImage(@"audio_call");
|
||||
}
|
||||
if (self.isVideoCall) {
|
||||
if (self.isCaller) {
|
||||
image = TUIChatCommonBundleImage(@"video_call_self");
|
||||
} else {
|
||||
image = TUIChatCommonBundleImage(@"video_call");
|
||||
}
|
||||
}
|
||||
attchment.image = image;
|
||||
attchment.bounds = CGRectMake(0, -(textFont.lineHeight - textFont.pointSize) / 2, 16, 16);
|
||||
NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:(NSTextAttachment *)(attchment)];
|
||||
NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:@" " attributes:@{NSFontAttributeName : textFont}];
|
||||
if (self.isCaller) {
|
||||
[_attributedString appendAttributedString:spaceString];
|
||||
[_attributedString appendAttributedString:imageString];
|
||||
} else {
|
||||
[_attributedString insertAttributedString:spaceString atIndex:0];
|
||||
[_attributedString insertAttributedString:imageString atIndex:0];
|
||||
}
|
||||
}
|
||||
}
|
||||
return _attributedString;
|
||||
}
|
||||
|
||||
- (CGSize)getContentAttributedStringSize:(NSAttributedString *)attributeString maxTextSize:(CGSize)maxTextSize {
|
||||
CGRect rect = [attributeString boundingRectWithSize:maxTextSize
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
context:nil];
|
||||
|
||||
CGFloat width = CGFLOAT_CEIL(rect.size.width);
|
||||
CGFloat height = CGFLOAT_CEIL(rect.size.height);
|
||||
return CGSizeMake(width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
42
TUIKit/TUIChat/BaseCellData/Chat/TUIFaceMessageCellData.h
Normal file
42
TUIKit/TUIChat/BaseCellData/Chat/TUIFaceMessageCellData.h
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
*
|
||||
* This file declares the TUIFaceMessageCellData class.
|
||||
* This class inherits from TUIMessageCellData and is used to store a series of data and information required by the emoticon message unit.
|
||||
*/
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* 【Module name】TUIFaceMessageCellData
|
||||
* 【Function description】Emoticon message unit data source.
|
||||
* - The emoticon message unit is the message unit used and displayed when displaying animated emoticons.
|
||||
* - The emoticon message unit data source is a class that provides a series of required data for the display of the emoticon message unit.
|
||||
*/
|
||||
@interface TUIFaceMessageCellData : TUIBubbleMessageCellData
|
||||
|
||||
/**
|
||||
*
|
||||
* The index of emoticon groups
|
||||
* - The subscript of the group where the emoticon is located, which is used to locate the emoticon group where the emoticon is located.
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger groupIndex;
|
||||
|
||||
/**
|
||||
* The path of the emoticon file
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *path;
|
||||
|
||||
/**
|
||||
* The name of emoticon.
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *faceName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
34
TUIKit/TUIChat/BaseCellData/Chat/TUIFaceMessageCellData.m
Normal file
34
TUIKit/TUIChat/BaseCellData/Chat/TUIFaceMessageCellData.m
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// TFaceMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFaceMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@implementation TUIFaceMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMFaceElem *elem = message.faceElem;
|
||||
TUIFaceMessageCellData *faceData = [[TUIFaceMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
faceData.groupIndex = elem.index;
|
||||
faceData.faceName = [[NSString alloc] initWithData:elem.data encoding:NSUTF8StringEncoding];
|
||||
for (TUIFaceGroup *group in [TIMConfig defaultConfig].faceGroups) {
|
||||
if (group.groupIndex == faceData.groupIndex) {
|
||||
NSString *path = [group.groupPath stringByAppendingPathComponent:faceData.faceName];
|
||||
faceData.path = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
faceData.reuseId = TFaceMessageCell_ReuseId;
|
||||
return faceData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return TIMCommonLocalizableString(TUIKitMessageTypeAnimateEmoji);
|
||||
}
|
||||
|
||||
@end
|
||||
72
TUIKit/TUIChat/BaseCellData/Chat/TUIFileMessageCellData.h
Normal file
72
TUIKit/TUIChat/BaseCellData/Chat/TUIFileMessageCellData.h
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIFileMessageCellData : TUIBubbleMessageCellData <TUIMessageCellDataFileUploadProtocol, TUIMessageCellDataFileDownloadProtocol>
|
||||
|
||||
/**
|
||||
* File path
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *path;
|
||||
|
||||
/**
|
||||
* File name, including suffix.
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *fileName;
|
||||
|
||||
/**
|
||||
* Inner ID for file
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *uuid;
|
||||
|
||||
/**
|
||||
* File size, used to display the data volume information of the file.
|
||||
*/
|
||||
@property(nonatomic, assign) int length;
|
||||
|
||||
/**
|
||||
* The progress of file uploading, which maintained by the cellData.
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger uploadProgress;
|
||||
|
||||
/**
|
||||
* The progress of file downloading, which maintained by the cellData.
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger downladProgress;
|
||||
|
||||
/**
|
||||
* The flag of indicating whether the file is downloading
|
||||
* YES: dowloading; NO: not download
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL isDownloading;
|
||||
|
||||
/**
|
||||
* Downloading the file
|
||||
* This method integrates and calls the IM SDK, and obtains the file through the interface provided by the SDK.
|
||||
* 1. Before downloading the file from server, it will try to read file from local when the file exists in the local.
|
||||
* 2. When the file not exists in the local, it will download from server through the api provided by IMSDK. But if there is downloading task, it will wait for
|
||||
* the task finished.
|
||||
* - The download progress (percentage value) is updated through the callback of the IMSDK.
|
||||
* - There are two parameters which is @curSize and @totalSize in the callback of IMSDK. The progress value equals to curSize * 100 / totalSize.
|
||||
* 3. When finished download, the file will be storaged to the @path.
|
||||
*/
|
||||
- (void)downloadFile;
|
||||
|
||||
/**
|
||||
* Determine if the file is already downloaded to local
|
||||
* This method will first try to get the file path from the local, if the acquisition is successful, record the path and return YES. Otherwise return NO.
|
||||
*/
|
||||
- (BOOL)isLocalExist;
|
||||
|
||||
/**
|
||||
* Getting the file path and it will return the flag of whether the file exists through @isExist.
|
||||
*/
|
||||
- (NSString *)getFilePath:(BOOL *)isExist;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
148
TUIKit/TUIChat/BaseCellData/Chat/TUIFileMessageCellData.m
Normal file
148
TUIKit/TUIChat/BaseCellData/Chat/TUIFileMessageCellData.m
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// TUIFileMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFileMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/NSString+TUIUtil.h>
|
||||
#import "TUIMessageProgressManager.h"
|
||||
|
||||
@interface TUIFileMessageCellData ()
|
||||
@property(nonatomic, strong) NSMutableArray *progressBlocks;
|
||||
@property(nonatomic, strong) NSMutableArray *responseBlocks;
|
||||
@end
|
||||
|
||||
@implementation TUIFileMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMFileElem *elem = message.fileElem;
|
||||
TUIFileMessageCellData *fileData = [[TUIFileMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
fileData.path = [elem.path safePathString];
|
||||
fileData.fileName = elem.filename;
|
||||
fileData.length = elem.fileSize;
|
||||
fileData.uuid = elem.uuid;
|
||||
fileData.reuseId = TFileMessageCell_ReuseId;
|
||||
return fileData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return TIMCommonLocalizableString(TUIkitMessageTypeFile); // @"[File]";
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUIFileReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUIFileReplyQuoteView");
|
||||
}
|
||||
|
||||
- (int)length {
|
||||
if (self.innerMessage) {
|
||||
_length = self.innerMessage.fileElem.fileSize;
|
||||
}
|
||||
return _length;
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
_uploadProgress = 100;
|
||||
_downladProgress = 100;
|
||||
_isDownloading = NO;
|
||||
_progressBlocks = [NSMutableArray array];
|
||||
_responseBlocks = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)downloadFile {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getFilePath:&isExist];
|
||||
if (isExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger progress = [TUIMessageProgressManager.shareManager downloadProgressForMessage:self.msgID];
|
||||
if (progress != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isDownloading) return;
|
||||
self.isDownloading = YES;
|
||||
@weakify(self);
|
||||
if (self.innerMessage.elemType == V2TIM_ELEM_TYPE_FILE) {
|
||||
NSString *msgID = self.msgID;
|
||||
[self.innerMessage.fileElem downloadFile:path
|
||||
progress:^(NSInteger curSize, NSInteger totalSize) {
|
||||
@strongify(self);
|
||||
NSInteger progress = curSize * 100 / totalSize;
|
||||
[self updateDownalodProgress:MIN(progress, 99)];
|
||||
[TUIMessageProgressManager.shareManager appendDownloadProgress:msgID progress:MIN(progress, 99)];
|
||||
}
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
self.isDownloading = NO;
|
||||
[self updateDownalodProgress:100];
|
||||
[TUIMessageProgressManager.shareManager appendDownloadProgress:msgID progress:100];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.path = path;
|
||||
});
|
||||
});
|
||||
}
|
||||
fail:^(int code, NSString *msg) {
|
||||
@strongify(self);
|
||||
self.isDownloading = NO;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateDownalodProgress:(NSUInteger)progress {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.downladProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)isLocalExist {
|
||||
BOOL isExist;
|
||||
[self getFilePath:&isExist];
|
||||
return isExist;
|
||||
}
|
||||
|
||||
- (NSString *)getFilePath:(BOOL *)isExist {
|
||||
NSString *path = nil;
|
||||
BOOL isDir = NO;
|
||||
*isExist = NO;
|
||||
if (self.direction == MsgDirectionOutgoing) {
|
||||
// The origin file path is valid when uploading
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_File_Path, _path.lastPathComponent];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isExist) {
|
||||
path = [NSString stringWithFormat:@"%@%@%@", TUIKit_File_Path,self.uuid, _fileName];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (*isExist) {
|
||||
_path = path;
|
||||
}
|
||||
|
||||
// TODO: uuid
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@end
|
||||
22
TUIKit/TUIChat/BaseCellData/Chat/TUIGroupNoticeCellData.h
Normal file
22
TUIKit/TUIChat/BaseCellData/Chat/TUIGroupNoticeCellData.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// TUIGroupNoticeCellData.h
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIGroupNoticeCellData : NSObject
|
||||
|
||||
@property(nonatomic, copy) NSString *name;
|
||||
@property(nonatomic, copy) NSString *desc;
|
||||
@property(nonatomic, weak) id target;
|
||||
@property(nonatomic, assign) SEL selector;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
13
TUIKit/TUIChat/BaseCellData/Chat/TUIGroupNoticeCellData.m
Normal file
13
TUIKit/TUIChat/BaseCellData/Chat/TUIGroupNoticeCellData.m
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// TUIGroupNoticeCellData.m
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupNoticeCellData.h"
|
||||
|
||||
@implementation TUIGroupNoticeCellData
|
||||
|
||||
@end
|
||||
106
TUIKit/TUIChat/BaseCellData/Chat/TUIImageMessageCellData.h
Normal file
106
TUIKit/TUIChat/BaseCellData/Chat/TUIImageMessageCellData.h
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import "TUIChatDefine.h"
|
||||
#import "TUIMessageItem.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIImageMessageCellData
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
/**
|
||||
*
|
||||
* 【Module name】 TUIImageMessageCellData
|
||||
* 【Function description】It is used to realize the picture bubble in the chat window, including the display of picture message sending progress.
|
||||
* At the same time, this module already supports three different types of "thumbnail", "large image" and "original image", and
|
||||
* has handled the business logic of displaying the corresponding image type under appropriate circumstances:
|
||||
* 1. Thumbnail - By default, you see thumbnails in the chat window, which is smaller and saves traffic.
|
||||
* 2. Large image - If the user clicks on the thumbnail, they see a larger image with a better resolution.
|
||||
* 3. Original image - If the sender chooses to send the original image, the recipient will see the "original image" button which can click to download the
|
||||
* image with the original size.
|
||||
*/
|
||||
@interface TUIImageMessageCellData : TUIBubbleMessageCellData <TUIMessageCellDataFileUploadProtocol>
|
||||
|
||||
@property(nonatomic, strong) UIImage *thumbImage;
|
||||
@property(nonatomic, strong) UIImage *originImage;
|
||||
@property(nonatomic, strong) UIImage *largeImage;
|
||||
|
||||
/**
|
||||
*
|
||||
* The file storage path
|
||||
*
|
||||
* @note
|
||||
* @path is maintained by the program by default, you can directly obtain the demo storage path by importing TIMDefine.h and referencing TUIKit_Image_Path
|
||||
* Other routes are also available if you have further individual needs
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *path;
|
||||
@property(nonatomic, assign) NSInteger length;
|
||||
|
||||
/**
|
||||
*
|
||||
* The set of image items
|
||||
*
|
||||
* @note
|
||||
* There are usually three imageItems stored in @items, namely thumb (thumb image), origin (original image), and large (large image), which is convenient to
|
||||
* obtain images flexibly according to needs.
|
||||
*
|
||||
*/
|
||||
@property(nonatomic, strong) NSMutableArray *items;
|
||||
|
||||
/**
|
||||
* The progress of loading thumbnail
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger thumbProgress;
|
||||
|
||||
/**
|
||||
* The progress of loading origin image
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger originProgress;
|
||||
|
||||
/**
|
||||
* The progress of loading large image
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger largeProgress;
|
||||
|
||||
/**
|
||||
* The progress of uploading (sending)
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger uploadProgress;
|
||||
|
||||
@property(nonatomic, assign) BOOL isSuperLongImage;
|
||||
|
||||
/**
|
||||
|
||||
* Downloading image.
|
||||
* This method integrates and calls the IM SDK, and obtains images from sever through the interface provided by the SDK.
|
||||
* 1. Before downloading the file from server, it will try to read file from local when the file exists in the local.
|
||||
* 2. If the file is not exist in the local, it will download from server through the api named @getImage which provided by the class of TIMImage in the IMSDK.
|
||||
* - The download progress (percentage value) is updated through the callback of the IMSDK.
|
||||
* - There are two parameters which is @curSize and @totalSize in the callback of IMSDK. The progress value equals to curSize * 100 / totalSize.
|
||||
* - The type of items in the image message is TIMElem. You can obtain image list from the paramter named imageList provided by TIMElem, which including
|
||||
* original image、large image and thumbnail and you can obtain the image from it with the @imageType.
|
||||
* 3. The image obtained through the SDK interface is a binary file, which needs to be decoded first, converted to CGIamge for decoding, and then packaged as a
|
||||
* UIImage before it can be used.
|
||||
* 4. When finished download, the image will be storaged to the @path.
|
||||
*/
|
||||
- (void)downloadImage:(TUIImageType)type;
|
||||
- (void)downloadImage:(TUIImageType)type finish:(TUIImageMessageDownloadCallback)finish;
|
||||
|
||||
/**
|
||||
*
|
||||
* Decode the image and assign the image to a variable of the corresponding type (@thumbImage, @largeImage or @originImage).
|
||||
*/
|
||||
- (void)decodeImage:(TUIImageType)type;
|
||||
|
||||
/**
|
||||
*
|
||||
* Getting image file path
|
||||
*/
|
||||
- (NSString *)getImagePath:(TUIImageType)type isExist:(BOOL *)isExist;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
227
TUIKit/TUIChat/BaseCellData/Chat/TUIImageMessageCellData.m
Normal file
227
TUIKit/TUIChat/BaseCellData/Chat/TUIImageMessageCellData.m
Normal file
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// TUIImageMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIImageMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/NSString+TUIUtil.h>
|
||||
|
||||
@interface TUIImageMessageCellData ()
|
||||
@property(nonatomic, assign) BOOL isDownloading;
|
||||
@property(nonatomic, copy) TUIImageMessageDownloadCallback onFinish;
|
||||
@end
|
||||
|
||||
@implementation TUIImageMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMImageElem *elem = message.imageElem;
|
||||
TUIImageMessageCellData *imageData = [[TUIImageMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
imageData.path = [elem.path safePathString];
|
||||
imageData.items = [NSMutableArray array];
|
||||
for (V2TIMImage *item in elem.imageList) {
|
||||
TUIImageItem *itemData = [[TUIImageItem alloc] init];
|
||||
itemData.uuid = item.uuid;
|
||||
itemData.size = CGSizeMake(item.width, item.height);
|
||||
// itemData.url = item.url;
|
||||
if (item.type == V2TIM_IMAGE_TYPE_THUMB) {
|
||||
itemData.type = TImage_Type_Thumb;
|
||||
} else if (item.type == V2TIM_IMAGE_TYPE_LARGE) {
|
||||
itemData.type = TImage_Type_Large;
|
||||
} else if (item.type == V2TIM_IMAGE_TYPE_ORIGIN) {
|
||||
itemData.type = TImage_Type_Origin;
|
||||
}
|
||||
[imageData.items addObject:itemData];
|
||||
}
|
||||
imageData.reuseId = TImageMessageCell_ReuseId;
|
||||
return imageData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return TIMCommonLocalizableString(TUIkitMessageTypeImage); // @"[Image]";
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUIImageReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUIImageReplyQuoteView");
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
_uploadProgress = 100;
|
||||
if (direction == MsgDirectionIncoming) {
|
||||
self.cellLayout = [TUIMessageCellLayout incommingImageMessageLayout];
|
||||
} else {
|
||||
self.cellLayout = [TUIMessageCellLayout outgoingImageMessageLayout];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)getImagePath:(TUIImageType)type isExist:(BOOL *)isExist {
|
||||
NSString *path = nil;
|
||||
BOOL isDir = NO;
|
||||
*isExist = NO;
|
||||
if (self.direction == MsgDirectionOutgoing) {
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_Image_Path, _path.lastPathComponent];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isExist) {
|
||||
TUIImageItem *tImageItem = [self getTImageItem:type];
|
||||
path = [NSString stringWithFormat:@"%@%@_%ld", TUIKit_Image_Path, tImageItem.uuid, (long)tImageItem.type];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
- (void)downloadImage:(TUIImageType)type finish:(TUIImageMessageDownloadCallback)finish {
|
||||
self.onFinish = finish;
|
||||
[self downloadImage:type];
|
||||
}
|
||||
|
||||
- (void)downloadImage:(TUIImageType)type {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getImagePath:type isExist:&isExist];
|
||||
if (isExist) {
|
||||
[self decodeImage:type];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isDownloading) {
|
||||
return;
|
||||
}
|
||||
self.isDownloading = YES;
|
||||
|
||||
V2TIMImage *imImage = [self getIMImage:type];
|
||||
|
||||
@weakify(self);
|
||||
[imImage downloadImage:path
|
||||
progress:^(NSInteger curSize, NSInteger totalSize) {
|
||||
@strongify(self);
|
||||
NSInteger progress = curSize * 100 / totalSize;
|
||||
[self updateProgress:MIN(progress, 99) withType:type];
|
||||
}
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
self.isDownloading = NO;
|
||||
[self updateProgress:100 withType:type];
|
||||
[self decodeImage:type];
|
||||
}
|
||||
fail:^(int code, NSString *msg) {
|
||||
@strongify(self);
|
||||
self.isDownloading = NO;
|
||||
/**
|
||||
* If the uuid of the picture is the same (the same user sends
|
||||
* the same picture continuously), the same path may trigger multiple download operations. Except for the first time, subsequent downloads will report
|
||||
* an error. At this time, it is necessary to judge whether the local file exists.
|
||||
*/
|
||||
[self decodeImage:type];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateProgress:(NSUInteger)progress withType:(TUIImageType)type {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (type == TImage_Type_Thumb) self.thumbProgress = progress;
|
||||
if (type == TImage_Type_Large) self.largeProgress = progress;
|
||||
if (type == TImage_Type_Origin) self.originProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)decodeImage:(TUIImageType)type {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getImagePath:type isExist:&isExist];
|
||||
if (!isExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^finishBlock)(UIImage *) = ^(UIImage *image) {
|
||||
if (type == TImage_Type_Thumb) {
|
||||
weakSelf.thumbImage = image;
|
||||
weakSelf.thumbProgress = 100;
|
||||
weakSelf.uploadProgress = 100;
|
||||
}
|
||||
if (type == TImage_Type_Large) {
|
||||
weakSelf.largeImage = image;
|
||||
weakSelf.largeProgress = 100;
|
||||
}
|
||||
if (type == TImage_Type_Origin) {
|
||||
weakSelf.originImage = image;
|
||||
weakSelf.originProgress = 100;
|
||||
}
|
||||
if (weakSelf.onFinish) {
|
||||
weakSelf.onFinish();
|
||||
}
|
||||
};
|
||||
|
||||
NSString *cacheKey = [path substringFromIndex:TUIKit_Image_Path.length];
|
||||
|
||||
UIImage *cacheImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:cacheKey];
|
||||
if (cacheImage) {
|
||||
finishBlock(cacheImage);
|
||||
} else {
|
||||
[TUITool asyncDecodeImage:path
|
||||
complete:^(NSString *path, UIImage *image) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSLog(@"image.sd_imageFormat: %ld path:%@ image.sd_imageData.length :%lu",(long)image.sd_imageFormat,path,(unsigned long)image.sd_imageData.length);
|
||||
if (![path tui_containsString:@".gif"] || (image.sd_imageFormat != SDImageFormatGIF) ) {
|
||||
[[SDImageCache sharedImageCache] storeImageToMemory:image forKey:cacheKey];
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* The gif image is too large to be cached in memory
|
||||
* Only cache images less than 1M
|
||||
*/
|
||||
if (image.sd_imageData.length < 1 * 1024 * 1024) {
|
||||
[[SDImageCache sharedImageCache] storeImageToMemory:image forKey:cacheKey];
|
||||
}
|
||||
}
|
||||
finishBlock(image);
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (TUIImageItem *)getTImageItem:(TUIImageType)type {
|
||||
for (TUIImageItem *item in self.items) {
|
||||
if (item.type == type) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (V2TIMImage *)getIMImage:(TUIImageType)type {
|
||||
V2TIMMessage *imMsg = self.innerMessage;
|
||||
if (imMsg.elemType == V2TIM_ELEM_TYPE_IMAGE) {
|
||||
for (V2TIMImage *imImage in imMsg.imageElem.imageList) {
|
||||
if (type == TImage_Type_Thumb && imImage.type == V2TIM_IMAGE_TYPE_THUMB) {
|
||||
return imImage;
|
||||
} else if (type == TImage_Type_Origin && imImage.type == V2TIM_IMAGE_TYPE_ORIGIN) {
|
||||
return imImage;
|
||||
} else if (type == TImage_Type_Large && imImage.type == V2TIM_IMAGE_TYPE_LARGE) {
|
||||
return imImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
*
|
||||
* This file declares the TUIJoinGroupMessageCellData class.
|
||||
* This document is responsible for realizing the function of the small gray bar for entering the group, and can also be further extended to a group message
|
||||
* unit with a single operator. That is, this file can highlight the operator's nickname in blue and provide a response interface for the highlighted part in
|
||||
* blue.
|
||||
*/
|
||||
#import <TIMCommon/TUISystemMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIJoinGroupMessageCellData : TUISystemMessageCellData
|
||||
|
||||
/**
|
||||
*
|
||||
* Operator nickname. For example, "Tom joined the group", the variable content is "Tom"
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *opUserName;
|
||||
|
||||
/**
|
||||
* The nickname of the operator.
|
||||
*/
|
||||
@property(nonatomic, strong) NSMutableArray<NSString *> *userNameList;
|
||||
|
||||
/**
|
||||
* Operator Id.
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *opUserID;
|
||||
|
||||
/**
|
||||
* List of the operator IDs.
|
||||
*/
|
||||
@property(nonatomic, strong) NSMutableArray<NSString *> *userIDList;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
#import "TUIJoinGroupMessageCellData.h"
|
||||
|
||||
@implementation TUIJoinGroupMessageCellData
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
self.userNameList = [NSMutableArray array];
|
||||
self.userIDList = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
25
TUIKit/TUIChat/BaseCellData/Chat/TUIMenuCellData.h
Normal file
25
TUIKit/TUIChat/BaseCellData/Chat/TUIMenuCellData.h
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIMenuCellData
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface TUIMenuCellData : NSObject
|
||||
|
||||
/**
|
||||
* Access path for grouped thumbnails in grouping units
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *path;
|
||||
|
||||
@property(nonatomic, assign) BOOL isSelected;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
9
TUIKit/TUIChat/BaseCellData/Chat/TUIMenuCellData.m
Normal file
9
TUIKit/TUIChat/BaseCellData/Chat/TUIMenuCellData.m
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMenuCellData.h"
|
||||
|
||||
@implementation TUIMenuCellData
|
||||
|
||||
@end
|
||||
28
TUIKit/TUIChat/BaseCellData/Chat/TUIMergeMessageCellData.h
Normal file
28
TUIKit/TUIChat/BaseCellData/Chat/TUIMergeMessageCellData.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// TUIMergeMessageCellData.h
|
||||
// Pods
|
||||
//
|
||||
// Created by harvy on 2020/12/9.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMergeMessageCellData : TUIMessageCellData
|
||||
|
||||
@property(nonatomic, copy) NSString *title;
|
||||
@property(nonatomic, strong) NSArray<NSString *> *abstractList;
|
||||
@property(nonatomic, strong) V2TIMMergerElem *mergerElem;
|
||||
@property(nonatomic, assign) CGSize abstractSize;
|
||||
@property(nonatomic, assign) CGSize abstractRow1Size;
|
||||
@property(nonatomic, assign) CGSize abstractRow2Size;
|
||||
@property(nonatomic, assign) CGSize abstractRow3Size;
|
||||
@property(nonatomic, strong) NSArray<NSDictionary *> *abstractSendDetailList;
|
||||
- (NSAttributedString *)abstractAttributedString;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
119
TUIKit/TUIChat/BaseCellData/Chat/TUIMergeMessageCellData.m
Normal file
119
TUIKit/TUIChat/BaseCellData/Chat/TUIMergeMessageCellData.m
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// TUIMergeMessageCellData.m
|
||||
// Pods
|
||||
//
|
||||
// Created by harvy on 2020/12/9.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMergeMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import "TUITextMessageCellData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
|
||||
@implementation TUIMergeMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMMergerElem *elem = message.mergerElem;
|
||||
if (elem.layersOverLimit) {
|
||||
TUITextMessageCellData *limitCell = [[TUITextMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
limitCell.content = TIMCommonLocalizableString(TUIKitRelayLayerLimitTips);
|
||||
return limitCell;
|
||||
}
|
||||
|
||||
TUIMergeMessageCellData *mergeData = [[TUIMergeMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
mergeData.title = elem.title;
|
||||
mergeData.abstractList = [NSArray arrayWithArray:elem.abstractList];
|
||||
mergeData.abstractSendDetailList = [self.class formatAbstractSendDetailList:elem.abstractList];
|
||||
mergeData.mergerElem = elem;
|
||||
mergeData.reuseId = TMergeMessageCell_ReuserId;
|
||||
return mergeData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return [NSString stringWithFormat:@"[%@]", TIMCommonLocalizableString(TUIKitRelayChatHistory)];
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUIMergeReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUIMergeReplyQuoteView");
|
||||
}
|
||||
|
||||
- (NSAttributedString *)abstractAttributedString {
|
||||
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
||||
style.lineSpacing = 4;
|
||||
style.alignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
|
||||
NSDictionary *attribute = @{
|
||||
NSForegroundColorAttributeName : [UIColor colorWithRed:187 / 255.0 green:187 / 255.0 blue:187 / 255.0 alpha:1 / 1.0],
|
||||
NSFontAttributeName : [UIFont systemFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : style
|
||||
};
|
||||
|
||||
NSMutableAttributedString *abstr = [[NSMutableAttributedString alloc] initWithString:@""];
|
||||
int i = 0;
|
||||
for (NSString *ab in self.abstractList) {
|
||||
if (i >= 4) {
|
||||
break;
|
||||
}
|
||||
NSString *resultStr = [NSString stringWithFormat:@"%@\n", ab];
|
||||
NSString *str = resultStr;
|
||||
[abstr appendAttributedString:[[NSAttributedString alloc] initWithString:str attributes:attribute]];
|
||||
i++;
|
||||
}
|
||||
return abstr;
|
||||
}
|
||||
|
||||
+ (NSMutableArray *)formatAbstractSendDetailList:(NSArray *)originAbstractList {
|
||||
NSMutableArray *array = [NSMutableArray arrayWithCapacity:3];
|
||||
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
||||
style.alignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
|
||||
style.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
NSDictionary *attribute = @{
|
||||
NSForegroundColorAttributeName : [UIColor colorWithRed:187 / 255.0 green:187 / 255.0 blue:187 / 255.0 alpha:1 / 1.0],
|
||||
NSFontAttributeName : [UIFont systemFontOfSize:12.0],
|
||||
NSParagraphStyleAttributeName : style
|
||||
};
|
||||
int i = 0;
|
||||
for (NSString *ab in originAbstractList) {
|
||||
if (i >= 4) {
|
||||
break;
|
||||
}
|
||||
NSString *str = ab;
|
||||
NSString * splitStr = @":";
|
||||
if ([str tui_containsString:@"\u202C:"]) {
|
||||
splitStr = @"\u202C:";
|
||||
}
|
||||
NSArray<NSString *> *result = [str componentsSeparatedByString:splitStr];
|
||||
NSString *sender = result[0];
|
||||
NSString *detail = result[1];
|
||||
sender = [NSString stringWithFormat:@"%@",sender];
|
||||
detail = [NSString stringWithFormat:@"%@",detail.getLocalizableStringWithFaceContent];
|
||||
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:3];
|
||||
if(sender.length>0 ){
|
||||
NSMutableAttributedString *abstr = [[NSMutableAttributedString alloc] initWithString:@""];
|
||||
[abstr appendAttributedString:[[NSAttributedString alloc] initWithString:sender attributes:attribute]];
|
||||
[dic setObject:abstr forKey:@"sender"];
|
||||
}
|
||||
if(detail.length>0 ){
|
||||
NSMutableAttributedString *abstr = [[NSMutableAttributedString alloc] initWithString:@""];
|
||||
[abstr appendAttributedString:[[NSAttributedString alloc] initWithString:detail attributes:attribute]];
|
||||
[dic setObject:abstr forKey:@"detail"];
|
||||
}
|
||||
[array addObject:dic];
|
||||
|
||||
i++;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
- (BOOL)isArString:(NSString *)text {
|
||||
NSString *isoLangCode = (__bridge_transfer NSString *)CFStringTokenizerCopyBestStringLanguage((__bridge CFStringRef)text, CFRangeMake(0, text.length));
|
||||
|
||||
if ([isoLangCode isEqualToString:@"ar"]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
@end
|
||||
19
TUIKit/TUIChat/BaseCellData/Chat/TUITypingStatusCellData.h
Normal file
19
TUIKit/TUIChat/BaseCellData/Chat/TUITypingStatusCellData.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// TUITypingStatusCellData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/7/4.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUITypingStatusCellData : TUIMessageCellData
|
||||
|
||||
@property(nonatomic, assign) NSInteger typingStatus;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
25
TUIKit/TUIChat/BaseCellData/Chat/TUITypingStatusCellData.m
Normal file
25
TUIKit/TUIChat/BaseCellData/Chat/TUITypingStatusCellData.m
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// TUITypingStatusCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/7/4.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITypingStatusCellData.h"
|
||||
|
||||
@implementation TUITypingStatusCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
TUITypingStatusCellData *cellData = [[TUITypingStatusCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
|
||||
cellData.msgID = message.msgID;
|
||||
|
||||
if ([param.allKeys containsObject:@"typingStatus"]) {
|
||||
cellData.typingStatus = [param[@"typingStatus"] intValue];
|
||||
}
|
||||
|
||||
return cellData;
|
||||
}
|
||||
|
||||
@end
|
||||
60
TUIKit/TUIChat/BaseCellData/Chat/TUIVideoMessageCellData.h
Normal file
60
TUIKit/TUIChat/BaseCellData/Chat/TUIVideoMessageCellData.h
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
* 1. This file declares the TUIVideoItem class, TUISnapshotItem class, and TUIVideoMessageCellData class.
|
||||
* - TUIVideoItem corresponds to V2TIMVideoElem in the IM SDK. We convert the classes in the SDK to TUIVideoItem, which is convenient for us to further
|
||||
* process and operate the data.
|
||||
* - TUISnapshotItem corresponds to the video cover class in the IM SDK. It is still an image in essence, but is bound to the corresponding Video.
|
||||
* - TUIVideoMessageCellData inherits from the TUIMessageCellData class and is used to store a series of data and information required by the image message
|
||||
* unit.
|
||||
* 2. The business logic for obtaining video information and cover information has been implemented in this document. When you need to get video and cover
|
||||
* data, you can directly call the relevant member functions declared in this file
|
||||
*/
|
||||
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import "TUIChatDefine.h"
|
||||
#import "TUIMessageItem.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIVideoMessageCellData
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface TUIVideoMessageCellData : TUIBubbleMessageCellData <TUIMessageCellDataFileUploadProtocol>
|
||||
|
||||
@property(nonatomic, strong) UIImage *thumbImage;
|
||||
@property(nonatomic, strong) NSString *videoPath;
|
||||
@property(nonatomic, strong) NSString *snapshotPath;
|
||||
@property(nonatomic, strong) TUIVideoItem *videoItem;
|
||||
@property(nonatomic, strong) TUISnapshotItem *snapshotItem;
|
||||
@property(nonatomic, assign) NSUInteger uploadProgress;
|
||||
@property(nonatomic, assign) NSUInteger thumbProgress;
|
||||
@property(nonatomic, assign) NSUInteger videoProgress;
|
||||
/// Is the current message a custom message
|
||||
@property(nonatomic, assign) BOOL isPlaceHolderCellData;
|
||||
|
||||
+ (TUIMessageCellData *)placeholderCellDataWithSnapshotUrl:(NSString *)snapshotUrl thubImage:(UIImage *)thubImage;
|
||||
|
||||
- (void)getVideoUrl:(void (^)(NSString *url))urlCallBack;
|
||||
|
||||
/**
|
||||
* Downloading the cover image of the video. It will download from server if the image not exist in local.
|
||||
*/
|
||||
- (void)downloadThumb;
|
||||
- (void)downloadThumb:(TUIVideoMessageDownloadCallback)finish;
|
||||
|
||||
/**
|
||||
* Downloading the video file. It will download from server if the video not exist in local.
|
||||
*/
|
||||
- (void)downloadVideo;
|
||||
|
||||
- (BOOL)isVideoExist;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
290
TUIKit/TUIChat/BaseCellData/Chat/TUIVideoMessageCellData.m
Normal file
290
TUIKit/TUIChat/BaseCellData/Chat/TUIVideoMessageCellData.m
Normal file
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// TUIVideoMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/NSString+TUIUtil.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
|
||||
#define TVideo_Block_Progress @"TVideo_Block_Progress";
|
||||
#define TVideo_Block_Response @"TVideo_Block_Response";
|
||||
|
||||
@interface TUIVideoMessageCellData ()
|
||||
@property(nonatomic, strong) NSString *videoUrl;
|
||||
@property(nonatomic, assign) BOOL isDownloadingSnapshot;
|
||||
@property(nonatomic, assign) BOOL isDownloadingVideo;
|
||||
@property(nonatomic, copy) TUIVideoMessageDownloadCallback onFinish;
|
||||
@end
|
||||
|
||||
@implementation TUIVideoMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMVideoElem *elem = message.videoElem;
|
||||
TUIVideoMessageCellData *videoData = [[TUIVideoMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
videoData.videoPath = [elem.videoPath safePathString];
|
||||
videoData.snapshotPath = [elem.snapshotPath safePathString];
|
||||
|
||||
videoData.videoItem = [[TUIVideoItem alloc] init];
|
||||
videoData.videoItem.uuid = elem.videoUUID;
|
||||
videoData.videoItem.type = elem.videoType;
|
||||
videoData.videoItem.length = elem.videoSize;
|
||||
videoData.videoItem.duration = elem.duration;
|
||||
|
||||
videoData.snapshotItem = [[TUISnapshotItem alloc] init];
|
||||
videoData.snapshotItem.uuid = elem.snapshotUUID;
|
||||
// videoData.snapshotItem.type = elem.snaps;
|
||||
videoData.snapshotItem.length = elem.snapshotSize;
|
||||
videoData.snapshotItem.size = CGSizeMake(elem.snapshotWidth, elem.snapshotHeight);
|
||||
videoData.reuseId = TVideoMessageCell_ReuseId;
|
||||
return videoData;
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)placeholderCellDataWithSnapshotUrl:(NSString *)snapshotUrl thubImage:(UIImage *)thubImage {
|
||||
TUIVideoMessageCellData *videoData = [[TUIVideoMessageCellData alloc] initWithDirection:(MsgDirectionOutgoing)];
|
||||
videoData.thumbImage = thubImage;
|
||||
videoData.snapshotPath = [snapshotUrl safePathString];
|
||||
videoData.videoItem = [[TUIVideoItem alloc] init];
|
||||
videoData.snapshotItem = [[TUISnapshotItem alloc] init];
|
||||
videoData.snapshotItem.size = CGSizeEqualToSize(thubImage.size, CGSizeZero) ? CGSizeMake(kScale375(100), kScale375(100)) : thubImage.size;
|
||||
videoData.reuseId = TVideoMessageCell_ReuseId;
|
||||
videoData.avatarUrl = [NSURL URLWithString:[TUILogin getFaceUrl]];
|
||||
videoData.isPlaceHolderCellData = YES;
|
||||
return videoData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return TIMCommonLocalizableString(TUIkitMessageTypeVideo);
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUIVideoReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUIVideoReplyQuoteView");
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
_uploadProgress = 100;
|
||||
_isDownloadingVideo = NO;
|
||||
_isDownloadingSnapshot = NO;
|
||||
if (direction == MsgDirectionIncoming) {
|
||||
self.cellLayout = [TUIMessageCellLayout incommingVideoMessageLayout];
|
||||
} else {
|
||||
self.cellLayout = [TUIMessageCellLayout outgoingVideoMessageLayout];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)downloadThumb:(TUIVideoMessageDownloadCallback)finish {
|
||||
self.onFinish = finish;
|
||||
[self downloadThumb];
|
||||
}
|
||||
|
||||
- (void)downloadThumb {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getSnapshotPath:&isExist];
|
||||
if (isExist) {
|
||||
[self decodeThumb];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isDownloadingSnapshot) {
|
||||
return;
|
||||
}
|
||||
self.isDownloadingSnapshot = YES;
|
||||
|
||||
@weakify(self);
|
||||
V2TIMMessage *imMsg = self.innerMessage;
|
||||
if (imMsg.elemType == V2TIM_ELEM_TYPE_VIDEO) {
|
||||
// Avoid large files that slow down callback progress.
|
||||
[self updateThumbProgress:1];
|
||||
[imMsg.videoElem downloadSnapshot:path
|
||||
progress:^(NSInteger curSize, NSInteger totalSize) {
|
||||
[self updateThumbProgress:MAX(1, curSize * 100 / totalSize)];
|
||||
}
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
self.isDownloadingSnapshot = NO;
|
||||
[self updateThumbProgress:100];
|
||||
[self decodeThumb];
|
||||
}
|
||||
fail:^(int code, NSString *msg) {
|
||||
@strongify(self);
|
||||
self.isDownloadingSnapshot = NO;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateThumbProgress:(NSUInteger)progress {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.thumbProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)decodeThumb {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getSnapshotPath:&isExist];
|
||||
if (!isExist) {
|
||||
return;
|
||||
}
|
||||
@weakify(self);
|
||||
[TUITool asyncDecodeImage:path
|
||||
complete:^(NSString *path, UIImage *image) {
|
||||
@strongify(self);
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
self.thumbImage = image;
|
||||
self.thumbProgress = 100;
|
||||
if (self.onFinish) {
|
||||
self.onFinish();
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)downloadVideo {
|
||||
BOOL isExist = NO;
|
||||
NSString *path = [self getVideoPath:&isExist];
|
||||
if (isExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isDownloadingVideo) {
|
||||
return;
|
||||
}
|
||||
self.isDownloadingVideo = YES;
|
||||
|
||||
@weakify(self);
|
||||
V2TIMMessage *imMsg = self.innerMessage;
|
||||
if (imMsg.elemType == V2TIM_ELEM_TYPE_VIDEO) {
|
||||
[imMsg.videoElem downloadVideo:path
|
||||
progress:^(NSInteger curSize, NSInteger totalSize) {
|
||||
@strongify(self);
|
||||
[self updateVideoProgress:curSize * 100 / totalSize];
|
||||
}
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
self.isDownloadingVideo = NO;
|
||||
[self updateVideoProgress:100];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.videoPath = path;
|
||||
});
|
||||
}
|
||||
fail:^(int code, NSString *msg) {
|
||||
@strongify(self);
|
||||
self.isDownloadingVideo = NO;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateVideoProgress:(NSUInteger)progress {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.videoProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)getVideoUrl:(void (^)(NSString *url))urlCallBack {
|
||||
if (!urlCallBack) {
|
||||
return;
|
||||
}
|
||||
if (self.videoUrl) {
|
||||
urlCallBack(self.videoUrl);
|
||||
}
|
||||
@weakify(self);
|
||||
V2TIMMessage *imMsg = self.innerMessage;
|
||||
if (imMsg.elemType == V2TIM_ELEM_TYPE_VIDEO) {
|
||||
[imMsg.videoElem getVideoUrl:^(NSString *url) {
|
||||
@strongify(self);
|
||||
self.videoUrl = url;
|
||||
urlCallBack(self.videoUrl);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isVideoExist {
|
||||
BOOL isExist;
|
||||
[self getVideoPath:&isExist];
|
||||
return isExist;
|
||||
}
|
||||
|
||||
- (NSString *)getVideoPath:(BOOL *)isExist {
|
||||
NSString *path = nil;
|
||||
BOOL isDir = NO;
|
||||
*isExist = NO;
|
||||
if (_videoPath && _videoPath.lastPathComponent.length) {
|
||||
path = _videoPath;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
else {
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_Video_Path, _videoPath.lastPathComponent];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isExist) {
|
||||
if (_videoItem) {
|
||||
if (_videoItem.uuid && _videoItem.uuid.length && _videoItem.type && _videoItem.type.length) {
|
||||
path = [NSString stringWithFormat:@"%@%@.%@", TUIKit_Video_Path, _videoItem.uuid, _videoItem.type];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (*isExist) {
|
||||
_videoPath = path;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
- (NSString *)getSnapshotPath:(BOOL *)isExist {
|
||||
NSString *path = nil;
|
||||
BOOL isDir = NO;
|
||||
*isExist = NO;
|
||||
if (_snapshotPath && _snapshotPath.length) {
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_Video_Path, _snapshotPath.lastPathComponent];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isExist) {
|
||||
if (_snapshotItem) {
|
||||
if (_snapshotItem.uuid && _snapshotItem.uuid.length) {
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_Video_Path, _snapshotItem.uuid];
|
||||
path = [TUIKit_Video_Path stringByAppendingString:_snapshotItem.uuid];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@end
|
||||
66
TUIKit/TUIChat/BaseCellData/Chat/TUIVoiceMessageCellData.h
Normal file
66
TUIKit/TUIChat/BaseCellData/Chat/TUIVoiceMessageCellData.h
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TUIVoiceAudioPlaybackStyle) {
|
||||
TUIVoiceAudioPlaybackStyleLoudspeaker = 1,
|
||||
TUIVoiceAudioPlaybackStyleHandset = 2,
|
||||
};
|
||||
|
||||
@interface TUIVoiceMessageCellData : TUIBubbleMessageCellData
|
||||
|
||||
@property(nonatomic, strong) NSString *path;
|
||||
@property(nonatomic, strong) NSString *uuid;
|
||||
@property(nonatomic, assign) int duration;
|
||||
@property(nonatomic, assign) int length;
|
||||
@property(nonatomic, assign) BOOL isDownloading;
|
||||
@property(nonatomic, assign) BOOL isPlaying;
|
||||
@property(nonatomic, assign) CGFloat voiceHeight;
|
||||
@property(nonatomic, assign) NSTimeInterval currentTime;
|
||||
|
||||
/**
|
||||
*
|
||||
* Play animation image
|
||||
* An animation used to implement the "sonic image" fade of the speech as it plays.
|
||||
* If you want to customize the implementation of other kinds of animation icons, you can refer to the implementation of voiceAnimationIamges here.
|
||||
*/
|
||||
@property NSArray<UIImage *> *voiceAnimationImages;
|
||||
|
||||
/**
|
||||
*
|
||||
* voice icon
|
||||
* Animated icon to show when the speech is not playing.
|
||||
*/
|
||||
@property UIImage *voiceImage;
|
||||
@property(nonatomic, assign) CGFloat voiceTop;
|
||||
|
||||
/**
|
||||
*
|
||||
* Top margin of voice message
|
||||
* This value is used to determine the position of the bubble, which is convenient for UI layout of the content in the bubble.
|
||||
* If the value is abnormal or set arbitrarily, UI errors such as message position dislocation will occur.
|
||||
*/
|
||||
@property(nonatomic, class) CGFloat incommingVoiceTop;
|
||||
@property(nonatomic, class) CGFloat outgoingVoiceTop;
|
||||
|
||||
- (void)stopVoiceMessage;
|
||||
|
||||
/**
|
||||
* Begin to play voice. It will download the voice file from server if it not exists in local.
|
||||
*/
|
||||
- (void)playVoiceMessage;
|
||||
|
||||
@property(nonatomic, copy) void (^audioPlayerDidFinishPlayingBlock)(void);
|
||||
|
||||
|
||||
//The style of audio playback.
|
||||
+ (TUIVoiceAudioPlaybackStyle)getAudioplaybackStyle;
|
||||
+ (void)changeAudioPlaybackStyle;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
263
TUIKit/TUIChat/BaseCellData/Chat/TUIVoiceMessageCellData.m
Normal file
263
TUIKit/TUIChat/BaseCellData/Chat/TUIVoiceMessageCellData.m
Normal file
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// TUIVoiceMessageCellData.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/5/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIVoiceMessageCellData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
@import AVFoundation;
|
||||
|
||||
@interface TUIVoiceMessageCellData () <AVAudioPlayerDelegate>
|
||||
@property AVAudioPlayer *audioPlayer;
|
||||
@property NSString *wavPath;
|
||||
@property(nonatomic, strong) NSTimer *timer;
|
||||
@end
|
||||
|
||||
@implementation TUIVoiceMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
V2TIMSoundElem *elem = message.soundElem;
|
||||
TUIVoiceMessageCellData *soundData = [[TUIVoiceMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
soundData.duration = elem.duration;
|
||||
soundData.length = elem.dataSize;
|
||||
soundData.uuid = elem.uuid;
|
||||
soundData.reuseId = TVoiceMessageCell_ReuseId;
|
||||
soundData.path = elem.path;
|
||||
return soundData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return TIMCommonLocalizableString(TUIKitMessageTypeVoice); // @"[Voice]";
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewDataClass {
|
||||
return NSClassFromString(@"TUIVoiceReplyQuoteViewData");
|
||||
}
|
||||
|
||||
- (Class)getReplyQuoteViewClass {
|
||||
return NSClassFromString(@"TUIVoiceReplyQuoteView");
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
if (direction == MsgDirectionIncoming) {
|
||||
self.cellLayout = [TUIMessageCellLayout incommingVoiceMessageLayout];
|
||||
_voiceImage = TUIChatDynamicImage(@"chat_voice_message_receiver_voice_normal_img",
|
||||
[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_receiver_normal")]);
|
||||
_voiceImage = [_voiceImage rtl_imageFlippedForRightToLeftLayoutDirection];
|
||||
_voiceAnimationImages = [NSArray arrayWithObjects:[self.class formatImageByName:@"message_voice_receiver_playing_1"],
|
||||
[self.class formatImageByName:@"message_voice_receiver_playing_2"],
|
||||
[self.class formatImageByName:@"message_voice_receiver_playing_3"], nil];
|
||||
_voiceTop = [[self class] incommingVoiceTop];
|
||||
} else {
|
||||
self.cellLayout = [TUIMessageCellLayout outgoingVoiceMessageLayout];
|
||||
_voiceImage = TUIChatDynamicImage(@"chat_voice_message_sender_voice_normal_img",
|
||||
[[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"message_voice_sender_normal")]);
|
||||
_voiceImage = [_voiceImage rtl_imageFlippedForRightToLeftLayoutDirection];
|
||||
_voiceAnimationImages = [NSArray arrayWithObjects:[self.class formatImageByName:@"message_voice_sender_playing_1"],
|
||||
[self.class formatImageByName:@"message_voice_sender_playing_2"],
|
||||
[self.class formatImageByName:@"message_voice_sender_playing_3"], nil];
|
||||
_voiceTop = [[self class] outgoingVoiceTop];
|
||||
}
|
||||
_voiceHeight = 21;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (UIImage *)formatImageByName:(NSString *)imgName {
|
||||
NSString *path = TUIChatImagePath(imgName);
|
||||
UIImage *img = [[TUIImageCache sharedInstance] getResourceFromCache:path];
|
||||
return [img rtl_imageFlippedForRightToLeftLayoutDirection];
|
||||
}
|
||||
|
||||
- (NSString *)getVoicePath:(BOOL *)isExist {
|
||||
NSString *path = nil;
|
||||
BOOL isDir = false;
|
||||
*isExist = NO;
|
||||
if (self.direction == MsgDirectionOutgoing) {
|
||||
if (_path.length) {
|
||||
path = [NSString stringWithFormat:@"%@%@", TUIKit_Voice_Path, _path.lastPathComponent];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*isExist) {
|
||||
if (_uuid.length) {
|
||||
path = [NSString stringWithFormat:@"%@%@.amr", TUIKit_Voice_Path, _uuid];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
|
||||
if (!isDir) {
|
||||
*isExist = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
- (V2TIMSoundElem *)getIMSoundElem {
|
||||
V2TIMMessage *imMsg = self.innerMessage;
|
||||
if (imMsg.elemType == V2TIM_ELEM_TYPE_SOUND) {
|
||||
return imMsg.soundElem;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)playVoiceMessage {
|
||||
if (self.isPlaying) {
|
||||
[self stopVoiceMessage];
|
||||
return;
|
||||
}
|
||||
self.isPlaying = YES;
|
||||
|
||||
if (self.innerMessage.localCustomInt == 0) self.innerMessage.localCustomInt = 1;
|
||||
|
||||
V2TIMSoundElem *imSound = [self getIMSoundElem];
|
||||
BOOL isExist = NO;
|
||||
if (self.uuid.length == 0) {
|
||||
self.uuid = imSound.uuid;
|
||||
}
|
||||
NSString *path = [self getVoicePath:&isExist];
|
||||
if (isExist) {
|
||||
[self playInternal:path];
|
||||
} else {
|
||||
if (self.isDownloading) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
self.isDownloading = YES;
|
||||
@weakify(self);
|
||||
[imSound downloadSound:path
|
||||
progress:^(NSInteger curSize, NSInteger totalSize) {
|
||||
|
||||
}
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
self.isDownloading = NO;
|
||||
[self playInternal:path];
|
||||
}
|
||||
fail:^(int code, NSString *msg) {
|
||||
@strongify(self);
|
||||
self.isDownloading = NO;
|
||||
[self stopVoiceMessage];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)playInternal:(NSString *)path {
|
||||
if (!self.isPlaying) return;
|
||||
// play current
|
||||
TUIVoiceAudioPlaybackStyle playbackStyle = [self.class getAudioplaybackStyle];
|
||||
if(playbackStyle == TUIVoiceAudioPlaybackStyleHandset) {
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
|
||||
}
|
||||
else {
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
|
||||
}
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
|
||||
self.audioPlayer.delegate = self;
|
||||
bool result = [self.audioPlayer play];
|
||||
if (!result) {
|
||||
self.wavPath = [[path stringByDeletingPathExtension] stringByAppendingString:@".wav"];
|
||||
NSURL *url = [NSURL fileURLWithPath:self.wavPath];
|
||||
[self.audioPlayer stop];
|
||||
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
|
||||
self.audioPlayer.delegate = self;
|
||||
[self.audioPlayer play];
|
||||
}
|
||||
|
||||
@weakify(self);
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
|
||||
repeats:YES
|
||||
block:^(NSTimer *_Nonnull timer) {
|
||||
@strongify(self);
|
||||
[self updateProgress];
|
||||
}];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
//The style of audio playback.
|
||||
+ (TUIVoiceAudioPlaybackStyle)getAudioplaybackStyle {
|
||||
NSString *style = [NSUserDefaults.standardUserDefaults objectForKey:@"tui_audioPlaybackStyle"];
|
||||
if ([style isEqualToString:@"1"]) {
|
||||
return TUIVoiceAudioPlaybackStyleLoudspeaker;
|
||||
} else if ([style isEqualToString:@"2"]) {
|
||||
return TUIVoiceAudioPlaybackStyleHandset;
|
||||
}
|
||||
return TUIVoiceAudioPlaybackStyleLoudspeaker;
|
||||
}
|
||||
|
||||
+ (void)changeAudioPlaybackStyle {
|
||||
TUIVoiceAudioPlaybackStyle style = [self getAudioplaybackStyle];
|
||||
if (style == TUIVoiceAudioPlaybackStyleLoudspeaker) {
|
||||
[NSUserDefaults.standardUserDefaults setObject:@"2" forKey:@"tui_audioPlaybackStyle"];
|
||||
}
|
||||
else {
|
||||
[NSUserDefaults.standardUserDefaults setObject:@"1" forKey:@"tui_audioPlaybackStyle"];
|
||||
}
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
|
||||
}
|
||||
- (void)updateProgress {
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
self.currentTime = self.audioPlayer.currentTime;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopVoiceMessage {
|
||||
if ([self.audioPlayer isPlaying]) {
|
||||
[self.audioPlayer stop];
|
||||
self.audioPlayer = nil;
|
||||
}
|
||||
if (self.timer) {
|
||||
[self.timer invalidate];
|
||||
self.timer = nil;
|
||||
}
|
||||
self.isPlaying = NO;
|
||||
}
|
||||
|
||||
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;
|
||||
{
|
||||
[self stopVoiceMessage];
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.wavPath error:nil];
|
||||
|
||||
if (self.audioPlayerDidFinishPlayingBlock) {
|
||||
self.audioPlayerDidFinishPlayingBlock();
|
||||
}
|
||||
}
|
||||
|
||||
static CGFloat gIncommingVoiceTop = 12;
|
||||
|
||||
+ (void)setIncommingVoiceTop:(CGFloat)incommingVoiceTop {
|
||||
gIncommingVoiceTop = incommingVoiceTop;
|
||||
}
|
||||
|
||||
+ (CGFloat)incommingVoiceTop {
|
||||
return gIncommingVoiceTop;
|
||||
}
|
||||
|
||||
static CGFloat gOutgoingVoiceTop = 12;
|
||||
|
||||
+ (void)setOutgoingVoiceTop:(CGFloat)outgoingVoiceTop {
|
||||
gOutgoingVoiceTop = outgoingVoiceTop;
|
||||
}
|
||||
|
||||
+ (CGFloat)outgoingVoiceTop {
|
||||
return gOutgoingVoiceTop;
|
||||
}
|
||||
|
||||
@end
|
||||
21
TUIKit/TUIChat/BaseCellData/Custom/TUIEvaluationCellData.h
Normal file
21
TUIKit/TUIChat/BaseCellData/Custom/TUIEvaluationCellData.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// TUIEvaluationCellData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by xia on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIEvaluationCellData : TUIBubbleMessageCellData
|
||||
|
||||
@property(nonatomic, assign) NSInteger score;
|
||||
@property(nonatomic, copy) NSString *desc;
|
||||
@property(nonatomic, copy) NSString *comment;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
40
TUIKit/TUIChat/BaseCellData/Custom/TUIEvaluationCellData.m
Normal file
40
TUIKit/TUIChat/BaseCellData/Custom/TUIEvaluationCellData.m
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// TUIEvaluationCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by xia on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIEvaluationCellData.h"
|
||||
|
||||
@implementation TUIEvaluationCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
if (param == nil) {
|
||||
return nil;
|
||||
}
|
||||
TUIEvaluationCellData *cellData = [[TUIEvaluationCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
|
||||
cellData.innerMessage = message;
|
||||
cellData.desc = message.customElem.desc;
|
||||
cellData.score = [param[@"score"] integerValue];
|
||||
cellData.comment = param[@"comment"];
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
return message.customElem.desc;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize {
|
||||
CGRect rect = [self.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 += self.comment.length > 0 ? 88 : 50;
|
||||
return size;
|
||||
}
|
||||
|
||||
@end
|
||||
19
TUIKit/TUIChat/BaseCellData/Custom/TUILinkCellData.h
Normal file
19
TUIKit/TUIChat/BaseCellData/Custom/TUILinkCellData.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// MyCustomCellData.h
|
||||
// TUIKitDemo
|
||||
//
|
||||
// Created by annidyfeng on 2019/6/10.
|
||||
// Copyright © 2019 Tencent. All rights reserved.
|
||||
//
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUILinkCellData : TUIBubbleMessageCellData
|
||||
|
||||
@property NSString *text;
|
||||
@property NSString *link;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
38
TUIKit/TUIChat/BaseCellData/Custom/TUILinkCellData.m
Normal file
38
TUIKit/TUIChat/BaseCellData/Custom/TUILinkCellData.m
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// MyCustomCellData.m
|
||||
// TUIKitDemo
|
||||
//
|
||||
// Created by annidyfeng on 2019/6/10.
|
||||
// Copyright © 2019 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUILinkCellData.h"
|
||||
|
||||
@implementation TUILinkCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
TUILinkCellData *cellData = [[TUILinkCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
|
||||
cellData.msgID = message.msgID;
|
||||
cellData.text = param[@"text"];
|
||||
cellData.link = param[@"link"];
|
||||
cellData.avatarUrl = [NSURL URLWithString:message.faceURL];
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
return param[@"text"];
|
||||
}
|
||||
|
||||
- (CGSize)contentSize {
|
||||
CGFloat textMaxWidth = 245.f;
|
||||
CGRect rect = [self.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
|
||||
17
TUIKit/TUIChat/BaseCellData/Custom/TUILocalTipsCellData.h
Normal file
17
TUIKit/TUIChat/BaseCellData/Custom/TUILocalTipsCellData.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TUILocalTipsCellData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2025/3/18.
|
||||
// Copyright © 2025 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TUISystemMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUILocalTipsCellData : TUISystemMessageCellData
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
29
TUIKit/TUIChat/BaseCellData/Custom/TUILocalTipsCellData.m
Normal file
29
TUIKit/TUIChat/BaseCellData/Custom/TUILocalTipsCellData.m
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// TUILocalTipsCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2025/3/18.
|
||||
// Copyright © 2025 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUILocalTipsCellData.h"
|
||||
|
||||
@implementation TUILocalTipsCellData
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
TUILocalTipsCellData *cellData = [[TUILocalTipsCellData alloc] initWithDirection:MsgDirectionIncoming];
|
||||
cellData.innerMessage = message;
|
||||
cellData.msgID = message.msgID;
|
||||
cellData.content = param[@"content"];
|
||||
cellData.reuseId = TSystemMessageCell_ReuseId;
|
||||
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
return param[@"content"];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
23
TUIKit/TUIChat/BaseCellData/Custom/TUIOrderCellData.h
Normal file
23
TUIKit/TUIChat/BaseCellData/Custom/TUIOrderCellData.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// TUIOrderCellData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by xia on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIOrderCellData : TUIBubbleMessageCellData
|
||||
|
||||
@property(nonatomic, copy) NSString *title;
|
||||
@property(nonatomic, copy) NSString *desc;
|
||||
@property(nonatomic, copy) NSString *price;
|
||||
@property(nonatomic, copy) NSString *imageUrl;
|
||||
@property(nonatomic, copy) NSString *link;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
37
TUIKit/TUIChat/BaseCellData/Custom/TUIOrderCellData.m
Normal file
37
TUIKit/TUIChat/BaseCellData/Custom/TUIOrderCellData.m
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// TUIOrderCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by xia on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIOrderCellData.h"
|
||||
|
||||
@implementation TUIOrderCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
TUIOrderCellData *cellData = [[TUIOrderCellData alloc] initWithDirection:message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming];
|
||||
cellData.innerMessage = message;
|
||||
cellData.msgID = message.msgID;
|
||||
cellData.title = param[@"title"];
|
||||
cellData.desc = param[@"description"];
|
||||
cellData.imageUrl = param[@"imageUrl"];
|
||||
cellData.link = param[@"link"];
|
||||
cellData.price = param[@"price"];
|
||||
cellData.avatarUrl = [NSURL URLWithString:message.faceURL];
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:nil];
|
||||
return param[@"title"];
|
||||
}
|
||||
|
||||
- (CGSize)contentSize {
|
||||
CGSize size = CGSizeMake(245, 80);
|
||||
return size;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TUIFileReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIVoiceReplyQuoteViewData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIFileReplyQuoteViewData : TUIVoiceReplyQuoteViewData
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// TUIFileReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIFileReplyQuoteViewData.h"
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIFileMessageCellData.h"
|
||||
|
||||
@implementation TUIFileReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUIFileMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIFileReplyQuoteViewData *myData = [[TUIFileReplyQuoteViewData alloc] init];
|
||||
myData.text = [(TUIFileMessageCellData *)originCellData fileName];
|
||||
myData.icon = TUIChatCommonBundleImage(@"msg_file");
|
||||
myData.originCellData = originCellData;
|
||||
return myData;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// TUIImageReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
typedef NS_ENUM(NSUInteger, TUIImageReplyQuoteStatus) {
|
||||
TUIImageReplyQuoteStatusInit,
|
||||
TUIImageReplyQuoteStatusDownloading,
|
||||
TUIImageReplyQuoteStatusSuccess,
|
||||
TUIImageReplyQuoteStatusFailed,
|
||||
};
|
||||
|
||||
@interface TUIImageReplyQuoteViewData : TUIReplyQuoteViewData
|
||||
|
||||
@property(nonatomic, assign) TUIImageReplyQuoteStatus imageStatus;
|
||||
|
||||
@property(nonatomic, strong) UIImage *image;
|
||||
|
||||
@property(nonatomic, assign) CGSize imageSize;
|
||||
|
||||
+ (CGSize)displaySizeWithOriginSize:(CGSize)originSize;
|
||||
- (void)downloadImage;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// TUIImageReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIImageReplyQuoteViewData.h"
|
||||
#import "TUIImageMessageCellData.h"
|
||||
|
||||
@implementation TUIImageReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUIImageMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIImageReplyQuoteViewData *myData = [[TUIImageReplyQuoteViewData alloc] init];
|
||||
V2TIMImage *thumb = nil;
|
||||
for (V2TIMImage *image in originCellData.innerMessage.imageElem.imageList) {
|
||||
if (image.type == V2TIM_IMAGE_TYPE_THUMB) {
|
||||
thumb = image;
|
||||
break;
|
||||
}
|
||||
}
|
||||
myData.imageSize = [TUIImageReplyQuoteViewData displaySizeWithOriginSize:CGSizeMake(thumb ? thumb.width : 60, thumb ? thumb.height : 60)];
|
||||
myData.originCellData = originCellData;
|
||||
myData.imageStatus = TUIImageReplyQuoteStatusInit;
|
||||
return myData;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth {
|
||||
return self.imageSize;
|
||||
}
|
||||
|
||||
+ (CGSize)displaySizeWithOriginSize:(CGSize)originSize {
|
||||
if (originSize.width == 0 || originSize.width == 0) {
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
CGFloat max = 60;
|
||||
CGFloat w = 0, h = 0;
|
||||
if (originSize.width > originSize.height) {
|
||||
w = max;
|
||||
h = max * originSize.height / originSize.width;
|
||||
} else {
|
||||
w = max * originSize.width / originSize.height;
|
||||
h = max;
|
||||
}
|
||||
return CGSizeMake(w, h);
|
||||
}
|
||||
|
||||
- (void)downloadImage {
|
||||
@weakify(self);
|
||||
self.imageStatus = TUIImageReplyQuoteStatusDownloading;
|
||||
if ([self.originCellData isKindOfClass:TUIImageMessageCellData.class]) {
|
||||
TUIImageMessageCellData *imageData = (TUIImageMessageCellData *)self.originCellData;
|
||||
[imageData downloadImage:TImage_Type_Thumb
|
||||
finish:^{
|
||||
@strongify(self);
|
||||
self.image = imageData.thumbImage;
|
||||
self.imageStatus = TUIImageReplyQuoteStatusSuccess;
|
||||
if (self.onFinish) {
|
||||
self.onFinish();
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// TUIMergeReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMergeReplyQuoteViewData : TUIReplyQuoteViewData
|
||||
|
||||
@property(nonatomic, copy) NSString *title;
|
||||
@property(nonatomic, copy) NSString *abstract;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// TUIMergeReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMergeReplyQuoteViewData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import "TUIMergeMessageCellData.h"
|
||||
|
||||
@implementation TUIMergeReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUIMergeMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIMergeReplyQuoteViewData *myData = [[TUIMergeReplyQuoteViewData alloc] init];
|
||||
myData.title = [(TUIMergeMessageCellData *)originCellData title];
|
||||
NSAttributedString *abstract = [(TUIMergeMessageCellData *)originCellData abstractAttributedString];
|
||||
myData.abstract = abstract.string;
|
||||
myData.originCellData = originCellData;
|
||||
return myData;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth {
|
||||
CGFloat singleHeight = [UIFont systemFontOfSize:10.0].lineHeight;
|
||||
NSAttributedString *titleAttributeString = [self.title getFormatEmojiStringWithFont:[UIFont systemFontOfSize:10.0] emojiLocations:nil];
|
||||
CGRect titleRect = [titleAttributeString boundingRectWithSize:CGSizeMake(maxWidth, singleHeight)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
context:nil];
|
||||
CGFloat width = titleRect.size.width;
|
||||
CGFloat height = titleRect.size.height;
|
||||
return CGSizeMake(MIN(width, maxWidth), height);
|
||||
}
|
||||
|
||||
@end
|
||||
107
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyMessageCellData.h
Normal file
107
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyMessageCellData.h
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// TUIReplyMessageCellData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <ImSDK_Plus/ImSDK_Plus.h>
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import "TUIChatDefine.h"
|
||||
#import "TUIReplyQuoteViewData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TUIReplyMessageCellData;
|
||||
|
||||
@interface TUIReplyMessageCellData : TUIBubbleMessageCellData
|
||||
|
||||
/**
|
||||
* The original message ID
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *__nullable originMsgID;
|
||||
|
||||
/**
|
||||
* The default abstract of original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *__nullable msgAbstract;
|
||||
|
||||
/**
|
||||
* The sender of original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *__nullable sender;
|
||||
|
||||
/**
|
||||
* The sender of original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *__nullable faceURL;
|
||||
|
||||
/**
|
||||
* The type of original message
|
||||
*/
|
||||
@property(nonatomic, assign) V2TIMElemType originMsgType;
|
||||
|
||||
/**
|
||||
*
|
||||
* Original message
|
||||
*/
|
||||
@property(nonatomic, strong) V2TIMMessage *__nullable originMessage;
|
||||
@property(nonatomic, strong) TUIMessageCellData *originCellData;
|
||||
@property(nonatomic, strong) TUIReplyQuoteViewData *quoteData;
|
||||
@property(nonatomic, assign) BOOL showRevokedOriginMessage;
|
||||
|
||||
/**
|
||||
* The content of replying the original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *content;
|
||||
@property(nonatomic, strong, readonly) NSAttributedString *attributeString;
|
||||
|
||||
/**
|
||||
* The size of quote view, including @senderSize and @quotePlaceholderSize
|
||||
*/
|
||||
@property(nonatomic, assign) CGSize quoteSize;
|
||||
|
||||
/**
|
||||
* The size of label which displays the sender displayname
|
||||
*/
|
||||
@property(nonatomic, assign) CGSize senderSize;
|
||||
|
||||
/**
|
||||
* The size of customize quote view
|
||||
*/
|
||||
@property(nonatomic, assign) CGSize quotePlaceholderSize;
|
||||
|
||||
/**
|
||||
* The size of label which displays the content of replying the original message.
|
||||
*/
|
||||
@property(nonatomic, assign) CGSize replyContentSize;
|
||||
|
||||
@property(nonatomic, copy) TUIReplyAsyncLoadFinish onFinish;
|
||||
|
||||
/**
|
||||
* The message ID of the root message which is replyed at first.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *messageRootID;
|
||||
|
||||
@property(nonatomic) UIColor *textColor;
|
||||
|
||||
@property(nonatomic, strong) NSString *selectContent;
|
||||
@property(nonatomic, strong) NSMutableArray<NSDictionary<NSValue *, NSAttributedString *> *> *emojiLocations;
|
||||
|
||||
// Deprecated
|
||||
// Search `loadOriginMessageFromReplyData` in TUIMessageDataProvider+MessageDeal
|
||||
//- (void)loadOriginMessage:(void(^)(void))callback;
|
||||
|
||||
- (TUIReplyQuoteViewData *)getQuoteData:(TUIMessageCellData *)originCellData;
|
||||
- (CGSize)quotePlaceholderSizeWithType:(V2TIMElemType)type data:(TUIReplyQuoteViewData *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIReferenceMessageCellData : TUIReplyMessageCellData
|
||||
|
||||
@property(nonatomic, assign) CGSize textSize;
|
||||
@property(nonatomic, assign) CGPoint textOrigin;
|
||||
|
||||
@end
|
||||
NS_ASSUME_NONNULL_END
|
||||
173
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyMessageCellData.m
Normal file
173
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyMessageCellData.m
Normal file
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// TUIReplyMessageCellData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/11.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
/**
|
||||
The protocol format of the custom field cloudMessageData of the message
|
||||
|
||||
{
|
||||
"messageReply":{
|
||||
"messageID": "xxxx0xxx=xx",
|
||||
"messageAbstract":"origin message abstract..."
|
||||
"messageSender":"NickName/99618",
|
||||
"messageType": "1/2/..",
|
||||
"version":"1",
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import "TUIFileMessageCellData.h"
|
||||
#import "TUIImageMessageCellData.h"
|
||||
#import "TUIMergeMessageCellData.h"
|
||||
#import "TUITextMessageCellData.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
#import "TUIVoiceMessageCellData.h"
|
||||
|
||||
#import "TUICloudCustomDataTypeCenter.h"
|
||||
#import "TUIFileReplyQuoteViewData.h"
|
||||
#import "TUIImageReplyQuoteViewData.h"
|
||||
#import "TUIMergeReplyQuoteViewData.h"
|
||||
#import "TUIReplyPreviewData.h"
|
||||
#import "TUITextReplyQuoteViewData.h"
|
||||
#import "TUIVideoReplyQuoteViewData.h"
|
||||
#import "TUIVoiceReplyQuoteViewData.h"
|
||||
|
||||
@implementation TUIReplyMessageCellData
|
||||
{
|
||||
NSString *_sender;
|
||||
}
|
||||
|
||||
- (void)setSender:(NSString *)sender {
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
- (NSString *__nullable)sender {
|
||||
if (self.originMessage) {
|
||||
return self.originMessage.nameCard ? : (self.originMessage.friendRemark ? : (self.originMessage.nickName ? : self.originMessage.sender));
|
||||
}
|
||||
return _sender;
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
if (message.cloudCustomData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block TUIReplyMessageCellData *replyData = nil;
|
||||
[message doThingsInContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReply
|
||||
callback:^(BOOL isContains, id obj) {
|
||||
if (isContains) {
|
||||
if (obj && [obj isKindOfClass:NSDictionary.class]) {
|
||||
NSDictionary *reply = (NSDictionary *)obj;
|
||||
// This message is a "reply message"
|
||||
replyData = [[TUIReplyMessageCellData alloc]
|
||||
initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
replyData.reuseId = TReplyMessageCell_ReuseId;
|
||||
replyData.originMsgID = reply[@"messageID"];
|
||||
replyData.msgAbstract = reply[@"messageAbstract"];
|
||||
replyData.sender = reply[@"messageSender"];
|
||||
replyData.originMsgType = (V2TIMElemType)[reply[@"messageType"] integerValue];
|
||||
replyData.content = message.textElem.text;
|
||||
replyData.messageRootID = reply[@"messageRootID"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return replyData;
|
||||
}
|
||||
|
||||
- (instancetype)initWithDirection:(TMsgDirection)direction {
|
||||
self = [super initWithDirection:direction];
|
||||
if (self) {
|
||||
if (direction == MsgDirectionIncoming) {
|
||||
self.cellLayout = [TUIMessageCellLayout incommingTextMessageLayout];
|
||||
} else {
|
||||
self.cellLayout = [TUIMessageCellLayout outgoingTextMessageLayout];
|
||||
}
|
||||
_emojiLocations = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)quotePlaceholderSizeWithType:(V2TIMElemType)type data:(TUIReplyQuoteViewData *)data {
|
||||
if (data == nil) {
|
||||
return CGSizeMake(20, 20);
|
||||
}
|
||||
|
||||
return [data contentSize:TReplyQuoteView_Max_Width - 12];
|
||||
}
|
||||
|
||||
- (TUIReplyQuoteViewData *)getQuoteData:(TUIMessageCellData *)originCellData {
|
||||
TUIReplyQuoteViewData *quoteData = nil;
|
||||
Class class = [originCellData getReplyQuoteViewDataClass];
|
||||
BOOL hasRiskContent = originCellData.innerMessage.hasRiskContent;
|
||||
if (hasRiskContent && [TIMConfig isClassicEntrance]){
|
||||
// Return text reply data in default
|
||||
TUITextReplyQuoteViewData *myData = [[TUITextReplyQuoteViewData alloc] init];
|
||||
myData.text = [TUIReplyPreviewData displayAbstract:self.originMsgType abstract:self.msgAbstract withFileName:NO isRisk:hasRiskContent];
|
||||
quoteData = myData;
|
||||
}
|
||||
else if (class && [class respondsToSelector:@selector(getReplyQuoteViewData:)]) {
|
||||
quoteData = [class getReplyQuoteViewData:originCellData];
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
if (quoteData == nil) {
|
||||
//
|
||||
// Return text reply data in default
|
||||
TUITextReplyQuoteViewData *myData = [[TUITextReplyQuoteViewData alloc] init];
|
||||
myData.text = [TUIReplyPreviewData displayAbstract:self.originMsgType abstract:self.msgAbstract withFileName:NO isRisk:hasRiskContent];
|
||||
quoteData = myData;
|
||||
}
|
||||
|
||||
quoteData.originCellData = originCellData;
|
||||
@weakify(self);
|
||||
quoteData.onFinish = ^{
|
||||
@strongify(self);
|
||||
if (self.onFinish) {
|
||||
self.onFinish();
|
||||
}
|
||||
};
|
||||
return quoteData;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIReferenceMessageCellData
|
||||
|
||||
+ (TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
if (message.cloudCustomData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block TUIReplyMessageCellData *replyData = nil;
|
||||
[message doThingsInContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReference
|
||||
callback:^(BOOL isContains, id obj) {
|
||||
if (isContains) {
|
||||
if (obj && [obj isKindOfClass:NSDictionary.class]) {
|
||||
NSDictionary *reply = (NSDictionary *)obj;
|
||||
if ([reply isKindOfClass:NSDictionary.class]) {
|
||||
// This message is 「quote message」which indicating the original message
|
||||
replyData = [[TUIReferenceMessageCellData alloc]
|
||||
initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
replyData.reuseId = TUIReferenceMessageCell_ReuseId;
|
||||
replyData.originMsgID = reply[@"messageID"];
|
||||
replyData.msgAbstract = reply[@"messageAbstract"];
|
||||
replyData.sender = reply[@"messageSender"];
|
||||
replyData.originMsgType = (V2TIMElemType)[reply[@"messageType"] integerValue];
|
||||
replyData.content = message.textElem.text; // text only
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
return replyData;
|
||||
}
|
||||
|
||||
@end
|
||||
55
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyPreviewData.h
Normal file
55
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyPreviewData.h
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// TUIReplyPreviewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/3/22.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TUIChatDefine.h"
|
||||
|
||||
@class V2TIMMessage;
|
||||
|
||||
@interface TUIReplyPreviewData : NSObject
|
||||
|
||||
/**
|
||||
* The message ID of the replyed original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *msgID;
|
||||
|
||||
/**
|
||||
* The abstract of the replyed original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *msgAbstract;
|
||||
|
||||
/**
|
||||
* The sender's displayname of the replyed original message. Nickname is prior than userID.
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *sender;
|
||||
|
||||
/**
|
||||
* The faceURL of the replyed original message
|
||||
*/
|
||||
@property(nonatomic, copy) NSString *faceURL;
|
||||
|
||||
/**
|
||||
* The message type of the replyed original message. For details, see the enumeration value of V2TIMElemType.
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger type;
|
||||
|
||||
/**
|
||||
* The replyed original message
|
||||
*/
|
||||
@property(nonatomic, strong) V2TIMMessage *originMessage;
|
||||
|
||||
// Message reply root RootID (not necessarily the msgID of the originMessage above, but the ID of the message at the top)
|
||||
@property(nonatomic, copy) NSString *messageRootID;
|
||||
|
||||
+ (NSString *)displayAbstract:(NSInteger)type abstract:(NSString *)abstract withFileName:(BOOL)withFilename isRisk:(BOOL)isRisk;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIReferencePreviewData : TUIReplyPreviewData
|
||||
|
||||
@end
|
||||
39
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyPreviewData.m
Normal file
39
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyPreviewData.m
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// TUIReplyPreviewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/3/22.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIReplyPreviewData.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@implementation TUIReplyPreviewData
|
||||
|
||||
+ (NSString *)displayAbstract:(NSInteger)type abstract:(NSString *)abstract withFileName:(BOOL)withFilename isRisk:(BOOL)isRisk {
|
||||
NSString *text = abstract;
|
||||
if (type == V2TIM_ELEM_TYPE_IMAGE) {
|
||||
text = isRisk? TIMCommonLocalizableString(TUIkitMessageTypeRiskImage):TIMCommonLocalizableString(TUIkitMessageTypeImage);
|
||||
} else if (type == V2TIM_ELEM_TYPE_VIDEO) {
|
||||
text = isRisk? TIMCommonLocalizableString(TUIkitMessageTypeRiskVideo):TIMCommonLocalizableString(TUIkitMessageTypeVideo);
|
||||
} else if (type == V2TIM_ELEM_TYPE_SOUND) {
|
||||
text = isRisk? TIMCommonLocalizableString(TUIkitMessageTypeRiskVoice):TIMCommonLocalizableString(TUIKitMessageTypeVoice);
|
||||
} else if (type == V2TIM_ELEM_TYPE_FACE) {
|
||||
text = TIMCommonLocalizableString(TUIKitMessageTypeAnimateEmoji);
|
||||
} else if (type == V2TIM_ELEM_TYPE_FILE) {
|
||||
if (withFilename) {
|
||||
text = [NSString stringWithFormat:@"%@%@", TIMCommonLocalizableString(TUIkitMessageTypeFile), abstract];
|
||||
;
|
||||
} else {
|
||||
text = TIMCommonLocalizableString(TUIkitMessageTypeFile);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIReferencePreviewData
|
||||
|
||||
@end
|
||||
37
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyQuoteViewData.h
Normal file
37
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyQuoteViewData.h
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// TUIReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIChatDefine.h"
|
||||
|
||||
@class TUIMessageCellData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIReplyQuoteViewData : NSObject
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData;
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth;
|
||||
|
||||
/**
|
||||
* If you want to download the custom reply content asynchronously, you need to call the callback after the download is complete, and the TUI will be
|
||||
* automatically refreshed.
|
||||
*/
|
||||
@property(nonatomic, copy) TUIReplyQuoteAsyncLoadFinish onFinish;
|
||||
|
||||
@property(nonatomic, strong) TUIMessageCellData *originCellData;
|
||||
|
||||
@property(nonatomic, assign) BOOL supportForReply;
|
||||
|
||||
@property(nonatomic, assign) BOOL showRevokedOriginMessage;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
21
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyQuoteViewData.m
Normal file
21
TUIKit/TUIChat/BaseCellData/Reply/TUIReplyQuoteViewData.m
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// TUIReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIReplyQuoteViewData.h"
|
||||
|
||||
@implementation TUIReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth {
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// TUITextReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUITextReplyQuoteViewData : TUIReplyQuoteViewData
|
||||
|
||||
@property(nonatomic, copy) NSString *text;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// TUITextReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITextReplyQuoteViewData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import "TUITextMessageCellData.h"
|
||||
|
||||
@implementation TUITextReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUITextMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUITextReplyQuoteViewData *myData = [[TUITextReplyQuoteViewData alloc] init];
|
||||
myData.text = [(TUITextMessageCellData *)originCellData content];
|
||||
myData.originCellData = originCellData;
|
||||
return myData;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth {
|
||||
NSAttributedString *attributeString = nil;
|
||||
BOOL showRevokeStr = (self.originCellData.innerMessage.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) &&
|
||||
!self.showRevokedOriginMessage;
|
||||
if (showRevokeStr) {
|
||||
NSString * revokeStr = self.supportForReply?
|
||||
TIMCommonLocalizableString(TUIKitRepliesOriginMessageRevoke):
|
||||
TIMCommonLocalizableString(TUIKitReferenceOriginMessageRevoke);
|
||||
attributeString = [revokeStr getFormatEmojiStringWithFont:[UIFont systemFontOfSize:10.0] emojiLocations:nil];
|
||||
} else {
|
||||
attributeString = [self.text getFormatEmojiStringWithFont:[UIFont systemFontOfSize:10.0] emojiLocations:nil];
|
||||
}
|
||||
|
||||
CGSize size = [@"0" sizeWithAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:10.0]}];
|
||||
CGRect rect = [attributeString boundingRectWithSize:CGSizeMake(maxWidth, size.height * 2)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
context:nil];
|
||||
CGFloat h = rect.size.height < size.height * 2 ? rect.size.height : size.height * 2;
|
||||
if (showRevokeStr && self.supportForReply) {
|
||||
h = size.height *2;
|
||||
}
|
||||
return CGSizeMake(rect.size.width, h);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TUIVideoReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIImageReplyQuoteViewData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIVideoReplyQuoteViewData : TUIImageReplyQuoteViewData
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// TUIVideoReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIVideoReplyQuoteViewData.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
|
||||
@implementation TUIVideoReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUIVideoMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIVideoReplyQuoteViewData *myData = [[TUIVideoReplyQuoteViewData alloc] init];
|
||||
CGSize snapSize = CGSizeMake(originCellData.innerMessage.videoElem ? originCellData.innerMessage.videoElem.snapshotWidth : 0,
|
||||
originCellData.innerMessage.videoElem ? originCellData.innerMessage.videoElem.snapshotHeight : 0);
|
||||
myData.imageSize = [TUIVideoReplyQuoteViewData displaySizeWithOriginSize:snapSize];
|
||||
myData.originCellData = originCellData;
|
||||
return myData;
|
||||
}
|
||||
|
||||
- (void)downloadImage {
|
||||
[super downloadImage];
|
||||
|
||||
@weakify(self);
|
||||
if ([self.originCellData isKindOfClass:TUIVideoMessageCellData.class]) {
|
||||
TUIVideoMessageCellData *videoData = (TUIVideoMessageCellData *)self.originCellData;
|
||||
[videoData downloadThumb:^{
|
||||
@strongify(self);
|
||||
self.image = videoData.thumbImage;
|
||||
if (self.onFinish) {
|
||||
self.onFinish();
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// TUIVoiceReplyQuoteViewData.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUITextReplyQuoteViewData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIVoiceReplyQuoteViewData : TUITextReplyQuoteViewData
|
||||
|
||||
@property(nonatomic, strong) UIImage *icon;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// TUIVoiceReplyQuoteViewData.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2021/11/25.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIVoiceReplyQuoteViewData.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIVoiceMessageCellData.h"
|
||||
@implementation TUIVoiceReplyQuoteViewData
|
||||
|
||||
+ (instancetype)getReplyQuoteViewData:(TUIMessageCellData *)originCellData {
|
||||
if (originCellData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![originCellData isKindOfClass:TUIVoiceMessageCellData.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUIVoiceReplyQuoteViewData *myData = [[TUIVoiceReplyQuoteViewData alloc] init];
|
||||
myData.text = [NSString stringWithFormat:@"%d\"", [(TUIVoiceMessageCellData *)originCellData duration]];
|
||||
myData.icon = TUIChatCommonBundleImage(@"voice_reply");
|
||||
myData.originCellData = originCellData;
|
||||
return myData;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize:(CGFloat)maxWidth {
|
||||
CGFloat marginWidth = 18;
|
||||
CGSize size = [@"0" sizeWithAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:10.0]}];
|
||||
CGRect rect = [self.text boundingRectWithSize:CGSizeMake(maxWidth - marginWidth, size.height)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
|
||||
attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:10.0]}
|
||||
context:nil];
|
||||
return CGSizeMake(rect.size.width + marginWidth, size.height);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import "TUIChatConversationModel.h"
|
||||
|
||||
@class TUIChatBaseDataProvider;
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol TUIChatBaseDataProviderDelegate <NSObject>
|
||||
|
||||
@required
|
||||
- (NSString *)dataProvider:(TUIChatBaseDataProvider *)dataProvider mergeForwardTitleWithMyName:(NSString *)name;
|
||||
- (NSString *)dataProvider:(TUIChatBaseDataProvider *)dataProvider mergeForwardMsgAbstactForMessage:(V2TIMMessage *)message;
|
||||
|
||||
- (void)dataProvider:(TUIChatBaseDataProvider *)dataProvider sendMessage:(V2TIMMessage *)message;
|
||||
- (void)onSelectPhotoMoreCellData;
|
||||
- (void)onTakePictureMoreCellData;
|
||||
- (void)onTakeVideoMoreCellData;
|
||||
- (void)onMultimediaRecordMoreCellData;
|
||||
- (void)onSelectFileMoreCellData;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIChatBaseDataProvider : NSObject
|
||||
|
||||
@property(nonatomic, weak) id<TUIChatBaseDataProviderDelegate> delegate;
|
||||
|
||||
- (void)getForwardMessageWithCellDatas:(NSArray<TUIMessageCellData *> *)uiMsgs
|
||||
toTargets:(NSArray<TUIChatConversationModel *> *)targets
|
||||
Merge:(BOOL)merge
|
||||
ResultBlock:(void (^)(TUIChatConversationModel *targetConversation, NSArray<V2TIMMessage *> *msgs))resultBlock
|
||||
fail:(nullable V2TIMFail)fail;
|
||||
|
||||
- (NSString *)abstractDisplayWithMessage:(V2TIMMessage *)msg;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - TUIChatBaseDataProvider (IMSDK)
|
||||
@interface TUIChatBaseDataProvider (IMSDK)
|
||||
|
||||
+ (void)getTotalUnreadMessageCountWithSuccBlock:(void (^)(UInt64 totalCount))succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)saveDraftWithConversationID:(NSString *)conversationId Text:(NSString *)text;
|
||||
|
||||
+ (void)findMessages:(NSArray *)msgIDs callback:(void (^)(BOOL succ, NSString *error_message, NSArray *msgs))callback;
|
||||
|
||||
#pragma mark - C2C
|
||||
+ (void)getFriendInfoWithUserId:(nullable NSString *)userID
|
||||
SuccBlock:(void (^)(V2TIMFriendInfoResult *friendInfoResult))succ
|
||||
failBlock:(nullable V2TIMFail)fail;
|
||||
+ (void)getUserInfoWithUserId:(NSString *)userID SuccBlock:(void (^)(V2TIMUserFullInfo *userInfo))succ failBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
#pragma mark - Group
|
||||
+ (void)getGroupInfoWithGroupID:(NSString *)groupID SuccBlock:(void (^)(V2TIMGroupInfoResult *groupResult))succ failBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)insertLocalTipsMessage:(NSString *)content chatID:(NSString *)chatID isGroup:(BOOL)isGroup;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
258
TUIKit/TUIChat/BaseDataProvider/Base/TUIChatBaseDataProvider.m
Normal file
258
TUIKit/TUIChat/BaseDataProvider/Base/TUIChatBaseDataProvider.m
Normal file
@@ -0,0 +1,258 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
@import ImSDK_Plus;
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import <TUICore/NSDictionary+TUISafe.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIChatBaseDataProvider.h"
|
||||
#import "TUIMessageBaseDataProvider.h"
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUIChatDefine.h"
|
||||
|
||||
#define Input_SendBtn_Key @"Input_SendBtn_Key"
|
||||
#define Input_SendBtn_Title @"Input_SendBtn_Title"
|
||||
#define Input_SendBtn_ImageName @"Input_SendBtn_ImageName"
|
||||
|
||||
static NSArray *gCustomInputBtnInfo = nil;
|
||||
|
||||
@implementation TUIChatBaseDataProvider
|
||||
+ (void)initialize {
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onChangeLanguage) name:TUIChangeLanguageNotification object:nil];
|
||||
}
|
||||
|
||||
+ (void)onChangeLanguage {
|
||||
gCustomInputBtnInfo = nil;
|
||||
}
|
||||
|
||||
+ (NSArray *)customInputBtnInfo {
|
||||
if (gCustomInputBtnInfo == nil) {
|
||||
gCustomInputBtnInfo = @[ @{
|
||||
Input_SendBtn_Key : TUIInputMoreCellKey_Link,
|
||||
Input_SendBtn_Title : TIMCommonLocalizableString(TUIKitMoreLink),
|
||||
Input_SendBtn_ImageName : @"chat_more_link_img"
|
||||
} ];
|
||||
}
|
||||
return gCustomInputBtnInfo;
|
||||
}
|
||||
|
||||
- (void)getForwardMessageWithCellDatas:(NSArray<TUIMessageCellData *> *)uiMsgs
|
||||
toTargets:(NSArray<TUIChatConversationModel *> *)targets
|
||||
Merge:(BOOL)merge
|
||||
ResultBlock:(void (^)(TUIChatConversationModel *targetConversation, NSArray<V2TIMMessage *> *msgs))resultBlock
|
||||
fail:(nullable V2TIMFail)fail {
|
||||
if (uiMsgs.count == 0) {
|
||||
if (fail) {
|
||||
fail(ERR_SVR_PROFILE_INVALID_PARAMETERS, @"uiMsgs is empty");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_apply(targets.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
|
||||
TUIChatConversationModel *convCellData = targets[index];
|
||||
|
||||
NSMutableArray *tmpMsgs = [NSMutableArray array];
|
||||
for (TUIMessageCellData *uiMsg in uiMsgs) {
|
||||
V2TIMMessage *msg = uiMsg.innerMessage;
|
||||
if (msg) {
|
||||
[tmpMsgs addObject:msg];
|
||||
}
|
||||
}
|
||||
NSArray *msgs = [NSArray arrayWithArray:tmpMsgs];
|
||||
msgs = [msgs sortedArrayUsingComparator:^NSComparisonResult(V2TIMMessage *obj1, V2TIMMessage *obj2) {
|
||||
if ([obj1.timestamp timeIntervalSince1970] == [obj2.timestamp timeIntervalSince1970]) {
|
||||
return obj1.seq > obj2.seq;
|
||||
} else {
|
||||
return [obj1.timestamp compare:obj2.timestamp];
|
||||
}
|
||||
}];
|
||||
|
||||
if (!merge) {
|
||||
NSMutableArray *forwardMsgs = [NSMutableArray array];
|
||||
for (V2TIMMessage *msg in msgs) {
|
||||
V2TIMMessage *forwardMessage = [V2TIMManager.sharedInstance createForwardMessage:msg];
|
||||
if (forwardMessage) {
|
||||
forwardMessage.isExcludedFromUnreadCount = [TUIConfig defaultConfig].isExcludedFromUnreadCount;
|
||||
forwardMessage.isExcludedFromLastMessage = [TUIConfig defaultConfig].isExcludedFromLastMessage;
|
||||
[forwardMsgs addObject:forwardMessage];
|
||||
}
|
||||
}
|
||||
if (resultBlock) {
|
||||
resultBlock(convCellData, forwardMsgs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@weakify(self);
|
||||
NSString *loginUserId = [V2TIMManager.sharedInstance getLoginUser];
|
||||
[V2TIMManager.sharedInstance getUsersInfo:@[ loginUserId ]
|
||||
succ:^(NSArray<V2TIMUserFullInfo *> *infoList) {
|
||||
@strongify(self);
|
||||
|
||||
NSString *myName = loginUserId;
|
||||
if (infoList.firstObject.nickName.length > 0) {
|
||||
myName = infoList.firstObject.nickName;
|
||||
}
|
||||
|
||||
NSString *title = [self.delegate dataProvider:self mergeForwardTitleWithMyName:myName];
|
||||
NSMutableArray *abstactList = [NSMutableArray array];
|
||||
if (uiMsgs.count > 0) {
|
||||
[abstactList addObject:[self abstractDisplayWithMessage:msgs[0]]];
|
||||
}
|
||||
if (uiMsgs.count > 1) {
|
||||
[abstactList addObject:[self abstractDisplayWithMessage:msgs[1]]];
|
||||
}
|
||||
if (uiMsgs.count > 2) {
|
||||
[abstactList addObject:[self abstractDisplayWithMessage:msgs[2]]];
|
||||
}
|
||||
NSString *compatibleText = TIMCommonLocalizableString(TUIKitRelayCompatibleText);
|
||||
V2TIMMessage *mergeMessage = [V2TIMManager.sharedInstance createMergerMessage:msgs
|
||||
title:title
|
||||
abstractList:abstactList
|
||||
compatibleText:compatibleText];
|
||||
if (mergeMessage == nil) {
|
||||
if (fail) {
|
||||
fail(ERR_NO_SUCC_RESULT, @"failed to merge-forward");
|
||||
}
|
||||
return;
|
||||
}
|
||||
mergeMessage.isExcludedFromUnreadCount = [TUIConfig defaultConfig].isExcludedFromUnreadCount;
|
||||
mergeMessage.isExcludedFromLastMessage = [TUIConfig defaultConfig].isExcludedFromLastMessage;
|
||||
if (resultBlock) {
|
||||
resultBlock(convCellData, @[ mergeMessage ]);
|
||||
}
|
||||
}
|
||||
fail:fail];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSString *)abstractDisplayWithMessage:(V2TIMMessage *)msg {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - TUIChatBaseDataProvider (IMSDK)
|
||||
@implementation TUIChatBaseDataProvider (IMSDK)
|
||||
|
||||
+ (void)getTotalUnreadMessageCountWithSuccBlock:(void (^)(UInt64 totalCount))succ fail:(nullable V2TIMFail)fail {
|
||||
[V2TIMManager.sharedInstance getTotalUnreadMessageCount:succ fail:fail];
|
||||
}
|
||||
|
||||
+ (void)saveDraftWithConversationID:(NSString *)conversationId Text:(NSString *)text {
|
||||
NSString *draft = text;
|
||||
draft = [draft stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
[[V2TIMManager sharedInstance] setConversationDraft:conversationId draftText:draft succ:nil fail:nil];
|
||||
}
|
||||
|
||||
#pragma mark - C2C
|
||||
+ (void)getFriendInfoWithUserId:(nullable NSString *)userID
|
||||
SuccBlock:(void (^)(V2TIMFriendInfoResult *friendInfoResult))succ
|
||||
failBlock:(nullable V2TIMFail)fail {
|
||||
NSParameterAssert(userID);
|
||||
if (fail && !userID) {
|
||||
fail(ERR_INVALID_PARAMETERS, @"userID is nil");
|
||||
return;
|
||||
}
|
||||
|
||||
[[V2TIMManager sharedInstance] getFriendsInfo:@[ userID ]
|
||||
succ:^(NSArray<V2TIMFriendInfoResult *> *resultList) {
|
||||
V2TIMFriendInfoResult *result = resultList.firstObject;
|
||||
succ(result);
|
||||
}
|
||||
fail:fail];
|
||||
}
|
||||
|
||||
+ (void)getUserInfoWithUserId:(NSString *)userID SuccBlock:(void (^)(V2TIMUserFullInfo *userInfo))succ failBlock:(nullable V2TIMFail)fail {
|
||||
NSParameterAssert(userID);
|
||||
if (!userID) {
|
||||
if (fail) {
|
||||
fail(ERR_INVALID_PARAMETERS, @"userID is nil");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[[V2TIMManager sharedInstance] getUsersInfo:@[ userID ]
|
||||
succ:^(NSArray<V2TIMUserFullInfo *> *infoList) {
|
||||
V2TIMUserFullInfo *info = infoList.firstObject;
|
||||
if (succ) {
|
||||
succ(info);
|
||||
}
|
||||
}
|
||||
fail:fail];
|
||||
}
|
||||
|
||||
#pragma mark - Group
|
||||
+ (void)getGroupInfoWithGroupID:(NSString *)groupID SuccBlock:(void (^)(V2TIMGroupInfoResult *groupResult))succ failBlock:(nullable V2TIMFail)fail {
|
||||
NSParameterAssert(groupID);
|
||||
if (fail && !groupID) {
|
||||
fail(ERR_INVALID_PARAMETERS, @"groupID is nil");
|
||||
return;
|
||||
}
|
||||
|
||||
[[V2TIMManager sharedInstance] getGroupsInfo:@[ groupID ]
|
||||
succ:^(NSArray<V2TIMGroupInfoResult *> *groupResultList) {
|
||||
V2TIMGroupInfoResult *result = groupResultList.firstObject;
|
||||
if (result && result.resultCode == 0) {
|
||||
if (succ) {
|
||||
succ(result);
|
||||
}
|
||||
} else {
|
||||
if (fail) {
|
||||
fail(result.resultCode, result.resultMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
fail:fail];
|
||||
}
|
||||
|
||||
+ (void)findMessages:(NSArray *)msgIDs callback:(void (^)(BOOL succ, NSString *error_message, NSArray *msgs))callback {
|
||||
[V2TIMManager.sharedInstance findMessages:msgIDs
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
if (callback) {
|
||||
callback(YES, nil, msgs);
|
||||
}
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
if (callback) {
|
||||
callback(NO, desc, @[]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)insertLocalTipsMessage:(NSString *)content chatID:(NSString *)chatID isGroup:(BOOL)isGroup {
|
||||
NSDictionary *dic = @{
|
||||
@"version" : @(1),
|
||||
BussinessID : @"local_tips",
|
||||
@"content" : content.length>0?content:@""
|
||||
};
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
|
||||
V2TIMMessage *msg = [[V2TIMManager sharedInstance] createCustomMessage:data];
|
||||
if (msg == nil) {
|
||||
return;
|
||||
}
|
||||
NSString *messageID = nil;
|
||||
NSString *senderID = [TUILogin getUserID];
|
||||
if (isGroup) {
|
||||
NSString *groupID = chatID.length>0?chatID:@"";
|
||||
messageID = [V2TIMManager.sharedInstance insertGroupMessageToLocalStorage:msg to:groupID sender:senderID succ:^{
|
||||
NSDictionary *userInfo = @{@"message" : msg,@"needScrollToBottom":@"1"};
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:TUIChatInsertMessageWithoutUpdateUINotification object:nil userInfo:userInfo];
|
||||
} fail:^(int code, NSString *desc) {
|
||||
|
||||
}];
|
||||
}
|
||||
else {
|
||||
NSString *userID = chatID.length>0?chatID:@"";
|
||||
messageID = [V2TIMManager.sharedInstance insertC2CMessageToLocalStorage:msg to:userID sender:senderID succ:^{
|
||||
NSDictionary *userInfo = @{@"message" : msg,@"needScrollToBottom":@"1"};
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:TUIChatInsertMessageWithoutUpdateUINotification object:nil userInfo:userInfo];
|
||||
} fail:^(int code, NSString *desc) {
|
||||
|
||||
}];
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// TUIChatCallingDataProvider.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2022/12/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <ImSDK_Plus/ImSDK_Plus.h>
|
||||
|
||||
@class TUIMessageCellData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* The protocol type of calls
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUICallProtocolType) {
|
||||
TUICallProtocolTypeUnknown = 0,
|
||||
TUICallProtocolTypeSend = 1,
|
||||
TUICallProtocolTypeAccept = 2,
|
||||
TUICallProtocolTypeReject = 3,
|
||||
TUICallProtocolTypeCancel = 4,
|
||||
TUICallProtocolTypeHangup = 5,
|
||||
TUICallProtocolTypeTimeout = 6,
|
||||
TUICallProtocolTypeLineBusy = 7,
|
||||
TUICallProtocolTypeSwitchToAudio = 8,
|
||||
TUICallProtocolTypeSwitchToAudioConfirm = 9,
|
||||
};
|
||||
|
||||
/**
|
||||
* The stream media type of calls
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUICallStreamMediaType) {
|
||||
TUICallStreamMediaTypeUnknown = 0,
|
||||
TUICallStreamMediaTypeVoice = 1,
|
||||
TUICallStreamMediaTypeVideo = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* The participant style of calls
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUICallParticipantType) {
|
||||
TUICallParticipantTypeUnknown = 0,
|
||||
TUICallParticipantTypeC2C = 1,
|
||||
TUICallParticipantTypeGroup = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* The role of participant
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUICallParticipantRole) {
|
||||
TUICallParticipantRoleUnknown = 0,
|
||||
TUICallParticipantRoleCaller = 1,
|
||||
TUICallParticipantRoleCallee = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* The direction of voice-video-call message
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUICallMessageDirection) {
|
||||
TUICallMessageDirectionIncoming = 0,
|
||||
TUICallMessageDirectionOutgoing = 1,
|
||||
};
|
||||
|
||||
@protocol TUIChatCallingInfoProtocol <NSObject>
|
||||
|
||||
/**
|
||||
* The protocol type of voice-video-call
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) TUICallProtocolType protocolType;
|
||||
|
||||
/**
|
||||
* The stream media type of voice-video-call
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) TUICallStreamMediaType streamMediaType;
|
||||
|
||||
/**
|
||||
* The participate type of voice-video-call, one-to-one and group are supported
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) TUICallParticipantType participantType;
|
||||
|
||||
/**
|
||||
* The participate role type of voice-video-call, caller and callee are supported
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) TUICallParticipantRole participantRole;
|
||||
|
||||
/**
|
||||
* Exclude from history of chat page,supported in TUIChat 7.1 and later
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) BOOL excludeFromHistory;
|
||||
|
||||
/**
|
||||
* The display text of voice-video-call message
|
||||
*/
|
||||
@property(nonatomic, copy, readonly, nonnull) NSString *content;
|
||||
|
||||
/**
|
||||
*
|
||||
* The display direction of voice-video-call message
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) TUICallMessageDirection direction;
|
||||
|
||||
/**
|
||||
*
|
||||
* Whether display unread point in call history
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) BOOL showUnreadPoint;
|
||||
|
||||
/**
|
||||
* Whether to use the receiver's avatar
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) BOOL isUseReceiverAvatar;
|
||||
|
||||
|
||||
@property(nonatomic, strong, readonly) NSArray<NSString *> *participantIDList;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* The style of voice-video-call message in TUIChat
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUIChatCallingMessageAppearance) {
|
||||
TUIChatCallingMessageAppearanceDetails = 0,
|
||||
TUIChatCallingMessageAppearanceSimplify = 1,
|
||||
};
|
||||
|
||||
@protocol TUIChatCallingDataProtocol <NSObject>
|
||||
|
||||
/**
|
||||
* Seting styles of voice-video-call message in TUIChat
|
||||
*/
|
||||
- (void)setCallingMessageStyle:(TUIChatCallingMessageAppearance)style;
|
||||
|
||||
/**
|
||||
* Redial based on the current voice-video-call message (generally used to redial after clicking the call history on the chat page)
|
||||
*/
|
||||
- (void)redialFromMessage:(V2TIMMessage *)innerMessage;
|
||||
|
||||
/**
|
||||
* Parse voice-video-call message
|
||||
*/
|
||||
- (BOOL)isCallingMessage:(V2TIMMessage *)innerMessage callingInfo:(id<TUIChatCallingInfoProtocol> __nullable *__nullable)callingInfo;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIChatCallingDataProvider : NSObject <TUIChatCallingDataProtocol>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,551 @@
|
||||
//
|
||||
// TUIChatCallingDataProvider.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2022/12/21.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatCallingDataProvider.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUIMessageBaseDataProvider.h"
|
||||
|
||||
typedef NSString *TUIChatMessageID;
|
||||
typedef NSDictionary *TUIChatCallingJsonData;
|
||||
|
||||
// ********************************************************************
|
||||
// TUIChatCallingInfo
|
||||
// ********************************************************************
|
||||
|
||||
@interface TUIChatCallingInfo : NSObject <TUIChatCallingInfoProtocol>
|
||||
|
||||
@property(nonatomic, strong) TUIChatMessageID msgID;
|
||||
@property(nonatomic, strong, nullable) TUIChatCallingJsonData jsonData;
|
||||
@property(nonatomic, strong, nullable) V2TIMSignalingInfo *signalingInfo;
|
||||
@property(nonatomic, strong, nullable) V2TIMMessage *innerMessage;
|
||||
@property(nonatomic, assign) TUIChatCallingMessageAppearance style;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatCallingInfo
|
||||
|
||||
#pragma mark - TUIChatCallingInfoProtocol
|
||||
|
||||
- (TUICallProtocolType)protocolType {
|
||||
if (self.jsonData == nil || self.signalingInfo == nil || self.innerMessage == nil) {
|
||||
return TUICallProtocolTypeUnknown;
|
||||
}
|
||||
|
||||
TUICallProtocolType type = TUICallProtocolTypeUnknown;
|
||||
|
||||
switch (self.signalingInfo.actionType) {
|
||||
case SignalingActionType_Invite: {
|
||||
NSDictionary *data = [self.jsonData objectForKey:@"data"];
|
||||
if (data && [data isKindOfClass:NSDictionary.class]) {
|
||||
// New version for calling
|
||||
NSString *cmd = [data objectForKey:@"cmd"];
|
||||
if ([cmd isKindOfClass:NSString.class]) {
|
||||
if ([cmd isEqualToString:@"switchToAudio"]) {
|
||||
type = TUICallProtocolTypeSwitchToAudio;
|
||||
} else if ([cmd isEqualToString:@"hangup"]) {
|
||||
type = TUICallProtocolTypeHangup;
|
||||
} else if ([cmd isEqualToString:@"videoCall"]) {
|
||||
type = TUICallProtocolTypeSend;
|
||||
} else if ([cmd isEqualToString:@"audioCall"]) {
|
||||
type = TUICallProtocolTypeSend;
|
||||
} else {
|
||||
type = TUICallProtocolTypeUnknown;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"calling protocol error, %@", self.jsonData);
|
||||
type = TUICallProtocolTypeUnknown;
|
||||
}
|
||||
} else {
|
||||
// Compatiable
|
||||
NSNumber *callEnd = [self.jsonData objectForKey:@"call_end"];
|
||||
if (callEnd && [callEnd isKindOfClass:NSNumber.class]) {
|
||||
type = TUICallProtocolTypeHangup;
|
||||
} else {
|
||||
type = TUICallProtocolTypeSend;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case SignalingActionType_Cancel_Invite: {
|
||||
type = TUICallProtocolTypeCancel;
|
||||
} break;
|
||||
case SignalingActionType_Accept_Invite: {
|
||||
NSDictionary *data = [self.jsonData objectForKey:@"data"];
|
||||
if (data && [data isKindOfClass:NSDictionary.class]) {
|
||||
// New version for calling
|
||||
NSString *cmd = [data objectForKey:@"cmd"];
|
||||
if ([cmd isKindOfClass:NSString.class]) {
|
||||
if ([cmd isEqualToString:@"switchToAudio"]) {
|
||||
type = TUICallProtocolTypeSwitchToAudioConfirm;
|
||||
} else {
|
||||
type = TUICallProtocolTypeAccept;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"calling protocol error, %@", self.jsonData);
|
||||
type = TUICallProtocolTypeAccept;
|
||||
}
|
||||
} else {
|
||||
// Compatiable
|
||||
type = TUICallProtocolTypeAccept;
|
||||
}
|
||||
} break;
|
||||
case SignalingActionType_Reject_Invite: {
|
||||
if ([self.jsonData objectForKey:@"line_busy"]) {
|
||||
type = TUICallProtocolTypeLineBusy;
|
||||
} else {
|
||||
type = TUICallProtocolTypeReject;
|
||||
}
|
||||
} break;
|
||||
case SignalingActionType_Invite_Timeout: {
|
||||
type = TUICallProtocolTypeTimeout;
|
||||
} break;
|
||||
default:
|
||||
type = TUICallProtocolTypeUnknown;
|
||||
break;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
- (TUICallStreamMediaType)streamMediaType {
|
||||
TUICallProtocolType protocolType = self.protocolType;
|
||||
if (protocolType == TUICallProtocolTypeUnknown) {
|
||||
return TUICallStreamMediaTypeUnknown;
|
||||
}
|
||||
|
||||
// Default type
|
||||
TUICallStreamMediaType type = TUICallStreamMediaTypeUnknown;
|
||||
NSNumber *callType = [self.jsonData objectForKey:@"call_type"];
|
||||
if (callType && [callType isKindOfClass:NSNumber.class]) {
|
||||
if ([callType integerValue] == 1) {
|
||||
type = TUICallStreamMediaTypeVoice;
|
||||
} else if ([callType integerValue] == 2) {
|
||||
type = TUICallStreamMediaTypeVideo;
|
||||
}
|
||||
}
|
||||
|
||||
// Read from special protocol
|
||||
if (protocolType == TUICallProtocolTypeSend) {
|
||||
NSDictionary *data = [self.jsonData objectForKey:@"data"];
|
||||
if (data && [data isKindOfClass:NSDictionary.class]) {
|
||||
NSString *cmd = [data objectForKey:@"cmd"];
|
||||
if ([cmd isEqual:@"audioCall"]) {
|
||||
type = TUICallStreamMediaTypeVoice;
|
||||
} else if ([cmd isEqual:@"videoCall"]) {
|
||||
type = TUICallStreamMediaTypeVideo;
|
||||
}
|
||||
}
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudio || protocolType == TUICallProtocolTypeSwitchToAudioConfirm) {
|
||||
type = TUICallStreamMediaTypeVideo;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
- (TUICallParticipantType)participantType {
|
||||
if (self.protocolType == TUICallProtocolTypeUnknown) {
|
||||
return TUICallParticipantTypeUnknown;
|
||||
}
|
||||
|
||||
if (self.signalingInfo.groupID.length > 0) {
|
||||
return TUICallParticipantTypeGroup;
|
||||
} else {
|
||||
return TUICallParticipantTypeC2C;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)caller {
|
||||
NSString *callerID = nil;
|
||||
NSDictionary *data = [self.jsonData objectForKey:@"data"];
|
||||
if (data && [data isKindOfClass:NSDictionary.class]) {
|
||||
NSString *inviter = [data objectForKey:@"inviter"];
|
||||
if (inviter && [inviter isKindOfClass:NSString.class]) {
|
||||
callerID = inviter;
|
||||
}
|
||||
}
|
||||
if (callerID == nil) {
|
||||
callerID = TUILogin.getUserID;
|
||||
}
|
||||
return callerID;
|
||||
}
|
||||
|
||||
- (TUICallParticipantRole)participantRole {
|
||||
if ([self.caller isEqualToString:TUILogin.getUserID]) {
|
||||
return TUICallParticipantRoleCaller;
|
||||
} else {
|
||||
return TUICallParticipantRoleCallee;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)excludeFromHistory {
|
||||
if (self.style == TUIChatCallingMessageAppearanceSimplify) {
|
||||
return self.protocolType != TUICallProtocolTypeUnknown && self.innerMessage.isExcludedFromLastMessage && self.innerMessage.isExcludedFromUnreadCount;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)content {
|
||||
if (self.style == TUIChatCallingMessageAppearanceSimplify) {
|
||||
return [self contentForSimplifyAppearance];
|
||||
} else {
|
||||
return [self contentForDetailsAppearance];
|
||||
}
|
||||
}
|
||||
|
||||
- (TUICallMessageDirection)direction {
|
||||
if (self.style == TUIChatCallingMessageAppearanceSimplify) {
|
||||
return [self directionForSimplifyAppearance];
|
||||
} else {
|
||||
return [self directionForDetailsAppearance];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)showUnreadPoint {
|
||||
if (self.excludeFromHistory) {
|
||||
return NO;
|
||||
}
|
||||
return (self.innerMessage.localCustomInt == 0) && (self.participantRole == TUICallParticipantRoleCallee) &&
|
||||
(self.participantType == TUICallParticipantTypeC2C) &&
|
||||
(self.protocolType == TUICallProtocolTypeCancel || self.protocolType == TUICallProtocolTypeTimeout ||
|
||||
self.protocolType == TUICallProtocolTypeLineBusy);
|
||||
}
|
||||
|
||||
- (BOOL)isUseReceiverAvatar {
|
||||
if (self.style == TUIChatCallingMessageAppearanceSimplify) {
|
||||
return [self isUseReceiverAvatarForSimplifyAppearance];
|
||||
} else {
|
||||
return [self isUseReceiverAvatarForDetailsAppearance];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)participantIDList {
|
||||
NSMutableArray *arrayM = [NSMutableArray array];
|
||||
if (self.signalingInfo.inviter) {
|
||||
[arrayM addObject:self.signalingInfo.inviter];
|
||||
}
|
||||
if (self.signalingInfo.inviteeList.count > 0) {
|
||||
[arrayM addObjectsFromArray:self.signalingInfo.inviteeList];
|
||||
}
|
||||
return [NSArray arrayWithArray:arrayM];
|
||||
}
|
||||
|
||||
#pragma mark - Details style
|
||||
- (NSString *)contentForDetailsAppearance {
|
||||
TUICallProtocolType protocolType = self.protocolType;
|
||||
BOOL isGroup = (self.participantType == TUICallParticipantTypeGroup);
|
||||
|
||||
if (protocolType == TUICallProtocolTypeUnknown) {
|
||||
return TIMCommonLocalizableString(TUIkitSignalingUnrecognlize);
|
||||
}
|
||||
|
||||
NSString *display = TIMCommonLocalizableString(TUIkitSignalingUnrecognlize);
|
||||
NSString *showName = [TUIMessageBaseDataProvider getShowName:self.innerMessage];
|
||||
|
||||
if (protocolType == TUICallProtocolTypeSend) {
|
||||
// Launch call
|
||||
display = isGroup ? [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitSignalingNewGroupCallFormat), showName]
|
||||
: TIMCommonLocalizableString(TUIKitSignalingNewCall);
|
||||
} else if (protocolType == TUICallProtocolTypeAccept) {
|
||||
// Accept call
|
||||
display = isGroup ? [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitSignalingHangonCallFormat), showName]
|
||||
: TIMCommonLocalizableString(TUIkitSignalingHangonCall);
|
||||
} else if (protocolType == TUICallProtocolTypeReject) {
|
||||
// Reject call
|
||||
display = isGroup ? [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitSignalingDeclineFormat), showName]
|
||||
: TIMCommonLocalizableString(TUIkitSignalingDecline);
|
||||
} else if (protocolType == TUICallProtocolTypeCancel) {
|
||||
// Cancel pending call
|
||||
display = isGroup ? [NSString stringWithFormat:TIMCommonLocalizableString(TUIkitSignalingCancelGroupCallFormat), showName]
|
||||
: TIMCommonLocalizableString(TUIkitSignalingCancelCall);
|
||||
} else if (protocolType == TUICallProtocolTypeHangup) {
|
||||
// Hang up
|
||||
NSUInteger duration = [[self.jsonData objectForKey:@"call_end"] unsignedIntegerValue];
|
||||
display = isGroup
|
||||
? TIMCommonLocalizableString(TUIKitSignalingFinishGroupChat)
|
||||
: [NSString stringWithFormat:@"%@:%.2d:%.2d",TIMCommonLocalizableString(TUIKitSignalingFinishConversationAndTimeFormat),duration / 60, duration % 60];
|
||||
} else if (protocolType == TUICallProtocolTypeTimeout) {
|
||||
// Call timeout
|
||||
NSMutableString *mutableContent = [NSMutableString string];
|
||||
if (isGroup) {
|
||||
for (NSString *invitee in self.signalingInfo.inviteeList) {
|
||||
[mutableContent appendString:@"\"{"];
|
||||
[mutableContent appendString:invitee];
|
||||
[mutableContent appendString:@"}\"、"];
|
||||
}
|
||||
if (mutableContent.length > 0) {
|
||||
[mutableContent replaceCharactersInRange:NSMakeRange(mutableContent.length - 1, 1) withString:@" "];
|
||||
}
|
||||
}
|
||||
[mutableContent appendString:TIMCommonLocalizableString(TUIKitSignalingNoResponse)];
|
||||
display = [NSString stringWithString:mutableContent];
|
||||
} else if (protocolType == TUICallProtocolTypeLineBusy) {
|
||||
// Hang up with line busy
|
||||
display = isGroup ? [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitSignalingBusyFormat), showName]
|
||||
: TIMCommonLocalizableString(TUIKitSignalingCallBusy);
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudio) {
|
||||
// Change video-call to voice-call
|
||||
display = TIMCommonLocalizableString(TUIKitSignalingSwitchToAudio);
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudioConfirm) {
|
||||
// Confirm the change of video-voice-call
|
||||
display = TIMCommonLocalizableString(TUIKitSignalingComfirmSwitchToAudio);
|
||||
}
|
||||
|
||||
return rtlString(display);
|
||||
}
|
||||
|
||||
- (TUICallMessageDirection)directionForDetailsAppearance {
|
||||
if (self.innerMessage.isSelf) {
|
||||
return TUICallMessageDirectionOutgoing;
|
||||
} else {
|
||||
return TUICallMessageDirectionIncoming;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isUseReceiverAvatarForDetailsAppearance {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Simplify style
|
||||
- (NSString *)contentForSimplifyAppearance {
|
||||
if (self.excludeFromHistory) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TUICallParticipantType participantType = self.participantType;
|
||||
TUICallProtocolType protocolType = self.protocolType;
|
||||
BOOL isCaller = (self.participantRole == TUICallParticipantRoleCaller);
|
||||
|
||||
NSString *display = nil;
|
||||
NSString *showName = [TUIMessageBaseDataProvider getShowName:self.innerMessage];
|
||||
|
||||
if (participantType == TUICallParticipantTypeC2C) {
|
||||
// C2C shown: reject、cancel、hangup、timeout、line_busy
|
||||
if (protocolType == TUICallProtocolTypeReject) {
|
||||
display = isCaller ? TUIChatLocalizableString(TUIChatCallRejectInCaller) : TUIChatLocalizableString(TUIChatCallRejectInCallee);
|
||||
} else if (protocolType == TUICallProtocolTypeCancel) {
|
||||
display = isCaller ? TUIChatLocalizableString(TUIChatCallCancelInCaller) : TUIChatLocalizableString(TUIChatCallCancelInCallee);
|
||||
} else if (protocolType == TUICallProtocolTypeHangup) {
|
||||
NSInteger duration = [[self.jsonData objectForKey:@"call_end"] integerValue];
|
||||
display = [NSString stringWithFormat:@"%@:%.2d:%.2d",TUIChatLocalizableString(TUIChatCallDurationFormat),duration / 60, duration % 60];
|
||||
} else if (protocolType == TUICallProtocolTypeTimeout) {
|
||||
display = isCaller ? TUIChatLocalizableString(TUIChatCallTimeoutInCaller) : TUIChatLocalizableString(TUIChatCallTimeoutInCallee);
|
||||
} else if (protocolType == TUICallProtocolTypeLineBusy) {
|
||||
display = isCaller ? TUIChatLocalizableString(TUIChatCallLinebusyInCaller) : TUIChatLocalizableString(TUIChatCallLinebusyInCallee);
|
||||
}
|
||||
// C2C compatiable
|
||||
else if (protocolType == TUICallProtocolTypeSend) {
|
||||
display = TUIChatLocalizableString(TUIChatCallSend);
|
||||
} else if (protocolType == TUICallProtocolTypeAccept) {
|
||||
display = TUIChatLocalizableString(TUIChatCallAccept);
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudio) {
|
||||
display = TUIChatLocalizableString(TUIChatCallSwitchToAudio);
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudioConfirm) {
|
||||
display = TUIChatLocalizableString(TUIChatCallConfirmSwitchToAudio);
|
||||
} else {
|
||||
display = TUIChatLocalizableString(TUIChatCallUnrecognized);
|
||||
}
|
||||
} else if (participantType == TUICallParticipantTypeGroup) {
|
||||
// Group shown: invite、cancel、hangup、timeout、line_busy
|
||||
if (protocolType == TUICallProtocolTypeSend) {
|
||||
display = [NSString stringWithFormat:TUIChatLocalizableString(TUIChatGroupCallSendFormat), showName];
|
||||
} else if (protocolType == TUICallProtocolTypeCancel) {
|
||||
display = TUIChatLocalizableString(TUIChatGroupCallEnd);
|
||||
} else if (protocolType == TUICallProtocolTypeHangup) {
|
||||
display = TUIChatLocalizableString(TUIChatGroupCallEnd);
|
||||
} else if (protocolType == TUICallProtocolTypeTimeout || protocolType == TUICallProtocolTypeLineBusy) {
|
||||
NSMutableString *mutableContent = [NSMutableString string];
|
||||
if (participantType == TUICallParticipantTypeGroup) {
|
||||
for (NSString *invitee in self.signalingInfo.inviteeList) {
|
||||
[mutableContent appendString:@"\"{"];
|
||||
[mutableContent appendString:invitee];
|
||||
[mutableContent appendString:@"}\"、"];
|
||||
}
|
||||
[mutableContent replaceCharactersInRange:NSMakeRange(mutableContent.length - 1, 1) withString:@" "];
|
||||
}
|
||||
|
||||
if (protocolType == TUICallProtocolTypeLineBusy) {
|
||||
[mutableContent appendString:TUIChatLocalizableString(TUIChatCallLinebusyInCallee)];
|
||||
} else {
|
||||
[mutableContent appendString:TUIChatLocalizableString(TUIChatGroupCallNoAnswer)];
|
||||
}
|
||||
display = [NSString stringWithString:mutableContent];
|
||||
|
||||
}
|
||||
// Group compatiable
|
||||
else if (protocolType == TUICallProtocolTypeReject) {
|
||||
display = [NSString stringWithFormat:TUIChatLocalizableString(TUIChatGroupCallRejectFormat), showName];
|
||||
} else if (protocolType == TUICallProtocolTypeAccept) {
|
||||
display = [NSString stringWithFormat:TUIChatLocalizableString(TUIChatGroupCallAcceptFormat), showName];
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudio) {
|
||||
display = [NSString stringWithFormat:TUIChatLocalizableString(TUIChatGroupCallSwitchToAudioFormat), showName];
|
||||
} else if (protocolType == TUICallProtocolTypeSwitchToAudioConfirm) {
|
||||
display = [NSString stringWithFormat:TUIChatLocalizableString(TUIChatGroupCallConfirmSwitchToAudioFormat), showName];
|
||||
} else {
|
||||
display = TUIChatLocalizableString(TUIChatCallUnrecognized);
|
||||
}
|
||||
} else {
|
||||
display = TUIChatLocalizableString(TUIChatCallUnrecognized);
|
||||
}
|
||||
return rtlString(display);
|
||||
}
|
||||
|
||||
- (TUICallMessageDirection)directionForSimplifyAppearance {
|
||||
if (self.participantRole == TUICallParticipantRoleCaller) {
|
||||
return TUICallMessageDirectionOutgoing;
|
||||
} else {
|
||||
return TUICallMessageDirectionIncoming;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isUseReceiverAvatarForSimplifyAppearance {
|
||||
if (self.direction == TUICallMessageDirectionOutgoing) {
|
||||
return !self.innerMessage.isSelf;
|
||||
} else {
|
||||
return self.innerMessage.isSelf;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Utils
|
||||
- (NSString *)convertProtocolTypeToString:(TUICallProtocolType)type {
|
||||
static NSDictionary *dict = nil;
|
||||
if (dict == nil) {
|
||||
dict = @{
|
||||
@(TUICallProtocolTypeSend) : @"TUICallProtocolTypeSend",
|
||||
@(TUICallProtocolTypeAccept) : @"TUICallProtocolTypeAccept",
|
||||
@(TUICallProtocolTypeReject) : @"TUICallProtocolTypeReject",
|
||||
@(TUICallProtocolTypeCancel) : @"TUICallProtocolTypeCancel",
|
||||
@(TUICallProtocolTypeHangup) : @"TUICallProtocolTypeHangup",
|
||||
@(TUICallProtocolTypeTimeout) : @"TUICallProtocolTypeTimeout",
|
||||
@(TUICallProtocolTypeLineBusy) : @"TUICallProtocolTypeLineBusy",
|
||||
@(TUICallProtocolTypeSwitchToAudio) : @"TUICallProtocolTypeSwitchToAudio",
|
||||
@(TUICallProtocolTypeSwitchToAudioConfirm) : @"TUICallProtocolTypeSwitchToAudioConfirm",
|
||||
};
|
||||
}
|
||||
return [dict objectForKey:@(type)] ?: @"unknown";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ********************************************************************
|
||||
|
||||
// ********************************************************************
|
||||
// TUIChatCallingDataProvider
|
||||
// ********************************************************************
|
||||
@interface TUIChatCallingDataProvider ()
|
||||
|
||||
@property(nonatomic, assign) TUIChatCallingMessageAppearance style;
|
||||
@property(nonatomic, strong) NSCache<TUIChatMessageID, TUIChatCallingInfo *> *callingCache;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatCallingDataProvider
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
self.style = TUIChatCallingMessageAppearanceSimplify;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setCallingMessageStyle:(TUIChatCallingMessageAppearance)style {
|
||||
self.style = style;
|
||||
}
|
||||
|
||||
- (void)redialFromMessage:(V2TIMMessage *)innerMessage {
|
||||
NSDictionary *param = nil;
|
||||
id<TUIChatCallingInfoProtocol> callingInfo = nil;
|
||||
if ([self isCallingMessage:innerMessage callingInfo:&callingInfo]) {
|
||||
if (callingInfo.streamMediaType == TUICallStreamMediaTypeVoice) {
|
||||
param = @{
|
||||
TUICore_TUICallingService_ShowCallingViewMethod_UserIDsKey : @[ innerMessage.userID ],
|
||||
TUICore_TUICallingService_ShowCallingViewMethod_CallTypeKey : @"0"
|
||||
};
|
||||
} else if (callingInfo.streamMediaType == TUICallStreamMediaTypeVideo) {
|
||||
param = @{
|
||||
TUICore_TUICallingService_ShowCallingViewMethod_UserIDsKey : @[ innerMessage.userID ],
|
||||
TUICore_TUICallingService_ShowCallingViewMethod_CallTypeKey : @"1"
|
||||
};
|
||||
}
|
||||
if (param) {
|
||||
[TUICore callService:TUICore_TUICallingService method:TUICore_TUICallingService_ShowCallingViewMethod param:param];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isCallingMessage:(V2TIMMessage *)innerMessage callingInfo:(id<TUIChatCallingInfoProtocol> __nullable *__nullable)callingInfo {
|
||||
TUIChatCallingInfo *item = [self callingInfoForMesssage:innerMessage];
|
||||
if (item == nil) {
|
||||
if (callingInfo) {
|
||||
*callingInfo = nil;
|
||||
}
|
||||
return NO;
|
||||
|
||||
} else {
|
||||
if (callingInfo) {
|
||||
*callingInfo = item;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (TUIChatCallingInfo *__nullable)callingInfoForMesssage:(V2TIMMessage *)innerMessage {
|
||||
// 1. Fetch from cache
|
||||
TUIChatMessageID msgID = innerMessage.msgID ?: @"";
|
||||
TUIChatCallingInfo *item = [self.callingCache objectForKey:msgID];
|
||||
if (item) {
|
||||
item.innerMessage = innerMessage;
|
||||
return item;
|
||||
}
|
||||
|
||||
// 2. Parse
|
||||
V2TIMSignalingInfo *info = [V2TIMManager.sharedInstance getSignallingInfo:innerMessage];
|
||||
if (info == nil || info.data.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
NSData *data = [info.data dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *err = nil;
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&err];
|
||||
if (param == nil || ![param isKindOfClass:NSDictionary.class]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *businessID = [param objectForKey:@"businessID"];
|
||||
if (businessID == nil || ![businessID isKindOfClass:NSString.class]) {
|
||||
return nil;
|
||||
}
|
||||
if (![businessID isEqualToString:@"av_call"] && ![businessID isEqualToString:@"rtc_call"] ) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 3 Cached and return
|
||||
item = [[TUIChatCallingInfo alloc] init];
|
||||
item.style = self.style;
|
||||
item.signalingInfo = info;
|
||||
item.jsonData = param;
|
||||
item.innerMessage = innerMessage;
|
||||
[self.callingCache setObject:item forKey:msgID];
|
||||
return item;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (NSCache<TUIChatMessageID, TUIChatCallingInfo *> *)callingCache {
|
||||
if (_callingCache == nil) {
|
||||
_callingCache = [[NSCache alloc] init];
|
||||
}
|
||||
return _callingCache;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ********************************************************************
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
#import <Foundation/Foundation.h>
|
||||
@class TUIGroupPendencyCellData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
*
|
||||
* 【Module name】 TUIGroupPendencyViewModel
|
||||
* 【Function description】Group request view model
|
||||
* This view model is responsible for unified processing of group request messages. For example, a series of logics such as obtaining the unread count,
|
||||
* receiving new request messages and updating them, agreeing/removing data in the existing data queue, etc.
|
||||
*/
|
||||
@interface TUIGroupPendencyDataProvider : NSObject
|
||||
|
||||
/**
|
||||
* Request data list
|
||||
* The object type stored in this list is TUIGroupPendencyCellData.
|
||||
* That is, this array stores all pending request data of the current group, and this attribute is read-only and cannot be modified.
|
||||
*/
|
||||
@property(readonly) NSArray *dataList;
|
||||
|
||||
/**
|
||||
*
|
||||
* Whether to have next request data.
|
||||
* When loading data, hasNextData is YES when the request list of the current group has not been read out.
|
||||
*/
|
||||
@property BOOL hasNextData;
|
||||
|
||||
/**
|
||||
*
|
||||
* Loading identifier
|
||||
* When the current view model is loading data, this property is YES. At this time, loading again is not allowed until the current loading process is
|
||||
* completed.
|
||||
*/
|
||||
@property BOOL isLoading;
|
||||
|
||||
/**
|
||||
*
|
||||
* Unread count, that is, the number of outstanding requests for the current group.
|
||||
*/
|
||||
@property int unReadCnt;
|
||||
|
||||
/**
|
||||
*
|
||||
* group ID
|
||||
* It is used to identify the current group and determine whether the request to join the group is a request for this group.
|
||||
*/
|
||||
@property NSString *groupId;
|
||||
|
||||
/**
|
||||
*
|
||||
* Load data
|
||||
* 1. First determine whether it is currently loading, and if so, terminate this loading.
|
||||
* 2. Pull the request data from the server through the getPendencyFromServer interface provided by the TIMGroupManager class in the IM SDK. The default is 100
|
||||
* requests per page.
|
||||
* 3. For the pulled data, determine whether the group ID corresponding to the request is the same as this group, and if so, convert the request to
|
||||
* TUIGroupPendencyCellData and store it in the datalist. (The request object pulled from the server is TIMGroupPendencyItem).
|
||||
*/
|
||||
- (void)loadData;
|
||||
|
||||
/**
|
||||
* Approve current request data.
|
||||
* This function directly calls accept implemented in TUIGroupPendencyCellData, and the unread count is decremented by 1.
|
||||
*/
|
||||
- (void)acceptData:(TUIGroupPendencyCellData *)data;
|
||||
|
||||
/**
|
||||
* Deny the current request data.
|
||||
* Remove the data in the parameter from the datalist, and call reject implemented in TUIGroupPendencyCellData, while the unread count is decremented by 1.
|
||||
*/
|
||||
- (void)removeData:(TUIGroupPendencyCellData *)data;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// TUIGroupPendencyViewModel.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/6/18.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupPendencyDataProvider.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@interface TUIGroupPendencyDataProvider ()
|
||||
|
||||
@property NSArray *dataList;
|
||||
|
||||
@property(nonatomic, assign) uint64_t origSeq;
|
||||
|
||||
@property(nonatomic, assign) uint64_t seq;
|
||||
|
||||
@property(nonatomic, assign) uint64_t timestamp;
|
||||
|
||||
@property(nonatomic, assign) uint64_t numPerPage;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIGroupPendencyDataProvider
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
|
||||
_numPerPage = 100;
|
||||
_dataList = @[];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPendencyChanged:) name:TUIGroupPendencyCellData_onPendencyChanged object:nil];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)onPendencyChanged:(NSNotification *)notification {
|
||||
int unReadCnt = 0;
|
||||
for (TUIGroupPendencyCellData *data in self.dataList) {
|
||||
if (data.isRejectd || data.isAccepted) {
|
||||
continue;
|
||||
}
|
||||
unReadCnt++;
|
||||
}
|
||||
self.unReadCnt = unReadCnt;
|
||||
}
|
||||
|
||||
- (void)loadData {
|
||||
if (self.isLoading) return;
|
||||
|
||||
self.isLoading = YES;
|
||||
@weakify(self);
|
||||
[[V2TIMManager sharedInstance]
|
||||
getGroupApplicationList:^(V2TIMGroupApplicationResult *result) {
|
||||
@strongify(self);
|
||||
NSMutableArray *list = @[].mutableCopy;
|
||||
for (V2TIMGroupApplication *item in result.applicationList) {
|
||||
if ([item.groupID isEqualToString:self.groupId] && item.handleStatus == V2TIM_GROUP_APPLICATION_HANDLE_STATUS_UNHANDLED) {
|
||||
TUIGroupPendencyCellData *data = [[TUIGroupPendencyCellData alloc] initWithPendency:item];
|
||||
[list addObject:data];
|
||||
}
|
||||
}
|
||||
self.dataList = list;
|
||||
self.unReadCnt = (int)list.count;
|
||||
self.isLoading = NO;
|
||||
self.hasNextData = NO;
|
||||
;
|
||||
}
|
||||
fail:nil];
|
||||
}
|
||||
|
||||
- (void)acceptData:(TUIGroupPendencyCellData *)data {
|
||||
[data accept];
|
||||
self.unReadCnt--;
|
||||
}
|
||||
|
||||
- (void)removeData:(TUIGroupPendencyCellData *)data {
|
||||
NSMutableArray *dataList = [NSMutableArray arrayWithArray:self.dataList];
|
||||
[dataList removeObject:data];
|
||||
self.dataList = dataList;
|
||||
[data reject];
|
||||
self.unReadCnt--;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// TUIMessageDataProvider+ProtectedAPI.h
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/7/9.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageBaseDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMessageBaseDataProvider ()
|
||||
|
||||
@property(nonatomic) NSMutableArray<TUIMessageCellData *> *uiMsgs_;
|
||||
@property(nonatomic) NSMutableDictionary<NSString *, NSNumber *> *heightCache_;
|
||||
@property(nonatomic) BOOL isLoadingData;
|
||||
@property(nonatomic) BOOL isNoMoreMsg;
|
||||
@property(nonatomic) BOOL isFirstLoad;
|
||||
@property(nonatomic) V2TIMMessage *msgForDate;
|
||||
|
||||
- (nullable TUIMessageCellData *)getSystemMsgFromDate:(NSDate *)date;
|
||||
- (NSMutableArray *)transUIMsgFromIMMsg:(NSArray *)msgs;
|
||||
- (void)onRecvNewMessage:(V2TIMMessage *)msg;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,249 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TUIMessageCell.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import "TUIChatConversationModel.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TUIMessageBaseDataProviderDataSourceChangeType) {
|
||||
TUIMessageBaseDataProviderDataSourceChangeTypeInsert,
|
||||
TUIMessageBaseDataProviderDataSourceChangeTypeDelete,
|
||||
TUIMessageBaseDataProviderDataSourceChangeTypeReload,
|
||||
};
|
||||
|
||||
@class TUIMessageBaseDataProvider;
|
||||
@protocol TUIMessageBaseDataProviderDataSource <NSObject>
|
||||
|
||||
@required
|
||||
- (void)dataProviderDataSourceWillChange:(TUIMessageBaseDataProvider *)dataProvider;
|
||||
- (void)dataProviderDataSourceChange:(TUIMessageBaseDataProvider *)dataProvider
|
||||
withType:(TUIMessageBaseDataProviderDataSourceChangeType)type
|
||||
atIndex:(NSUInteger)index
|
||||
animation:(BOOL)animation;
|
||||
- (void)dataProviderDataSourceDidChange:(TUIMessageBaseDataProvider *)dataProvider;
|
||||
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider onRemoveHeightCache:(TUIMessageCellData *)cellData;
|
||||
|
||||
@optional
|
||||
/**
|
||||
* Message read event
|
||||
*
|
||||
* @param userID recevier of one-to-one message
|
||||
* @param timestamp Read receipt time, messages before this timestamp can be considered read by the other party
|
||||
*/
|
||||
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider ReceiveReadMsgWithUserID:(NSString *)userId Time:(time_t)timestamp;
|
||||
|
||||
|
||||
/**
|
||||
* Group message read event
|
||||
*
|
||||
* @param groupID Group ID
|
||||
* @param msgID Message idenetifier
|
||||
* @param readCount Count of read message
|
||||
* @param unreadCount Count of unread message
|
||||
*/
|
||||
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider
|
||||
ReceiveReadMsgWithGroupID:(NSString *)groupID
|
||||
msgID:(NSString *)msgID
|
||||
readCount:(NSUInteger)readCount
|
||||
unreadCount:(NSUInteger)unreadCount;
|
||||
|
||||
/**
|
||||
* A new message is received, the data has been changed, refreshed, it has been processed internally, and subsequent processing can be done in this method
|
||||
*
|
||||
* @param uiMsg The new message
|
||||
*/
|
||||
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider ReceiveNewUIMsg:(TUIMessageCellData *)uiMsg;
|
||||
|
||||
/**
|
||||
*
|
||||
* Reveived a recalled message
|
||||
*/
|
||||
- (void)dataProvider:(TUIMessageBaseDataProvider *)dataProvider ReceiveRevokeUIMsg:(TUIMessageCellData *)uiMsg;
|
||||
|
||||
/**
|
||||
* This event is fired when a new message is received after the request for a new message is completed
|
||||
* External can use this method to modify the CellData to be displayed, add messages (such as time messages), and customize messages
|
||||
*/
|
||||
- (nullable TUIMessageCellData *)dataProvider:(TUIMessageBaseDataProvider *)dataProvider CustomCellDataFromNewIMMessage:(V2TIMMessage *)msg;
|
||||
|
||||
- (BOOL)isDataSourceConsistent;
|
||||
@end
|
||||
|
||||
/**
|
||||
*
|
||||
* 【Module name】Chat message list view model (TUIMessageDataProvider)
|
||||
* 【Function description】Responsible for implementing the data processing and business logic of the message list in the chat page
|
||||
* 1. The view model can pull the message list data from the server through the interface provided by the IM SDK, and load the data.
|
||||
* 2. The view model can synchronously remove the message list data when the user needs to delete the session list.
|
||||
*/
|
||||
@interface TUIMessageBaseDataProvider : NSObject
|
||||
|
||||
@property(nonatomic, weak) id<TUIMessageBaseDataProviderDataSource> dataSource;
|
||||
@property(nonatomic, strong, readonly) TUIChatConversationModel *conversationModel;
|
||||
@property(nonatomic, strong, readonly) NSArray<TUIMessageCellData *> *uiMsgs;
|
||||
@property(nonatomic, strong, readonly) NSDictionary<NSString *, NSNumber *> *heightCache;
|
||||
@property(nonatomic, assign, readonly) BOOL isLoadingData;
|
||||
@property(nonatomic, assign, readonly) BOOL isNoMoreMsg;
|
||||
@property(nonatomic, assign, readonly) BOOL isFirstLoad;
|
||||
|
||||
/**
|
||||
* ,
|
||||
*
|
||||
* If adjacent messages are sent by the same user, the messages will be merged for display.
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL mergeAdjacentMsgsFromTheSameSender;
|
||||
|
||||
/**
|
||||
*
|
||||
* Count of per page, default is 20.
|
||||
*/
|
||||
@property(nonatomic, assign) NSInteger pageCount;
|
||||
|
||||
- (instancetype)initWithConversationModel:(TUIChatConversationModel *)conversationModel;
|
||||
|
||||
- (void)loadMessageSucceedBlock:(void (^)(BOOL isFirstLoad, BOOL isNoMoreMsg, NSArray<TUIMessageCellData *> *newMsgs))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock;
|
||||
|
||||
- (void)sendUIMsg:(TUIMessageCellData *)uiMsg
|
||||
toConversation:(TUIChatConversationModel *)conversationData
|
||||
willSendBlock:(void (^)(BOOL isReSend, TUIMessageCellData *dateUIMsg))willSendBlock
|
||||
SuccBlock:(nullable V2TIMSucc)succ
|
||||
FailBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
- (void)revokeUIMsg:(TUIMessageCellData *)uiMsg SuccBlock:(nullable V2TIMSucc)succ FailBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
- (void)deleteUIMsgs:(NSArray<TUIMessageCellData *> *)uiMsgs SuccBlock:(nullable V2TIMSucc)succ FailBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
- (void)addUIMsg:(TUIMessageCellData *)cellData;
|
||||
|
||||
- (void)removeUIMsg:(TUIMessageCellData *)cellData;
|
||||
|
||||
- (void)insertUIMsgs:(NSArray<TUIMessageCellData *> *)uiMsgs atIndexes:(NSIndexSet *)indexes;
|
||||
|
||||
- (void)sendPlaceHolderUIMessage:(TUIMessageCellData *)placeHolderCellData; //Only send PlaceHolder UI Message
|
||||
|
||||
- (void)addUIMsgs:(NSArray<TUIMessageCellData *> *)uiMsgs;
|
||||
|
||||
- (void)replaceUIMsg:(TUIMessageCellData *)cellData atIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
* Preprocessing reply messages (asynchronously loading original messages and downloading corresponding thumbnails)
|
||||
*/
|
||||
- (void)preProcessMessage:(NSArray<TUIMessageCellData *> *)uiMsgs callback:(void (^)(void))callback;
|
||||
|
||||
- (NSArray<NSString *> *)getUserIDListForAdditionalUserInfo:(NSArray<TUIMessageCellData *> *)uiMsgs;
|
||||
- (void)requestForAdditionalUserInfo:(NSArray<TUIMessageCellData *> *)uiMsgs callback:(void (^)(void))callback;
|
||||
|
||||
/**
|
||||
* Send read receipts for latest messages
|
||||
*/
|
||||
- (void)sendLatestMessageReadReceipt;
|
||||
|
||||
/**
|
||||
* Send a read receipt for the specified index message
|
||||
*/
|
||||
- (void)sendMessageReadReceiptAtIndexes:(NSArray *)indexes;
|
||||
|
||||
/**
|
||||
* Get the index of the message in the mesage data through msgID
|
||||
*/
|
||||
- (NSInteger)getIndexOfMessage:(NSString *)msgID;
|
||||
|
||||
- (NSMutableArray *)transUIMsgFromIMMsg:(NSArray *)msgs;
|
||||
|
||||
- (void)clearUIMsgList;
|
||||
|
||||
- (void)processQuoteMessage:(NSArray<TUIMessageCellData *> *)uiMsgs; // subclass override required
|
||||
|
||||
+ (void)updateUIMsgStatus:(TUIMessageCellData *)cellData uiMsgs:(NSArray *)uiMsgs;
|
||||
|
||||
- (void)getPinMessageList;
|
||||
|
||||
- (void)loadGroupInfo:(dispatch_block_t)callback;
|
||||
|
||||
- (void)getSelfInfoInGroup:(dispatch_block_t)callback;
|
||||
|
||||
- (void)pinGroupMessage:(NSString *)groupID
|
||||
message:(V2TIMMessage *)message
|
||||
isPinned:(BOOL)isPinned
|
||||
succ:(V2TIMSucc)succ
|
||||
fail:(V2TIMFail)fail;
|
||||
|
||||
- (BOOL)isCurrentUserRoleSuperAdminInGroup;
|
||||
|
||||
- (BOOL)isCurrentMessagePin:(NSString *)msgID;
|
||||
|
||||
@property(nonatomic, copy) void (^groupRoleChanged)(V2TIMGroupMemberRole role);
|
||||
@property(nonatomic, copy) void (^pinGroupMessageChanged)(NSArray *);
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIMessageBaseDataProvider (IMSDK)
|
||||
/// imsdk interface call
|
||||
+ (NSString *)sendMessage:(V2TIMMessage *)message
|
||||
toConversation:(TUIChatConversationModel *)conversationData
|
||||
appendParams:(TUISendMessageAppendParams *)appendParams
|
||||
Progress:(nullable V2TIMProgress)progress
|
||||
SuccBlock:(nullable V2TIMSucc)succ
|
||||
FailBlock:(nullable V2TIMFail)fail;
|
||||
|
||||
- (void)getLastMessage:(BOOL)isFromLocal succ:(void (^)(V2TIMMessage *message))succ fail:(V2TIMFail)fail;
|
||||
|
||||
+ (void)markC2CMessageAsRead:(NSString *)userID succ:(nullable V2TIMSucc)succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)markGroupMessageAsRead:(NSString *)groupID succ:(nullable V2TIMSucc)succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)markConversationAsUndead:(NSArray<NSString *> *)conversationIDList enableMark:(BOOL)enableMark;
|
||||
|
||||
+ (void)revokeMessage:(V2TIMMessage *)msg succ:(nullable V2TIMSucc)succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)deleteMessages:(NSArray<V2TIMMessage *> *)msgList succ:(nullable V2TIMSucc)succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
+ (void)modifyMessage:(V2TIMMessage *)msg completion:(V2TIMMessageModifyCompletion)completion;
|
||||
|
||||
/**
|
||||
* Send message read receipts
|
||||
*/
|
||||
+ (void)sendMessageReadReceipts:(NSArray *)msgs;
|
||||
|
||||
/**
|
||||
* Getting the list of read and unread members of group messages
|
||||
*/
|
||||
+ (void)getReadMembersOfMessage:(V2TIMMessage *)msg
|
||||
filter:(V2TIMGroupMessageReadMembersFilter)filter
|
||||
nextSeq:(NSUInteger)nextSeq
|
||||
completion:(void (^)(int code, NSString *desc, NSArray *members, NSUInteger nextSeq, BOOL isFinished))block;
|
||||
|
||||
/**
|
||||
* Getting the read receipt of the message
|
||||
*/
|
||||
+ (void)getMessageReadReceipt:(NSArray *)messages succ:(nullable V2TIMMessageReadReceiptsSucc)succ fail:(nullable V2TIMFail)fail;
|
||||
|
||||
/// message -> cellData
|
||||
+ (nullable TUIMessageCellData *)getCellData:(V2TIMMessage *)message;
|
||||
+ (nullable TUIMessageCellData *)getSystemMsgFromDate:(NSDate *)date;
|
||||
+ (nullable TUIMessageCellData *)getRevokeCellData:(V2TIMMessage *)message;
|
||||
|
||||
/// message -> displayString
|
||||
+ (nullable NSString *)getDisplayString:(V2TIMMessage *)message;
|
||||
+ (nullable NSString *)getRevokeDispayString:(V2TIMMessage *)message;
|
||||
+ (nullable NSString *)getRevokeDispayString:(V2TIMMessage *)message operateUser:(V2TIMUserFullInfo *)operateUser reason:(NSString *)reason;
|
||||
+ (nullable NSString *)getGroupTipsDisplayString:(V2TIMMessage *)message;
|
||||
|
||||
/// message <-> info
|
||||
+ (V2TIMMessage *)getCustomMessageWithJsonData:(NSData *)data;
|
||||
+ (V2TIMMessage *)getCustomMessageWithJsonData:(NSData *)data desc:(NSString *)desc extension:(NSString *)extension;
|
||||
+ (NSMutableArray *)getUserIDList:(NSArray<V2TIMGroupMemberInfo *> *)infoList;
|
||||
+ (NSString *)getShowName:(V2TIMMessage *)message;
|
||||
+ (NSString *)getOpUserName:(V2TIMGroupMemberInfo *)info;
|
||||
+ (NSMutableArray *)getUserNameList:(NSArray<V2TIMGroupMemberInfo *> *)infoList;
|
||||
+ (NSString *)getUserName:(V2TIMGroupTipsElem *)tips with:(NSString *)userId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
1602
TUIKit/TUIChat/BaseDataProvider/Base/TUIMessageBaseDataProvider.m
Normal file
1602
TUIKit/TUIChat/BaseDataProvider/Base/TUIMessageBaseDataProvider.m
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMessageBaseDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMessageBaseMediaDataProvider : TUIMessageBaseDataProvider
|
||||
@property(nonatomic, strong) NSMutableArray *medias;
|
||||
|
||||
- (instancetype)initWithConversationModel:(nullable TUIChatConversationModel *)conversationModel;
|
||||
|
||||
/**
|
||||
* Pull 20 video (picture) messages before and after the current message
|
||||
*/
|
||||
- (void)loadMediaWithMessage:(V2TIMMessage *)curMessage;
|
||||
|
||||
/**
|
||||
* Pull older 20 video (image) messages
|
||||
*/
|
||||
- (void)loadOlderMedia;
|
||||
|
||||
/**
|
||||
* Pull the last 20 video (image) messages
|
||||
*/
|
||||
- (void)loadNewerMedia;
|
||||
|
||||
- (void)removeCache;
|
||||
|
||||
+ (TUIMessageCellData *)getMediaCellData:(V2TIMMessage *)message;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// TUIMessageSearchDataProvider.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/7/8.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageBaseMediaDataProvider.h"
|
||||
#import "TUIMessageBaseDataProvider+ProtectedAPI.h"
|
||||
|
||||
/**
|
||||
* Message pull method
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, TUIMediaLoadType) {
|
||||
TUIMediaLoadType_Older = 1,
|
||||
TUIMediaLoadType_Newer = 2,
|
||||
TUIMediaLoadType_Older_And_Newer = 3,
|
||||
};
|
||||
|
||||
@interface TUIMessageBaseMediaDataProvider ()
|
||||
@property(nonatomic) TUIChatConversationModel *conversationModel;
|
||||
@property(nonatomic, assign) TUIMediaLoadType loadType;
|
||||
@property(nonatomic, strong) V2TIMMessage *loadMessage;
|
||||
@property(nonatomic, assign) BOOL isOlderNoMoreMsg;
|
||||
@property(nonatomic, assign) BOOL isNewerNoMoreMsg;
|
||||
@end
|
||||
|
||||
@implementation TUIMessageBaseMediaDataProvider
|
||||
|
||||
- (instancetype)initWithConversationModel:(nullable TUIChatConversationModel *)conversationModel {
|
||||
self = [super initWithConversationModel:conversationModel];
|
||||
if (self) {
|
||||
self.conversationModel = conversationModel;
|
||||
self.isOlderNoMoreMsg = NO;
|
||||
self.isNewerNoMoreMsg = NO;
|
||||
self.pageCount = 20;
|
||||
self.medias = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadMediaWithMessage:(V2TIMMessage *)curMessage {
|
||||
self.loadMessage = curMessage;
|
||||
self.loadType = TUIMediaLoadType_Older_And_Newer;
|
||||
/**
|
||||
* When the message is being sent, an exception will occur when pulling the before and after video (picture) messages through the current message. Only the
|
||||
* current message is displayed here for the time being.
|
||||
*/
|
||||
if (self.loadMessage.status != V2TIM_MSG_STATUS_SENDING) {
|
||||
[self loadMedia];
|
||||
} else {
|
||||
NSMutableArray *medias = self.medias;
|
||||
TUIMessageCellData *data = [self.class getMediaCellData:self.loadMessage];
|
||||
if (data) {
|
||||
[medias addObject:data];
|
||||
self.medias = medias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadOlderMedia {
|
||||
if (self.loadMessage.status != V2TIM_MSG_STATUS_SENDING) {
|
||||
TUIMessageCellData *firstData = (TUIMessageCellData *)self.medias.firstObject;
|
||||
self.loadMessage = firstData.innerMessage;
|
||||
self.loadType = TUIMediaLoadType_Older;
|
||||
[self loadMedia];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadNewerMedia {
|
||||
if (self.loadMessage.status != V2TIM_MSG_STATUS_SENDING) {
|
||||
TUIMessageCellData *lastData = (TUIMessageCellData *)self.medias.lastObject;
|
||||
self.loadMessage = lastData.innerMessage;
|
||||
self.loadType = TUIMediaLoadType_Newer;
|
||||
[self loadMedia];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadMedia {
|
||||
if (!self.loadMessage) {
|
||||
return;
|
||||
}
|
||||
if (![self isNeedLoad:self.loadType]) {
|
||||
return;
|
||||
}
|
||||
@weakify(self);
|
||||
[self loadMediaMessage:self.loadMessage
|
||||
loadType:self.loadType
|
||||
SucceedBlock:^(NSArray<V2TIMMessage *> *_Nonnull olders, NSArray<V2TIMMessage *> *_Nonnull newers) {
|
||||
@strongify(self);
|
||||
NSMutableArray *medias = self.medias;
|
||||
for (V2TIMMessage *msg in olders) {
|
||||
TUIMessageCellData *data = [self.class getMediaCellData:msg];
|
||||
if (data) {
|
||||
[medias insertObject:data atIndex:0];
|
||||
}
|
||||
}
|
||||
if (self.loadType == TUIMediaLoadType_Older_And_Newer) {
|
||||
TUIMessageCellData *data = [self.class getMediaCellData:self.loadMessage];
|
||||
if (data) {
|
||||
[medias addObject:data];
|
||||
;
|
||||
}
|
||||
}
|
||||
for (V2TIMMessage *msg in newers) {
|
||||
TUIMessageCellData *data = [self.class getMediaCellData:msg];
|
||||
if (data) {
|
||||
[medias addObject:data];
|
||||
}
|
||||
}
|
||||
self.medias = medias;
|
||||
}
|
||||
FailBlock:^(int code, NSString *desc) {
|
||||
NSLog(@"load message failed!");
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)isNeedLoad:(TUIMediaLoadType)type {
|
||||
if ((TUIMediaLoadType_Older == type && self.isOlderNoMoreMsg) || (TUIMediaLoadType_Newer == type && self.isNewerNoMoreMsg) ||
|
||||
(TUIMediaLoadType_Older_And_Newer == type && self.isOlderNoMoreMsg && self.isNewerNoMoreMsg)) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)loadMediaMessage:(V2TIMMessage *)loadMsg
|
||||
loadType:(TUIMediaLoadType)type
|
||||
SucceedBlock:(void (^)(NSArray<V2TIMMessage *> *_Nonnull olders, NSArray<V2TIMMessage *> *_Nonnull newers))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock {
|
||||
if (self.isLoadingData) {
|
||||
failBlock(ERR_SUCC, @"loading");
|
||||
return;
|
||||
}
|
||||
self.isLoadingData = YES;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
__block NSArray *olders = @[];
|
||||
__block NSArray *newers = @[];
|
||||
__block BOOL isOldLoadFail = NO;
|
||||
__block BOOL isNewLoadFail = NO;
|
||||
__block int failCode = 0;
|
||||
__block NSString *failDesc = nil;
|
||||
|
||||
/**
|
||||
* Loading the oldest 20 media messages starting from the positioning message
|
||||
*/
|
||||
if (TUIMediaLoadType_Older == type || TUIMediaLoadType_Older_And_Newer == type) {
|
||||
dispatch_group_enter(group);
|
||||
V2TIMMessageListGetOption *option = [[V2TIMMessageListGetOption alloc] init];
|
||||
option.getType = V2TIM_GET_LOCAL_OLDER_MSG;
|
||||
option.count = self.pageCount;
|
||||
option.groupID = self.conversationModel.groupID;
|
||||
option.userID = self.conversationModel.userID;
|
||||
option.lastMsg = loadMsg;
|
||||
option.messageTypeList = @[ @(V2TIM_ELEM_TYPE_IMAGE), @(V2TIM_ELEM_TYPE_VIDEO) ];
|
||||
[V2TIMManager.sharedInstance getHistoryMessageList:option
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
olders = msgs ?: @[];
|
||||
if (olders.count < self.pageCount) {
|
||||
self.isOlderNoMoreMsg = YES;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
isOldLoadFail = YES;
|
||||
failCode = code;
|
||||
failDesc = desc;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the latest 20 rich media messages starting from the positioning message
|
||||
*/
|
||||
if (TUIMediaLoadType_Newer == type || TUIMediaLoadType_Older_And_Newer == type) {
|
||||
dispatch_group_enter(group);
|
||||
V2TIMMessageListGetOption *option = [[V2TIMMessageListGetOption alloc] init];
|
||||
option.getType = V2TIM_GET_LOCAL_NEWER_MSG;
|
||||
option.count = self.pageCount;
|
||||
option.groupID = self.conversationModel.groupID;
|
||||
option.userID = self.conversationModel.userID;
|
||||
option.lastMsg = loadMsg;
|
||||
option.messageTypeList = @[ @(V2TIM_ELEM_TYPE_IMAGE), @(V2TIM_ELEM_TYPE_VIDEO) ];
|
||||
[V2TIMManager.sharedInstance getHistoryMessageList:option
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
newers = msgs ?: @[];
|
||||
if (newers.count < self.pageCount) {
|
||||
self.isNewerNoMoreMsg = YES;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
isNewLoadFail = YES;
|
||||
failCode = code;
|
||||
failDesc = desc;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
@weakify(self);
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
@strongify(self);
|
||||
self.isLoadingData = NO;
|
||||
if (isOldLoadFail || isNewLoadFail) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
failBlock(failCode, failDesc);
|
||||
});
|
||||
}
|
||||
self.isFirstLoad = NO;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
succeedBlock(olders, newers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)removeCache {
|
||||
[self.medias removeAllObjects];
|
||||
self.isNewerNoMoreMsg = NO;
|
||||
self.isOlderNoMoreMsg = NO;
|
||||
self.isFirstLoad = YES;
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)getMediaCellData:(V2TIMMessage *)message {
|
||||
// subclass override required
|
||||
return nil;
|
||||
}
|
||||
@end
|
||||
36
TUIKit/TUIChat/BaseDataProvider/Impl/TUIChatDataProvider.h
Normal file
36
TUIKit/TUIChat/BaseDataProvider/Impl/TUIChatDataProvider.h
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TIMInputViewMoreActionProtocol.h>
|
||||
#import "TUIChatBaseDataProvider.h"
|
||||
#import "TUIChatConversationModel.h"
|
||||
#import "TUIInputMoreCellData.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
|
||||
@class TUIChatDataProvider;
|
||||
@class TUICustomActionSheetItem;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIChatDataProvider : TUIChatBaseDataProvider
|
||||
|
||||
#pragma mark - CellData
|
||||
// For Classic Edition.
|
||||
- (NSMutableArray<TUIInputMoreCellData *> *)getMoreMenuCellDataArray:(NSString *)groupID
|
||||
userID:(NSString *)userID
|
||||
conversationModel:(TUIChatConversationModel *)conversationModel
|
||||
actionController:(id<TIMInputViewMoreActionProtocol>)actionController;
|
||||
|
||||
// For Minimalist Edition.
|
||||
- (NSArray<TUICustomActionSheetItem *> *)getInputMoreActionItemList:(nullable NSString *)userID
|
||||
groupID:(nullable NSString *)groupID
|
||||
conversationModel:(TUIChatConversationModel *)conversationModel
|
||||
pushVC:(nullable UINavigationController *)pushVC
|
||||
actionController:(id<TIMInputViewMoreActionProtocol>)actionController;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
503
TUIKit/TUIChat/BaseDataProvider/Impl/TUIChatDataProvider.m
Normal file
503
TUIKit/TUIChat/BaseDataProvider/Impl/TUIChatDataProvider.m
Normal file
@@ -0,0 +1,503 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
@import ImSDK_Plus;
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import <TUICore/NSDictionary+TUISafe.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "UIAlertController+TUICustomStyle.h"
|
||||
#import "TUIChatConfig.h"
|
||||
#import "TUIChatDataProvider.h"
|
||||
#import "TUIMessageDataProvider.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
#import "TUIChatConversationModel.h"
|
||||
#import <TIMCommon/TIMCommonMediator.h>
|
||||
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
|
||||
|
||||
#define Input_SendBtn_Key @"Input_SendBtn_Key"
|
||||
#define Input_SendBtn_Title @"Input_SendBtn_Title"
|
||||
#define Input_SendBtn_ImageName @"Input_SendBtn_ImageName"
|
||||
|
||||
@interface TUISplitEmojiData : NSObject
|
||||
@property (nonatomic, assign) NSInteger start;
|
||||
@property (nonatomic, assign) NSInteger end;
|
||||
@end
|
||||
|
||||
@implementation TUISplitEmojiData
|
||||
@end
|
||||
|
||||
@interface TUIChatDataProvider ()
|
||||
@property(nonatomic, strong) TUIInputMoreCellData *welcomeInputMoreMenu;
|
||||
@property(nonatomic, strong) NSMutableArray<TUIInputMoreCellData *> *customInputMoreMenus;
|
||||
@property(nonatomic, strong) NSArray<TUIInputMoreCellData *> *builtInInputMoreMenus;
|
||||
@property(nonatomic, strong) NSArray<TUICustomActionSheetItem *> *customInputMoreActionItemList;
|
||||
@property(nonatomic, strong) NSMutableArray<TUICustomActionSheetItem *> *builtInInputMoreActionItemList;
|
||||
@end
|
||||
|
||||
@implementation TUIChatDataProvider
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onChangeLanguage) name:TUIChangeLanguageNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
// For Classic Edition.
|
||||
- (NSMutableArray<TUIInputMoreCellData *> *)getMoreMenuCellDataArray:(NSString *)groupID
|
||||
userID:(NSString *)userID
|
||||
conversationModel:(TUIChatConversationModel *)conversationModel
|
||||
actionController:(id<TIMInputViewMoreActionProtocol>)actionController {
|
||||
self.builtInInputMoreMenus = [self createBuiltInInputMoreMenusWithConversationModel:conversationModel];
|
||||
NSMutableArray *moreMenus = [NSMutableArray array];
|
||||
[moreMenus addObjectsFromArray:self.builtInInputMoreMenus];
|
||||
|
||||
BOOL isNeedWelcomeCustomMessage = [TUIChatConfig defaultConfig].enableWelcomeCustomMessage && conversationModel.enableWelcomeCustomMessage;
|
||||
if (isNeedWelcomeCustomMessage) {
|
||||
if (![self.customInputMoreMenus containsObject:self.welcomeInputMoreMenu]) {
|
||||
[self.customInputMoreMenus addObject:self.welcomeInputMoreMenu];
|
||||
}
|
||||
}
|
||||
[moreMenus addObjectsFromArray:self.customInputMoreMenus];
|
||||
|
||||
// Extension items
|
||||
BOOL isNeedVideoCall = [TUIChatConfig defaultConfig].enableVideoCall && conversationModel.enableVideoCall;
|
||||
BOOL isNeedAudioCall = [TUIChatConfig defaultConfig].enableAudioCall && conversationModel.enableAudioCall;
|
||||
BOOL isNeedRoom = [TUIChatConfig defaultConfig].showRoomButton && conversationModel.enableRoom;
|
||||
BOOL isNeedPoll = [TUIChatConfig defaultConfig].showPollButton && conversationModel.enablePoll;
|
||||
BOOL isNeedGroupNote = [TUIChatConfig defaultConfig].showGroupNoteButton && conversationModel.enableGroupNote;
|
||||
|
||||
NSMutableDictionary *extensionParam = [NSMutableDictionary dictionary];
|
||||
if (userID.length > 0) {
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_UserID] = userID;
|
||||
} else if (groupID.length > 0) {
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_GroupID] = groupID;
|
||||
}
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_FilterVideoCall] = @(!isNeedVideoCall);
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_FilterAudioCall] = @(!isNeedAudioCall);
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_FilterRoom] = @(!isNeedRoom);
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_FilterPoll] = @(!isNeedPoll);
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_FilterGroupNote] = @(!isNeedGroupNote);
|
||||
extensionParam[TUICore_TUIChatExtension_InputViewMoreItem_ActionVC] = actionController;
|
||||
NSArray *extensionList = [TUICore getExtensionList:TUICore_TUIChatExtension_InputViewMoreItem_ClassicExtensionID param:extensionParam];
|
||||
for (TUIExtensionInfo *info in extensionList) {
|
||||
NSAssert(info.icon && info.text && info.onClicked, @"extension for input view is invalid, check icon/text/onclick");
|
||||
if (info.icon && info.text && info.onClicked) {
|
||||
TUIInputMoreCellData *data = [[TUIInputMoreCellData alloc] init];
|
||||
data.priority = info.weight;
|
||||
data.image = info.icon;
|
||||
data.title = info.text;
|
||||
data.onClicked = info.onClicked;
|
||||
[moreMenus addObject:data];
|
||||
}
|
||||
}
|
||||
|
||||
// Customized items
|
||||
if (conversationModel.customizedNewItemsInMoreMenu.count > 0) {
|
||||
[moreMenus addObjectsFromArray:conversationModel.customizedNewItemsInMoreMenu];
|
||||
}
|
||||
|
||||
// Sort with priority
|
||||
NSArray *sortedMenus = [moreMenus sortedArrayUsingComparator:^NSComparisonResult(TUIInputMoreCellData *obj1, TUIInputMoreCellData *obj2) {
|
||||
return obj1.priority > obj2.priority ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
return [NSMutableArray arrayWithArray:sortedMenus];
|
||||
}
|
||||
|
||||
// For Minimalist Edition.
|
||||
- (NSArray<TUICustomActionSheetItem *> *)getInputMoreActionItemList:(NSString *)userID
|
||||
groupID:(NSString *)groupID
|
||||
conversationModel:(TUIChatConversationModel *)conversationModel
|
||||
pushVC:(UINavigationController *)pushVC
|
||||
actionController:(id<TIMInputViewMoreActionProtocol>)actionController {
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
[result addObjectsFromArray:[self createBuiltInInputMoreActionItemList:conversationModel]];
|
||||
[result addObjectsFromArray:[self createCustomInputMoreActionItemList:conversationModel]];
|
||||
|
||||
// Extension items
|
||||
NSMutableArray<TUICustomActionSheetItem *> *items = [NSMutableArray array];
|
||||
NSMutableDictionary *param = [NSMutableDictionary dictionary];
|
||||
if (userID.length > 0) {
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_UserID] = userID;
|
||||
} else if (groupID.length > 0) {
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_GroupID] = groupID;
|
||||
}
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_FilterVideoCall] = @(!TUIChatConfig.defaultConfig.enableVideoCall);
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_FilterAudioCall] = @(!TUIChatConfig.defaultConfig.enableAudioCall);
|
||||
if (pushVC) {
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_PushVC] = pushVC;
|
||||
}
|
||||
param[TUICore_TUIChatExtension_InputViewMoreItem_ActionVC] = actionController;
|
||||
NSArray *extensionList = [TUICore getExtensionList:TUICore_TUIChatExtension_InputViewMoreItem_MinimalistExtensionID param:param];
|
||||
for (TUIExtensionInfo *info in extensionList) {
|
||||
if (info.icon && info.text && info.onClicked) {
|
||||
TUICustomActionSheetItem *item = [[TUICustomActionSheetItem alloc] initWithTitle:info.text
|
||||
leftMark:info.icon
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
info.onClicked(param);
|
||||
}];
|
||||
item.priority = info.weight;
|
||||
[items addObject:item];
|
||||
}
|
||||
}
|
||||
if (items.count > 0) {
|
||||
[result addObjectsFromArray:items];
|
||||
}
|
||||
|
||||
// Sort with priority
|
||||
NSArray *sorted = [result sortedArrayUsingComparator:^NSComparisonResult(TUICustomActionSheetItem *obj1, TUICustomActionSheetItem *obj2) {
|
||||
return obj1.priority > obj2.priority ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
return sorted;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
- (void)onChangeLanguage {
|
||||
self.customInputMoreActionItemList = nil;
|
||||
self.builtInInputMoreActionItemList = nil;
|
||||
}
|
||||
|
||||
#pragma mark -- Classic
|
||||
- (NSArray<TUIInputMoreCellData *> *)createBuiltInInputMoreMenusWithConversationModel:(TUIChatConversationModel *)conversationModel {
|
||||
|
||||
BOOL isNeedRecordVideo = [TUIChatConfig defaultConfig].showRecordVideoButton && conversationModel.enableRecordVideo;
|
||||
BOOL isNeedTakePhoto = [TUIChatConfig defaultConfig].showTakePhotoButton && conversationModel.enableTakePhoto;
|
||||
BOOL isNeedAlbum = [TUIChatConfig defaultConfig].showAlbumButton && conversationModel.enableAlbum;
|
||||
BOOL isNeedFile = [TUIChatConfig defaultConfig].showFileButton && conversationModel.enableFile;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
TUIInputMoreCellData *albumData = [[TUIInputMoreCellData alloc] init];
|
||||
albumData.priority = 1000;
|
||||
albumData.title = TIMCommonLocalizableString(TUIKitMorePhoto);
|
||||
albumData.image = TUIChatBundleThemeImage(@"chat_more_picture_img", @"more_picture");
|
||||
albumData.onClicked = ^(NSDictionary *actionParam) {
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(onSelectPhotoMoreCellData)]) {
|
||||
[weakSelf.delegate onSelectPhotoMoreCellData];
|
||||
}
|
||||
};
|
||||
|
||||
TUIInputMoreCellData *takePictureData = [[TUIInputMoreCellData alloc] init];
|
||||
takePictureData.priority = 900;
|
||||
takePictureData.title = TIMCommonLocalizableString(TUIKitMoreCamera);
|
||||
takePictureData.image = TUIChatBundleThemeImage(@"chat_more_camera_img", @"more_camera");
|
||||
takePictureData.onClicked = ^(NSDictionary *actionParam) {
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(onTakePictureMoreCellData)]) {
|
||||
[weakSelf.delegate onTakePictureMoreCellData];
|
||||
}
|
||||
};
|
||||
|
||||
TUIInputMoreCellData *videoData = [[TUIInputMoreCellData alloc] init];
|
||||
videoData.priority = 800;
|
||||
videoData.title = TIMCommonLocalizableString(TUIKitMoreVideo);
|
||||
videoData.image = TUIChatBundleThemeImage(@"chat_more_video_img", @"more_video");
|
||||
videoData.onClicked = ^(NSDictionary *actionParam) {
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(onTakeVideoMoreCellData)]) {
|
||||
[weakSelf.delegate onTakeVideoMoreCellData];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TUIInputMoreCellData *fileData = [[TUIInputMoreCellData alloc] init];
|
||||
fileData.priority = 700;
|
||||
fileData.title = TIMCommonLocalizableString(TUIKitMoreFile);
|
||||
fileData.image = TUIChatBundleThemeImage(@"chat_more_file_img", @"more_file");
|
||||
fileData.onClicked = ^(NSDictionary *actionParam) {
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(onSelectFileMoreCellData)]) {
|
||||
[weakSelf.delegate onSelectFileMoreCellData];
|
||||
}
|
||||
};
|
||||
|
||||
NSMutableArray *formatArray = [NSMutableArray array];
|
||||
if (isNeedAlbum) {
|
||||
[formatArray addObject:albumData];
|
||||
}
|
||||
if (isNeedTakePhoto) {
|
||||
[formatArray addObject:takePictureData];
|
||||
}
|
||||
if (isNeedRecordVideo) {
|
||||
[formatArray addObject:videoData];
|
||||
}
|
||||
|
||||
if (isNeedFile) {
|
||||
[formatArray addObject:fileData];
|
||||
}
|
||||
_builtInInputMoreMenus = [NSArray arrayWithArray:formatArray];
|
||||
|
||||
return _builtInInputMoreMenus;
|
||||
}
|
||||
|
||||
#pragma mark -- Minimalist
|
||||
- (NSArray<TUICustomActionSheetItem *> *)createBuiltInInputMoreActionItemList:(TUIChatConversationModel *)model {
|
||||
if (_builtInInputMoreActionItemList == nil) {
|
||||
self.builtInInputMoreActionItemList = [NSMutableArray array];
|
||||
|
||||
BOOL showTakePhoto = [TUIChatConfig defaultConfig].showTakePhotoButton && model.enableTakePhoto;
|
||||
BOOL showAlbum = [TUIChatConfig defaultConfig].showAlbumButton && model.enableAlbum;
|
||||
BOOL showRecordVideo = [TUIChatConfig defaultConfig].showRecordVideoButton && model.enableRecordVideo;
|
||||
BOOL showFile = [TUIChatConfig defaultConfig].showFileButton && model.enableFile;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
if (showAlbum) {
|
||||
TUICustomActionSheetItem *album =
|
||||
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMorePhoto)
|
||||
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_photo")]
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(onSelectPhotoMoreCellData)]) {
|
||||
[weakSelf.delegate onSelectPhotoMoreCellData];
|
||||
}
|
||||
}];
|
||||
album.priority = 1000;
|
||||
[self.builtInInputMoreActionItemList addObject:album];
|
||||
}
|
||||
|
||||
if (showTakePhoto) {
|
||||
TUICustomActionSheetItem *takePhoto =
|
||||
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMoreCamera)
|
||||
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_camera")]
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(onTakePictureMoreCellData)]) {
|
||||
[weakSelf.delegate onTakePictureMoreCellData];
|
||||
}
|
||||
}];
|
||||
takePhoto.priority = 900;
|
||||
[self.builtInInputMoreActionItemList addObject:takePhoto];
|
||||
}
|
||||
|
||||
if (showRecordVideo) {
|
||||
TUICustomActionSheetItem *recordVideo =
|
||||
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMoreVideo)
|
||||
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_video")]
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(onTakeVideoMoreCellData)]) {
|
||||
[weakSelf.delegate onTakeVideoMoreCellData];
|
||||
}
|
||||
}];
|
||||
recordVideo.priority = 800;
|
||||
[self.builtInInputMoreActionItemList addObject:recordVideo];
|
||||
}
|
||||
|
||||
if (showFile) {
|
||||
TUICustomActionSheetItem *file =
|
||||
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMoreFile)
|
||||
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_document")]
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(onSelectFileMoreCellData)]) {
|
||||
[weakSelf.delegate onSelectFileMoreCellData];
|
||||
}
|
||||
}];
|
||||
file.priority = 700;
|
||||
[self.builtInInputMoreActionItemList addObject:file];
|
||||
}
|
||||
}
|
||||
return self.builtInInputMoreActionItemList;
|
||||
}
|
||||
|
||||
- (NSArray<TUICustomActionSheetItem *> *)createCustomInputMoreActionItemList:(TUIChatConversationModel *)model {
|
||||
if (_customInputMoreActionItemList == nil) {
|
||||
NSMutableArray *arrayM = [NSMutableArray array];
|
||||
|
||||
BOOL showCustom = [TUIChatConfig defaultConfig].enableWelcomeCustomMessage && model.enableWelcomeCustomMessage;
|
||||
if (showCustom) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
TUICustomActionSheetItem *link =
|
||||
[[TUICustomActionSheetItem alloc] initWithTitle:TIMCommonLocalizableString(TUIKitMoreLink)
|
||||
leftMark:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_more_custom")]
|
||||
withActionHandler:^(UIAlertAction *_Nonnull action) {
|
||||
link.priority = 100;
|
||||
NSString *text = TIMCommonLocalizableString(TUIKitWelcome);
|
||||
NSString *link = TUITencentCloudHomePageEN;
|
||||
NSString *language = [TUIGlobalization tk_localizableLanguageKey];
|
||||
if ([language tui_containsString:@"zh-"]) {
|
||||
link = TUITencentCloudHomePageCN;
|
||||
}
|
||||
NSError *error = nil;
|
||||
NSDictionary *param = @{BussinessID : BussinessID_TextLink, @"text" : text, @"link" : link};
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:param options:0 error:&error];
|
||||
if (error) {
|
||||
NSLog(@"[%@] Post Json Error", [self class]);
|
||||
return;
|
||||
}
|
||||
V2TIMMessage *message = [TUIMessageDataProvider getCustomMessageWithJsonData:data desc:text extension:text];
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(dataProvider:sendMessage:)]) {
|
||||
[weakSelf.delegate dataProvider:weakSelf sendMessage:message];
|
||||
}
|
||||
}];
|
||||
[arrayM addObject:link];
|
||||
}
|
||||
|
||||
if (model.customizedNewItemsInMoreMenu.count > 0) {
|
||||
[arrayM addObjectsFromArray:model.customizedNewItemsInMoreMenu];
|
||||
}
|
||||
_customInputMoreActionItemList = [NSArray arrayWithArray:arrayM];
|
||||
}
|
||||
return _customInputMoreActionItemList;
|
||||
}
|
||||
|
||||
#pragma mark -- Override
|
||||
- (NSString *)abstractDisplayWithMessage:(V2TIMMessage *)msg {
|
||||
NSString *desc = @"";
|
||||
if (msg.nickName.length > 0) {
|
||||
desc = msg.nickName;
|
||||
} else if (msg.sender.length > 0) {
|
||||
desc = msg.sender;
|
||||
}
|
||||
NSString *display = [self.delegate dataProvider:self mergeForwardMsgAbstactForMessage:msg];
|
||||
|
||||
if (display.length == 0) {
|
||||
display = [self.class parseAbstractDisplayWStringFromMessageElement:msg];
|
||||
}
|
||||
NSString * splitStr = @":";
|
||||
splitStr = @"\u202C:";
|
||||
|
||||
NSString *nameFormat = [desc stringByAppendingFormat:@"%@", splitStr];
|
||||
return [self.class alignEmojiStringWithUserName:nameFormat
|
||||
text:display];
|
||||
}
|
||||
|
||||
+ (nullable NSString *)parseAbstractDisplayWStringFromMessageElement:(V2TIMMessage *)message {
|
||||
NSString *str = nil;
|
||||
if (message.elemType == V2TIM_ELEM_TYPE_TEXT) {
|
||||
NSString *content = message.textElem.text;
|
||||
str = content;
|
||||
}
|
||||
else {
|
||||
str = [TUIMessageDataProvider getDisplayString:message];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
+ (NSString *)alignEmojiStringWithUserName:(NSString *)userName text:(NSString *)text {
|
||||
NSArray *textList = [self.class splitEmojiText:text];
|
||||
NSInteger forwardMsgLength = 98;
|
||||
NSMutableString *sb = [NSMutableString string];
|
||||
[sb appendString:userName];
|
||||
NSInteger length = userName.length;
|
||||
for (NSString *textItem in textList) {
|
||||
BOOL isFaceChar = [self.class isFaceStrKey:textItem];
|
||||
if (isFaceChar) {
|
||||
if (length + textItem.length < forwardMsgLength) {
|
||||
[sb appendString:textItem];
|
||||
length += textItem.length;
|
||||
} else {
|
||||
[sb appendString:@"..."];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (length + textItem.length < forwardMsgLength) {
|
||||
[sb appendString:textItem];
|
||||
length += textItem.length;
|
||||
} else {
|
||||
[sb appendString:textItem];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
+ (BOOL)isFaceStrKey:(NSString*) strkey {
|
||||
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
|
||||
NSArray <TUIFaceGroup *> * groups = service.getFaceGroup;
|
||||
if ([groups.firstObject.facesMap objectForKey:strkey] != nil) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)splitEmojiText:(NSString *)text {
|
||||
NSString *regex = @"\\[(\\S+?)\\]";
|
||||
NSRegularExpression *regexExp = [NSRegularExpression regularExpressionWithPattern:regex options:0 error:nil];
|
||||
NSArray<NSTextCheckingResult *> *matches = [regexExp matchesInString:text options:0 range:NSMakeRange(0, text.length)];
|
||||
NSMutableArray<TUISplitEmojiData *> *emojiDataList = [NSMutableArray array];
|
||||
NSInteger lastMentionIndex = -1;
|
||||
for (NSTextCheckingResult *match in matches) {
|
||||
NSString *emojiKey = [text substringWithRange:match.range];
|
||||
NSInteger start;
|
||||
if (lastMentionIndex != -1) {
|
||||
start = [text rangeOfString:emojiKey options:0 range:NSMakeRange(lastMentionIndex, text.length - lastMentionIndex)].location;
|
||||
} else {
|
||||
start = [text rangeOfString:emojiKey].location;
|
||||
}
|
||||
NSInteger end = start + emojiKey.length;
|
||||
lastMentionIndex = end;
|
||||
|
||||
|
||||
if (![self.class isFaceStrKey:emojiKey]) {
|
||||
continue;
|
||||
}
|
||||
TUISplitEmojiData *emojiData = [[TUISplitEmojiData alloc] init];
|
||||
emojiData.start = start;
|
||||
emojiData.end = end;
|
||||
[emojiDataList addObject:emojiData];
|
||||
}
|
||||
NSMutableArray<NSString *> *stringList = [NSMutableArray array];
|
||||
NSInteger offset = 0;
|
||||
for (TUISplitEmojiData *emojiData in emojiDataList) {
|
||||
NSInteger start = emojiData.start - offset;
|
||||
NSInteger end = emojiData.end - offset;
|
||||
NSString *startStr = [text substringToIndex:start];
|
||||
NSString *middleStr = [text substringWithRange:NSMakeRange(start, end - start)];
|
||||
text = [text substringFromIndex:end];
|
||||
if (startStr.length > 0) {
|
||||
[stringList addObject:startStr];
|
||||
}
|
||||
[stringList addObject:middleStr];
|
||||
offset += startStr.length + middleStr.length;
|
||||
}
|
||||
if (text.length > 0) {
|
||||
[stringList addObject:text];
|
||||
}
|
||||
return stringList;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
- (TUIInputMoreCellData *)welcomeInputMoreMenu {
|
||||
if (!_welcomeInputMoreMenu) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_welcomeInputMoreMenu = [[TUIInputMoreCellData alloc] init];
|
||||
_welcomeInputMoreMenu.priority = 0;
|
||||
_welcomeInputMoreMenu.title = TIMCommonLocalizableString(TUIKitMoreLink);
|
||||
_welcomeInputMoreMenu.image = TUIChatBundleThemeImage(@"chat_more_link_img", @"chat_more_link_img");
|
||||
_welcomeInputMoreMenu.onClicked = ^(NSDictionary *actionParam) {
|
||||
NSString *text = TIMCommonLocalizableString(TUIKitWelcome);
|
||||
NSString *link = TUITencentCloudHomePageEN;
|
||||
NSString *language = [TUIGlobalization tk_localizableLanguageKey];
|
||||
if ([language tui_containsString:@"zh-"]) {
|
||||
link = TUITencentCloudHomePageCN;
|
||||
}
|
||||
NSError *error = nil;
|
||||
NSDictionary *param = @{BussinessID : BussinessID_TextLink, @"text" : text, @"link" : link};
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:param options:0 error:&error];
|
||||
if (error) {
|
||||
NSLog(@"[%@] Post Json Error", [weakSelf class]);
|
||||
return;
|
||||
}
|
||||
V2TIMMessage *message = [TUIMessageDataProvider getCustomMessageWithJsonData:data desc:text extension:text];
|
||||
if ([weakSelf.delegate respondsToSelector:@selector(dataProvider:sendMessage:)]) {
|
||||
[weakSelf.delegate dataProvider:weakSelf sendMessage:message];
|
||||
}
|
||||
};
|
||||
}
|
||||
return _welcomeInputMoreMenu;
|
||||
}
|
||||
|
||||
- (NSMutableArray<TUIInputMoreCellData *> *)customInputMoreMenus {
|
||||
if (!_customInputMoreMenus) {
|
||||
_customInputMoreMenus = [NSMutableArray array];
|
||||
}
|
||||
return _customInputMoreMenus;
|
||||
}
|
||||
|
||||
- (NSArray<TUIInputMoreCellData *> *)builtInInputMoreMenus {
|
||||
if (_builtInInputMoreMenus == nil) {
|
||||
return [self createBuiltInInputMoreMenusWithConversationModel:nil];
|
||||
}
|
||||
return _builtInInputMoreMenus;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// TUIGroupNoticeDataProvider.h
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ImSDK_Plus/ImSDK_Plus.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIGroupNoticeDataProvider : NSObject
|
||||
|
||||
@property(nonatomic, strong, readonly) V2TIMGroupInfo *groupInfo;
|
||||
@property(nonatomic, copy) NSString *groupID;
|
||||
|
||||
- (void)getGroupInfo:(dispatch_block_t)callback;
|
||||
- (BOOL)canEditNotice;
|
||||
- (void)updateNotice:(NSString *)notice callback:(void (^)(int, NSString *))callback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// TUIGroupNoticeDataProvider.m
|
||||
// TUIGroup
|
||||
//
|
||||
// Created by harvy on 2022/1/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupNoticeDataProvider.h"
|
||||
|
||||
@interface TUIGroupNoticeDataProvider ()
|
||||
|
||||
@property(nonatomic, strong) V2TIMGroupInfo *groupInfo;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIGroupNoticeDataProvider
|
||||
|
||||
- (void)getGroupInfo:(dispatch_block_t)callback {
|
||||
if (self.groupInfo && [self.groupInfo.groupID isEqual:self.groupID]) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[V2TIMManager.sharedInstance getGroupsInfo:@[ self.groupID ?: @"" ]
|
||||
succ:^(NSArray<V2TIMGroupInfoResult *> *groupResultList) {
|
||||
V2TIMGroupInfoResult *result = groupResultList.firstObject;
|
||||
if (result && result.resultCode == 0) {
|
||||
weakSelf.groupInfo = result.info;
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)canEditNotice {
|
||||
return self.groupInfo.role == V2TIM_GROUP_MEMBER_ROLE_ADMIN || self.groupInfo.role == V2TIM_GROUP_MEMBER_ROLE_SUPER;
|
||||
}
|
||||
|
||||
- (void)updateNotice:(NSString *)notice callback:(void (^)(int, NSString *))callback {
|
||||
V2TIMGroupInfo *info = [[V2TIMGroupInfo alloc] init];
|
||||
info.groupID = self.groupID;
|
||||
info.notification = notice;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[V2TIMManager.sharedInstance setGroupInfo:info
|
||||
succ:^{
|
||||
if (callback) {
|
||||
callback(0, nil);
|
||||
}
|
||||
|
||||
[weakSelf sendNoticeMessage:notice];
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
if (callback) {
|
||||
callback(code, desc);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)sendNoticeMessage:(NSString *)notice {
|
||||
if (notice.length == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// TUIMessageDataProvider+MessageDeal.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/3/22.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageDataProvider.h"
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMessageDataProvider (MessageDeal)
|
||||
- (void)loadOriginMessageFromReplyData:(TUIReplyMessageCellData *)replycellData dealCallback:(void (^)(void))callback;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// TUIMessageDataProvider+MessageDeal.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/3/22.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import "TUIChatDataProvider.h"
|
||||
#import "TUIImageMessageCellData.h"
|
||||
#import "TUIMessageDataProvider+MessageDeal.h"
|
||||
#import "TUIMessageDataProvider.h"
|
||||
@implementation TUIMessageDataProvider (MessageDeal)
|
||||
|
||||
- (void)loadOriginMessageFromReplyData:(TUIReplyMessageCellData *)replycellData dealCallback:(void (^)(void))callback {
|
||||
if (replycellData.originMsgID.length == 0) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@weakify(replycellData)[TUIChatDataProvider findMessages:@[ replycellData.originMsgID ]
|
||||
callback:^(BOOL succ, NSString *_Nonnull error_message, NSArray *_Nonnull msgs) {
|
||||
@strongify(replycellData) if (!succ) {
|
||||
replycellData.quoteData = [replycellData getQuoteData:nil];
|
||||
replycellData.originMessage = nil;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
V2TIMMessage *originMessage = msgs.firstObject;
|
||||
if (originMessage == nil) {
|
||||
replycellData.quoteData = [replycellData getQuoteData:nil];
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
TUIMessageCellData *cellData = [TUIMessageDataProvider getCellData:originMessage];
|
||||
replycellData.originCellData = cellData;
|
||||
if ([cellData isKindOfClass:TUIImageMessageCellData.class]) {
|
||||
TUIImageMessageCellData *imageData = (TUIImageMessageCellData *)cellData;
|
||||
[imageData downloadImage:TImage_Type_Thumb];
|
||||
replycellData.quoteData = [replycellData getQuoteData:imageData];
|
||||
replycellData.originMessage = originMessage;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else if ([cellData isKindOfClass:TUIVideoMessageCellData.class]) {
|
||||
TUIVideoMessageCellData *videoData = (TUIVideoMessageCellData *)cellData;
|
||||
[videoData downloadThumb];
|
||||
replycellData.quoteData = [replycellData getQuoteData:videoData];
|
||||
replycellData.originMessage = originMessage;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
replycellData.quoteData = [replycellData getQuoteData:cellData];
|
||||
replycellData.originMessage = originMessage;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TUIMessageBaseDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TUITextMessageCellData;
|
||||
@class TUIFaceMessageCellData;
|
||||
@class TUIImageMessageCellData;
|
||||
@class TUIVoiceMessageCellData;
|
||||
@class TUIVideoMessageCellData;
|
||||
@class TUIFileMessageCellData;
|
||||
@class TUISystemMessageCellData;
|
||||
@class TUIChatCallingDataProvider;
|
||||
|
||||
@protocol TUIMessageDataProviderDataSource <TUIMessageBaseDataProviderDataSource>
|
||||
|
||||
+ (nullable Class)onGetCustomMessageCellDataClass:(NSString *)businessID;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIMessageDataProvider : TUIMessageBaseDataProvider
|
||||
|
||||
+ (void)setDataSourceClass:(Class<TUIMessageDataProviderDataSource>)dataSourceClass;
|
||||
|
||||
#pragma mark - TUIMessageCellData parser
|
||||
+ (nullable TUIMessageCellData *)getCellData:(V2TIMMessage *)message;
|
||||
|
||||
#pragma mark - Last message parser
|
||||
+ (void)asyncGetDisplayString:(NSArray<V2TIMMessage *> *)messageList callback:(void(^)(NSDictionary<NSString *, NSString *> *))callback;
|
||||
+ (nullable NSString *)getDisplayString:(V2TIMMessage *)message;
|
||||
|
||||
#pragma mark - Data source operate
|
||||
- (void)processQuoteMessage:(NSArray<TUIMessageCellData *> *)uiMsgs;
|
||||
- (void)deleteUIMsgs:(NSArray<TUIMessageCellData *> *)uiMsgs SuccBlock:(nullable V2TIMSucc)succ FailBlock:(nullable V2TIMFail)fail;
|
||||
- (void)removeUIMsgList:(NSArray<TUIMessageCellData *> *)cellDatas;
|
||||
|
||||
|
||||
+ (TUIChatCallingDataProvider *)callingDataProvider;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
754
TUIKit/TUIChat/BaseDataProvider/Impl/TUIMessageDataProvider.m
Normal file
754
TUIKit/TUIChat/BaseDataProvider/Impl/TUIMessageDataProvider.m
Normal file
@@ -0,0 +1,754 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMessageDataProvider.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <TIMCommon/TUIRelationUserModel.h>
|
||||
#import <TIMCommon/TUISystemMessageCellData.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUIChatCallingDataProvider.h"
|
||||
#import "TUICloudCustomDataTypeCenter.h"
|
||||
#import "TUIFaceMessageCellData.h"
|
||||
#import "TUIFileMessageCellData.h"
|
||||
#import "TUIImageMessageCellData.h"
|
||||
#import "TUIJoinGroupMessageCellData.h"
|
||||
#import "TUIMergeMessageCellData.h"
|
||||
#import "TUIMessageDataProvider+MessageDeal.h"
|
||||
#import "TUIMessageProgressManager.h"
|
||||
#import "TUIReplyMessageCellData.h"
|
||||
#import "TUITextMessageCellData.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
#import "TUIVoiceMessageCellData.h"
|
||||
|
||||
/**
|
||||
* The maximum editable time after the message is recalled, default is (2 * 60)
|
||||
*/
|
||||
#define MaxReEditMessageDelay 2 * 60
|
||||
|
||||
#define kIsCustomMessageFromPlugin @"kIsCustomMessageFromPlugin"
|
||||
|
||||
static Class<TUIMessageDataProviderDataSource> gDataSourceClass = nil;
|
||||
|
||||
@implementation TUIMessageDataProvider
|
||||
|
||||
#pragma mark - Life cycle
|
||||
|
||||
- (void)dealloc {
|
||||
gCallingDataProvider = nil;
|
||||
}
|
||||
|
||||
+ (void)setDataSourceClass:(Class<TUIMessageDataProviderDataSource>)dataSourceClass {
|
||||
gDataSourceClass = dataSourceClass;
|
||||
}
|
||||
|
||||
#pragma mark - TUIMessageCellData parser
|
||||
+ (nullable TUIMessageCellData *)getCellData:(V2TIMMessage *)message {
|
||||
// 1 Parse cell data
|
||||
TUIMessageCellData *data = [self parseMessageCellDataFromMessageStatus:message];
|
||||
if (data == nil) {
|
||||
data = [self parseMessageCellDataFromMessageCustomData:message];
|
||||
}
|
||||
if (data == nil) {
|
||||
data = [self parseMessageCellDataFromMessageElement:message];
|
||||
}
|
||||
|
||||
// 2 Fill in property if needed
|
||||
if (data) {
|
||||
[self fillPropertyToCellData:data ofMessage:message];
|
||||
} else {
|
||||
NSLog(@"current message will be ignored in chat page, msg:%@", message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (nullable TUIMessageCellData *)parseMessageCellDataFromMessageStatus:(V2TIMMessage *)message {
|
||||
TUIMessageCellData *data = nil;
|
||||
if (message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) {
|
||||
data = [TUIMessageDataProvider getRevokeCellData:message];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (nullable TUIMessageCellData *)parseMessageCellDataFromMessageCustomData:(V2TIMMessage *)message {
|
||||
TUIMessageCellData *data = nil;
|
||||
if ([message isContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReply]) {
|
||||
/**
|
||||
* Determine whether to include "reply-message"
|
||||
*/
|
||||
data = [TUIReplyMessageCellData getCellData:message];
|
||||
} else if ([message isContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReference]) {
|
||||
/**
|
||||
* Determine whether to include "quote-message"
|
||||
*/
|
||||
data = [TUIReferenceMessageCellData getCellData:message];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (nullable TUIMessageCellData *)parseMessageCellDataFromMessageElement:(V2TIMMessage *)message {
|
||||
TUIMessageCellData *data = nil;
|
||||
switch (message.elemType) {
|
||||
case V2TIM_ELEM_TYPE_TEXT: {
|
||||
data = [TUITextMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_IMAGE: {
|
||||
data = [TUIImageMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_SOUND: {
|
||||
data = [TUIVoiceMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_VIDEO: {
|
||||
data = [TUIVideoMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_FILE: {
|
||||
data = [TUIFileMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_FACE: {
|
||||
data = [TUIFaceMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_GROUP_TIPS: {
|
||||
data = [self getSystemCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_MERGER: {
|
||||
data = [TUIMergeMessageCellData getCellData:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_CUSTOM: {
|
||||
data = [self getCustomMessageCellData:message];
|
||||
} break;
|
||||
default:
|
||||
data = [self getUnsupportedCellData:message];
|
||||
break;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (void)fillPropertyToCellData:(TUIMessageCellData *)data ofMessage:(V2TIMMessage *)message {
|
||||
data.innerMessage = message;
|
||||
if (message.groupID.length > 0 && !message.isSelf && ![data isKindOfClass:[TUISystemMessageCellData class]]) {
|
||||
data.showName = YES;
|
||||
}
|
||||
switch (message.status) {
|
||||
case V2TIM_MSG_STATUS_SEND_SUCC:
|
||||
data.status = Msg_Status_Succ;
|
||||
break;
|
||||
case V2TIM_MSG_STATUS_SEND_FAIL:
|
||||
data.status = Msg_Status_Fail;
|
||||
break;
|
||||
case V2TIM_MSG_STATUS_SENDING:
|
||||
data.status = Msg_Status_Sending_2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress of message uploading/downloading
|
||||
*/
|
||||
{
|
||||
NSInteger uploadProgress = [TUIMessageProgressManager.shareManager uploadProgressForMessage:message.msgID];
|
||||
NSInteger downloadProgress = [TUIMessageProgressManager.shareManager downloadProgressForMessage:message.msgID];
|
||||
if ([data conformsToProtocol:@protocol(TUIMessageCellDataFileUploadProtocol)]) {
|
||||
((id<TUIMessageCellDataFileUploadProtocol>)data).uploadProgress = uploadProgress;
|
||||
}
|
||||
if ([data conformsToProtocol:@protocol(TUIMessageCellDataFileDownloadProtocol)]) {
|
||||
((id<TUIMessageCellDataFileDownloadProtocol>)data).downladProgress = downloadProgress;
|
||||
((id<TUIMessageCellDataFileDownloadProtocol>)data).isDownloading = (downloadProgress != 0) && (downloadProgress != 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to include "replies-message"
|
||||
*/
|
||||
if ([message isContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReplies]) {
|
||||
[message doThingsInContainsCloudCustomOfDataType:TUICloudCustomDataType_MessageReplies
|
||||
callback:^(BOOL isContains, id obj) {
|
||||
if (isContains) {
|
||||
if ([data isKindOfClass:TUISystemMessageCellData.class] ||
|
||||
[data isKindOfClass:TUIJoinGroupMessageCellData.class]) {
|
||||
data.showMessageModifyReplies = NO;
|
||||
} else {
|
||||
data.showMessageModifyReplies = YES;
|
||||
}
|
||||
if (obj && [obj isKindOfClass:NSDictionary.class]) {
|
||||
NSDictionary *dic = (NSDictionary *)obj;
|
||||
NSString *typeStr =
|
||||
[TUICloudCustomDataTypeCenter convertType2String:TUICloudCustomDataType_MessageReplies];
|
||||
NSDictionary *messageReplies = [dic valueForKey:typeStr];
|
||||
NSArray *repliesArr = [messageReplies valueForKey:@"replies"];
|
||||
if ([repliesArr isKindOfClass:NSArray.class]) {
|
||||
data.messageModifyReplies = repliesArr.copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable TUIMessageCellData *)getCustomMessageCellData:(V2TIMMessage *)message {
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// **The compatible processing logic of TUICallKit will be removed after***************
|
||||
// **TUICallKit is connected according to the standard process. ***********************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
TUIMessageCellData *data = nil;
|
||||
id<TUIChatCallingInfoProtocol> callingInfo = nil;
|
||||
if ([self.callingDataProvider isCallingMessage:message callingInfo:&callingInfo]) {
|
||||
// Voice-video-call message
|
||||
if (callingInfo) {
|
||||
// Supported voice-video-call message
|
||||
if (callingInfo.excludeFromHistory) {
|
||||
// This message will be ignore in chat page
|
||||
data = nil;
|
||||
} else {
|
||||
data = [self getCallingCellData:callingInfo];
|
||||
if (data == nil) {
|
||||
data = [self getUnsupportedCellData:message];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unsupported voice-video-call message
|
||||
data = [self getUnsupportedCellData:message];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
|
||||
NSString *businessID = nil;
|
||||
BOOL excludeFromHistory = NO;
|
||||
|
||||
V2TIMSignalingInfo *signalingInfo = [V2TIMManager.sharedInstance getSignallingInfo:message];
|
||||
if (signalingInfo) {
|
||||
// This message is signaling message
|
||||
BOOL isOnlineOnly = NO;
|
||||
@try {
|
||||
isOnlineOnly = [[message valueForKeyPath:@"message.IsOnlineOnly"] boolValue];
|
||||
} @catch (NSException *exception) {
|
||||
isOnlineOnly = NO;
|
||||
}
|
||||
excludeFromHistory = isOnlineOnly || (message.isExcludedFromLastMessage && message.isExcludedFromUnreadCount);
|
||||
businessID = [self getSignalingBusinessID:signalingInfo];
|
||||
} else {
|
||||
// This message is normal custom message
|
||||
excludeFromHistory = NO;
|
||||
businessID = [self getCustomBusinessID:message];
|
||||
}
|
||||
|
||||
if (excludeFromHistory) {
|
||||
// Return nil means not display in the chat page
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (businessID.length > 0) {
|
||||
Class cellDataClass = nil;
|
||||
if (gDataSourceClass && [gDataSourceClass respondsToSelector:@selector(onGetCustomMessageCellDataClass:)]) {
|
||||
cellDataClass = [gDataSourceClass onGetCustomMessageCellDataClass:businessID];
|
||||
}
|
||||
if (cellDataClass && [cellDataClass respondsToSelector:@selector(getCellData:)]) {
|
||||
TUIMessageCellData *data = [cellDataClass getCellData:message];
|
||||
if (data.shouldHide) {
|
||||
return nil;
|
||||
} else {
|
||||
data.reuseId = businessID;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
// In CustomerService scenarios, unsupported messages are not displayed directly.
|
||||
if ([businessID tui_containsString:BussinessID_CustomerService]) {
|
||||
return nil;
|
||||
}
|
||||
return [self getUnsupportedCellData:message];
|
||||
} else {
|
||||
return [self getUnsupportedCellData:message];
|
||||
}
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)getUnsupportedCellData:(V2TIMMessage *)message {
|
||||
TUITextMessageCellData *cellData = [[TUITextMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
cellData.content = TIMCommonLocalizableString(TUIKitNotSupportThisMessage);
|
||||
cellData.reuseId = TTextMessageCell_ReuseId;
|
||||
return cellData;
|
||||
}
|
||||
|
||||
+ (nullable TUISystemMessageCellData *)getSystemCellData:(V2TIMMessage *)message {
|
||||
V2TIMGroupTipsElem *tip = message.groupTipsElem;
|
||||
NSString *opUserName = [self getOpUserName:tip.opMember];
|
||||
NSMutableArray<NSString *> *userNameList = [self getUserNameList:tip.memberList];
|
||||
NSMutableArray<NSString *> *userIDList = [self getUserIDList:tip.memberList];
|
||||
if (tip.type == V2TIM_GROUP_TIPS_TYPE_JOIN || tip.type == V2TIM_GROUP_TIPS_TYPE_INVITE || tip.type == V2TIM_GROUP_TIPS_TYPE_KICKED ||
|
||||
tip.type == V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE || tip.type == V2TIM_GROUP_TIPS_TYPE_QUIT ||
|
||||
tip.type == V2TIM_GROUP_TIPS_TYPE_PINNED_MESSAGE_ADDED || tip.type == V2TIM_GROUP_TIPS_TYPE_PINNED_MESSAGE_DELETED) {
|
||||
TUIJoinGroupMessageCellData *joinGroupData = [[TUIJoinGroupMessageCellData alloc] initWithDirection:MsgDirectionIncoming];
|
||||
joinGroupData.content = [self getDisplayString:message];
|
||||
joinGroupData.opUserName = opUserName;
|
||||
joinGroupData.opUserID = tip.opMember.userID;
|
||||
joinGroupData.userNameList = userNameList;
|
||||
joinGroupData.userIDList = userIDList;
|
||||
joinGroupData.reuseId = TJoinGroupMessageCell_ReuseId;
|
||||
return joinGroupData;
|
||||
} else {
|
||||
TUISystemMessageCellData *sysdata = [[TUISystemMessageCellData alloc] initWithDirection:MsgDirectionIncoming];
|
||||
sysdata.content = [self getDisplayString:message];
|
||||
sysdata.reuseId = TSystemMessageCell_ReuseId;
|
||||
if (sysdata.content.length) {
|
||||
return sysdata;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (nullable TUISystemMessageCellData *)getRevokeCellData:(V2TIMMessage *)message {
|
||||
TUISystemMessageCellData *revoke = [[TUISystemMessageCellData alloc] initWithDirection:(message.isSelf ? MsgDirectionOutgoing : MsgDirectionIncoming)];
|
||||
revoke.reuseId = TSystemMessageCell_ReuseId;
|
||||
revoke.content = [self getRevokeDispayString:message];
|
||||
revoke.innerMessage = message;
|
||||
V2TIMUserFullInfo *revokerInfo = message.revokerInfo;
|
||||
if (message.isSelf) {
|
||||
if (message.elemType == V2TIM_ELEM_TYPE_TEXT && fabs([[NSDate date] timeIntervalSinceDate:message.timestamp]) < MaxReEditMessageDelay) {
|
||||
if (revokerInfo && ![revokerInfo.userID isEqualToString:message.sender]) {
|
||||
// Super User revoke
|
||||
revoke.supportReEdit = NO;
|
||||
} else {
|
||||
revoke.supportReEdit = YES;
|
||||
}
|
||||
}
|
||||
} else if (message.groupID.length > 0) {
|
||||
/**
|
||||
* For the name display of group messages, the group business card is displayed first, the nickname has the second priority, and the user ID has the
|
||||
* lowest priority.
|
||||
*/
|
||||
NSString *userName = [TUIMessageDataProvider getShowName:message];
|
||||
TUIJoinGroupMessageCellData *joinGroupData = [[TUIJoinGroupMessageCellData alloc] initWithDirection:MsgDirectionIncoming];
|
||||
joinGroupData.content = [self getRevokeDispayString:message];
|
||||
joinGroupData.opUserID = message.sender;
|
||||
joinGroupData.opUserName = userName;
|
||||
joinGroupData.reuseId = TJoinGroupMessageCell_ReuseId;
|
||||
return joinGroupData;
|
||||
}
|
||||
return revoke;
|
||||
}
|
||||
|
||||
+ (nullable TUISystemMessageCellData *)getSystemMsgFromDate:(NSDate *)date {
|
||||
TUISystemMessageCellData *system = [[TUISystemMessageCellData alloc] initWithDirection:MsgDirectionOutgoing];
|
||||
system.content = [TUITool convertDateToStr:date];
|
||||
system.reuseId = TSystemMessageCell_ReuseId;
|
||||
system.type = TUISystemMessageTypeDate;
|
||||
return system;
|
||||
}
|
||||
|
||||
#pragma mark - Last message parser
|
||||
+ (void)asyncGetDisplayString:(NSArray<V2TIMMessage *> *)messageList callback:(void (^)(NSDictionary<NSString *, NSString *> *))callback {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary *originDisplayMap = [NSMutableDictionary dictionary];
|
||||
NSMutableArray *cellDataList = [NSMutableArray array];
|
||||
for (V2TIMMessage *message in messageList) {
|
||||
TUIMessageCellData *cellData = [self getCellData:message];
|
||||
if (cellData) {
|
||||
[cellDataList addObject:cellData];
|
||||
}
|
||||
|
||||
NSString *displayString = [self getDisplayString:message];
|
||||
if (displayString && message.msgID) {
|
||||
originDisplayMap[message.msgID] = displayString;
|
||||
}
|
||||
}
|
||||
|
||||
if (cellDataList.count == 0) {
|
||||
callback(@{});
|
||||
return;
|
||||
}
|
||||
|
||||
TUIMessageDataProvider *provider = [[TUIMessageDataProvider alloc] init];
|
||||
NSArray *additionUserIDList = [provider getUserIDListForAdditionalUserInfo:cellDataList];
|
||||
if (additionUserIDList.count == 0) {
|
||||
callback(@{});
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionary];
|
||||
[provider
|
||||
requestForAdditionalUserInfo:cellDataList
|
||||
callback:^{
|
||||
for (TUIMessageCellData *cellData in cellDataList) {
|
||||
[cellData.additionalUserInfoResult
|
||||
enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, TUIRelationUserModel *_Nonnull obj, BOOL *_Nonnull stop) {
|
||||
NSString *str = [NSString stringWithFormat:@"{%@}", key];
|
||||
NSString *showName = obj.userID;
|
||||
if (obj.nameCard.length > 0) {
|
||||
showName = obj.nameCard;
|
||||
} else if (obj.friendRemark.length > 0) {
|
||||
showName = obj.friendRemark;
|
||||
} else if (obj.nickName.length > 0) {
|
||||
showName = obj.nickName;
|
||||
}
|
||||
|
||||
NSString *displayString = [originDisplayMap objectForKey:cellData.msgID];
|
||||
if (displayString && [displayString containsString:str]) {
|
||||
displayString = [displayString stringByReplacingOccurrencesOfString:str withString:showName];
|
||||
result[cellData.msgID] = displayString;
|
||||
}
|
||||
|
||||
callback(result);
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
+ (nullable NSString *)getDisplayString:(V2TIMMessage *)message {
|
||||
BOOL hasRiskContent = message.hasRiskContent;
|
||||
BOOL isRevoked = (message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED);
|
||||
if (hasRiskContent && !isRevoked) {
|
||||
return TIMCommonLocalizableString(TUIKitMessageDisplayRiskContent);
|
||||
}
|
||||
NSString *str = [self parseDisplayStringFromMessageStatus:message];
|
||||
if (str == nil) {
|
||||
str = [self parseDisplayStringFromMessageElement:message];
|
||||
}
|
||||
|
||||
if (str == nil) {
|
||||
NSLog(@"current message will be ignored in chat page or conversation list page, msg:%@", message);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)parseDisplayStringFromMessageStatus:(V2TIMMessage *)message {
|
||||
NSString *str = nil;
|
||||
if (message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) {
|
||||
str = [self getRevokeDispayString:message];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)parseDisplayStringFromMessageElement:(V2TIMMessage *)message {
|
||||
NSString *str = nil;
|
||||
switch (message.elemType) {
|
||||
case V2TIM_ELEM_TYPE_TEXT: {
|
||||
str = [TUITextMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_IMAGE: {
|
||||
str = [TUIImageMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_SOUND: {
|
||||
str = [TUIVoiceMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_VIDEO: {
|
||||
str = [TUIVideoMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_FILE: {
|
||||
str = [TUIFileMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_FACE: {
|
||||
str = [TUIFaceMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_MERGER: {
|
||||
str = [TUIMergeMessageCellData getDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_GROUP_TIPS: {
|
||||
str = [self getGroupTipsDisplayString:message];
|
||||
} break;
|
||||
case V2TIM_ELEM_TYPE_CUSTOM: {
|
||||
str = [self getCustomDisplayString:message];
|
||||
} break;
|
||||
default:
|
||||
str = TIMCommonLocalizableString(TUIKitMessageTipsUnsupportCustomMessage);
|
||||
break;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)getCustomDisplayString:(V2TIMMessage *)message {
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// ************** TUICallKit , TUICallKit *************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
NSString *str = nil;
|
||||
id<TUIChatCallingInfoProtocol> callingInfo = nil;
|
||||
if ([self.callingDataProvider isCallingMessage:message callingInfo:&callingInfo]) {
|
||||
// Voice-video-call message
|
||||
if (callingInfo) {
|
||||
// Supported voice-video-call message
|
||||
if (callingInfo.excludeFromHistory) {
|
||||
// This message will be ignore in chat page
|
||||
str = nil;
|
||||
} else {
|
||||
// Get display text
|
||||
str = callingInfo.content ?: TIMCommonLocalizableString(TUIKitMessageTipsUnsupportCustomMessage);
|
||||
}
|
||||
} else {
|
||||
// Unsupported voice-video-call message
|
||||
str = TIMCommonLocalizableString(TUIKitMessageTipsUnsupportCustomMessage);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
// ************************************************************************************
|
||||
|
||||
NSString *businessID = nil;
|
||||
BOOL excludeFromHistory = NO;
|
||||
|
||||
V2TIMSignalingInfo *signalingInfo = [V2TIMManager.sharedInstance getSignallingInfo:message];
|
||||
if (signalingInfo) {
|
||||
// This message is signaling message
|
||||
excludeFromHistory = message.isExcludedFromLastMessage && message.isExcludedFromUnreadCount;
|
||||
businessID = [self getSignalingBusinessID:signalingInfo];
|
||||
} else {
|
||||
// This message is normal custom message
|
||||
excludeFromHistory = NO;
|
||||
businessID = [self getCustomBusinessID:message];
|
||||
}
|
||||
|
||||
if (excludeFromHistory) {
|
||||
// Return nil means not display int the chat page
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (businessID.length > 0) {
|
||||
Class cellDataClass = nil;
|
||||
if (gDataSourceClass && [gDataSourceClass respondsToSelector:@selector(onGetCustomMessageCellDataClass:)]) {
|
||||
cellDataClass = [gDataSourceClass onGetCustomMessageCellDataClass:businessID];
|
||||
}
|
||||
if (cellDataClass && [cellDataClass respondsToSelector:@selector(getDisplayString:)]) {
|
||||
return [cellDataClass getDisplayString:message];
|
||||
}
|
||||
// In CustomerService scenarios, unsupported messages are not displayed directly.
|
||||
if ([businessID tui_containsString:BussinessID_CustomerService]) {
|
||||
return nil;
|
||||
}
|
||||
return TIMCommonLocalizableString(TUIKitMessageTipsUnsupportCustomMessage);
|
||||
} else {
|
||||
return TIMCommonLocalizableString(TUIKitMessageTipsUnsupportCustomMessage);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Data source operate
|
||||
- (void)processQuoteMessage:(NSArray<TUIMessageCellData *> *)uiMsgs {
|
||||
if (uiMsgs.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@weakify(self);
|
||||
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
|
||||
dispatch_group_async(group, concurrentQueue, ^{
|
||||
for (TUIMessageCellData *cellData in uiMsgs) {
|
||||
if (![cellData isKindOfClass:TUIReplyMessageCellData.class]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TUIReplyMessageCellData *myData = (TUIReplyMessageCellData *)cellData;
|
||||
__weak typeof(myData) weakMyData = myData;
|
||||
myData.onFinish = ^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSUInteger index = [self.uiMsgs indexOfObject:weakMyData];
|
||||
if (index != NSNotFound) {
|
||||
// if messageData exist In datasource, reload this data.
|
||||
[UIView performWithoutAnimation:^{
|
||||
@strongify(self);
|
||||
[self.dataSource dataProviderDataSourceWillChange:self];
|
||||
[self.dataSource dataProviderDataSourceChange:self
|
||||
withType:TUIMessageBaseDataProviderDataSourceChangeTypeReload
|
||||
atIndex:index
|
||||
animation:NO];
|
||||
[self.dataSource dataProviderDataSourceDidChange:self];
|
||||
}];
|
||||
}
|
||||
});
|
||||
};
|
||||
dispatch_group_enter(group);
|
||||
[self loadOriginMessageFromReplyData:myData
|
||||
dealCallback:^{
|
||||
dispatch_group_leave(group);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSUInteger index = [self.uiMsgs indexOfObject:weakMyData];
|
||||
if (index != NSNotFound) {
|
||||
// if messageData exist In datasource, reload this data.
|
||||
[UIView performWithoutAnimation:^{
|
||||
@strongify(self);
|
||||
[self.dataSource dataProviderDataSourceWillChange:self];
|
||||
[self.dataSource dataProvider:self onRemoveHeightCache:weakMyData];
|
||||
[self.dataSource dataProviderDataSourceChange:self
|
||||
withType:TUIMessageBaseDataProviderDataSourceChangeTypeReload
|
||||
atIndex:index
|
||||
animation:NO];
|
||||
[self.dataSource dataProviderDataSourceDidChange:self];
|
||||
}];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(),
|
||||
^{
|
||||
// complete
|
||||
});
|
||||
}
|
||||
|
||||
- (void)deleteUIMsgs:(NSArray<TUIMessageCellData *> *)uiMsgs SuccBlock:(nullable V2TIMSucc)succ FailBlock:(nullable V2TIMFail)fail {
|
||||
NSMutableArray *uiMsgList = [NSMutableArray array];
|
||||
NSMutableArray *imMsgList = [NSMutableArray array];
|
||||
for (TUIMessageCellData *uiMsg in uiMsgs) {
|
||||
if ([self.uiMsgs containsObject:uiMsg]) {
|
||||
// Check content cell
|
||||
[uiMsgList addObject:uiMsg];
|
||||
[imMsgList addObject:uiMsg.innerMessage];
|
||||
|
||||
// Check time cell which also need to be deleted
|
||||
NSInteger index = [self.uiMsgs indexOfObject:uiMsg];
|
||||
index--;
|
||||
if (index >= 0 && index < self.uiMsgs.count && [[self.uiMsgs objectAtIndex:index] isKindOfClass:TUISystemMessageCellData.class]) {
|
||||
TUISystemMessageCellData *systemCellData = (TUISystemMessageCellData *)[self.uiMsgs objectAtIndex:index];
|
||||
if (systemCellData.type == TUISystemMessageTypeDate) {
|
||||
[uiMsgList addObject:systemCellData];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imMsgList.count == 0) {
|
||||
if (fail) {
|
||||
fail(ERR_INVALID_PARAMETERS, @"not found uiMsgs");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@weakify(self);
|
||||
[self.class deleteMessages:imMsgList
|
||||
succ:^{
|
||||
@strongify(self);
|
||||
[self.dataSource dataProviderDataSourceWillChange:self];
|
||||
for (TUIMessageCellData *uiMsg in uiMsgList) {
|
||||
NSInteger index = [self.uiMsgs indexOfObject:uiMsg];
|
||||
[self.dataSource dataProviderDataSourceChange:self
|
||||
withType:TUIMessageBaseDataProviderDataSourceChangeTypeDelete
|
||||
atIndex:index
|
||||
animation:YES];
|
||||
}
|
||||
[self removeUIMsgList:uiMsgList];
|
||||
|
||||
[self.dataSource dataProviderDataSourceDidChange:self];
|
||||
if (succ) {
|
||||
succ();
|
||||
}
|
||||
}
|
||||
fail:fail];
|
||||
}
|
||||
|
||||
- (void)removeUIMsgList:(NSArray<TUIMessageCellData *> *)cellDatas {
|
||||
for (TUIMessageCellData *uiMsg in cellDatas) {
|
||||
[self removeUIMsg:uiMsg];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Utils
|
||||
+ (nullable NSString *)getCustomBusinessID:(V2TIMMessage *)message {
|
||||
if (message == nil || message.customElem.data == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSError *error = nil;
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:message.customElem.data options:NSJSONReadingAllowFragments error:&error];
|
||||
if (error) {
|
||||
NSLog(@"parse customElem data error: %@", error);
|
||||
return nil;
|
||||
}
|
||||
if (!param || ![param isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *businessID = param[BussinessID];
|
||||
if ([businessID isKindOfClass:[NSString class]] && businessID.length > 0) {
|
||||
return businessID;
|
||||
} else {
|
||||
if ([param.allKeys containsObject:BussinessID_CustomerService]) {
|
||||
NSString *src = param[BussinessID_Src_CustomerService];
|
||||
if (src.length > 0 && [src isKindOfClass:[NSString class]]) {
|
||||
return [NSString stringWithFormat:@"%@%@", BussinessID_CustomerService, src];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable NSString *)getSignalingBusinessID:(V2TIMSignalingInfo *)signalInfo {
|
||||
if (signalInfo.data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *param = [NSJSONSerialization JSONObjectWithData:[signalInfo.data dataUsingEncoding:NSUTF8StringEncoding]
|
||||
options:NSJSONReadingAllowFragments
|
||||
error:&error];
|
||||
if (error) {
|
||||
NSLog(@"parse customElem data error: %@", error);
|
||||
return nil;
|
||||
}
|
||||
if (!param || ![param isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSString *businessID = param[BussinessID];
|
||||
if (!businessID || ![businessID isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return businessID;
|
||||
}
|
||||
|
||||
#pragma mark - TUICallKit
|
||||
|
||||
static TUIChatCallingDataProvider *gCallingDataProvider;
|
||||
+ (TUIChatCallingDataProvider *)callingDataProvider {
|
||||
if (gCallingDataProvider == nil) {
|
||||
gCallingDataProvider = [[TUIChatCallingDataProvider alloc] init];
|
||||
}
|
||||
return gCallingDataProvider;
|
||||
}
|
||||
|
||||
+ (TUIMessageCellData *)getCallingCellData:(id<TUIChatCallingInfoProtocol>)callingInfo {
|
||||
TMsgDirection direction = MsgDirectionIncoming;
|
||||
if (callingInfo.direction == TUICallMessageDirectionIncoming) {
|
||||
direction = MsgDirectionIncoming;
|
||||
} else if (callingInfo.direction == TUICallMessageDirectionOutgoing) {
|
||||
direction = MsgDirectionOutgoing;
|
||||
}
|
||||
|
||||
if (callingInfo.participantType == TUICallParticipantTypeC2C) {
|
||||
TUITextMessageCellData *cellData = [[TUITextMessageCellData alloc] initWithDirection:direction];
|
||||
if (callingInfo.streamMediaType == TUICallStreamMediaTypeVoice) {
|
||||
cellData.isAudioCall = YES;
|
||||
} else if (callingInfo.streamMediaType == TUICallStreamMediaTypeVideo) {
|
||||
cellData.isVideoCall = YES;
|
||||
} else {
|
||||
cellData.isAudioCall = NO;
|
||||
cellData.isVideoCall = NO;
|
||||
}
|
||||
cellData.content = callingInfo.content;
|
||||
cellData.isCaller = (callingInfo.participantRole == TUICallParticipantRoleCaller);
|
||||
cellData.showUnreadPoint = callingInfo.showUnreadPoint;
|
||||
cellData.isUseMsgReceiverAvatar = callingInfo.isUseReceiverAvatar;
|
||||
cellData.reuseId = TTextMessageCell_ReuseId;
|
||||
return cellData;
|
||||
} else if (callingInfo.participantType == TUICallParticipantTypeGroup) {
|
||||
TUISystemMessageCellData *cellData = [[TUISystemMessageCellData alloc] initWithDirection:direction];
|
||||
cellData.content = callingInfo.content;
|
||||
cellData.replacedUserIDList = callingInfo.participantIDList;
|
||||
cellData.reuseId = TSystemMessageCell_ReuseId;
|
||||
return cellData;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMessageBaseMediaDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMessageMediaDataProvider : TUIMessageBaseMediaDataProvider
|
||||
+ (TUIMessageCellData *)getMediaCellData:(V2TIMMessage *)message;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// TUIMessageSearchDataProvider.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/7/8.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageMediaDataProvider.h"
|
||||
#import "TUIImageMessageCellData.h"
|
||||
#import "TUIMessageBaseDataProvider+ProtectedAPI.h"
|
||||
#import "TUIVideoMessageCellData.h"
|
||||
|
||||
@implementation TUIMessageMediaDataProvider
|
||||
|
||||
+ (TUIMessageCellData *)getMediaCellData:(V2TIMMessage *)message {
|
||||
if (message.status == V2TIM_MSG_STATUS_HAS_DELETED || message.status == V2TIM_MSG_STATUS_LOCAL_REVOKED) {
|
||||
return nil;
|
||||
}
|
||||
TUIMessageCellData *data = nil;
|
||||
if (message.elemType == V2TIM_ELEM_TYPE_IMAGE) {
|
||||
data = [TUIImageMessageCellData getCellData:message];
|
||||
} else if (message.elemType == V2TIM_ELEM_TYPE_VIDEO) {
|
||||
data = [TUIVideoMessageCellData getCellData:message];
|
||||
}
|
||||
if (data) {
|
||||
data.innerMessage = message;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// TUIMessageSearchDataProvider.h
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/7/8.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMessageSearchDataProvider : TUIMessageDataProvider
|
||||
|
||||
@property(nonatomic) BOOL isOlderNoMoreMsg;
|
||||
@property(nonatomic) BOOL isNewerNoMoreMsg;
|
||||
@property(nonatomic) V2TIMMessage *msgForOlderGet;
|
||||
@property(nonatomic) V2TIMMessage *msgForNewerGet;
|
||||
|
||||
- (void)loadMessageWithSearchMsg:(V2TIMMessage *)searchMsg
|
||||
SearchMsgSeq:(uint64_t)searchSeq
|
||||
ConversationInfo:(TUIChatConversationModel *)conversation
|
||||
SucceedBlock:(void (^)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, NSArray<TUIMessageCellData *> *newMsgs))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock;
|
||||
|
||||
- (void)loadMessageWithIsRequestOlderMsg:(BOOL)orderType
|
||||
ConversationInfo:(TUIChatConversationModel *)conversation
|
||||
SucceedBlock:(void (^)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, BOOL isFirstLoad,
|
||||
NSArray<TUIMessageCellData *> *newUIMsgs))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock;
|
||||
|
||||
- (void)removeAllSearchData;
|
||||
|
||||
- (void)findMessages:(NSArray<NSString *> *)msgIDs callback:(void (^)(BOOL success, NSString *desc, NSArray<V2TIMMessage *> *messages))callback;
|
||||
|
||||
- (void)preProcessMessage:(NSArray *)uiMsgs;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,361 @@
|
||||
//
|
||||
// TUIMessageSearchDataProvider.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/7/8.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageSearchDataProvider.h"
|
||||
#import "TUIMessageBaseDataProvider+ProtectedAPI.h"
|
||||
#import "TUIChatMediaSendingManager.h"
|
||||
typedef void (^LoadSearchMsgSucceedBlock)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, NSArray<TUIMessageCellData *> *newMsgs);
|
||||
typedef void (^LoadMsgSucceedBlock)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, BOOL isFirstLoad, NSArray<TUIMessageCellData *> *newUIMsgs);
|
||||
|
||||
@interface TUIMessageSearchDataProvider ()
|
||||
|
||||
@property(nonatomic, copy) LoadSearchMsgSucceedBlock loadSearchMsgSucceedBlock;
|
||||
@property(nonatomic, copy) LoadMsgSucceedBlock loadMsgSucceedBlock;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIMessageSearchDataProvider
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_isOlderNoMoreMsg = NO;
|
||||
_isNewerNoMoreMsg = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadMessageWithSearchMsg:(V2TIMMessage *)searchMsg
|
||||
SearchMsgSeq:(uint64_t)searchSeq
|
||||
ConversationInfo:(TUIChatConversationModel *)conversation
|
||||
SucceedBlock:(void (^)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, NSArray<TUIMessageCellData *> *newMsgs))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock {
|
||||
if (self.isLoadingData) {
|
||||
failBlock(ERR_SUCC, @"refreshing");
|
||||
return;
|
||||
}
|
||||
self.isLoadingData = YES;
|
||||
self.isOlderNoMoreMsg = NO;
|
||||
self.isNewerNoMoreMsg = NO;
|
||||
self.loadSearchMsgSucceedBlock = succeedBlock;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
__block NSArray *olders = @[];
|
||||
__block NSArray *newers = @[];
|
||||
__block BOOL isOldLoadFail = NO;
|
||||
__block BOOL isNewLoadFail = NO;
|
||||
__block int failCode = 0;
|
||||
__block NSString *failDesc = nil;
|
||||
|
||||
/**
|
||||
* Load the oldest pageCount messages starting from locating message
|
||||
*/
|
||||
{
|
||||
dispatch_group_enter(group);
|
||||
V2TIMMessageListGetOption *option = [[V2TIMMessageListGetOption alloc] init];
|
||||
option.getType = V2TIM_GET_CLOUD_OLDER_MSG;
|
||||
option.count = self.pageCount;
|
||||
option.groupID = conversation.groupID;
|
||||
option.userID = conversation.userID;
|
||||
if (searchMsg) {
|
||||
option.lastMsg = searchMsg;
|
||||
} else {
|
||||
option.lastMsgSeq = searchSeq;
|
||||
}
|
||||
[V2TIMManager.sharedInstance getHistoryMessageList:option
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
msgs = msgs.reverseObjectEnumerator.allObjects;
|
||||
olders = msgs ?: @[];
|
||||
if (olders.count < self.pageCount) {
|
||||
self.isOlderNoMoreMsg = YES;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
isOldLoadFail = YES;
|
||||
failCode = code;
|
||||
failDesc = desc;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
/**
|
||||
* Load the latest pageCount messages starting from the locating message
|
||||
*/
|
||||
{
|
||||
dispatch_group_enter(group);
|
||||
V2TIMMessageListGetOption *option = [[V2TIMMessageListGetOption alloc] init];
|
||||
option.getType = V2TIM_GET_CLOUD_NEWER_MSG;
|
||||
option.count = self.pageCount;
|
||||
option.groupID = conversation.groupID;
|
||||
option.userID = conversation.userID;
|
||||
if (searchMsg) {
|
||||
option.lastMsg = searchMsg;
|
||||
} else {
|
||||
option.lastMsgSeq = searchSeq;
|
||||
}
|
||||
[V2TIMManager.sharedInstance getHistoryMessageList:option
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
newers = msgs ?: @[];
|
||||
if (newers.count < self.pageCount) {
|
||||
self.isNewerNoMoreMsg = YES;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
isNewLoadFail = YES;
|
||||
failCode = code;
|
||||
failDesc = desc;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
@weakify(self);
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
@strongify(self);
|
||||
self.isLoadingData = NO;
|
||||
if (isOldLoadFail || isNewLoadFail) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
failBlock(failCode, failDesc);
|
||||
});
|
||||
}
|
||||
self.isFirstLoad = NO;
|
||||
|
||||
NSMutableArray *results = [NSMutableArray array];
|
||||
[results addObjectsFromArray:olders];
|
||||
if (searchMsg) {
|
||||
/**
|
||||
* Pulling messages through the msg will not return the msg object itself, here you need to actively add the msg to the results list
|
||||
*/
|
||||
[results addObject:searchMsg];
|
||||
} else {
|
||||
/**
|
||||
* Pulling messages through the msg seq, pulling old messages and new messages will return the msg object itself, here you need to deduplicate the msg
|
||||
* object in results
|
||||
*/
|
||||
[results removeLastObject];
|
||||
}
|
||||
[results addObjectsFromArray:newers];
|
||||
self.msgForOlderGet = results.firstObject;
|
||||
self.msgForNewerGet = results.lastObject;
|
||||
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
[self.heightCache_ removeAllObjects];
|
||||
[self.uiMsgs_ removeAllObjects];
|
||||
|
||||
NSArray *msgs = results.reverseObjectEnumerator.allObjects;
|
||||
NSMutableArray *uiMsgs = [self transUIMsgFromIMMsg:msgs];
|
||||
if (uiMsgs.count == 0) {
|
||||
return;
|
||||
}
|
||||
[self getGroupMessageReceipts:msgs
|
||||
uiMsgs:uiMsgs
|
||||
succ:^{
|
||||
[self preProcessMessage:uiMsgs];
|
||||
}
|
||||
fail:^{
|
||||
[self preProcessMessage:uiMsgs];
|
||||
}];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)loadMessageWithIsRequestOlderMsg:(BOOL)orderType
|
||||
ConversationInfo:(TUIChatConversationModel *)conversation
|
||||
SucceedBlock:(void (^)(BOOL isOlderNoMoreMsg, BOOL isNewerNoMoreMsg, BOOL isFirstLoad,
|
||||
NSArray<TUIMessageCellData *> *newUIMsgs))succeedBlock
|
||||
FailBlock:(V2TIMFail)failBlock {
|
||||
self.isLoadingData = YES;
|
||||
self.loadMsgSucceedBlock = succeedBlock;
|
||||
|
||||
int requestCount = self.pageCount;
|
||||
V2TIMMessageListGetOption *option = [[V2TIMMessageListGetOption alloc] init];
|
||||
option.userID = conversation.userID;
|
||||
option.groupID = conversation.groupID;
|
||||
option.getType = orderType ? V2TIM_GET_CLOUD_OLDER_MSG : V2TIM_GET_CLOUD_NEWER_MSG;
|
||||
option.count = requestCount;
|
||||
option.lastMsg = orderType ? self.msgForOlderGet : self.msgForNewerGet;
|
||||
@weakify(self);
|
||||
[V2TIMManager.sharedInstance getHistoryMessageList:option
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
@strongify(self);
|
||||
if (!orderType) {
|
||||
msgs = msgs.reverseObjectEnumerator.allObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the lastMsg flag
|
||||
* -- The current pull operation is to pull from the latest time point to the past
|
||||
*/
|
||||
BOOL isLastest = (self.msgForNewerGet == nil) && (self.msgForOlderGet == nil) && orderType;
|
||||
if (msgs.count != 0) {
|
||||
if (orderType) {
|
||||
self.msgForOlderGet = msgs.lastObject;
|
||||
if (self.msgForNewerGet == nil) {
|
||||
self.msgForNewerGet = msgs.firstObject;
|
||||
}
|
||||
} else {
|
||||
if (self.msgForOlderGet == nil) {
|
||||
self.msgForOlderGet = msgs.lastObject;
|
||||
}
|
||||
self.msgForNewerGet = msgs.firstObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update no data flag
|
||||
*/
|
||||
if (msgs.count < requestCount) {
|
||||
if (orderType) {
|
||||
self.isOlderNoMoreMsg = YES;
|
||||
} else {
|
||||
self.isNewerNoMoreMsg = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastest) {
|
||||
/**
|
||||
* The current pull operation is to pull from the latest time point to the past
|
||||
*/
|
||||
self.isNewerNoMoreMsg = YES;
|
||||
}
|
||||
|
||||
NSMutableArray<TUIMessageCellData *> *uiMsgs = [self transUIMsgFromIMMsg:msgs];
|
||||
if (uiMsgs.count == 0) {
|
||||
if (self.loadMsgSucceedBlock) {
|
||||
self.loadMsgSucceedBlock(self.isOlderNoMoreMsg, self.isNewerNoMoreMsg, self.isFirstLoad, uiMsgs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//add media placeholder celldata
|
||||
if (self.conversationModel.conversationID.length > 0) {
|
||||
NSMutableArray<TUIChatMediaTask *> * tasks = [TUIChatMediaSendingManager.sharedInstance
|
||||
findPlaceHolderListByConversationID:self.conversationModel.conversationID];
|
||||
for (TUIChatMediaTask * task in tasks) {
|
||||
if (task.placeHolderCellData) {
|
||||
[uiMsgs addObject:task.placeHolderCellData];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[self getGroupMessageReceipts:msgs
|
||||
uiMsgs:uiMsgs
|
||||
succ:^{
|
||||
[self preProcessMessage:uiMsgs orderType:orderType];
|
||||
}
|
||||
fail:^{
|
||||
[self preProcessMessage:uiMsgs orderType:orderType];
|
||||
}];
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
self.isLoadingData = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)getGroupMessageReceipts:(NSArray *)msgs uiMsgs:(NSArray *)uiMsgs succ:(void (^)(void))succBlock fail:(void (^)(void))failBlock {
|
||||
[[V2TIMManager sharedInstance] getMessageReadReceipts:msgs
|
||||
succ:^(NSArray<V2TIMMessageReceipt *> *receiptList) {
|
||||
NSLog(@"getGroupMessageReceipts succeed, receiptList: %@", receiptList);
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
for (V2TIMMessageReceipt *receipt in receiptList) {
|
||||
[dict setObject:receipt forKey:receipt.msgID];
|
||||
}
|
||||
for (TUIMessageCellData *data in uiMsgs) {
|
||||
V2TIMMessageReceipt *receipt = dict[data.msgID];
|
||||
data.messageReceipt = receipt;
|
||||
}
|
||||
|
||||
if (succBlock) {
|
||||
succBlock();
|
||||
}
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
NSLog(@"getGroupMessageReceipts failed, code: %d, desc: %@", code, desc);
|
||||
if (failBlock) {
|
||||
failBlock();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)preProcessMessage:(NSArray *)uiMsgs {
|
||||
@weakify(self);
|
||||
[self preProcessMessage:uiMsgs
|
||||
callback:^{
|
||||
@strongify(self);
|
||||
[self addUIMsgs:uiMsgs];
|
||||
self.loadSearchMsgSucceedBlock(self.isOlderNoMoreMsg, self.isNewerNoMoreMsg, self.uiMsgs_);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)preProcessMessage:(NSArray *)uiMsgs orderType:(BOOL)orderType {
|
||||
@weakify(self);
|
||||
[self preProcessMessage:uiMsgs
|
||||
callback:^{
|
||||
@strongify(self);
|
||||
|
||||
if (orderType) {
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, uiMsgs.count)];
|
||||
[self insertUIMsgs:uiMsgs atIndexes:indexSet];
|
||||
} else {
|
||||
[self addUIMsgs:uiMsgs];
|
||||
}
|
||||
|
||||
if (self.loadMsgSucceedBlock) {
|
||||
self.loadMsgSucceedBlock(self.isOlderNoMoreMsg, self.isNewerNoMoreMsg, self.isFirstLoad, uiMsgs);
|
||||
}
|
||||
|
||||
self.isLoadingData = NO;
|
||||
self.isFirstLoad = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)removeAllSearchData {
|
||||
[self.uiMsgs_ removeAllObjects];
|
||||
self.isNewerNoMoreMsg = NO;
|
||||
self.isOlderNoMoreMsg = NO;
|
||||
self.isFirstLoad = YES;
|
||||
self.msgForNewerGet = nil;
|
||||
self.msgForOlderGet = nil;
|
||||
self.loadSearchMsgSucceedBlock = nil;
|
||||
}
|
||||
|
||||
- (void)findMessages:(NSArray<NSString *> *)msgIDs callback:(void (^)(BOOL success, NSString *desc, NSArray<V2TIMMessage *> *messages))callback {
|
||||
[V2TIMManager.sharedInstance findMessages:msgIDs
|
||||
succ:^(NSArray<V2TIMMessage *> *msgs) {
|
||||
if (callback) {
|
||||
callback(YES, @"", msgs);
|
||||
}
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
if (callback) {
|
||||
callback(NO, desc, nil);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Override
|
||||
- (void)onRecvNewMessage:(V2TIMMessage *)msg {
|
||||
if (self.isNewerNoMoreMsg == NO) {
|
||||
/**
|
||||
* If the current message list has not pulled the last message, ignore the new message;
|
||||
* If it is processed at this time, it will cause new messages to be added to the history list, resulting in the problem of position confusion.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
if (self.dataSource.isDataSourceConsistent == NO ) {
|
||||
self.isNewerNoMoreMsg = NO;
|
||||
return;
|
||||
}
|
||||
|
||||
[super onRecvNewMessage:msg];
|
||||
}
|
||||
|
||||
@end
|
||||
23
TUIKit/TUIChat/CommonModel/TUIAIDenoiseSignatureManager.h
Normal file
23
TUIKit/TUIChat/CommonModel/TUIAIDenoiseSignatureManager.h
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
// TUIAIDenoiseSignatureManager.h
|
||||
// TUIChat
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIAIDenoiseSignatureManager : NSObject
|
||||
|
||||
@property(nonatomic, copy, readonly) NSString *signature;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (void)updateSignature;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
61
TUIKit/TUIChat/CommonModel/TUIAIDenoiseSignatureManager.m
Normal file
61
TUIKit/TUIChat/CommonModel/TUIAIDenoiseSignatureManager.m
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
// TUIAIDenoiseSignatureManager.m
|
||||
// TUIChat
|
||||
//
|
||||
|
||||
#import "TUIAIDenoiseSignatureManager.h"
|
||||
#import <ImSDK_Plus/ImSDK_Plus.h>
|
||||
|
||||
static TUIAIDenoiseSignatureManager *gSharedInstance = nil;
|
||||
static NSString *const kAPIKey = @"getAIDenoiseSignature";
|
||||
static NSString *const kSignatureKey = @"signature";
|
||||
static NSString *const kExpiredTimeKey = @"expired_time";
|
||||
|
||||
@interface TUIAIDenoiseSignatureManager ()
|
||||
@property(nonatomic, copy, readwrite) NSString *signature;
|
||||
@property(nonatomic, assign) NSTimeInterval expiredTime;
|
||||
@end
|
||||
|
||||
@implementation TUIAIDenoiseSignatureManager
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static dispatch_once_t once_token;
|
||||
dispatch_once(&once_token, ^{
|
||||
gSharedInstance = [[TUIAIDenoiseSignatureManager alloc] init];
|
||||
});
|
||||
return gSharedInstance;
|
||||
}
|
||||
|
||||
- (void)updateSignature {
|
||||
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
|
||||
if (currentTime < self.expiredTime) {
|
||||
return;
|
||||
}
|
||||
[[V2TIMManager sharedInstance] callExperimentalAPI:kAPIKey
|
||||
param:nil
|
||||
succ:^(NSObject *result) {
|
||||
if (result == nil || ![result isKindOfClass:NSDictionary.class]) {
|
||||
return;
|
||||
}
|
||||
NSDictionary *dict = (NSDictionary *)result;
|
||||
if (dict[kSignatureKey] != nil && [dict[kSignatureKey] isKindOfClass:NSString.class]) {
|
||||
self.signature = dict[kSignatureKey];
|
||||
}
|
||||
if (dict[kExpiredTimeKey] != nil && [dict[kExpiredTimeKey] isKindOfClass:NSNumber.class]) {
|
||||
self.expiredTime = [dict[kExpiredTimeKey] doubleValue];
|
||||
}
|
||||
}
|
||||
fail:^(int code, NSString *desc) {
|
||||
NSLog(@"getAIDenoiseSignature failed, code: %d, desc: %@", code, desc);
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSString *)signature {
|
||||
[self updateSignature];
|
||||
return _signature;
|
||||
}
|
||||
|
||||
@end
|
||||
37
TUIKit/TUIChat/CommonModel/TUIAudioRecorder.h
Normal file
37
TUIKit/TUIChat/CommonModel/TUIAudioRecorder.h
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
// TUIAudioRecorder.h
|
||||
// TUIChat
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// TUIAudioRecorder is designed for recording audio when sending audio message.
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TUIAudioRecorder;
|
||||
@protocol TUIAudioRecorderDelegate <NSObject>
|
||||
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didCheckPermission:(BOOL)isGranted isFirstTime:(BOOL)isFirstTime;
|
||||
/// Power value can be used to simulate the animation of mic changes when speaking.
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didPowerChanged:(float)power;
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didRecordTimeChanged:(NSTimeInterval)time;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIAudioRecorder : NSObject
|
||||
|
||||
@property(nonatomic, weak) id<TUIAudioRecorderDelegate> delegate;
|
||||
|
||||
@property(nonatomic, copy, readonly) NSString *recordedFilePath;
|
||||
|
||||
- (void)record;
|
||||
- (void)stop;
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
361
TUIKit/TUIChat/CommonModel/TUIAudioRecorder.m
Normal file
361
TUIKit/TUIChat/CommonModel/TUIAudioRecorder.m
Normal file
@@ -0,0 +1,361 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
// TUIAudioRecorder.m
|
||||
// TUIChat
|
||||
//
|
||||
|
||||
#import "TUIAudioRecorder.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUIAIDenoiseSignatureManager.h"
|
||||
|
||||
@interface TUIAudioRecorder () <AVAudioRecorderDelegate, TUINotificationProtocol>
|
||||
|
||||
@property(nonatomic, strong) AVAudioRecorder *recorder;
|
||||
@property(nonatomic, strong) NSTimer *recordTimer;
|
||||
|
||||
@property(nonatomic, assign) BOOL isUsingCallKitRecorder;
|
||||
|
||||
@property(nonatomic, copy, readwrite) NSString *recordedFilePath;
|
||||
@property(nonatomic, assign) NSTimeInterval currentRecordTime;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIAudioRecorder
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self configNotify];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configNotify {
|
||||
[TUICore registerEvent:TUICore_RecordAudioMessageNotify subKey:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey object:self];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[TUICore unRegisterEventByObject:self];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
- (void)record {
|
||||
[self checkMicPermissionWithCompletion:^(BOOL isGranted, BOOL isFirstChek) {
|
||||
if (TUILogin.getCurrentBusinessScene != None) {
|
||||
[TUITool makeToast:TIMCommonLocalizableString(TUIKitMessageTypeOtherUseMic) duration:3];
|
||||
return;
|
||||
}
|
||||
if (isFirstChek) {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) {
|
||||
[self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:YES];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didCheckPermission:isFirstTime:)]) {
|
||||
[self.delegate audioRecorder:self didCheckPermission:isGranted isFirstTime:NO];
|
||||
}
|
||||
if (isGranted) {
|
||||
[self createRecordedFilePath];
|
||||
if (![self startCallKitRecording]) {
|
||||
[self startSystemRecording];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
[self stopRecordTimer];
|
||||
|
||||
if (self.isUsingCallKitRecorder) {
|
||||
[self stopCallKitRecording];
|
||||
} else {
|
||||
[self stopSystemRecording];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancel {
|
||||
[self stopRecordTimer];
|
||||
|
||||
if (self.isUsingCallKitRecorder) {
|
||||
[self stopCallKitRecording];
|
||||
} else {
|
||||
[self cancelSystemRecording];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
- (void)createRecordedFilePath {
|
||||
self.recordedFilePath = [TUIKit_Voice_Path stringByAppendingString:[TUITool genVoiceName:nil withExtension:@"m4a"]];
|
||||
}
|
||||
|
||||
- (void)stopRecordTimer {
|
||||
if (self.recordTimer) {
|
||||
[self.recordTimer invalidate];
|
||||
self.recordTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark-- Timer
|
||||
- (void)triggerRecordTimer {
|
||||
self.currentRecordTime = 0;
|
||||
self.recordTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(onRecordTimerTriggered:) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
- (void)onRecordTimerTriggered:(NSTimer *)timer {
|
||||
[self.recorder updateMeters];
|
||||
|
||||
if (self.isUsingCallKitRecorder) {
|
||||
/// To ensure the callkit recorder's recording time is enough for 60 seconds.
|
||||
self.currentRecordTime += 0.2;
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) {
|
||||
[self.delegate audioRecorder:self didRecordTimeChanged:self.currentRecordTime];
|
||||
}
|
||||
} else {
|
||||
float power = [self.recorder averagePowerForChannel:0];
|
||||
NSTimeInterval currentTime = self.recorder.currentTime;
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) {
|
||||
[self.delegate audioRecorder:self didPowerChanged:power];
|
||||
}
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didRecordTimeChanged:)]) {
|
||||
[self.delegate audioRecorder:self didRecordTimeChanged:currentTime];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)checkMicPermissionWithCompletion:(void (^)(BOOL isGranted, BOOL isFirstChek))completion {
|
||||
AVAudioSessionRecordPermission permission = AVAudioSession.sharedInstance.recordPermission;
|
||||
|
||||
/**
|
||||
* For the first request for authorization after a new installation, it is necessary to
|
||||
* determine whether it is Undetermined again to avoid errors.
|
||||
*/
|
||||
if (permission == AVAudioSessionRecordPermissionDenied || permission == AVAudioSessionRecordPermissionUndetermined) {
|
||||
[AVAudioSession.sharedInstance requestRecordPermission:^(BOOL granted) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (completion) {
|
||||
completion(granted, YES);
|
||||
}
|
||||
});
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isGranted = permission == AVAudioSessionRecordPermissionGranted;
|
||||
if (completion) {
|
||||
completion(isGranted, NO);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark-- Record audio using system framework
|
||||
- (void)startSystemRecording {
|
||||
self.isUsingCallKitRecorder = NO;
|
||||
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
NSDictionary *recordSetting = [[NSDictionary alloc] initWithObjectsAndKeys:
|
||||
/**
|
||||
* Sampling rate: 8000/11025/22050/44100/96000 (this parameter affects the audio
|
||||
* quality)
|
||||
*/
|
||||
[NSNumber numberWithFloat:16000.0], AVSampleRateKey,
|
||||
/**
|
||||
* Audio format
|
||||
*/
|
||||
[NSNumber numberWithInt:kAudioFormatMPEG4AAC], AVFormatIDKey,
|
||||
/**
|
||||
* Sampling bits: 8, 16, 24, 32, default is 16
|
||||
*/
|
||||
[NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
|
||||
/**
|
||||
* Number of audio channels 1 or 2
|
||||
*/
|
||||
[NSNumber numberWithInt:1], AVNumberOfChannelsKey,
|
||||
/**
|
||||
* Recording quality
|
||||
*/
|
||||
[NSNumber numberWithInt:AVAudioQualityHigh], AVEncoderAudioQualityKey, nil];
|
||||
|
||||
[self createRecordedFilePath];
|
||||
|
||||
NSURL *url = [NSURL fileURLWithPath:self.recordedFilePath];
|
||||
self.recorder = [[AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:nil];
|
||||
self.recorder.meteringEnabled = YES;
|
||||
[self.recorder prepareToRecord];
|
||||
[self.recorder record];
|
||||
[self.recorder updateMeters];
|
||||
|
||||
[self triggerRecordTimer];
|
||||
NSLog(@"start system recording");
|
||||
}
|
||||
|
||||
- (void)stopSystemRecording {
|
||||
if (AVAudioSession.sharedInstance.recordPermission == AVAudioSessionRecordPermissionDenied) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.recorder isRecording]) {
|
||||
[self.recorder stop];
|
||||
}
|
||||
|
||||
self.recorder = nil;
|
||||
NSLog(@"stop system recording");
|
||||
}
|
||||
|
||||
- (void)cancelSystemRecording {
|
||||
if ([self.recorder isRecording]) {
|
||||
[self.recorder stop];
|
||||
}
|
||||
|
||||
NSString *path = self.recorder.url.path;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
|
||||
}
|
||||
|
||||
self.recorder = nil;
|
||||
NSLog(@"cancel system recording");
|
||||
}
|
||||
|
||||
#pragma mark-- Record audio using TUICallKit framework
|
||||
- (BOOL)startCallKitRecording {
|
||||
if (![TUICore getService:TUICore_TUIAudioMessageRecordService]) {
|
||||
NSLog(@"TUICallKit audio recording service does not exist");
|
||||
return NO;
|
||||
}
|
||||
NSString *signature = [TUIAIDenoiseSignatureManager sharedInstance].signature;
|
||||
if (signature.length == 0) {
|
||||
NSLog(@"denoise signature is empty");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableDictionary *audioRecordParam = [[NSMutableDictionary alloc] init];
|
||||
[audioRecordParam setValue:signature forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SignatureKey];
|
||||
[audioRecordParam setValue:@([TUILogin getSdkAppID]) forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_SdkappidKey];
|
||||
[audioRecordParam setValue:self.recordedFilePath forKey:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod_PathKey];
|
||||
|
||||
@weakify(self);
|
||||
void (^startCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) =
|
||||
^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) {
|
||||
@strongify(self);
|
||||
NSString *method = param[@"method"];
|
||||
if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StartRecordAudioMessageSubKey]) {
|
||||
[self onTUICallKitRecordStarted:errorCode];
|
||||
}
|
||||
};
|
||||
|
||||
[TUICore callService:TUICore_TUIAudioMessageRecordService
|
||||
method:TUICore_TUIAudioMessageRecordService_StartRecordAudioMessageMethod
|
||||
param:audioRecordParam
|
||||
resultCallback:startCallBack];
|
||||
|
||||
self.isUsingCallKitRecorder = YES;
|
||||
NSLog(@"start TUICallKit recording");
|
||||
return true;
|
||||
}
|
||||
|
||||
- (void)stopCallKitRecording {
|
||||
@weakify(self);
|
||||
void (^stopCallBack)(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) =
|
||||
^(NSInteger errorCode, NSString *errorMessage, NSDictionary *param) {
|
||||
@strongify(self);
|
||||
NSString *method = param[@"method"];
|
||||
if ([method isEqualToString:TUICore_RecordAudioMessageNotify_StopRecordAudioMessageSubKey]) {
|
||||
[self onTUICallKitRecordCompleted:errorCode];
|
||||
}
|
||||
};
|
||||
|
||||
[TUICore callService:TUICore_TUIAudioMessageRecordService
|
||||
method:TUICore_TUIAudioMessageRecordService_StopRecordAudioMessageMethod
|
||||
param:nil
|
||||
resultCallback:stopCallBack];
|
||||
|
||||
NSLog(@"stop TUICallKit recording");
|
||||
}
|
||||
|
||||
#pragma mark - TUINotificationProtocol
|
||||
- (void)onNotifyEvent:(NSString *)key subKey:(NSString *)subKey object:(nullable id)anObject param:(NSDictionary *)param {
|
||||
if ([key isEqualToString:TUICore_RecordAudioMessageNotify]) {
|
||||
if (param == nil) {
|
||||
NSLog(@"TUICallKit notify param is invalid");
|
||||
return;
|
||||
}
|
||||
if ([subKey isEqualToString:TUICore_RecordAudioMessageNotify_RecordAudioVoiceVolumeSubKey]) {
|
||||
NSUInteger volume = [param[@"volume"] unsignedIntegerValue];
|
||||
[self onTUICallKitVolumeChanged:volume];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onTUICallKitRecordStarted:(NSInteger)errorCode {
|
||||
switch (errorCode) {
|
||||
case TUICore_RecordAudioMessageNotifyError_None: {
|
||||
[self triggerRecordTimer];
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_MicPermissionRefused: {
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_StatusInCall: {
|
||||
[TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedInCall)];
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_StatusIsAudioRecording: {
|
||||
[TUITool makeToast:TIMCommonLocalizableString(TUIKitInputRecordRejectedIsRecording)];
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_RequestAudioFocusFailed:
|
||||
case TUICore_RecordAudioMessageNotifyError_RecordInitFailed:
|
||||
case TUICore_RecordAudioMessageNotifyError_PathFormatNotSupport:
|
||||
case TUICore_RecordAudioMessageNotifyError_MicStartFail:
|
||||
case TUICore_RecordAudioMessageNotifyError_MicNotAuthorized:
|
||||
case TUICore_RecordAudioMessageNotifyError_MicSetParamFail:
|
||||
case TUICore_RecordAudioMessageNotifyError_MicOccupy: {
|
||||
[self stopCallKitRecording];
|
||||
NSLog(@"start TUICallKit recording failed, errorCode: %ld", (long)errorCode);
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_InvalidParam:
|
||||
case TUICore_RecordAudioMessageNotifyError_SignatureError:
|
||||
case TUICore_RecordAudioMessageNotifyError_SignatureExpired:
|
||||
default: {
|
||||
[self stopCallKitRecording];
|
||||
[self startSystemRecording];
|
||||
NSLog(@"start TUICallKit recording failed, errorCode: %ld, switch to system recorder", (long)errorCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onTUICallKitRecordCompleted:(NSInteger)errorCode {
|
||||
switch (errorCode) {
|
||||
case TUICore_RecordAudioMessageNotifyError_None: {
|
||||
[self stopRecordTimer];
|
||||
break;
|
||||
}
|
||||
case TUICore_RecordAudioMessageNotifyError_NoMessageToRecord:
|
||||
case TUICore_RecordAudioMessageNotifyError_RecordFailed: {
|
||||
NSLog(@"stop TUICallKit recording failed, errorCode: %ld", (long)errorCode);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onTUICallKitVolumeChanged:(NSUInteger)volume {
|
||||
/// Adapt volume to power.
|
||||
float power = (NSInteger)volume - 90;
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(audioRecorder:didPowerChanged:)]) {
|
||||
[self.delegate audioRecorder:self didPowerChanged:power];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
273
TUIKit/TUIChat/CommonModel/TUIChatConfig.h
Normal file
273
TUIKit/TUIChat/CommonModel/TUIChatConfig.h
Normal file
@@ -0,0 +1,273 @@
|
||||
//
|
||||
// TUIChatConfig.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import "TUIChatConversationModel.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TUIChatEventConfig;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TUIChatRegisterCustomMessageStyleType) {
|
||||
TUIChatRegisterCustomMessageStyleTypeClassic = 0,
|
||||
TUIChatRegisterCustomMessageStyleTypeMinimalist = 1,
|
||||
};
|
||||
|
||||
@class TUICustomActionSheetItem;
|
||||
@class TUIChatConversationModel;
|
||||
typedef NS_OPTIONS(NSInteger, TUIChatInputBarMoreMenuItem) {
|
||||
TUIChatInputBarMoreMenuItem_None = 0,
|
||||
TUIChatInputBarMoreMenuItem_CustomMessage = 1 << 0,
|
||||
TUIChatInputBarMoreMenuItem_TakePhoto = 1 << 1,
|
||||
TUIChatInputBarMoreMenuItem_RecordVideo = 1 << 2,
|
||||
TUIChatInputBarMoreMenuItem_Album = 1 << 3,
|
||||
TUIChatInputBarMoreMenuItem_File = 1 << 4,
|
||||
TUIChatInputBarMoreMenuItem_Room = 1 << 5,
|
||||
TUIChatInputBarMoreMenuItem_Poll = 1 << 6,
|
||||
TUIChatInputBarMoreMenuItem_GroupNote = 1 << 7,
|
||||
TUIChatInputBarMoreMenuItem_VideoCall = 1 << 8,
|
||||
TUIChatInputBarMoreMenuItem_AudioCall = 1 << 9,
|
||||
};
|
||||
@protocol TUIChatInputBarConfigDataSource <NSObject>
|
||||
@optional
|
||||
/**
|
||||
* Implement this method to hide items in more menu of the specified model.
|
||||
*/
|
||||
- (TUIChatInputBarMoreMenuItem)inputBarShouldHideItemsInMoreMenuOfModel:(TUIChatConversationModel *)model;
|
||||
/**
|
||||
* Implement this method to add new items to the more menu of the specified model only for the classic version.
|
||||
*/
|
||||
- (NSArray<TUIInputMoreCellData *> *)inputBarShouldAddNewItemsToMoreMenuOfModel:(TUIChatConversationModel *)model;
|
||||
/**
|
||||
* Implement this method to add new items to the more list of the specified model only for the minimalist version.
|
||||
*/
|
||||
- (NSArray<TUICustomActionSheetItem *> *)inputBarShouldAddNewItemsToMoreListOfModel:(TUIChatConversationModel *)model;
|
||||
@end
|
||||
|
||||
@protocol TUIChatShortcutViewDataSource <NSObject>
|
||||
@optional
|
||||
- (NSArray<TUIChatShortcutMenuCellData *> *)itemsInShortcutViewOfModel:(TUIChatConversationModel *)model;
|
||||
- (UIColor *)shortcutViewBackgroundColorOfModel:(TUIChatConversationModel *)model;
|
||||
- (CGFloat)shortcutViewHeightOfModel:(TUIChatConversationModel *)model;
|
||||
@end
|
||||
|
||||
|
||||
@interface TUIChatConfig : NSObject
|
||||
|
||||
+ (TUIChatConfig *)defaultConfig;
|
||||
|
||||
@property(nonatomic, strong) NSArray<TUIFaceGroup *> *chatContextEmojiDetailGroups;
|
||||
|
||||
/**
|
||||
* A read receipt is required to send a message, default is No
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL msgNeedReadReceipt;
|
||||
|
||||
/**
|
||||
* Display the video call button, if the TUICalling component is integrated, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableVideoCall;
|
||||
|
||||
/**
|
||||
* Whether to display the audio call button, if the TUICalling component is integrated, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableAudioCall;
|
||||
|
||||
/**
|
||||
* Display custom welcome message button, default YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableWelcomeCustomMessage;
|
||||
|
||||
/**
|
||||
* In the chat interface, long press the pop-up box to display the emoji interactive message function, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enablePopMenuEmojiReactAction;
|
||||
|
||||
/**
|
||||
* Chat long press the pop-up box to display the message reply function entry, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enablePopMenuReplyAction;
|
||||
|
||||
/**
|
||||
* Chat long press the pop-up box to display the entry of the message reference function, the default is YES
|
||||
*/
|
||||
|
||||
@property(nonatomic, assign) BOOL enablePopMenuReferenceAction;
|
||||
|
||||
@property(nonatomic, assign) BOOL enablePopMenuPinAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuRecallAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuTranslateAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuConvertAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuForwardAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuSelectAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuCopyAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuDeleteAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuInfoAction;
|
||||
@property(nonatomic, assign) BOOL enablePopMenuAudioPlaybackAction;
|
||||
|
||||
/**
|
||||
* Whether the C2C chat dialog box displays "The other party is typing...", the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableTypingStatus;
|
||||
|
||||
/**
|
||||
* Whether the chat dialog box displays "InputBar", the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableMainPageInputBar;
|
||||
|
||||
/**
|
||||
* Setup the backgroud color of chat page
|
||||
*/
|
||||
@property(nonatomic, strong) UIColor *backgroudColor;
|
||||
|
||||
/**
|
||||
* Setup the backgroud image of chat page
|
||||
*/
|
||||
@property(nonatomic, strong) UIImage *backgroudImage;
|
||||
|
||||
/**
|
||||
* Whether to turn on audio and video call suspension windows, default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableFloatWindowForCall;
|
||||
|
||||
/**
|
||||
* Whether to enable multi-terminal login function for audio and video calls, default is NO
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableMultiDeviceForCall;
|
||||
|
||||
/**
|
||||
* Set whether to enable incoming banner when user received audio and video calls, default is false
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableIncomingBanner;
|
||||
|
||||
/**
|
||||
* Set whether to enable the virtual background for audio and video calls, default value is false
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableVirtualBackgroundForCall;
|
||||
|
||||
/**
|
||||
* The time interval for message recall, in seconds, default is 120 seconds. If you want to adjust this configuration, please modify the IM console settings
|
||||
* synchronously.
|
||||
* https://cloud.tencent.com/document/product/269/38656#.E6.B6.88.E6.81.AF.E6.92.A4.E5.9B.9E.E8.AE.BE.E7.BD.AE
|
||||
*/
|
||||
@property(nonatomic, assign) NSUInteger timeIntervalForMessageRecall;
|
||||
|
||||
/**
|
||||
不超过 60s
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat maxAudioRecordDuration;
|
||||
|
||||
/**
|
||||
不超过 15s
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat maxVideoRecordDuration;
|
||||
|
||||
@property(nonatomic, assign) BOOL showRoomButton;
|
||||
@property(nonatomic, assign) BOOL showPollButton;
|
||||
@property(nonatomic, assign) BOOL showGroupNoteButton;
|
||||
@property(nonatomic, assign) BOOL showRecordVideoButton;
|
||||
@property(nonatomic, assign) BOOL showTakePhotoButton;
|
||||
@property(nonatomic, assign) BOOL showAlbumButton;
|
||||
@property(nonatomic, assign) BOOL showFileButton;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class is used to register event listeners for Chat from external sources, to listen for various events in Chat and respond accordingly,
|
||||
* such as listening for avatar click events, long-press message events, etc.
|
||||
* You need to set a delegate for the implementation method: TUIChatConfig.defaultConfig.eventConfig.chatEventListener = "YourDelegateViewController".
|
||||
* YourDelegateViewController needs to conform to the <TUIChatEventListener> protocol and implement the protocol method.
|
||||
* Taking - (BOOL)onUserIconClicked:messageCellData: as an example, returning NO indicates an insertion behavior,
|
||||
* which is not intercepted and will be further processed by the Chat module.
|
||||
* Taking - (BOOL)onUserIconClicked:messageCellData: as an example, returning YES indicates an override behavior,
|
||||
* which will be intercepted and only the overridden method will be executed. The Chat module will not continue to process it.
|
||||
*/
|
||||
@property(nonatomic, strong) TUIChatEventConfig * eventConfig;
|
||||
/**
|
||||
* DataSource for inputBar.
|
||||
*/
|
||||
@property (nonatomic, weak) id<TUIChatInputBarConfigDataSource> inputBarDataSource;
|
||||
/**
|
||||
* DataSource for shortcutView above inputBar.
|
||||
*/
|
||||
@property (nonatomic, weak) id<TUIChatShortcutViewDataSource> shortcutViewDataSource;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol TUIChatEventListener <NSObject>
|
||||
|
||||
/**
|
||||
* This callback is triggered when a user avatar in the chat list interface 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)onUserIconClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
|
||||
/**
|
||||
* This callback is triggered when a user avatar in the chat list interface 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)onUserIconLongClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
|
||||
|
||||
/**
|
||||
* This callback is triggered when a message in the chat list interface 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;
|
||||
|
||||
/**
|
||||
* This callback is triggered when a message in the chat list interface 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)onMessageLongClicked:(UIView *)view messageCellData:(TUIMessageCellData *)celldata;
|
||||
@end
|
||||
|
||||
@interface TUIChatEventConfig : NSObject
|
||||
@property (nonatomic,weak)id <TUIChatEventListener>chatEventListener;
|
||||
@end
|
||||
|
||||
// Regiser custom message category
|
||||
// You can call this method like :
|
||||
//
|
||||
// [TUIChatConfig.defaultConfig registerCustomMessage:@"YourBusinessID"
|
||||
// messageCellClassName:@"YourCustomCellNameString"
|
||||
// messageCellDataClassName:@"YourCustomCellDataNameString"];
|
||||
@interface TUIChatConfig (CustomMessageRegiser)
|
||||
|
||||
/**
|
||||
* Register custom message , by default, register to the classic UI.
|
||||
* param businessID Custom message businessID (note that it must be unique)
|
||||
* param cellName Custom message messagCell type
|
||||
* param cellDataName Custom message MessagCellData type
|
||||
*/
|
||||
- (void)registerCustomMessage:(NSString *)businessID
|
||||
messageCellClassName:(NSString *)cellName
|
||||
messageCellDataClassName:(NSString *)cellDataName;
|
||||
|
||||
/**
|
||||
* Register custom message
|
||||
* param businessID Custom message businessID (note that it must be unique)
|
||||
* param cellName Custom message messagCell type
|
||||
* param cellDataName Custom message MessagCellData type
|
||||
* param styleType UI style corresponding to this custom message, for example TUIChatRegisterCustomMessageStyleTypeClassic
|
||||
*/
|
||||
- (void)registerCustomMessage:(NSString *)businessID
|
||||
messageCellClassName:(NSString *)cellName
|
||||
messageCellDataClassName:(NSString *)cellDataName
|
||||
styleType:(TUIChatRegisterCustomMessageStyleType)styleType;
|
||||
@end
|
||||
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
122
TUIKit/TUIChat/CommonModel/TUIChatConfig.m
Normal file
122
TUIKit/TUIChat/CommonModel/TUIChatConfig.m
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// TUIChatConfig.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by wyl on 2022/6/10.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatConfig.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TIMCommonMediator.h>
|
||||
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
@implementation TUIChatConfig
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.msgNeedReadReceipt = YES;
|
||||
self.enableVideoCall = YES;
|
||||
self.enableAudioCall = YES;
|
||||
self.enableWelcomeCustomMessage = YES;
|
||||
self.showFileButton = YES;
|
||||
self.showAlbumButton = YES;
|
||||
self.showTakePhotoButton = YES;
|
||||
self.showRecordVideoButton = YES;
|
||||
self.showGroupNoteButton = YES;
|
||||
self.showPollButton = YES;
|
||||
self.showRoomButton = YES;
|
||||
self.enablePopMenuEmojiReactAction = YES;
|
||||
self.enablePopMenuReplyAction = YES;
|
||||
self.enablePopMenuReferenceAction = YES;
|
||||
self.enablePopMenuPinAction = YES;
|
||||
self.enablePopMenuRecallAction = YES;
|
||||
self.enablePopMenuTranslateAction = YES;
|
||||
self.enablePopMenuConvertAction = YES;
|
||||
self.enablePopMenuForwardAction = YES;
|
||||
self.enablePopMenuSelectAction = YES;
|
||||
self.enablePopMenuCopyAction = YES;
|
||||
self.enablePopMenuDeleteAction = YES;
|
||||
self.enablePopMenuInfoAction = YES;
|
||||
self.enablePopMenuAudioPlaybackAction = YES;
|
||||
self.enableMainPageInputBar = YES;
|
||||
self.enableTypingStatus = YES;
|
||||
self.enableFloatWindowForCall = YES;
|
||||
self.enableMultiDeviceForCall = NO;
|
||||
self.enableIncomingBanner = YES;
|
||||
self.enableVirtualBackgroundForCall = NO;
|
||||
self.timeIntervalForMessageRecall = 120;
|
||||
self.maxAudioRecordDuration = 60;
|
||||
self.maxVideoRecordDuration = 15;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (TUIChatConfig *)defaultConfig {
|
||||
static dispatch_once_t onceToken;
|
||||
static TUIChatConfig *config;
|
||||
dispatch_once(&onceToken, ^{
|
||||
config = [[TUIChatConfig alloc] init];
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
- (NSArray<TUIFaceGroup *> *)chatContextEmojiDetailGroups {
|
||||
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
|
||||
return [service getChatContextEmojiDetailGroups];
|
||||
}
|
||||
|
||||
- (TUIChatEventConfig *)eventConfig {
|
||||
if (!_eventConfig) {
|
||||
_eventConfig = [[TUIChatEventConfig alloc] init];
|
||||
}
|
||||
return _eventConfig;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatEventConfig
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation TUIChatConfig (CustomMessageRegiser)
|
||||
|
||||
- (void)registerCustomMessage:(NSString *)businessID
|
||||
messageCellClassName:(NSString *)cellName
|
||||
messageCellDataClassName:(NSString *)cellDataName {
|
||||
[self registerCustomMessage:businessID
|
||||
messageCellClassName:cellName
|
||||
messageCellDataClassName:cellDataName
|
||||
styleType:TUIChatRegisterCustomMessageStyleTypeClassic];
|
||||
}
|
||||
|
||||
- (void)registerCustomMessage:(NSString *)businessID
|
||||
messageCellClassName:(NSString *)cellName
|
||||
messageCellDataClassName:(NSString *)cellDataName
|
||||
styleType:(TUIChatRegisterCustomMessageStyleType)styleType {
|
||||
|
||||
if (businessID.length <0 || cellName.length <0 ||cellDataName.length <0) {
|
||||
NSLog(@"registerCustomMessage Error, check info %s", __func__);
|
||||
return;
|
||||
}
|
||||
NSString * serviceName = @"";
|
||||
if (styleType == TUIChatRegisterCustomMessageStyleTypeClassic) {
|
||||
serviceName = TUICore_TUIChatService;
|
||||
}
|
||||
else {
|
||||
serviceName = TUICore_TUIChatService_Minimalist;
|
||||
}
|
||||
[TUICore callService:serviceName
|
||||
method:TUICore_TUIChatService_AppendCustomMessageMethod
|
||||
param:@{BussinessID : businessID,
|
||||
TMessageCell_Name : cellName,
|
||||
TMessageCell_Data_Name : cellDataName
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
121
TUIKit/TUIChat/CommonModel/TUIChatConversationModel.h
Normal file
121
TUIKit/TUIChat/CommonModel/TUIChatConversationModel.h
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// TUIChatConversationModel.h
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/8/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@import UIKit;
|
||||
@class TUIChatShortcutMenuCellData;
|
||||
@class TUIInputMoreCellData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIChatConversationModel : NSObject
|
||||
|
||||
/**
|
||||
* UniqueID for a conversation
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *conversationID;
|
||||
|
||||
/**
|
||||
* If the conversation type is group chat, the groupID means group id
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *groupID;
|
||||
|
||||
/**
|
||||
* Group type
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *groupType;
|
||||
|
||||
/**
|
||||
* If the conversation type is one-to-one chat, the userID means peer user id
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *userID;
|
||||
|
||||
/**
|
||||
* title
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *title;
|
||||
|
||||
/**
|
||||
* The avatar of the user or group corresponding to the conversation
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *faceUrl;
|
||||
|
||||
/**
|
||||
* Image for avatar
|
||||
*/
|
||||
@property(nonatomic, strong) UIImage *avatarImage;
|
||||
|
||||
/**
|
||||
*
|
||||
* Conversation draft
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *draftText;
|
||||
|
||||
/**
|
||||
* Group@ message tip string
|
||||
*/
|
||||
@property(nonatomic, strong) NSString *atTipsStr;
|
||||
|
||||
/**
|
||||
* Sequence list of group-at message
|
||||
*/
|
||||
@property(nonatomic, strong) NSMutableArray<NSNumber *> *atMsgSeqs;
|
||||
|
||||
/**
|
||||
* The input status of the other Side (C2C Only)
|
||||
*/
|
||||
|
||||
@property(nonatomic, assign) BOOL otherSideTyping;
|
||||
|
||||
/**
|
||||
* A read receipt is required to send a message, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL msgNeedReadReceipt;
|
||||
|
||||
/**
|
||||
* Display the video call button, if the TUICalling component is integrated, the default is YES
|
||||
*/
|
||||
|
||||
@property(nonatomic, assign) BOOL enableVideoCall;
|
||||
|
||||
/**
|
||||
* Whether to display the audio call button, if the TUICalling component is integrated, the default is YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableAudioCall;
|
||||
|
||||
/**
|
||||
* Display custom welcome message button, default YES
|
||||
*/
|
||||
@property(nonatomic, assign) BOOL enableWelcomeCustomMessage;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableRoom;
|
||||
|
||||
@property(nonatomic, assign) BOOL isLimitedPortraitOrientation;
|
||||
|
||||
@property(nonatomic, assign) BOOL enablePoll;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableGroupNote;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableTakePhoto;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableRecordVideo;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableAlbum;
|
||||
|
||||
@property(nonatomic, assign) BOOL enableFile;
|
||||
|
||||
@property (nonatomic, copy) NSArray *customizedNewItemsInMoreMenu;
|
||||
|
||||
@property (nonatomic, strong) UIColor *shortcutViewBackgroundColor;
|
||||
@property (nonatomic, assign) CGFloat shortcutViewHeight;
|
||||
@property (nonatomic, strong) NSArray<TUIChatShortcutMenuCellData *> *shortcutMenuItems;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
31
TUIKit/TUIChat/CommonModel/TUIChatConversationModel.m
Normal file
31
TUIKit/TUIChat/CommonModel/TUIChatConversationModel.m
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// TUIChatConversationModel.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by kayev on 2021/8/12.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatConversationModel.h"
|
||||
|
||||
@implementation TUIChatConversationModel
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self){
|
||||
self.msgNeedReadReceipt = YES;
|
||||
self.enableVideoCall = YES;
|
||||
self.enableAudioCall = YES;
|
||||
self.enableRoom = YES;
|
||||
self.enableWelcomeCustomMessage = YES;
|
||||
self.isLimitedPortraitOrientation = NO;
|
||||
self.enablePoll = YES;
|
||||
self.enableGroupNote = YES;
|
||||
self.enableTakePhoto = YES;
|
||||
self.enableRecordVideo = YES;
|
||||
self.enableAlbum = YES;
|
||||
self.enableFile = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user