增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/**
* This document declares the voice message recording component
* Voice view, that is, a view that provides guidance and result prompts when a user records a voice message.
* This file contains the TUIRecordView class.
* This class is responsible for providing operational guidance to the user when the user is recording, such as indicating the recording volume, reminding the
* current recording status, etc.
*/
#import <UIKit/UIKit.h>
#import "TUIChatDefine.h"
@interface TUIRecordView : UIView
/**
* Icon view for recording
* This icon contains the corresponding icons under each volume level (a total of 8 volume indications from 1 to 8).
*/
@property(nonatomic, strong) UIImageView *recordImage;
/**
* Label for displaying tips
* Prompt the user about the current recording status. Such as "release to send", "swipe up to cancel sending", "talk time is too short", etc.
*/
@property(nonatomic, strong) UILabel *title;
@property(nonatomic, strong) UIView *background;
@property(nonatomic, strong) UILabel *timeLabel;
/**
* Sets the volume of the current recording.
* It is convenient for the image in the recording icon view to change according to the volume.
* For example: when power < 25, use the "one grid" icon; when power > 25, calculate the icon format according to a certain formula and replace the current
* icon.
*/
- (void)setPower:(NSInteger)power;
- (void)setStatus:(RecordStatus)status;
@end

View File

@@ -0,0 +1,135 @@
//
// TRecordView.m
// UIKit
//
// Created by kennethmiao on 2018/10/9.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIRecordView.h"
#import <TIMCommon/TIMDefine.h>
#import "TUIChatConfig.h"
@implementation TUIRecordView
- (id)init {
self = [super init];
if (self) {
[self setupViews];
[self defaultLayout];
}
return self;
}
- (void)setupViews {
self.backgroundColor = [UIColor clearColor];
_background = [[UIView alloc] init];
_background.backgroundColor = Record_Background_Color;
_background.layer.cornerRadius = 5;
[_background.layer setMasksToBounds:YES];
[self addSubview:_background];
_recordImage = [[UIImageView alloc] init];
_recordImage.image = [UIImage imageNamed:TUIChatImagePath(@"record_1")];
_recordImage.alpha = 0.8;
_recordImage.contentMode = UIViewContentModeCenter;
[_background addSubview:_recordImage];
_title = [[UILabel alloc] init];
_title.font = [UIFont systemFontOfSize:14];
_title.textColor = [UIColor whiteColor];
_title.textAlignment = NSTextAlignmentCenter;
_title.layer.cornerRadius = 5;
[_title.layer setMasksToBounds:YES];
[_background addSubview:_title];
_timeLabel = [[UILabel alloc] init];
_timeLabel.font = [UIFont systemFontOfSize:14];
_timeLabel.textColor = [UIColor whiteColor];
_timeLabel.textAlignment = NSTextAlignmentCenter;
_timeLabel.layer.cornerRadius = 5;
_timeLabel.text = [NSString stringWithFormat:@"%.0f\"", MIN(60, [TUIChatConfig defaultConfig].maxAudioRecordDuration)];
[_background addSubview:_timeLabel];
}
- (void)defaultLayout {
CGSize backSize = CGSizeMake(150, 150);
_title.text = TIMCommonLocalizableString(TUIKitInputRecordSlideToCancel);
CGSize titleSize = [_title sizeThatFits:CGSizeMake(Screen_Width, Screen_Height)];
CGSize timeSize = CGSizeMake(100, 15);
if (titleSize.width > backSize.width) {
backSize.width = titleSize.width + 2 * Record_Margin;
}
CGFloat imageHeight = backSize.height - titleSize.height - 2 * Record_Margin;
[self.timeLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.background).mas_offset(10);
make.width.mas_equalTo(100);
make.height.mas_equalTo(10);
make.centerX.mas_equalTo(self.background);
}];
[self.recordImage mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.timeLabel.mas_bottom).mas_offset(-13);
make.centerX.mas_equalTo(self.background);
make.width.mas_equalTo(backSize.width);
make.height.mas_equalTo(imageHeight);
}];
[self.title mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.background);
make.top.mas_equalTo(self.recordImage.mas_bottom);
make.width.mas_equalTo(backSize.width);
make.height.mas_equalTo(15);
}];
[self.background mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.timeLabel.mas_top).mas_offset(-3);
make.bottom.mas_equalTo(self.title.mas_bottom).mas_offset(3);
make.center.mas_equalTo(self);
make.width.mas_equalTo(backSize.width);
}];
}
- (void)setStatus:(RecordStatus)status {
switch (status) {
case Record_Status_Recording: {
_title.text = TIMCommonLocalizableString(TUIKitInputRecordSlideToCancel);
_title.backgroundColor = [UIColor clearColor];
break;
}
case Record_Status_Cancel: {
_title.text = TIMCommonLocalizableString(TUIKitInputRecordReleaseToCancel);
_title.backgroundColor = [UIColor clearColor];
break;
}
case Record_Status_TooShort: {
_title.text = TIMCommonLocalizableString(TUIKitInputRecordTimeshort);
_title.backgroundColor = [UIColor clearColor];
break;
}
case Record_Status_TooLong: {
_title.text = TIMCommonLocalizableString(TUIKitInputRecordTimeLong);
_title.backgroundColor = [UIColor clearColor];
break;
}
default:
break;
}
}
- (void)setPower:(NSInteger)power {
NSString *imageName = [self getRecordImage:power];
_recordImage.image = [UIImage imageNamed:TUIChatImagePath(imageName)];
}
- (NSString *)getRecordImage:(NSInteger)power {
power = power + 60;
int index = 0;
if (power < 25) {
index = 1;
} else {
index = ceil((power - 25) / 5.0) + 1;
}
index = MIN(index, 8);
return [NSString stringWithFormat:@"record_%d", index];
}
@end

View File

@@ -0,0 +1,26 @@
//
// TUIInputPreviewBar.h
// TUIChat
//
// Created by harvy on 2021/11/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TUIReplyPreviewData.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIReplyPreviewBar : UIView
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UIButton *closeButton;
@property(nonatomic, copy) TUIInputPreviewBarCallback onClose;
@property(nonatomic, strong) TUIReplyPreviewData *previewData;
@property(nonatomic, strong) TUIReferencePreviewData *previewReferenceData;
@end
@interface TUIReferencePreviewBar : TUIReplyPreviewBar
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,101 @@
//
// TUIInputPreviewBar.m
// TUIChat
//
// Created by harvy on 2021/11/9.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIReplyPreviewBar.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMDefine.h>
#import <TUICore/TUIDarkModel.h>
#import <TUICore/TUIThemeManager.h>
@implementation TUIReplyPreviewBar
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupViews];
}
return self;
}
- (void)setupViews {
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
[self addSubview:self.titleLabel];
[self addSubview:self.closeButton];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.closeButton.mm_width(16).mm_height(16);
self.closeButton.mm_centerY = self.mm_centerY;
self.closeButton.mm_right(16.0);
self.titleLabel.mm_x = 16.0;
self.titleLabel.mm_y = 10;
self.titleLabel.mm_w = self.closeButton.mm_x - 10 - 16;
self.titleLabel.mm_h = self.mm_h - 20;
}
- (void)onClose:(UIButton *)closeButton {
if (self.onClose) {
self.onClose();
}
}
- (void)setPreviewData:(TUIReplyPreviewData *)previewData {
_previewData = previewData;
NSString *abstract = [TUIReplyPreviewData displayAbstract:previewData.type abstract:previewData.msgAbstract withFileName:YES isRisk:NO];
_titleLabel.text = [[NSString stringWithFormat:@"%@: %@", previewData.sender, abstract] getLocalizableStringWithFaceContent];
_titleLabel.lineBreakMode = previewData.type == (NSInteger)V2TIM_ELEM_TYPE_FILE ? NSLineBreakByTruncatingMiddle : NSLineBreakByTruncatingTail;
}
- (void)setPreviewReferenceData:(TUIReferencePreviewData *)previewReferenceData {
_previewReferenceData = previewReferenceData;
NSString *abstract = [TUIReferencePreviewData displayAbstract:previewReferenceData.type
abstract:previewReferenceData.msgAbstract
withFileName:YES
isRisk:NO];
_titleLabel.text = [[NSString stringWithFormat:@"%@: %@", previewReferenceData.sender, abstract] getLocalizableStringWithFaceContent];
_titleLabel.lineBreakMode = previewReferenceData.type == (NSInteger)V2TIM_ELEM_TYPE_FILE ? NSLineBreakByTruncatingMiddle : NSLineBreakByTruncatingTail;
}
- (UILabel *)titleLabel {
if (_titleLabel == nil) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:16];
_titleLabel.textColor = [UIColor colorWithRed:143 / 255.0 green:150 / 255.0 blue:160 / 255.0 alpha:1 / 1.0];
}
return _titleLabel;
}
- (UIButton *)closeButton {
if (_closeButton == nil) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setImage:TUIChatCommonBundleImage(@"icon_close") forState:UIControlStateNormal];
[_closeButton addTarget:self action:@selector(onClose:) forControlEvents:UIControlEventTouchUpInside];
[_closeButton sizeToFit];
}
return _closeButton;
}
@end
@implementation TUIReferencePreviewBar
- (void)layoutSubviews {
[super layoutSubviews];
self.closeButton.mm_right(16.0);
self.closeButton.frame = CGRectMake(self.closeButton.frame.origin.x, (self.frame.size.height - 16) * 0.5, 16, 16);
self.titleLabel.mm_x = 16.0;
self.titleLabel.mm_y = 10;
self.titleLabel.mm_w = self.closeButton.mm_x - 10 - 16;
self.titleLabel.mm_h = self.mm_h - 20;
}
@end

View File

@@ -0,0 +1,21 @@
//
// TResponderTextView.h
// TUIKit
//
// Created by kennethmiao on 2018/10/25.
// Copyright © 2018 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
@class TUIResponderTextView;
@protocol TUIResponderTextViewDelegate <UITextViewDelegate>
- (void)onDeleteBackward:(TUIResponderTextView *)textView;
@end
@interface TUIResponderTextView : UITextView
@property(nonatomic, weak) UIResponder *overrideNextResponder;
@end

View File

@@ -0,0 +1,66 @@
//
// TResponderTextView.m
// TUIKit
//
// Created by kennethmiao on 2018/10/25.
// Copyright © 2018 Tencent. All rights reserved.
//
#import "TUIResponderTextView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
@implementation TUIResponderTextView
- (UIResponder *)nextResponder {
if (_overrideNextResponder == nil) {
return [super nextResponder];
} else {
return _overrideNextResponder;
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (_overrideNextResponder != nil)
return NO;
else
return [super canPerformAction:action withSender:sender];
}
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder API_AVAILABLE(ios(13.0)) {
if (@available(iOS 16.0, *)) {
[builder removeMenuForIdentifier:UIMenuLookup];
}
[super buildMenuWithBuilder:builder];
}
- (void)deleteBackward {
id<TUIResponderTextViewDelegate> delegate = (id<TUIResponderTextViewDelegate>)self.delegate;
if ([delegate respondsToSelector:@selector(onDeleteBackward:)]) {
[delegate onDeleteBackward:self];
}
[super deleteBackward];
}
- (void)setText:(NSString *)text {
[super setText:text];
if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
[self.delegate textViewDidChange:self];
}
}
- (void)copy:(__unused id)sender {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = [[self.textStorage attributedSubstringFromRange:self.selectedRange] tui_getPlainString];
}
- (void)cut:(nullable id)sender {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = [[self.textStorage attributedSubstringFromRange:self.selectedRange] tui_getPlainString];
UIFont *textFont = [UIFont systemFontOfSize:16.0];
NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:@"" attributes:@{NSFontAttributeName : textFont}];
[self.textStorage replaceCharactersInRange:self.selectedRange withAttributedString:spaceString];
}
@end