element-ios/Riot/Modules/MatrixKit/Controllers/MXKRecentListViewController.m

648 lines
20 KiB
Objective-C

/*
Copyright 2015 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 "MXKRecentListViewController.h"
#import "MXKRoomDataSourceManager.h"
#import "MXKInterleavedRecentsDataSource.h"
#import "MXKInterleavedRecentTableViewCell.h"
#import "MXKSwiftHeader.h"
@interface MXKRecentListViewController ()
{
/**
The data source providing UITableViewCells
*/
MXKRecentsDataSource *dataSource;
/**
Search handling
*/
UIBarButtonItem *searchButton;
BOOL ignoreSearchRequest;
/**
The reconnection animated view.
*/
__weak UIView* reconnectingView;
/**
The current table view header if any.
*/
UIView* tableViewHeaderView;
/**
The latest server sync date
*/
NSDate* latestServerSync;
/**
The restart the event connnection
*/
BOOL restartConnection;
}
@end
@implementation MXKRecentListViewController
@synthesize dataSource;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKRecentListViewController class])
bundle:[NSBundle bundleForClass:[MXKRecentListViewController class]]];
}
+ (instancetype)recentListViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRecentListViewController class])
bundle:[NSBundle bundleForClass:[MXKRecentListViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
_recentsUpdateEnabled = YES;
_enableBarButtonSearch = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_recentsTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
// Adjust search bar Top constraint to take into account potential navBar.
if (_recentsSearchBarTopConstraint)
{
_recentsSearchBarTopConstraint.active = NO;
_recentsSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.recentsSearchBar
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
_recentsSearchBarTopConstraint.active = YES;
}
// Adjust table view Bottom constraint to take into account tabBar.
if (_recentsTableViewBottomConstraint)
{
_recentsTableViewBottomConstraint.active = NO;
_recentsTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.recentsTableView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
_recentsTableViewBottomConstraint.active = YES;
}
#pragma clang diagnostic pop
// Hide search bar by default
[self hideSearchBar:YES];
// Apply search option in navigation bar
self.enableBarButtonSearch = _enableBarButtonSearch;
// Add an accessory view to the search bar in order to retrieve keyboard view.
self.recentsSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
// Finalize table view configuration
self.recentsTableView.delegate = self;
self.recentsTableView.dataSource = dataSource; // Note: dataSource may be nil here
// Set up classes to use for cells
[self.recentsTableView registerNib:MXKRecentTableViewCell.nib forCellReuseIdentifier:MXKRecentTableViewCell.defaultReuseIdentifier];
// Consider here the specific case where interleaved recents are supported
[self.recentsTableView registerNib:MXKInterleavedRecentTableViewCell.nib forCellReuseIdentifier:MXKInterleavedRecentTableViewCell.defaultReuseIdentifier];
// Add a top view which will be displayed in case of vertical bounce.
CGFloat height = self.recentsTableView.frame.size.height;
UIView *topview = [[UIView alloc] initWithFrame:CGRectMake(0,-height,self.recentsTableView.frame.size.width,height)];
topview.autoresizingMask = UIViewAutoresizingFlexibleWidth;
topview.backgroundColor = [UIColor groupTableViewBackgroundColor];
[self.recentsTableView addSubview:topview];
self->topview = topview;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
// Observe server sync at room data source level too
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionChange) name:kMXKRoomDataSourceSyncStatusChanged object:nil];
// Observe the server sync
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil];
self.recentsUpdateEnabled = YES;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil];
[self removeReconnectingView];
}
- (void)dealloc
{
self.recentsSearchBar.inputAccessoryView = nil;
searchButton = nil;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Override MXKViewController
- (void)onMatrixSessionChange
{
[super onMatrixSessionChange];
// Check whether no server sync is in progress in room data sources
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
if ([MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession].isServerSyncInProgress)
{
// sync is in progress for at least one data source, keep running the loading wheel
[self startActivityIndicator];
break;
}
}
}
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
self.keyboardView = _recentsSearchBar.inputAccessoryView.superview;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (tableViewBottomConst < 0)
{
tableViewBottomConst = 0;
}
// Update constraints
_recentsTableViewBottomConstraint.constant = tableViewBottomConst;
// Force layout immediately to take into account new constraint
[self.view layoutIfNeeded];
}
#pragma clang diagnostic pop
- (void)destroy
{
self.recentsTableView.dataSource = nil;
self.recentsTableView.delegate = nil;
self.recentsTableView = nil;
dataSource.delegate = nil;
dataSource = nil;
_delegate = nil;
[topview removeFromSuperview];
topview = nil;
[super destroy];
}
#pragma mark -
- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
_enableBarButtonSearch = enableBarButtonSearch;
if (enableBarButtonSearch)
{
if (!searchButton)
{
searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
}
// Add it in right bar items
NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems;
self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:searchButton] : @[searchButton];
}
else
{
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems];
[rightBarButtonItems removeObject:searchButton];
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}
}
- (void)displayList:(MXKRecentsDataSource *)listDataSource
{
// Cancel registration on existing dataSource if any
if (dataSource)
{
dataSource.delegate = nil;
// Remove associated matrix sessions
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
[self removeMatrixSession:mxSession];
}
}
dataSource = listDataSource;
dataSource.delegate = self;
// Report all matrix sessions at view controller level to update UI according to sessions state
NSArray *mxSessions = listDataSource.mxSessions;
for (MXSession *mxSession in mxSessions)
{
[self addMatrixSession:mxSession];
}
if (self.recentsTableView)
{
// Set up table data source
self.recentsTableView.dataSource = dataSource;
}
}
- (void)refreshRecentsTable
{
if (!self.recentsUpdateEnabled) return;
isRefreshNeeded = NO;
// For now, do a simple full reload
[self.recentsTableView reloadData];
}
- (void)hideSearchBar:(BOOL)hidden
{
self.recentsSearchBar.hidden = hidden;
self.recentsSearchBarHeightConstraint.constant = hidden ? 0 : 44;
[self.view setNeedsUpdateConstraints];
}
- (void)setRecentsUpdateEnabled:(BOOL)activeUpdate
{
_recentsUpdateEnabled = activeUpdate;
if (_recentsUpdateEnabled && isRefreshNeeded)
{
[self refreshRecentsTable];
}
}
#pragma mark - Action
- (IBAction)search:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (self.recentsSearchBar.isHidden)
{
// Check whether there are data in which search
if ([self.dataSource numberOfSectionsInTableView:self.recentsTableView])
{
[self hideSearchBar:NO];
// Create search bar
[self.recentsSearchBar becomeFirstResponder];
}
}
else
{
[self searchBarCancelButtonClicked: self.recentsSearchBar];
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Consider here the specific case where interleaved recents are supported
if ([dataSource isKindOfClass:MXKInterleavedRecentsDataSource.class])
{
return MXKInterleavedRecentTableViewCell.class;
}
// Return the default recent table view cell
return MXKRecentTableViewCell.class;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Consider here the specific case where interleaved recents are supported
if ([dataSource isKindOfClass:MXKInterleavedRecentsDataSource.class])
{
return MXKInterleavedRecentTableViewCell.defaultReuseIdentifier;
}
// Return the default recent table view cell
return MXKRecentTableViewCell.defaultReuseIdentifier;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
if (!_recentsUpdateEnabled)
{
isRefreshNeeded = YES;
return;
}
// For now, do a simple full reload
[self refreshRecentsTable];
}
- (void)dataSource:(MXKDataSource *)dataSource didAddMatrixSession:(MXSession *)mxSession
{
[self addMatrixSession:mxSession];
}
- (void)dataSource:(MXKDataSource *)dataSource didRemoveMatrixSession:(MXSession *)mxSession
{
[self removeMatrixSession:mxSession];
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [dataSource cellHeightAtIndexPath:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// Section header is required only when several recent lists are displayed.
if (self.dataSource.displayedRecentsDataSourcesCount > 1)
{
return 35;
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// Let dataSource provide the section header.
return [dataSource viewForHeaderInSection:section
withFrame:[tableView rectForHeaderInSection:section]
inTableView:tableView];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_delegate)
{
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
if ([selectedCell conformsToProtocol:@protocol(MXKCellRendering)])
{
id<MXKCellRendering> cell = (id<MXKCellRendering>)selectedCell;
if ([cell respondsToSelector:@selector(renderedCellData)])
{
MXKCellData *cellData = cell.renderedCellData;
if ([cellData conformsToProtocol:@protocol(MXKRecentCellDataStoring)])
{
id<MXKRecentCellDataStoring> recentCellData = (id<MXKRecentCellDataStoring>)cellData;
if (recentCellData.isSuggestedRoom)
{
[_delegate recentListViewController:self
didSelectSuggestedRoom:recentCellData.roomSummary.spaceChildInfo
from:selectedCell];
}
else
{
[_delegate recentListViewController:self
didSelectRoom:recentCellData.roomIdentifier
inMatrixSession:recentCellData.mxSession];
}
}
}
}
}
// Hide the keyboard when user select a room
// do not hide the searchBar until the view controller disappear
// on tablets / iphone 6+, the user could expect to search again while looking at a room
[self.recentsSearchBar resignFirstResponder];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Detect vertical bounce at the top of the tableview to trigger reconnection.
if (scrollView == _recentsTableView)
{
[self detectPullToKick:scrollView];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == _recentsTableView)
{
[self managePullToKick:scrollView];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == _recentsTableView)
{
if (scrollView.contentOffset.y + scrollView.adjustedContentInset.top == 0)
{
[self managePullToKick:scrollView];
}
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
// Apply filter
if (searchText.length)
{
[self.dataSource searchWithPatterns:@[searchText]];
}
else
{
[self.dataSource searchWithPatterns:nil];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
// Leave search
[searchBar resignFirstResponder];
[self hideSearchBar:YES];
self.recentsSearchBar.text = nil;
// Refresh display
[self.dataSource searchWithPatterns:nil];
}
#pragma mark - resync management
- (void)onSyncNotification
{
latestServerSync = [NSDate date];
[self removeReconnectingView];
}
- (BOOL)canReconnect
{
// avoid restarting connection if some data has been received within 1 second (1000 : latestServerSync is null)
NSTimeInterval interval = latestServerSync ? [[NSDate date] timeIntervalSinceDate:latestServerSync] : 1000;
return (interval > 1) && [self.mainSession reconnect];
}
- (void)addReconnectingView
{
if (!reconnectingView)
{
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f);
CGRect frame = spinner.frame;
frame.size.height = 80; // 80 * 0.75 = 60
spinner.bounds = frame;
spinner.color = [UIColor darkGrayColor];
spinner.hidesWhenStopped = NO;
spinner.backgroundColor = _recentsTableView.backgroundColor;
[spinner startAnimating];
// no need to manage constraints here, IOS defines them.
tableViewHeaderView = _recentsTableView.tableHeaderView;
_recentsTableView.tableHeaderView = reconnectingView = spinner;
}
}
- (void)removeReconnectingView
{
if (reconnectingView && !restartConnection)
{
_recentsTableView.tableHeaderView = tableViewHeaderView;
reconnectingView = nil;
}
}
/**
Detect if the current connection must be restarted.
The spinner is displayed until the overscroll ends (and scrollViewDidEndDecelerating is called).
*/
- (void)detectPullToKick:(UIScrollView *)scrollView
{
if (!reconnectingView)
{
// detect if the user scrolls over the tableview top
restartConnection = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top < -128);
if (restartConnection)
{
// wait that list decelerate to display / hide it
[self addReconnectingView];
}
}
}
/**
Restarts the current connection if it is required.
The 0.3s delay is added to avoid flickering if the connection does not require to be restarted.
*/
- (void)managePullToKick:(UIScrollView *)scrollView
{
// the current connection must be restarted
if (restartConnection)
{
// display at least 0.3s the spinner to show to the user that something is pending
// else the UI is flickering
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self->restartConnection = NO;
if (![self canReconnect])
{
// if the event stream has not been restarted
// hide the spinner
[self removeReconnectingView];
}
// else wait that onSyncNotification is called.
});
}
}
@end