/* Copyright 2015 OpenMarket 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 "MXKRoomMemberListDataSource.h" @import MatrixSDK.MXCallManager; #import "MXKRoomMemberCellData.h" #pragma mark - Constant definitions NSString *const kMXKRoomMemberCellIdentifier = @"kMXKRoomMemberCellIdentifier"; @interface MXKRoomMemberListDataSource () { /** The room in which members are listed. */ MXRoom *mxRoom; /** Cache for loaded room state. */ MXRoomState *mxRoomState; /** The members events listener. */ id membersListener; /** The typing notification listener in the room. */ id typingNotifListener; } @end @implementation MXKRoomMemberListDataSource - (instancetype)initWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession { self = [super initWithMatrixSession:mxSession]; if (self) { _roomId = roomId; cellDataArray = [NSMutableArray array]; filteredCellDataArray = nil; // Consider the shared app settings by default _settings = [MXKAppSettings standardAppSettings]; // Set default data class [self registerCellDataClass:MXKRoomMemberCellData.class forCellIdentifier:kMXKRoomMemberCellIdentifier]; } return self; } - (void)destroy { cellDataArray = nil; filteredCellDataArray = nil; if (membersListener) { [self.mxSession removeListener:membersListener]; membersListener = nil; } if (typingNotifListener) { MXWeakify(self); [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->typingNotifListener]; self->typingNotifListener = nil; }]; } [super destroy]; } - (void)didMXSessionStateChange { if (MXSessionStateStoreDataReady <= self.mxSession.state) { // Check whether the room is not already set if (!mxRoom) { mxRoom = [self.mxSession roomWithRoomId:_roomId]; if (mxRoom) { MXWeakify(self); [mxRoom state:^(MXRoomState *roomState) { MXStrongifyAndReturnIfNil(self); self->mxRoomState = roomState; [self loadData]; // Register on typing notif [self listenTypingNotifications]; // Register on members events [self listenMembersEvents]; // Update here data source state self->state = MXKDataSourceStateReady; // Notify delegate if (self.delegate) { if ([self.delegate respondsToSelector:@selector(dataSource:didStateChange:)]) { [self.delegate dataSource:self didStateChange:self->state]; } [self.delegate dataSource:self didCellChange:nil]; } }]; } else { MXLogDebug(@"[MXKRoomMemberDataSource] The user does not know the room %@", _roomId); // Update here data source state state = MXKDataSourceStateFailed; // Notify delegate if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)]) { [self.delegate dataSource:self didStateChange:state]; } } } } } - (void)searchWithPatterns:(NSArray*)patternsList { if (patternsList.count) { if (filteredCellDataArray) { [filteredCellDataArray removeAllObjects]; } else { filteredCellDataArray = [NSMutableArray arrayWithCapacity:cellDataArray.count]; } for (id cellData in cellDataArray) { for (NSString* pattern in patternsList) { if ([[cellData memberDisplayName] rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) { [filteredCellDataArray addObject:cellData]; break; } } } } else { filteredCellDataArray = nil; } if (self.delegate) { [self.delegate dataSource:self didCellChange:nil]; } } - (id)cellDataAtIndex:(NSInteger)index { if (filteredCellDataArray) { return filteredCellDataArray[index]; } return cellDataArray[index]; } - (CGFloat)cellHeightAtIndex:(NSInteger)index { if (self.delegate) { id cellData = [self cellDataAtIndex:index]; Class class = [self.delegate cellViewClassForCellData:cellData]; return [class heightForCellData:cellData withMaximumWidth:0]; } return 0; } #pragma mark - Members processing - (void)loadData { NSArray* membersList = [mxRoomState.members membersWithoutConferenceUser]; if (!_settings.showLeftMembersInRoomMemberList) { NSMutableArray* filteredMembers = [[NSMutableArray alloc] init]; for (MXRoomMember* member in membersList) { // Filter out left users if (member.membership != MXMembershipLeave) { [filteredMembers addObject:member]; } } membersList = filteredMembers; } [cellDataArray removeAllObjects]; // Retrieve the MXKCellData class to manage the data Class class = [self cellDataClassForCellIdentifier:kMXKRoomMemberCellIdentifier]; NSAssert([class conformsToProtocol:@protocol(MXKRoomMemberCellDataStoring)], @"MXKRoomMemberListDataSource only manages MXKCellData that conforms to MXKRoomMemberCellDataStoring protocol"); for (MXRoomMember *member in membersList) { id cellData = [[class alloc] initWithRoomMember:member roomState:mxRoomState andRoomMemberListDataSource:self]; if (cellData) { [cellDataArray addObject:cellData]; } } [self sortMembers]; } - (void)sortMembers { NSArray *sortedMembers = [cellDataArray sortedArrayUsingComparator:^NSComparisonResult(id member1, id member2) { // Move banned and left members at the end of the list if (member1.roomMember.membership == MXMembershipLeave || member1.roomMember.membership == MXMembershipBan) { if (member2.roomMember.membership != MXMembershipLeave && member2.roomMember.membership != MXMembershipBan) { return NSOrderedDescending; } } else if (member2.roomMember.membership == MXMembershipLeave || member2.roomMember.membership == MXMembershipBan) { return NSOrderedAscending; } // Move invited members just before left and banned members if (member1.roomMember.membership == MXMembershipInvite) { if (member2.roomMember.membership != MXMembershipInvite) { return NSOrderedDescending; } } else if (member2.roomMember.membership == MXMembershipInvite) { return NSOrderedAscending; } if (self->_settings.sortRoomMembersUsingLastSeenTime) { // Get the users that correspond to these members MXUser *user1 = [self.mxSession userWithUserId:member1.roomMember.userId]; MXUser *user2 = [self.mxSession userWithUserId:member2.roomMember.userId]; // Move users who are not online or unavailable at the end (before invited users) if ((user1.presence == MXPresenceOnline) || (user1.presence == MXPresenceUnavailable)) { if ((user2.presence != MXPresenceOnline) && (user2.presence != MXPresenceUnavailable)) { return NSOrderedAscending; } } else if ((user2.presence == MXPresenceOnline) || (user2.presence == MXPresenceUnavailable)) { return NSOrderedDescending; } else { // Here both users are neither online nor unavailable (the lastActive ago is useless) // We will sort them according to their display, by keeping in front the offline users if (user1.presence == MXPresenceOffline) { if (user2.presence != MXPresenceOffline) { return NSOrderedAscending; } } else if (user2.presence == MXPresenceOffline) { return NSOrderedDescending; } return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch]; } // Consider user's lastActive ago value if (user1.lastActiveAgo < user2.lastActiveAgo) { return NSOrderedAscending; } else if (user1.lastActiveAgo == user2.lastActiveAgo) { return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch]; } return NSOrderedDescending; } else { // Move user without display name at the end (before invited users) if (member1.roomMember.displayname.length) { if (!member2.roomMember.displayname.length) { return NSOrderedAscending; } } else if (member2.roomMember.displayname.length) { return NSOrderedDescending; } return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch]; } }]; cellDataArray = [NSMutableArray arrayWithArray:sortedMembers]; } - (void)listenMembersEvents { // Remove the previous live listener if (membersListener) { [self.mxSession removeListener:membersListener]; } // Register a listener for events that concern room members NSArray *mxMembersEvents = @[ kMXEventTypeStringRoomMember, kMXEventTypeStringRoomPowerLevels, kMXEventTypeStringPresence ]; membersListener = [self.mxSession listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) { // consider only live event if (direction == MXTimelineDirectionForwards) { // Check the room Id (if any) if (event.roomId && [event.roomId isEqualToString:self->mxRoom.roomId] == NO) { // This event does not concern the current room members return; } // refresh the whole members list. TODO GFO refresh only the updated members. [self loadData]; if (self.delegate) { [self.delegate dataSource:self didCellChange:nil]; } } }]; } - (void)listenTypingNotifications { // Remove the previous live listener if (self->typingNotifListener) { [mxRoom removeListener:self->typingNotifListener]; } // Add typing notification listener self->typingNotifListener = [mxRoom listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) { // Handle only live events if (direction == MXTimelineDirectionForwards) { // Retrieve typing users list NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self->mxRoom.typingUsers]; // Remove typing info for the current user NSUInteger index = [typingUsers indexOfObject:self.mxSession.myUser.userId]; if (index != NSNotFound) { [typingUsers removeObjectAtIndex:index]; } for (id cellData in self->cellDataArray) { if ([typingUsers indexOfObject:cellData.roomMember.userId] == NSNotFound) { cellData.isTyping = NO; } else { cellData.isTyping = YES; } } if (self.delegate) { [self.delegate dataSource:self didCellChange:nil]; } } }]; } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (filteredCellDataArray) { return filteredCellDataArray.count; } return cellDataArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { id roomData = [self cellDataAtIndex:indexPath.row]; if (roomData && self.delegate) { NSString *identifier = [self.delegate cellReuseIdentifierForCellData:roomData]; if (identifier) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath]; // Make the bubble display the data [cell render:roomData]; return cell; } } // Return a fake cell to prevent app from crashing. return [[UITableViewCell alloc] init]; } @end