569 lines
19 KiB
Objective-C
569 lines
19 KiB
Objective-C
/*
|
|
Copyright 2018-2024 New Vector Ltd.
|
|
Copyright 2017 Vector Creations Ltd
|
|
Copyright 2015 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
#import "MXKRoomMemberListViewController.h"
|
|
|
|
#import "MXKRoomMemberTableViewCell.h"
|
|
|
|
#import "MXKConstants.h"
|
|
|
|
#import "NSBundle+MatrixKit.h"
|
|
|
|
#import "MXKSwiftHeader.h"
|
|
|
|
@interface MXKRoomMemberListViewController ()
|
|
{
|
|
/**
|
|
The data source providing UITableViewCells
|
|
*/
|
|
MXKRoomMemberListDataSource *dataSource;
|
|
|
|
/**
|
|
Timer used to update members presence
|
|
*/
|
|
NSTimer* presenceUpdateTimer;
|
|
|
|
/**
|
|
Optional bar buttons
|
|
*/
|
|
UIBarButtonItem *searchBarButton;
|
|
UIBarButtonItem *addBarButton;
|
|
|
|
/**
|
|
The current displayed alert (if any).
|
|
*/
|
|
UIAlertController *currentAlert;
|
|
|
|
/**
|
|
Search bar
|
|
*/
|
|
BOOL ignoreSearchRequest;
|
|
|
|
/**
|
|
Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
|
|
*/
|
|
id leaveRoomNotificationObserver;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MXKRoomMemberListViewController
|
|
@synthesize dataSource;
|
|
|
|
#pragma mark - Class methods
|
|
|
|
+ (UINib *)nib
|
|
{
|
|
return [UINib nibWithNibName:NSStringFromClass([MXKRoomMemberListViewController class])
|
|
bundle:[NSBundle bundleForClass:[MXKRoomMemberListViewController class]]];
|
|
}
|
|
|
|
+ (instancetype)roomMemberListViewController
|
|
{
|
|
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRoomMemberListViewController class])
|
|
bundle:[NSBundle bundleForClass:[MXKRoomMemberListViewController class]]];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
|
|
- (void)finalizeInit
|
|
{
|
|
[super finalizeInit];
|
|
|
|
// Enable both bar button by default.
|
|
_enableMemberInvitation = YES;
|
|
_enableMemberSearch = YES;
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
// Check whether the view controller has been pushed via storyboard
|
|
if (!self.membersTableView)
|
|
{
|
|
// Instantiate view controller objects
|
|
[[[self class] nib] instantiateWithOwner:self options:nil];
|
|
}
|
|
|
|
// Adjust Top and Bottom constraints to take into account potential navBar and tabBar.
|
|
[NSLayoutConstraint deactivateConstraints:@[_membersSearchBarTopConstraint, _membersTableViewBottomConstraint]];
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated"
|
|
_membersSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
|
|
attribute:NSLayoutAttributeBottom
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.membersSearchBar
|
|
attribute:NSLayoutAttributeTop
|
|
multiplier:1.0f
|
|
constant:0.0f];
|
|
|
|
_membersTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
|
|
attribute:NSLayoutAttributeTop
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.membersTableView
|
|
attribute:NSLayoutAttributeBottom
|
|
multiplier:1.0f
|
|
constant:0.0f];
|
|
#pragma clang diagnostic pop
|
|
|
|
[NSLayoutConstraint activateConstraints:@[_membersSearchBarTopConstraint, _membersTableViewBottomConstraint]];
|
|
|
|
// Hide search bar by default
|
|
self.membersSearchBar.hidden = YES;
|
|
self.membersSearchBarHeightConstraint.constant = 0;
|
|
[self.view setNeedsUpdateConstraints];
|
|
|
|
searchBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
|
|
addBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(inviteNewMember:)];
|
|
|
|
// Refresh bar button display.
|
|
[self refreshUIBarButtons];
|
|
|
|
// Add an accessory view to the search bar in order to retrieve keyboard view.
|
|
self.membersSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
|
|
|
|
// Finalize table view configuration
|
|
self.membersTableView.delegate = self;
|
|
self.membersTableView.dataSource = dataSource; // Note datasource may be nil here.
|
|
|
|
// Set up default table view cell class
|
|
[self.membersTableView registerNib:MXKRoomMemberTableViewCell.nib forCellReuseIdentifier:MXKRoomMemberTableViewCell.defaultReuseIdentifier];
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
|
|
// Check whether the user still belongs to the room's members.
|
|
if (self.dataSource && [self.mainSession roomWithRoomId:self.dataSource.roomId])
|
|
{
|
|
[self refreshUIBarButtons];
|
|
|
|
// Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
|
|
leaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif)
|
|
{
|
|
|
|
// Check whether the user will leave the room related to the displayed member list
|
|
if (notif.object == self.mainSession)
|
|
{
|
|
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
|
|
if (roomId && [roomId isEqualToString:self.dataSource.roomId])
|
|
{
|
|
// We remove the current view controller.
|
|
[self withdrawViewControllerAnimated:YES completion:nil];
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// We remove the current view controller.
|
|
[self withdrawViewControllerAnimated:YES completion:nil];
|
|
}
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:animated];
|
|
|
|
// Restore search mechanism (if enabled)
|
|
ignoreSearchRequest = NO;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
[super viewWillDisappear:animated];
|
|
|
|
// The user may still press search button whereas the view disappears
|
|
ignoreSearchRequest = YES;
|
|
|
|
if (leaveRoomNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:leaveRoomNotificationObserver];
|
|
leaveRoomNotificationObserver = nil;
|
|
}
|
|
|
|
// Leave potential search session
|
|
if (!self.membersSearchBar.isHidden)
|
|
{
|
|
[self searchBarCancelButtonClicked:self.membersSearchBar];
|
|
}
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
self.membersSearchBar.inputAccessoryView = nil;
|
|
}
|
|
|
|
- (void)didReceiveMemoryWarning
|
|
{
|
|
[super didReceiveMemoryWarning];
|
|
|
|
// Dispose of any resources that can be recreated.
|
|
}
|
|
|
|
#pragma mark - Override MXKTableViewController
|
|
|
|
- (void)onKeyboardShowAnimationComplete
|
|
{
|
|
// Report the keyboard view in order to track keyboard frame changes
|
|
self.keyboardView = _membersSearchBar.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
|
|
_membersTableViewBottomConstraint.constant = tableViewBottomConst;
|
|
|
|
// Force layout immediately to take into account new constraint
|
|
[self.view layoutIfNeeded];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
|
|
- (void)destroy
|
|
{
|
|
if (presenceUpdateTimer)
|
|
{
|
|
[presenceUpdateTimer invalidate];
|
|
presenceUpdateTimer = nil;
|
|
}
|
|
|
|
self.membersTableView.dataSource = nil;
|
|
self.membersTableView.delegate = nil;
|
|
self.membersTableView = nil;
|
|
dataSource.delegate = nil;
|
|
dataSource = nil;
|
|
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
searchBarButton = nil;
|
|
addBarButton = nil;
|
|
|
|
_delegate = nil;
|
|
|
|
[super destroy];
|
|
}
|
|
|
|
#pragma mark - Internal methods
|
|
|
|
- (void)updateMembersActivityInfo
|
|
{
|
|
for (id memberCell in self.membersTableView.visibleCells)
|
|
{
|
|
if ([memberCell respondsToSelector:@selector(updateActivityInfo)])
|
|
{
|
|
[memberCell updateActivityInfo];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - UIBarButton handling
|
|
|
|
- (void)setEnableMemberSearch:(BOOL)enableMemberSearch
|
|
{
|
|
_enableMemberSearch = enableMemberSearch;
|
|
[self refreshUIBarButtons];
|
|
}
|
|
|
|
- (void)setEnableMemberInvitation:(BOOL)enableMemberInvitation
|
|
{
|
|
_enableMemberInvitation = enableMemberInvitation;
|
|
[self refreshUIBarButtons];
|
|
}
|
|
|
|
- (void)refreshUIBarButtons
|
|
{
|
|
MXRoom *mxRoom = [self.mainSession roomWithRoomId:dataSource.roomId];
|
|
|
|
MXWeakify(self);
|
|
[mxRoom state:^(MXRoomState *roomState) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
BOOL showInvitationOption = self.enableMemberInvitation;
|
|
|
|
if (showInvitationOption && self->dataSource)
|
|
{
|
|
// Check conditions to be able to invite someone
|
|
NSInteger oneSelfPowerLevel = [roomState.powerLevels powerLevelOfUserWithUserID:self.mainSession.myUser.userId];
|
|
if (oneSelfPowerLevel < [roomState.powerLevels invite])
|
|
{
|
|
showInvitationOption = NO;
|
|
}
|
|
}
|
|
|
|
if (showInvitationOption)
|
|
{
|
|
if (self.enableMemberSearch)
|
|
{
|
|
self.navigationItem.rightBarButtonItems = @[self->searchBarButton, self->addBarButton];
|
|
}
|
|
else
|
|
{
|
|
self.navigationItem.rightBarButtonItems = @[self->addBarButton];
|
|
}
|
|
}
|
|
else if (self.enableMemberSearch)
|
|
{
|
|
self.navigationItem.rightBarButtonItems = @[self->searchBarButton];
|
|
}
|
|
else
|
|
{
|
|
self.navigationItem.rightBarButtonItems = nil;
|
|
}
|
|
}];
|
|
}
|
|
|
|
#pragma mark -
|
|
- (void)displayList:(MXKRoomMemberListDataSource *)listDataSource
|
|
{
|
|
if (dataSource)
|
|
{
|
|
dataSource.delegate = nil;
|
|
dataSource = nil;
|
|
[self removeMatrixSession:self.mainSession];
|
|
}
|
|
|
|
dataSource = listDataSource;
|
|
dataSource.delegate = self;
|
|
|
|
// Report the matrix session at view controller level to update UI according to session state
|
|
[self addMatrixSession:dataSource.mxSession];
|
|
|
|
if (self.membersTableView)
|
|
{
|
|
// Set up table data source
|
|
self.membersTableView.dataSource = dataSource;
|
|
}
|
|
}
|
|
|
|
- (void)scrollToTop:(BOOL)animated
|
|
{
|
|
[self.membersTableView setContentOffset:CGPointMake(-self.membersTableView.adjustedContentInset.left, -self.membersTableView.adjustedContentInset.top) animated:animated];
|
|
}
|
|
|
|
#pragma mark - MXKDataSourceDelegate
|
|
|
|
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
|
|
{
|
|
// Return the default member table view cell
|
|
return MXKRoomMemberTableViewCell.class;
|
|
}
|
|
|
|
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
|
|
{
|
|
// Consider the default member table view cell
|
|
return MXKRoomMemberTableViewCell.defaultReuseIdentifier;
|
|
}
|
|
|
|
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
|
|
{
|
|
if (presenceUpdateTimer)
|
|
{
|
|
[presenceUpdateTimer invalidate];
|
|
presenceUpdateTimer = nil;
|
|
}
|
|
|
|
// For now, do a simple full reload
|
|
[self.membersTableView reloadData];
|
|
|
|
if (shouldScrollToTopOnRefresh)
|
|
{
|
|
[self scrollToTop:NO];
|
|
shouldScrollToTopOnRefresh = NO;
|
|
}
|
|
|
|
// Place a timer to update members's activity information
|
|
presenceUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(updateMembersActivityInfo) userInfo:self repeats:YES];
|
|
}
|
|
|
|
#pragma mark - UITableView delegate
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return [dataSource cellHeightAtIndex:indexPath.row];
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
if (_delegate)
|
|
{
|
|
id<MXKRoomMemberCellDataStoring> cellData = [dataSource cellDataAtIndex:indexPath.row];
|
|
|
|
[_delegate roomMemberListViewController:self didSelectMember:cellData.roomMember];
|
|
}
|
|
[tableView deselectRowAtIndexPath:indexPath animated:NO];
|
|
}
|
|
|
|
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
}
|
|
|
|
#pragma mark - UISearchBarDelegate
|
|
|
|
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
|
|
{
|
|
// Apply filter
|
|
shouldScrollToTopOnRefresh = YES;
|
|
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.membersSearchBar.hidden = YES;
|
|
self.membersSearchBarHeightConstraint.constant = 0;
|
|
[self.view setNeedsUpdateConstraints];
|
|
|
|
self.membersSearchBar.text = nil;
|
|
|
|
// Refresh display
|
|
shouldScrollToTopOnRefresh = YES;
|
|
[self.dataSource searchWithPatterns:nil];
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (void)search:(id)sender
|
|
{
|
|
// The user may have pressed search button whereas the view controller was disappearing
|
|
if (ignoreSearchRequest)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self.membersSearchBar.isHidden)
|
|
{
|
|
// Check whether there are data in which search
|
|
if ([self.dataSource tableView:self.membersTableView numberOfRowsInSection:0])
|
|
{
|
|
self.membersSearchBar.hidden = NO;
|
|
self.membersSearchBarHeightConstraint.constant = 44;
|
|
[self.view setNeedsUpdateConstraints];
|
|
|
|
// Create search bar
|
|
[self.membersSearchBar becomeFirstResponder];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self searchBarCancelButtonClicked: self.membersSearchBar];
|
|
}
|
|
}
|
|
|
|
- (void)inviteNewMember:(id)sender
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
}
|
|
|
|
// Ask for userId to invite
|
|
currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n userIdTitle] message:nil preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
|
|
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
}
|
|
|
|
}]];
|
|
|
|
|
|
[currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField)
|
|
{
|
|
textField.secureTextEntry = NO;
|
|
textField.placeholder = [VectorL10n userIdPlaceholder];
|
|
}];
|
|
|
|
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n invite]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
|
|
NSString *userId = [self->currentAlert textFields].firstObject.text;
|
|
|
|
self->currentAlert = nil;
|
|
|
|
if (userId.length)
|
|
{
|
|
MXRoom *mxRoom = [self.mainSession roomWithRoomId:self.dataSource.roomId];
|
|
if (mxRoom)
|
|
{
|
|
[mxRoom inviteUser:userId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[MXKRoomVC] Invite %@ failed", userId);
|
|
// Notify MatrixKit user
|
|
NSString *myUserId = self.mainSession.myUser.userId;
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
|
|
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
}]];
|
|
|
|
[self presentViewController:currentAlert animated:YES completion:nil];
|
|
}
|
|
|
|
@end
|