978 lines
36 KiB
Objective-C
978 lines
36 KiB
Objective-C
/*
|
|
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 <Contacts/Contacts.h>
|
|
#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<MXKContact*> *searchProcessingLocalContacts;
|
|
NSMutableArray<MXKContact*> *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 <NSString*,NSNumber*> *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<MXKContact*> *unfilteredLocalContacts;
|
|
NSMutableArray<MXKContact*> *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];
|
|
|
|
// Keep the response order as the hs ordered users by relevance
|
|
for (MXUser *mxUser in userSearchResponse.results)
|
|
{
|
|
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<MXKContact*>*)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<MXKContact*>*)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 (BuildSettings.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 (BuildSettings.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];
|
|
}
|
|
|
|
// Make the cell display the contact
|
|
[contactCell render:contact];
|
|
|
|
contactCell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
|
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
|