/* Copyright 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 "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 inTableView:(UITableView*)tableView { 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)cellDataAtIndexPath:(NSIndexPath *)indexPath { id 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 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 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 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 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 roomData = [self cellDataAtIndexPath:indexPath]; if (roomData && self.delegate) { NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:roomData]; if (cellIdentifier) { UITableViewCell *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 updatedCellData = nil; if (numberOfUpdatedCells) { updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++]; } // Review all cell data items of the current list while (currentCellIndex < interleavedCellDataArray.count) { id 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