element-ios/Riot/Modules/Contacts/ContactsTableViewController.m
Doug b475afc455 Move RequestContactsAccessFooterView into ContactsTableViewController.
Remove automatic triggering of contacts access.
2021-08-05 17:34:09 +01:00

543 lines
18 KiB
Objective-C

/*
Copyright 2017 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Contacts/Contacts.h>
#import "ContactsTableViewController.h"
#import "UIViewController+RiotSearch.h"
#import "Riot-Swift.h"
#define CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE 0x01
#define CONTACTS_TABLEVC_USERDIRECTORY_BITWISE 0x02
#define CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT 30.0
#define CONTACTS_TABLEVC_LOCALCONTACTS_SECTION_HEADER_HEIGHT 65.0
@interface ContactsTableViewController () <RequestContactsAccessFooterViewDelegate>
{
/**
Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar.
*/
id kAppDelegateDidTapStatusBarNotificationObserver;
/**
Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
*/
id kThemeServiceDidChangeThemeNotificationObserver;
}
@property (nonatomic, strong) RequestContactsAccessFooterView *requestContactsAccessFooterView;
@property (nonatomic) BOOL shouldHideFooterView;
@end
@implementation ContactsTableViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([ContactsTableViewController class])
bundle:[NSBundle bundleForClass:[ContactsTableViewController class]]];
}
+ (instancetype)contactsTableViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([ContactsTableViewController class])
bundle:[NSBundle bundleForClass:[ContactsTableViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
// Setup `MXKViewControllerHandling` properties
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
_screenName = @"ContactsTable";
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Check whether the view controller has been pushed via storyboard
if (!self.contactsTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// Finalize table view configuration
self.contactsTableView.delegate = self;
self.contactsTableView.dataSource = contactsDataSource; // Note: dataSource may be nil here
[self.contactsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:ContactTableViewCell.defaultReuseIdentifier];
// Hide line separators of empty cells
self.contactsTableView.tableFooterView = [[UIView alloc] init];
self.shouldHideFooterView = NO;
// Observe user interface theme change.
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self userInterfaceThemeDidChange];
}];
[self userInterfaceThemeDidChange];
}
- (void)userInterfaceThemeDidChange
{
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
// Check the table view style to select its bg color.
self.contactsTableView.backgroundColor = ((self.contactsTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor);
self.view.backgroundColor = self.contactsTableView.backgroundColor;
self.contactsTableView.separatorColor = ThemeService.shared.theme.lineBreakColor;
if (self.contactsTableView.dataSource)
{
[self refreshContactsTable];
}
[self setNeedsStatusBarAppearanceUpdate];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return ThemeService.shared.theme.statusBarStyle;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)destroy
{
[super destroy];
if (kThemeServiceDidChangeThemeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver];
kThemeServiceDidChangeThemeNotificationObserver = nil;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Screen tracking
[[Analytics sharedInstance] trackScreen:_screenName];
// Observe kAppDelegateDidTapStatusBarNotification.
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:YES];
}];
[self refreshContactsTable];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Load the local contacts for display.
[self refreshLocalContacts];
[self updateFooterView];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self updateRequestContactsAccessFooterViewHeight];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (kAppDelegateDidTapStatusBarNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver];
kAppDelegateDidTapStatusBarNotificationObserver = nil;
}
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
}
}
#pragma mark -
- (RequestContactsAccessFooterView*)makeFooterView
{
RequestContactsAccessFooterView *footerView = [RequestContactsAccessFooterView instantiate];
footerView.delegate = self;
self.requestContactsAccessFooterView = footerView;
return footerView;
}
- (void)updateFooterView
{
if (!RiotSettings.shared.allowInviteExernalUsers || self->contactsDataSource.hasLocalContacts)
{
self.contactsTableView.tableFooterView = nil;
return;
}
if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized
&& MXKAppSettings.standardAppSettings.syncLocalContacts
&& contactsDataSource.mxSession.hasAccountDataIdentityServerValue)
{
self.contactsTableView.tableFooterView = nil;
return;
}
if (self.shouldHideFooterView)
{
self.contactsTableView.tableFooterView = nil;
return;
}
self.contactsTableView.tableFooterView = self.requestContactsAccessFooterView ?: [self makeFooterView];
[self updateRequestContactsAccessFooterViewHeight];
}
- (void)updateRequestContactsAccessFooterViewHeight
{
if (self.requestContactsAccessFooterView && self.requestContactsAccessFooterView == self.contactsTableView.tableFooterView)
{
CGSize footerSize = [self.requestContactsAccessFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
CGFloat gapHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom;
if (self.contactsTableView.tableHeaderView)
{
gapHeight -= self.contactsTableView.tableHeaderView.frame.size.height;
}
if (gapHeight > footerSize.height)
{
self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x,
self.requestContactsAccessFooterView.frame.origin.y,
self.requestContactsAccessFooterView.frame.size.width,
gapHeight);
}
else
{
self.requestContactsAccessFooterView.frame = CGRectMake(self.requestContactsAccessFooterView.frame.origin.x,
self.requestContactsAccessFooterView.frame.origin.y,
self.requestContactsAccessFooterView.frame.size.width,
footerSize.height);
}
}
}
- (void)displayList:(ContactsDataSource*)listDataSource
{
// Cancel registration on existing dataSource if any
if (contactsDataSource)
{
contactsDataSource.delegate = nil;
}
contactsDataSource = listDataSource;
contactsDataSource.delegate = self;
if (self.contactsTableView)
{
// Set up table data source
self.contactsTableView.dataSource = contactsDataSource;
}
}
- (void)refreshLocalContacts
{
if (!BuildSettings.allowLocalContactsAccess)
{
return;
}
// Check whether the user has not decided yet about using an identity server
// Check whether the application is allowed to access the local contacts.
if (contactsDataSource.mxSession.hasAccountDataIdentityServerValue
&& [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized)
{
// If the user hasn't enabled local contact sync in the app...
if (![MXKAppSettings standardAppSettings].syncLocalContacts)
{
// ... Check whether they have been directed to the Settings app to enable contact access.
if ([MXKAppSettings standardAppSettings].syncLocalContactsPermissionOpenedSystemSettings)
{
// If they have enable local contact sync and reset the system settings app flag.
[MXKAppSettings standardAppSettings].syncLocalContacts = YES;
[MXKAppSettings standardAppSettings].syncLocalContactsPermissionOpenedSystemSettings = NO;
}
else
{
// Otherwise local contact sync is disabled so we're done.
return;
}
}
// Refresh the local contacts list.
[[MXKContactManager sharedManager] refreshLocalContacts];
}
}
- (void)refreshContactsTable
{
[self.contactsTableView reloadData];
if (_shouldScrollToTopOnRefresh)
{
[self scrollToTop:NO];
_shouldScrollToTopOnRefresh = NO;
}
// In case of split view controller where the primary and secondary view controllers are displayed side-by-side on screen,
// the selected room (if any) is updated and kept visible.
if (self.splitViewController && !self.splitViewController.isCollapsed)
{
[self refreshCurrentSelectedCell:YES];
}
}
- (void)refreshCurrentSelectedCell:(BOOL)forceVisible
{
// Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller.
NSIndexPath *currentSelectedCellIndexPath = nil;
MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController;
if (masterTabBarController.currentContactDetailViewController)
{
// Look for the rank of this selected contact in displayed recents
currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact];
}
if (currentSelectedCellIndexPath)
{
// Select the right row
[self.contactsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
if (forceVisible)
{
// Scroll table view to make the selected row appear at second position
NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row;
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section];
[self.contactsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}
else
{
NSIndexPath *indexPath = [self.contactsTableView indexPathForSelectedRow];
if (indexPath)
{
[self.contactsTableView deselectRowAtIndexPath:indexPath animated:NO];
}
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
if ([cellData isKindOfClass:MXKContact.class])
{
return ContactTableViewCell.class;
}
return nil;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
if ([cellData isKindOfClass:MXKContact.class])
{
return [ContactTableViewCell defaultReuseIdentifier];
}
return nil;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
[self refreshContactsTable];
}
#pragma mark - Internal methods
- (void)scrollToTop:(BOOL)animated
{
// Scroll to the top
[self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:animated];
}
#pragma mark - UITableView delegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
{
cell.backgroundColor = ThemeService.shared.theme.backgroundColor;
// Update the selected background view
if (ThemeService.shared.theme.selectedBackgroundColor)
{
cell.selectedBackgroundView = [[UIView alloc] init];
cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor;
}
else
{
if (tableView.style == UITableViewStylePlain)
{
cell.selectedBackgroundView = nil;
}
else
{
cell.selectedBackgroundView.backgroundColor = nil;
}
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return [contactsDataSource heightForHeaderInSection:section];
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return [contactsDataSource viewForHeaderInSection:section withFrame:[tableView rectForHeaderInSection:section]];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([contactsDataSource contactAtIndexPath:indexPath])
{
// Return the default height of the contact cell
return 74.0;
}
return 50;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.contactsTableViewControllerDelegate)
{
MXKContact *mxkContact = [contactsDataSource contactAtIndexPath:indexPath];
if (mxkContact)
{
[self.contactsTableViewControllerDelegate contactsTableViewController:self didSelectContact:mxkContact];
// Keep selected the cell by default.
return;
}
}
// Else do nothing by default - `ContactsTableViewController-inherited` instance must override this method.
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - UISearchBar delegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[contactsDataSource searchWithPattern:searchText forceReset:NO];
// FIXME: This should be based off of the data source as it doesn't work in StartChat.
if (searchText.length && self.contactsTableView.tableFooterView)
{
self.shouldHideFooterView = YES;
[self updateFooterView];
}
else if (!searchText.length && !self.contactsTableView.tableFooterView)
{
self.shouldHideFooterView = NO;
[self updateFooterView];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed.
if (self.contactsTableViewControllerDelegate)
{
// Check whether the current search input is a valid email or a Matrix user ID
MXKContact* filedContact = [contactsDataSource searchInputContact];
if (filedContact)
{
// Select the contact related to the search input, rather than having to hit +
[self.contactsTableViewControllerDelegate contactsTableViewController:self didSelectContact:filedContact];
}
}
// Dismiss keyboard
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
searchBar.text = nil;
// Reset filtering
[contactsDataSource searchWithPattern:nil forceReset:NO];
// Leave search
[searchBar resignFirstResponder];
[self withdrawViewControllerAnimated:YES completion:nil];
}
#pragma mark - RequestContactsAccessFooterViewDelegate
- (void)didRequestContactsAccess
{
[MXKTools checkAccessForContacts:@"Contacts disabled"
withManualChangeMessage:@"To enable contacts, go to your device settings."
showPopUpInViewController:self
completionHandler:^(BOOL granted) {
if (granted)
{
// Hide the request access view.
[self updateFooterView];
// Enable sync local contacts and refresh the contacts manager.
MXKAppSettings.standardAppSettings.syncLocalContacts = YES;
[self refreshLocalContacts];
}
}];
}
@end