Merge MatrixKit develop with commit hash: b85b736313bec0592bd1cabc68035d97f5331137

This commit is contained in:
SBiOSoftWhare
2021-12-03 11:47:24 +01:00
parent af65f71177
commit f57941177e
475 changed files with 87437 additions and 0 deletions
@@ -0,0 +1,26 @@
/*
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 "MXKRecentsDataSource.h"
/**
'MXKInterleavedRecentsDataSource' class inherits from 'MXKRecentsDataSource'.
It interleaves the recents in case of multiple sessions to display first the most recent room.
*/
@interface MXKInterleavedRecentsDataSource : MXKRecentsDataSource
@end
@@ -0,0 +1,439 @@
/*
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 "MXKInterleavedRecentsDataSource.h"
#import "MXKInterleavedRecentTableViewCell.h"
#import "MXKAccountManager.h"
#import "NSBundle+MatrixKit.h"
@interface MXKInterleavedRecentsDataSource ()
{
/**
The interleaved recents: cell data served by `MXKInterleavedRecentsDataSource`.
*/
NSMutableArray *interleavedCellDataArray;
}
@end
@implementation MXKInterleavedRecentsDataSource
- (instancetype)init
{
self = [super init];
if (self)
{
interleavedCellDataArray = [NSMutableArray array];
}
return self;
}
#pragma mark - Override MXKDataSource
- (void)destroy
{
interleavedCellDataArray = nil;
[super destroy];
}
#pragma mark - Override MXKRecentsDataSource
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame
{
UIView *sectionHeader = nil;
if (displayedRecentsDataSourceArray.count > 1 && section == 0)
{
sectionHeader = [[UIView alloc] initWithFrame:frame];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
CGFloat btnWidth = frame.size.width / displayedRecentsDataSourceArray.count;
UIButton *previousShrinkButton;
for (NSInteger index = 0; index < displayedRecentsDataSourceArray.count; index++)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:index];
NSString* btnTitle = recentsDataSource.mxSession.myUser.userId;
// Add shrink button
UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom];
CGRect btnFrame = CGRectMake(index * btnWidth, 0, btnWidth, sectionHeader.frame.size.height);
shrinkButton.frame = btnFrame;
shrinkButton.backgroundColor = [UIColor clearColor];
[shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
shrinkButton.tag = index;
[sectionHeader addSubview:shrinkButton];
sectionHeader.userInteractionEnabled = YES;
// Set shrink button constraints
NSLayoutConstraint *leftConstraint;
NSLayoutConstraint *widthConstraint;
shrinkButton.translatesAutoresizingMaskIntoConstraints = NO;
if (!previousShrinkButton)
{
leftConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:sectionHeader
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
widthConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:sectionHeader
attribute:NSLayoutAttributeWidth
multiplier:(1.0 /displayedRecentsDataSourceArray.count)
constant:0];
}
else
{
leftConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:previousShrinkButton
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
widthConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:previousShrinkButton
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0];
}
[NSLayoutConstraint activateConstraints:@[leftConstraint, widthConstraint]];
previousShrinkButton = shrinkButton;
// Add shrink icon
UIImage *chevron;
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"disclosure"];
}
else
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"shrink"];
}
UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron];
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Display the tint color of the user
MXKAccount *account = [[MXKAccountManager sharedManager] accountForUserId:recentsDataSource.mxSession.myUser.userId];
if (account)
{
chevronView.backgroundColor = account.userTintColor;
}
else
{
chevronView.backgroundColor = [UIColor clearColor];
}
}
else
{
chevronView.backgroundColor = [UIColor lightGrayColor];
}
chevronView.contentMode = UIViewContentModeCenter;
frame = chevronView.frame;
frame.size.width = frame.size.height = shrinkButton.frame.size.height - 10;
frame.origin.x = shrinkButton.frame.size.width - frame.size.width - 8;
frame.origin.y = (shrinkButton.frame.size.height - frame.size.height) / 2;
chevronView.frame = frame;
[shrinkButton addSubview:chevronView];
chevronView.autoresizingMask |= (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
// Add label
frame = shrinkButton.frame;
frame.origin.x = 5;
frame.origin.y = 5;
frame.size.width = chevronView.frame.origin.x - 10;
frame.size.height -= 10;
UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame];
headerLabel.font = [UIFont boldSystemFontOfSize:16];
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.text = btnTitle;
[shrinkButton addSubview:headerLabel];
headerLabel.autoresizingMask |= (UIViewAutoresizingFlexibleWidth);
}
}
return sectionHeader;
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKRecentCellDataStoring> cellData = nil;
// Only one section is handled by this data source
if (indexPath.section == 0)
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
cellData = [recentsDataSource cellDataAtIndex:indexPath.row];
}
// Else all the cells have been interleaved.
else if (indexPath.row < interleavedCellDataArray.count)
{
cellData = interleavedCellDataArray[indexPath.row];
}
}
return cellData;
}
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat height = 0;
// Only one section is handled by this data source
if (indexPath.section == 0)
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
height = [recentsDataSource cellHeightAtIndex:indexPath.row];
}
// Else all the cells have been interleaved.
else if (indexPath.row < interleavedCellDataArray.count)
{
id<MXKRecentCellDataStoring> recentCellData = interleavedCellDataArray[indexPath.row];
// Select the related recent data source
MXKDataSource *dataSource = recentCellData.dataSource;
if ([dataSource isKindOfClass:[MXKSessionRecentsDataSource class]])
{
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
// Count the index of this cell data in original data source array
NSInteger rank = 0;
for (NSInteger index = 0; index < indexPath.row; index++)
{
id<MXKRecentCellDataStoring> cellData = interleavedCellDataArray[index];
if (cellData.roomSummary == recentCellData.roomSummary)
{
rank++;
}
}
height = [recentsDataSource cellHeightAtIndex:rank];
}
}
}
return height;
}
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession
{
NSIndexPath *indexPath = nil;
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
if (recentsDataSource.mxSession == matrixSession)
{
// Look for the cell
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = [recentsDataSource cellDataAtIndex:index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
}
}
}
else
{
// Look for the right data source
for (MXKSessionRecentsDataSource *recentsDataSource in displayedRecentsDataSourceArray)
{
if (recentsDataSource.mxSession == matrixSession)
{
// Check whether the source is not shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Look for the cell
for (NSInteger index = 0; index < interleavedCellDataArray.count; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = interleavedCellDataArray[index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
}
}
break;
}
}
}
return indexPath;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Check whether all data sources are ready before rendering recents
if (self.state == MXKDataSourceStateReady)
{
// Only one section is handled by this data source.
return (displayedRecentsDataSourceArray.count ? 1 : 0);
}
return 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
return recentsDataSource.numberOfCells;
}
return interleavedCellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKRecentCellDataStoring> roomData = [self cellDataAtIndexPath:indexPath];
if (roomData && self.delegate)
{
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:roomData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make sure we listen to user actions on the cell
cell.delegate = self;
// Make the bubble display the data
[cell render:roomData];
// Clear the user flag, if only one recents list is available
if (displayedRecentsDataSourceArray.count == 1 && [cell isKindOfClass:[MXKInterleavedRecentTableViewCell class]])
{
((MXKInterleavedRecentTableViewCell*)cell).userFlag.backgroundColor = [UIColor clearColor];
}
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
#pragma mark - MXKDataSourceDelegate
- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
// Flush interleaved cells array, we will refer directly to the cell data of the unique data source.
[interleavedCellDataArray removeAllObjects];
}
else
{
// Handle here the specific case where a second source is just added.
// The empty interleaved cells array has to be prefilled with the cell data of the other source (except if this other source is shrinked).
if (!interleavedCellDataArray.count && displayedRecentsDataSourceArray.count == 2)
{
// This is the first interleaving, look for the other source
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
if (recentsDataSource == dataSource)
{
recentsDataSource = displayedRecentsDataSourceArray.lastObject;
}
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Report all cell data
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
[interleavedCellDataArray addObject:[recentsDataSource cellDataAtIndex:index]];
}
}
}
// Update now interleaved cells array, TODO take into account 'changes' parameter
MXKSessionRecentsDataSource *updateRecentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
NSInteger numberOfUpdatedCells = 0;
// Check whether this dataSource is used
if ([displayedRecentsDataSourceArray indexOfObject:dataSource] != NSNotFound && [shrinkedRecentsDataSourceArray indexOfObject:dataSource] == NSNotFound)
{
numberOfUpdatedCells = updateRecentsDataSource.numberOfCells;
}
NSInteger currentCellIndex = 0;
NSInteger updatedCellIndex = 0;
id<MXKRecentCellDataStoring> updatedCellData = nil;
if (numberOfUpdatedCells)
{
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
// Review all cell data items of the current list
while (currentCellIndex < interleavedCellDataArray.count)
{
id<MXKRecentCellDataStoring> currentCellData = interleavedCellDataArray[currentCellIndex];
// Remove existing cell data of the updated data source
if (currentCellData.dataSource == dataSource)
{
[interleavedCellDataArray removeObjectAtIndex:currentCellIndex];
}
else
{
while (updatedCellData && (updatedCellData.roomSummary.lastMessage.originServerTs > currentCellData.roomSummary.lastMessage.originServerTs))
{
[interleavedCellDataArray insertObject:updatedCellData atIndex:currentCellIndex++];
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
currentCellIndex++;
}
}
while (updatedCellData)
{
[interleavedCellDataArray addObject:updatedCellData];
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
}
// Call super to keep update readyRecentsDataSourceArray.
[super dataSource:dataSource didCellChange:changes];
}
@end
@@ -0,0 +1,26 @@
/*
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 <Foundation/Foundation.h>
#import "MXKRecentCellDataStoring.h"
/**
`MXKRecentCellData` modelised the data for a `MXKRecentTableViewCell` cell.
*/
@interface MXKRecentCellData : MXKCellData <MXKRecentCellDataStoring>
@end
@@ -0,0 +1,133 @@
/*
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 "MXKRecentCellData.h"
@import MatrixSDK;
#import "MXKDataSource.h"
#import "MXEvent+MatrixKit.h"
#import "MXKSwiftHeader.h"
@implementation MXKRecentCellData
@synthesize roomSummary, dataSource, lastEventDate;
- (instancetype)initWithRoomSummary:(id<MXRoomSummaryProtocol>)theRoomSummary
dataSource:(MXKDataSource*)theDataSource;
{
self = [self init];
if (self)
{
roomSummary = theRoomSummary;
dataSource = theDataSource;
}
return self;
}
- (void)dealloc
{
roomSummary = nil;
}
- (MXSession *)mxSession
{
return dataSource.mxSession;
}
- (NSString*)lastEventDate
{
return (NSString*)roomSummary.lastMessage.others[@"lastEventDate"];
}
- (BOOL)hasUnread
{
return (roomSummary.localUnreadEventCount != 0);
}
- (NSString *)roomIdentifier
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.childRoomId;
}
return roomSummary.roomId;
}
- (NSString *)roomDisplayname
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.displayName;
}
return roomSummary.displayname;
}
- (NSString *)avatarUrl
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.avatarUrl;
}
return roomSummary.avatar;
}
- (NSString *)lastEventTextMessage
{
if (self.isSuggestedRoom)
{
return roomSummary.spaceChildInfo.topic;
}
return roomSummary.lastMessage.text;
}
- (NSAttributedString *)lastEventAttributedTextMessage
{
if (self.isSuggestedRoom)
{
return nil;
}
return roomSummary.lastMessage.attributedText;
}
- (NSUInteger)notificationCount
{
return roomSummary.notificationCount;
}
- (NSUInteger)highlightCount
{
return roomSummary.highlightCount;
}
- (NSString*)notificationCountStringValue
{
return [NSString stringWithFormat:@"%tu", self.notificationCount];
}
- (NSString*)description
{
return [NSString stringWithFormat:@"%@ %@: %@ - %@", super.description, self.roomSummary.roomId, self.roomDisplayname, self.lastEventTextMessage];
}
- (BOOL)isSuggestedRoom
{
// As off now, we only store MXSpaceChildInfo in case of suggested rooms
return self.roomSummary.spaceChildInfo != nil;
}
@end
@@ -0,0 +1,75 @@
/*
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 <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellData.h"
@class MXKDataSource;
@class MXSpaceChildInfo;
/**
`MXKRecentCellDataStoring` defines a protocol a class must conform in order to store recent cell data
managed by `MXKSessionRecentsDataSource`.
*/
@protocol MXKRecentCellDataStoring <NSObject>
#pragma mark - Data displayed by a room recent cell
/**
The original data source of the recent displayed by the cell.
*/
@property (nonatomic, weak, readonly) MXKDataSource *dataSource;
/**
The `MXRoomSummaryProtocol` instance of the room for the recent displayed by the cell.
*/
@property (nonatomic, readonly) id<MXRoomSummaryProtocol> roomSummary;
@property (nonatomic, readonly) NSString *roomIdentifier;
@property (nonatomic, readonly) NSString *roomDisplayname;
@property (nonatomic, readonly) NSString *avatarUrl;
@property (nonatomic, readonly) NSString *lastEventTextMessage;
@property (nonatomic, readonly) NSString *lastEventDate;
@property (nonatomic, readonly) BOOL hasUnread;
@property (nonatomic, readonly) NSUInteger notificationCount;
@property (nonatomic, readonly) NSUInteger highlightCount;
@property (nonatomic, readonly) NSString *notificationCountStringValue;
@property (nonatomic, readonly) BOOL isSuggestedRoom;
@property (nonatomic, readonly) MXSession *mxSession;
#pragma mark - Public methods
/**
Create a new `MXKCellData` object for a new recent cell.
@param roomSummary the `id<MXRoomSummaryProtocol>` object that has data about the room.
@param dataSource the `MXKDataSource` object that will use this instance.
@return the newly created instance.
*/
- (instancetype)initWithRoomSummary:(id<MXRoomSummaryProtocol>)roomSummary
dataSource:(MXKDataSource*)dataSource;
@optional
/**
The `lastEventTextMessage` with sets of attributes.
*/
@property (nonatomic, readonly) NSAttributedString *lastEventAttributedTextMessage;
@end
@@ -0,0 +1,140 @@
/*
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 "MXKSessionRecentsDataSource.h"
/**
'MXKRecentsDataSource' is a base class to handle recents from one or multiple matrix sessions.
A 'MXKRecentsDataSource' instance provides the recents data source for `MXKRecentListViewController`.
By default, the recents list of different sessions are handled into separate sections.
*/
@interface MXKRecentsDataSource : MXKDataSource <UITableViewDataSource, MXKDataSourceDelegate>
{
@protected
/**
Array of `MXKSessionRecentsDataSource` instances. Only ready and non empty data source are listed here.
(Note: a data source may be considered as empty during searching)
*/
NSMutableArray *displayedRecentsDataSourceArray;
/**
Array of shrinked sources. Sub-list of displayedRecentsDataSourceArray.
*/
NSMutableArray *shrinkedRecentsDataSourceArray;
}
/**
List of associated matrix sessions.
*/
@property (nonatomic, readonly) NSArray* mxSessions;
/**
The number of available recents data sources (This count may be different than mxSession.count because empty data sources are ignored).
*/
@property (nonatomic, readonly) NSUInteger displayedRecentsDataSourcesCount;
/**
Tell whether there are some unread messages.
*/
@property (nonatomic, readonly) BOOL hasUnread;
/**
The current search patterns list.
*/
@property (nonatomic, readonly) NSArray* searchPatternsList;
@property (nonatomic, strong) MXSpace *currentSpace;
#pragma mark - Configuration
/**
Add recents data from a matrix session.
@param mxSession the Matrix session to retrieve contextual data.
@return the new 'MXKSessionRecentsDataSource' instance created for this Matrix session.
*/
- (MXKSessionRecentsDataSource *)addMatrixSession:(MXSession*)mxSession;
/**
Remove recents data related to a matrix session.
@param mxSession the session to remove.
*/
- (void)removeMatrixSession:(MXSession*)mxSession;
/**
Filter the current recents list according to the provided patterns.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the section header view.
@param section the section index
@param frame the drawing area for the header of the specified section.
@return the section header.
*/
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame;
/**
Get the data for the cell at the given index path.
@param indexPath the index of the cell
@return the cell data
*/
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath*)indexPath;
/**
Get the height of the cell at the given index path.
@param indexPath the index of the cell
@return the cell height
*/
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath*)indexPath;
/**
Get the index path of the cell related to the provided roomId and session.
@param roomId the room identifier.
@param mxSession the matrix session in which the room should be available.
@return indexPath the index of the cell (nil if not found or if the related section is shrinked).
*/
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession;
/**
Returns the room at the index path
@param indexPath the index of the cell
@return the MXRoom if it exists
*/
- (MXRoom*)getRoomAtIndexPath:(NSIndexPath *)indexPath;
/**
Leave the room at the index path
@param indexPath the index of the cell
*/
- (void)leaveRoomAtIndexPath:(NSIndexPath *)indexPath;
/**
Action registered on buttons used to shrink/disclose recents sources.
*/
- (IBAction)onButtonPressed:(id)sender;
@end
@@ -0,0 +1,657 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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 "MXKRecentsDataSource.h"
@import MatrixSDK.MXMediaManager;
#import "NSBundle+MatrixKit.h"
#import "MXKConstants.h"
@interface MXKRecentsDataSource ()
{
/**
Array of `MXSession` instances.
*/
NSMutableArray *mxSessionArray;
/**
Array of `MXKSessionRecentsDataSource` instances (one by matrix session).
*/
NSMutableArray *recentsDataSourceArray;
}
@end
@implementation MXKRecentsDataSource
- (instancetype)init
{
self = [super init];
if (self)
{
mxSessionArray = [NSMutableArray array];
recentsDataSourceArray = [NSMutableArray array];
displayedRecentsDataSourceArray = [NSMutableArray array];
shrinkedRecentsDataSourceArray = [NSMutableArray array];
// Set default data and view classes
[self registerCellDataClass:MXKRecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionInviteRoomUpdate:) name:kMXSessionInvitedRoomsDidChangeNotification object:nil];
}
return self;
}
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [self init];
if (self)
{
[self addMatrixSession:matrixSession];
}
return self;
}
- (MXKSessionRecentsDataSource *)addMatrixSession:(MXSession *)matrixSession
{
MXKSessionRecentsDataSource *recentsDataSource = [[MXKSessionRecentsDataSource alloc] initWithMatrixSession:matrixSession];
if (recentsDataSource)
{
// Set the actual data and view classes
[recentsDataSource registerCellDataClass:[self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier] forCellIdentifier:kMXKRecentCellIdentifier];
[mxSessionArray addObject:matrixSession];
recentsDataSource.delegate = self;
[recentsDataSourceArray addObject:recentsDataSource];
[recentsDataSource finalizeInitialization];
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didAddMatrixSession:)])
{
[self.delegate dataSource:self didAddMatrixSession:matrixSession];
}
// Check the current state of the data source
[self dataSource:recentsDataSource didStateChange:recentsDataSource.state];
}
return recentsDataSource;
}
- (void)removeMatrixSession:(MXSession*)matrixSession
{
for (NSUInteger index = 0; index < mxSessionArray.count; index++)
{
MXSession *mxSession = [mxSessionArray objectAtIndex:index];
if (mxSession == matrixSession)
{
MXKSessionRecentsDataSource *recentsDataSource = [recentsDataSourceArray objectAtIndex:index];
[recentsDataSource destroy];
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
[recentsDataSourceArray removeObjectAtIndex:index];
[mxSessionArray removeObjectAtIndex:index];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didRemoveMatrixSession:)])
{
[self.delegate dataSource:self didRemoveMatrixSession:matrixSession];
}
break;
}
}
}
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
_currentSpace = currentSpace;
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray) {
recentsDataSource.currentSpace = currentSpace;
}
}
#pragma mark - MXKDataSource overridden
- (MXSession*)mxSession
{
if (mxSessionArray.count > 1)
{
MXLogDebug(@"[MXKRecentsDataSource] CAUTION: mxSession property is not relevant in case of multi-sessions (%tu)", mxSessionArray.count);
}
// TODO: This property is not well adapted in case of multi-sessions
// We consider by default the first added session as the main one...
if (mxSessionArray.count)
{
return [mxSessionArray firstObject];
}
return nil;
}
- (MXKDataSourceState)state
{
// Manage a global state based on the state of each internal data source.
MXKDataSourceState currentState = MXKDataSourceStateUnknown;
MXKSessionRecentsDataSource *dataSource;
if (recentsDataSourceArray.count)
{
dataSource = [recentsDataSourceArray firstObject];
currentState = dataSource.state;
// Deduce the current state according to the internal data sources
for (NSUInteger index = 1; index < recentsDataSourceArray.count; index++)
{
dataSource = [recentsDataSourceArray objectAtIndex:index];
switch (dataSource.state)
{
case MXKDataSourceStateUnknown:
break;
case MXKDataSourceStatePreparing:
currentState = MXKDataSourceStatePreparing;
break;
case MXKDataSourceStateFailed:
if (currentState == MXKDataSourceStateUnknown)
{
currentState = MXKDataSourceStateFailed;
}
break;
case MXKDataSourceStateReady:
if (currentState == MXKDataSourceStateUnknown || currentState == MXKDataSourceStateFailed)
{
currentState = MXKDataSourceStateReady;
}
break;
default:
break;
}
}
}
return currentState;
}
- (void)destroy
{
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray)
{
[recentsDataSource destroy];
}
displayedRecentsDataSourceArray = nil;
recentsDataSourceArray = nil;
shrinkedRecentsDataSourceArray = nil;
mxSessionArray = nil;
_searchPatternsList = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionInvitedRoomsDidChangeNotification object:nil];
[super destroy];
}
#pragma mark -
- (NSArray*)mxSessions
{
return [NSArray arrayWithArray:mxSessionArray];
}
- (NSUInteger)displayedRecentsDataSourcesCount
{
return displayedRecentsDataSourceArray.count;
}
- (BOOL)hasUnread
{
// Check hasUnread flag in all ready data sources
for (MXKSessionRecentsDataSource *recentsDataSource in displayedRecentsDataSourceArray)
{
if (recentsDataSource.hasUnread)
{
return YES;
}
}
return NO;
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
_searchPatternsList = patternsList;
// CAUTION: Apply here the search pattern to all ready data sources (not only displayed ones).
// Some data sources may have been removed from 'displayedRecentsDataSourceArray' during a previous search if no recent was matching.
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray)
{
if (recentsDataSource.state == MXKDataSourceStateReady)
{
[recentsDataSource searchWithPatterns:patternsList];
}
}
}
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame
{
UIView *sectionHeader = nil;
if (displayedRecentsDataSourceArray.count > 1 && section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
NSString* sectionTitle = recentsDataSource.mxSession.myUser.userId;
sectionHeader = [[UIView alloc] initWithFrame:frame];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
// Add shrink button
UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom];
frame.origin.x = frame.origin.y = 0;
shrinkButton.frame = frame;
shrinkButton.backgroundColor = [UIColor clearColor];
[shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
shrinkButton.tag = section;
[sectionHeader addSubview:shrinkButton];
sectionHeader.userInteractionEnabled = YES;
// Add shrink icon
UIImage *chevron;
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"disclosure"];
}
else
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"shrink"];
}
UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron];
chevronView.contentMode = UIViewContentModeCenter;
frame = chevronView.frame;
frame.origin.x = sectionHeader.frame.size.width - frame.size.width - 8;
frame.origin.y = (sectionHeader.frame.size.height - frame.size.height) / 2;
chevronView.frame = frame;
[sectionHeader addSubview:chevronView];
chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
// Add label
frame = sectionHeader.frame;
frame.origin.x = 5;
frame.origin.y = 5;
frame.size.width = chevronView.frame.origin.x - 10;
frame.size.height -= 10;
UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame];
headerLabel.font = [UIFont boldSystemFontOfSize:16];
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.text = sectionTitle;
[sectionHeader addSubview:headerLabel];
}
return sectionHeader;
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
return [recentsDataSource cellDataAtIndex:indexPath.row];
}
return nil;
}
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
return [recentsDataSource cellHeightAtIndex:indexPath.row];
}
return 0;
}
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession
{
NSIndexPath *indexPath = nil;
// Look for the right data source
for (NSInteger section = 0; section < displayedRecentsDataSourceArray.count; section++)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray[section];
if (recentsDataSource.mxSession == matrixSession)
{
// Check whether the source is not shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Look for the cell
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = [recentsDataSource cellDataAtIndex:index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:section];
break;
}
}
}
break;
}
}
return indexPath;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Check whether all data sources are ready before rendering recents
if (self.state == MXKDataSourceStateReady)
{
return displayedRecentsDataSourceArray.count;
}
return 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
// Check whether the source is shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
return recentsDataSource.numberOfCells;
}
}
return 0;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString* sectionTitle = nil;
if (displayedRecentsDataSourceArray.count > 1 && section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
sectionTitle = recentsDataSource.mxSession.myUser.userId;
}
return sectionTitle;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count && self.delegate)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
id<MXKRecentCellDataStoring> roomData = [recentsDataSource cellDataAtIndex:indexPath.row];
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:roomData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make sure we listen to user actions on the cell
cell.delegate = self;
// Make the bubble display the data
[cell render:roomData];
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self leaveRoomAtIndexPath:indexPath];
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Retrieve the class from the delegate here
if (self.delegate)
{
return [self.delegate cellViewClassForCellData:cellData];
}
return nil;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Retrieve the identifier from the delegate here
if (self.delegate)
{
return [self.delegate cellReuseIdentifierForCellData:cellData];
}
return nil;
}
- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes
{
// Keep update readyRecentsDataSourceArray by checking number of cells
if (dataSource.state == MXKDataSourceStateReady)
{
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
if (recentsDataSource.numberOfCells)
{
// Check whether the data source must be added
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Add this data source first
[self dataSource:dataSource didStateChange:dataSource.state];
return;
}
}
else
{
// Check whether this data source must be removed
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
return;
}
}
}
// Notify delegate
[self.delegate dataSource:self didCellChange:changes];
}
- (void)dataSource:(MXKDataSource*)dataSource didStateChange:(MXKDataSourceState)state
{
// Update list of ready data sources
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
if (dataSource.state == MXKDataSourceStateReady && recentsDataSource.numberOfCells)
{
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Add this new recents data source.
if (!displayedRecentsDataSourceArray.count)
{
[displayedRecentsDataSourceArray addObject:recentsDataSource];
}
else
{
// To display multiple accounts in a consistent order, we sort the recents data source by considering the account user id (alphabetic order).
NSUInteger index;
for (index = 0; index < displayedRecentsDataSourceArray.count; index++)
{
MXKSessionRecentsDataSource *currentRecentsDataSource = displayedRecentsDataSourceArray[index];
if ([currentRecentsDataSource.mxSession.myUser.userId compare:recentsDataSource.mxSession.myUser.userId] == NSOrderedDescending)
{
break;
}
}
// Insert this data source
[displayedRecentsDataSourceArray insertObject:recentsDataSource atIndex:index];
}
// Check whether a search session is in progress
if (_searchPatternsList)
{
[recentsDataSource searchWithPatterns:_searchPatternsList];
}
else
{
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this new added data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
}
}
else if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:self.state];
}
}
#pragma mark - Action
- (IBAction)onButtonPressed:(id)sender
{
if ([sender isKindOfClass:[UIButton class]])
{
UIButton *shrinkButton = (UIButton*)sender;
if (shrinkButton.tag < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:shrinkButton.tag];
NSUInteger index = [shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource];
if (index != NSNotFound)
{
// Disclose the
[shrinkedRecentsDataSourceArray removeObjectAtIndex:index];
}
else
{
// Shrink the recents from this session
[shrinkedRecentsDataSourceArray addObject:recentsDataSource];
}
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle change on this data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
}
}
#pragma mark - room actions
- (MXRoom*)getRoomAtIndexPath:(NSIndexPath *)indexPath
{
// Leave the selected room
id<MXKRecentCellDataStoring> recentCellData = [self cellDataAtIndexPath:indexPath];
if (recentCellData)
{
return [self.mxSession roomWithRoomId:recentCellData.roomIdentifier];
}
return nil;
}
- (void)leaveRoomAtIndexPath:(NSIndexPath *)indexPath
{
MXRoom* room = [self getRoomAtIndexPath:indexPath];
if (room)
{
// cancel pending uploads/downloads
// they are useless by now
[MXMediaManager cancelDownloadsInCacheFolder:room.roomId];
// TODO GFO cancel pending uploads related to this room
[room leave:^{
// Trigger recents table refresh
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKRecentsDataSource] Failed to leave room (%@) failed", room.roomId);
// Notify MatrixKit user
NSString *myUserId = room.mxSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}];
}
}
- (void)didMXSessionInviteRoomUpdate:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if ([self.mxSessions indexOfObject:mxSession] != NSNotFound)
{
// do nothing by default
// the inherited classes might require to perform a full or a particial refresh.
//[self.delegate dataSource:self didCellChange:nil];
}
}
@end
@@ -0,0 +1,90 @@
/*
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 <UIKit/UIKit.h>
#import "MXKConstants.h"
#import "MXKDataSource.h"
#import "MXKRecentCellData.h"
@class MXSpace;
/**
Identifier to use for cells that display a room in the recents list.
*/
extern NSString *const kMXKRecentCellIdentifier;
/**
The recents data source based on a unique matrix session.
*/
MXK_DEPRECATED_ATTRIBUTE_WITH_MSG("See MXSession.roomListDataManager")
@interface MXKSessionRecentsDataSource : MXKDataSource {
@protected
/**
The data for the cells served by `MXKSessionRecentsDataSource`.
*/
NSMutableArray *cellDataArray;
/**
The filtered recents: sub-list of `cellDataArray` defined by `searchWithPatterns:` call.
*/
NSMutableArray *filteredCellDataArray;
}
/**
The current number of cells.
*/
@property (nonatomic, readonly) NSInteger numberOfCells;
/**
Tell whether there are some unread messages.
*/
@property (nonatomic, readonly) BOOL hasUnread;
@property (nonatomic, strong, nullable) MXSpace *currentSpace;
#pragma mark - Life cycle
/**
Filter the current recents list according to the provided patterns.
When patterns are not empty, the search result is stored in `filteredCellDataArray`,
this array provides then data for the cells served by `MXKRecentsDataSource`.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the data for the cell at the given index.
@param index the index of the cell in the array
@return the cell data
*/
- (id<MXKRecentCellDataStoring>)cellDataAtIndex:(NSInteger)index;
/**
Get height of the cell at the given index.
@param index the index of the cell in the array
@return the cell height
*/
- (CGFloat)cellHeightAtIndex:(NSInteger)index;
@end
@@ -0,0 +1,552 @@
/*
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 "MXKSessionRecentsDataSource.h"
@import MatrixSDK;
#import "MXKRoomDataSourceManager.h"
#import "MXKSwiftHeader.h"
#pragma mark - Constant definitions
NSString *const kMXKRecentCellIdentifier = @"kMXKRecentCellIdentifier";
static NSTimeInterval const roomSummaryChangeThrottlerDelay = .5;
@interface MXKSessionRecentsDataSource ()
{
MXKRoomDataSourceManager *roomDataSourceManager;
/**
Internal array used to regulate change notifications.
Cell data changes are stored instantly in this array.
These changes are reported to the delegate only if no server sync is in progress.
*/
NSMutableArray *internalCellDataArray;
/**
Store the current search patterns list.
*/
NSArray* searchPatternsList;
/**
Do not react on every summary change
*/
MXThrottler *roomSummaryChangeThrottler;
/**
Last received suggested rooms per space ID
*/
NSMutableDictionary<NSString*, NSArray<MXSpaceChildInfo *> *> *lastSuggestedRooms;
/**
Event listener of the current space used to update the UI if an event occurs.
*/
id spaceEventsListener;
/**
Observer used to reload data when the space service is initialised
*/
id spaceServiceDidInitialiseObserver;
}
/**
Additional suggestedRooms related to the current selected Space
*/
@property (nonatomic, strong) NSArray<MXSpaceChildInfo *> *suggestedRooms;
@end
@implementation MXKSessionRecentsDataSource
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [super initWithMatrixSession:matrixSession];
if (self)
{
roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mxSession];
internalCellDataArray = [NSMutableArray array];
filteredCellDataArray = nil;
lastSuggestedRooms = [NSMutableDictionary new];
// Set default data and view classes
[self registerCellDataClass:MXKRecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier];
roomSummaryChangeThrottler = [[MXThrottler alloc] initWithMinimumDelay:roomSummaryChangeThrottlerDelay];
[[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllRoomsInHomeSpace" options:0 context:nil];
}
return self;
}
- (void)destroy
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDirectRoomsDidChangeNotification object:nil];
if (spaceServiceDidInitialiseObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:spaceServiceDidInitialiseObserver];
}
[roomSummaryChangeThrottler cancelAll];
roomSummaryChangeThrottler = nil;
cellDataArray = nil;
internalCellDataArray = nil;
filteredCellDataArray = nil;
lastSuggestedRooms = nil;
searchPatternsList = nil;
[[MXKAppSettings standardAppSettings] removeObserver:self forKeyPath:@"showAllRoomsInHomeSpace" context:nil];
[super destroy];
}
- (void)didMXSessionStateChange
{
if (MXSessionStateStoreDataReady <= self.mxSession.state)
{
// Check whether some data have been already load
if (0 == internalCellDataArray.count)
{
[self loadData];
}
else if (!roomDataSourceManager.isServerSyncInProgress)
{
// Sort cell data and notify the delegate
[self sortCellDataAndNotifyChanges];
}
}
}
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
if (_currentSpace == currentSpace)
{
return;
}
if (_currentSpace && spaceEventsListener)
{
[_currentSpace.room removeListener:spaceEventsListener];
}
_currentSpace = currentSpace;
self.suggestedRooms = _currentSpace ? lastSuggestedRooms[_currentSpace.spaceId] : nil;
[self updateSuggestedRooms];
MXWeakify(self);
spaceEventsListener = [self.currentSpace.room listenToEvents:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
[self updateSuggestedRooms];
}];
}
-(void)setSuggestedRooms:(NSArray<MXSpaceChildInfo *> *)suggestedRooms
{
_suggestedRooms = suggestedRooms;
[self loadData];
}
-(void)updateSuggestedRooms
{
if (self.currentSpace)
{
NSString *currentSpaceId = self.currentSpace.spaceId;
MXWeakify(self);
[self.mxSession.spaceService getSpaceChildrenForSpaceWithId:currentSpaceId suggestedOnly:YES limit:5 maxDepth:1 paginationToken:nil success:^(MXSpaceChildrenSummary * _Nonnull childrenSummary) {
MXLogDebug(@"[MXKSessionRecentsDataSource] getSpaceChildrenForSpaceWithId %@: %ld found", self.currentSpace.spaceId, childrenSummary.childInfos.count);
MXStrongifyAndReturnIfNil(self);
self->lastSuggestedRooms[currentSpaceId] = childrenSummary.childInfos;
if ([self.currentSpace.spaceId isEqual:currentSpaceId]) {
self.suggestedRooms = childrenSummary.childInfos;
}
} failure:^(NSError * _Nonnull error) {
MXLogError(@"[MXKSessionRecentsDataSource] getSpaceChildrenForSpaceWithId failed with error: %@", error);
}];
}
}
#pragma mark -
- (NSInteger)numberOfCells
{
if (filteredCellDataArray)
{
return filteredCellDataArray.count;
}
return cellDataArray.count;
}
- (BOOL)hasUnread
{
// Check all current cells
// Use numberOfRowsInSection methods so that we take benefit of the filtering
for (NSUInteger i = 0; i < self.numberOfCells; i++)
{
id<MXKRecentCellDataStoring> cellData = [self cellDataAtIndex:i];
if (cellData.hasUnread)
{
return YES;
}
}
return NO;
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
if (patternsList.count)
{
searchPatternsList = patternsList;
if (filteredCellDataArray)
{
[filteredCellDataArray removeAllObjects];
}
else
{
filteredCellDataArray = [NSMutableArray arrayWithCapacity:cellDataArray.count];
}
for (id<MXKRecentCellDataStoring> cellData in cellDataArray)
{
for (NSString* pattern in patternsList)
{
if (cellData.roomDisplayname && [cellData.roomDisplayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[filteredCellDataArray addObject:cellData];
break;
}
}
}
}
else
{
filteredCellDataArray = nil;
searchPatternsList = nil;
}
[self.delegate dataSource:self didCellChange:nil];
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndex:(NSInteger)index
{
if (filteredCellDataArray)
{
if (index < filteredCellDataArray.count)
{
return filteredCellDataArray[index];
}
}
else if (index < cellDataArray.count)
{
return cellDataArray[index];
}
return nil;
}
- (CGFloat)cellHeightAtIndex:(NSInteger)index
{
if (self.delegate)
{
id<MXKRecentCellDataStoring> cellData = [self cellDataAtIndex:index];
Class<MXKCellRendering> class = [self.delegate cellViewClassForCellData:cellData];
return [class heightForCellData:cellData withMaximumWidth:0];
}
return 0;
}
#pragma mark - Events processing
/**
Filtering in this method won't have any effect anymore. This class is not maintained.
*/
- (void)loadData
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDirectRoomsDidChangeNotification object:nil];
if (!self.mxSession.spaceService.isInitialised && !spaceServiceDidInitialiseObserver) {
MXWeakify(self);
spaceServiceDidInitialiseObserver = [[NSNotificationCenter defaultCenter] addObserverForName:MXSpaceService.didInitialise object:self.mxSession.spaceService queue:nil usingBlock:^(NSNotification * _Nonnull note) {
MXStrongifyAndReturnIfNil(self);
[self loadData];
}];
}
// Reset the table
[internalCellDataArray removeAllObjects];
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
NSAssert([class conformsToProtocol:@protocol(MXKRecentCellDataStoring)], @"MXKSessionRecentsDataSource only manages MXKCellData that conforms to MXKRecentCellDataStoring protocol");
NSDate *startDate = [NSDate date];
for (MXRoomSummary *roomSummary in self.mxSession.roomsSummaries)
{
// Filter out private rooms with conference users
if (!roomSummary.isConferenceUserRoom // @TODO Abstract this condition with roomSummary.hiddenFromUser
&& !roomSummary.hiddenFromUser)
{
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
}
}
}
for (MXSpaceChildInfo *childInfo in _suggestedRooms)
{
id<MXRoomSummaryProtocol> summary = [[MXRoomSummary alloc] initWithSpaceChildInfo:childInfo];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:summary
dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
}
}
MXLogDebug(@"[MXKSessionRecentsDataSource] Loaded %tu recents in %.3fms", self.mxSession.rooms.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
// Make sure all rooms have a last message
[self.mxSession fixRoomsSummariesLastMessage];
// Report loaded array except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
// Listen to MXSession rooms count changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionHaveNewRoom:) name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionDidLeaveRoom:) name:kMXSessionDidLeaveRoomNotification object:nil];
// Listen to the direct rooms list
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didDirectRoomsChange:) name:kMXSessionDirectRoomsDidChangeNotification object:nil];
// Listen to MXRoomSummary
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didRoomSummaryChanged:) name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionStateChange) name:kMXKRoomDataSourceSyncStatusChanged object:nil];
}
- (void)didDirectRoomsChange:(NSNotification *)notif
{
// Inform the delegate about the update
[self.delegate dataSource:self didCellChange:nil];
}
- (void)didRoomSummaryChanged:(NSNotification *)notif
{
[roomSummaryChangeThrottler throttle:^{
[self didRoomSummaryChanged2:notif];
}];
}
- (void)didRoomSummaryChanged2:(NSNotification *)notif
{
MXRoomSummary *roomSummary = notif.object;
if (roomSummary.mxSession == self.mxSession && internalCellDataArray.count)
{
// Find the index of the related cell data
NSInteger index = NSNotFound;
for (index = 0; index < internalCellDataArray.count; index++)
{
id<MXKRecentCellDataStoring> theRoomData = [internalCellDataArray objectAtIndex:index];
if (theRoomData.roomSummary == roomSummary)
{
break;
}
}
if (index < internalCellDataArray.count)
{
if (roomSummary.hiddenFromUser)
{
[internalCellDataArray removeObjectAtIndex:index];
}
else
{
// Create a new instance to not modify the content of 'cellDataArray' (the copy is not a deep copy).
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray replaceObjectAtIndex:index withObject:cellData];
}
}
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
else
{
MXLogDebug(@"[MXKSessionRecentsDataSource] didRoomLastMessageChanged: Cannot find the changed room summary for %@ (%@). It is probably not managed by this recents data source", roomSummary.roomId, roomSummary);
}
}
else
{
// Inform the delegate that all the room summaries have been updated.
[self.delegate dataSource:self didCellChange:nil];
}
}
- (void)didMXSessionHaveNewRoom:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if (mxSession == self.mxSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
// Add the room if there is not yet a cell for it
id<MXKRecentCellDataStoring> roomData = [self cellDataWithRoomId:roomId];
if (nil == roomData)
{
MXLogDebug(@"MXKSessionRecentsDataSource] Add newly joined room: %@", roomId);
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:roomId];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
}
}
}
- (void)didMXSessionDidLeaveRoom:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if (mxSession == self.mxSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
id<MXKRecentCellDataStoring> roomData = [self cellDataWithRoomId:roomId];
if (roomData)
{
MXLogDebug(@"MXKSessionRecentsDataSource] Remove left room: %@", roomId);
[internalCellDataArray removeObject:roomData];
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
}
}
// Order cells
- (void)sortCellDataAndNotifyChanges
{
// Order them by origin_server_ts
[internalCellDataArray sortUsingComparator:^NSComparisonResult(id<MXKRecentCellDataStoring> cellData1, id<MXKRecentCellDataStoring> cellData2)
{
return [cellData1.roomSummary.lastMessage compareOriginServerTs:cellData2.roomSummary.lastMessage];
}];
// Snapshot the cell data array
cellDataArray = [internalCellDataArray copy];
// Update search result if any
if (searchPatternsList)
{
[self searchWithPatterns:searchPatternsList];
}
// Update here data source state
if (state != MXKDataSourceStateReady)
{
state = MXKDataSourceStateReady;
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
// And inform the delegate about the update
[self.delegate dataSource:self didCellChange:nil];
}
// Find the cell data that stores information about the given room id
- (id<MXKRecentCellDataStoring>)cellDataWithRoomId:(NSString*)roomId
{
id<MXKRecentCellDataStoring> theRoomData;
NSMutableArray *dataArray = internalCellDataArray;
if (!roomDataSourceManager.isServerSyncInProgress)
{
dataArray = cellDataArray;
}
for (id<MXKRecentCellDataStoring> roomData in dataArray)
{
if ([roomData.roomSummary.roomId isEqualToString:roomId])
{
theRoomData = roomData;
break;
}
}
return theRoomData;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == [MXKAppSettings standardAppSettings] && [keyPath isEqualToString:@"showAllRoomsInHomeSpace"])
{
if (self.currentSpace == nil)
{
[self loadData];
}
}
}
@end