/* Copyright 2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import #import "ContactsDataSource.h" #import "ContactTableViewCell.h" #import "SectionHeaderView.h" #import "LocalContactsSectionHeaderContainerView.h" #import "ThemeService.h" #import "GeneratedInterface-Swift.h" #define CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE 0x01 #define CONTACTSDATASOURCE_USERDIRECTORY_BITWISE 0x02 #define CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0 #define CONTACTSDATASOURCE_LOCALCONTACTS_SECTION_HEADER_HEIGHT 65.0 @interface ContactsDataSource () { // Search processing dispatch_queue_t searchProcessingQueue; NSUInteger searchProcessingCount; NSString *searchProcessingText; NSMutableArray *searchProcessingLocalContacts; NSMutableArray *searchProcessingMatrixContacts; // The current request to the homeserver user directory MXHTTPOperation *hsUserDirectoryOperation; BOOL forceSearchResultRefresh; // This dictionary tells for each display name whether it appears several times. NSMutableDictionary *isMultiUseNameByDisplayName; // Shrinked sections. NSInteger shrinkedSectionsBitMask; LocalContactsSectionHeaderContainerView *localContactsCheckboxContainer; UILabel *checkboxLabel; UIImageView *localContactsCheckbox; } @end @implementation ContactsDataSource - (instancetype)init { self = [super init]; if (self) { // Prepare search session searchProcessingQueue = dispatch_queue_create("ContactsDataSource", DISPATCH_QUEUE_SERIAL); searchProcessingCount = 0; searchProcessingText = nil; searchProcessingLocalContacts = nil; searchProcessingMatrixContacts = nil; _ignoredContactsByEmail = [NSMutableDictionary dictionary]; _ignoredContactsByMatrixId = [NSMutableDictionary dictionary]; isMultiUseNameByDisplayName = [NSMutableDictionary dictionary]; _forceMatrixIdInDisplayName = NO; _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; hideNonMatrixEnabledContacts = NO; _displaySearchInputInContactsList = NO; // Register on contact update notifications [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; } return self; } - (instancetype)initWithMatrixSession:(MXSession *)mxSession { self = [super initWithMatrixSession:mxSession]; if (self) { // Only show local contacts when contact sync is enabled and the identity server terms of service have been accepted. _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts && self.mxSession.identityService.areAllTermsAgreed; } return self; } - (void)destroy { [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; filteredLocalContacts = nil; filteredMatrixContacts = nil; _ignoredContactsByEmail = nil; _ignoredContactsByMatrixId = nil; forceSearchResultRefresh = NO; searchProcessingQueue = nil; searchProcessingLocalContacts = nil; searchProcessingMatrixContacts = nil; isMultiUseNameByDisplayName = nil; _contactCellAccessoryImage = nil; localContactsCheckboxContainer = nil; checkboxLabel = nil; localContactsCheckbox = nil; [hsUserDirectoryOperation cancel]; hsUserDirectoryOperation = nil; [super destroy]; } #pragma mark - - (void)forceRefresh { // Check whether a search is in progress if (searchProcessingCount) { forceSearchResultRefresh = YES; return; } // Refresh the search result [self searchWithPattern:currentSearchText forceReset:YES]; } - (void)setForceMatrixIdInDisplayName:(BOOL)forceMatrixIdInDisplayName { if (_forceMatrixIdInDisplayName != forceMatrixIdInDisplayName) { _forceMatrixIdInDisplayName = forceMatrixIdInDisplayName; [self forceRefresh]; } } - (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceRefresh { // If possible, always start a new search by asking the homeserver user directory BOOL hsUserDirectory = (self.mxSession.state != MXSessionStateHomeserverNotReachable); [self searchWithPattern:searchText forceReset:forceRefresh hsUserDirectory:hsUserDirectory]; } - (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceRefresh hsUserDirectory:(BOOL)hsUserDirectory { // Update search results. searchText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSMutableArray *unfilteredLocalContacts; NSMutableArray *unfilteredMatrixContacts; searchProcessingCount++; if (!searchText.length) { // Disclose by default the sections if a search was in progress. if (searchProcessingText.length) { shrinkedSectionsBitMask = 0; } } else if (forceRefresh || ![searchText isEqualToString:searchProcessingText]) { // Prepare on the main thread the arrays used to initialize the search on the processing queue. unfilteredLocalContacts = [self unfilteredLocalContactsArray]; if (!hsUserDirectory) { _userDirectoryState = ContactsDataSourceUserDirectoryStateOfflineLoading; unfilteredMatrixContacts = [self unfilteredMatrixContactsArray]; } else if (![searchText isEqualToString:searchProcessingText]) { _userDirectoryState = ContactsDataSourceUserDirectoryStateLoading; // Make a search on the homeserver user directory [filteredMatrixContacts removeAllObjects]; filteredMatrixContacts = nil; // Cancel previous operation if (hsUserDirectoryOperation) { [hsUserDirectoryOperation cancel]; hsUserDirectoryOperation = nil; } MXWeakify(self); hsUserDirectoryOperation = [self.mxSession.matrixRestClient searchUsers:searchText limit:50 success:^(MXUserSearchResponse *userSearchResponse) { MXStrongifyAndReturnIfNil(self); self->filteredMatrixContacts = [NSMutableArray arrayWithCapacity:userSearchResponse.results.count]; NSArray *sortedArray; if (BWIBuildSettings.shared.sortUserSearchResultsAlphabetically) { sortedArray = [userSearchResponse.results sortedArrayUsingComparator:^NSComparisonResult(MXUser *a, MXUser *b) { return [a.displayname caseInsensitiveCompare:b.displayname]; }]; } else { sortedArray = userSearchResponse.results; } // Keep the response order as the hs ordered users by relevance for (MXUser *mxUser in sortedArray) { MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:mxUser.displayname andMatrixID:mxUser.userId]; [self->filteredMatrixContacts addObject:contact]; } self->hsUserDirectoryOperation = nil; self->_userDirectoryState = userSearchResponse.limited ? ContactsDataSourceUserDirectoryStateLoadedButLimited : ContactsDataSourceUserDirectoryStateLoaded; // And inform the delegate about the update [self.delegate dataSource:self didCellChange:nil]; } failure:^(NSError *error) { // Ignore connection cancellation error if ((![error.domain isEqualToString:NSURLErrorDomain] || error.code != NSURLErrorCancelled)) { // But for other errors, launch a local search MXLogDebug(@"[ContactsDataSource] [MXRestClient searchUsers] returns an error. Do a search on local known contacts"); [self searchWithPattern:searchText forceReset:forceRefresh hsUserDirectory:NO]; } }]; } // Disclose the sections shrinkedSectionsBitMask = 0; } MXWeakify(self); dispatch_async(searchProcessingQueue, ^{ MXStrongifyAndReturnIfNil(self); // Reset the current arrays if it is required if (!searchText.length) { self->searchProcessingLocalContacts = nil; self->searchProcessingMatrixContacts = nil; } else if (unfilteredLocalContacts) { self->searchProcessingLocalContacts = unfilteredLocalContacts; self->searchProcessingMatrixContacts = unfilteredMatrixContacts; } for (NSUInteger index = 0; index < self->searchProcessingLocalContacts.count;) { MXKContact* contact = self->searchProcessingLocalContacts[index]; if (![contact hasPrefix:searchText]) { [self->searchProcessingLocalContacts removeObjectAtIndex:index]; } else { // Next index++; } } for (NSUInteger index = 0; index < self->searchProcessingMatrixContacts.count;) { MXKContact* contact = self->searchProcessingMatrixContacts[index]; if (![contact hasPrefix:searchText]) { [self->searchProcessingMatrixContacts removeObjectAtIndex:index]; } else { // Next index++; } } // Sort the refreshed list of the invitable contacts [[MXKContactManager sharedManager] sortAlphabeticallyContacts:self->searchProcessingLocalContacts]; [[MXKContactManager sharedManager] sortContactsByLastActiveInformation:self->searchProcessingMatrixContacts]; self->searchProcessingText = searchText; MXWeakify(self); dispatch_sync(dispatch_get_main_queue(), ^{ // Sanity check: check whether self has been destroyed. MXStrongifyAndReturnIfNil(self); // Render the search result only if there is no other search in progress. self->searchProcessingCount --; if (!self->searchProcessingCount) { if (!self->forceSearchResultRefresh) { // Update the filtered contacts. self->currentSearchText = self->searchProcessingText; self->filteredLocalContacts = self->searchProcessingLocalContacts; if (!hsUserDirectory) { self->filteredMatrixContacts = self->searchProcessingMatrixContacts; self->_userDirectoryState = ContactsDataSourceUserDirectoryStateOfflineLoaded; } if (!self.forceMatrixIdInDisplayName) { [self->isMultiUseNameByDisplayName removeAllObjects]; for (MXKContact* contact in self->filteredMatrixContacts) { self->isMultiUseNameByDisplayName[contact.displayName] = (self->isMultiUseNameByDisplayName[contact.displayName] ? @(YES) : @(NO)); } } // And inform the delegate about the update [self.delegate dataSource:self didCellChange:nil]; } else { // Launch a new search self->forceSearchResultRefresh = NO; [self searchWithPattern:self->searchProcessingText forceReset:YES]; } } }); }); } - (void)setDisplaySearchInputInContactsList:(BOOL)displaySearchInputInContactsList { if (_displaySearchInputInContactsList != displaySearchInputInContactsList) { _displaySearchInputInContactsList = displaySearchInputInContactsList; [self forceRefresh]; } } - (MXKContact*)searchInputContact { // Check whether the current search input is a valid email or a Matrix user ID if (currentSearchText.length && ([MXTools isEmailAddress:currentSearchText] || [MXTools isMatrixUserIdentifier:currentSearchText])) { return [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; } return nil; } #pragma mark - Internals - (void)onContactManagerDidUpdate:(NSNotification *)notif { [self forceRefresh]; } - (NSMutableArray*)unfilteredLocalContactsArray { // Retrieve all the contacts obtained by splitting each local contact by contact method. This list is ordered alphabetically. NSMutableArray *unfilteredLocalContacts = [NSMutableArray arrayWithArray:[MXKContactManager sharedManager].localContactsSplitByContactMethod]; // Remove the ignored contacts // + Check whether the non-matrix-enabled contacts must be ignored for (NSUInteger index = 0; index < unfilteredLocalContacts.count;) { MXKContact* contact = unfilteredLocalContacts[index]; NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { if (_ignoredContactsByMatrixId[identifiers.firstObject]) { [unfilteredLocalContacts removeObjectAtIndex:index]; continue; } } else if (hideNonMatrixEnabledContacts) { // Ignore non-matrix-enabled contact [unfilteredLocalContacts removeObjectAtIndex:index]; continue; } else { NSArray *emails = contact.emailAddresses; if (emails.count) { // Here the contact has only one email address. MXKEmail *email = emails.firstObject; // Trick: ignore @facebook.com email addresses from the results - facebook have discontinued that service... if (_ignoredContactsByEmail[email.emailAddress] || [email.emailAddress hasSuffix:@"@facebook.com"]) { [unfilteredLocalContacts removeObjectAtIndex:index]; continue; } } else { // The contact has here a phone number. // Ignore this contact if the phone number is not linked to a matrix id because the invitation by SMS is not supported yet. MXKPhoneNumber *phoneNumber = contact.phoneNumbers.firstObject; if (!phoneNumber.matrixID) { [unfilteredLocalContacts removeObjectAtIndex:index]; continue; } } } index++; } return unfilteredLocalContacts; } - (NSMutableArray*)unfilteredMatrixContactsArray { NSArray *matrixContacts = [MXKContactManager sharedManager].matrixContacts; NSMutableArray *unfilteredMatrixContacts = [NSMutableArray arrayWithCapacity:matrixContacts.count]; // Matrix ids: split contacts with several ids, and remove the current participants. for (MXKContact* contact in matrixContacts) { NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count > 1) { for (NSString *userId in identifiers) { if (_ignoredContactsByMatrixId[userId] == nil) { MXKContact *splitContact = [[MXKContact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; [unfilteredMatrixContacts addObject:splitContact]; } } } else if (identifiers.count) { NSString *userId = identifiers.firstObject; if (_ignoredContactsByMatrixId[userId] == nil) { [unfilteredMatrixContacts addObject:contact]; } } } return unfilteredMatrixContacts; } #pragma mark - UITableView data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { NSInteger count = 0; searchInputSection = filteredLocalContactsSection = filteredMatrixContactsSection = -1; if (currentSearchText.length) { if (_displaySearchInputInContactsList) { searchInputSection = count++; } // Keep visible the header for the both contact sections, even if they're are empty. if (BWIBuildSettings.shared.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } filteredMatrixContactsSection = count++; } else { // Display by default the full address book ordered alphabetically, mixing Matrix enabled and non-Matrix enabled users. if (!filteredLocalContacts) { filteredLocalContacts = [self unfilteredLocalContactsArray]; } // Keep visible the local contact header, even if the section is empty. if (BWIBuildSettings.shared.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } } return count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger count = 0; if (section == searchInputSection) { count = RiotSettings.shared.allowInviteExernalUsers ? 1 : 0; } else if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) { // Display a default cell when no local contacts is available. count = filteredLocalContacts.count ? filteredLocalContacts.count : 1; } else if (section == filteredMatrixContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_USERDIRECTORY_BITWISE)) { // Display a default cell when no contacts is available. count = filteredMatrixContacts.count ? filteredMatrixContacts.count : 1; } return count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // Prepare a contact cell here MXKContact *contact; BOOL showMatrixIdInDisplayName = NO; if (indexPath.section == searchInputSection) { // Show what the user is typing in a cell. So that he can click on it contact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; } else if (indexPath.section == filteredLocalContactsSection) { if (indexPath.row < filteredLocalContacts.count) { contact = filteredLocalContacts[indexPath.row]; showMatrixIdInDisplayName = YES; } } else if (indexPath.section == filteredMatrixContactsSection) { if (indexPath.row < filteredMatrixContacts.count) { contact = filteredMatrixContacts[indexPath.row]; showMatrixIdInDisplayName = self.forceMatrixIdInDisplayName ? YES : [isMultiUseNameByDisplayName[contact.displayName] isEqualToNumber:@(YES)]; } } if (contact) { ContactTableViewCell *contactCell = [tableView dequeueReusableCellWithIdentifier:[ContactTableViewCell defaultReuseIdentifier]]; if (!contactCell) { contactCell = [[ContactTableViewCell alloc] init]; } // BWI: 5208 if (BWIBuildSettings.shared.isFederationEnabled && self.mxSession && self.mxSession.myUser) { NSArray *myUserIdComponents = [self.mxSession.myUserId componentsSeparatedByString:@":"]; if (myUserIdComponents.count == 2) { [contact checkFederation:[myUserIdComponents objectAtIndex:1]]; } } // Make the cell display the contact [contactCell render:contact]; contactCell.selectionStyle = UITableViewCellSelectionStyleDefault; if([BWIBuildSettings.shared showContactIdentifierInDetailRow]) { contactCell.showMatrixIdInDisplayName = false; } else { contactCell.showMatrixIdInDisplayName = showMatrixIdInDisplayName; } // The search displays contacts to invite. if (indexPath.section == filteredLocalContactsSection || indexPath.section == filteredMatrixContactsSection) { // Add the right accessory view if any contactCell.accessoryType = self.contactCellAccessoryType; if (self.contactCellAccessoryImage) { contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; } } else if (indexPath.section == searchInputSection) { // This is the text entered by the user // Check whether the search input is a valid email or a Matrix user ID before adding the accessory view. if (![MXTools isEmailAddress:currentSearchText] && ![MXTools isMatrixUserIdentifier:currentSearchText]) { contactCell.contentView.alpha = 0.5; contactCell.userInteractionEnabled = NO; } else { // Add the right accessory view if any contactCell.accessoryType = self.contactCellAccessoryType; if (self.contactCellAccessoryImage) { contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; } } } return contactCell; } else { MXKTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCell defaultReuseIdentifier]]; if (!tableViewCell) { tableViewCell = [[MXKTableViewCell alloc] init]; tableViewCell.textLabel.textColor = ThemeService.shared.theme.textSecondaryColor; tableViewCell.textLabel.font = [UIFont systemFontOfSize:15.0]; tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; } // Check whether a search session is in progress if (currentSearchText.length) { if (indexPath.section == filteredMatrixContactsSection && (_userDirectoryState == ContactsDataSourceUserDirectoryStateLoading || _userDirectoryState == ContactsDataSourceUserDirectoryStateOfflineLoading)) { tableViewCell.textLabel.text = [VectorL10n searchSearching]; } else { tableViewCell.textLabel.text = [VectorL10n searchNoResult]; } } else if (indexPath.section == filteredLocalContactsSection) { tableViewCell.textLabel.numberOfLines = 0; // Indicate to the user why there is no contacts switch ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts]) { case CNAuthorizationStatusAuthorized: if (hideNonMatrixEnabledContacts && !self.mxSession.identityService) { // Because we cannot make lookups with no IS tableViewCell.textLabel.text = [VectorL10n contactsAddressBookNoIdentityServer]; } else { // Because there is no contacts on the device tableViewCell.textLabel.text = [VectorL10n contactsAddressBookNoContact]; } break; case CNAuthorizationStatusNotDetermined: // Because the user have not granted the permission yet // (The permission request popup is displayed at the same time) tableViewCell.textLabel.text = [VectorL10n contactsAddressBookPermissionRequired]; break; default: { // Because the user didn't allow the app to access local contacts tableViewCell.textLabel.text = [VectorL10n contactsAddressBookPermissionDenied:AppInfo.current.displayName]; break; } } } return tableViewCell; } } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } #pragma mark - -(MXKContact *)contactAtIndexPath:(NSIndexPath*)indexPath { NSInteger row = indexPath.row; MXKContact *mxkContact; if (indexPath.section == searchInputSection) { mxkContact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; } else if (indexPath.section == filteredLocalContactsSection && row < filteredLocalContacts.count) { mxkContact = filteredLocalContacts[row]; } else if (indexPath.section == filteredMatrixContactsSection && row < filteredMatrixContacts.count) { mxkContact = filteredMatrixContacts[row]; } return mxkContact; } - (NSIndexPath*)cellIndexPathWithContact:(MXKContact*)contact { NSIndexPath *indexPath = nil; NSUInteger index = [filteredLocalContacts indexOfObject:contact]; if (index != NSNotFound) { // if local section is collapsed there is no cell if (!(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) { indexPath = [NSIndexPath indexPathForRow:index inSection:filteredLocalContactsSection]; } } else { index = [filteredMatrixContacts indexOfObject:contact]; // if matrix section is collapsed or we are not showing the matrix section(as with empty query) there is no cell if (index != NSNotFound && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_USERDIRECTORY_BITWISE) && filteredMatrixContactsSection != -1) { indexPath = [NSIndexPath indexPathForRow:index inSection:filteredMatrixContactsSection]; } } return indexPath; } - (CGFloat)heightForHeaderInSection:(NSInteger)section { if (section == filteredLocalContactsSection || section == filteredMatrixContactsSection) { if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) { return CONTACTSDATASOURCE_LOCALCONTACTS_SECTION_HEADER_HEIGHT; } return CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; } return 0; } - (NSAttributedString *)attributedStringForHeaderTitleInSection:(NSInteger)section { NSAttributedString *sectionTitle; NSString* title; NSUInteger count = 0; if (section == filteredLocalContactsSection) { count = filteredLocalContacts.count; title = [VectorL10n contactsAddressBookSection]; } else //if (section == filteredMatrixContactsSection) { switch (_userDirectoryState) { case ContactsDataSourceUserDirectoryStateOfflineLoading: case ContactsDataSourceUserDirectoryStateOfflineLoaded: title = [VectorL10n contactsUserDirectoryOfflineSection]; break; default: title = [VectorL10n contactsUserDirectorySection]; break; } if (currentSearchText.length) { count = filteredMatrixContacts.count; } } if (count) { NSString *roomCountFormat = (_userDirectoryState == ContactsDataSourceUserDirectoryStateLoadedButLimited) ? @" > %tu" : @" %tu"; NSString *roomCount = [NSString stringWithFormat:roomCountFormat, count]; NSMutableAttributedString *mutableSectionTitle = [[NSMutableAttributedString alloc] initWithString:title attributes:@{NSForegroundColorAttributeName : ThemeService.shared.theme.headerTextPrimaryColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; [mutableSectionTitle appendAttributedString:[[NSMutableAttributedString alloc] initWithString:roomCount attributes:@{NSForegroundColorAttributeName : ThemeService.shared.theme.headerTextSecondaryColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]]; sectionTitle = mutableSectionTitle; } else if (title) { sectionTitle = [[NSAttributedString alloc] initWithString:title attributes:@{NSForegroundColorAttributeName : ThemeService.shared.theme.headerTextPrimaryColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; } return sectionTitle; } - (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame inTableView:(UITableView *)tableView { NSInteger sectionBitwise = 0; SectionHeaderView *sectionHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:SectionHeaderView.defaultReuseIdentifier]; if (sectionHeader == nil) { sectionHeader = [[SectionHeaderView alloc] initWithReuseIdentifier:SectionHeaderView.defaultReuseIdentifier]; } sectionHeader.frame = frame; sectionHeader.backgroundView = [UIView new]; sectionHeader.backgroundView.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; sectionHeader.topViewHeight = CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; frame.size.height = CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT - 10; UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame]; headerLabel.attributedText = [self attributedStringForHeaderTitleInSection:section]; headerLabel.backgroundColor = [UIColor clearColor]; sectionHeader.headerLabel = headerLabel; if (_areSectionsShrinkable) { if (section == filteredLocalContactsSection) { sectionBitwise = CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE; } else //if (section == filteredMatrixContactsSection) { if (currentSearchText.length) { // This section is collapsable only if it is not empty if (filteredMatrixContacts.count) { sectionBitwise = CONTACTSDATASOURCE_USERDIRECTORY_BITWISE; } } } } if (sectionBitwise) { // Add shrink button UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom]; shrinkButton.backgroundColor = [UIColor clearColor]; [shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; shrinkButton.tag = sectionBitwise; sectionHeader.topSpanningView = shrinkButton; sectionHeader.userInteractionEnabled = YES; // Add shrink icon UIImage *chevron; if (shrinkedSectionsBitMask & sectionBitwise) { chevron = AssetImages.disclosureIcon.image; } else { chevron = AssetImages.shrinkIcon.image; } UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron]; chevronView.tintColor = ThemeService.shared.theme.textSecondaryColor; chevronView.contentMode = UIViewContentModeCenter; sectionHeader.accessoryView = chevronView; } if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) { if (!localContactsCheckboxContainer) { localContactsCheckboxContainer = [[LocalContactsSectionHeaderContainerView alloc] initWithFrame:CGRectZero]; localContactsCheckboxContainer.backgroundColor = [UIColor clearColor]; // Add Checkbox and Label localContactsCheckbox = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 22, 22)]; [localContactsCheckboxContainer addSubview:localContactsCheckbox]; localContactsCheckboxContainer.checkboxView = localContactsCheckbox; checkboxLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 30)]; checkboxLabel.font = [UIFont systemFontOfSize:16.0]; checkboxLabel.text = [VectorL10n contactsAddressBookMatrixUsersToggle]; [localContactsCheckboxContainer addSubview:checkboxLabel]; localContactsCheckboxContainer.checkboxLabel = checkboxLabel; UIView *checkboxMask = [[UIView alloc] initWithFrame:CGRectZero]; checkboxMask.translatesAutoresizingMaskIntoConstraints = NO; [localContactsCheckboxContainer addSubview:checkboxMask]; localContactsCheckboxContainer.maskView = checkboxMask; // Listen to check box tap checkboxMask.userInteractionEnabled = YES; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCheckBoxTap:)]; [tapGesture setNumberOfTouchesRequired:1]; [tapGesture setNumberOfTapsRequired:1]; [tapGesture setDelegate:self]; [checkboxMask addGestureRecognizer:tapGesture]; } // Apply UI theme checkboxLabel.textColor = ThemeService.shared.theme.textPrimaryColor; // Set the right value of the tick box localContactsCheckbox.image = hideNonMatrixEnabledContacts ? AssetImages.selectionTick.image : AssetImages.selectionUntick.image; localContactsCheckbox.tintColor = ThemeService.shared.theme.tintColor; // Add the check box container sectionHeader.bottomView = localContactsCheckboxContainer; } return sectionHeader; } - (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame inTableView:(UITableView *)tableView { // Return the section header used when the section is shrinked NSInteger savedShrinkedSectionsBitMask = shrinkedSectionsBitMask; shrinkedSectionsBitMask = CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE | CONTACTSDATASOURCE_USERDIRECTORY_BITWISE; UIView *stickyHeader = [self viewForHeaderInSection:section withFrame:frame inTableView:tableView]; shrinkedSectionsBitMask = savedShrinkedSectionsBitMask; return stickyHeader; } #pragma mark - Action - (IBAction)onButtonPressed:(id)sender { if ([sender isKindOfClass:[UIButton class]]) { UIButton *shrinkButton = (UIButton*)sender; NSInteger selectedSectionBit = shrinkButton.tag; if (shrinkedSectionsBitMask & selectedSectionBit) { // Disclose the section shrinkedSectionsBitMask &= ~selectedSectionBit; } else { // Shrink this section shrinkedSectionsBitMask |= selectedSectionBit; } // Inform the delegate about the update [self.delegate dataSource:self didCellChange:nil]; } } #pragma mark - Action - (IBAction)onCheckBoxTap:(UITapGestureRecognizer*)sender { // Update local contacts filter hideNonMatrixEnabledContacts = !hideNonMatrixEnabledContacts; // Check whether a search is in progress if (searchProcessingCount) { forceSearchResultRefresh = YES; return; } // Refresh the search result if (hideNonMatrixEnabledContacts) { // Remove the non-matrix-enabled contacts from the current filtered local contacts for (NSUInteger index = 0; index < filteredLocalContacts.count;) { MXKContact* contact = filteredLocalContacts[index]; NSArray *identifiers = contact.matrixIdentifiers; if (!identifiers.count) { [filteredLocalContacts removeObjectAtIndex:index]; continue; } index++; } // Refresh display [self.delegate dataSource:self didCellChange:nil]; } else { // Refresh the search result by launching a new search session. [self searchWithPattern:currentSearchText forceReset:YES]; } } @end