TUIInputBar.m 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. //
  2. // TUIInputBar.m
  3. // UIKit
  4. //
  5. // Created by kennethmiao on 2018/9/18.
  6. // Copyright © 2018 Tencent. All rights reserved.
  7. //
  8. #import "TUIInputBar.h"
  9. #import <TIMCommon/TIMDefine.h>
  10. #import <TUICore/TUITool.h>
  11. #import "TUIRecordView.h"
  12. #import <TIMCommon/NSString+TUIEmoji.h>
  13. #import <TIMCommon/NSTimer+TUISafe.h>
  14. #import <TUICore/TUICore.h>
  15. #import <TUICore/TUIDarkModel.h>
  16. #import <TUICore/TUIGlobalization.h>
  17. #import <TUICore/UIView+TUILayout.h>
  18. #import "ReactiveObjC/ReactiveObjC.h"
  19. #import "TUIAudioRecorder.h"
  20. #import "TUIChatConfig.h"
  21. @interface TUIInputBar () <UITextViewDelegate, TUIAudioRecorderDelegate>
  22. @property(nonatomic, strong) TUIRecordView *recordView;
  23. @property(nonatomic, strong) NSDate *recordStartTime;
  24. @property(nonatomic, strong) TUIAudioRecorder *recorder;
  25. @property(nonatomic, assign) BOOL isFocusOn;
  26. @property(nonatomic, strong) NSTimer *sendTypingStatusTimer;
  27. @property(nonatomic, assign) BOOL allowSendTypingStatusByChangeWord;
  28. @end
  29. @implementation TUIInputBar
  30. - (id)initWithFrame:(CGRect)frame {
  31. self = [super initWithFrame:frame];
  32. if (self) {
  33. [self setupViews];
  34. [self defaultLayout];
  35. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThemeChanged) name:TUIDidApplyingThemeChangedNotfication object:nil];
  36. }
  37. return self;
  38. }
  39. - (void)dealloc {
  40. if (_sendTypingStatusTimer) {
  41. [_sendTypingStatusTimer invalidate];
  42. _sendTypingStatusTimer = nil;
  43. }
  44. [[NSNotificationCenter defaultCenter] removeObserver:self];
  45. }
  46. #pragma mark - UI
  47. - (void)setupViews {
  48. self.backgroundColor = TUIChatDynamicColor(@"chat_input_controller_bg_color", @"#EBF0F6");
  49. _lineView = [[UIView alloc] init];
  50. _lineView.backgroundColor = TIMCommonDynamicColor(@"separator_color", @"#FFFFFF");
  51. [self addSubview:_lineView];
  52. _micButton = [[UIButton alloc] init];
  53. [_micButton addTarget:self action:@selector(onMicButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  54. [_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoice_img", @"ToolViewInputVoice") forState:UIControlStateNormal];
  55. [_micButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewInputVoiceHL_img", @"ToolViewInputVoiceHL") forState:UIControlStateHighlighted];
  56. [self addSubview:_micButton];
  57. _faceButton = [[UIButton alloc] init];
  58. [_faceButton addTarget:self action:@selector(onFaceEmojiButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  59. [_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotion_img", @"ToolViewEmotion") forState:UIControlStateNormal];
  60. [_faceButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewEmotionHL_img", @"ToolViewEmotionHL") forState:UIControlStateHighlighted];
  61. [self addSubview:_faceButton];
  62. _keyboardButton = [[UIButton alloc] init];
  63. [_keyboardButton addTarget:self action:@selector(onKeyboardButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  64. [_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboard_img", @"ToolViewKeyboard") forState:UIControlStateNormal];
  65. [_keyboardButton setImage:TUIChatBundleThemeImage(@"chat_ToolViewKeyboardHL_img", @"ToolViewKeyboardHL") forState:UIControlStateHighlighted];
  66. _keyboardButton.hidden = YES;
  67. [self addSubview:_keyboardButton];
  68. _moreButton = [[UIButton alloc] init];
  69. [_moreButton addTarget:self action:@selector(onMoreButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  70. [_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtn_Black_img", @"TypeSelectorBtn_Black") forState:UIControlStateNormal];
  71. [_moreButton setImage:TUIChatBundleThemeImage(@"chat_TypeSelectorBtnHL_Black_img", @"TypeSelectorBtnHL_Black") forState:UIControlStateHighlighted];
  72. [self addSubview:_moreButton];
  73. _recordButton = [[UIButton alloc] init];
  74. [_recordButton.titleLabel setFont:[UIFont systemFontOfSize:15.0f]];
  75. [_recordButton addTarget:self action:@selector(onRecordButtonTouchDown:) forControlEvents:UIControlEventTouchDown];
  76. [_recordButton addTarget:self action:@selector(onRecordButtonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
  77. [_recordButton addTarget:self action:@selector(onRecordButtonTouchCancel:) forControlEvents:UIControlEventTouchUpOutside | UIControlEventTouchCancel];
  78. [_recordButton addTarget:self action:@selector(onRecordButtonTouchDragExit:) forControlEvents:UIControlEventTouchDragExit];
  79. [_recordButton addTarget:self action:@selector(onRecordButtonTouchDragEnter:) forControlEvents:UIControlEventTouchDragEnter];
  80. [_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
  81. [_recordButton setTitleColor:TUIChatDynamicColor(@"chat_input_text_color", @"#000000") forState:UIControlStateNormal];
  82. _recordButton.hidden = YES;
  83. [self addSubview:_recordButton];
  84. _inputTextView = [[TUIResponderTextView alloc] init];
  85. _inputTextView.delegate = self;
  86. [_inputTextView setFont:kTUIInputNoramlFont];
  87. _inputTextView.backgroundColor = TUIChatDynamicColor(@"chat_input_bg_color", @"#FFFFFF");
  88. _inputTextView.textColor = TUIChatDynamicColor(@"chat_input_text_color", @"#000000");
  89. _inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
  90. [_inputTextView setReturnKeyType:UIReturnKeySend];
  91. [self addSubview:_inputTextView];
  92. [self applyBorderTheme];
  93. }
  94. - (void)onThemeChanged {
  95. [self applyBorderTheme];
  96. }
  97. - (void)applyBorderTheme {
  98. if (_recordButton) {
  99. [_recordButton.layer setMasksToBounds:YES];
  100. [_recordButton.layer setCornerRadius:4.0f];
  101. [_recordButton.layer setBorderWidth:1.0f];
  102. [_recordButton.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
  103. }
  104. if (_inputTextView) {
  105. [_inputTextView.layer setMasksToBounds:YES];
  106. [_inputTextView.layer setCornerRadius:4.0f];
  107. [_inputTextView.layer setBorderWidth:0.5f];
  108. [_inputTextView.layer setBorderColor:TIMCommonDynamicColor(@"separator_color", @"#DBDBDB").CGColor];
  109. }
  110. }
  111. - (void)defaultLayout {
  112. [_lineView mas_remakeConstraints:^(MASConstraintMaker *make) {
  113. make.top.mas_equalTo(0);
  114. make.width.mas_equalTo(self);
  115. make.height.mas_equalTo(TLine_Heigh);
  116. }];
  117. CGSize buttonSize = TTextView_Button_Size;
  118. CGFloat buttonOriginY = (TTextView_Height - buttonSize.height) * 0.5;
  119. [_micButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  120. make.leading.mas_equalTo(self.mas_leading);
  121. make.centerY.mas_equalTo(self);
  122. make.size.mas_equalTo(buttonSize);
  123. }];
  124. [_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  125. make.edges.mas_equalTo(_micButton);
  126. }];
  127. [_moreButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  128. make.trailing.mas_equalTo(self.mas_trailing).mas_offset(0);
  129. make.size.mas_equalTo(buttonSize);
  130. make.centerY.mas_equalTo(self);
  131. }];
  132. [_faceButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  133. make.trailing.mas_equalTo(_moreButton.mas_leading).mas_offset(- TTextView_Margin);
  134. make.size.mas_equalTo(buttonSize);
  135. make.centerY.mas_equalTo(self);
  136. }];
  137. [_recordButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  138. make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
  139. make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
  140. make.height.mas_equalTo(TTextView_TextView_Height_Min);
  141. make.centerY.mas_equalTo(self);
  142. }];
  143. [_inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
  144. if (self.isFromReplyPage) {
  145. make.leading.mas_equalTo(self.mas_leading).mas_offset(10);
  146. }
  147. else {
  148. // make.leading.mas_equalTo(_micButton.mas_trailing).mas_offset(10);
  149. make.leading.mas_equalTo(self.mas_leading);
  150. }
  151. make.trailing.mas_equalTo(_faceButton.mas_leading).mas_offset(-10);;
  152. make.height.mas_equalTo(TTextView_TextView_Height_Min);
  153. make.centerY.mas_equalTo(self);
  154. }];
  155. }
  156. - (void)layoutButton:(CGFloat)height {
  157. CGRect frame = self.frame;
  158. CGFloat offset = height - frame.size.height;
  159. frame.size.height = height;
  160. self.frame = frame;
  161. CGSize buttonSize = TTextView_Button_Size;
  162. CGFloat bottomMargin = (TTextView_Height - buttonSize.height) * 0.5;
  163. CGFloat originY = frame.size.height - buttonSize.height - bottomMargin;
  164. CGRect faceFrame = _faceButton.frame;
  165. faceFrame.origin.y = originY;
  166. _faceButton.frame = faceFrame;
  167. CGRect moreFrame = _moreButton.frame;
  168. moreFrame.origin.y = originY;
  169. _moreButton.frame = moreFrame;
  170. CGRect voiceFrame = _micButton.frame;
  171. voiceFrame.origin.y = originY;
  172. _micButton.frame = voiceFrame;
  173. [_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  174. make.edges.mas_equalTo(_faceButton);
  175. }];
  176. if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didChangeInputHeight:)]) {
  177. [_delegate inputBar:self didChangeInputHeight:offset];
  178. }
  179. }
  180. #pragma mark - Event response
  181. - (void)onMicButtonClicked:(UIButton *)sender {
  182. _recordButton.hidden = NO;
  183. _inputTextView.hidden = YES;
  184. _micButton.hidden = YES;
  185. _keyboardButton.hidden = NO;
  186. _faceButton.hidden = NO;
  187. [_inputTextView resignFirstResponder];
  188. [self layoutButton:TTextView_Height];
  189. if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
  190. [_delegate inputBarDidTouchVoice:self];
  191. }
  192. [_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  193. make.edges.mas_equalTo(_micButton);
  194. }];
  195. }
  196. - (void)onKeyboardButtonClicked:(UIButton *)sender {
  197. _micButton.hidden = NO;
  198. _keyboardButton.hidden = YES;
  199. _recordButton.hidden = YES;
  200. _inputTextView.hidden = NO;
  201. _faceButton.hidden = NO;
  202. [self layoutButton:_inputTextView.frame.size.height + 2 * TTextView_Margin];
  203. if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchKeyboard:)]) {
  204. [_delegate inputBarDidTouchKeyboard:self];
  205. }
  206. }
  207. - (void)onFaceEmojiButtonClicked:(UIButton *)sender {
  208. _micButton.hidden = NO;
  209. _faceButton.hidden = YES;
  210. _keyboardButton.hidden = NO;
  211. _recordButton.hidden = YES;
  212. _inputTextView.hidden = NO;
  213. if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchFace:)]) {
  214. [_delegate inputBarDidTouchFace:self];
  215. }
  216. [_keyboardButton mas_remakeConstraints:^(MASConstraintMaker *make) {
  217. make.edges.mas_equalTo(_faceButton);
  218. }];
  219. }
  220. - (void)onMoreButtonClicked:(UIButton *)sender {
  221. if (_delegate && [_delegate respondsToSelector:@selector(inputBarDidTouchMore:)]) {
  222. [_delegate inputBarDidTouchMore:self];
  223. }
  224. }
  225. - (void)onRecordButtonTouchDown:(UIButton *)sender {
  226. [self.recorder record];
  227. }
  228. - (void)onRecordButtonTouchUpInside:(UIButton *)sender {
  229. self.recordButton.backgroundColor = [UIColor clearColor];
  230. [self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
  231. NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:self.recordStartTime];
  232. @weakify(self);
  233. if (interval < 1) {
  234. [self.recordView setStatus:Record_Status_TooShort];
  235. [self.recorder cancel];
  236. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  237. @strongify(self);
  238. [self.recordView removeFromSuperview];
  239. self.recordView = nil;
  240. });
  241. } else if (interval > MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration)) {
  242. [self.recordView setStatus:Record_Status_TooLong];
  243. [self.recorder cancel];
  244. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  245. @strongify(self);
  246. [self.recordView removeFromSuperview];
  247. self.recordView = nil;
  248. });
  249. } else {
  250. /// TUICallKit may need some time to stop all services, so remove UI immediately then stop the recorder.
  251. if (_recordView) {
  252. [self.recordView removeFromSuperview];
  253. self.recordView = nil;
  254. }
  255. dispatch_queue_t main_queue = dispatch_get_main_queue();
  256. dispatch_async(main_queue, ^{
  257. @strongify(self);
  258. dispatch_async(main_queue, ^{
  259. [self.recorder stop];
  260. NSString *path = self.recorder.recordedFilePath;
  261. if (path) {
  262. if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
  263. [self.delegate inputBar:self didSendVoice:path];
  264. }
  265. }
  266. });
  267. });
  268. }
  269. }
  270. - (void)onRecordButtonTouchCancel:(UIButton *)sender {
  271. [self.recordView removeFromSuperview];
  272. self.recordView = nil;
  273. self.recordButton.backgroundColor = [UIColor clearColor];
  274. [self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputHoldToTalk) forState:UIControlStateNormal];
  275. [self.recorder cancel];
  276. }
  277. - (void)onRecordButtonTouchDragExit:(UIButton *)sender {
  278. [self.recordView setStatus:Record_Status_Cancel];
  279. [_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToCancel) forState:UIControlStateNormal];
  280. }
  281. - (void)onRecordButtonTouchDragEnter:(UIButton *)sender {
  282. [self.recordView setStatus:Record_Status_Recording];
  283. [_recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
  284. }
  285. - (void)showHapticFeedback {
  286. if (@available(iOS 10.0, *)) {
  287. dispatch_async(dispatch_get_main_queue(), ^{
  288. UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
  289. [generator prepare];
  290. [generator impactOccurred];
  291. });
  292. } else {
  293. // Fallback on earlier versions
  294. }
  295. }
  296. #pragma mark - Text input
  297. #pragma mark-- UITextViewDelegate
  298. - (void)textViewDidBeginEditing:(UITextView *)textView {
  299. self.keyboardButton.hidden = YES;
  300. self.micButton.hidden = NO;
  301. self.faceButton.hidden = NO;
  302. self.isFocusOn = YES;
  303. self.allowSendTypingStatusByChangeWord = YES;
  304. __weak typeof(self) weakSelf = self;
  305. self.sendTypingStatusTimer = [NSTimer tui_scheduledTimerWithTimeInterval:4
  306. repeats:YES
  307. block:^(NSTimer *_Nonnull timer) {
  308. __strong typeof(weakSelf) strongSelf = weakSelf;
  309. strongSelf.allowSendTypingStatusByChangeWord = YES;
  310. }];
  311. if (self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
  312. if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
  313. [_delegate inputTextViewShouldBeginTyping:textView];
  314. }
  315. }
  316. }
  317. - (void)textViewDidEndEditing:(UITextView *)textView {
  318. self.isFocusOn = NO;
  319. if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
  320. [_delegate inputTextViewShouldEndTyping:textView];
  321. }
  322. }
  323. - (void)textViewDidChange:(UITextView *)textView {
  324. if (self.allowSendTypingStatusByChangeWord && self.isFocusOn && [textView.textStorage tui_getPlainString].length > 0) {
  325. if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldBeginTyping:)]) {
  326. self.allowSendTypingStatusByChangeWord = NO;
  327. [_delegate inputTextViewShouldBeginTyping:textView];
  328. }
  329. }
  330. if (self.isFocusOn && [textView.textStorage tui_getPlainString].length == 0) {
  331. if (_delegate && [_delegate respondsToSelector:@selector(inputTextViewShouldEndTyping:)]) {
  332. [_delegate inputTextViewShouldEndTyping:textView];
  333. }
  334. }
  335. if (self.inputBarTextChanged) {
  336. self.inputBarTextChanged(_inputTextView);
  337. }
  338. CGSize size = [_inputTextView sizeThatFits:CGSizeMake(_inputTextView.frame.size.width, TTextView_TextView_Height_Max)];
  339. CGFloat oldHeight = _inputTextView.frame.size.height;
  340. CGFloat newHeight = size.height;
  341. if (newHeight > TTextView_TextView_Height_Max) {
  342. newHeight = TTextView_TextView_Height_Max;
  343. }
  344. if (newHeight < TTextView_TextView_Height_Min) {
  345. newHeight = TTextView_TextView_Height_Min;
  346. }
  347. if (oldHeight == newHeight) {
  348. return;
  349. }
  350. __weak typeof(self) ws = self;
  351. [UIView animateWithDuration:0.3
  352. animations:^{
  353. [ws.inputTextView mas_remakeConstraints:^(MASConstraintMaker *make) {
  354. // make.leading.mas_equalTo(ws.micButton.mas_trailing).mas_offset(10);
  355. make.leading.mas_equalTo(ws.mas_leading);
  356. make.trailing.mas_equalTo(ws.faceButton.mas_leading).mas_offset(-10);
  357. make.height.mas_equalTo(newHeight);
  358. make.centerY.mas_equalTo(self);
  359. }];
  360. [ws layoutButton:newHeight + 2 * TTextView_Margin];
  361. }];
  362. }
  363. - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
  364. if ([text tui_containsString:@"["] && [text tui_containsString:@"]"]) {
  365. NSRange selectedRange = textView.selectedRange;
  366. if (selectedRange.length > 0) {
  367. [textView.textStorage deleteCharactersInRange:selectedRange];
  368. }
  369. NSMutableAttributedString *textChange = [text getAdvancedFormatEmojiStringWithFont:kTUIInputNoramlFont
  370. textColor:kTUIInputNormalTextColor
  371. emojiLocations:nil];
  372. [textView.textStorage insertAttributedString:textChange atIndex:textView.textStorage.length];
  373. dispatch_async(dispatch_get_main_queue(), ^{
  374. self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
  375. });
  376. return NO;
  377. }
  378. if ([text isEqualToString:@"\n"]) {
  379. if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendText:)]) {
  380. NSString *sp = [[textView.textStorage tui_getPlainString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  381. if (sp.length == 0) {
  382. UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputBlankMessageTitle)
  383. message:nil
  384. preferredStyle:UIAlertControllerStyleAlert];
  385. [ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
  386. [self.mm_viewController presentViewController:ac animated:YES completion:nil];
  387. } else {
  388. [_delegate inputBar:self didSendText:[textView.textStorage tui_getPlainString]];
  389. [self clearInput];
  390. }
  391. }
  392. return NO;
  393. } else if ([text isEqualToString:@""]) {
  394. if (textView.textStorage.length > range.location) {
  395. // Delete the @ message like @xxx at one time
  396. NSAttributedString *lastAttributedStr = [textView.textStorage attributedSubstringFromRange:NSMakeRange(range.location, 1)];
  397. NSString *lastStr = [lastAttributedStr tui_getPlainString];
  398. if (lastStr && lastStr.length > 0 && [lastStr characterAtIndex:0] == ' ') {
  399. NSUInteger location = range.location;
  400. NSUInteger length = range.length;
  401. // corresponds to ascii code
  402. int at = 64;
  403. // (space) ascii
  404. // Space (space) corresponding ascii code
  405. int space = 32;
  406. while (location != 0) {
  407. location--;
  408. length++;
  409. // Convert characters to ascii code, copy to int, avoid out of bounds
  410. int c = (int)[[[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, 1)] tui_getPlainString] characterAtIndex:0];
  411. if (c == at) {
  412. NSString *atText = [[textView.textStorage attributedSubstringFromRange:NSMakeRange(location, length)] tui_getPlainString];
  413. UIFont *textFont = kTUIInputNoramlFont;
  414. NSAttributedString *spaceString = [[NSAttributedString alloc] initWithString:@"" attributes:@{NSFontAttributeName : textFont}];
  415. [textView.textStorage replaceCharactersInRange:NSMakeRange(location, length) withAttributedString:spaceString];
  416. if (self.delegate && [self.delegate respondsToSelector:@selector(inputBar:didDeleteAt:)]) {
  417. [self.delegate inputBar:self didDeleteAt:atText];
  418. }
  419. return NO;
  420. } else if (c == space) {
  421. // Avoid "@nickname Hello, nice to meet you (space) "" Press del after a space to over-delete to @
  422. break;
  423. }
  424. }
  425. }
  426. }
  427. }
  428. // Monitor the input of @ character, including full-width/half-width
  429. else if ([text isEqualToString:@"@"] || [text isEqualToString:@"@"]) {
  430. if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidInputAt:)]) {
  431. [self.delegate inputBarDidInputAt:self];
  432. }
  433. return NO;
  434. }
  435. return YES;
  436. }
  437. - (void)onDeleteBackward:(TUIResponderTextView *)textView {
  438. if (self.delegate && [self.delegate respondsToSelector:@selector(inputBarDidDeleteBackward:)]) {
  439. [self.delegate inputBarDidDeleteBackward:self];
  440. }
  441. }
  442. - (void)clearInput {
  443. [_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(0, _inputTextView.textStorage.length)];
  444. [self textViewDidChange:_inputTextView];
  445. }
  446. - (NSString *)getInput {
  447. return [_inputTextView.textStorage tui_getPlainString];
  448. }
  449. - (void)addEmoji:(TUIFaceCellData *)emoji {
  450. // Create emoji attachment
  451. TUIEmojiTextAttachment *emojiTextAttachment = [[TUIEmojiTextAttachment alloc] init];
  452. emojiTextAttachment.faceCellData = emoji;
  453. NSString *localizableFaceName = emoji.name;
  454. // Set tag and image
  455. emojiTextAttachment.emojiTag = localizableFaceName;
  456. emojiTextAttachment.image = [[TUIImageCache sharedInstance] getFaceFromCache:emoji.path];
  457. // Set emoji size
  458. emojiTextAttachment.emojiSize = kTIMDefaultEmojiSize;
  459. NSAttributedString *str = [NSAttributedString attributedStringWithAttachment:emojiTextAttachment];
  460. NSRange selectedRange = _inputTextView.selectedRange;
  461. if (selectedRange.length > 0) {
  462. [_inputTextView.textStorage deleteCharactersInRange:selectedRange];
  463. }
  464. // Insert emoji image
  465. [_inputTextView.textStorage insertAttributedString:str atIndex:_inputTextView.selectedRange.location];
  466. _inputTextView.selectedRange = NSMakeRange(_inputTextView.selectedRange.location + 1, 0);
  467. [self resetTextStyle];
  468. if (_inputTextView.contentSize.height > TTextView_TextView_Height_Max) {
  469. float offset = _inputTextView.contentSize.height - _inputTextView.frame.size.height;
  470. [_inputTextView scrollRectToVisible:CGRectMake(0, offset, _inputTextView.frame.size.width, _inputTextView.frame.size.height) animated:YES];
  471. }
  472. [self textViewDidChange:_inputTextView];
  473. }
  474. - (void)resetTextStyle {
  475. // After changing text selection, should reset style.
  476. NSRange wholeRange = NSMakeRange(0, _inputTextView.textStorage.length);
  477. [_inputTextView.textStorage removeAttribute:NSFontAttributeName range:wholeRange];
  478. [_inputTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:wholeRange];
  479. [_inputTextView.textStorage addAttribute:NSForegroundColorAttributeName value:kTUIInputNormalTextColor range:wholeRange];
  480. [_inputTextView.textStorage addAttribute:NSFontAttributeName value:kTUIInputNoramlFont range:wholeRange];
  481. [_inputTextView setFont:kTUIInputNoramlFont];
  482. _inputTextView.textAlignment = isRTL()?NSTextAlignmentRight: NSTextAlignmentLeft;
  483. // In iOS 15.0 and later, you need set styles again as belows
  484. _inputTextView.textColor = kTUIInputNormalTextColor;
  485. _inputTextView.font = kTUIInputNoramlFont;
  486. }
  487. - (void)backDelete {
  488. if (_inputTextView.textStorage.length > 0) {
  489. [_inputTextView.textStorage deleteCharactersInRange:NSMakeRange(_inputTextView.textStorage.length - 1, 1)];
  490. [self textViewDidChange:_inputTextView];
  491. }
  492. }
  493. - (void)updateTextViewFrame {
  494. [self textViewDidChange:[UITextView new]];
  495. }
  496. - (void)changeToKeyboard {
  497. [self onKeyboardButtonClicked:self.keyboardButton];
  498. }
  499. - (void)addDraftToInputBar:(NSAttributedString *)draft {
  500. [self addWordsToInputBar:draft];
  501. }
  502. - (void)addWordsToInputBar:(NSAttributedString *)words {
  503. NSRange selectedRange = self.inputTextView.selectedRange;
  504. if (selectedRange.length > 0) {
  505. [self.inputTextView.textStorage deleteCharactersInRange:selectedRange];
  506. }
  507. // Insert draft
  508. [self.inputTextView.textStorage insertAttributedString:words atIndex:self.inputTextView.selectedRange.location];
  509. self.inputTextView.selectedRange = NSMakeRange(self.inputTextView.textStorage.length + 1, 0);
  510. [self resetTextStyle];
  511. [self updateTextViewFrame];
  512. }
  513. #pragma mark - TUIAudioRecorderDelegate
  514. - (void)audioRecorder:(TUIAudioRecorder *)recorder didCheckPermission:(BOOL)isGranted isFirstTime:(BOOL)isFirstTime {
  515. if (isFirstTime) {
  516. if (!isGranted) {
  517. [self showRequestMicAuthorizationAlert];
  518. }
  519. return;
  520. }
  521. [self updateViewsToRecordingStatus];
  522. }
  523. - (void)showRequestMicAuthorizationAlert {
  524. UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicTitle)
  525. message:TIMCommonLocalizableString(TUIKitInputNoMicTips)
  526. preferredStyle:UIAlertControllerStyleAlert];
  527. [ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateLater) style:UIAlertActionStyleCancel handler:nil]];
  528. [ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(TUIKitInputNoMicOperateEnable)
  529. style:UIAlertActionStyleDefault
  530. handler:^(UIAlertAction *_Nonnull action) {
  531. UIApplication *app = [UIApplication sharedApplication];
  532. NSURL *settingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
  533. if ([app canOpenURL:settingsURL]) {
  534. [app openURL:settingsURL];
  535. }
  536. }]];
  537. dispatch_async(dispatch_get_main_queue(), ^{
  538. [self.mm_viewController presentViewController:ac animated:YES completion:nil];
  539. });
  540. }
  541. - (void)updateViewsToRecordingStatus {
  542. [self.window addSubview:self.recordView];
  543. [self.recordView mas_makeConstraints:^(MASConstraintMaker *make) {
  544. make.center.mas_equalTo(self.window);
  545. make.width.height.mas_equalTo(self.window);
  546. }];
  547. self.recordStartTime = [NSDate date];
  548. [self.recordView setStatus:Record_Status_Recording];
  549. self.recordButton.backgroundColor = [UIColor lightGrayColor];
  550. [self.recordButton setTitle:TIMCommonLocalizableString(TUIKitInputReleaseToSend) forState:UIControlStateNormal];
  551. [self showHapticFeedback];
  552. }
  553. - (void)audioRecorder:(TUIAudioRecorder *)recorder didPowerChanged:(float)power {
  554. if (!self.recordView.hidden) {
  555. [self.recordView setPower:power];
  556. }
  557. }
  558. - (void)audioRecorder:(TUIAudioRecorder *)recorder didRecordTimeChanged:(NSTimeInterval)time {
  559. float uiMaxDuration = MIN(59, [TUIChatConfig defaultConfig].maxAudioRecordDuration);
  560. float realMaxDuration = uiMaxDuration + 0.7;
  561. NSInteger seconds = uiMaxDuration - time;
  562. self.recordView.timeLabel.text = [[NSString alloc] initWithFormat:@"%ld\"", (long)seconds + 1];
  563. if (time >= (uiMaxDuration - 4) && time <= uiMaxDuration) {
  564. NSInteger seconds = uiMaxDuration - time;
  565. /**
  566. * The long type is cast here to eliminate compiler warnings.
  567. * Here +1 is to round up and optimize the time logic.
  568. */
  569. self.recordView.title.text = [NSString stringWithFormat:TIMCommonLocalizableString(TUIKitInputWillFinishRecordInSeconds), (long)seconds + 1];
  570. } else if (time > realMaxDuration) {
  571. [self.recorder stop];
  572. NSString *path = self.recorder.recordedFilePath;
  573. [self.recordView setStatus:Record_Status_TooLong];
  574. @weakify(self);
  575. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  576. @strongify(self);
  577. [self.recordView removeFromSuperview];
  578. self.recordView = nil;
  579. });
  580. if (path) {
  581. if (_delegate && [_delegate respondsToSelector:@selector(inputBar:didSendVoice:)]) {
  582. [_delegate inputBar:self didSendVoice:path];
  583. }
  584. }
  585. }
  586. }
  587. #pragma mark - Getter
  588. - (TUIAudioRecorder *)recorder {
  589. if (!_recorder) {
  590. _recorder = [[TUIAudioRecorder alloc] init];
  591. _recorder.delegate = self;
  592. }
  593. return _recorder;
  594. }
  595. - (TUIRecordView *)recordView {
  596. if (!_recordView) {
  597. _recordView = [[TUIRecordView alloc] init];
  598. _recordView.frame = self.frame;
  599. }
  600. return _recordView;
  601. }
  602. @end