/* Copyright 2024 New Vector Ltd. Copyright 2015 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "MXKSearchViewController.h" #import "MXKSearchTableViewCell.h" #import "NSBundle+MatrixKit.h" #import "MXKSwiftHeader.h" @interface MXKSearchViewController () { /** Optional bar buttons */ UIBarButtonItem *searchBarButton; /** Search handling */ BOOL ignoreSearchRequest; } @end @implementation MXKSearchViewController @synthesize dataSource, shouldScrollToBottomOnRefresh; #pragma mark - Class methods + (UINib *)nib { return [UINib nibWithNibName:NSStringFromClass([MXKSearchViewController class]) bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]]; } + (instancetype)searchViewController { return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKSearchViewController class]) bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]]; } #pragma mark - - (void)finalizeInit { [super finalizeInit]; _enableBarButtonSearch = YES; } - (void)viewDidLoad { [super viewDidLoad]; // Check whether the view controller has been pushed via storyboard if (!_searchTableView) { // Instantiate view controller objects [[[self class] nib] instantiateWithOwner:self options:nil]; } // Adjust Top and Bottom constraints to take into account potential navBar and tabBar. [NSLayoutConstraint deactivateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" _searchSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.searchSearchBar attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]; _searchTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.searchTableView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]; #pragma clang diagnostic pop [NSLayoutConstraint activateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]]; // Hide search bar by default self.searchSearchBar.hidden = YES; self.searchSearchBarHeightConstraint.constant = 0; [self.view setNeedsUpdateConstraints]; self.noResultsLabel.text = [VectorL10n searchNoResults]; self.noResultsLabel.hidden = YES; searchBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(showSearchBar:)]; // Apply search option in navigation bar self.enableBarButtonSearch = _enableBarButtonSearch; // Finalize table view configuration _searchTableView.delegate = self; _searchTableView.dataSource = dataSource; // Note: dataSource may be nil here // Set up classes to use for cells [self.searchTableView registerNib:MXKSearchTableViewCell.nib forCellReuseIdentifier:MXKSearchTableViewCell.defaultReuseIdentifier]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Restore search mechanism (if enabled) ignoreSearchRequest = NO; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // The user may still press search button whereas the view disappears ignoreSearchRequest = YES; } #pragma mark - Override MXKViewController - (void)onKeyboardShowAnimationComplete { // Report the keyboard view in order to track keyboard frame changes self.keyboardView = _searchSearchBar.inputAccessoryView.superview; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" - (void)setKeyboardHeight:(CGFloat)keyboardHeight { // Deduce the bottom constraint for the table view (Don't forget the potential tabBar) CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length; // Check whether the keyboard is over the tabBar if (tableViewBottomConst < 0) { tableViewBottomConst = 0; } // Update constraints _searchTableViewBottomConstraint.constant = tableViewBottomConst; // Force layout immediately to take into account new constraint [self.view layoutIfNeeded]; } #pragma clang diagnostic pop - (void)destroy { _searchTableView.dataSource = nil; _searchTableView.delegate = nil; _searchTableView = nil; dataSource.delegate = nil; [dataSource destroy]; dataSource = nil; [super destroy]; } #pragma mark - - (void)displaySearch:(MXKSearchDataSource*)searchDataSource { // Cancel registration on existing dataSource if any if (dataSource) { dataSource.delegate = nil; // Remove associated matrix sessions [self removeMatrixSession:dataSource.mxSession]; [dataSource destroy]; } dataSource = searchDataSource; dataSource.delegate = self; // Report the related matrix sessions at view controller level to update UI according to sessions state [self addMatrixSession:searchDataSource.mxSession]; if (_searchTableView) { // Set up table data source _searchTableView.dataSource = dataSource; } } #pragma mark - UIBarButton handling - (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch { _enableBarButtonSearch = enableBarButtonSearch; [self refreshUIBarButtons]; } - (void)refreshUIBarButtons { if (_enableBarButtonSearch) { self.navigationItem.rightBarButtonItems = @[searchBarButton]; } else { self.navigationItem.rightBarButtonItems = nil; } } #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData { return MXKSearchTableViewCell.class; } - (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData { return MXKSearchTableViewCell.defaultReuseIdentifier; } - (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes { __block CGPoint tableViewOffset; if (!shouldScrollToBottomOnRefresh) { // Store current tableview scrolling point to restore it after [UITableView reloadData] // This avoids unexpected scrolling for the user tableViewOffset = _searchTableView.contentOffset; } [_searchTableView reloadData]; if (shouldScrollToBottomOnRefresh) { [self scrollToBottomAnimated:NO]; shouldScrollToBottomOnRefresh = NO; } else { // Restore the user scrolling point by computing the offset introduced by new cells // New cells are always introduced at the top of the table NSIndexSet *insertedIndexes = (NSIndexSet*)changes; // Get each new cell height [insertedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { MXKCellData* cellData = [self.dataSource cellDataAtIndex:idx]; Class class = [self cellViewClassForCellData:cellData]; tableViewOffset.y += [class heightForCellData:cellData withMaximumWidth:self->_searchTableView.frame.size.width]; }]; [_searchTableView setContentOffset:tableViewOffset animated:NO]; } self.title = [NSString stringWithFormat:@"%@ (%tu)", self.dataSource.searchText, self.dataSource.serverCount]; } - (void)dataSource:(MXKDataSource*)dataSource2 didStateChange:(MXKDataSourceState)state { // MXKSearchDataSource comes back to the `MXKDataSourceStatePreparing` when searching if (state == MXKDataSourceStatePreparing) { _noResultsLabel.hidden = YES; [self startActivityIndicator]; } else { [self stopActivityIndicator]; // Display "No Results" if a search is active with an empty result if (dataSource.searchText.length && ![dataSource tableView:_searchTableView numberOfRowsInSection:0]) { _noResultsLabel.hidden = NO; _searchTableView.hidden = YES; } else { _noResultsLabel.hidden = YES; _searchTableView.hidden = NO; } } } #pragma mark - UITableView delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { MXKCellData *cellData = [dataSource cellDataAtIndex:indexPath.row]; Class class = [self cellViewClassForCellData:cellData]; return [class heightForCellData:cellData withMaximumWidth:tableView.frame.size.width]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Must be implemented at app level } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath { // Release here resources, and restore reusable cells if ([cell respondsToSelector:@selector(didEndDisplay)]) { [(id)cell didEndDisplay]; } } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { // Detect vertical bounce at the top of the tableview to trigger pagination if (scrollView == _searchTableView) { // paginate ? if (scrollView.contentOffset.y < -64) { [self triggerBackPagination]; } } } #pragma mark - UISearchBarDelegate - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { // "Done" key has been pressed [searchBar resignFirstResponder]; // Apply filter if (searchBar.text.length) { shouldScrollToBottomOnRefresh = YES; [dataSource searchMessages:searchBar.text force:NO]; } } - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { // Leave search [searchBar resignFirstResponder]; self.searchSearchBar.hidden = YES; self.searchSearchBarHeightConstraint.constant = 0; [self.view setNeedsUpdateConstraints]; self.searchSearchBar.text = nil; } #pragma mark - Actions - (void)showSearchBar:(id)sender { // The user may have pressed search button whereas the view controller was disappearing if (ignoreSearchRequest) { return; } if (self.searchSearchBar.isHidden) { self.searchSearchBar.hidden = NO; self.searchSearchBarHeightConstraint.constant = 44; [self.view setNeedsUpdateConstraints]; [self.searchSearchBar becomeFirstResponder]; } else { [self searchBarCancelButtonClicked: self.searchSearchBar]; } } #pragma mark - Private methods - (void)triggerBackPagination { // Paginate only if possible if (NO == dataSource.canPaginate) { return; } [dataSource paginateBack]; } - (void)scrollToBottomAnimated:(BOOL)animated { if (_searchTableView.contentSize.height) { CGFloat visibleHeight = _searchTableView.frame.size.height - _searchTableView.adjustedContentInset.top - _searchTableView.adjustedContentInset.bottom; if (visibleHeight < _searchTableView.contentSize.height) { CGFloat wantedOffsetY = _searchTableView.contentSize.height - visibleHeight - _searchTableView.adjustedContentInset.top; CGFloat currentOffsetY = _searchTableView.contentOffset.y; if (wantedOffsetY != currentOffsetY) { [_searchTableView setContentOffset:CGPointMake(0, wantedOffsetY) animated:animated]; } } else { _searchTableView.contentOffset = CGPointMake(0, - _searchTableView.adjustedContentInset.top); } } } @end