ChatBar.m 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "ChatBar.h"
  21. #import "ChatDefines.h"
  22. #import "UserSettings.h"
  23. #import "ServerConnector.h"
  24. #import "BundleUtil.h"
  25. #import "UIImage+ColoredImage.h"
  26. #import "RectUtil.h"
  27. #import "Contact.h"
  28. #import "QuoteView.h"
  29. #import <MobileCoreServices/MobileCoreServices.h>
  30. #import "AppDelegate.h"
  31. #import "Threema-Swift.h"
  32. #ifdef DEBUG
  33. static const DDLogLevel ddLogLevel = DDLogLevelInfo;
  34. #else
  35. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  36. #endif
  37. @interface ChatBar () <OEMentionsHelperDelegate, HPGrowingTextViewDelegate>
  38. @end
  39. @implementation ChatBar {
  40. BOOL typing;
  41. dispatch_source_t typing_timer;
  42. UITapGestureRecognizer *chatBarTapRecognizer;
  43. UIPanGestureRecognizer *chatBarPanRecognizer;
  44. NSInteger takePhotoIndex;
  45. NSInteger chooseExistingIndex;
  46. CGFloat minChatBarHeight;
  47. float fontSize;
  48. NSString *sendButtonText;
  49. BOOL microphoneShowing;
  50. CGSize sendButtonTextSize;
  51. CGFloat additionalWidth;
  52. UIView *hairlineView;
  53. UIImage *microphoneImage;
  54. UIView *chatInputBackgroundView;
  55. QuoteView *quoteView;
  56. CGFloat chatBarHeight;
  57. OEMentionsHelper *oementionsHelper;
  58. UIView *addMentionView;
  59. dispatch_queue_t typingQueue;
  60. BOOL updateTextColorForEmptyString;
  61. }
  62. #define kChatInputPadding 6.0
  63. #define kTypingTimeout 10
  64. #define kQuoteViewSpacing 6.0
  65. @synthesize chatInput;
  66. @synthesize sendButton;
  67. @synthesize addButton;
  68. @synthesize delegate;
  69. @synthesize canSendAudio;
  70. - (id)initWithFrame:(CGRect)frame
  71. {
  72. self = [super initWithFrame:frame];
  73. if (self) {
  74. fontSize = [UserSettings sharedUserSettings].chatFontSize;
  75. minChatBarHeight = frame.size.height;
  76. chatBarHeight = minChatBarHeight;
  77. self.userInteractionEnabled = YES;
  78. self.clearsContextBeforeDrawing = NO;
  79. self.clipsToBounds = YES;
  80. self.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
  81. chatBarTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(chatBarTapped)];
  82. chatBarTapRecognizer.delegate = self;
  83. [self addGestureRecognizer:chatBarTapRecognizer];
  84. chatBarPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(chatBarPanned:)];
  85. chatBarPanRecognizer.delegate = self;
  86. [self addGestureRecognizer:chatBarPanRecognizer];
  87. sendButtonText = NSLocalizedString(@"send", nil);
  88. UIFont *sendButtonFont = [UIFont boldSystemFontOfSize:16.0f];
  89. sendButtonTextSize = [sendButtonText sizeWithAttributes:@{NSFontAttributeName : sendButtonFont}];
  90. additionalWidth = 18.0f;
  91. CGRect chatInputBackgroundRect = CGRectMake(42, kChatInputPadding, 244 - sendButtonTextSize.width + additionalWidth, self.frame.size.height - (2*kChatInputPadding));
  92. chatInputBackgroundView = [[UIView alloc] initWithFrame:chatInputBackgroundRect];
  93. chatInputBackgroundView.clipsToBounds = YES;
  94. [self addSubview:chatInputBackgroundView];
  95. // Create chatInput.
  96. CGRect chatInputRect = CGRectMake(0.0, 0.0, chatInputBackgroundRect.size.width, 40.0);
  97. chatInput = [[HPGrowingTextView alloc] initWithFrame:chatInputRect];
  98. chatInput.isScrollable = NO;
  99. chatInput.contentInset = UIEdgeInsetsMake(0, 3, 0, 3);
  100. chatInput.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(5, 0, 5, 0);
  101. chatInput.delegate = self;
  102. chatInput.minNumberOfLines = 1;
  103. chatInput.maxNumberOfLines = 6;
  104. chatInput.font = [UIFont systemFontOfSize:fontSize];
  105. chatInput.dataDetectorTypes = UIDataDetectorTypeAll;
  106. chatInput.internalTextView.scrollsToTop = NO;
  107. ((CustomResponderTextView*)chatInput.internalTextView).pasteImageHandler = self;
  108. [self updateMaxNumberOfLines];
  109. [chatInputBackgroundView addSubview:chatInput];
  110. // center chatInput again since it might have adapted its height after initialising
  111. chatInput.frame = [RectUtil rect:chatInput.frame centerIn:chatInputBackgroundView.bounds round:YES];
  112. // Create addButton.
  113. CGFloat addButtonYOffset = 1;
  114. addButton = [UIButton buttonWithType:UIButtonTypeCustom];
  115. addButton.clearsContextBeforeDrawing = NO;
  116. addButton.frame = CGRectMake(0, 0, 40, 40);
  117. addButton.autoresizingMask = UIViewAutoresizingFlexibleTopMargin;
  118. addButton.imageEdgeInsets = UIEdgeInsetsMake(addButtonYOffset, 2, 0, 0);
  119. addButton.accessibilityLabel = NSLocalizedString(@"send_media_or_location", nil);
  120. addButton.accessibilityIdentifier = @"PlusButton";
  121. [addButton addTarget:self action:@selector(addAction:) forControlEvents:UIControlEventTouchUpInside];
  122. [self addSubview:addButton];
  123. // Create sendButton.
  124. CGFloat sendButtonOffset = 8.0f;
  125. sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
  126. sendButton.clearsContextBeforeDrawing = NO;
  127. sendButton.frame = CGRectMake(self.frame.size.width - 24.0f - sendButtonTextSize.width + sendButtonOffset, 7.0f, sendButtonTextSize.width + 16.0f, 27.0f);
  128. sendButton.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
  129. sendButton.titleLabel.font = sendButtonFont;
  130. [sendButton setTitle:sendButtonText forState:UIControlStateNormal];
  131. [sendButton addTarget:self action:@selector(sendItemAction:) forControlEvents:UIControlEventTouchUpInside];
  132. [self checkEnableSendButton]; // disable initially
  133. [self addSubview:sendButton];
  134. // Set typing timer
  135. typingQueue = dispatch_queue_create("ch.threema.typingQueue", 0);
  136. typing_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, typingQueue);
  137. dispatch_source_set_event_handler(typing_timer, ^{
  138. [self stopTyping];
  139. });
  140. CGRect hairlineRect = CGRectMake(0.0, 0.0, CGRectGetWidth(self.bounds), 0.5);
  141. hairlineView = [[UIView alloc] initWithFrame:hairlineRect];
  142. hairlineView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  143. [self addSubview:hairlineView];
  144. quoteView = [[QuoteView alloc] init];
  145. quoteView.hidden = YES;
  146. quoteView.delegate = self;
  147. quoteView.buttonWidthHint = sendButtonTextSize.width + additionalWidth;
  148. [self addSubview:quoteView];
  149. addMentionView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, self.frame.size.height)];
  150. addMentionView.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.7];
  151. addMentionView.hidden = true;
  152. [self addSubview:addMentionView];
  153. [self setupColors];
  154. // Listen for connection status changes so we can enable/disable the send button
  155. [[ServerConnector sharedServerConnector] addObserver:self forKeyPath:@"connectionState" options:0 context:nil];
  156. updateTextColorForEmptyString = false;
  157. }
  158. return self;
  159. }
  160. - (void)setupColors {
  161. [Colors updateKeyboardAppearanceFor:(id<UITextInputTraits>)chatInput];
  162. self.backgroundColor = [Colors chatBarBackground];
  163. chatInput.backgroundColor = [UIColor clearColor];
  164. chatInputBackgroundView.backgroundColor = [Colors chatBarInput];
  165. chatInputBackgroundView.layer.borderWidth = 0.5;
  166. chatInputBackgroundView.layer.cornerRadius = 4.0;
  167. chatInputBackgroundView.layer.borderColor = [Colors chatBarBorder].CGColor;
  168. hairlineView.backgroundColor = [Colors chatBarBorder];
  169. [addButton setImage:[UIImage imageNamed:@"Plus"inColor:[Colors main]] forState:UIControlStateNormal];
  170. [addButton setImage:[UIImage imageNamed:@"Plus"inColor:[Colors fontNormal]] forState:UIControlStateHighlighted];
  171. [sendButton setTitleColor:[Colors main] forState:UIControlStateNormal];
  172. [sendButton setTitleColor:[Colors fontLight] forState:UIControlStateDisabled];
  173. [sendButton setTitleColor:[Colors fontNormal] forState:UIControlStateHighlighted];
  174. microphoneImage = [UIImage imageNamed:@"Microphone" inColor:[Colors main]];
  175. if (microphoneShowing) {
  176. [self.sendButton setImage:microphoneImage forState:UIControlStateNormal];
  177. }
  178. [quoteView setupColors];
  179. [oementionsHelper setupColors];
  180. }
  181. - (void)setupMentions:(NSArray *)sortedMembers {
  182. if (oementionsHelper != nil) {
  183. [oementionsHelper updateOeObjectsWithSortedContacts:sortedMembers];
  184. } else {
  185. if (sortedMembers.count > 0) {
  186. UIView *mainView = [delegate chatContainterView];
  187. oementionsHelper = [[OEMentionsHelper alloc] initWithContainerView:self chatInputView:chatInput mainView:mainView sortedContacts:sortedMembers];
  188. addMentionView.frame = CGRectMake(0.0, mainView.frame.origin.y, mainView.frame.size.width, mainView.frame.size.height - self.frame.origin.y);
  189. oementionsHelper.delegate = self;
  190. }
  191. }
  192. }
  193. - (void)updateMentionsFromDraft:(NSString *)draft {
  194. if (oementionsHelper != nil) {
  195. [oementionsHelper addMentionsWithDraft:draft];
  196. } else {
  197. chatInput.text = draft;
  198. }
  199. }
  200. - (NSString *)formattedMentionText {
  201. if (oementionsHelper != nil) {
  202. return [oementionsHelper formattedMentionText];
  203. }
  204. return chatInput.text;
  205. }
  206. - (void)refresh {
  207. [self setupColors];
  208. }
  209. - (void)dealloc {
  210. if (typing_timer != nil) {
  211. dispatch_source_cancel(typing_timer);
  212. typing_timer = nil;
  213. }
  214. [[ServerConnector sharedServerConnector] removeObserver:self forKeyPath:@"connectionState"];
  215. }
  216. - (void)layoutSubviews {
  217. [super layoutSubviews];
  218. CGFloat chatInputLeftPadding = 42.0f;
  219. CGFloat leftPadding = 0.0f;
  220. CGFloat rightPadding = 0.0f;
  221. if ([AppDelegate hasBottomSafeAreaInsets]) {
  222. UIInterfaceOrientation orientation = [self.delegate interfaceOrientationForChatBar:self];
  223. if (orientation == UIInterfaceOrientationLandscapeRight) {
  224. leftPadding += kIphoneXChatBarLandscapePadding;
  225. } else if (orientation == UIInterfaceOrientationLandscapeLeft) {
  226. rightPadding += kIphoneXChatBarLandscapePadding;
  227. }
  228. addButton.frame = CGRectMake(leftPadding, addButton.frame.origin.y, addButton.frame.size.width, addButton.frame.size.height);
  229. sendButton.frame = CGRectMake(self.frame.size.width - sendButton.frame.size.width - rightPadding, sendButton.frame.origin.y, sendButton.frame.size.width, sendButton.frame.size.height);
  230. }
  231. CGFloat chatInputXOffset = chatInputLeftPadding + leftPadding;
  232. CGFloat chatInputYOffset = 0.0f;
  233. if (quoteView.hidden == NO) {
  234. // Reposition quote view
  235. CGSize quoteViewAvailableSize = CGSizeMake(self.frame.size.width - chatInputLeftPadding - rightPadding, CGFLOAT_MAX);
  236. CGSize quoteViewPreferredSize = [quoteView sizeThatFits:quoteViewAvailableSize];
  237. quoteView.frame = CGRectMake(chatInputLeftPadding, kQuoteViewSpacing, quoteViewAvailableSize.width, quoteViewPreferredSize.height);
  238. chatInputYOffset = quoteView.frame.size.height + kQuoteViewSpacing;
  239. }
  240. chatInputBackgroundView.frame = CGRectMake(chatInputLeftPadding + leftPadding, kChatInputPadding + chatInputYOffset, self.frame.size.width - chatInputXOffset - sendButtonTextSize.width - additionalWidth - rightPadding, self.frame.size.height - 2*kChatInputPadding - chatInputYOffset);
  241. chatInput.frame = CGRectMake(0, (chatInputBackgroundView.frame.size.height - chatInput.frame.size.height)/2, chatInputBackgroundView.frame.size.width, chatInput.frame.size.height);
  242. [oementionsHelper updateTextColor];
  243. }
  244. - (void)resizeChatInput {
  245. [self updateMaxNumberOfLines];
  246. [chatInput refreshHeightForce:YES];
  247. }
  248. - (void)updateMaxNumberOfLines {
  249. if (UIInterfaceOrientationIsLandscape([self.delegate interfaceOrientationForChatBar:self])) {
  250. if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) >= 667) {
  251. /* iPhone 6 (Plus) */
  252. if (fontSize >= 28.0)
  253. chatInput.maxNumberOfLines = 3;
  254. else
  255. chatInput.maxNumberOfLines = 4;
  256. } else {
  257. if (fontSize >= 28.0)
  258. chatInput.maxNumberOfLines = 2;
  259. else
  260. chatInput.maxNumberOfLines = 3;
  261. }
  262. } else {
  263. if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) >= 667) {
  264. /* iPhone 6 (Plus) */
  265. if (fontSize >= 30.0)
  266. chatInput.maxNumberOfLines = 7;
  267. else
  268. chatInput.maxNumberOfLines = 8;
  269. }
  270. else if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 480) {
  271. /* iPhone 4s */
  272. if (fontSize >= 30.0)
  273. chatInput.maxNumberOfLines = 3;
  274. else
  275. chatInput.maxNumberOfLines = 4;
  276. }
  277. else {
  278. if (fontSize >= 30.0)
  279. chatInput.maxNumberOfLines = 5;
  280. else
  281. chatInput.maxNumberOfLines = 6;
  282. }
  283. }
  284. }
  285. - (void)updateHeight {
  286. CGFloat height = chatBarHeight;
  287. if (quoteView.hidden == NO) {
  288. height += quoteView.frame.size.height + kQuoteViewSpacing;
  289. }
  290. if ([self.delegate respondsToSelector:@selector(chatBar:didChangeHeight:)]) {
  291. [self.delegate chatBar:self didChangeHeight:height];
  292. }
  293. }
  294. - (void)resetKeyboardType:(BOOL)resetType {
  295. if (resetType)
  296. chatInput.internalTextView.keyboardType = UIKeyboardTypeDefault;
  297. if (chatInput.isFirstResponder) {
  298. dispatch_async(dispatch_get_main_queue(), ^{
  299. self.resettingKeyboard = YES;
  300. if ([self.delegate respondsToSelector:@selector(canBecomeFirstResponder)]) {
  301. if (delegate.canBecomeFirstResponder) {
  302. [UIView performWithoutAnimation: ^{
  303. [chatInput resignFirstResponder];
  304. [chatInput becomeFirstResponder];
  305. }];
  306. }
  307. } else {
  308. [UIView performWithoutAnimation: ^{
  309. [chatInput resignFirstResponder];
  310. [chatInput becomeFirstResponder];
  311. }];
  312. }
  313. self.resettingKeyboard = NO;
  314. });
  315. }
  316. }
  317. - (void)sendItemAction: (id) sender {
  318. // Ensure last auto correction is applied
  319. [chatInput.internalTextView.inputDelegate selectionWillChange:chatInput.internalTextView];
  320. [chatInput.internalTextView.inputDelegate selectionDidChange:chatInput.internalTextView];
  321. // switch back to default keyboard (in case we're currently using the numeric or emoji keypad)
  322. [self resetKeyboardType:YES];
  323. [self sendText];
  324. }
  325. - (void)sendText {
  326. if (oementionsHelper != nil) {
  327. NSString *formattedMentionText = [oementionsHelper formattedMentionText];
  328. [oementionsHelper resetMentionsIndexes];
  329. chatInput.text = formattedMentionText;
  330. }
  331. if ([self.delegate respondsToSelector:@selector(chatBar:didSendText:)]) {
  332. NSString *textToSend = chatInput.text;
  333. if (textToSend.length > 0 && quoteView.hidden == NO) {
  334. textToSend = [quoteView makeQuoteWithReply:textToSend];
  335. }
  336. [self.delegate chatBar: self didSendText: textToSend];
  337. }
  338. }
  339. - (void)addAction:(id)sender {
  340. [self.delegate chatBarDidPushAddButton:self];
  341. }
  342. - (NSString*)text {
  343. return chatInput.text;
  344. }
  345. - (void)setText:(NSString *)text {
  346. chatInput.text = text;
  347. [self resizeChatInput];
  348. }
  349. - (void)setCanSendAudio:(BOOL)newCanSendAudio {
  350. canSendAudio = newCanSendAudio;
  351. [self checkEnableSendButton];
  352. }
  353. - (void)clearChatInput {
  354. @try {
  355. chatInput.text = @"";
  356. }
  357. @catch (NSException *exception) {
  358. /* Setting the text may trigger an exception ("Range or index out of bounds") if
  359. dictation is currently in process and the spinner is showing. It seems to be
  360. an Apple bug, so we simply catch it to avoid crashing */
  361. DDLogWarn(@"Exception: %@", exception);
  362. }
  363. // iOS 10 workaround: set font again to fix wonky text field after entering certain emojis
  364. // (e.g. policewoman, soccer ball and many more), which would cause incorrect height
  365. // calculations and excessive word spacing.
  366. chatInput.font = [UIFont systemFontOfSize:fontSize];
  367. [self quoteCancelled];
  368. [self resizeChatInput];
  369. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  370. [self stopTyping];
  371. }
  372. }
  373. - (void)disableSendButton {
  374. if (!sendButton.enabled)
  375. return;
  376. sendButton.enabled = NO;
  377. }
  378. - (void)enableSendButton {
  379. if (sendButton.enabled)
  380. return;
  381. sendButton.enabled = YES;
  382. }
  383. - (void)chatBarTapped {
  384. if ([self.delegate respondsToSelector:@selector(canBecomeFirstResponder)]) {
  385. if (delegate.canBecomeFirstResponder) {
  386. [chatInput becomeFirstResponder];
  387. }
  388. } else {
  389. [chatInput becomeFirstResponder];
  390. }
  391. if ([self.delegate respondsToSelector:@selector(chatBarTapped:)]) {
  392. [delegate chatBarTapped:self];
  393. }
  394. }
  395. - (void)chatBarPanned:(UIPanGestureRecognizer *)gesture {
  396. CGPoint translation = [gesture translationInView:self];
  397. if (translation.y <= -10) {
  398. if ([self.delegate respondsToSelector:@selector(canBecomeFirstResponder)]) {
  399. if (delegate.canBecomeFirstResponder) {
  400. [chatInput becomeFirstResponder];
  401. }
  402. } else {
  403. [chatInput becomeFirstResponder];
  404. }
  405. }
  406. }
  407. - (BOOL)becomeFirstResponder {
  408. if ([self.delegate respondsToSelector:@selector(canBecomeFirstResponder)]) {
  409. if (delegate.canBecomeFirstResponder) {
  410. return [chatInput becomeFirstResponder];
  411. } else {
  412. return NO;
  413. }
  414. } else {
  415. return [chatInput becomeFirstResponder];
  416. }
  417. }
  418. - (BOOL)resignFirstResponder {
  419. DDLogVerbose(@"ChatBar resignFirstResponder");
  420. BOOL res = [chatInput resignFirstResponder];
  421. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  422. [self stopTyping];
  423. }
  424. return res;
  425. }
  426. - (BOOL)isFirstResponder {
  427. return [chatInput isFirstResponder];
  428. }
  429. - (void)checkEnableSendButton {
  430. /* replace send button with microphone if there is no text */
  431. if (chatInput.text.length > 0) {
  432. if (microphoneShowing) {
  433. [self.sendButton setTitle:sendButtonText forState:UIControlStateNormal];
  434. [self.sendButton setImage:nil forState:UIControlStateNormal];
  435. self.sendButton.accessibilityLabel = [BundleUtil localizedStringForKey:@"send"];
  436. microphoneShowing = NO;
  437. }
  438. } else {
  439. if (self.canSendAudio) {
  440. if (!microphoneShowing) {
  441. [self.sendButton setTitle:nil forState:UIControlStateNormal];
  442. [self.sendButton setImage:microphoneImage forState:UIControlStateNormal];
  443. self.sendButton.accessibilityLabel = [BundleUtil localizedStringForKey:@"voice message"];
  444. microphoneShowing = YES;
  445. }
  446. }
  447. }
  448. /* only enable send button if there is some text and we're currently connected */
  449. NSString *trimmedText = [chatInput.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  450. if (trimmedText.length > 0) {
  451. if ([ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn) {
  452. [self enableSendButton];
  453. } else {
  454. [self disableSendButton];
  455. }
  456. } else {
  457. if (self.canSendAudio) {
  458. [self enableSendButton];
  459. } else {
  460. [self disableSendButton];
  461. }
  462. }
  463. }
  464. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  465. if (object == [ServerConnector sharedServerConnector] && [keyPath isEqualToString:@"connectionState"]) {
  466. dispatch_async(dispatch_get_main_queue(), ^{
  467. [self checkEnableSendButton];
  468. });
  469. }
  470. }
  471. #pragma mark OEMentionsHelperDelegate
  472. - (void)textView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
  473. [self growingTextView:growingTextView willChangeHeight:height];
  474. }
  475. - (BOOL)textView:(HPGrowingTextView *)growingTextView shouldChangeTextIn:(NSRange)range replacementText:(NSString *)text {
  476. return [self growingTextView:growingTextView shouldChangeTextInRange:range replacementText:text];
  477. }
  478. - (void)textViewDidChange:(HPGrowingTextView *)growingTextView {
  479. [self growingTextViewDidChange:growingTextView];
  480. if (growingTextView.text.length == 0 && updateTextColorForEmptyString == false) {
  481. updateTextColorForEmptyString = true;
  482. growingTextView.text = @" ";
  483. [oementionsHelper updateTextColor];
  484. growingTextView.text = @"";
  485. updateTextColorForEmptyString = false;
  486. }
  487. }
  488. - (void)mentionSelectedWithId:(NSInteger)id name:(NSString *)name {
  489. }
  490. #pragma mark HPGrowingTextViewDelegate
  491. - (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height {
  492. chatBarHeight = height + 5.0f;
  493. if (chatBarHeight < minChatBarHeight) {
  494. chatBarHeight = minChatBarHeight;
  495. }
  496. if (height < growingTextView.frame.size.height) {
  497. // workaround for the weird HPGrowingTextView on iOS8
  498. // force height update after resize animation to prevent contentOffset issue
  499. // when more than maxLines were entered and input field is shrinked again
  500. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  501. [chatInput setNeedsLayout];
  502. [chatInput refreshHeight];
  503. });
  504. }
  505. [self updateHeight];
  506. }
  507. - (BOOL)growingTextView:(HPGrowingTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
  508. if ([text hasPrefix:@"tel:"]) {
  509. NSString *newText = [text stringByRemovingPercentEncoding];
  510. newText = [newText substringFromIndex:4];
  511. self.text = [self.text stringByReplacingCharactersInRange:range withString:newText];
  512. return NO;
  513. }
  514. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  515. dispatch_source_set_timer(typing_timer, dispatch_time(DISPATCH_TIME_NOW, kTypingTimeout * NSEC_PER_SEC),
  516. kTypingTimeout * NSEC_PER_SEC, NSEC_PER_SEC);
  517. if (!typing) {
  518. typing = YES;
  519. [delegate chatBarWillStartTyping:self];
  520. dispatch_resume(typing_timer);
  521. }
  522. }
  523. if ([text isEqualToString:@"\n"] && [UserSettings sharedUserSettings].returnToSend) {
  524. if ([ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn) {
  525. dispatch_async(dispatch_get_main_queue(), ^{
  526. [self sendText];
  527. });
  528. }
  529. return NO;
  530. }
  531. return YES;
  532. }
  533. - (void)growingTextViewDidChange:(HPGrowingTextView *)growingTextView {
  534. [self checkEnableSendButton];
  535. }
  536. - (void)stopTyping {
  537. if ([UserSettings sharedUserSettings].sendTypingIndicator == true) {
  538. DDLogVerbose(@"stopTyping");
  539. if (typing) {
  540. typing = NO;
  541. dispatch_async(dispatch_get_main_queue(), ^{
  542. [delegate chatBarDidStopTyping:self];
  543. });
  544. dispatch_suspend(typing_timer);
  545. }
  546. }
  547. }
  548. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
  549. if (gestureRecognizer == chatBarTapRecognizer && [touch.view isKindOfClass:[UIControl class]]) {
  550. // we touched a button, slider, or other UIControl
  551. return NO; // ignore the touch
  552. }
  553. return YES; // handle the touch
  554. }
  555. #pragma mark - Paste image handler
  556. - (void)handlePasteImage {
  557. if ([UIPasteboard generalPasteboard].image == nil)
  558. return;
  559. // Check if there is also an (animated) GIF on the pasteboard; if so, send it as a file to preserve the animation
  560. NSData *gifData = [[UIPasteboard generalPasteboard] dataForPasteboardType:(__bridge NSString *)kUTTypeGIF];
  561. if (gifData != nil) {
  562. [delegate chatBar:self didSendGIF:gifData fallbackImage:[UIPasteboard generalPasteboard].image];
  563. } else {
  564. NSData *imageData = [[UIPasteboard generalPasteboard] dataForPasteboardType:(__bridge NSString *)kUTTypeImage];
  565. if (imageData != nil) {
  566. [delegate chatBar:self didSendImageData:imageData];
  567. } else {
  568. [delegate chatBar:self didSendImageData:UIImageJPEGRepresentation([UIPasteboard generalPasteboard].image, 1.0)];
  569. }
  570. }
  571. }
  572. #pragma mark - Quoting
  573. - (void)addQuotedMessage:(BaseMessage *)message {
  574. [quoteView setQuotedMessage:message];
  575. [self showQuotedMessage];
  576. }
  577. - (void)addQuotedText:(NSString*)quotedText quotedContact:(Contact*)contact {
  578. [quoteView setQuotedText:quotedText quotedContact:contact];
  579. [self showQuotedMessage];
  580. }
  581. - (void)showQuotedMessage {
  582. CGSize quoteViewAvailableSize = CGSizeMake(self.frame.size.width - 42, CGFLOAT_MAX);
  583. CGSize quoteViewPreferredSize = [quoteView sizeThatFits:quoteViewAvailableSize];
  584. quoteView.frame = CGRectMake(42, kQuoteViewSpacing, quoteViewAvailableSize.width, quoteViewPreferredSize.height);
  585. [UIView beginAnimations:nil context:NULL];
  586. [UIView setAnimationDuration:0.5];
  587. quoteView.hidden = NO;
  588. [self updateHeight];
  589. [UIView commitAnimations];
  590. if ([self.delegate respondsToSelector:@selector(chatBarDidAddQuote)]) {
  591. [self.delegate chatBarDidAddQuote];
  592. }
  593. if ([self.delegate respondsToSelector:@selector(canBecomeFirstResponder)]) {
  594. if (delegate.canBecomeFirstResponder) {
  595. [chatInput becomeFirstResponder];
  596. }
  597. } else {
  598. [chatInput becomeFirstResponder];
  599. }
  600. }
  601. - (void)quoteCancelled {
  602. if (quoteView.hidden)
  603. return;
  604. quoteView.hidden = YES;
  605. [self updateHeight];
  606. }
  607. @end