This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,81 @@
//
// TUIChatDefine.h
// Pods
//
// Created by xiangzhang on 2022/10/14.
// Copyright © 2023 Tencent. All rights reserved.
//
#ifndef TUI_COMPONENTS_IOS_TUICHAT_COMMONMODEL_TUICHATDEFINE_H_
#define TUI_COMPONENTS_IOS_TUICHAT_COMMONMODEL_TUICHATDEFINE_H_
static NSString* const kMemberCellReuseId = @"kMemberCellReuseId";
typedef void (^TUIImageMessageDownloadCallback)(void);
typedef void (^TUIVideoMessageDownloadCallback)(void);
typedef void (^TUIReplyAsyncLoadFinish)(void);
typedef void (^TUIInputPreviewBarCallback)(void);
typedef void (^TUIReplyQuoteAsyncLoadFinish)(void);
typedef void (^TUIChatSelectAllContentCallback)(BOOL);
typedef void (^TUIReferenceSelectAllContentCallback)(BOOL);
typedef void (^TUIReplySelectAllContentCallback)(BOOL);
typedef NS_ENUM(NSInteger, TUIMultiResultOption) {
/**
*
* Get all selected results
*/
TUIMultiResultOptionAll = 0,
/**
*
* Filter out data that does not support forwarding
*/
TUIMultiResultOptionFiterUnsupportRelay = 1 << 0,
};
typedef NS_ENUM(NSInteger, TUIMessageReadViewTag) {
TUIMessageReadViewTagUnknown = 0, // unknown
TUIMessageReadViewTagRead, // read group members
TUIMessageReadViewTagUnread, // unread group members
TUIMessageReadViewTagReadDisable, // disable read group members
TUIMessageReadViewTagC2C, // c2c member
};
typedef NS_ENUM(NSUInteger, InputStatus) {
Input_Status_Input,
Input_Status_Input_Face,
Input_Status_Input_More,
Input_Status_Input_Keyboard,
Input_Status_Input_Talk,
Input_Status_Input_Camera,
};
typedef NS_ENUM(NSUInteger, RecordStatus) {
Record_Status_TooShort,
Record_Status_TooLong,
Record_Status_Recording,
Record_Status_Cancel,
};
typedef NS_ENUM(NSInteger, TUIChatSmallTongueType) {
TUIChatSmallTongueType_None,
TUIChatSmallTongueType_ScrollToBoom,
TUIChatSmallTongueType_ReceiveNewMsg,
TUIChatSmallTongueType_SomeoneAt,
};
#define TUITencentCloudHomePageCN @"https://cloud.tencent.com/document/product/269/68228"
#define TUITencentCloudHomePageEN @"https://www.tencentcloud.com/document/product/1047/45913"
#define TUIChatSendMessageNotification @"TUIChatSendMessageNotification"
#define TUIChatSendMessageWithoutUpdateUINotification @"TUIChatSendMessageWithoutUpdateUINotification"
#define TUIChatInsertMessageWithoutUpdateUINotification @"TUIChatInsertMessageWithoutUpdateUINotification"
#endif // TUI_COMPONENTS_IOS_TUICHAT_COMMONMODEL_TUICHATDEFINE_H_

View File

@@ -0,0 +1,32 @@
//
// TUIChatMediaSendingManager.h
// TUIChat
//
// Created by yiliangwang on 2025/1/6.
// Copyright © 2025 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
#import <TIMCommon/TUIMessageCellData.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIChatMediaTask : NSObject
@property (nonatomic, strong) TUIMessageCellData* placeHolderCellData;
@property (nonatomic, copy) NSString *msgID;
@property (nonatomic, copy) NSString *conversationID;
@end
@interface TUIChatMediaSendingManager : NSObject
@property (nonatomic, strong) NSMutableDictionary<NSString *, TUIChatMediaTask *> *tasks;
@property (nonatomic, strong) NSHashTable<UIViewController *> *mediaSendingControllers;
+ (instancetype)sharedInstance;
- (void)addMediaTask:(TUIChatMediaTask *)task forKey:(NSString *)key;
- (void)updateProgress:(float)progress forKey:(NSString *)key;
- (void)removeMediaTaskForKey:(NSString *)key;
- (NSMutableArray<TUIChatMediaTask *> *)findPlaceHolderListByConversationID:(NSString *)conversationID;
- (void)addCurrentVC:(UIViewController *)vc;
- (void)removeCurrentVC:(UIViewController *)vc;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,62 @@
//
// TUIChatMediaSendingManager.m
// TUIChat
//
// Created by yiliangwang on 2025/1/6.
// Copyright © 2025 Tencent. All rights reserved.
#import "TUIChatMediaSendingManager.h"
@implementation TUIChatMediaTask
@end
@implementation TUIChatMediaSendingManager
+ (instancetype)sharedInstance {
static TUIChatMediaSendingManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[TUIChatMediaSendingManager alloc] init];
instance.tasks = [NSMutableDictionary dictionary];
instance.mediaSendingControllers = [NSHashTable weakObjectsHashTable];
});
return instance;
}
- (void)addMediaTask:(TUIChatMediaTask *)task forKey:(NSString *)key {
self.tasks[key] = task;
}
- (void)updateProgress:(float)progress forKey:(NSString *)key {
TUIChatMediaTask *task = self.tasks[key];;
TUIMessageCellData *message = task.placeHolderCellData;
if (message) {
message.videoTranscodingProgress = progress;
}
}
- (void)removeMediaTaskForKey:(NSString *)key {
[self.tasks removeObjectForKey:key];
}
- (NSMutableArray<TUIChatMediaTask *> *)findPlaceHolderListByConversationID:(NSString *)conversationID {
NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:1];
for (TUIChatMediaTask * task in self.tasks.allValues) {
if ([task.conversationID isEqualToString:conversationID]) {
[tasks addObject:task];
}
}
return tasks;
}
- (void)addCurrentVC:(UIViewController *)vc {
[self.mediaSendingControllers addObject:vc];
}
- (void)removeCurrentVC:(UIViewController *)vc {
[self.mediaSendingControllers removeObject:vc];
}
@end

View File

@@ -0,0 +1,20 @@
//
// TUIChatModifyMessageHelper.h
// TUIChat
//
// Created by wyl on 2022/6/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
@import ImSDK_Plus;
@interface TUIChatModifyMessageHelper : NSObject
+ (TUIChatModifyMessageHelper *)defaultHelper;
- (void)modifyMessage:(V2TIMMessage *)msg simpleCurrentContent:(NSDictionary *)simpleCurrentContent;
- (void)modifyMessage:(V2TIMMessage *)msg revokeMsgID:(NSString *)revokeMsgID;
@end

View File

@@ -0,0 +1,340 @@
//
// TUIChatModifyMessageHelper.m
// TUIChat
//
// Created by wyl on 2022/6/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatModifyMessageHelper.h"
#import <TUICore/TUILogin.h>
#import <TUICore/TUITool.h>
#import "TUICloudCustomDataTypeCenter.h"
#define RETRY_MIN_TIME 500
#define RETRY_MAX_TIME 3000
#define TIMES_CONTROL 3
@interface TUIChatModifyMessageObj : NSObject
@property(nonatomic, assign) NSInteger time;
@property(nonatomic, copy) NSString *msgID;
@property(nonatomic, strong) V2TIMMessage *msg;
// reply
@property(nonatomic, strong) NSDictionary *simpleCurrentContent;
// revoke
@property(nonatomic, copy) NSString *revokeMsgID;
@end
@implementation TUIChatModifyMessageObj
- (instancetype)init {
if (self = [super init]) {
self.time = 0;
}
return self;
}
- (V2TIMMessage *)resolveOriginCloudCustomData:(V2TIMMessage *)rootMsg {
if (self.simpleCurrentContent) {
return [self.class resolveOriginCloudCustomData:rootMsg simpleCurrentContent:self.simpleCurrentContent];
}
if (self.revokeMsgID) {
return [self.class resolveOriginCloudCustomData:rootMsg revokeMsgID:self.revokeMsgID];
}
return rootMsg;
}
// Input
+ (V2TIMMessage *)resolveOriginCloudCustomData:(V2TIMMessage *)rootMsg simpleCurrentContent:(NSDictionary *)simpleCurrentContent {
NSMutableDictionary *mudic = [[NSMutableDictionary alloc] initWithCapacity:5];
NSMutableArray *replies = [[NSMutableArray alloc] initWithCapacity:5];
NSMutableDictionary *messageReplies = [[NSMutableDictionary alloc] initWithCapacity:5];
if (rootMsg.cloudCustomData) {
NSDictionary *originDic = [TUITool jsonData2Dictionary:rootMsg.cloudCustomData];
if (originDic && [originDic isKindOfClass:[NSDictionary class]]) {
[messageReplies addEntriesFromDictionary:originDic];
}
NSArray *messageRepliesArray = originDic[@"messageReplies"][@"replies"];
if (messageRepliesArray && [messageRepliesArray isKindOfClass:NSArray.class] && messageRepliesArray.count > 0) {
[replies addObjectsFromArray:messageRepliesArray];
}
}
[replies addObject:simpleCurrentContent];
[mudic setValue:replies forKey:@"replies"];
[messageReplies setValue:mudic forKey:@"messageReplies"];
[messageReplies setValue:@"1" forKey:@"version"];
NSData *data = [TUITool dictionary2JsonData:messageReplies];
if (data) {
rootMsg.cloudCustomData = data;
}
return rootMsg;
}
// revoke
+ (V2TIMMessage *)resolveOriginCloudCustomData:(V2TIMMessage *)rootMsg revokeMsgID:(NSString *)revokeMsgId {
NSMutableDictionary *mudic = [[NSMutableDictionary alloc] initWithCapacity:5];
NSMutableArray *replies = [[NSMutableArray alloc] initWithCapacity:5];
NSMutableDictionary *messageReplies = [[NSMutableDictionary alloc] initWithCapacity:5];
if (rootMsg.cloudCustomData) {
NSDictionary *originDic = [TUITool jsonData2Dictionary:rootMsg.cloudCustomData];
if (originDic && [originDic isKindOfClass:[NSDictionary class]]) {
[messageReplies addEntriesFromDictionary:originDic];
if (originDic[@"messageReplies"] && [originDic[@"messageReplies"] isKindOfClass:[NSDictionary class]]) {
NSArray *messageRepliesArray = originDic[@"messageReplies"][@"replies"];
if (messageRepliesArray && [messageRepliesArray isKindOfClass:NSArray.class] && messageRepliesArray.count > 0) {
[replies addObjectsFromArray:messageRepliesArray];
}
}
}
}
NSMutableArray *filterReplies = [[NSMutableArray alloc] initWithCapacity:5];
[replies enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
if (obj && [obj isKindOfClass:NSDictionary.class]) {
NSDictionary *dic = (NSDictionary *)obj;
if (IS_NOT_EMPTY_NSSTRING(dic[@"messageID"])) {
NSString *messageID = dic[@"messageID"];
if (![messageID isEqualToString:revokeMsgId]) {
[filterReplies addObject:dic];
}
}
}
}];
[mudic setValue:filterReplies forKey:@"replies"];
[messageReplies setValue:mudic forKey:@"messageReplies"];
[messageReplies setValue:@"1" forKey:@"version"];
NSData *data = [TUITool dictionary2JsonData:messageReplies];
if (data) {
rootMsg.cloudCustomData = data;
}
return rootMsg;
}
@end
@interface ModifyCustomOperation : NSOperation
@property(nonatomic, strong) TUIChatModifyMessageObj *obj;
@property(nonatomic, copy) void (^SuccessBlock)(void);
@property(nonatomic, copy) void (^FailedBlock)(int code, NSString *desc, V2TIMMessage *msg);
@property(nonatomic, assign) BOOL bees_executing;
@property(nonatomic, assign) BOOL bees_finished;
@end
@implementation ModifyCustomOperation
- (void)dealloc {
NSLog(@"operation-------dealloc");
}
- (void)start {
if (self.isCancelled) {
[self completeOperation];
return;
}
__block V2TIMMessage *resolveMsg = nil;
__weak typeof(self) weakSelf = self;
self.bees_executing = YES;
resolveMsg = [self.obj resolveOriginCloudCustomData:self.obj.msg];
[[V2TIMManager sharedInstance] modifyMessage:resolveMsg
completion:^(int code, NSString *desc, V2TIMMessage *msg) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (code != 0) {
if (strongSelf.FailedBlock) {
strongSelf.FailedBlock(code, desc, msg);
}
} else {
if (strongSelf.SuccessBlock) {
strongSelf.SuccessBlock();
}
}
[strongSelf completeOperation];
}];
}
- (void)completeOperation {
self.bees_executing = NO;
self.bees_finished = YES;
}
- (void)cancel {
[super cancel];
[self completeOperation];
}
#pragma mark - settter and getter
- (void)setBees_executing:(BOOL)bees_executing {
[self willChangeValueForKey:@"isExecuting"];
_bees_executing = bees_executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setBees_finished:(BOOL)bees_finished {
[self willChangeValueForKey:@"isFinished"];
_bees_finished = bees_finished;
[self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isExecuting {
return self.bees_executing;
}
- (BOOL)isFinished {
return self.bees_finished;
}
@end
@interface TUIChatModifyMessageHelper () <V2TIMAdvancedMsgListener>
@property(nonatomic, strong) NSMutableDictionary<NSString *, TUIChatModifyMessageObj *> *modifyMessageHelperMap;
@property(nonatomic, strong) NSOperationQueue *queue;
@end
@implementation TUIChatModifyMessageHelper
+ (TUIChatModifyMessageHelper *)defaultHelper {
static dispatch_once_t onceToken;
static TUIChatModifyMessageHelper *config;
dispatch_once(&onceToken, ^{
config = [[TUIChatModifyMessageHelper alloc] init];
});
return config;
}
- (id)init {
self = [super init];
if (self) {
[self registerTUIKitNotification];
self.modifyMessageHelperMap = [NSMutableDictionary dictionaryWithCapacity:10];
[self setupOpQueue];
}
return self;
}
- (void)setupOpQueue {
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 1;
}
- (void)registerTUIKitNotification {
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
}
- (void)onRecvMessageModified:(V2TIMMessage *)msg {
NSString *msgID = msg.msgID;
TUIChatModifyMessageObj *obj = self.modifyMessageHelperMap[msgID];
if (obj && [obj isKindOfClass:[TUIChatModifyMessageObj class]]) {
// update;
obj.msg = msg;
}
}
#pragma mark - public
- (void)modifyMessage:(V2TIMMessage *)msg reactEmoji:(NSString *)emojiName {
[self modifyMessage:msg reactEmoji:nil simpleCurrentContent:nil revokeMsgID:nil timeControl:0];
}
- (void)modifyMessage:(V2TIMMessage *)msg simpleCurrentContent:(NSDictionary *)simpleCurrentContent {
[self modifyMessage:msg reactEmoji:nil simpleCurrentContent:simpleCurrentContent revokeMsgID:nil timeControl:0];
}
- (void)modifyMessage:(V2TIMMessage *)msg revokeMsgID:(NSString *)revokeMsgID {
[self modifyMessage:msg reactEmoji:nil simpleCurrentContent:nil revokeMsgID:revokeMsgID timeControl:0];
}
#pragma mark - private
- (void)modifyMessage:(V2TIMMessage *)msg
reactEmoji:(NSString *)emojiName
simpleCurrentContent:(NSDictionary *)simpleCurrentContent
revokeMsgID:(NSString *)revokeMsgID
timeControl:(NSInteger)time {
NSString *msgID = msg.msgID;
if (!IS_NOT_EMPTY_NSSTRING(msgID)) {
return;
}
TUIChatModifyMessageObj *obj = [[TUIChatModifyMessageObj alloc] init];
obj.msgID = msgID;
obj.msg = msg;
obj.time = time;
if (simpleCurrentContent) {
obj.simpleCurrentContent = simpleCurrentContent;
}
if (revokeMsgID) {
obj.revokeMsgID = revokeMsgID;
}
[self.modifyMessageHelperMap setObject:obj forKey:obj.msgID];
__weak typeof(self) weakSelf = self;
ModifyCustomOperation *modifyop = [[ModifyCustomOperation alloc] init];
modifyop.obj = obj;
modifyop.SuccessBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.modifyMessageHelperMap removeObjectForKey:msgID];
};
modifyop.FailedBlock = ^(int code, NSString *desc, V2TIMMessage *msg) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (obj.time <= TIMES_CONTROL) {
int delay;
delay = [self getRandomNumber:RETRY_MIN_TIME to:RETRY_MAX_TIME];
TUIChatModifyMessageObj *obj = strongSelf.modifyMessageHelperMap[msgID];
// update
obj.msg = msg;
obj.time = obj.time + 1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
TUIChatModifyMessageObj *obj = strongSelf.modifyMessageHelperMap[msgID];
if (obj && [obj isKindOfClass:[TUIChatModifyMessageObj class]]) {
[strongSelf modifyMessage:obj.msg
reactEmoji:nil
simpleCurrentContent:obj.simpleCurrentContent
revokeMsgID:obj.revokeMsgID
timeControl:obj.time];
}
});
} else {
[strongSelf.modifyMessageHelperMap removeObjectForKey:msgID];
}
};
if (!modifyop.isCancelled) {
[self.queue addOperation:modifyop];
}
}
- (int)getRandomNumber:(int)from to:(int)to {
return (int)(from + (arc4random() % (to - from + 1)));
}
@end

View File

@@ -0,0 +1,21 @@
//
// TUICircleLodingView.h
// TUIChat
//
// Created by wyl on 2023/4/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICircleLodingView : UIView
@property(nonatomic, strong) UILabel *labProgress;
@property(nonatomic, strong) CAShapeLayer *progressLayer;
@property(nonatomic, strong) CAShapeLayer *grayProgressLayer;
// 【0,1.0】
@property(nonatomic, assign) double progress;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
//
// TUICircleLodingView.m
// TUIChat
//
// Created by wyl on 2023/4/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUICircleLodingView.h"
#define kCircleUnFillColor [[UIColor whiteColor] colorWithAlphaComponent:0.4]
#define kCircleFillColor [UIColor whiteColor]
@interface TUICircleLodingView ()
@end
@implementation TUICircleLodingView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self drawProgressCircleWithEndAngle:-M_PI_2 + M_PI * 2 isGrayCircle:YES];
}
return self;
}
- (void)setProgress:(double)progress {
_progress = progress;
self.labProgress.text = [NSString stringWithFormat:@"%.0f%%", progress];
[self drawProgress];
}
- (void)drawProgress {
if (self.progress >= 100) {
return;
}
[self drawProgressCircleWithEndAngle:-M_PI_2 + M_PI * 2 * (self.progress) * 0.01 isGrayCircle:NO];
}
- (void)drawProgressCircleWithEndAngle:(CGFloat)endAngle isGrayCircle:(BOOL)isGrayCircle {
CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.width / 2);
CGFloat radius = self.frame.size.width / 2;
CGFloat startA = -M_PI_2;
CGFloat endA = endAngle;
CAShapeLayer *layer;
if (isGrayCircle) {
layer = self.grayProgressLayer;
} else {
layer = self.progressLayer;
}
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES];
layer.path = [path CGPath];
}
- (UILabel *)labProgress {
if (!_labProgress) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
label.textAlignment = NSTextAlignmentCenter;
label.center = CGPointMake(self.frame.size.width / 2, self.frame.size.width / 2);
label.textColor = kCircleFillColor;
label.font = [UIFont systemFontOfSize:10];
[self addSubview:label];
_labProgress = label;
}
return _labProgress;
}
- (CAShapeLayer *)grayProgressLayer {
if (!_grayProgressLayer) {
_grayProgressLayer = [CAShapeLayer layer];
_grayProgressLayer.frame = self.bounds;
_grayProgressLayer.fillColor = [[UIColor clearColor] CGColor];
_grayProgressLayer.strokeColor = kCircleUnFillColor.CGColor;
_grayProgressLayer.opacity = 1;
_grayProgressLayer.lineCap = kCALineCapRound;
_grayProgressLayer.lineWidth = 3;
[self.layer addSublayer:_grayProgressLayer];
}
return _grayProgressLayer;
}
- (CAShapeLayer *)progressLayer {
if (!_progressLayer) {
_progressLayer = [CAShapeLayer layer];
_progressLayer.frame = self.bounds;
_progressLayer.fillColor = [[UIColor clearColor] CGColor];
_progressLayer.strokeColor = kCircleFillColor.CGColor;
_progressLayer.opacity = 1;
_progressLayer.lineCap = kCALineCapRound;
_progressLayer.lineWidth = 3;
[self.layer addSublayer:_progressLayer];
}
return _progressLayer;
}
@end

View File

@@ -0,0 +1,53 @@
//
// TUICloudCustomDataTypeCenter.h
// TUIChat
//
// Created by wyl on 2022/4/29.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ImSDK_Plus/ImSDK_Plus.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_OPTIONS(NSUInteger, TUICloudCustomDataType) {
TUICloudCustomDataType_None = 1 << 0,
TUICloudCustomDataType_MessageReply = 1 << 1,
// TUICloudCustomDataType_MessageReact = 1 << 2,
TUICloudCustomDataType_MessageReplies = 1 << 3,
TUICloudCustomDataType_MessageReference = 1 << 4,
};
typedef NSString *TUICustomType;
FOUNDATION_EXTERN TUICustomType messageFeature;
@interface V2TIMMessage (CloudCustomDataType)
- (void)doThingsInContainsCloudCustomOfDataType:(TUICloudCustomDataType)type callback:(void (^)(BOOL isContains, id obj))callback;
/**
* Whether this state is included
*/
- (BOOL)isContainsCloudCustomOfDataType:(TUICloudCustomDataType)type;
/**
* Parse data of specified type
* The return value is: data of type NSDictionary/NSArray/NSString/NSNumber
*/
- (NSObject *)parseCloudCustomData:(TUICustomType)customType;
/**
* Set the specified type of data
* jsonData: NSDictionary/NSArray/NSString/NSNumber type data, which can be directly converted to json
*/
- (void)setCloudCustomData:(NSObject *)jsonData forType:(TUICustomType)customType;
- (void)modifyIfNeeded:(V2TIMMessageModifyCompletion)callback;
@end
@interface TUICloudCustomDataTypeCenter : NSObject
+ (NSString *)convertType2String:(TUICloudCustomDataType)type;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,250 @@
//
// TUICloudCustomDataTypeCenter.m
// TUIChat
//
// Created by wyl on 2022/4/29.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUICloudCustomDataTypeCenter.h"
#import <TIMCommon/TIMDefine.h>
TUICustomType messageFeature = @"messageFeature";
@implementation V2TIMMessage (CloudCustomDataType)
- (BOOL)hasAnyCloudCustomDataType {
if (self.cloudCustomData == nil) {
return NO;
}
return YES;
}
- (void)doThingsInContainsCloudCustomOfDataType:(TUICloudCustomDataType)type callback:(void (^)(BOOL isContains, id obj))callback {
if (self.cloudCustomData == nil) {
if (callback) {
callback(NO, nil);
}
}
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.cloudCustomData options:0 error:&error];
NSString *typeStr = [TUICloudCustomDataTypeCenter convertType2String:type];
if (![typeStr isKindOfClass:[NSString class]] || typeStr.length <= 0) {
if (callback) {
callback(NO, nil);
}
}
if (error || dict == nil || ![dict isKindOfClass:NSDictionary.class] || ![dict.allKeys containsObject:typeStr]) {
if (callback) {
callback(NO, nil);
}
return;
}
// extra condition
if (type == TUICloudCustomDataType_MessageReply) {
NSDictionary *reply = [dict valueForKey:typeStr];
if (reply == nil || ![reply isKindOfClass:NSDictionary.class]) {
if (callback) {
callback(NO, nil);
}
return;
}
if (![reply.allKeys containsObject:@"version"] || [reply[@"version"] intValue] > kMessageReplyVersion) {
NSLog(@"not match the version of message rely");
if (callback) {
callback(NO, nil);
}
return;
}
if (callback) {
callback(YES, reply);
}
}
if (type == TUICloudCustomDataType_MessageReference) {
NSDictionary *reply = [dict valueForKey:typeStr];
if (reply == nil || ![reply isKindOfClass:NSDictionary.class]) {
if (callback) {
callback(NO, nil);
}
return;
}
if (![reply.allKeys containsObject:@"version"] || [reply[@"version"] intValue] > kMessageReplyVersion) {
NSLog(@"not match the version of message rely");
if (callback) {
callback(NO, nil);
}
return;
}
if ([reply.allKeys containsObject:@"messageRootID"]) {
if (callback) {
callback(NO, nil);
}
return;
}
if (callback) {
callback(YES, reply);
}
return;
}
if (type == TUICloudCustomDataType_MessageReplies) {
NSDictionary *messageReplies = [dict valueForKey:typeStr];
NSArray *reply = [messageReplies valueForKey:@"replies"];
if (reply == nil || ![reply isKindOfClass:NSArray.class]) {
if (callback) {
callback(NO, nil);
}
return;
}
if (reply.count <= 0) {
if (callback) {
callback(NO, nil);
}
return;
}
if (callback) {
callback(YES, dict);
}
return;
}
return;
}
- (BOOL)isContainsCloudCustomOfDataType:(TUICloudCustomDataType)type {
if (self.cloudCustomData == nil) {
return NO;
}
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.cloudCustomData options:0 error:&error];
NSString *typeStr = [TUICloudCustomDataTypeCenter convertType2String:type];
// NSDictionary *customElemDic = [NSJSONSerialization JSONObjectWithData:self.customElem.data options:0 error:&error];
if (![typeStr isKindOfClass:[NSString class]] || typeStr.length <= 0) {
return NO;
}
if (error || dict == nil || ![dict isKindOfClass:NSDictionary.class] || ![dict.allKeys containsObject:typeStr]) {
return NO;
}
// extra condition
if (type == TUICloudCustomDataType_MessageReply) {
NSDictionary *reply = [dict valueForKey:typeStr];
if (reply == nil || ![reply isKindOfClass:NSDictionary.class]) {
return NO;
}
if (![reply.allKeys containsObject:@"version"] || [reply[@"version"] intValue] > kMessageReplyVersion) {
NSLog(@"not match the version of message rely");
return NO;
}
if (![reply.allKeys containsObject:@"messageRootID"]) {
return NO;
}
return YES;
}
if (type == TUICloudCustomDataType_MessageReference) {
NSDictionary *reply = [dict valueForKey:typeStr];
if (reply == nil || ![reply isKindOfClass:NSDictionary.class]) {
return NO;
}
if (![reply.allKeys containsObject:@"version"] || [reply[@"version"] intValue] > kMessageReplyVersion) {
NSLog(@"not match the version of message rely");
return NO;
}
if ([reply.allKeys containsObject:@"messageRootID"]) {
return NO;
}
return YES;
}
if (type == TUICloudCustomDataType_MessageReplies) {
NSDictionary *messageReplies = [dict valueForKey:typeStr];
NSArray *reply = [messageReplies valueForKey:@"replies"];
if (reply == nil || ![reply isKindOfClass:NSArray.class]) {
return NO;
}
if (reply.count <= 0) {
return NO;
}
return YES;
}
return NO;
}
- (NSObject *)parseCloudCustomData:(TUICustomType)customType {
if (self.cloudCustomData == nil || customType.length == 0) {
return nil;
}
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.cloudCustomData options:0 error:&error];
if (error || dict == nil || ![dict isKindOfClass:NSDictionary.class] || ![dict.allKeys containsObject:customType]) {
return nil;
}
return [dict objectForKey:customType];
}
- (void)setCloudCustomData:(NSObject *)jsonData forType:(TUICustomType)customType {
if (jsonData == nil || customType.length == 0) {
return;
}
NSDictionary *dict = @{};
if (self.cloudCustomData) {
dict = [NSJSONSerialization JSONObjectWithData:self.cloudCustomData options:0 error:nil];
if (dict == nil) {
dict = @{};
}
}
if (![dict isKindOfClass:NSDictionary.class]) {
return;
}
NSMutableDictionary *originDataDict = [NSMutableDictionary dictionaryWithDictionary:dict];
if ([originDataDict.allKeys containsObject:customType]) {
[originDataDict removeObjectForKey:customType];
}
[originDataDict setObject:jsonData forKey:customType];
NSData *data = [NSJSONSerialization dataWithJSONObject:originDataDict options:0 error:nil];
if (data) {
self.cloudCustomData = data;
}
}
- (void)modifyIfNeeded:(V2TIMMessageModifyCompletion)callback {
[V2TIMManager.sharedInstance modifyMessage:self completion:callback];
}
@end
@implementation TUICloudCustomDataTypeCenter
+ (NSString *)convertType2String:(TUICloudCustomDataType)type {
NSString *resultString = @"";
switch (type) {
case TUICloudCustomDataType_MessageReply:
case TUICloudCustomDataType_MessageReference:
resultString = @"messageReply";
break;
case TUICloudCustomDataType_MessageReplies:
resultString = @"messageReplies";
break;
default:
break;
}
return resultString;
}
@end

View File

@@ -0,0 +1,45 @@
//
// TUIEmojiConfig.h
// TUIEmojiPlugin
//
// Created by wyl on 2023/11/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <TIMCommon/TIMCommonModel.h>
NS_ASSUME_NONNULL_BEGIN
@class TUIFaceGroup;
@interface TUIEmojiConfig : NSObject
+ (TUIEmojiConfig *)defaultConfig;
/**
* In respect for the copyright of the emoji design, the Chat Demo/TUIKit project does not include the cutouts of large emoji elements. Please replace them
* with your own designed or copyrighted emoji packs before the official launch for commercial use. The default small yellow face emoji pack is copyrighted by
* Tencent Cloud and can be authorized for a fee. If you wish to obtain authorization, please submit a ticket to contact us.
*
* submit a ticket urlhttps://console.cloud.tencent.com/workorder/category?level1_id=29&level2_id=40&source=14&data_title=%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%20IM&step=1 (China mainland)
* submit a ticket urlhttps://console.tencentcloud.com/workorder/category?level1_id=29&level2_id=40&source=14&data_title=Chat&step=1 (Other regions)
*/
@property(nonatomic, strong) NSArray<TUIFaceGroup *> *faceGroups;
/**
* The list of emoticons displayed after long-pressing the message on the chat interface
*/
@property(nonatomic, strong) NSArray<TUIFaceGroup *> *chatPopDetailGroups;
@property(nonatomic, strong) NSArray<TUIFaceGroup *> *chatContextEmojiDetailGroups;
- (void)appendFaceGroup:(TUIFaceGroup *)faceGroup;
@end
@interface TUIEmojiConfig (defaultFace)
- (TUIFaceGroup *)getChatPopMenuRecentQueue;
- (void)updateEmojiGroups;
- (void)updateRecentMenuQueue:(NSString *)faceName;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,235 @@
//
// TUIEmojiConfig.m
// TUIEmojiPlugin
//
// Created by wyl on 2023/11/13.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIEmojiConfig.h"
typedef NS_ENUM(NSUInteger, TUIEmojiFaceType) {
TUIEmojiFaceTypeKeyBoard = 0,
TUIEmojiFaceTypePopDetail = 1,
TUIEmojiFaceTypePopContextDetail = 2,
};
@interface TUIEmojiConfig ()
@end
@implementation TUIEmojiConfig
+ (void)load {
NSLog(@"TUIEmojiConfig load%@",[TUIEmojiConfig defaultConfig]);
}
- (id)init {
self = [super init];
if (self) {
[self updateEmojiGroups];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onChangeLanguage) name:TUIChangeLanguageNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(onChangeTheme) name:TUIDidApplyingThemeChangedNotfication object:nil];
}
return self;
}
+ (id)defaultConfig {
static dispatch_once_t onceToken;
static TUIEmojiConfig *config;
dispatch_once(&onceToken, ^{
config = [[TUIEmojiConfig alloc] init];
});
return config;
}
- (void)appendFaceGroup:(TUIFaceGroup *)faceGroup {
NSMutableArray *faceGroupMenu = [NSMutableArray arrayWithArray:self.faceGroups];
[faceGroupMenu addObject:faceGroup];
self.faceGroups = faceGroupMenu;
}
- (void)onChangeLanguage {
[self updateEmojiGroups];
}
- (void)onChangeTheme { }
@end
@implementation TUIEmojiConfig (defaultFace)
- (void)updateEmojiGroups {
self.faceGroups = [self updateFaceGroups:self.faceGroups type:TUIEmojiFaceTypeKeyBoard];
self.chatPopDetailGroups = [self updateFaceGroups:self.chatPopDetailGroups type:TUIEmojiFaceTypePopDetail];
self.chatContextEmojiDetailGroups = [self updateFaceGroups:self.chatContextEmojiDetailGroups type:TUIEmojiFaceTypePopContextDetail];
}
- (NSArray *)updateFaceGroups:(NSArray *)groups type:(TUIEmojiFaceType)type {
if (groups.count) {
NSMutableArray *arrayM = [NSMutableArray arrayWithArray:groups];
[arrayM removeObjectAtIndex:0];
TUIFaceGroup *defaultFaceGroup = [self findFaceGroupAboutType:type];
if (defaultFaceGroup) {
[arrayM insertObject:[self findFaceGroupAboutType:type] atIndex:0];
}
return [NSArray arrayWithArray:arrayM];
} else {
NSMutableArray *faceArray = [NSMutableArray array];
TUIFaceGroup *defaultFaceGroup = [self findFaceGroupAboutType:type];
if (defaultFaceGroup) {
[faceArray addObject:defaultFaceGroup];
}
return faceArray;
}
return @[];
}
- (TUIFaceGroup *)findFaceGroupAboutType:(TUIEmojiFaceType)type {
// emoji group
NSMutableArray *emojiFaces = [NSMutableArray array];
NSArray *emojis = [NSArray arrayWithContentsOfFile:TUIChatFaceImagePath(@"emoji/emoji.plist")];
for (NSDictionary *dic in emojis) {
TUIFaceCellData *data = [[TUIFaceCellData alloc] init];
NSString *name = [dic objectForKey:@"face_name"];
NSString *fileName = [dic objectForKey:@"face_file"];
NSString *path = [NSString stringWithFormat:@"emoji/%@", fileName];
NSString *localizableName = [TUIGlobalization getLocalizedStringForKey:name bundle:@"TUIChatFace"];
data.name = name;
data.path = TUIChatFaceImagePath(path);
data.localizableName = localizableName;
[self addFaceToCache:data.path];
[emojiFaces addObject:data];
}
if (emojiFaces.count != 0) {
TUIFaceGroup *emojiGroup = [[TUIFaceGroup alloc] init];
emojiGroup.faces = emojiFaces;
emojiGroup.groupIndex = 0;
emojiGroup.groupPath = TUIChatFaceImagePath(@"emoji/");
emojiGroup.menuPath = TUIChatFaceImagePath(@"emoji/menu");
emojiGroup.isNeedAddInInputBar = YES;
emojiGroup.groupName = TIMCommonLocalizableString(TUIChatFaceGroupAllEmojiName);
if (type == TUIEmojiFaceTypeKeyBoard) {
emojiGroup.rowCount = 4;
emojiGroup.itemCountPerRow = 8;
emojiGroup.needBackDelete = NO;
} else if (type == TUIEmojiFaceTypePopDetail) {
emojiGroup.rowCount = 3;
emojiGroup.itemCountPerRow = 8;
emojiGroup.needBackDelete = NO;
}
else if (type == TUIEmojiFaceTypePopContextDetail) {
emojiGroup.rowCount = 20;
emojiGroup.itemCountPerRow = 7;
emojiGroup.needBackDelete = NO;
}
[self addFaceToCache:emojiGroup.menuPath];
[self addFaceToCache:TUIChatFaceImagePath(@"del_normal")];
[self addFaceToCache:TUIChatFaceImagePath(@"ic_unknown_image")];
return emojiGroup;
}
return nil;
}
#pragma mark - chatPopMenuQueue
- (NSArray *)getChatPopMenuQueue {
NSArray *emojis = [[NSUserDefaults standardUserDefaults] objectForKey:@"TUIChatPopMenuQueue"];
if (emojis && [emojis isKindOfClass:[NSArray class]]) {
if (emojis.count > 0) {
//Randomly check whether an emoticon matches the current emoticon resource package
//to avoid overwriting the installation context emoticon inconsistency.
NSDictionary *dic = emojis.lastObject;
NSString *name = [dic objectForKey:@"face_name"];
NSString *fileName = [dic objectForKey:@"face_file"];
NSString *path = [NSString stringWithFormat:@"emoji/%@", fileName];
UIImage * image = [UIImage imageWithContentsOfFile:TUIChatFaceImagePath(path)];
if (image) {
return emojis;
}
}
}
return [NSArray arrayWithContentsOfFile:TUIChatFaceImagePath(@"emoji/emojiRecentDefaultList.plist")];
}
- (TUIFaceGroup *)getChatPopMenuRecentQueue {
// emoji group
NSMutableArray *emojiFaces = [NSMutableArray array];
NSArray *emojis = [self getChatPopMenuQueue];
for (NSDictionary *dic in emojis) {
TUIFaceCellData *data = [[TUIFaceCellData alloc] init];
NSString *name = [dic objectForKey:@"face_name"];
NSString *fileName = [dic objectForKey:@"face_file"];
NSString *path = [NSString stringWithFormat:@"emoji/%@", fileName];
NSString *localizableName = [TUIGlobalization g_localizedStringForKey:name bundle:@"TUIChatFace"];
data.name = name;
data.path = TUIChatFaceImagePath(path);
data.localizableName = localizableName;
[emojiFaces addObject:data];
}
if (emojiFaces.count != 0) {
TUIFaceGroup *emojiGroup = [[TUIFaceGroup alloc] init];
emojiGroup.faces = emojiFaces;
emojiGroup.groupIndex = 0;
emojiGroup.groupPath = TUIChatFaceImagePath(@"emoji/");
emojiGroup.menuPath = TUIChatFaceImagePath(@"emoji/menu");
emojiGroup.rowCount = 1;
emojiGroup.itemCountPerRow = 6;
emojiGroup.needBackDelete = NO;
emojiGroup.isNeedAddInInputBar = YES;
return emojiGroup;
}
return nil;
}
- (void)updateRecentMenuQueue:(NSString *)faceName {
NSArray *emojis = [self getChatPopMenuQueue];
NSMutableArray *muArray = [NSMutableArray arrayWithArray:emojis];
BOOL hasInQueue = NO;
int index = 0;
for (NSDictionary *dic in emojis) {
NSString *name = [dic objectForKey:@"face_name"];
if ([name isEqualToString:faceName]) {
hasInQueue = YES;
break;
}
index ++;
}
if (hasInQueue) {
NSDictionary *targetDic = emojis[index];
[muArray removeObjectAtIndex:index];
[muArray insertObject:targetDic atIndex:0];
}else {
[muArray removeLastObject];
NSArray *emojis = [NSArray arrayWithContentsOfFile:TUIChatFaceImagePath(@"emoji/emoji.plist")];
NSDictionary *targetDic = @{@"face_name" : faceName};
for (NSDictionary *dic in emojis) {
NSString *name = [dic objectForKey:@"face_name"];
if ([name isEqualToString:faceName]) {
targetDic = dic;
break;
}
}
[muArray insertObject:targetDic atIndex:0];
}
[[NSUserDefaults standardUserDefaults] setObject:muArray forKey:@"TUIChatPopMenuQueue"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - resource
- (void)addResourceToCache:(NSString *)path {
[[TUIImageCache sharedInstance] addResourceToCache:path];
}
- (void)addFaceToCache:(NSString *)path {
[[TUIImageCache sharedInstance] addFaceToCache:path];
}
@end

View File

@@ -0,0 +1,18 @@
//
// TUIEmojiMeditorProtocolProvider.h
// TUIEmojiPlugin
//
// Created by wyl on 2023/11/14.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
#import <TIMCommon/TIMCommonModel.h>
NS_ASSUME_NONNULL_BEGIN
@class TUIFaceGroup;
@interface TUIEmojiMeditorProtocolProvider : NSObject <TUIEmojiMeditorProtocol>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,46 @@
//
// TUIEmojiMeditorProtocolProvider.m
// TUIEmojiPlugin
//
// Created by wyl on 2023/11/14.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIEmojiMeditorProtocolProvider.h"
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
#import <TIMCommon/TIMCommonMediator.h>
#import <TIMCommon/TIMCommonModel.h>
#import "TUIEmojiConfig.h"
@implementation TUIEmojiMeditorProtocolProvider
+ (void)load {
[TIMCommonMediator.share registerService:@protocol(TUIEmojiMeditorProtocol) class:self];
}
- (id)getFaceGroup {
return [TUIEmojiConfig.defaultConfig faceGroups];
}
- (void)appendFaceGroup:(TUIFaceGroup *)faceGroup {
[TUIEmojiConfig.defaultConfig appendFaceGroup:faceGroup];
}
- (id)getChatPopDetailGroups {
return [TUIEmojiConfig.defaultConfig chatPopDetailGroups];
}
- (id)getChatContextEmojiDetailGroups {
return [TUIEmojiConfig.defaultConfig chatContextEmojiDetailGroups];
}
- (id)getChatPopMenuRecentQueue {
return [TUIEmojiConfig.defaultConfig getChatPopMenuRecentQueue];
}
- (void)updateRecentMenuQueue:(NSString *)faceName {
[TUIEmojiConfig.defaultConfig updateRecentMenuQueue:faceName];
}
- (void)updateEmojiGroups {
[TUIEmojiConfig.defaultConfig updateEmojiGroups];
}
@end

View File

@@ -0,0 +1,42 @@
//
// TUIGroupConfig.h
// TUIGroup
//
// Created by Tencent on 2024/9/6.
// Copyright © 2024 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_OPTIONS(NSInteger, TUIGroupConfigItem) {
TUIGroupConfigItem_None = 0,
TUIGroupConfigItem_Members = 1 << 0,
TUIGroupConfigItem_Notice = 1 << 1,
TUIGroupConfigItem_Manage = 1 << 2,
TUIGroupConfigItem_Alias = 1 << 3,
TUIGroupConfigItem_MuteAndPin = 1 << 4,
TUIGroupConfigItem_Background = 1 << 5,
TUIGroupConfigItem_ClearChatHistory = 1 << 6,
TUIGroupConfigItem_DeleteAndLeave = 1 << 7,
TUIGroupConfigItem_Transfer = 1 << 8,
TUIGroupConfigItem_Dismiss = 1 << 9,
TUIGroupConfigItem_Report = 1 << 10,
};
@interface TUIGroupConfig : NSObject
+ (TUIGroupConfig *)sharedConfig;
/**
* Hide items in group config interface.
*/
- (void)hideItemsInGroupConfig:(TUIGroupConfigItem)items;
/**
* Get the hidden status of specified item.
*/
- (BOOL)isItemHiddenInGroupConfig:(TUIGroupConfigItem)item;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,75 @@
//
// TUIGroupConfig.m
// TUIGroup
//
// Created by Tencent on 2024/9/6.
// Copyright © 2024 Tencent. All rights reserved.
//
#import "TUIGroupConfig.h"
@interface TUIGroupConfig()
@property (nonatomic, assign) BOOL hideGroupMembersItems;
@property (nonatomic, assign) BOOL hideGroupNoticeItem;
@property (nonatomic, assign) BOOL hideGroupManageItems;
@property (nonatomic, assign) BOOL hideGroupAliasItem;
@property (nonatomic, assign) BOOL hideGroupMuteAndPinItems;
@property (nonatomic, assign) BOOL hideGroupBackgroundItem;
@property (nonatomic, assign) BOOL hideGroupClearChatHistory;
@property (nonatomic, assign) BOOL hideGroupDeleteAndLeave;
@property (nonatomic, assign) BOOL hideGroupTransfer;
@property (nonatomic, assign) BOOL hideGroupDismiss;
@property (nonatomic, assign) BOOL hideGroupReport;
@end
@implementation TUIGroupConfig
+ (TUIGroupConfig *)sharedConfig {
static dispatch_once_t onceToken;
static TUIGroupConfig *config;
dispatch_once(&onceToken, ^{
config = [[TUIGroupConfig alloc] init];
});
return config;
}
- (void)hideItemsInGroupConfig:(TUIGroupConfigItem)items {
self.hideGroupMuteAndPinItems = items & TUIGroupConfigItem_MuteAndPin;
self.hideGroupManageItems = items & TUIGroupConfigItem_Manage;
self.hideGroupAliasItem = items & TUIGroupConfigItem_Alias;
self.hideGroupBackgroundItem = items & TUIGroupConfigItem_Background;
self.hideGroupMembersItems = items & TUIGroupConfigItem_Members;
self.hideGroupClearChatHistory = items & TUIGroupConfigItem_ClearChatHistory;
self.hideGroupDeleteAndLeave = items & TUIGroupConfigItem_DeleteAndLeave;
self.hideGroupTransfer = items & TUIGroupConfigItem_Transfer;
self.hideGroupDismiss = items & TUIGroupConfigItem_Dismiss;
self.hideGroupReport = items & TUIGroupConfigItem_Report;
}
- (BOOL)isItemHiddenInGroupConfig:(TUIGroupConfigItem)item {
if (item & TUIGroupConfigItem_MuteAndPin) {
return self.hideGroupMuteAndPinItems;
} else if (item & TUIGroupConfigItem_Manage) {
return self.hideGroupManageItems;
} else if (item & TUIGroupConfigItem_Alias) {
return self.hideGroupAliasItem;
} else if (item & TUIGroupConfigItem_Background) {
return self.hideGroupBackgroundItem;
} else if (item & TUIGroupConfigItem_Members) {
return self.hideGroupMembersItems;
} else if (item & TUIGroupConfigItem_ClearChatHistory) {
return self.hideGroupClearChatHistory;
} else if (item & TUIGroupConfigItem_DeleteAndLeave) {
return self.hideGroupDeleteAndLeave;
} else if (item & TUIGroupConfigItem_Transfer) {
return self.hideGroupTransfer;
} else if (item & TUIGroupConfigItem_Dismiss) {
return self.hideGroupDismiss;
} else if (item & TUIGroupConfigItem_Report) {
return self.hideGroupReport;
} else {
return NO;
}
}
@end

View File

@@ -0,0 +1,93 @@
//
// TUIChatConfig.h
// TUIChat
//
// Created by wyl on 2022/6/10.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, TUIImageType) {
TImage_Type_Origin = 1,
TImage_Type_Thumb = 2,
TImage_Type_Large = 4,
};
/////////////////////////////////////////////////////////////////////////////////
//
// TUIImageItem
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUIImageItem : NSObject
/**
* The inner ID for the image, can be used for external cache key
*/
@property(nonatomic, strong) NSString *uuid;
@property(nonatomic, strong) NSString *url;
@property(nonatomic, assign) CGSize size;
@property(nonatomic, assign) TUIImageType type;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUIVideoItem
//
/////////////////////////////////////////////////////////////////////////////////
///
@interface TUIVideoItem : NSObject
/**
* The internal ID of the video message, which does not need to be set, is obtained from the video instance pulled by the SDK.
*/
@property(nonatomic, strong) NSString *uuid;
/**
* The video type - the suffix of the video file - is set when sending a message. For example "mp4".
*/
@property(nonatomic, strong) NSString *type;
/**
* The video size, no need to set, is obtained from the instance pulled by the SDK.
*/
@property(nonatomic, assign) NSInteger length;
/**
* video duration
*/
@property(nonatomic, assign) NSInteger duration;
@end
/////////////////////////////////////////////////////////////////////////////////
//
// TUISnapshotItem
//
/////////////////////////////////////////////////////////////////////////////////
@interface TUISnapshotItem : NSObject
/**
* Image ID, internal identifier, can be used for external cache key
*/
@property(nonatomic, strong) NSString *uuid;
/**
* Cover image type
*/
@property(nonatomic, strong) NSString *type;
/**
* The size of the cover on the UI.
*/
@property(nonatomic, assign) CGSize size;
@property(nonatomic, assign) NSInteger length;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,19 @@
//
// TUIChatConfig.m
// TUIChat
//
// Created by wyl on 2022/6/10.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMessageItem.h"
@implementation TUIImageItem
@end
@implementation TUIVideoItem
@end
@implementation TUISnapshotItem
@end

View File

@@ -0,0 +1,39 @@
//
// TUIMessageProgressManager.h
// TUIChat
//
// Created by harvy on 2022/1/4.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, TUIMessageSendingResultType) { TUIMessageSendingResultTypeSucc = 0, TUIMessageSendingResultTypeFail = 1 };
@protocol TUIMessageProgressManagerDelegate <NSObject>
- (void)onUploadProgress:(NSString *)msgID progress:(NSInteger)progress;
- (void)onDownloadProgress:(NSString *)msgID progress:(NSInteger)progress;
- (void)onMessageSendingResultChanged:(TUIMessageSendingResultType)type messageID:(NSString *)msgID;
@end
@interface TUIMessageProgressManager : NSObject
+ (instancetype)shareManager;
- (void)addDelegate:(id<TUIMessageProgressManagerDelegate>)delegate;
- (void)removeDelegate:(id<TUIMessageProgressManagerDelegate>)delegate;
- (NSInteger)uploadProgressForMessage:(NSString *)msgID;
- (NSInteger)downloadProgressForMessage:(NSString *)msgID;
- (void)appendUploadProgress:(NSString *)msgID progress:(NSInteger)progress;
- (void)appendDownloadProgress:(NSString *)msgID progress:(NSInteger)progress;
- (void)notifyMessageSendingResult:(NSString *)msgID result:(TUIMessageSendingResultType)result;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,246 @@
//
// TUIMessageProgressManager.m
// TUIChat
//
// Created by harvy on 2022/1/4.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIMessageProgressManager.h"
#import <ImSDK_Plus/ImSDK_Plus.h>
@interface TUIMessageProgressManager () <V2TIMSDKListener>
@property(nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *uploadProgress;
@property(nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *dowonloadProgress;
@property(nonatomic, strong) NSHashTable *delegates;
@end
@implementation TUIMessageProgressManager
static id gShareInstance;
+ (instancetype)shareManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gShareInstance = [[self alloc] init];
});
return gShareInstance;
}
- (instancetype)init {
if (self = [super init]) {
[V2TIMManager.sharedInstance addIMSDKListener:self];
}
return self;
}
- (void)addDelegate:(id<TUIMessageProgressManagerDelegate>)delegate {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf addDelegate:delegate];
});
return;
}
if ([self.delegates containsObject:delegate]) {
return;
}
[self.delegates addObject:delegate];
}
- (void)removeDelegate:(id<TUIMessageProgressManagerDelegate>)delegate {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf removeDelegate:delegate];
});
return;
}
if ([self.delegates containsObject:delegate]) {
[self.delegates removeObject:delegate];
}
}
- (NSString *)getUploadIdentityForMessage:(V2TIMMessage *)message {
NSString *msgID = message.msgID;
if (message.elemType == V2TIM_ELEM_TYPE_VIDEO) {
NSString *path = message.videoElem.videoPath;
NSString *uuid = message.videoElem.videoUUID;
return [msgID stringByAppendingString:message.videoElem.videoPath];
} else if (message.elemType == V2TIM_ELEM_TYPE_IMAGE) {
NSString *path = message.imageElem.path;
return [msgID stringByAppendingString:message.imageElem.path];
} else if (message.elemType == V2TIM_ELEM_TYPE_SOUND) {
return [msgID stringByAppendingString:message.soundElem.path];
}
return msgID;
}
- (NSString *)getDownloadIdentityForMessage:(V2TIMMessage *)message {
NSString *msgID = message.msgID;
if (message.elemType == V2TIM_ELEM_TYPE_VIDEO) {
NSString *uuid = message.videoElem.videoUUID;
NSString *snapShotuuid = message.videoElem.snapshotUUID;
return [msgID stringByAppendingString:uuid];
} else if (message.elemType == V2TIM_ELEM_TYPE_IMAGE) {
NSMutableArray<V2TIMImage *> *imageList = message.imageElem.imageList;
for (V2TIMImage *img in imageList) {
if (img.type == V2TIM_IMAGE_TYPE_ORIGIN) {
NSString *url = img.url;
}
}
return @"";
}
return @"";
}
- (void)uploadCallback:(NSString *)msgID {
NSNumber *progress = @(100);
if ([self.uploadProgress.allKeys containsObject:msgID]) {
progress = [self.uploadProgress objectForKey:msgID];
}
for (id<TUIMessageProgressManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(onUploadProgress:progress:)]) {
[delegate onUploadProgress:msgID progress:progress.integerValue];
}
}
}
- (void)appendUploadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf appendUploadProgress:msgID progress:progress];
});
return;
}
if (msgID.length == 0) {
return;
}
if ([self.uploadProgress.allKeys containsObject:msgID]) {
[self.uploadProgress removeObjectForKey:msgID];
}
if (progress >= 100 || progress <= 0) {
[self uploadCallback:msgID];
return;
}
[self.uploadProgress setObject:@(progress) forKey:msgID];
[self uploadCallback:msgID];
}
- (void)downloadCallback:(NSString *)msgID {
NSNumber *progress = @(100);
if ([self.dowonloadProgress.allKeys containsObject:msgID]) {
progress = [self.dowonloadProgress objectForKey:msgID];
}
for (id<TUIMessageProgressManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(onDownloadProgress:progress:)]) {
[delegate onDownloadProgress:msgID progress:progress.integerValue];
}
}
}
- (void)appendDownloadProgress:(NSString *)msgID progress:(NSInteger)progress {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf appendDownloadProgress:msgID progress:progress];
});
return;
}
if (msgID.length == 0) {
return;
}
if ([self.dowonloadProgress.allKeys containsObject:msgID]) {
[self.dowonloadProgress removeObjectForKey:msgID];
}
if (progress >= 100 || progress <= 0) {
[self downloadCallback:msgID];
return;
}
[self.dowonloadProgress setObject:@(progress) forKey:msgID];
[self downloadCallback:msgID];
}
- (void)notifyMessageSendingResult:(NSString *)msgID result:(TUIMessageSendingResultType)result {
for (id<TUIMessageProgressManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(onMessageSendingResultChanged:messageID:)]) {
[delegate onMessageSendingResultChanged:result messageID:msgID];
}
}
}
- (NSInteger)uploadProgressForMessage:(NSString *)msgID {
if (![self.uploadProgress.allKeys containsObject:msgID]) {
return 0;
}
NSInteger progress = 0;
@synchronized(self) {
progress = [[self.uploadProgress objectForKey:msgID] integerValue];
}
return progress;
}
- (NSInteger)downloadProgressForMessage:(NSString *)msgID {
if (![self.dowonloadProgress.allKeys containsObject:msgID]) {
return 0;
}
NSInteger progress = 0;
@synchronized(self) {
progress = [[self.dowonloadProgress objectForKey:msgID] integerValue];
}
return progress;
}
- (void)reset {
if (![NSThread isMainThread]) {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf reset];
});
return;
}
[self.uploadProgress removeAllObjects];
[self.dowonloadProgress removeAllObjects];
}
- (NSMutableDictionary<NSString *, NSNumber *> *)uploadProgress {
if (_uploadProgress == nil) {
_uploadProgress = [NSMutableDictionary dictionary];
}
return _uploadProgress;
}
- (NSMutableDictionary<NSString *, NSNumber *> *)dowonloadProgress {
if (_dowonloadProgress == nil) {
_dowonloadProgress = [NSMutableDictionary dictionary];
}
return _dowonloadProgress;
}
- (NSHashTable *)delegates {
if (_delegates == nil) {
_delegates = [NSHashTable weakObjectsHashTable];
}
return _delegates;
}
- (void)onConnecting {
[self reset];
}
@end

View File

@@ -0,0 +1,34 @@
//
// UIAlertController+TUICustomStyle.h
// TUIChat
//
// Created by wyl on 2022/10/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICustomActionSheetItem : NSObject
@property(nonatomic, assign) NSInteger priority;
@property(nonatomic, copy) NSString *title;
@property(nonatomic, strong) UIImage *leftMark;
@property(nonatomic, assign) UIAlertActionStyle actionStyle;
@property(nonatomic, copy) void (^actionHandler)(UIAlertAction *action);
- (instancetype)initWithTitle:(NSString *)title leftMark:(UIImage *)leftMark withActionHandler:(void (^)(UIAlertAction *action))actionHandler;
@end
@interface UIAlertController (TUICustomStyle)
- (void)configItems:(NSArray<TUICustomActionSheetItem *> *)items;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,78 @@
//
// UIAlertController+TUICustomStyle.m
// TUIChat
//
// Created by wyl on 2022/10/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <objc/runtime.h>
#import "UIAlertController+TUICustomStyle.h"
@implementation TUICustomActionSheetItem
- (instancetype)initWithTitle:(NSString *)title leftMark:(UIImage *)leftMark withActionHandler:(void (^)(UIAlertAction *action))actionHandler {
self = [super init];
if (self) {
_title = title;
_leftMark = leftMark;
_actionHandler = actionHandler;
}
return self;
}
@end
CGFloat padding = 10;
CGFloat itemHeight = 57;
CGFloat lineHeight = 0.5;
CGFloat itemCount = 2;
@interface UIAlertController ()
@end
@implementation UIAlertController (TUICustomStyle)
- (void)configItems:(NSArray<TUICustomActionSheetItem *> *)items {
CGFloat alertVCWidth = self.view.frame.size.width - 2 * padding;
if (items.count > 0) {
for (int i = 0; i < items.count; i++) {
UIView *itemView = [[UIView alloc] init];
itemView.frame = CGRectMake(padding, (itemHeight + lineHeight) * i, alertVCWidth - 2 * padding, itemHeight);
itemView.userInteractionEnabled = NO;
[self.view addSubview:itemView];
UIImageView *icon = [[UIImageView alloc] init];
[itemView addSubview:icon];
icon.contentMode = UIViewContentModeScaleAspectFit;
icon.frame = CGRectMake(kScale390(20), itemHeight * 0.5 - kScale390(30) * 0.5, kScale390(30), kScale390(30));
icon.image = items[i].leftMark;
UILabel *l = [[UILabel alloc] init];
l.frame = CGRectMake(icon.frame.origin.x + icon.frame.size.width + padding + kScale390(15), 0, alertVCWidth * 0.5, itemHeight);
l.text = items[i].title;
l.font = [UIFont systemFontOfSize:17];
l.textAlignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
l.textColor = [UIColor systemBlueColor];
l.userInteractionEnabled = false;
[itemView addSubview:l];
if (isRTL()) {
[icon resetFrameToFitRTL];
[l resetFrameToFitRTL];
}
}
}
// actions
if (items.count > 0) {
for (int i = 0; i < items.count; i++) {
UIAlertAction *action = [UIAlertAction actionWithTitle:@"" style:items[i].actionStyle handler:items[i].actionHandler];
[self addAction:action];
}
}
}
@end