增加换肤功能
This commit is contained in:
177
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.h
Normal file
177
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.h
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* This file declares the TUIInputBarDelegate protocol and the TUIInputBar class.
|
||||
* TUI input bar, a UI component used to detect and obtain user input.
|
||||
* TUIInputBar, the UI component at the bottom of the chat message. Includes text input box, emoji button, voice button, and "+" button ("More" button)
|
||||
* TUIInputBarDelegate provides callbacks for various situations of the input bar, including the callback for the emoticon of clicking the input bar, the
|
||||
* "more" view, and the voice button. And callbacks to send message, send voice, change input height.
|
||||
*/
|
||||
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIResponderTextView.h"
|
||||
|
||||
#define kTUIInputNoramlFont [UIFont systemFontOfSize:16.0]
|
||||
#define kTUIInputNormalTextColor TUIChatDynamicColor(@"chat_input_text_color", @"#000000")
|
||||
|
||||
@class TUIInputBar;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIInputBarDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol TUIInputBarDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Callback after clicking the emoji button - "smiley" button.
|
||||
* You can use this callback to achieve: After clicking the emoticon button, the corresponding emoticon view is displayed.
|
||||
*/
|
||||
- (void)inputBarDidTouchFace:(TUIInputBar *)textView;
|
||||
|
||||
/**
|
||||
* Callback after more button - "+" is clicked.
|
||||
* You can use this callback to achieve: corresponding user's click operation to display more corresponding views.
|
||||
*/
|
||||
- (void)inputBarDidTouchMore:(TUIInputBar *)textView;
|
||||
|
||||
/**
|
||||
* Callback after clicking the voice button - "Sound Wave" icon .
|
||||
* You can use this callback to display the corresponding operation prompt view and start voice recording
|
||||
*/
|
||||
- (void)inputBarDidTouchVoice:(TUIInputBar *)textView;
|
||||
|
||||
/**
|
||||
* Callback when input bar height changes
|
||||
* This callback is fired when the InputBar height changes when you click the voice button, emoji button, "+" button, or call out/retract the keyboard
|
||||
* You can use this callback to achieve: UI layout adjustment when InputBar height changes through this callback function.
|
||||
* In the default implementation of TUIKit, this callback function further calls the didChangeHeight delegate in TUIInputController to adjust the height of the
|
||||
* UI layout after processing the appearance of the expression view and more views.
|
||||
*/
|
||||
- (void)inputBar:(TUIInputBar *)textView didChangeInputHeight:(CGFloat)offset;
|
||||
|
||||
/**
|
||||
* Callback when sending a text message.
|
||||
* This callback is fired when you send a text message through the InputBar (click the send button from the keyboard).
|
||||
* You can use this callback to get the content of the InputBar and send the message.
|
||||
* In the default implementation of TUIKit, this callback further calls the didSendMessage delegate in TUIInputController for further logical processing of
|
||||
* message sending after processing the appearance of the expression view and more views.
|
||||
*/
|
||||
- (void)inputBar:(TUIInputBar *)textView didSendText:(NSString *)text;
|
||||
|
||||
/**
|
||||
* Callback when sending voice
|
||||
* This callback is triggered when you long press and release the voice button.
|
||||
* You can use this callback to process the recorded voice information and send the voice message.
|
||||
* In the default implementation of TUIKit, this callback function further calls the didSendMessage delegate in TUIInputController for further logical
|
||||
* processing of message sending after processing the appearance of the expression view and more views.
|
||||
*/
|
||||
- (void)inputBar:(TUIInputBar *)textView didSendVoice:(NSString *)path;
|
||||
|
||||
/**
|
||||
* Callback after entering text containing the @ character
|
||||
*/
|
||||
- (void)inputBarDidInputAt:(TUIInputBar *)textView;
|
||||
|
||||
/**
|
||||
* Callback after removing text containing @ characters (e.g. removing @xxx)
|
||||
*/
|
||||
- (void)inputBar:(TUIInputBar *)textView didDeleteAt:(NSString *)text;
|
||||
|
||||
/**
|
||||
* Callback after keyboard button click
|
||||
* After clicking the emoticon button, the "smiley face" icon at the corresponding position will become the "keyboard" icon, which is the keyboard button at
|
||||
* this time. You can use this callback to: hide the currently displayed emoticon view or more views, and open the keyboard.
|
||||
*/
|
||||
- (void)inputBarDidTouchKeyboard:(TUIInputBar *)textView;
|
||||
|
||||
/**
|
||||
* Callback after clicking delete button on keyboard
|
||||
*/
|
||||
- (void)inputBarDidDeleteBackward:(TUIInputBar *)textView;
|
||||
|
||||
- (void)inputTextViewShouldBeginTyping:(UITextView *)textView;
|
||||
|
||||
- (void)inputTextViewShouldEndTyping:(UITextView *)textView;
|
||||
|
||||
@end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIInputBar
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface TUIInputBar : UIView
|
||||
|
||||
/**
|
||||
* Separtor
|
||||
*/
|
||||
@property(nonatomic, strong) UIView *lineView;
|
||||
|
||||
/**
|
||||
* Voice button
|
||||
* Switch to voice input state after clicking
|
||||
*/
|
||||
@property(nonatomic, strong) UIButton *micButton;
|
||||
|
||||
/**
|
||||
* Keyboard button
|
||||
* Switch to keyboard input state after clicking
|
||||
*/
|
||||
@property(nonatomic, strong) UIButton *keyboardButton;
|
||||
|
||||
/**
|
||||
* Input view
|
||||
*/
|
||||
@property(nonatomic, strong) TUIResponderTextView *inputTextView;
|
||||
|
||||
/**
|
||||
* Emoticon button
|
||||
* Switch to emoji input state after clicking
|
||||
*/
|
||||
@property(nonatomic, strong) UIButton *faceButton;
|
||||
|
||||
/**
|
||||
* More button
|
||||
* A button that, when clicked, opens up more menu options
|
||||
*/
|
||||
@property(nonatomic, strong) UIButton *moreButton;
|
||||
|
||||
/**
|
||||
* Record button, long press the button to start recording
|
||||
*/
|
||||
@property(nonatomic, strong) UIButton *recordButton;
|
||||
|
||||
@property(nonatomic, weak) id<TUIInputBarDelegate> delegate;
|
||||
|
||||
@property(nonatomic, copy) void (^inputBarTextChanged)(UITextView * textview);
|
||||
|
||||
@property(nonatomic, assign) BOOL isFromReplyPage;
|
||||
|
||||
- (void)defaultLayout;
|
||||
/**
|
||||
* Add emoticon text
|
||||
* Used to input emoticon text in the current text input box
|
||||
*
|
||||
* @param emoji The string representation of the emoticon to be entered.
|
||||
*/
|
||||
- (void)addEmoji:(TUIFaceCellData *)emoji;
|
||||
|
||||
- (void)backDelete;
|
||||
|
||||
- (void)clearInput;
|
||||
|
||||
- (NSString *)getInput;
|
||||
|
||||
- (void)updateTextViewFrame;
|
||||
|
||||
- (void)changeToKeyboard;
|
||||
|
||||
- (void)addDraftToInputBar:(NSAttributedString *)draft;
|
||||
- (void)addWordsToInputBar:(NSAttributedString *)words;
|
||||
@end
|
||||
696
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.m
Normal file
696
TUIKit/TUIChat/UI_Classic/Input/TUIInputBar.m
Normal file
@@ -0,0 +1,696 @@
|
||||
//
|
||||
// TUIInputBar.m
|
||||
// UIKit
|
||||
//
|
||||
// Created by kennethmiao on 2018/9/18.
|
||||
// Copyright © 2018 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIInputBar.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUITool.h>
|
||||
#import "TUIRecordView.h"
|
||||
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TIMCommon/NSTimer+TUISafe.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIDarkModel.h>
|
||||
#import <TUICore/TUIGlobalization.h>
|
||||
#import <TUICore/UIView+TUILayout.h>
|
||||
#import "ReactiveObjC/ReactiveObjC.h"
|
||||
#import "TUIAudioRecorder.h"
|
||||
#import "TUIChatConfig.h"
|
||||
|
||||
@interface TUIInputBar () <UITextViewDelegate, TUIAudioRecorderDelegate>
|
||||
@property(nonatomic, strong) TUIRecordView *recordView;
|
||||
@property(nonatomic, strong) NSDate *recordStartTime;
|
||||
|
||||
@property(nonatomic, strong) TUIAudioRecorder *recorder;
|
||||
|
||||
@property(nonatomic, assign) BOOL isFocusOn;
|
||||
@property(nonatomic, strong) NSTimer *sendTypingStatusTimer;
|
||||
@property(nonatomic, assign) BOOL allowSendTypingStatusByChangeWord;
|
||||
@end
|
||||
|
||||
@implementation TUIInputBar
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setupViews];
|
||||
[self defaultLayout];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_sendTypingStatusTimer) {
|
||||
[_sendTypingStatusTimer invalidate];
|
||||
_sendTypingStatusTimer = nil;
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
- (void)setupViews {
|
||||
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
|
||||
|
||||
_lineView = [[UIView alloc] init];
|
||||
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#FFFFFF");
|
||||
[self addSubview:_lineView];
|
||||
|
||||
_micButton = [[UIButton alloc] init];
|
||||
[_micButton addTarget:self action:@selector(onMicButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoice_img", @"ToolViewInputVoice") forState:UIControlStateNormal];
|
||||
[_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoiceHL_img", @"ToolViewInputVoiceHL") forState:UIControlStateHighlighted];
|
||||
[self addSubview:_micButton];
|
||||
|
||||
_faceButton = [[UIButton alloc] init];
|
||||
[_faceButton addTarget:self action:@selector(onFaceEmojiButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotion_img", @"ToolViewEmotion") forState:UIControlStateNormal];
|
||||
[_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotionHL_img", @"ToolViewEmotionHL") forState:UIControlStateHighlighted];
|
||||
[self addSubview:_faceButton];
|
||||
|
||||
_keyboardButton = [[UIButton alloc] init];
|
||||
[_keyboardButton addTarget:self action:@selector(onKeyboardButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboard_img", @"ToolViewKeyboard") forState:UIControlStateNormal];
|
||||
[_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboardHL_img", @"ToolViewKeyboardHL") forState:UIControlStateHighlighted];
|
||||
_keyboardButton.hidden = YES;
|
||||
[self addSubview:_keyboardButton];
|
||||
|
||||
_moreButton = [[UIButton alloc] init];
|
||||
[_moreButton addTarget:self action:@selector(onMoreButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtn_Black_img", @"TypeSelectorBtn_Black") forState:UIControlStateNormal];
|
||||
[_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtnHL_Black_img", @"TypeSelectorBtnHL_Black") forState:UIControlStateHighlighted];
|
||||
[self addSubview:_moreButton];
|
||||
|
||||
_recordButton = [[UIButton alloc] init];
|
||||
[_recordButton.titleLabel setFont:[UIFont systemFontOfSize:15.0f]];
|
||||
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDown:) forControlEvents:UIControlEventTouchDown];
|
||||
[_recordButton addTarget:self action:@selector(onRecordButtonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[_recordButton addTarget:self action:@selector(onRecordButtonTouchCancel:) forControlEvents:UIControlEventTouchUpOutside | UIControlEventTouchCancel];
|
||||
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDragExit:) forControlEvents:UIControlEventTouchDragExit];
|
||||
[_recordButton addTarget:self action:@selector(onRecordButtonTouchDragEnter:) forControlEvents:UIControlEventTouchDragEnter];
|
||||
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
|
||||
[_recordButton setTitleColor:TUIChatDynamicColor(@"chat_input_text_color", @"#000000") forState:UIControlStateNormal];
|
||||
_recordButton.hidden = YES;
|
||||
[self addSubview:_recordButton];
|
||||
|
||||
_inputTextView = [[TUIResponderTextView alloc] init];
|
||||
_inputTextView.delegate = self;
|
||||
[_inputTextView setFont:kTUIInputNoramlFont];
|
||||
_inputTextView.backgroundColor = TUIChatDynamicColor(@"chat_input_bg_color", @"#FFFFFF");
|
||||
_inputTextView.textColor = TUIChatDynamicColor(@"chat_input_text_color", @"#000000");
|
||||
_inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
|
||||
[_inputTextView setReturnKeyType:UIReturnKeySend];
|
||||
[self addSubview:_inputTextView];
|
||||
|
||||
[self applyBorderTheme];
|
||||
}
|
||||
|
||||
- (void)onThemeChanged {
|
||||
[self applyBorderTheme];
|
||||
}
|
||||
|
||||
- (void)applyBorderTheme {
|
||||
if (_recordButton) {
|
||||
[_recordButton.layer setMasksToBounds:YES];
|
||||
[_recordButton.layer setCornerRadius:4.0f];
|
||||
[_recordButton.layer setBorderWidth:1.0f];
|
||||
[_recordButton.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
|
||||
}
|
||||
|
||||
if (_inputTextView) {
|
||||
[_inputTextView.layer setMasksToBounds:YES];
|
||||
[_inputTextView.layer setCornerRadius:4.0f];
|
||||
[_inputTextView.layer setBorderWidth:0.5f];
|
||||
[_inputTextView.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)defaultLayout {
|
||||
[_lineView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.mas_equalTo(0);
|
||||
make.width.mas_equalTo(self);
|
||||
make.height.mas_equalTo(TLine_Heigh);
|
||||
}];
|
||||
|
||||
CGSize buttonSize = TTextView_Button_Size;
|
||||
CGFloat buttonOriginY = (TTextView_Height - buttonSize.height) * 0.5;
|
||||
|
||||
[_micButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.mas_leading);
|
||||
make.centerY.mas_equalTo(self);
|
||||
make.size.mas_equalTo(buttonSize);
|
||||
}];
|
||||
|
||||
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(_micButton);
|
||||
}];
|
||||
|
||||
[_moreButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(0);
|
||||
make.size.mas_equalTo(buttonSize);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
[_faceButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(_moreButton.mas_leading).mas_offset(- TTextView_Margin);
|
||||
make.size.mas_equalTo(buttonSize);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
[_recordButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
|
||||
make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
|
||||
make.height.mas_equalTo(TTextView_TextView_Height_Min);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
|
||||
[_inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
if (self.isFromReplyPage) {
|
||||
make.leading.mas_equalTo(self.mas_leading).mas_offset(10);
|
||||
}
|
||||
else {
|
||||
make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
|
||||
}
|
||||
make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
|
||||
make.height.mas_equalTo(TTextView_TextView_Height_Min);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)layoutButton:(CGFloat)height {
|
||||
CGRect frame = self.frame;
|
||||
CGFloat offset = height - frame.size.height;
|
||||
frame.size.height = height;
|
||||
self.frame = frame;
|
||||
|
||||
CGSize buttonSize = TTextView_Button_Size;
|
||||
CGFloat bottomMargin = (TTextView_Height - buttonSize.height) * 0.5;
|
||||
CGFloat originY = frame.size.height - buttonSize.height - bottomMargin;
|
||||
|
||||
CGRect faceFrame = _faceButton.frame;
|
||||
faceFrame.origin.y = originY;
|
||||
_faceButton.frame = faceFrame;
|
||||
|
||||
CGRect moreFrame = _moreButton.frame;
|
||||
moreFrame.origin.y = originY;
|
||||
_moreButton.frame = moreFrame;
|
||||
|
||||
CGRect voiceFrame = _micButton.frame;
|
||||
voiceFrame.origin.y = originY;
|
||||
_micButton.frame = voiceFrame;
|
||||
|
||||
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(_faceButton);
|
||||
}];
|
||||
|
||||
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didChangeInputHeight:)]) {
|
||||
[_delegate inputBar:self didChangeInputHeight:offset];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Event response
|
||||
- (void)onMicButtonClicked:(UIButton *)sender {
|
||||
_recordButton.hidden = NO;
|
||||
_inputTextView.hidden = YES;
|
||||
_micButton.hidden = YES;
|
||||
_keyboardButton.hidden = NO;
|
||||
_faceButton.hidden = NO;
|
||||
[_inputTextView resignFirstResponder];
|
||||
[self layoutButton:TTextView_Height];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
|
||||
[_delegate inputBarDidTouchVoice:self];
|
||||
}
|
||||
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(_micButton);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)onKeyboardButtonClicked:(UIButton *)sender {
|
||||
_micButton.hidden = NO;
|
||||
_keyboardButton.hidden = YES;
|
||||
_recordButton.hidden = YES;
|
||||
_inputTextView.hidden = NO;
|
||||
_faceButton.hidden = NO;
|
||||
[self layoutButton:_inputTextView.frame.size.height + 2 * TTextView_Margin];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchKeyboard:)]) {
|
||||
[_delegate inputBarDidTouchKeyboard:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onFaceEmojiButtonClicked:(UIButton *)sender {
|
||||
_micButton.hidden = NO;
|
||||
_faceButton.hidden = YES;
|
||||
_keyboardButton.hidden = NO;
|
||||
_recordButton.hidden = YES;
|
||||
_inputTextView.hidden = NO;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchFace:)]) {
|
||||
[_delegate inputBarDidTouchFace:self];
|
||||
}
|
||||
[_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.edges.mas_equalTo(_faceButton);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)onMoreButtonClicked:(UIButton *)sender {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
|
||||
[_delegate inputBarDidTouchMore:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onRecordButtonTouchDown:(UIButton *)sender {
|
||||
[self.recorder record];
|
||||
}
|
||||
|
||||
- (void)onRecordButtonTouchUpInside:(UIButton *)sender {
|
||||
self.recordButton.backgroundColor = [UIColor clearColor];
|
||||
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
|
||||
|
||||
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:self.recordStartTime];
|
||||
@weakify(self);
|
||||
if (interval < 1) {
|
||||
[self.recordView setStatus:Record_Status_TooShort];
|
||||
[self.recorder cancel];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
[self.recordView removeFromSuperview];
|
||||
self.recordView = nil;
|
||||
});
|
||||
} else if (interval > MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration)) {
|
||||
[self.recordView setStatus:Record_Status_TooLong];
|
||||
[self.recorder cancel];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
[self.recordView removeFromSuperview];
|
||||
self.recordView = nil;
|
||||
});
|
||||
} else {
|
||||
/// TUICallKit may need some time to stop all services, so remove UI immediately then stop the recorder.
|
||||
if (_recordView) {
|
||||
[self.recordView removeFromSuperview];
|
||||
self.recordView = nil;
|
||||
}
|
||||
dispatch_queue_t main_queue = dispatch_get_main_queue();
|
||||
dispatch_async(main_queue, ^{
|
||||
@strongify(self);
|
||||
dispatch_async(main_queue, ^{
|
||||
[self.recorder stop];
|
||||
NSString *path = self.recorder.recordedFilePath;
|
||||
if (path) {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
|
||||
[self.delegate inputBar:self didSendVoice:path];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onRecordButtonTouchCancel:(UIButton *)sender {
|
||||
[self.recordView removeFromSuperview];
|
||||
self.recordView = nil;
|
||||
self.recordButton.backgroundColor = [UIColor clearColor];
|
||||
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
|
||||
[self.recorder cancel];
|
||||
}
|
||||
|
||||
- (void)onRecordButtonTouchDragExit:(UIButton *)sender {
|
||||
[self.recordView setStatus:Record_Status_Cancel];
|
||||
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToCancel) forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
- (void)onRecordButtonTouchDragEnter:(UIButton *)sender {
|
||||
[self.recordView setStatus:Record_Status_Recording];
|
||||
[_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
|
||||
}
|
||||
|
||||
- (void)showHapticFeedback {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[generator prepare];
|
||||
[generator impactOccurred];
|
||||
});
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Text input
|
||||
#pragma mark-- UITextViewDelegate
|
||||
- (void)textViewDidBeginEditing:(UITextView *)textView {
|
||||
self.keyboardButton.hidden = YES;
|
||||
self.micButton.hidden = NO;
|
||||
self.faceButton.hidden = NO;
|
||||
|
||||
self.isFocusOn = YES;
|
||||
self.allowSendTypingStatusByChangeWord = YES;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.sendTypingStatusTimer = [NSTimer tui_scheduledTimerWithTimeInterval:4
|
||||
repeats:YES
|
||||
block:^(NSTimer *_Nonnull timer) {
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
strongSelf.allowSendTypingStatusByChangeWord = YES;
|
||||
}];
|
||||
|
||||
if (self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
|
||||
[_delegate inputTextViewShouldBeginTyping:textView];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)textViewDidEndEditing:(UITextView *)textView {
|
||||
self.isFocusOn = NO;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
|
||||
[_delegate inputTextViewShouldEndTyping:textView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
if (self.allowSendTypingStatusByChangeWord && self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
|
||||
self.allowSendTypingStatusByChangeWord = NO;
|
||||
[_delegate inputTextViewShouldBeginTyping:textView];
|
||||
}
|
||||
}
|
||||
|
||||
if (self.isFocusOn && [textView.textStorage tui_getPlainString].length == 0) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
|
||||
[_delegate inputTextViewShouldEndTyping:textView];
|
||||
}
|
||||
}
|
||||
if (self.inputBarTextChanged) {
|
||||
self.inputBarTextChanged(_inputTextView);
|
||||
}
|
||||
CGSize size = [_inputTextView sizeThatFits:CGSizeMake(_inputTextView.frame.size.width, TTextView_TextView_Height_Max)];
|
||||
CGFloat oldHeight = _inputTextView.frame.size.height;
|
||||
CGFloat newHeight = size.height;
|
||||
|
||||
if (newHeight > TTextView_TextView_Height_Max) {
|
||||
newHeight = TTextView_TextView_Height_Max;
|
||||
}
|
||||
if (newHeight < TTextView_TextView_Height_Min) {
|
||||
newHeight = TTextView_TextView_Height_Min;
|
||||
}
|
||||
if (oldHeight == newHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) ws = self;
|
||||
[UIView animateWithDuration:0.3
|
||||
animations:^{
|
||||
[ws.inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(ws.micButton.mas_trailing).mas_offset(10);
|
||||
make.trailing.mas_equalTo(ws.faceButton.mas_leading).mas_offset(-10);
|
||||
make.height.mas_equalTo(newHeight);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
[ws layoutButton:newHeight + 2 * TTextView_Margin];
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
|
||||
if ([text tui_containsString:@"["] && [text tui_containsString:@"]"]) {
|
||||
NSRange selectedRange = textView.selectedRange;
|
||||
if (selectedRange.length > 0) {
|
||||
[textView.textStorage deleteCharactersInRange:selectedRange];
|
||||
}
|
||||
|
||||
NSMutableAttributedString *textChange = [text getAdvancedFormatEmojiStringWithFont:kTUIInputNoramlFont
|
||||
textColor:kTUIInputNormalTextColor
|
||||
emojiLocations:nil];
|
||||
[textView.textStorage insertAttributedString:textChange atIndex:textView.textStorage.length];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
|
||||
});
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([text isEqualToString:@"\n"]) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendText:)]) {
|
||||
NSString *sp = [[textView.textStorage tui_getPlainString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (sp.length == 0) {
|
||||
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputBlankMessageTitle)
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self.mm_viewController presentViewController:ac animated:YES completion:nil];
|
||||
} else {
|
||||
[_delegate inputBar:self didSendText:[textView.textStorage tui_getPlainString]];
|
||||
[self clearInput];
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
} else if ([text isEqualToString:@""]) {
|
||||
if (textView.textStorage.length > range.location) {
|
||||
// Delete the @ message like @xxx at one time
|
||||
NSAttributedString *lastAttributedStr = [textView.textStorage attributedSubstringFromRange:NSMakeRange(range.location, 1)];
|
||||
NSString *lastStr = [lastAttributedStr tui_getPlainString];
|
||||
if (lastStr && lastStr.length > 0 && [lastStr characterAtIndex:0] == ' ') {
|
||||
NSUInteger location = range.location;
|
||||
NSUInteger length = range.length;
|
||||
|
||||
// corresponds to ascii code
|
||||
int at = 64;
|
||||
// (space) ascii
|
||||
// Space (space) corresponding ascii code
|
||||
int space = 32;
|
||||
|
||||
while (location != 0) {
|
||||
location--;
|
||||
length++;
|
||||
// Convert characters to ascii code, copy to int, avoid out of bounds
|
||||
int c = (int)[[[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, 1)] tui_getPlainString] characterAtIndex:0];
|
||||
|
||||
if (c == at) {
|
||||
NSString *atText = [[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, length)] tui_getPlainString];
|
||||
UIFont *textFont = kTUIInputNoramlFont;
|
||||
NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:@"" attributes:@{NSFontAttributeName : textFont}];
|
||||
[textView.textStorage replaceCharactersInRange:NSMakeRange(location, length) withAttributedString:spaceString];
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didDeleteAt:)]) {
|
||||
[self.delegate inputBar:self didDeleteAt:atText];
|
||||
}
|
||||
return NO;
|
||||
} else if (c == space) {
|
||||
// Avoid "@nickname Hello, nice to meet you (space) "" Press del after a space to over-delete to @
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Monitor the input of @ character, including full-width/half-width
|
||||
else if ([text isEqualToString:@"@"] || [text isEqualToString:@"@"]) {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidInputAt:)]) {
|
||||
[self.delegate inputBarDidInputAt:self];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)onDeleteBackward:(TUIResponderTextView *)textView {
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidDeleteBackward:)]) {
|
||||
[self.delegate inputBarDidDeleteBackward:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearInput {
|
||||
[_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(0, _inputTextView.textStorage.length)];
|
||||
[self textViewDidChange:_inputTextView];
|
||||
}
|
||||
|
||||
- (NSString *)getInput {
|
||||
return [_inputTextView.textStorage tui_getPlainString];
|
||||
}
|
||||
|
||||
- (void)addEmoji:(TUIFaceCellData *)emoji {
|
||||
// Create emoji attachment
|
||||
TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
|
||||
emojiTextAttachment.faceCellData = emoji;
|
||||
|
||||
NSString *localizableFaceName = emoji.name;
|
||||
|
||||
// Set tag and image
|
||||
emojiTextAttachment.emojiTag = localizableFaceName;
|
||||
emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:emoji.path];
|
||||
|
||||
// Set emoji size
|
||||
emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
|
||||
NSAttributedString *str = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
|
||||
|
||||
NSRange selectedRange = _inputTextView.selectedRange;
|
||||
if (selectedRange.length > 0) {
|
||||
[_inputTextView.textStorage deleteCharactersInRange:selectedRange];
|
||||
}
|
||||
// Insert emoji image
|
||||
[_inputTextView.textStorage insertAttributedString:str atIndex:_inputTextView.selectedRange.location];
|
||||
|
||||
_inputTextView.selectedRange = NSMakeRange(_inputTextView.selectedRange.location + 1, 0);
|
||||
[self resetTextStyle];
|
||||
|
||||
if (_inputTextView.contentSize.height > TTextView_TextView_Height_Max) {
|
||||
float offset = _inputTextView.contentSize.height - _inputTextView.frame.size.height;
|
||||
[_inputTextView scrollRectToVisible:CGRectMake(0, offset, _inputTextView.frame.size.width, _inputTextView.frame.size.height) animated:YES];
|
||||
}
|
||||
[self textViewDidChange:_inputTextView];
|
||||
}
|
||||
|
||||
- (void)resetTextStyle {
|
||||
// After changing text selection, should reset style.
|
||||
NSRange wholeRange = NSMakeRange(0, _inputTextView.textStorage.length);
|
||||
|
||||
[_inputTextView.textStorage removeAttribute:NSFontAttributeName range:wholeRange];
|
||||
|
||||
[_inputTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:wholeRange];
|
||||
|
||||
[_inputTextView.textStorage addAttribute:NSForegroundColorAttributeName value:kTUIInputNormalTextColor range:wholeRange];
|
||||
|
||||
[_inputTextView.textStorage addAttribute:NSFontAttributeName value:kTUIInputNoramlFont range:wholeRange];
|
||||
[_inputTextView setFont:kTUIInputNoramlFont];
|
||||
|
||||
_inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
|
||||
|
||||
// In iOS 15.0 and later, you need set styles again as belows
|
||||
_inputTextView.textColor = kTUIInputNormalTextColor;
|
||||
_inputTextView.font = kTUIInputNoramlFont;
|
||||
}
|
||||
|
||||
- (void)backDelete {
|
||||
if (_inputTextView.textStorage.length > 0) {
|
||||
[_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(_inputTextView.textStorage.length - 1, 1)];
|
||||
[self textViewDidChange:_inputTextView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateTextViewFrame {
|
||||
[self textViewDidChange:[UITextView new]];
|
||||
}
|
||||
|
||||
- (void)changeToKeyboard {
|
||||
[self onKeyboardButtonClicked:self.keyboardButton];
|
||||
}
|
||||
|
||||
- (void)addDraftToInputBar:(NSAttributedString *)draft {
|
||||
[self addWordsToInputBar:draft];
|
||||
}
|
||||
|
||||
- (void)addWordsToInputBar:(NSAttributedString *)words {
|
||||
NSRange selectedRange = self.inputTextView.selectedRange;
|
||||
if (selectedRange.length > 0) {
|
||||
[self.inputTextView.textStorage deleteCharactersInRange:selectedRange];
|
||||
}
|
||||
// Insert draft
|
||||
[self.inputTextView.textStorage insertAttributedString:words atIndex:self.inputTextView.selectedRange.location];
|
||||
|
||||
self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
|
||||
[self resetTextStyle];
|
||||
|
||||
[self updateTextViewFrame];
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - TUIAudioRecorderDelegate
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didCheckPermission:(BOOL)isGranted isFirstTime:(BOOL)isFirstTime {
|
||||
if (isFirstTime) {
|
||||
if (!isGranted) {
|
||||
[self showRequestMicAuthorizationAlert];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
[self updateViewsToRecordingStatus];
|
||||
}
|
||||
|
||||
- (void)showRequestMicAuthorizationAlert {
|
||||
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicTitle)
|
||||
message:TIMCommonLocalizableString(TUIKitInputNoMicTips)
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateLater) style:UIAlertActionStyleCancel handler:nil]];
|
||||
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateEnable)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
UIApplication *app = [UIApplication sharedApplication];
|
||||
NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
|
||||
if ([app canOpenURL:settingsURL]) {
|
||||
[app openURL:settingsURL];
|
||||
}
|
||||
}]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.mm_viewController presentViewController:ac animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateViewsToRecordingStatus {
|
||||
[self.window addSubview:self.recordView];
|
||||
[self.recordView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(self.window);
|
||||
make.width.height.mas_equalTo(self.window);
|
||||
}];
|
||||
self.recordStartTime = [NSDate date];
|
||||
[self.recordView setStatus:Record_Status_Recording];
|
||||
self.recordButton.backgroundColor = [UIColor lightGrayColor];
|
||||
[self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
|
||||
[self showHapticFeedback];
|
||||
}
|
||||
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didPowerChanged:(float)power {
|
||||
if (!self.recordView.hidden) {
|
||||
[self.recordView setPower:power];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)audioRecorder:(TUIAudioRecorder *)recorder didRecordTimeChanged:(NSTimeInterval)time {
|
||||
float uiMaxDuration = MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration);
|
||||
float realMaxDuration = uiMaxDuration + 0.7;
|
||||
NSInteger seconds = uiMaxDuration - time;
|
||||
self.recordView.timeLabel.text = [[NSString alloc] initWithFormat:@"%ld\"", (long)seconds + 1];
|
||||
if (time >= (uiMaxDuration - 4) && time <= uiMaxDuration) {
|
||||
NSInteger seconds = uiMaxDuration - time;
|
||||
/**
|
||||
* The long type is cast here to eliminate compiler warnings.
|
||||
* Here +1 is to round up and optimize the time logic.
|
||||
*/
|
||||
self.recordView.title.text = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitInputWillFinishRecordInSeconds), (long)seconds + 1];
|
||||
} else if (time > realMaxDuration) {
|
||||
[self.recorder stop];
|
||||
NSString *path = self.recorder.recordedFilePath;
|
||||
[self.recordView setStatus:Record_Status_TooLong];
|
||||
|
||||
@weakify(self);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
[self.recordView removeFromSuperview];
|
||||
self.recordView = nil;
|
||||
});
|
||||
if (path) {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
|
||||
[_delegate inputBar:self didSendVoice:path];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
- (TUIAudioRecorder *)recorder {
|
||||
if (!_recorder) {
|
||||
_recorder = [[TUIAudioRecorder alloc] init];
|
||||
_recorder.delegate = self;
|
||||
}
|
||||
return _recorder;
|
||||
}
|
||||
|
||||
- (TUIRecordView *)recordView {
|
||||
if (!_recordView) {
|
||||
_recordView = [[TUIRecordView alloc] init];
|
||||
_recordView.frame = self.frame;
|
||||
}
|
||||
return _recordView;
|
||||
}
|
||||
|
||||
@end
|
||||
153
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.h
Normal file
153
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.h
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* This document declares the relevant components to implement the input area.
|
||||
* The input area includes the emoticon view input area (TUIFaceView+TUIMoreView), the "more" functional area (TUIMoreView) and the text input area
|
||||
* (TUIInputBar). This file contains the TUIInputControllerDelegate protocol and the TInputController class. In the input bar (TUIInputBar), button response
|
||||
* callbacks for expressions, voices, and more views are provided. In this class, the InputBar is actually combined with the above three views to realize the
|
||||
* display and switching logic of each view.
|
||||
*/
|
||||
#import <TIMCommon/TUIMessageCell.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIChatDefine.h"
|
||||
#import "TUIFaceView.h"
|
||||
#import "TUIInputBar.h"
|
||||
#import "TUIMenuView.h"
|
||||
#import "TUIMoreView.h"
|
||||
#import "TUIReplyPreviewBar.h"
|
||||
#import "TUIFaceSegementScrollView.h"
|
||||
|
||||
@class TUIInputController;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIInputControllerDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol TUIInputControllerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Callback when the current InputController height changes.
|
||||
* You can use this callback to adjust the UI layout of each component in the controller according to the changed height.
|
||||
*/
|
||||
- (void)inputController:(TUIInputController *)inputController didChangeHeight:(CGFloat)height;
|
||||
|
||||
/**
|
||||
* Callback when the current InputController sends a message.
|
||||
*/
|
||||
- (void)inputController:(TUIInputController *)inputController didSendMessage:(V2TIMMessage *)msg;
|
||||
|
||||
/**
|
||||
* Callback for clicking a more item
|
||||
* You can use this callback to achieve: according to the clicked cell type, do the next step. For example, select pictures, select files, etc.
|
||||
* At the same time, the implementation of this delegate contains the following code:
|
||||
* <pre>
|
||||
* - (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell {
|
||||
* ……
|
||||
* ……
|
||||
* if(_delegate && [_delegate respondsToSelector:@selector(chatController:onSelectMoreCell:)]){
|
||||
* [_delegate chatController:self onSelectMoreCell:cell];
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* The above code can help you to customize the "more" unit.
|
||||
* For more information you can refer to the comments in TUIChat\UI\Chat\TUIBaseChatController.h
|
||||
*/
|
||||
- (void)inputController:(TUIInputController *)inputController didSelectMoreCell:(TUIInputMoreCell *)cell;
|
||||
|
||||
/**
|
||||
* Callback when @ character is entered
|
||||
*/
|
||||
- (void)inputControllerDidInputAt:(TUIInputController *)inputController;
|
||||
|
||||
/**
|
||||
* Callback when there are @xxx characters removed
|
||||
*/
|
||||
- (void)inputController:(TUIInputController *)inputController didDeleteAt:(NSString *)atText;
|
||||
|
||||
- (void)inputControllerBeginTyping:(TUIInputController *)inputController;
|
||||
|
||||
- (void)inputControllerEndTyping:(TUIInputController *)inputController;
|
||||
|
||||
- (void)inputControllerDidClickMore:(TUIInputController *)inputController;
|
||||
|
||||
@end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIInputControllerDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface TUIInputController : UIViewController
|
||||
|
||||
/**
|
||||
* A preview view above the input box for message reply scenarios
|
||||
*/
|
||||
@property(nonatomic, strong) TUIReplyPreviewBar *replyPreviewBar;
|
||||
|
||||
/**
|
||||
* The preview view below the input box, with the message reference scene
|
||||
*
|
||||
*/
|
||||
@property(nonatomic, strong) TUIReferencePreviewBar *referencePreviewBar;
|
||||
|
||||
/**
|
||||
* Message currently being replied to
|
||||
*/
|
||||
@property(nonatomic, strong) TUIReplyPreviewData *replyData;
|
||||
|
||||
@property(nonatomic, strong) TUIReferencePreviewData *referenceData;
|
||||
|
||||
/**
|
||||
* Input bar
|
||||
* The input bar contains a series of interactive components such as text input box, voice button, "more" button, emoticon button, etc., and provides
|
||||
* corresponding callbacks for these components.
|
||||
*/
|
||||
@property(nonatomic, strong) TUIInputBar *inputBar;
|
||||
|
||||
/**
|
||||
* Emoticon view
|
||||
* The emoticon view generally appears after clicking the "Smiley" button. Responsible for displaying each expression group and the expressions within the
|
||||
* group.
|
||||
*
|
||||
*/
|
||||
//@property(nonatomic, strong) TUIFaceView *faceView;
|
||||
|
||||
@property(nonatomic, strong) TUIFaceSegementScrollView *faceSegementScrollView;
|
||||
/**
|
||||
* Menu view
|
||||
* The menu view is located below the emoticon view and is responsible for providing the emoticon grouping unit and the send button.
|
||||
*/
|
||||
@property(nonatomic, strong) TUIMenuView *menuView;
|
||||
|
||||
/**
|
||||
* More view
|
||||
* More views generally appear after clicking the "More" button ("+" button), and are responsible for displaying each more unit, such as shooting, video, file,
|
||||
* album, etc.
|
||||
*/
|
||||
@property(nonatomic, strong) TUIMoreView *moreView;
|
||||
|
||||
@property(nonatomic, weak) id<TUIInputControllerDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Reset the current input controller.
|
||||
* If there is currently an emoji view or a "more" view being displayed, collapse the corresponding view and set the current status to Input_Status_Input.
|
||||
* That is, no matter what state the current InputController is in, reset it to its initialized state.
|
||||
*/
|
||||
- (void)reset;
|
||||
|
||||
/**
|
||||
* Show/hide preview bar of message reply input box
|
||||
*/
|
||||
- (void)showReplyPreview:(TUIReplyPreviewData *)data;
|
||||
- (void)showReferencePreview:(TUIReferencePreviewData *)data;
|
||||
- (void)exitReplyAndReference:(void (^__nullable)(void))finishedCallback;
|
||||
|
||||
/**
|
||||
* Current input box state
|
||||
*/
|
||||
@property(nonatomic, assign, readonly) InputStatus status;
|
||||
@end
|
||||
730
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.m
Normal file
730
TUIKit/TUIChat/UI_Classic/Input/TUIInputController.m
Normal file
@@ -0,0 +1,730 @@
|
||||
//
|
||||
// TInputController.m
|
||||
// UIKit
|
||||
//
|
||||
// Created by kennethmiao on 2018/9/18.
|
||||
// Copyright © 2018 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIInputController.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUIDarkModel.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIChatDataProvider.h"
|
||||
#import "TUIChatModifyMessageHelper.h"
|
||||
#import "TUICloudCustomDataTypeCenter.h"
|
||||
#import "TUIFaceMessageCell.h"
|
||||
#import "TUIInputMoreCell.h"
|
||||
#import "TUIMenuCell.h"
|
||||
#import "TUIMenuCellData.h"
|
||||
#import "TUIMessageDataProvider.h"
|
||||
#import "TUITextMessageCell.h"
|
||||
#import "TUIVoiceMessageCell.h"
|
||||
#import <TIMCommon/TIMCommonMediator.h>
|
||||
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
|
||||
|
||||
|
||||
@interface TUIInputController () <TUIInputBarDelegate, TUIMenuViewDelegate, TUIFaceViewDelegate, TUIMoreViewDelegate>
|
||||
@property(nonatomic, assign) InputStatus status;
|
||||
@property(nonatomic, assign) CGRect keyboardFrame;
|
||||
|
||||
@property(nonatomic, copy) void (^modifyRootReplyMsgBlock)(TUIMessageCellData *);
|
||||
@end
|
||||
|
||||
@implementation TUIInputController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self setupViews];
|
||||
|
||||
_inputBar.frame = CGRectMake(16, CGRectGetMaxY(self.replyPreviewBar.frame), self.view.frame.size.width - 32, TTextView_Height);
|
||||
[_inputBar setNeedsLayout];
|
||||
_menuView.frame = CGRectMake(16, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width - 32, TMenuView_Menu_Height);
|
||||
[_menuView setNeedsLayout];
|
||||
_faceSegementScrollView.frame = CGRectMake(0, _menuView.frame.origin.y + _menuView.frame.size.height, self.view.frame.size.width, TFaceView_Height);
|
||||
[_faceSegementScrollView setNeedsLayout];
|
||||
_moreView.frame = CGRectMake(0, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width, _moreView.frame.size.height);
|
||||
[_moreView setNeedsLayout];
|
||||
}
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[_inputBar setNeedsLayout];
|
||||
[_menuView setNeedsLayout];
|
||||
[_faceSegementScrollView setNeedsLayout];
|
||||
[_moreView setNeedsLayout];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMessageStatusChanged:) name:@"kTUINotifyMessageStatusChanged" object:nil];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
for (UIGestureRecognizer *gesture in self.view.window.gestureRecognizers) {
|
||||
NSLog(@"gesture = %@", gesture);
|
||||
gesture.delaysTouchesBegan = NO;
|
||||
NSLog(@"delaysTouchesBegan = %@", gesture.delaysTouchesBegan ? @"YES" : @"NO");
|
||||
NSLog(@"delaysTouchesEnded = %@", gesture.delaysTouchesEnded ? @"YES" : @"NO");
|
||||
}
|
||||
self.navigationController.interactivePopGestureRecognizer.delaysTouchesBegan = NO;
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setupViews {
|
||||
self.view.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
|
||||
_status = Input_Status_Input;
|
||||
|
||||
_inputBar = [[TUIInputBar alloc] initWithFrame:CGRectZero];
|
||||
_inputBar.delegate = self;
|
||||
[self.view addSubview:_inputBar];
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
CGFloat inputContainerBottom = [self getInputContainerBottom];
|
||||
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
|
||||
}
|
||||
if (_status == Input_Status_Input_Keyboard) {
|
||||
_status = Input_Status_Input;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
if (_status == Input_Status_Input_Face) {
|
||||
[self hideFaceAnimation];
|
||||
} else if (_status == Input_Status_Input_More) {
|
||||
[self hideMoreAnimation];
|
||||
} else {
|
||||
//[self hideFaceAnimation:NO];
|
||||
//[self hideMoreAnimation:NO];
|
||||
}
|
||||
_status = Input_Status_Input_Keyboard;
|
||||
}
|
||||
|
||||
- (void)keyboardWillChangeFrame:(NSNotification *)notification {
|
||||
CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
CGFloat inputContainerBottom = [self getInputContainerBottom];
|
||||
[_delegate inputController:self didChangeHeight:keyboardFrame.size.height + inputContainerBottom];
|
||||
}
|
||||
self.keyboardFrame = keyboardFrame;
|
||||
}
|
||||
|
||||
- (void)hideFaceAnimation {
|
||||
self.faceSegementScrollView.hidden = NO;
|
||||
self.faceSegementScrollView.alpha = 1.0;
|
||||
self.menuView.hidden = NO;
|
||||
self.menuView.alpha = 1.0;
|
||||
__weak typeof(self) ws = self;
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^{
|
||||
ws.faceSegementScrollView.alpha = 0.0;
|
||||
ws.menuView.alpha = 0.0;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
ws.faceSegementScrollView.hidden = YES;
|
||||
ws.faceSegementScrollView.alpha = 1.0;
|
||||
ws.menuView.hidden = YES;
|
||||
ws.menuView.alpha = 1.0;
|
||||
[ws.menuView removeFromSuperview];
|
||||
[ws.faceSegementScrollView removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showFaceAnimation {
|
||||
[self.view addSubview:self.faceSegementScrollView];
|
||||
[self.view addSubview:self.menuView];
|
||||
__weak typeof(self) ws = self;
|
||||
[self.faceSegementScrollView updateRecentView];
|
||||
[self.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:(self.inputBar.inputTextView.text.length > 0)?YES:NO];
|
||||
self.faceSegementScrollView.onScrollCallback = ^(NSInteger indexPage) {
|
||||
[ws.menuView scrollToMenuIndex:indexPage];
|
||||
};
|
||||
self.inputBar.inputBarTextChanged = ^(UITextView *textview) {
|
||||
if(textview.text.length > 0) {
|
||||
[ws.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:YES];
|
||||
}
|
||||
else {
|
||||
[ws.faceSegementScrollView setAllFloatCtrlViewAllowSendSwitch:NO];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
self.faceSegementScrollView.hidden = NO;
|
||||
CGRect frame = self.menuView.frame;
|
||||
frame.origin.y = self.view.window.frame.size.height;
|
||||
self.menuView.frame = frame;
|
||||
self.menuView.hidden = NO;
|
||||
frame = self.faceSegementScrollView.frame;
|
||||
frame.origin.y = self.menuView.frame.origin.y + self.menuView.frame.size.height;
|
||||
self.faceSegementScrollView.frame = frame;
|
||||
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^{
|
||||
CGRect newFrame = ws.menuView.frame;
|
||||
newFrame.origin.y = CGRectGetMaxY(ws.inputBar.frame); // ws.inputBar.frame.origin.y + ws.inputBar.frame.size.height;
|
||||
ws.menuView.frame = newFrame;
|
||||
|
||||
newFrame = ws.faceSegementScrollView.frame;
|
||||
newFrame.origin.y = ws.menuView.frame.origin.y + ws.menuView.frame.size.height;
|
||||
ws.faceSegementScrollView.frame = newFrame;
|
||||
}
|
||||
completion:nil];
|
||||
}
|
||||
|
||||
- (void)hideMoreAnimation {
|
||||
self.moreView.hidden = NO;
|
||||
self.moreView.alpha = 1.0;
|
||||
__weak typeof(self) ws = self;
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^{
|
||||
ws.moreView.alpha = 0.0;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
ws.moreView.hidden = YES;
|
||||
ws.moreView.alpha = 1.0;
|
||||
[ws.moreView removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showMoreAnimation {
|
||||
[self.view addSubview:self.moreView];
|
||||
|
||||
self.moreView.hidden = NO;
|
||||
CGRect frame = self.moreView.frame;
|
||||
frame.origin.y = self.view.window.frame.size.height;
|
||||
self.moreView.frame = frame;
|
||||
__weak typeof(self) ws = self;
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^{
|
||||
CGRect newFrame = ws.moreView.frame;
|
||||
newFrame.origin.y = ws.inputBar.frame.origin.y + ws.inputBar.frame.size.height;
|
||||
ws.moreView.frame = newFrame;
|
||||
}
|
||||
completion:nil];
|
||||
}
|
||||
|
||||
- (void)inputBarDidTouchVoice:(TUIInputBar *)textView {
|
||||
if (_status == Input_Status_Input_Talk) {
|
||||
return;
|
||||
}
|
||||
[_inputBar.inputTextView resignFirstResponder];
|
||||
[self hideFaceAnimation];
|
||||
[self hideMoreAnimation];
|
||||
_status = Input_Status_Input_Talk;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
CGFloat inputContainerBottom = [self getInputContainerBottom];
|
||||
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBarDidTouchMore:(TUIInputBar *)textView {
|
||||
if (_status == Input_Status_Input_More) {
|
||||
return;
|
||||
}
|
||||
if (_status == Input_Status_Input_Face) {
|
||||
[self hideFaceAnimation];
|
||||
}
|
||||
[_inputBar.inputTextView resignFirstResponder];
|
||||
[self showMoreAnimation];
|
||||
_status = Input_Status_Input_More;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[_delegate inputController:self didChangeHeight:CGRectGetMaxY(_inputBar.frame) + self.moreView.frame.size.height + Bottom_SafeHeight];
|
||||
}
|
||||
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerDidClickMore:)]) {
|
||||
[_delegate inputControllerDidClickMore:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBarDidTouchFace:(TUIInputBar *)textView {
|
||||
if ([TIMConfig defaultConfig].faceGroups.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
|
||||
|
||||
if (_status == Input_Status_Input_More) {
|
||||
[self hideMoreAnimation];
|
||||
}
|
||||
[_inputBar.inputTextView resignFirstResponder];
|
||||
_status = Input_Status_Input_Face;
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[_delegate inputController:self
|
||||
didChangeHeight:CGRectGetMaxY(_inputBar.frame) + self.faceSegementScrollView.frame.size.height + self.menuView.frame.size.height ];
|
||||
}
|
||||
[self showFaceAnimation];
|
||||
}
|
||||
|
||||
- (void)inputBarDidTouchKeyboard:(TUIInputBar *)textView {
|
||||
if (_status == Input_Status_Input_More) {
|
||||
[self hideMoreAnimation];
|
||||
}
|
||||
if (_status == Input_Status_Input_Face) {
|
||||
[self hideFaceAnimation];
|
||||
}
|
||||
_status = Input_Status_Input_Keyboard;
|
||||
[_inputBar.inputTextView becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)inputBar:(TUIInputBar *)textView didChangeInputHeight:(CGFloat)offset {
|
||||
if (_status == Input_Status_Input_Face) {
|
||||
[self showFaceAnimation];
|
||||
} else if (_status == Input_Status_Input_More) {
|
||||
[self showMoreAnimation];
|
||||
}
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[_delegate inputController:self didChangeHeight:self.view.frame.size.height + offset];
|
||||
if (_referencePreviewBar) {
|
||||
CGRect referencePreviewBarFrame = _referencePreviewBar.frame;
|
||||
_referencePreviewBar.frame = CGRectMake(referencePreviewBarFrame.origin.x, referencePreviewBarFrame.origin.y + offset,
|
||||
referencePreviewBarFrame.size.width, referencePreviewBarFrame.size.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBar:(TUIInputBar *)textView didSendText:(NSString *)text {
|
||||
/**
|
||||
* Emoticon internationalization --> restore to actual Chinese key
|
||||
*/
|
||||
NSString *content = [text getInternationalStringWithfaceContent];
|
||||
V2TIMMessage *message = [[V2TIMManager sharedInstance] createTextMessage:content];
|
||||
[self appendReplyDataIfNeeded:message];
|
||||
[self appendReferenceDataIfNeeded:message];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
|
||||
[_delegate inputController:self didSendMessage:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputMessageStatusChanged:(NSNotification *)noti {
|
||||
NSDictionary *userInfo = noti.userInfo;
|
||||
TUIMessageCellData *msg = userInfo[@"msg"];
|
||||
long status = [userInfo[@"status"] intValue];
|
||||
if ([msg isKindOfClass:TUIMessageCellData.class] && status == Msg_Status_Succ) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.modifyRootReplyMsgBlock) {
|
||||
self.modifyRootReplyMsgBlock(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)appendReplyDataIfNeeded:(V2TIMMessage *)message {
|
||||
if (self.replyData) {
|
||||
V2TIMMessage *parentMsg = self.replyData.originMessage;
|
||||
NSMutableDictionary *simpleReply = [NSMutableDictionary dictionary];
|
||||
[simpleReply addEntriesFromDictionary:@{
|
||||
@"messageID" : self.replyData.msgID ?: @"",
|
||||
@"messageAbstract" : [self.replyData.msgAbstract ?: @"" getInternationalStringWithfaceContent],
|
||||
@"messageSender" : self.replyData.sender ?: @"",
|
||||
@"messageType" : @(self.replyData.type),
|
||||
@"messageTime" : @(self.replyData.originMessage.timestamp ? [self.replyData.originMessage.timestamp timeIntervalSince1970] : 0),
|
||||
@"messageSequence" : @(self.replyData.originMessage.seq),
|
||||
@"version" : @(kMessageReplyVersion),
|
||||
}];
|
||||
|
||||
NSMutableDictionary *cloudResultDic = [[NSMutableDictionary alloc] initWithCapacity:5];
|
||||
if (parentMsg.cloudCustomData) {
|
||||
NSDictionary *originDic = [TUITool jsonData2Dictionary:parentMsg.cloudCustomData];
|
||||
if (originDic && [originDic isKindOfClass:[NSDictionary class]]) {
|
||||
[cloudResultDic addEntriesFromDictionary:originDic];
|
||||
}
|
||||
/**
|
||||
* Accept the data in the parent, but cannot save messageReplies\messageReact, because the root message topic creator has this field.
|
||||
* messageReplies\messageReact cannot be stored in the new message currently sent
|
||||
*/
|
||||
[cloudResultDic removeObjectForKey:@"messageReplies"];
|
||||
[cloudResultDic removeObjectForKey:@"messageReact"];
|
||||
}
|
||||
NSString *messageParentReply = cloudResultDic[@"messageReply"];
|
||||
NSString *messageRootID = [messageParentReply valueForKey:@"messageRootID"];
|
||||
if (self.replyData.messageRootID.length > 0) {
|
||||
messageRootID = self.replyData.messageRootID;
|
||||
}
|
||||
if (!IS_NOT_EMPTY_NSSTRING(messageRootID)) {
|
||||
/**
|
||||
* If the original message does not have a messageRootID, you need to use the msgID of the current original message as root
|
||||
*/
|
||||
if (IS_NOT_EMPTY_NSSTRING(parentMsg.msgID)) {
|
||||
messageRootID = parentMsg.msgID;
|
||||
}
|
||||
}
|
||||
[simpleReply setObject:messageRootID forKey:@"messageRootID"];
|
||||
[cloudResultDic setObject:simpleReply forKey:@"messageReply"];
|
||||
NSData *data = [TUITool dictionary2JsonData:cloudResultDic];
|
||||
if (data) {
|
||||
message.cloudCustomData = data;
|
||||
}
|
||||
|
||||
[self exitReplyAndReference:nil];
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.modifyRootReplyMsgBlock = ^(TUIMessageCellData *cellData) {
|
||||
__strong typeof(self) strongSelf = weakSelf;
|
||||
[strongSelf modifyRootReplyMsgByID:messageRootID currentMsg:cellData];
|
||||
strongSelf.modifyRootReplyMsgBlock = nil;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
- (void)modifyRootReplyMsgByID:(NSString *)messageRootID currentMsg:(TUIMessageCellData *)messageCellData {
|
||||
NSDictionary *simpleCurrentContent = @{
|
||||
@"messageID" : messageCellData.innerMessage.msgID ?: @"",
|
||||
@"messageAbstract" : [messageCellData.innerMessage.textElem.text ?: @"" getInternationalStringWithfaceContent],
|
||||
@"messageSender" : messageCellData.senderName ? : @"",
|
||||
@"messageType" : @(messageCellData.innerMessage.elemType),
|
||||
@"messageTime" : @(messageCellData.innerMessage.timestamp ? [messageCellData.innerMessage.timestamp timeIntervalSince1970] : 0),
|
||||
@"messageSequence" : @(messageCellData.innerMessage.seq),
|
||||
@"version" : @(kMessageReplyVersion)
|
||||
};
|
||||
if (messageRootID) {
|
||||
[TUIChatDataProvider findMessages:@[ messageRootID ]
|
||||
callback:^(BOOL succ, NSString *_Nonnull error_message, NSArray *_Nonnull msgs) {
|
||||
if (succ) {
|
||||
if (msgs.count > 0) {
|
||||
V2TIMMessage *rootMsg = msgs.firstObject;
|
||||
[[TUIChatModifyMessageHelper defaultHelper] modifyMessage:rootMsg simpleCurrentContent:simpleCurrentContent];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)appendReferenceDataIfNeeded:(V2TIMMessage *)message {
|
||||
if (self.referenceData) {
|
||||
NSDictionary *dict = @{
|
||||
@"messageReply" : @{
|
||||
@"messageID" : self.referenceData.msgID ?: @"",
|
||||
@"messageAbstract" : [self.referenceData.msgAbstract ?: @"" getInternationalStringWithfaceContent],
|
||||
@"messageSender" : self.referenceData.sender ?: @"",
|
||||
@"messageType" : @(self.referenceData.type),
|
||||
@"messageTime" : @(self.referenceData.originMessage.timestamp ? [self.referenceData.originMessage.timestamp timeIntervalSince1970] : 0),
|
||||
@"messageSequence" : @(self.referenceData.originMessage.seq),
|
||||
@"version" : @(kMessageReplyVersion)
|
||||
}
|
||||
};
|
||||
NSError *error = nil;
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error];
|
||||
if (error == nil) {
|
||||
message.cloudCustomData = data;
|
||||
}
|
||||
[self exitReplyAndReference:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBar:(TUIInputBar *)textView didSendVoice:(NSString *)path {
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
AVURLAsset *audioAsset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
float duration = (float)CMTimeGetSeconds(audioAsset.duration);
|
||||
int formatDuration = duration > 59 ? 60 : duration + 1 ;
|
||||
V2TIMMessage *message = [[V2TIMManager sharedInstance] createSoundMessage:path duration:formatDuration];
|
||||
if (message && _delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
|
||||
[_delegate inputController:self didSendMessage:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBarDidInputAt:(TUIInputBar *)textView {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerDidInputAt:)]) {
|
||||
[_delegate inputControllerDidInputAt:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBar:(TUIInputBar *)textView didDeleteAt:(NSString *)atText {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didDeleteAt:)]) {
|
||||
[_delegate inputController:self didDeleteAt:atText];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputBarDidDeleteBackward:(TUIInputBar *)textView {
|
||||
if (textView.inputTextView.text.length == 0) {
|
||||
[self exitReplyAndReference:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputTextViewShouldBeginTyping:(UITextView *)textView {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerBeginTyping:)]) {
|
||||
[_delegate inputControllerBeginTyping:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputTextViewShouldEndTyping:(UITextView *)textView {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputControllerEndTyping:)]) {
|
||||
[_delegate inputControllerEndTyping:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
if (_status == Input_Status_Input) {
|
||||
return;
|
||||
} else if (_status == Input_Status_Input_More) {
|
||||
[self hideMoreAnimation];
|
||||
} else if (_status == Input_Status_Input_Face) {
|
||||
[self hideFaceAnimation];
|
||||
}
|
||||
_status = Input_Status_Input;
|
||||
[_inputBar.inputTextView resignFirstResponder];
|
||||
|
||||
[TUICore notifyEvent:TUICore_TUIChatNotify
|
||||
subKey:TUICore_TUIChatNotify_KeyboardWillHideSubKey
|
||||
object:nil
|
||||
param:nil];
|
||||
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
CGFloat inputContainerBottom = [self getInputContainerBottom];
|
||||
[_delegate inputController:self didChangeHeight:inputContainerBottom + Bottom_SafeHeight];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showReferencePreview:(TUIReferencePreviewData *)data {
|
||||
self.referenceData = data;
|
||||
[self.referencePreviewBar removeFromSuperview];
|
||||
[self.view addSubview:self.referencePreviewBar];
|
||||
self.inputBar.lineView.hidden = YES;
|
||||
|
||||
self.referencePreviewBar.previewReferenceData = data;
|
||||
|
||||
self.inputBar.mm_y = 0;
|
||||
|
||||
self.referencePreviewBar.frame = CGRectMake(0, 0, self.view.bounds.size.width, TMenuView_Menu_Height);
|
||||
self.referencePreviewBar.mm_y = CGRectGetMaxY(self.inputBar.frame);
|
||||
|
||||
// Set the default position to solve the UI confusion when the keyboard does not become the first responder
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + Bottom_SafeHeight + TMenuView_Menu_Height];
|
||||
}
|
||||
|
||||
if (self.status == Input_Status_Input_Keyboard) {
|
||||
CGFloat keyboradHeight = self.keyboardFrame.size.height;
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.referencePreviewBar.frame) + keyboradHeight];
|
||||
}
|
||||
} else if (self.status == Input_Status_Input_Face || self.status == Input_Status_Input_Talk) {
|
||||
[self.inputBar changeToKeyboard];
|
||||
} else {
|
||||
[self.inputBar.inputTextView becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
- (void)showReplyPreview:(TUIReplyPreviewData *)data {
|
||||
self.replyData = data;
|
||||
[self.replyPreviewBar removeFromSuperview];
|
||||
[self.view addSubview:self.replyPreviewBar];
|
||||
self.inputBar.lineView.hidden = YES;
|
||||
|
||||
self.replyPreviewBar.previewData = data;
|
||||
|
||||
self.replyPreviewBar.frame = CGRectMake(0, 0, self.view.bounds.size.width, TMenuView_Menu_Height);
|
||||
self.inputBar.mm_y = CGRectGetMaxY(self.replyPreviewBar.frame);
|
||||
|
||||
// Set the default position to solve the UI confusion when the keyboard does not become the first responder
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + Bottom_SafeHeight];
|
||||
}
|
||||
|
||||
if (self.status == Input_Status_Input_Keyboard) {
|
||||
CGFloat keyboradHeight = self.keyboardFrame.size.height;
|
||||
if (self.delegate && [self.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[self.delegate inputController:self didChangeHeight:CGRectGetMaxY(self.inputBar.frame) + keyboradHeight];
|
||||
}
|
||||
} else if (self.status == Input_Status_Input_Face || self.status == Input_Status_Input_Talk) {
|
||||
[self.inputBar changeToKeyboard];
|
||||
} else {
|
||||
[self.inputBar.inputTextView becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)exitReplyAndReference:(void (^__nullable)(void))finishedCallback {
|
||||
if (self.replyData == nil && self.referenceData == nil) {
|
||||
if (finishedCallback) {
|
||||
finishedCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.replyData = nil;
|
||||
self.referenceData = nil;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[UIView animateWithDuration:0.25
|
||||
animations:^{
|
||||
weakSelf.replyPreviewBar.hidden = YES;
|
||||
weakSelf.referencePreviewBar.hidden = YES;
|
||||
weakSelf.inputBar.mm_y = 0;
|
||||
|
||||
if (weakSelf.status == Input_Status_Input_Keyboard) {
|
||||
CGFloat keyboradHeight = weakSelf.keyboardFrame.size.height;
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[weakSelf.delegate inputController:weakSelf didChangeHeight:CGRectGetMaxY(weakSelf.inputBar.frame) + keyboradHeight];
|
||||
}
|
||||
} else {
|
||||
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(inputController:didChangeHeight:)]) {
|
||||
[weakSelf.delegate inputController:weakSelf didChangeHeight:CGRectGetMaxY(weakSelf.inputBar.frame) + Bottom_SafeHeight];
|
||||
}
|
||||
}
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
[weakSelf.replyPreviewBar removeFromSuperview];
|
||||
[weakSelf.referencePreviewBar removeFromSuperview];
|
||||
weakSelf.replyPreviewBar = nil;
|
||||
weakSelf.referencePreviewBar = nil;
|
||||
[weakSelf hideFaceAnimation];
|
||||
weakSelf.inputBar.lineView.hidden = NO;
|
||||
if (finishedCallback) {
|
||||
finishedCallback();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)menuView:(TUIMenuView *)menuView didSelectItemAtIndex:(NSInteger)index {
|
||||
[self.faceSegementScrollView setPageIndex:index];
|
||||
}
|
||||
|
||||
- (void)menuViewDidSendMessage:(TUIMenuView *)menuView {
|
||||
NSString *text = [_inputBar getInput];
|
||||
if ([text isEqualToString:@""]) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Emoticon internationalization --> restore to actual Chinese key
|
||||
*/
|
||||
NSString *content = [text getInternationalStringWithfaceContent];
|
||||
[_inputBar clearInput];
|
||||
V2TIMMessage *message = [[V2TIMManager sharedInstance] createTextMessage:content];
|
||||
[self appendReplyDataIfNeeded:message];
|
||||
[self appendReferenceDataIfNeeded:message];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
|
||||
[_delegate inputController:self didSendMessage:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)faceView:(TUIFaceView *)faceView scrollToFaceGroupIndex:(NSInteger)index {
|
||||
[self.menuView scrollToMenuIndex:index];
|
||||
}
|
||||
|
||||
- (void)faceViewDidBackDelete:(TUIFaceView *)faceView {
|
||||
[_inputBar backDelete];
|
||||
}
|
||||
|
||||
- (void)faceViewClickSendMessageBtn {
|
||||
[self menuViewDidSendMessage:self.menuView];
|
||||
}
|
||||
|
||||
- (void)faceView:(TUIFaceView *)faceView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIFaceGroup *group = faceView.faceGroups[indexPath.section];
|
||||
TUIFaceCellData *face = group.faces[indexPath.row];
|
||||
if (group.isNeedAddInInputBar) {
|
||||
[_inputBar addEmoji:face];
|
||||
[self updateRecentMenuQueue:face.name];
|
||||
} else {
|
||||
if (face.name) {
|
||||
V2TIMMessage *message = [[V2TIMManager sharedInstance] createFaceMessage:group.groupIndex data:[face.name dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSendMessage:)]) {
|
||||
[_delegate inputController:self didSendMessage:message];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateRecentMenuQueue:(NSString *)faceName {
|
||||
id<TUIEmojiMeditorProtocol> service = [[TIMCommonMediator share] getObject:@protocol(TUIEmojiMeditorProtocol)];
|
||||
return [service updateRecentMenuQueue:faceName];
|
||||
}
|
||||
|
||||
#pragma mark - more view delegate
|
||||
- (void)moreView:(TUIMoreView *)moreView didSelectMoreCell:(TUIInputMoreCell *)cell {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(inputController:didSelectMoreCell:)]) {
|
||||
[_delegate inputController:self didSelectMoreCell:cell];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - lazy load
|
||||
|
||||
- (TUIFaceSegementScrollView *)faceSegementScrollView {
|
||||
if(!_faceSegementScrollView) {
|
||||
_faceSegementScrollView = [[TUIFaceSegementScrollView alloc]
|
||||
initWithFrame:CGRectMake(0,
|
||||
_inputBar.frame.origin.y + _inputBar.frame.size.height,
|
||||
self.view.frame.size.width,
|
||||
TFaceView_Height)];
|
||||
[_faceSegementScrollView setItems:[TIMConfig defaultConfig].faceGroups delegate:self];
|
||||
}
|
||||
return _faceSegementScrollView;
|
||||
}
|
||||
- (TUIMoreView *)moreView {
|
||||
if (!_moreView) {
|
||||
_moreView =
|
||||
[[TUIMoreView alloc] initWithFrame:CGRectMake(0,
|
||||
_inputBar.frame.origin.y + _inputBar.frame.size.height,
|
||||
_faceSegementScrollView.frame.size.width,
|
||||
0)];
|
||||
_moreView.delegate = self;
|
||||
}
|
||||
return _moreView;
|
||||
}
|
||||
|
||||
- (TUIMenuView *)menuView {
|
||||
if (!_menuView) {
|
||||
_menuView = [[TUIMenuView alloc]
|
||||
initWithFrame:CGRectMake(16, _inputBar.frame.origin.y + _inputBar.frame.size.height, self.view.frame.size.width - 32, TMenuView_Menu_Height)];
|
||||
_menuView.delegate = self;
|
||||
|
||||
TIMConfig *config = [TIMConfig defaultConfig];
|
||||
NSMutableArray *menus = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < config.faceGroups.count; ++i) {
|
||||
TUIFaceGroup *group = config.faceGroups[i];
|
||||
TUIMenuCellData *data = [[TUIMenuCellData alloc] init];
|
||||
data.path = group.menuPath;
|
||||
data.isSelected = NO;
|
||||
if (i == 0) {
|
||||
data.isSelected = YES;
|
||||
}
|
||||
[menus addObject:data];
|
||||
}
|
||||
[_menuView setData:menus];
|
||||
}
|
||||
return _menuView;
|
||||
}
|
||||
|
||||
- (TUIReplyPreviewBar *)replyPreviewBar {
|
||||
if (_replyPreviewBar == nil) {
|
||||
_replyPreviewBar = [[TUIReplyPreviewBar alloc] init];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_replyPreviewBar.onClose = ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf exitReplyAndReference:nil];
|
||||
};
|
||||
}
|
||||
return _replyPreviewBar;
|
||||
}
|
||||
|
||||
- (TUIReferencePreviewBar *)referencePreviewBar {
|
||||
if (_referencePreviewBar == nil) {
|
||||
_referencePreviewBar = [[TUIReferencePreviewBar alloc] init];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_referencePreviewBar.onClose = ^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf exitReplyAndReference:nil];
|
||||
};
|
||||
}
|
||||
return _referencePreviewBar;
|
||||
}
|
||||
|
||||
- (CGFloat)getInputContainerBottom {
|
||||
CGFloat inputHeight = CGRectGetMaxY(_inputBar.frame);
|
||||
if (_referencePreviewBar) {
|
||||
inputHeight = CGRectGetMaxY(_referencePreviewBar.frame);
|
||||
}
|
||||
return inputHeight;
|
||||
}
|
||||
|
||||
@end
|
||||
61
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.h
Normal file
61
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.h
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* This file declares the components used to implement the emoji menu view.
|
||||
* The emoji menu view, the bright white view at the bottom of the emoji view, is responsible for displaying individual emoji groups and their thumbnails, and
|
||||
* providing a "Send" button.
|
||||
*
|
||||
* The TUIMenuViewDelegate protocol provides the emoticon menu view with event callbacks for sending messages and cell selection.
|
||||
* The TUIMenuView class, the "ontology" of the emoticon menu view, is responsible for displaying it in the form of a view in the UI, and at the same time
|
||||
* serving as a "container" for each component. You can switch between different groups of emoticons or send emoticons through the emoticon menu view.
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIMenuCellData.h"
|
||||
|
||||
@class TUIMenuView;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIMenuViewDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol TUIMenuViewDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Callback after clicking on a specific menuCell
|
||||
* You can use this callback to achieve: in response to the user's click, switch to the corresponding emoticon group view according to the menuCell selected by
|
||||
* the user.
|
||||
*/
|
||||
- (void)menuView:(TUIMenuView *)menuView didSelectItemAtIndex:(NSInteger)index;
|
||||
|
||||
/**
|
||||
* Callback after click of send button on menuView
|
||||
* You can send the content of the current input box (TUIInputBar) through this callback
|
||||
* In the default implementation of TUIKit, the delegate call chain is menuView -> inputController -> messageController.
|
||||
* Call the sendMessage function in the above classes respectively, so that the functions are reasonably layered and the code reuse rate is improved.
|
||||
*/
|
||||
- (void)menuViewDidSendMessage:(TUIMenuView *)menuView;
|
||||
|
||||
@end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIMenuView
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
@interface TUIMenuView : UIView
|
||||
|
||||
@property(nonatomic, strong) UICollectionView *menuCollectionView;
|
||||
|
||||
@property(nonatomic, strong) UICollectionViewFlowLayout *menuFlowLayout;
|
||||
|
||||
@property(nonatomic, weak) id<TUIMenuViewDelegate> delegate;
|
||||
|
||||
- (void)scrollToMenuIndex:(NSInteger)index;
|
||||
|
||||
- (void)setData:(NSMutableArray<TUIMenuCellData *> *)data;
|
||||
|
||||
@end
|
||||
123
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.m
Normal file
123
TUIKit/TUIChat/UI_Classic/Input/TUIMenuView.m
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// MenuView.m
|
||||
// UIKit
|
||||
//
|
||||
// Created by kennethmiao on 2018/9/18.
|
||||
// Copyright © 2018 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMenuView.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIDarkModel.h>
|
||||
#import <TUICore/TUIGlobalization.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIMenuCell.h"
|
||||
|
||||
@interface TUIMenuView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
||||
@property(nonatomic, strong) NSMutableArray<TUIMenuCellData *> *data;
|
||||
@end
|
||||
|
||||
@implementation TUIMenuView
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setupViews];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setData:(NSMutableArray<TUIMenuCellData *> *)data {
|
||||
_data = data;
|
||||
[_menuCollectionView reloadData];
|
||||
[self defaultLayout];
|
||||
[_menuCollectionView layoutIfNeeded];
|
||||
[_menuCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
||||
}
|
||||
|
||||
- (void)setupViews {
|
||||
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
|
||||
|
||||
_menuFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
|
||||
_menuFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
_menuFlowLayout.minimumLineSpacing = 0;
|
||||
_menuFlowLayout.minimumInteritemSpacing = 0;
|
||||
//_menuFlowLayout.headerReferenceSize = CGSizeMake(TMenuView_Margin, 1);
|
||||
|
||||
_menuCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_menuFlowLayout];
|
||||
[_menuCollectionView registerClass:[TUIMenuCell class] forCellWithReuseIdentifier:TMenuCell_ReuseId];
|
||||
[_menuCollectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:TMenuCell_Line_ReuseId];
|
||||
_menuCollectionView.collectionViewLayout = _menuFlowLayout;
|
||||
_menuCollectionView.delegate = self;
|
||||
_menuCollectionView.dataSource = self;
|
||||
_menuCollectionView.showsHorizontalScrollIndicator = NO;
|
||||
_menuCollectionView.showsVerticalScrollIndicator = NO;
|
||||
_menuCollectionView.backgroundColor = self.backgroundColor;
|
||||
_menuCollectionView.alwaysBounceHorizontal = YES;
|
||||
_menuCollectionView.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
|
||||
[self addSubview:_menuCollectionView];
|
||||
}
|
||||
|
||||
- (void)defaultLayout {
|
||||
[_menuCollectionView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(0);
|
||||
make.trailing.mas_equalTo(self.mas_trailing).mas_offset(0);
|
||||
make.height.mas_equalTo(40);
|
||||
make.centerY.mas_equalTo(self);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)sendUpInside:(UIButton *)sender {
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(menuViewDidSendMessage:)]) {
|
||||
[_delegate menuViewDidSendMessage:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return _data.count * 2;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row % 2 == 0) {
|
||||
TUIMenuCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMenuCell_ReuseId forIndexPath:indexPath];
|
||||
[cell setData:_data[indexPath.row / 2]];
|
||||
return cell;
|
||||
} else {
|
||||
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMenuCell_Line_ReuseId forIndexPath:indexPath];
|
||||
cell.backgroundColor = [UIColor clearColor];
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row % 2 != 0) {
|
||||
return;
|
||||
}
|
||||
for (NSInteger i = 0; i < _data.count; ++i) {
|
||||
TUIMenuCellData *data = _data[i];
|
||||
data.isSelected = (i == indexPath.row / 2);
|
||||
}
|
||||
[_menuCollectionView reloadData];
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(menuView:didSelectItemAtIndex:)]) {
|
||||
[_delegate menuView:self didSelectItemAtIndex:indexPath.row / 2];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (indexPath.row % 2 == 0) {
|
||||
CGFloat wh = collectionView.frame.size.height;
|
||||
return CGSizeMake(wh, wh);
|
||||
} else {
|
||||
return CGSizeMake(TLine_Heigh, collectionView.frame.size.height);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)scrollToMenuIndex:(NSInteger)index {
|
||||
for (NSInteger i = 0; i < _data.count; ++i) {
|
||||
TUIMenuCellData *data = _data[i];
|
||||
data.isSelected = (i == index);
|
||||
}
|
||||
[_menuCollectionView reloadData];
|
||||
}
|
||||
@end
|
||||
61
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.h
Normal file
61
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.h
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
* This file declares the components used to implement the "more" view.
|
||||
* More view, the window that appears when you click the "+" button in the lower right corner of the chat window.
|
||||
* More view are usually responsible for providing some additional important functions, such as sending pictures, taking pictures and sending, sending videos,
|
||||
* sending files, etc. Currently TUIKit implements and provides the above four functions. If the above 4 functions cannot meet your functional requirements, you
|
||||
* can also add your custom unit in this view.
|
||||
*
|
||||
* TUIMoreView provides an entry for sending multimedia messages such as videos, pictures, and files based on the existing text messaging.
|
||||
* The TUIMoreViewDelegate protocol provides callbacks for more views in response to user actions.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIInputMoreCell.h"
|
||||
|
||||
@class TUIMoreView;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIMoreViewDelegate
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol TUIMoreViewDelegate <NSObject>
|
||||
|
||||
- (void)moreView:(TUIMoreView *)moreView didSelectMoreCell:(TUIInputMoreCell *)cell;
|
||||
|
||||
@end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TUIMoreView
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 【Module name】 TUIMoreView
|
||||
* 【Function description】More view are displayed after clicking the "+" on the far right of the input box.
|
||||
* This view can provide you with functional extensions on the current page. for example:
|
||||
* 1. Camera. Call the system camera to take a photo and send
|
||||
* 2. Photo. Select a picture from the system album and send it.
|
||||
* 3. Video. Select a video from the system gallery and send it.
|
||||
* 4. File. Select file from system files and send.
|
||||
*/
|
||||
@interface TUIMoreView : UIView
|
||||
|
||||
@property(nonatomic, strong) UIView *lineView;
|
||||
|
||||
@property(nonatomic, strong) UICollectionView *moreCollectionView;
|
||||
|
||||
@property(nonatomic, strong) UICollectionViewFlowLayout *moreFlowLayout;
|
||||
|
||||
@property(nonatomic, strong) UIPageControl *pageControl;
|
||||
|
||||
@property(nonatomic, weak) id<TUIMoreViewDelegate> delegate;
|
||||
|
||||
- (void)setData:(NSArray *)data;
|
||||
|
||||
@end
|
||||
171
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.m
Normal file
171
TUIKit/TUIChat/UI_Classic/Input/TUIMoreView.m
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// TUIMoreView.m
|
||||
// UIKit
|
||||
//
|
||||
// Created by kennethmiao on 2018/9/21.
|
||||
// Copyright © 2018 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMoreView.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/TUIThemeManager.h>
|
||||
#import "TUIInputMoreCell.h"
|
||||
|
||||
@interface TUIMoreView () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
||||
@property(nonatomic, strong) NSArray *data;
|
||||
@property(nonatomic, strong) NSMutableDictionary *itemIndexs;
|
||||
@property(nonatomic, assign) NSInteger sectionCount;
|
||||
@property(nonatomic, assign) NSInteger itemsInSection;
|
||||
@property(nonatomic, assign) NSInteger rowCount;
|
||||
@end
|
||||
|
||||
@implementation TUIMoreView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self setupViews];
|
||||
[self defaultLayout];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupViews {
|
||||
self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
|
||||
|
||||
_moreFlowLayout = [[TUICollectionRTLFitFlowLayout alloc] init];
|
||||
_moreFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
_moreFlowLayout.minimumLineSpacing = 0;
|
||||
_moreFlowLayout.minimumInteritemSpacing = 0;
|
||||
_moreFlowLayout.sectionInset = UIEdgeInsetsMake(0, TMoreView_Section_Padding, 0, TMoreView_Section_Padding);
|
||||
|
||||
_moreCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_moreFlowLayout];
|
||||
[_moreCollectionView registerClass:[TUIInputMoreCell class] forCellWithReuseIdentifier:TMoreCell_ReuseId];
|
||||
_moreCollectionView.collectionViewLayout = _moreFlowLayout;
|
||||
_moreCollectionView.pagingEnabled = YES;
|
||||
_moreCollectionView.delegate = self;
|
||||
_moreCollectionView.dataSource = self;
|
||||
_moreCollectionView.showsHorizontalScrollIndicator = NO;
|
||||
_moreCollectionView.showsVerticalScrollIndicator = NO;
|
||||
_moreCollectionView.backgroundColor = self.backgroundColor;
|
||||
_moreCollectionView.alwaysBounceHorizontal = YES;
|
||||
[self addSubview:_moreCollectionView];
|
||||
|
||||
_lineView = [[UIView alloc] init];
|
||||
_lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#DBDBDB");
|
||||
[self addSubview:_lineView];
|
||||
|
||||
_pageControl = [[UIPageControl alloc] init];
|
||||
_pageControl.currentPageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_current_color", @"#7D7D7D");
|
||||
_pageControl.pageIndicatorTintColor = TUIChatDynamicColor(@"chat_face_page_control_color", @"#DEDEDE");
|
||||
[self addSubview:_pageControl];
|
||||
}
|
||||
|
||||
- (void)defaultLayout {
|
||||
CGSize cellSize = [TUIInputMoreCell getSize];
|
||||
CGFloat collectionHeight = cellSize.height * _rowCount + TMoreView_Margin * (_rowCount - 1);
|
||||
|
||||
_lineView.frame = CGRectMake(0, 0, self.frame.size.width, TLine_Heigh);
|
||||
_moreCollectionView.frame =
|
||||
CGRectMake(0, _lineView.frame.origin.y + _lineView.frame.size.height + TMoreView_Margin, self.frame.size.width, collectionHeight);
|
||||
|
||||
if (_sectionCount > 1) {
|
||||
_pageControl.frame =
|
||||
CGRectMake(0, _moreCollectionView.frame.origin.y + _moreCollectionView.frame.size.height, self.frame.size.width, TMoreView_Page_Height);
|
||||
_pageControl.hidden = NO;
|
||||
} else {
|
||||
_pageControl.hidden = YES;
|
||||
}
|
||||
if (_rowCount > 1) {
|
||||
_moreFlowLayout.minimumInteritemSpacing = (_moreCollectionView.frame.size.height - cellSize.height * _rowCount) / (_rowCount - 1);
|
||||
}
|
||||
|
||||
CGFloat margin = TMoreView_Section_Padding;
|
||||
CGFloat spacing = (_moreCollectionView.frame.size.width - cellSize.width * TMoreView_Column_Count - 2 * margin) / (TMoreView_Column_Count - 1);
|
||||
_moreFlowLayout.minimumLineSpacing = spacing;
|
||||
_moreFlowLayout.sectionInset = UIEdgeInsetsMake(0, margin, 0, margin);
|
||||
|
||||
CGFloat height = _moreCollectionView.frame.origin.y + _moreCollectionView.frame.size.height + TMoreView_Margin;
|
||||
if (_sectionCount > 1) {
|
||||
height = _pageControl.frame.origin.y + _pageControl.frame.size.height;
|
||||
}
|
||||
CGRect frame = self.frame;
|
||||
frame.size.height = height;
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (void)setData:(NSArray *)data {
|
||||
_data = data;
|
||||
|
||||
if (_data.count > TMoreView_Column_Count) {
|
||||
_rowCount = 2;
|
||||
} else {
|
||||
_rowCount = 1;
|
||||
}
|
||||
_itemsInSection = TMoreView_Column_Count * _rowCount;
|
||||
_sectionCount = ceil(_data.count * 1.0 / _itemsInSection);
|
||||
_pageControl.numberOfPages = _sectionCount;
|
||||
|
||||
_itemIndexs = [NSMutableDictionary dictionary];
|
||||
for (NSInteger curSection = 0; curSection < _sectionCount; ++curSection) {
|
||||
for (NSInteger itemIndex = 0; itemIndex < _itemsInSection; ++itemIndex) {
|
||||
// transpose line/row
|
||||
NSInteger row = itemIndex % _rowCount;
|
||||
NSInteger column = itemIndex / _rowCount;
|
||||
NSInteger reIndex = TMoreView_Column_Count * row + column + curSection * _itemsInSection;
|
||||
[_itemIndexs setObject:@(reIndex) forKey:[NSIndexPath indexPathForRow:itemIndex inSection:curSection]];
|
||||
}
|
||||
}
|
||||
|
||||
[_moreCollectionView reloadData];
|
||||
|
||||
[self defaultLayout];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
||||
return _sectionCount;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return _itemsInSection;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIInputMoreCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:TMoreCell_ReuseId forIndexPath:indexPath];
|
||||
TUIInputMoreCellData *data;
|
||||
NSNumber *index = _itemIndexs[indexPath];
|
||||
if (index.integerValue >= _data.count) {
|
||||
data = nil;
|
||||
} else {
|
||||
data = _data[index.integerValue];
|
||||
}
|
||||
[cell fillWithData:data];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
|
||||
|
||||
if (_delegate && [_delegate respondsToSelector:@selector(moreView:didSelectMoreCell:)]) {
|
||||
if ([cell isKindOfClass:[TUIInputMoreCell class]]) {
|
||||
[_delegate moreView:self didSelectMoreCell:(TUIInputMoreCell *)cell];
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView
|
||||
layout:(UICollectionViewLayout *)collectionViewLayout
|
||||
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [TUIInputMoreCell getSize];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
CGFloat contentOffset = scrollView.contentOffset.x;
|
||||
float page = contentOffset / scrollView.frame.size.width;
|
||||
if ((int)(page * 10) % 10 == 0) {
|
||||
_pageControl.currentPage = page;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
42
TUIKit/TUIChat/UI_Classic/Input/TUIRecordView.h
Normal file
42
TUIKit/TUIChat/UI_Classic/Input/TUIRecordView.h
Normal 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
|
||||
135
TUIKit/TUIChat/UI_Classic/Input/TUIRecordView.m
Normal file
135
TUIKit/TUIChat/UI_Classic/Input/TUIRecordView.m
Normal 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
|
||||
26
TUIKit/TUIChat/UI_Classic/Input/TUIReplyPreviewBar.h
Normal file
26
TUIKit/TUIChat/UI_Classic/Input/TUIReplyPreviewBar.h
Normal 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
|
||||
101
TUIKit/TUIChat/UI_Classic/Input/TUIReplyPreviewBar.m
Normal file
101
TUIKit/TUIChat/UI_Classic/Input/TUIReplyPreviewBar.m
Normal 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
|
||||
21
TUIKit/TUIChat/UI_Classic/Input/TUIResponderTextView.h
Normal file
21
TUIKit/TUIChat/UI_Classic/Input/TUIResponderTextView.h
Normal 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
|
||||
66
TUIKit/TUIChat/UI_Classic/Input/TUIResponderTextView.m
Normal file
66
TUIKit/TUIChat/UI_Classic/Input/TUIResponderTextView.m
Normal 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
|
||||
Reference in New Issue
Block a user