/* Copyright 2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd Copyright (c) 2021 BWI GmbH SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "UnifiedSearchViewController.h" #import "UnifiedSearchRecentsDataSource.h" #import "RecentsViewController.h" #import "RoomDataSource.h" #import "RoomViewController.h" #import "DirectoryViewController.h" #import "ContactDetailsViewController.h" #import "SettingsViewController.h" #import "HomeMessagesSearchViewController.h" #import "HomeMessagesSearchDataSource.h" #import "HomeFilesSearchViewController.h" #import "FilesSearchCellData.h" #import "GeneratedInterface-Swift.h" #import "GBDeviceInfo_iOS.h" @interface UnifiedSearchViewController () { RecentsViewController *recentsViewController; UnifiedSearchRecentsDataSource *recentsDataSource; HomeMessagesSearchViewController *messagesSearchViewController; HomeMessagesSearchDataSource *messagesSearchDataSource; HomeFilesSearchViewController *filesSearchViewController; MXKSearchDataSource *filesSearchDataSource; ContactsTableViewController *peopleSearchViewController; ContactsDataSource *peopleSearchDataSource; // Current alert (if any). UIAlertController *currentAlert; } @end @implementation UnifiedSearchViewController + (instancetype)instantiate { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; UnifiedSearchViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"UnifiedSearchViewController"]; return viewController; } - (void)finalizeInit { [super finalizeInit]; // The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController). } - (void)viewDidLoad { // Set up the SegmentedVC tabs before calling [super viewDidLoad] NSMutableArray* viewControllers = [[NSMutableArray alloc] init]; NSMutableArray* titles = [[NSMutableArray alloc] init]; [titles addObject:[VectorL10n searchRooms]]; recentsViewController = [RecentsViewController recentListViewController]; recentsViewController.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenSearchRooms]; recentsViewController.enableSearchBar = NO; [viewControllers addObject:recentsViewController]; messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; if (BWIBuildSettings.shared.showUnifiedSearchViewMessagesTab) { messagesSearchViewController.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenSearchMessages]; [viewControllers addObject:messagesSearchViewController]; } // Add search People tab [titles addObject:[VectorL10n searchPeople]]; peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; peopleSearchViewController.disableFindYourContactsFooter = YES; peopleSearchViewController.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenSearchPeople]; [viewControllers addObject:peopleSearchViewController]; filesSearchViewController = [HomeFilesSearchViewController searchViewController]; if (BWIBuildSettings.shared.showUnifiedSearchViewFilesTab) { filesSearchViewController.screenTracker = [[AnalyticsScreenTracker alloc] initWithScreen:AnalyticsScreenSearchFiles]; [viewControllers addObject:filesSearchViewController]; } [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; [super viewDidLoad]; // Initialize here the data sources if a matrix session has been already set. [self initializeDataSources]; self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; self.searchBar.placeholder = [VectorL10n searchDefaultPlaceholder]; [super showSearch:NO]; } - (void)destroy { [super destroy]; if (currentAlert) { [currentAlert dismissViewControllerAnimated:NO completion:nil]; currentAlert = nil; } self.searchBar.delegate = nil; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Let's child display the loading not the home view controller if (self.activityIndicator) { [self.activityIndicator stopAnimating]; self.activityIndicator = nil; } // Reset searches [recentsDataSource searchWithPatterns:nil]; [self updateSearch]; if (BuildSettings.newAppLayoutEnabled) { [self.searchBar vc_searchTextField].backgroundColor = nil; [self vc_setLargeTitleDisplayMode: UINavigationItemLargeTitleDisplayModeAutomatic]; } } - (void)viewWillDisappear:(BOOL)animated { if (!self.searchBarHidden && self.extendedLayoutIncludesOpaqueBars) { // if a search bar is visible, navigationBar height will be increased. Below code will force update layout on previous view controller. [self.navigationController.view setNeedsLayout]; // force update layout [self.navigationController.view layoutIfNeeded]; // to fix height of the navigation bar } [super viewWillDisappear:animated]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (self.splitViewController && !self.splitViewController.isCollapsed) { // In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen, // the selected room (if any) is highlighted. [self refreshCurrentSelectedCellInChild:YES]; } } - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; } #pragma mark - - (MXEvent*)selectedSearchEvent { if (messagesSearchViewController.selectedEvent) { return messagesSearchViewController.selectedEvent; } return filesSearchViewController.selectedEvent; } - (MXSession*)selectedSearchEventSession { if (messagesSearchViewController.selectedEvent) { return messagesSearchDataSource.mxSession; } return filesSearchDataSource.mxSession; } #pragma mark - - (void)initializeDataSources { MXSession *mainSession = self.mainSession; if (mainSession) { // Init the recents data source RecentsListService *recentsListService = [[RecentsListService alloc] initWithSession:mainSession]; recentsDataSource = [[UnifiedSearchRecentsDataSource alloc] initWithMatrixSession:mainSession recentsListService:recentsListService]; [recentsViewController displayList:recentsDataSource]; // Init the search for messages messagesSearchDataSource = [[HomeMessagesSearchDataSource alloc] initWithMatrixSession:mainSession]; [messagesSearchViewController displaySearch:messagesSearchDataSource]; // Init the search for messages filesSearchDataSource = [[MXKSearchDataSource alloc] initWithMatrixSession:mainSession]; filesSearchDataSource.roomEventFilter.containsURL = YES; filesSearchDataSource.shouldShowRoomDisplayName = YES; [filesSearchDataSource registerCellDataClass:FilesSearchCellData.class forCellIdentifier:kMXKSearchCellDataIdentifier]; [filesSearchViewController displaySearch:filesSearchDataSource]; // Init the search for people peopleSearchDataSource = [[ContactsDataSource alloc] initWithMatrixSession:mainSession]; peopleSearchDataSource.showLocalContacts = NO; peopleSearchDataSource.areSectionsShrinkable = YES; peopleSearchDataSource.displaySearchInputInContactsList = YES; peopleSearchDataSource.contactCellAccessoryImage = [AssetImages.disclosureIcon.image vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor];; [peopleSearchViewController displayList:peopleSearchDataSource]; // Check whether there are others sessions NSArray* mxSessions = self.mxSessions; if (mxSessions.count > 1) { for (MXSession *mxSession in mxSessions) { if (mxSession != mainSession) { // Add the session to the recents data source [recentsDataSource addMatrixSession:mxSession]; // FIXME: Update messagesSearchDataSource and filesSearchDataSource } } } } } - (void)addMatrixSession:(MXSession *)mxSession { // Check whether the controller'€™s view is loaded into memory. if (recentsViewController) { // Check whether the data sources have been initialized. if (!recentsDataSource) { // Add first the session. The updated sessions list will be used during data sources initialization. [super addMatrixSession:mxSession]; // Prepare data sources and return [self initializeDataSources]; return; } else { // Add the session to the existing recents data source [recentsDataSource addMatrixSession:mxSession]; // FIXME: Update messagesSearchDataSource and filesSearchDataSource } } [super addMatrixSession:mxSession]; } - (void)removeMatrixSession:(MXSession *)mxSession { [recentsDataSource removeMatrixSession:mxSession]; // Check whether there are others sessions if (!recentsDataSource.mxSessions.count) { [recentsViewController displayList:nil]; [recentsDataSource destroy]; recentsDataSource = nil; } // FIXME: Handle correctly messagesSearchDataSource and filesSearchDataSource [super removeMatrixSession:mxSession]; } - (void)showPublicRoomsDirectory { // Force hiding the keyboard [self.searchBar resignFirstResponder]; [self performSegueWithIdentifier:@"showDirectory" sender:self]; } #pragma mark - Override MXKViewController - (void)startActivityIndicator { // Redirect the operation to the currently displayed VC // It is a MXKViewController or a MXKTableViewController. So it supports startActivityIndicator [self.selectedViewController performSelector:@selector(startActivityIndicator)]; } - (void)stopActivityIndicator { // The selected view controller mwy have changed since the call of [self startActivityIndicator] // So, stop the activity indicator for all children for (UIViewController *viewController in self.viewControllers) { [viewController performSelector:@selector(stopActivityIndicator)]; } } #pragma mark - Override SegmentedViewController - (void)setSelectedIndex:(NSUInteger)selectedIndex { [super setSelectedIndex:selectedIndex]; if (self.selectedViewController == peopleSearchViewController) { self.searchBar.placeholder = [BWIL10n searchPeoplePlaceholder]; } else { self.searchBar.placeholder = [VectorL10n searchDefaultPlaceholder]; } [self updateSearch]; } #pragma mark - Internal methods // Made the currently displayed child update its selected cell - (void)refreshCurrentSelectedCellInChild:(BOOL)forceVisible { // TODO: Manage other children than recents [recentsViewController refreshCurrentSelectedCell:forceVisible]; [peopleSearchViewController refreshCurrentSelectedCell:forceVisible]; } #pragma mark - Navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { // Keep ref on destinationViewController [super prepareForSegue:segue sender:sender]; if ([[segue identifier] isEqualToString:@"showDirectory"]) { DirectoryViewController *directoryViewController = segue.destinationViewController; [directoryViewController displayWitDataSource:recentsDataSource.publicRoomsDirectoryDataSource]; } // Hide back button title [self vc_removeBackTitle]; } #pragma mark - Search - (void)hideSearch:(BOOL)animated { [self withdrawViewControllerAnimated:animated completion:nil]; } // Update search results under the currently selected tab - (void)updateSearch { if (self.searchBar.text.length) { recentsDataSource.hideRecents = NO; // Forward the search request to the data source if (self.selectedViewController == recentsViewController) { // Do a AND search on words separated by a space NSArray *patterns = [self.searchBar.text componentsSeparatedByString:@" "]; [recentsDataSource searchWithPatterns:patterns]; recentsViewController.shouldScrollToTopOnRefresh = YES; } else if (self.selectedViewController == messagesSearchViewController) { // Launch the search only if the keyboard is no more visible if (!self.searchBar.isFirstResponder) { // Do it asynchronously to give time to messagesSearchViewController to be set up // so that it can display its loading wheel dispatch_async(dispatch_get_main_queue(), ^{ [self->messagesSearchDataSource searchMessages:self.searchBar.text force:NO]; self->messagesSearchViewController.shouldScrollToBottomOnRefresh = YES; }); } } else if (self.selectedViewController == peopleSearchViewController) { [peopleSearchDataSource searchWithPattern:self.searchBar.text forceReset:NO]; } else if (self.selectedViewController == filesSearchViewController) { // Launch the search only if the keyboard is no more visible if (!self.searchBar.isFirstResponder) { // Do it asynchronously to give time to filesSearchViewController to be set up // so that it can display its loading wheel dispatch_async(dispatch_get_main_queue(), ^{ [self->filesSearchDataSource searchMessages:self.searchBar.text force:NO]; self->filesSearchViewController.shouldScrollToBottomOnRefresh = YES; }); } } } else { // Nothing to search, show only the public dictionary recentsDataSource.hideRecents = YES; // Reset search result (if any) [recentsDataSource searchWithPatterns:nil]; if (messagesSearchDataSource.searchText.length) { [messagesSearchDataSource searchMessages:nil force:NO]; } [peopleSearchDataSource searchWithPattern:nil forceReset:NO]; if (filesSearchDataSource.searchText.length) { [filesSearchDataSource searchMessages:nil force:NO]; } } } #pragma mark - UISearchBarDelegate - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { AccountRestrictionService* service = [[AccountRestrictionService alloc] initWithMxSession:self.mainSession]; if (self.selectedViewController == recentsViewController) { if (![service isRoomAccessRestriction]) { // As the public room search is local, it can be updated on each text change [self updateSearch]; } } else if (self.selectedViewController == peopleSearchViewController) { if (![service isAddressListRestriction]) { // As the contact search is local, it can be updated on each text change [self updateSearch]; } } else if (!self.searchBar.text.length) { // Reset message search if any [self updateSearch]; } } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { [searchBar resignFirstResponder]; if (self.selectedViewController == messagesSearchViewController || self.selectedViewController == filesSearchViewController) { // As the messages/files search is done homeserver-side, launch it only on the "Search" button [self updateSearch]; } } #pragma mark - ContactsTableViewControllerDelegate - (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact { // Force hiding the keyboard [self.searchBar resignFirstResponder]; [[AppDelegate theDelegate].masterTabBarController selectContact:contact]; } @end