PageRenderTime 28ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/Pods/JSQMessagesViewController/JSQMessagesViewController/Controllers/JSQMessagesViewController.m

https://gitlab.com/joaopaulogalvao/browteco
Objective C | 1061 lines | 784 code | 239 blank | 38 comment | 109 complexity | 28c77cd2ac26f4090a43c600217dd3c9 MD5 | raw file
  1. //
  2. // Created by Jesse Squires
  3. // http://www.jessesquires.com
  4. //
  5. //
  6. // Documentation
  7. // http://cocoadocs.org/docsets/JSQMessagesViewController
  8. //
  9. //
  10. // GitHub
  11. // https://github.com/jessesquires/JSQMessagesViewController
  12. //
  13. //
  14. // License
  15. // Copyright (c) 2014 Jesse Squires
  16. // Released under an MIT license: http://opensource.org/licenses/MIT
  17. //
  18. #import "JSQMessagesViewController.h"
  19. #import "JSQMessagesCollectionViewFlowLayoutInvalidationContext.h"
  20. #import "JSQMessageData.h"
  21. #import "JSQMessageBubbleImageDataSource.h"
  22. #import "JSQMessageAvatarImageDataSource.h"
  23. #import "JSQMessagesCollectionViewCellIncoming.h"
  24. #import "JSQMessagesCollectionViewCellOutgoing.h"
  25. #import "JSQMessagesTypingIndicatorFooterView.h"
  26. #import "JSQMessagesLoadEarlierHeaderView.h"
  27. #import "JSQMessagesToolbarContentView.h"
  28. #import "JSQMessagesInputToolbar.h"
  29. #import "JSQMessagesComposerTextView.h"
  30. #import "JSQMessagesTimestampFormatter.h"
  31. #import "NSString+JSQMessages.h"
  32. #import "UIColor+JSQMessages.h"
  33. #import "UIDevice+JSQMessages.h"
  34. #import "NSBundle+JSQMessages.h"
  35. static void * kJSQMessagesKeyValueObservingContext = &kJSQMessagesKeyValueObservingContext;
  36. @interface JSQMessagesViewController () <JSQMessagesInputToolbarDelegate,
  37. JSQMessagesKeyboardControllerDelegate>
  38. @property (weak, nonatomic) IBOutlet JSQMessagesCollectionView *collectionView;
  39. @property (weak, nonatomic) IBOutlet JSQMessagesInputToolbar *inputToolbar;
  40. @property (weak, nonatomic) IBOutlet NSLayoutConstraint *toolbarHeightConstraint;
  41. @property (weak, nonatomic) IBOutlet NSLayoutConstraint *toolbarBottomLayoutGuide;
  42. @property (weak, nonatomic) UIView *snapshotView;
  43. @property (assign, nonatomic) BOOL jsq_isObserving;
  44. @property (strong, nonatomic) NSIndexPath *selectedIndexPathForMenu;
  45. @property (weak, nonatomic) UIGestureRecognizer *currentInteractivePopGestureRecognizer;
  46. @property (assign, nonatomic) BOOL textViewWasFirstResponderDuringInteractivePop;
  47. - (void)jsq_configureMessagesViewController;
  48. - (NSString *)jsq_currentlyComposedMessageText;
  49. - (void)jsq_handleDidChangeStatusBarFrameNotification:(NSNotification *)notification;
  50. - (void)jsq_didReceiveMenuWillShowNotification:(NSNotification *)notification;
  51. - (void)jsq_didReceiveMenuWillHideNotification:(NSNotification *)notification;
  52. - (void)jsq_updateKeyboardTriggerPoint;
  53. - (void)jsq_setToolbarBottomLayoutGuideConstant:(CGFloat)constant;
  54. - (void)jsq_handleInteractivePopGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer;
  55. - (BOOL)jsq_inputToolbarHasReachedMaximumHeight;
  56. - (void)jsq_adjustInputToolbarForComposerTextViewContentSizeChange:(CGFloat)dy;
  57. - (void)jsq_adjustInputToolbarHeightConstraintByDelta:(CGFloat)dy;
  58. - (void)jsq_scrollComposerTextViewToBottomAnimated:(BOOL)animated;
  59. - (void)jsq_updateCollectionViewInsets;
  60. - (void)jsq_setCollectionViewInsetsTopValue:(CGFloat)top bottomValue:(CGFloat)bottom;
  61. - (BOOL)jsq_isMenuVisible;
  62. - (void)jsq_addObservers;
  63. - (void)jsq_removeObservers;
  64. - (void)jsq_registerForNotifications:(BOOL)registerForNotifications;
  65. - (void)jsq_addActionToInteractivePopGestureRecognizer:(BOOL)addAction;
  66. @end
  67. @implementation JSQMessagesViewController
  68. #pragma mark - Class methods
  69. + (UINib *)nib
  70. {
  71. return [UINib nibWithNibName:NSStringFromClass([JSQMessagesViewController class])
  72. bundle:[NSBundle bundleForClass:[JSQMessagesViewController class]]];
  73. }
  74. + (instancetype)messagesViewController
  75. {
  76. return [[[self class] alloc] initWithNibName:NSStringFromClass([JSQMessagesViewController class])
  77. bundle:[NSBundle bundleForClass:[JSQMessagesViewController class]]];
  78. }
  79. #pragma mark - Initialization
  80. - (void)jsq_configureMessagesViewController
  81. {
  82. self.view.backgroundColor = [UIColor whiteColor];
  83. self.jsq_isObserving = NO;
  84. self.toolbarHeightConstraint.constant = self.inputToolbar.preferredDefaultHeight;
  85. self.collectionView.dataSource = self;
  86. self.collectionView.delegate = self;
  87. self.inputToolbar.delegate = self;
  88. self.inputToolbar.contentView.textView.placeHolder = [NSBundle jsq_localizedStringForKey:@"new_message"];
  89. self.inputToolbar.contentView.textView.delegate = self;
  90. self.automaticallyScrollsToMostRecentMessage = YES;
  91. self.outgoingCellIdentifier = [JSQMessagesCollectionViewCellOutgoing cellReuseIdentifier];
  92. self.outgoingMediaCellIdentifier = [JSQMessagesCollectionViewCellOutgoing mediaCellReuseIdentifier];
  93. self.incomingCellIdentifier = [JSQMessagesCollectionViewCellIncoming cellReuseIdentifier];
  94. self.incomingMediaCellIdentifier = [JSQMessagesCollectionViewCellIncoming mediaCellReuseIdentifier];
  95. // NOTE: let this behavior be opt-in for now
  96. // [JSQMessagesCollectionViewCell registerMenuAction:@selector(delete:)];
  97. self.showTypingIndicator = NO;
  98. self.showLoadEarlierMessagesHeader = NO;
  99. self.topContentAdditionalInset = 0.0f;
  100. [self jsq_updateCollectionViewInsets];
  101. // Don't set keyboardController if client creates custom content view via -loadToolbarContentView
  102. if (self.inputToolbar.contentView.textView != nil) {
  103. self.keyboardController = [[JSQMessagesKeyboardController alloc] initWithTextView:self.inputToolbar.contentView.textView
  104. contextView:self.view
  105. panGestureRecognizer:self.collectionView.panGestureRecognizer
  106. delegate:self];
  107. }
  108. }
  109. - (void)dealloc
  110. {
  111. [self jsq_registerForNotifications:NO];
  112. [self jsq_removeObservers];
  113. _collectionView.dataSource = nil;
  114. _collectionView.delegate = nil;
  115. _collectionView = nil;
  116. _inputToolbar.contentView.textView.delegate = nil;
  117. _inputToolbar.delegate = nil;
  118. _inputToolbar = nil;
  119. _toolbarHeightConstraint = nil;
  120. _toolbarBottomLayoutGuide = nil;
  121. _senderId = nil;
  122. _senderDisplayName = nil;
  123. _outgoingCellIdentifier = nil;
  124. _incomingCellIdentifier = nil;
  125. [_keyboardController endListeningForKeyboard];
  126. _keyboardController = nil;
  127. }
  128. #pragma mark - Setters
  129. - (void)setShowTypingIndicator:(BOOL)showTypingIndicator
  130. {
  131. if (_showTypingIndicator == showTypingIndicator) {
  132. return;
  133. }
  134. _showTypingIndicator = showTypingIndicator;
  135. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  136. [self.collectionView.collectionViewLayout invalidateLayout];
  137. }
  138. - (void)setShowLoadEarlierMessagesHeader:(BOOL)showLoadEarlierMessagesHeader
  139. {
  140. if (_showLoadEarlierMessagesHeader == showLoadEarlierMessagesHeader) {
  141. return;
  142. }
  143. _showLoadEarlierMessagesHeader = showLoadEarlierMessagesHeader;
  144. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  145. [self.collectionView.collectionViewLayout invalidateLayout];
  146. [self.collectionView reloadData];
  147. }
  148. - (void)setTopContentAdditionalInset:(CGFloat)topContentAdditionalInset
  149. {
  150. _topContentAdditionalInset = topContentAdditionalInset;
  151. [self jsq_updateCollectionViewInsets];
  152. }
  153. #pragma mark - View lifecycle
  154. - (void)viewDidLoad
  155. {
  156. [super viewDidLoad];
  157. [[[self class] nib] instantiateWithOwner:self options:nil];
  158. [self jsq_configureMessagesViewController];
  159. [self jsq_registerForNotifications:YES];
  160. }
  161. - (void)viewWillAppear:(BOOL)animated
  162. {
  163. NSParameterAssert(self.senderId != nil);
  164. NSParameterAssert(self.senderDisplayName != nil);
  165. [super viewWillAppear:animated];
  166. [self.view layoutIfNeeded];
  167. [self.collectionView.collectionViewLayout invalidateLayout];
  168. if (self.automaticallyScrollsToMostRecentMessage) {
  169. dispatch_async(dispatch_get_main_queue(), ^{
  170. [self scrollToBottomAnimated:NO];
  171. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  172. });
  173. }
  174. [self jsq_updateKeyboardTriggerPoint];
  175. }
  176. - (void)viewDidAppear:(BOOL)animated
  177. {
  178. [super viewDidAppear:animated];
  179. [self jsq_addObservers];
  180. [self jsq_addActionToInteractivePopGestureRecognizer:YES];
  181. [self.keyboardController beginListeningForKeyboard];
  182. if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
  183. [self.snapshotView removeFromSuperview];
  184. }
  185. }
  186. - (void)viewWillDisappear:(BOOL)animated
  187. {
  188. [super viewWillDisappear:animated];
  189. self.collectionView.collectionViewLayout.springinessEnabled = NO;
  190. }
  191. - (void)viewDidDisappear:(BOOL)animated
  192. {
  193. [super viewDidDisappear:animated];
  194. [self jsq_addActionToInteractivePopGestureRecognizer:NO];
  195. [self jsq_removeObservers];
  196. [self.keyboardController endListeningForKeyboard];
  197. }
  198. - (void)didReceiveMemoryWarning
  199. {
  200. [super didReceiveMemoryWarning];
  201. NSLog(@"MEMORY WARNING: %s", __PRETTY_FUNCTION__);
  202. }
  203. #pragma mark - View rotation
  204. - (BOOL)shouldAutorotate
  205. {
  206. return YES;
  207. }
  208. - (UIInterfaceOrientationMask)supportedInterfaceOrientations
  209. {
  210. if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
  211. return UIInterfaceOrientationMaskAllButUpsideDown;
  212. }
  213. return UIInterfaceOrientationMaskAll;
  214. }
  215. - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
  216. {
  217. [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
  218. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  219. }
  220. - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
  221. {
  222. [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
  223. if (self.showTypingIndicator) {
  224. self.showTypingIndicator = NO;
  225. self.showTypingIndicator = YES;
  226. [self.collectionView reloadData];
  227. }
  228. }
  229. #pragma mark - Messages view controller
  230. - (void)didPressSendButton:(UIButton *)button
  231. withMessageText:(NSString *)text
  232. senderId:(NSString *)senderId
  233. senderDisplayName:(NSString *)senderDisplayName
  234. date:(NSDate *)date
  235. {
  236. NSAssert(NO, @"Error! required method not implemented in subclass. Need to implement %s", __PRETTY_FUNCTION__);
  237. }
  238. - (void)didPressAccessoryButton:(UIButton *)sender
  239. {
  240. NSAssert(NO, @"Error! required method not implemented in subclass. Need to implement %s", __PRETTY_FUNCTION__);
  241. }
  242. - (void)finishSendingMessage
  243. {
  244. [self finishSendingMessageAnimated:YES];
  245. }
  246. - (void)finishSendingMessageAnimated:(BOOL)animated {
  247. UITextView *textView = self.inputToolbar.contentView.textView;
  248. textView.text = nil;
  249. [textView.undoManager removeAllActions];
  250. [self.inputToolbar toggleSendButtonEnabled];
  251. [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:textView];
  252. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  253. [self.collectionView reloadData];
  254. if (self.automaticallyScrollsToMostRecentMessage) {
  255. [self scrollToBottomAnimated:animated];
  256. }
  257. }
  258. - (void)finishReceivingMessage
  259. {
  260. [self finishReceivingMessageAnimated:YES];
  261. }
  262. - (void)finishReceivingMessageAnimated:(BOOL)animated {
  263. self.showTypingIndicator = NO;
  264. [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
  265. [self.collectionView reloadData];
  266. if (self.automaticallyScrollsToMostRecentMessage && ![self jsq_isMenuVisible]) {
  267. [self scrollToBottomAnimated:animated];
  268. }
  269. }
  270. - (void)scrollToBottomAnimated:(BOOL)animated
  271. {
  272. if ([self.collectionView numberOfSections] == 0) {
  273. return;
  274. }
  275. NSInteger items = [self.collectionView numberOfItemsInSection:0];
  276. if (items == 0) {
  277. return;
  278. }
  279. CGFloat collectionViewContentHeight = [self.collectionView.collectionViewLayout collectionViewContentSize].height;
  280. BOOL isContentTooSmall = (collectionViewContentHeight < CGRectGetHeight(self.collectionView.bounds));
  281. if (isContentTooSmall) {
  282. // workaround for the first few messages not scrolling
  283. // when the collection view content size is too small, `scrollToItemAtIndexPath:` doesn't work properly
  284. // this seems to be a UIKit bug, see #256 on GitHub
  285. [self.collectionView scrollRectToVisible:CGRectMake(0.0, collectionViewContentHeight - 1.0f, 1.0f, 1.0f)
  286. animated:animated];
  287. return;
  288. }
  289. // workaround for really long messages not scrolling
  290. // if last message is too long, use scroll position bottom for better appearance, else use top
  291. // possibly a UIKit bug, see #480 on GitHub
  292. NSUInteger finalRow = MAX(0, [self.collectionView numberOfItemsInSection:0] - 1);
  293. NSIndexPath *finalIndexPath = [NSIndexPath indexPathForItem:finalRow inSection:0];
  294. CGSize finalCellSize = [self.collectionView.collectionViewLayout sizeForItemAtIndexPath:finalIndexPath];
  295. CGFloat maxHeightForVisibleMessage = CGRectGetHeight(self.collectionView.bounds) - self.collectionView.contentInset.top - CGRectGetHeight(self.inputToolbar.bounds);
  296. UICollectionViewScrollPosition scrollPosition = (finalCellSize.height > maxHeightForVisibleMessage) ? UICollectionViewScrollPositionBottom : UICollectionViewScrollPositionTop;
  297. [self.collectionView scrollToItemAtIndexPath:finalIndexPath
  298. atScrollPosition:scrollPosition
  299. animated:animated];
  300. }
  301. #pragma mark - JSQMessages collection view data source
  302. - (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
  303. {
  304. NSAssert(NO, @"ERROR: required method not implemented: %s", __PRETTY_FUNCTION__);
  305. return nil;
  306. }
  307. - (void)collectionView:(JSQMessagesCollectionView *)collectionView didDeleteMessageAtIndexPath:(NSIndexPath *)indexPath
  308. {
  309. NSAssert(NO, @"ERROR: required method not implemented: %s", __PRETTY_FUNCTION__);
  310. }
  311. - (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
  312. {
  313. NSAssert(NO, @"ERROR: required method not implemented: %s", __PRETTY_FUNCTION__);
  314. return nil;
  315. }
  316. - (id<JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
  317. {
  318. NSAssert(NO, @"ERROR: required method not implemented: %s", __PRETTY_FUNCTION__);
  319. return nil;
  320. }
  321. - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
  322. {
  323. return nil;
  324. }
  325. - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
  326. {
  327. return nil;
  328. }
  329. - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
  330. {
  331. return nil;
  332. }
  333. #pragma mark - Collection view data source
  334. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
  335. {
  336. return 0;
  337. }
  338. - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
  339. {
  340. return 1;
  341. }
  342. - (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
  343. {
  344. id<JSQMessageData> messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath];
  345. NSParameterAssert(messageItem != nil);
  346. NSString *messageSenderId = [messageItem senderId];
  347. NSParameterAssert(messageSenderId != nil);
  348. BOOL isOutgoingMessage = [messageSenderId isEqualToString:self.senderId];
  349. BOOL isMediaMessage = [messageItem isMediaMessage];
  350. NSString *cellIdentifier = nil;
  351. if (isMediaMessage) {
  352. cellIdentifier = isOutgoingMessage ? self.outgoingMediaCellIdentifier : self.incomingMediaCellIdentifier;
  353. }
  354. else {
  355. cellIdentifier = isOutgoingMessage ? self.outgoingCellIdentifier : self.incomingCellIdentifier;
  356. }
  357. JSQMessagesCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
  358. cell.delegate = collectionView;
  359. if (!isMediaMessage) {
  360. cell.textView.text = [messageItem text];
  361. if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
  362. // workaround for iOS 7 textView data detectors bug
  363. cell.textView.text = nil;
  364. cell.textView.attributedText = [[NSAttributedString alloc] initWithString:[messageItem text]
  365. attributes:@{ NSFontAttributeName : collectionView.collectionViewLayout.messageBubbleFont }];
  366. }
  367. NSParameterAssert(cell.textView.text != nil);
  368. id<JSQMessageBubbleImageDataSource> bubbleImageDataSource = [collectionView.dataSource collectionView:collectionView messageBubbleImageDataForItemAtIndexPath:indexPath];
  369. cell.messageBubbleImageView.image = [bubbleImageDataSource messageBubbleImage];
  370. cell.messageBubbleImageView.highlightedImage = [bubbleImageDataSource messageBubbleHighlightedImage];
  371. }
  372. else {
  373. id<JSQMessageMediaData> messageMedia = [messageItem media];
  374. cell.mediaView = [messageMedia mediaView] ?: [messageMedia mediaPlaceholderView];
  375. NSParameterAssert(cell.mediaView != nil);
  376. }
  377. BOOL needsAvatar = YES;
  378. if (isOutgoingMessage && CGSizeEqualToSize(collectionView.collectionViewLayout.outgoingAvatarViewSize, CGSizeZero)) {
  379. needsAvatar = NO;
  380. }
  381. else if (!isOutgoingMessage && CGSizeEqualToSize(collectionView.collectionViewLayout.incomingAvatarViewSize, CGSizeZero)) {
  382. needsAvatar = NO;
  383. }
  384. id<JSQMessageAvatarImageDataSource> avatarImageDataSource = nil;
  385. if (needsAvatar) {
  386. avatarImageDataSource = [collectionView.dataSource collectionView:collectionView avatarImageDataForItemAtIndexPath:indexPath];
  387. if (avatarImageDataSource != nil) {
  388. UIImage *avatarImage = [avatarImageDataSource avatarImage];
  389. if (avatarImage == nil) {
  390. cell.avatarImageView.image = [avatarImageDataSource avatarPlaceholderImage];
  391. cell.avatarImageView.highlightedImage = nil;
  392. }
  393. else {
  394. cell.avatarImageView.image = avatarImage;
  395. cell.avatarImageView.highlightedImage = [avatarImageDataSource avatarHighlightedImage];
  396. }
  397. }
  398. }
  399. cell.cellTopLabel.attributedText = [collectionView.dataSource collectionView:collectionView attributedTextForCellTopLabelAtIndexPath:indexPath];
  400. cell.messageBubbleTopLabel.attributedText = [collectionView.dataSource collectionView:collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:indexPath];
  401. cell.cellBottomLabel.attributedText = [collectionView.dataSource collectionView:collectionView attributedTextForCellBottomLabelAtIndexPath:indexPath];
  402. CGFloat bubbleTopLabelInset = (avatarImageDataSource != nil) ? 60.0f : 15.0f;
  403. if (isOutgoingMessage) {
  404. cell.messageBubbleTopLabel.textInsets = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, bubbleTopLabelInset);
  405. }
  406. else {
  407. cell.messageBubbleTopLabel.textInsets = UIEdgeInsetsMake(0.0f, bubbleTopLabelInset, 0.0f, 0.0f);
  408. }
  409. cell.textView.dataDetectorTypes = UIDataDetectorTypeAll;
  410. cell.backgroundColor = [UIColor clearColor];
  411. cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
  412. cell.layer.shouldRasterize = YES;
  413. return cell;
  414. }
  415. - (UICollectionReusableView *)collectionView:(JSQMessagesCollectionView *)collectionView
  416. viewForSupplementaryElementOfKind:(NSString *)kind
  417. atIndexPath:(NSIndexPath *)indexPath
  418. {
  419. if (self.showTypingIndicator && [kind isEqualToString:UICollectionElementKindSectionFooter]) {
  420. return [collectionView dequeueTypingIndicatorFooterViewForIndexPath:indexPath];
  421. }
  422. else if (self.showLoadEarlierMessagesHeader && [kind isEqualToString:UICollectionElementKindSectionHeader]) {
  423. return [collectionView dequeueLoadEarlierMessagesViewHeaderForIndexPath:indexPath];
  424. }
  425. return nil;
  426. }
  427. - (CGSize)collectionView:(UICollectionView *)collectionView
  428. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
  429. {
  430. if (!self.showTypingIndicator) {
  431. return CGSizeZero;
  432. }
  433. return CGSizeMake([collectionViewLayout itemWidth], kJSQMessagesTypingIndicatorFooterViewHeight);
  434. }
  435. - (CGSize)collectionView:(UICollectionView *)collectionView
  436. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
  437. {
  438. if (!self.showLoadEarlierMessagesHeader) {
  439. return CGSizeZero;
  440. }
  441. return CGSizeMake([collectionViewLayout itemWidth], kJSQMessagesLoadEarlierHeaderViewHeight);
  442. }
  443. #pragma mark - Collection view delegate
  444. - (BOOL)collectionView:(JSQMessagesCollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
  445. {
  446. // disable menu for media messages
  447. id<JSQMessageData> messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath];
  448. if ([messageItem isMediaMessage]) {
  449. return NO;
  450. }
  451. self.selectedIndexPathForMenu = indexPath;
  452. // textviews are selectable to allow data detectors
  453. // however, this allows the 'copy, define, select' UIMenuController to show
  454. // which conflicts with the collection view's UIMenuController
  455. // temporarily disable 'selectable' to prevent this issue
  456. JSQMessagesCollectionViewCell *selectedCell = (JSQMessagesCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
  457. selectedCell.textView.selectable = NO;
  458. return YES;
  459. }
  460. - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  461. {
  462. if (action == @selector(copy:) || action == @selector(delete:)) {
  463. return YES;
  464. }
  465. return NO;
  466. }
  467. - (void)collectionView:(JSQMessagesCollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  468. {
  469. if (action == @selector(copy:)) {
  470. id<JSQMessageData> messageData = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath];
  471. [[UIPasteboard generalPasteboard] setString:[messageData text]];
  472. }
  473. else if (action == @selector(delete:)) {
  474. [collectionView.dataSource collectionView:collectionView didDeleteMessageAtIndexPath:indexPath];
  475. [collectionView deleteItemsAtIndexPaths:@[indexPath]];
  476. [collectionView.collectionViewLayout invalidateLayout];
  477. }
  478. }
  479. #pragma mark - Collection view delegate flow layout
  480. - (CGSize)collectionView:(JSQMessagesCollectionView *)collectionView
  481. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
  482. {
  483. return [collectionViewLayout sizeForItemAtIndexPath:indexPath];
  484. }
  485. - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
  486. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
  487. {
  488. return 0.0f;
  489. }
  490. - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
  491. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
  492. {
  493. return 0.0f;
  494. }
  495. - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
  496. layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
  497. {
  498. return 0.0f;
  499. }
  500. - (void)collectionView:(JSQMessagesCollectionView *)collectionView
  501. didTapAvatarImageView:(UIImageView *)avatarImageView
  502. atIndexPath:(NSIndexPath *)indexPath { }
  503. - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath { }
  504. - (void)collectionView:(JSQMessagesCollectionView *)collectionView
  505. didTapCellAtIndexPath:(NSIndexPath *)indexPath
  506. touchLocation:(CGPoint)touchLocation { }
  507. #pragma mark - Input toolbar delegate
  508. - (void)messagesInputToolbar:(JSQMessagesInputToolbar *)toolbar didPressLeftBarButton:(UIButton *)sender
  509. {
  510. if (toolbar.sendButtonOnRight) {
  511. [self didPressAccessoryButton:sender];
  512. }
  513. else {
  514. [self didPressSendButton:sender
  515. withMessageText:[self jsq_currentlyComposedMessageText]
  516. senderId:self.senderId
  517. senderDisplayName:self.senderDisplayName
  518. date:[NSDate date]];
  519. }
  520. }
  521. - (void)messagesInputToolbar:(JSQMessagesInputToolbar *)toolbar didPressRightBarButton:(UIButton *)sender
  522. {
  523. if (toolbar.sendButtonOnRight) {
  524. [self didPressSendButton:sender
  525. withMessageText:[self jsq_currentlyComposedMessageText]
  526. senderId:self.senderId
  527. senderDisplayName:self.senderDisplayName
  528. date:[NSDate date]];
  529. }
  530. else {
  531. [self didPressAccessoryButton:sender];
  532. }
  533. }
  534. - (NSString *)jsq_currentlyComposedMessageText
  535. {
  536. // auto-accept any auto-correct suggestions
  537. [self.inputToolbar.contentView.textView.inputDelegate selectionWillChange:self.inputToolbar.contentView.textView];
  538. [self.inputToolbar.contentView.textView.inputDelegate selectionDidChange:self.inputToolbar.contentView.textView];
  539. return [self.inputToolbar.contentView.textView.text jsq_stringByTrimingWhitespace];
  540. }
  541. #pragma mark - Text view delegate
  542. - (void)textViewDidBeginEditing:(UITextView *)textView
  543. {
  544. if (textView != self.inputToolbar.contentView.textView) {
  545. return;
  546. }
  547. [textView becomeFirstResponder];
  548. if (self.automaticallyScrollsToMostRecentMessage) {
  549. [self scrollToBottomAnimated:YES];
  550. }
  551. }
  552. - (void)textViewDidChange:(UITextView *)textView
  553. {
  554. if (textView != self.inputToolbar.contentView.textView) {
  555. return;
  556. }
  557. [self.inputToolbar toggleSendButtonEnabled];
  558. }
  559. - (void)textViewDidEndEditing:(UITextView *)textView
  560. {
  561. if (textView != self.inputToolbar.contentView.textView) {
  562. return;
  563. }
  564. [textView resignFirstResponder];
  565. }
  566. #pragma mark - Notifications
  567. - (void)jsq_handleDidChangeStatusBarFrameNotification:(NSNotification *)notification
  568. {
  569. if (self.keyboardController.keyboardIsVisible) {
  570. [self jsq_setToolbarBottomLayoutGuideConstant:CGRectGetHeight(self.keyboardController.currentKeyboardFrame)];
  571. }
  572. }
  573. - (void)jsq_didReceiveMenuWillShowNotification:(NSNotification *)notification
  574. {
  575. if (!self.selectedIndexPathForMenu) {
  576. return;
  577. }
  578. [[NSNotificationCenter defaultCenter] removeObserver:self
  579. name:UIMenuControllerWillShowMenuNotification
  580. object:nil];
  581. UIMenuController *menu = [notification object];
  582. [menu setMenuVisible:NO animated:NO];
  583. JSQMessagesCollectionViewCell *selectedCell = (JSQMessagesCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:self.selectedIndexPathForMenu];
  584. CGRect selectedCellMessageBubbleFrame = [selectedCell convertRect:selectedCell.messageBubbleContainerView.frame toView:self.view];
  585. [menu setTargetRect:selectedCellMessageBubbleFrame inView:self.view];
  586. [menu setMenuVisible:YES animated:YES];
  587. [[NSNotificationCenter defaultCenter] addObserver:self
  588. selector:@selector(jsq_didReceiveMenuWillShowNotification:)
  589. name:UIMenuControllerWillShowMenuNotification
  590. object:nil];
  591. }
  592. - (void)jsq_didReceiveMenuWillHideNotification:(NSNotification *)notification
  593. {
  594. if (!self.selectedIndexPathForMenu) {
  595. return;
  596. }
  597. // per comment above in 'shouldShowMenuForItemAtIndexPath:'
  598. // re-enable 'selectable', thus re-enabling data detectors if present
  599. JSQMessagesCollectionViewCell *selectedCell = (JSQMessagesCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:self.selectedIndexPathForMenu];
  600. selectedCell.textView.selectable = YES;
  601. self.selectedIndexPathForMenu = nil;
  602. }
  603. #pragma mark - Key-value observing
  604. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  605. {
  606. if (context == kJSQMessagesKeyValueObservingContext) {
  607. if (object == self.inputToolbar.contentView.textView
  608. && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
  609. CGSize oldContentSize = [[change objectForKey:NSKeyValueChangeOldKey] CGSizeValue];
  610. CGSize newContentSize = [[change objectForKey:NSKeyValueChangeNewKey] CGSizeValue];
  611. CGFloat dy = newContentSize.height - oldContentSize.height;
  612. [self jsq_adjustInputToolbarForComposerTextViewContentSizeChange:dy];
  613. [self jsq_updateCollectionViewInsets];
  614. if (self.automaticallyScrollsToMostRecentMessage) {
  615. [self scrollToBottomAnimated:NO];
  616. }
  617. }
  618. }
  619. }
  620. #pragma mark - Keyboard controller delegate
  621. - (void)keyboardController:(JSQMessagesKeyboardController *)keyboardController keyboardDidChangeFrame:(CGRect)keyboardFrame
  622. {
  623. if (![self.inputToolbar.contentView.textView isFirstResponder] && self.toolbarBottomLayoutGuide.constant == 0.0f) {
  624. return;
  625. }
  626. CGFloat heightFromBottom = CGRectGetMaxY(self.collectionView.frame) - CGRectGetMinY(keyboardFrame);
  627. heightFromBottom = MAX(0.0f, heightFromBottom);
  628. [self jsq_setToolbarBottomLayoutGuideConstant:heightFromBottom];
  629. }
  630. - (void)jsq_setToolbarBottomLayoutGuideConstant:(CGFloat)constant
  631. {
  632. self.toolbarBottomLayoutGuide.constant = constant;
  633. [self.view setNeedsUpdateConstraints];
  634. [self.view layoutIfNeeded];
  635. [self jsq_updateCollectionViewInsets];
  636. }
  637. - (void)jsq_updateKeyboardTriggerPoint
  638. {
  639. self.keyboardController.keyboardTriggerPoint = CGPointMake(0.0f, CGRectGetHeight(self.inputToolbar.bounds));
  640. }
  641. #pragma mark - Gesture recognizers
  642. - (void)jsq_handleInteractivePopGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
  643. {
  644. switch (gestureRecognizer.state) {
  645. case UIGestureRecognizerStateBegan:
  646. {
  647. if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
  648. [self.snapshotView removeFromSuperview];
  649. }
  650. self.textViewWasFirstResponderDuringInteractivePop = [self.inputToolbar.contentView.textView isFirstResponder];
  651. [self.keyboardController endListeningForKeyboard];
  652. if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
  653. [self.inputToolbar.contentView.textView resignFirstResponder];
  654. [UIView animateWithDuration:0.0
  655. animations:^{
  656. [self jsq_setToolbarBottomLayoutGuideConstant:0.0f];
  657. }];
  658. UIView *snapshot = [self.view snapshotViewAfterScreenUpdates:YES];
  659. [self.view addSubview:snapshot];
  660. self.snapshotView = snapshot;
  661. }
  662. }
  663. break;
  664. case UIGestureRecognizerStateChanged:
  665. break;
  666. case UIGestureRecognizerStateCancelled:
  667. case UIGestureRecognizerStateEnded:
  668. case UIGestureRecognizerStateFailed:
  669. [self.keyboardController beginListeningForKeyboard];
  670. if (self.textViewWasFirstResponderDuringInteractivePop) {
  671. [self.inputToolbar.contentView.textView becomeFirstResponder];
  672. }
  673. if ([UIDevice jsq_isCurrentDeviceBeforeiOS8]) {
  674. [self.snapshotView removeFromSuperview];
  675. }
  676. break;
  677. default:
  678. break;
  679. }
  680. }
  681. #pragma mark - Input toolbar utilities
  682. - (BOOL)jsq_inputToolbarHasReachedMaximumHeight
  683. {
  684. return CGRectGetMinY(self.inputToolbar.frame) == (self.topLayoutGuide.length + self.topContentAdditionalInset);
  685. }
  686. - (void)jsq_adjustInputToolbarForComposerTextViewContentSizeChange:(CGFloat)dy
  687. {
  688. BOOL contentSizeIsIncreasing = (dy > 0);
  689. if ([self jsq_inputToolbarHasReachedMaximumHeight]) {
  690. BOOL contentOffsetIsPositive = (self.inputToolbar.contentView.textView.contentOffset.y > 0);
  691. if (contentSizeIsIncreasing || contentOffsetIsPositive) {
  692. [self jsq_scrollComposerTextViewToBottomAnimated:YES];
  693. return;
  694. }
  695. }
  696. CGFloat toolbarOriginY = CGRectGetMinY(self.inputToolbar.frame);
  697. CGFloat newToolbarOriginY = toolbarOriginY - dy;
  698. // attempted to increase origin.Y above topLayoutGuide
  699. if (newToolbarOriginY <= self.topLayoutGuide.length + self.topContentAdditionalInset) {
  700. dy = toolbarOriginY - (self.topLayoutGuide.length + self.topContentAdditionalInset);
  701. [self jsq_scrollComposerTextViewToBottomAnimated:YES];
  702. }
  703. [self jsq_adjustInputToolbarHeightConstraintByDelta:dy];
  704. [self jsq_updateKeyboardTriggerPoint];
  705. if (dy < 0) {
  706. [self jsq_scrollComposerTextViewToBottomAnimated:NO];
  707. }
  708. }
  709. - (void)jsq_adjustInputToolbarHeightConstraintByDelta:(CGFloat)dy
  710. {
  711. CGFloat proposedHeight = self.toolbarHeightConstraint.constant + dy;
  712. CGFloat finalHeight = MAX(proposedHeight, self.inputToolbar.preferredDefaultHeight);
  713. if (self.inputToolbar.maximumHeight != NSNotFound) {
  714. finalHeight = MIN(finalHeight, self.inputToolbar.maximumHeight);
  715. }
  716. if (self.toolbarHeightConstraint.constant != finalHeight) {
  717. self.toolbarHeightConstraint.constant = finalHeight;
  718. [self.view setNeedsUpdateConstraints];
  719. [self.view layoutIfNeeded];
  720. }
  721. }
  722. - (void)jsq_scrollComposerTextViewToBottomAnimated:(BOOL)animated
  723. {
  724. UITextView *textView = self.inputToolbar.contentView.textView;
  725. CGPoint contentOffsetToShowLastLine = CGPointMake(0.0f, textView.contentSize.height - CGRectGetHeight(textView.bounds));
  726. if (!animated) {
  727. textView.contentOffset = contentOffsetToShowLastLine;
  728. return;
  729. }
  730. [UIView animateWithDuration:0.01
  731. delay:0.01
  732. options:UIViewAnimationOptionCurveLinear
  733. animations:^{
  734. textView.contentOffset = contentOffsetToShowLastLine;
  735. }
  736. completion:nil];
  737. }
  738. #pragma mark - Collection view utilities
  739. - (void)jsq_updateCollectionViewInsets
  740. {
  741. [self jsq_setCollectionViewInsetsTopValue:self.topLayoutGuide.length + self.topContentAdditionalInset
  742. bottomValue:CGRectGetMaxY(self.collectionView.frame) - CGRectGetMinY(self.inputToolbar.frame)];
  743. }
  744. - (void)jsq_setCollectionViewInsetsTopValue:(CGFloat)top bottomValue:(CGFloat)bottom
  745. {
  746. UIEdgeInsets insets = UIEdgeInsetsMake(top, 0.0f, bottom, 0.0f);
  747. self.collectionView.contentInset = insets;
  748. self.collectionView.scrollIndicatorInsets = insets;
  749. }
  750. - (BOOL)jsq_isMenuVisible
  751. {
  752. // check if cell copy menu is showing
  753. // it is only our menu if `selectedIndexPathForMenu` is not `nil`
  754. return self.selectedIndexPathForMenu != nil && [[UIMenuController sharedMenuController] isMenuVisible];
  755. }
  756. #pragma mark - Utilities
  757. - (void)jsq_addObservers
  758. {
  759. if (self.jsq_isObserving) {
  760. return;
  761. }
  762. [self.inputToolbar.contentView.textView addObserver:self
  763. forKeyPath:NSStringFromSelector(@selector(contentSize))
  764. options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
  765. context:kJSQMessagesKeyValueObservingContext];
  766. self.jsq_isObserving = YES;
  767. }
  768. - (void)jsq_removeObservers
  769. {
  770. if (!_jsq_isObserving) {
  771. return;
  772. }
  773. @try {
  774. [_inputToolbar.contentView.textView removeObserver:self
  775. forKeyPath:NSStringFromSelector(@selector(contentSize))
  776. context:kJSQMessagesKeyValueObservingContext];
  777. }
  778. @catch (NSException * __unused exception) { }
  779. _jsq_isObserving = NO;
  780. }
  781. - (void)jsq_registerForNotifications:(BOOL)registerForNotifications
  782. {
  783. if (registerForNotifications) {
  784. [[NSNotificationCenter defaultCenter] addObserver:self
  785. selector:@selector(jsq_handleDidChangeStatusBarFrameNotification:)
  786. name:UIApplicationDidChangeStatusBarFrameNotification
  787. object:nil];
  788. [[NSNotificationCenter defaultCenter] addObserver:self
  789. selector:@selector(jsq_didReceiveMenuWillShowNotification:)
  790. name:UIMenuControllerWillShowMenuNotification
  791. object:nil];
  792. [[NSNotificationCenter defaultCenter] addObserver:self
  793. selector:@selector(jsq_didReceiveMenuWillHideNotification:)
  794. name:UIMenuControllerWillHideMenuNotification
  795. object:nil];
  796. }
  797. else {
  798. [[NSNotificationCenter defaultCenter] removeObserver:self
  799. name:UIApplicationDidChangeStatusBarFrameNotification
  800. object:nil];
  801. [[NSNotificationCenter defaultCenter] removeObserver:self
  802. name:UIMenuControllerWillShowMenuNotification
  803. object:nil];
  804. [[NSNotificationCenter defaultCenter] removeObserver:self
  805. name:UIMenuControllerWillHideMenuNotification
  806. object:nil];
  807. }
  808. }
  809. - (void)jsq_addActionToInteractivePopGestureRecognizer:(BOOL)addAction
  810. {
  811. if (self.currentInteractivePopGestureRecognizer != nil) {
  812. [self.currentInteractivePopGestureRecognizer removeTarget:nil
  813. action:@selector(jsq_handleInteractivePopGestureRecognizer:)];
  814. self.currentInteractivePopGestureRecognizer = nil;
  815. }
  816. if (addAction) {
  817. [self.navigationController.interactivePopGestureRecognizer addTarget:self
  818. action:@selector(jsq_handleInteractivePopGestureRecognizer:)];
  819. self.currentInteractivePopGestureRecognizer = self.navigationController.interactivePopGestureRecognizer;
  820. }
  821. }
  822. @end