// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2015-2020 Threema GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#import "ChatViewSearchHeader.h"
#import "EntityManager.h"
#import "BundleUtil.h"
#import "ChatMessageCell.h"
#import "MBProgressHUD.h"
@interface ChatViewSearchHeader ()
@property EntityManager *entityManager;
@property NSString *searchPattern;
@property NSArray *messageHits;
@property NSInteger currentIndex;
@property ChatMessageCell *currentCell;
@property BOOL shouldCancel;
@end
@implementation ChatViewSearchHeader
- (void)awakeFromNib {
self.searchBar.delegate = self;
self.entityManager = [[EntityManager alloc] init];
[self setup];
[super awakeFromNib];
}
- (void)setup {
_label.hidden = YES;
[_cancelButton setTitle:[BundleUtil localizedStringForKey:@"cancel"] forState:UIControlStateNormal];
[_prevButton setTitle:[BundleUtil localizedStringForKey:@"previous"] forState:UIControlStateNormal];
[_nextButton setTitle:[BundleUtil localizedStringForKey:@"next"] forState:UIControlStateNormal];
_cancelButton.tintColor = [Colors main];
_prevButton.tintColor = [Colors main];
_nextButton.tintColor = [Colors main];
_label.textColor = [Colors fontNormal];
_searchBar.barStyle = UIBarStyleBlackTranslucent;
[Colors updateSearchBar:self.searchBar];
_hairlineView1.backgroundColor = [Colors hairline];
_hairlineView2.backgroundColor = [Colors hairline];
[self updateButtons];
}
- (BOOL)becomeFirstResponder {
return [_searchBar becomeFirstResponder];
}
- (BOOL)resignFirstResponder {
return [_searchBar resignFirstResponder];
}
- (void)searchPattern:(NSString *)pattern {
// cancel previous
_shouldCancel = YES;
[_currentCell setBubbleHighlighted:NO];
_searchPattern = pattern;
_messageHits = [_entityManager.entityFetcher messagesContaining:pattern inConversation:_chatViewController.conversation];
[self updateChatViewController];
if ([_messageHits count] > 0) {
_currentIndex = 0;
[self stepSearchResults];
}
[self updateLabel];
[self updateButtons];
}
- (void)updateChatViewController {
if ([_messageHits count] > 0) {
_chatViewController.searchPattern = _searchPattern;
} else {
_chatViewController.searchPattern = nil;
}
[self updateVisibleCells];
}
- (void)cancelSearch {
_shouldCancel = YES;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)addLoadEarlierMessagesHUD {
if ([MBProgressHUD HUDForView:_chatViewController.view] != nil) {
return;
}
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:_chatViewController.view animated:YES];
hud.label.text = [BundleUtil localizedStringForKey:@"load_earlier_messages"];
[hud.button setTitle:NSLocalizedString(@"cancel", nil) forState:UIControlStateNormal];
[hud.button addTarget:self action:@selector(cancelSearch) forControlEvents:UIControlEventTouchUpInside];
}
- (void)inChatGoToMessage:(BaseMessage *)message {
_shouldCancel = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
__block NSIndexPath *indexPath = nil;
while (_shouldCancel == NO) {
indexPath = [_chatViewController indexPathForMessage:message];
if (indexPath) {
// found message
break;
} else {
NSInteger offset = [_chatViewController messageOffset];
if (offset > 0) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self addLoadEarlierMessagesHUD];
[_chatViewController loadEarlierMessagesAction:nil];
if (_shouldCancel) {
return;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[_chatViewController.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
});
} else {
break;
}
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:_chatViewController.view animated:YES];
});
dispatch_async(dispatch_get_main_queue(), ^{
if (indexPath) {
// safety check if indexPath is still valid
if ([self isValidIndexPath:indexPath] == NO) {
return;
}
//deselect previous cell
[_currentCell setBubbleHighlighted:NO];
UITableViewScrollPosition scrollPosition;
if (_searchBar.isFirstResponder) {
scrollPosition = UITableViewScrollPositionBottom;
} else {
scrollPosition = UITableViewScrollPositionMiddle;
}
[_chatViewController.chatContent scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES];
_currentCell = (ChatMessageCell *)[_chatViewController.chatContent cellForRowAtIndexPath:indexPath];
CGFloat delayMs;
if (_currentCell) {
delayMs = 100.0;
} else {
// cell not visible yet
delayMs = 400.0;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
_currentCell = (ChatMessageCell *)[_chatViewController.chatContent cellForRowAtIndexPath:indexPath];
[_currentCell setBubbleHighlighted:YES];
if (UIAccessibilityIsVoiceOverRunning()) {
NSString *text = _currentCell.accessibilityLabel;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text);
}
});
}
});
});
}
- (BOOL)isValidIndexPath:(NSIndexPath *)indexPath {
NSInteger sectionCount = [_chatViewController.chatContent.dataSource numberOfSectionsInTableView:_chatViewController.chatContent];
if (indexPath.section >= sectionCount) {
return NO;
}
NSInteger rowCount = [_chatViewController.chatContent.dataSource tableView:_chatViewController.chatContent numberOfRowsInSection:indexPath.section];
if (indexPath.row >= rowCount) {
return NO;
}
return YES;
}
- (void)updateVisibleCells {
NSArray *visibleCells = [_chatViewController.chatContent visibleCells];
for (ChatMessageCell *cell in visibleCells) {
if ([cell isKindOfClass:[ChatMessageCell class]]) {
if ([_messageHits containsObject:cell.message]) {
[cell highlightOccurencesOf:_searchPattern];
} else {
[cell highlightOccurencesOf:nil];
}
[cell setNeedsLayout];
}
}
}
- (void)selectMessageAt:(NSInteger)index {
BaseMessage *message = [_messageHits objectAtIndex:index];
[self inChatGoToMessage:message];
[self updateLabel];
}
- (void)updateLabel {
if ([_messageHits count] > 0) {
NSString *format = [BundleUtil localizedStringForKey:@"chat_search_label_format"];
_label.text = [NSString stringWithFormat:format, _currentIndex + 1, [_messageHits count]];
_label.hidden = NO;
} else {
_label.hidden = YES;
}
}
- (void)updateButtons {
if ([_messageHits count] > 1) {
_prevButton.enabled = YES;
_nextButton.enabled = YES;
} else {
_prevButton.enabled = NO;
_nextButton.enabled = NO;
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:@selector(searchPattern:) withObject:searchText afterDelay:0.75];
}
- (IBAction)cancelAction:(id)sender {
_shouldCancel = YES;
_messageHits = nil;
_searchPattern = nil;
[_currentCell setBubbleHighlighted:NO];
[self updateChatViewController];
[self resignFirstResponder];
[_delegate didCancelSearch];
}
- (void)stepSearchResults {
NSInteger count = [_messageHits count];
[self selectMessageAt:_currentIndex];
_prevButton.enabled = (_currentIndex < count - 1);
_nextButton.enabled = (_currentIndex > 0);
}
// note: order in _messageHits is descending
- (IBAction)prevAction:(id)sender {
if (_currentIndex < [_messageHits count] - 1) {
_currentIndex++;
}
[self resignFirstResponder];
[self stepSearchResults];
}
- (IBAction)nextAction:(id)sender {
if (_currentIndex > 0) {
_currentIndex--;
}
[self resignFirstResponder];
[self stepSearchResults];
}
@end