ChatViewSearchHeader.m 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-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 "ChatViewSearchHeader.h"
  21. #import "EntityManager.h"
  22. #import "BundleUtil.h"
  23. #import "ChatMessageCell.h"
  24. #import "MBProgressHUD.h"
  25. @interface ChatViewSearchHeader () <UISearchBarDelegate>
  26. @property EntityManager *entityManager;
  27. @property NSString *searchPattern;
  28. @property NSArray *messageHits;
  29. @property NSInteger currentIndex;
  30. @property ChatMessageCell *currentCell;
  31. @property BOOL shouldCancel;
  32. @end
  33. @implementation ChatViewSearchHeader
  34. - (void)awakeFromNib {
  35. self.searchBar.delegate = self;
  36. self.entityManager = [[EntityManager alloc] init];
  37. [self setup];
  38. [super awakeFromNib];
  39. }
  40. - (void)setup {
  41. _label.hidden = YES;
  42. [_cancelButton setTitle:[BundleUtil localizedStringForKey:@"cancel"] forState:UIControlStateNormal];
  43. [_prevButton setTitle:[BundleUtil localizedStringForKey:@"previous"] forState:UIControlStateNormal];
  44. [_nextButton setTitle:[BundleUtil localizedStringForKey:@"next"] forState:UIControlStateNormal];
  45. _cancelButton.tintColor = [Colors main];
  46. _prevButton.tintColor = [Colors main];
  47. _nextButton.tintColor = [Colors main];
  48. _label.textColor = [Colors fontNormal];
  49. _searchBar.barStyle = UIBarStyleBlackTranslucent;
  50. [Colors updateSearchBar:self.searchBar];
  51. _hairlineView1.backgroundColor = [Colors hairline];
  52. _hairlineView2.backgroundColor = [Colors hairline];
  53. [self updateButtons];
  54. }
  55. - (BOOL)becomeFirstResponder {
  56. return [_searchBar becomeFirstResponder];
  57. }
  58. - (BOOL)resignFirstResponder {
  59. return [_searchBar resignFirstResponder];
  60. }
  61. - (void)searchPattern:(NSString *)pattern {
  62. // cancel previous
  63. _shouldCancel = YES;
  64. [_currentCell setBubbleHighlighted:NO];
  65. _searchPattern = pattern;
  66. _messageHits = [_entityManager.entityFetcher messagesContaining:pattern inConversation:_chatViewController.conversation];
  67. [self updateChatViewController];
  68. if ([_messageHits count] > 0) {
  69. _currentIndex = 0;
  70. [self stepSearchResults];
  71. }
  72. [self updateLabel];
  73. [self updateButtons];
  74. }
  75. - (void)updateChatViewController {
  76. if ([_messageHits count] > 0) {
  77. _chatViewController.searchPattern = _searchPattern;
  78. } else {
  79. _chatViewController.searchPattern = nil;
  80. }
  81. [self updateVisibleCells];
  82. }
  83. - (void)cancelSearch {
  84. _shouldCancel = YES;
  85. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  86. }
  87. - (void)addLoadEarlierMessagesHUD {
  88. if ([MBProgressHUD HUDForView:_chatViewController.view] != nil) {
  89. return;
  90. }
  91. MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:_chatViewController.view animated:YES];
  92. hud.label.text = [BundleUtil localizedStringForKey:@"load_earlier_messages"];
  93. [hud.button setTitle:NSLocalizedString(@"cancel", nil) forState:UIControlStateNormal];
  94. [hud.button addTarget:self action:@selector(cancelSearch) forControlEvents:UIControlEventTouchUpInside];
  95. }
  96. - (void)inChatGoToMessage:(BaseMessage *)message {
  97. _shouldCancel = NO;
  98. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  99. __block NSIndexPath *indexPath = nil;
  100. while (_shouldCancel == NO) {
  101. indexPath = [_chatViewController indexPathForMessage:message];
  102. if (indexPath) {
  103. // found message
  104. break;
  105. } else {
  106. NSInteger offset = [_chatViewController messageOffset];
  107. if (offset > 0) {
  108. dispatch_sync(dispatch_get_main_queue(), ^{
  109. [self addLoadEarlierMessagesHUD];
  110. [_chatViewController loadEarlierMessagesAction:nil];
  111. if (_shouldCancel) {
  112. return;
  113. }
  114. NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  115. [_chatViewController.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
  116. });
  117. } else {
  118. break;
  119. }
  120. }
  121. }
  122. dispatch_sync(dispatch_get_main_queue(), ^{
  123. [MBProgressHUD hideHUDForView:_chatViewController.view animated:YES];
  124. });
  125. dispatch_async(dispatch_get_main_queue(), ^{
  126. if (indexPath) {
  127. // safety check if indexPath is still valid
  128. if ([self isValidIndexPath:indexPath] == NO) {
  129. return;
  130. }
  131. //deselect previous cell
  132. [_currentCell setBubbleHighlighted:NO];
  133. UITableViewScrollPosition scrollPosition;
  134. if (_searchBar.isFirstResponder) {
  135. scrollPosition = UITableViewScrollPositionBottom;
  136. } else {
  137. scrollPosition = UITableViewScrollPositionMiddle;
  138. }
  139. [_chatViewController.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES];
  140. _currentCell = (ChatMessageCell *)[_chatViewController.chatContent cellForRowAtIndexPath:indexPath];
  141. CGFloat delayMs;
  142. if (_currentCell) {
  143. delayMs = 100.0;
  144. } else {
  145. // cell not visible yet
  146. delayMs = 400.0;
  147. }
  148. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  149. _currentCell = (ChatMessageCell *)[_chatViewController.chatContent cellForRowAtIndexPath:indexPath];
  150. [_currentCell setBubbleHighlighted:YES];
  151. if (UIAccessibilityIsVoiceOverRunning()) {
  152. NSString *text = _currentCell.accessibilityLabel;
  153. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text);
  154. }
  155. });
  156. }
  157. });
  158. });
  159. }
  160. - (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
  161. NSInteger sectionCount = [_chatViewController.chatContent.dataSource numberOfSectionsInTableView:_chatViewController.chatContent];
  162. if (indexPath.section >= sectionCount) {
  163. return NO;
  164. }
  165. NSInteger rowCount = [_chatViewController.chatContent.dataSource tableView:_chatViewController.chatContent numberOfRowsInSection:indexPath.section];
  166. if (indexPath.row >= rowCount) {
  167. return NO;
  168. }
  169. return YES;
  170. }
  171. - (void)updateVisibleCells {
  172. NSArray *visibleCells = [_chatViewController.chatContent visibleCells];
  173. for (ChatMessageCell *cell in visibleCells) {
  174. if ([cell isKindOfClass:[ChatMessageCell class]]) {
  175. if ([_messageHits containsObject:cell.message]) {
  176. [cell highlightOccurencesOf:_searchPattern];
  177. } else {
  178. [cell highlightOccurencesOf:nil];
  179. }
  180. [cell setNeedsLayout];
  181. }
  182. }
  183. }
  184. - (void)selectMessageAt:(NSInteger)index {
  185. BaseMessage *message = [_messageHits objectAtIndex:index];
  186. [self inChatGoToMessage:message];
  187. [self updateLabel];
  188. }
  189. - (void)updateLabel {
  190. if ([_messageHits count] > 0) {
  191. NSString *format = [BundleUtil localizedStringForKey:@"chat_search_label_format"];
  192. _label.text = [NSString stringWithFormat:format, _currentIndex + 1, [_messageHits count]];
  193. _label.hidden = NO;
  194. } else {
  195. _label.hidden = YES;
  196. }
  197. }
  198. - (void)updateButtons {
  199. if ([_messageHits count] > 1) {
  200. _prevButton.enabled = YES;
  201. _nextButton.enabled = YES;
  202. } else {
  203. _prevButton.enabled = NO;
  204. _nextButton.enabled = NO;
  205. }
  206. }
  207. #pragma mark - UISearchBarDelegate
  208. - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
  209. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  210. [self performSelector:@selector(searchPattern:) withObject:searchText afterDelay:0.75];
  211. }
  212. - (IBAction)cancelAction:(id)sender {
  213. _shouldCancel = YES;
  214. _messageHits = nil;
  215. _searchPattern = nil;
  216. [_currentCell setBubbleHighlighted:NO];
  217. [self updateChatViewController];
  218. [self resignFirstResponder];
  219. [_delegate didCancelSearch];
  220. }
  221. - (void)stepSearchResults {
  222. NSInteger count = [_messageHits count];
  223. [self selectMessageAt:_currentIndex];
  224. _prevButton.enabled = (_currentIndex < count - 1);
  225. _nextButton.enabled = (_currentIndex > 0);
  226. }
  227. // note: order in _messageHits is descending
  228. - (IBAction)prevAction:(id)sender {
  229. if (_currentIndex < [_messageHits count] - 1) {
  230. _currentIndex++;
  231. }
  232. [self resignFirstResponder];
  233. [self stepSearchResults];
  234. }
  235. - (IBAction)nextAction:(id)sender {
  236. if (_currentIndex > 0) {
  237. _currentIndex--;
  238. }
  239. [self resignFirstResponder];
  240. [self stepSearchResults];
  241. }
  242. @end