// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "ChatTableDataSource.h" #import "Contact.h" #import "ChatDefines.h" #import "UserSettings.h" #import "CachedCellHeight.h" #import "ChatSectionHeaderView.h" #import "UTIConverter.h" #import "SystemMessage.h" #import "TextMessage.h" #import "ImageMessage.h" #import "LocationMessage.h" #import "VideoMessage.h" #import "AudioMessage.h" #import "FileMessage.h" #import "LocationMessage.h" #import "BallotMessage.h" #import "UnreadMessageLine.h" #import "ChatTextMessageCell.h" #import "ChatVideoMessageCell.h" #import "ChatLocationMessageCell.h" #import "ChatContactCell.h" #import "ChatAudioMessageCell.h" #import "ChatBallotMessageCell.h" #import "ChatFileMessageCell.h" #import "UnreadMessageLineCell.h" #import "ChatCallMessageCell.h" #import "ValidationLogger.h" #import "Threema-Swift.h" #define SECTION_HEADER_PADDING 24.0 @interface SectionHeaderCacheElement : NSObject @property NSInteger section; @property CGFloat minY; @property ChatSectionHeaderView *sectionHeaderView; @end @implementation SectionHeaderCacheElement @end @interface ChatTableDataSource () // table sections @property NSMutableArray *dayArray; // rows per section @property NSMutableArray *messagesPerDayArray; // store section header views to show/hide later @property NSMutableSet *sectionHeaderViewCache; @property NSIndexPath *lastIndex; @property NSMutableDictionary *cellHeightCache; @property NSInteger loadedMessagesCount; @property NSIndexPath *unreadLineIndexPath; @end @implementation ChatTableDataSource - (instancetype)init { self = [super init]; if (self) { _messagesPerDayArray = [NSMutableArray array]; _dayArray = [NSMutableArray array]; _cellHeightCache = [[NSMutableDictionary alloc] init]; _sectionHeaderViewCache = [NSMutableSet set]; _forceShowSections = NO; _loadedMessagesCount = 0; _unreadLineIndexPath = nil; _openTableView = NO; } return self; } - (BOOL)hasData { if (_dayArray.count > 0) { return YES; } return NO; } - (NSIndexPath *)indexPathForLastCell { NSInteger sectionCount = _dayArray.count; if (sectionCount > 0) { NSArray *sectionData = [_messagesPerDayArray objectAtIndex:sectionCount - 1]; NSInteger rowCount = [sectionData count]; if (rowCount > 0) { return [NSIndexPath indexPathForRow:rowCount - 1 inSection:sectionCount - 1]; } } return nil; } - (NSIndexPath *)indexPathForMessage:(BaseMessage *)message { NSInteger section = 0; for (NSArray *sectionData in _messagesPerDayArray) { NSInteger row = 0; for (BaseMessage *indexMessage in sectionData) { if ([indexMessage isKindOfClass:[BaseMessage class]] && [indexMessage.id isEqual:message.id]) { return [NSIndexPath indexPathForRow:row inSection:section]; } row++; } section++; } return nil; } - (id)objectForIndexPath:(NSIndexPath *)indexPath { if (_messagesPerDayArray.count) { NSArray *sectionData = [_messagesPerDayArray objectAtIndex:indexPath.section]; return [sectionData objectAtIndex:indexPath.row]; } return nil; } - (CGFloat)getSentDateFontSize { float fontSize = roundf([UserSettings sharedUserSettings].chatFontSize * 13.0 / 16.0); if (fontSize < kSentDateMinFontSize) fontSize = kSentDateMinFontSize; else if (fontSize > kSentDateMaxFontSize) fontSize = kSentDateMaxFontSize; return fontSize; } // appends added sections and rows to newSections or newRows collections - (void)addMessage:(BaseMessage *)message newSections:(NSMutableIndexSet *)newSections newRows:(NSMutableArray *)newRows visible:(BOOL)visible { NSDate *currentSentDate = message.remoteSentDate; NSString *dayString = [DateFormatter relativeMediumDateFor:currentSentDate]; UIApplicationState applicationState = [[UIApplication sharedApplication] applicationState]; BOOL showUnreadLine = YES; NSMutableArray *sectionData; NSInteger sectionIndex = [_dayArray indexOfObject:dayString]; // fix for update from 2.8.0 to new version --> hide new messages line for the first time in a chat when first message is not read if (_messagesPerDayArray.count) { id firstMessage = _messagesPerDayArray[0][0]; if ([firstMessage isKindOfClass:[BaseMessage class]]) { if (!((BaseMessage *)firstMessage).read.boolValue && !((BaseMessage *)firstMessage).isOwn.boolValue) { showUnreadLine = NO; } } } if(sectionIndex != NSNotFound) { sectionData = [_messagesPerDayArray objectAtIndex:sectionIndex]; id lastObject = [sectionData lastObject]; if (lastObject && [lastObject isKindOfClass:[BaseMessage class]]) { BaseMessage * temp = (BaseMessage *)lastObject; BOOL addSender = NO; if (temp.sender != message.sender) addSender = YES; if ([temp isKindOfClass:[SystemMessage class]]) addSender = YES; /* add sender name in group conversation if necessary */ if (addSender && message.conversation.groupId != nil && !message.isOwn.boolValue && message.sender != nil) { BOOL isSystemMessage = NO; BOOL isCallSystemMessage = NO; if ([message isKindOfClass:[SystemMessage class]]) { isSystemMessage = YES; if ([((SystemMessage *)message) isCallType]) { isCallSystemMessage = YES; } } if (!isSystemMessage || (isSystemMessage && isCallSystemMessage)) { if (!_unreadLineIndexPath && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) { [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:[UnreadMessageLine new]]; _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex]; } [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:message.sender]; } } } } else { sectionIndex = _dayArray.count; sectionData = [NSMutableArray new]; [newSections addIndex:sectionIndex]; if (message.conversation.groupId != nil && !message.isOwn.boolValue && message.sender != nil) { BOOL isSystemMessage = NO; BOOL isCallSystemMessage = NO; if ([message isKindOfClass:[SystemMessage class]]) { isSystemMessage = YES; if ([((SystemMessage *)message) isCallType]) { isCallSystemMessage = YES; } } if (!isSystemMessage || (isSystemMessage && isCallSystemMessage)) { if (!_unreadLineIndexPath && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) { [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:[UnreadMessageLine new]]; _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex]; } [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:message.sender]; } } [_dayArray addObject:dayString]; [_messagesPerDayArray addObject:sectionData]; } if (!_unreadLineIndexPath && !message.isOwn.boolValue && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) { [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:[UnreadMessageLine new]]; _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex]; } [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]]; [sectionData addObject:message]; [_chatVC observeUpdatesForMessage:message]; _loadedMessagesCount++; _lastIndex = [newRows lastObject]; } - (void)removeObjectFromCellHeightCache:(NSIndexPath *)indexPath { [_cellHeightCache removeObjectForKey:indexPath]; } - (void)cleanCellHeightCache { [_cellHeightCache removeAllObjects]; } - (void)addObjectsFrom:(ChatTableDataSource *)otherDataSource { //note: other contains always later messages NSString *lastDay = [_dayArray lastObject]; NSString *otherFirstDay = [otherDataSource.dayArray firstObject]; if ([lastDay isEqualToString:otherFirstDay]) { // mix first day entries [_messagesPerDayArray.lastObject addObjectsFromArray:otherDataSource.messagesPerDayArray.firstObject]; // append the rest [self array:_dayArray addArray:otherDataSource.dayArray startingAtIndex:1]; [self array:_messagesPerDayArray addArray:otherDataSource.messagesPerDayArray startingAtIndex:1]; } else { // just append everything [_dayArray addObjectsFromArray:otherDataSource.dayArray]; [_messagesPerDayArray addObjectsFromArray:otherDataSource.messagesPerDayArray]; } _loadedMessagesCount += otherDataSource.numberOfLoadedMessages; } - (void)array:(NSMutableArray *)array addArray:(NSMutableArray *)otherArray startingAtIndex:(NSInteger)index { for (NSInteger i = index; i < otherArray.count; i++) { id obj = [otherArray objectAtIndex:i]; [array addObject:obj]; } } - (void)refreshSectionHeadersInTableView:(UITableView *)tableView { for (SectionHeaderCacheElement *cacheElement in _sectionHeaderViewCache) { CGRect sectionRect = cacheElement.sectionHeaderView.frame; CGFloat alpha = 0.9; CGFloat delay = 0.0; NSArray *indexPaths = [tableView indexPathsForVisibleRows]; if (!indexPaths || indexPaths.count == 0) { return; } NSIndexPath *firstVisibleIndexPath = [indexPaths objectAtIndex:0]; if (_forceShowSections) { delay = 0.8; } else if (firstVisibleIndexPath.section == cacheElement.section && sectionRect.origin.y > cacheElement.minY && cacheElement.minY < MAXFLOAT) { // if y offset remains at minY the section header frame does not cover any table cells in the corresponding section alpha = 0.0; delay = 0.6; } else { // section 0 should be more persistent -> add offset if (cacheElement.section == 0) { if (_chatVC.loadEarlierMessages.hidden == NO) { cacheElement.minY = sectionRect.size.height + _chatVC.loadEarlierMessages.frame.size.height; } else { cacheElement.minY = sectionRect.size.height; } } else { cacheElement.minY = sectionRect.origin.y; } } if (cacheElement.sectionHeaderView.alpha == alpha) { continue; } UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState; [UIView animateWithDuration:0.3 delay:delay options:options animations:^{ cacheElement.sectionHeaderView.alpha = alpha; } completion:^(BOOL finished) { ;//nop }]; } } - (NSInteger)numberOfLoadedMessages { return _loadedMessagesCount; } - (NSIndexPath *)getUnreadLineIndexPath { return _unreadLineIndexPath; } - (BOOL)removeUnreadLine { // remove unread from array if (_messagesPerDayArray.count && _unreadLineIndexPath) { NSMutableArray *chatSection = [_messagesPerDayArray objectAtIndex:_unreadLineIndexPath.section]; [chatSection removeObjectAtIndex:_unreadLineIndexPath.row]; [_cellHeightCache removeAllObjects]; _unreadLineIndexPath = nil; return YES; } return NO; } #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return _dayArray.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray *chatSection = [_messagesPerDayArray objectAtIndex:section]; return chatSection.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSObject *object = [self objectForIndexPath:indexPath]; if ([object isKindOfClass:[SystemMessage class]]) { if ([((SystemMessage *)object) isCallType]) { ChatMessageCell *cell = nil; static NSString *kCallMessageCell = @"CallMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kCallMessageCell]; if (cell == nil) { cell = [[ChatCallMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCallMessageCell transparent:YES]; } cell.chatVc = _chatVC; cell.message = (SystemMessage*)object; cell.typing = NO; if (indexPath == _lastIndex && _openTableView) { NSString *accessabilityText = [NSString stringWithFormat:@"%@%@", NSLocalizedString(@"new_message_accessibility", @""), cell.accessibilityLabelForContent]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, accessabilityText); } return cell; } else { if (((SystemMessage *)object).type.intValue == kSystemMessageContactOtherAppInfo) { static NSString *kSystemContactInfoMessageCell = @"SystemContactInfoMessageCell"; ChatContactInfoSystemMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:kSystemContactInfoMessageCell]; if (cell == nil) { cell = [[ChatContactInfoSystemMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kSystemContactInfoMessageCell]; } [cell setMessageWithSystemMessage:(SystemMessage*)object]; return cell; } else { static NSString *kSystemMessageCell = @"SystemMessageCell"; ChatSystemMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:kSystemMessageCell]; if (cell == nil) { cell = [[ChatSystemMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kSystemMessageCell]; } [cell setMessageWithSystemMessage:(SystemMessage*)object]; return cell; } } } else if ([object isKindOfClass:[BaseMessage class]]) { ChatMessageCell *cell = nil; if ([object isKindOfClass:[TextMessage class]]) { static NSString *kTextMessageCell = @"TextMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kTextMessageCell]; if (cell == nil) { cell = [[ChatTextMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kTextMessageCell transparent:YES]; } } else if ([object isKindOfClass:[ImageMessage class]]) { static NSString *kImageMessageCell = @"ImageMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kImageMessageCell]; if (cell == nil) { cell = [[ChatImageMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kImageMessageCell transparent:YES]; } } else if ([object isKindOfClass:[VideoMessage class]]) { static NSString *kVideoMessageCell = @"VideoMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kVideoMessageCell]; if (cell == nil) { cell = [[ChatVideoMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kVideoMessageCell transparent:YES]; } } else if ([object isKindOfClass:[LocationMessage class]]) { static NSString *kLocationMessageCell = @"LocationMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kLocationMessageCell]; if (cell == nil) { cell = [[ChatLocationMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kLocationMessageCell transparent:YES]; } } else if ([object isKindOfClass:[AudioMessage class]]) { static NSString *kAudioMessageCell = @"AudioMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kAudioMessageCell]; if (cell == nil) { cell = [[ChatAudioMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kAudioMessageCell transparent:YES]; } } else if ([object isKindOfClass:[BallotMessage class]]) { static NSString *kBallotMessageCell = @"BallotMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kBallotMessageCell]; if (cell == nil) { cell = [[ChatBallotMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kBallotMessageCell transparent:YES]; } } else if ([object isKindOfClass:[FileMessage class]]) { FileMessage *fileMessage = (FileMessage *)object; if ([fileMessage renderFileGifMessage] == true) { static NSString *kAnimGifMessageCell = @"AnimGifMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kAnimGifMessageCell]; if (cell == nil) { cell = [[ChatAnimatedGifMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kAnimGifMessageCell transparent:YES]; } } else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) { static NSString *kFileImageMessageCell = @"FileImageMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kFileImageMessageCell]; if (cell == nil) { cell = [[ChatFileImageMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kFileImageMessageCell transparent:YES]; } } else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) { static NSString *kFileVideoMessageCell = @"FileVideoMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kFileVideoMessageCell]; if (cell == nil) { cell = [[ChatFileVideoMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kFileVideoMessageCell transparent:YES]; } } else if ([fileMessage renderFileAudioMessage] == true) { static NSString *kFileAudioMessageCell = @"FileAudioMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kFileAudioMessageCell]; if (cell == nil) { cell = [[ChatFileAudioMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kFileAudioMessageCell transparent:YES]; } } else { static NSString *kFileMessageCell = @"FileMessageCell"; cell = [tableView dequeueReusableCellWithIdentifier:kFileMessageCell]; if (cell == nil) { cell = [[ChatFileMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kFileMessageCell transparent:YES]; } } } cell.chatVc = _chatVC; cell.message = (BaseMessage*)object; if (cell.message.conversation.typing.boolValue && [indexPath isEqual:_lastIndex]) { cell.typing = YES; } else { cell.typing = NO; } if (_searching) { [cell highlightOccurencesOf:_searchPattern]; } if (indexPath == _lastIndex && _openTableView) { NSString *accessabilityText = [NSString stringWithFormat:@"%@%@", NSLocalizedString(@"new_message_accessibility", @""), cell.accessibilityLabelForContent]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, accessabilityText); } return cell; } else if ([object isKindOfClass:[Contact class]]) { static NSString *kChatContactCell = @"ChatContactCell"; ChatContactCell *cell = [tableView dequeueReusableCellWithIdentifier:kChatContactCell]; if (cell == nil) { cell = [[ChatContactCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kChatContactCell]; cell.userInteractionEnabled = NO; } cell.contact = (Contact*)object; [cell setupColors]; return cell; } else if ([object isKindOfClass:[UnreadMessageLine class]]) { static NSString *kUnreadMessageLineCell = @"UnreadMessageLineCell"; UnreadMessageLineCell *cell = [tableView dequeueReusableCellWithIdentifier:kUnreadMessageLineCell]; if (cell == nil) { cell = [[UnreadMessageLineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kUnreadMessageLineCell]; cell.userInteractionEnabled = NO; } [cell setupColors]; return cell; } return nil; } #pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return UITableViewAutomaticDimension; } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section { CGFloat sentDateFontSize = [self getSentDateFontSize]; return sentDateFontSize + SECTION_HEADER_PADDING; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { CGFloat sentDateFontSize = [self getSentDateFontSize]; CGFloat viewHeight = sentDateFontSize + SECTION_HEADER_PADDING; CGRect frame = CGRectMake(0.0, 0.0, tableView.frame.size.width, viewHeight); ChatSectionHeaderView *headerView = [[ChatSectionHeaderView alloc] initWithFrame:frame]; headerView.fontSize = sentDateFontSize; headerView.text = [_dayArray objectAtIndex:section]; __block SectionHeaderCacheElement *foundObject; [_sectionHeaderViewCache enumerateObjectsUsingBlock:^(SectionHeaderCacheElement *cE, BOOL * _Nonnull stop) { if (cE.section == section) { foundObject = cE; *stop = YES; } }]; if (foundObject) { [_sectionHeaderViewCache removeObject:foundObject]; } SectionHeaderCacheElement *cacheElement = [SectionHeaderCacheElement new]; cacheElement.sectionHeaderView = headerView; cacheElement.section = section; cacheElement.minY = MAXFLOAT; [_sectionHeaderViewCache addObject:cacheElement]; return headerView; } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat curTableWidth; if (@available(iOS 11.0, *)) { curTableWidth = tableView.safeAreaLayoutGuide.layoutFrame.size.width; } else { curTableWidth = tableView.frame.size.width; } if (_rotationOverrideTableWidth > 0) { curTableWidth = _rotationOverrideTableWidth; } CachedCellHeight *cachedHeight = [_cellHeightCache objectForKey:indexPath]; if (cachedHeight != nil && cachedHeight.tableWidth == curTableWidth) { return cachedHeight.cellHeight; } NSObject *object = [self objectForIndexPath:indexPath]; CGFloat height = 0; CGFloat additionalBubbleMarging = 22.0f; // Set SentDateCell height. if ([object isKindOfClass:[NSDate class]]) { height = [self getSentDateFontSize] + 48.0f; } else if ([object isKindOfClass:[TextMessage class]]) { height = [ChatTextMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[ImageMessage class]]) { height = [ChatImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[VideoMessage class]]) { height = [ChatVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[LocationMessage class]]) { height = [ChatLocationMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[AudioMessage class]]) { height = [ChatAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[BallotMessage class]]) { height = [ChatBallotMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([object isKindOfClass:[FileMessage class]]) { FileMessage *fileMessage = (FileMessage *)object; if ([fileMessage renderFileGifMessage] == true) { height = [ChatAnimatedGifMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) { height = [ChatFileImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) { height = [ChatFileVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else if ([fileMessage renderFileAudioMessage] == true) { height = [ChatFileAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } else { height = [ChatFileMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging; } } else if ([object isKindOfClass:[Contact class]]) { height = 20; } else if ([object isKindOfClass:[SystemMessage class]]) { SystemMessage *message = (SystemMessage *)object; if ([message isCallType]) { height = [ChatCallMessageCell heightForMessage:message forTableWidth:curTableWidth] + additionalBubbleMarging; } else { if (message.type.intValue == kSystemMessageContactOtherAppInfo) { height = [ChatContactInfoSystemMessageCell heightFor:message forTableWidth:curTableWidth] + additionalBubbleMarging + 24.0; } else { height = [ChatSystemMessageCell heightFor:message forTableWidth:curTableWidth] + additionalBubbleMarging + 5.0; } } } else if ([object isKindOfClass:[UnreadMessageLine class]]) { height = 40; } if (height > 0) { cachedHeight = [[CachedCellHeight alloc] init]; cachedHeight.cellHeight = height; cachedHeight.tableWidth = curTableWidth; [_cellHeightCache setObject:cachedHeight forKey:indexPath]; } return height; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat curTableWidth; if (@available(iOS 11.0, *)) { curTableWidth = tableView.safeAreaLayoutGuide.layoutFrame.size.width; } else { curTableWidth = tableView.frame.size.width; } if (_rotationOverrideTableWidth > 0) { curTableWidth = _rotationOverrideTableWidth; } CachedCellHeight *cachedHeight = [_cellHeightCache objectForKey:indexPath]; if (cachedHeight != nil && cachedHeight.tableWidth == curTableWidth) { return cachedHeight.cellHeight; } NSObject *object = [self objectForIndexPath:indexPath]; CGFloat height = 0; // Set SentDateCell height. if ([object isKindOfClass:[NSDate class]]) { height = [self getSentDateFontSize] + 48.0f; } else if ([object isKindOfClass:[TextMessage class]]) { height = [ChatTextMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[ImageMessage class]]) { height = [ChatImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[VideoMessage class]]) { height = [ChatVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[LocationMessage class]]) { height = [ChatLocationMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[AudioMessage class]]) { height = [ChatAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[BallotMessage class]]) { height = [ChatBallotMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([object isKindOfClass:[FileMessage class]]) { FileMessage *fileMessage = (FileMessage *)object; if ([fileMessage renderFileGifMessage] == true) { height = [ChatAnimatedGifMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) { height = [ChatFileImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) { height = [ChatFileVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else if ([fileMessage renderFileAudioMessage] == true) { height = [ChatFileAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } else { height = [ChatFileMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f; } } else if ([object isKindOfClass:[Contact class]]) { height = 20; } else if ([object isKindOfClass:[SystemMessage class]]) { SystemMessage *message = (SystemMessage *)object; if ([message isCallType]) { height = [ChatCallMessageCell heightForMessage:message forTableWidth:curTableWidth] + 17.0; } else { if (message.type.intValue == kSystemMessageContactOtherAppInfo) { height = [ChatContactInfoSystemMessageCell heightFor:message forTableWidth:curTableWidth] + 40.0f; } else { height = [ChatSystemMessageCell heightFor:message forTableWidth:curTableWidth] + 22.0; } } } else if ([object isKindOfClass:[UnreadMessageLine class]]) { height = 40; } if (height > 0) { cachedHeight = [[CachedCellHeight alloc] init]; cachedHeight.cellHeight = height; cachedHeight.tableWidth = curTableWidth; [_cellHeightCache setObject:cachedHeight forKey:indexPath]; } return height; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { NSObject *object = [self objectForIndexPath:indexPath]; if (![object isKindOfClass:[BaseMessage class]]) { return NO; } /* don't allow image messages in sending progress to be deleted */ if ([object isKindOfClass:[ImageMessage class]]) { ImageMessage *message = (ImageMessage*)object; if (message.isOwn.boolValue && !message.sent.boolValue && !message.sendFailed.boolValue) return NO; } return YES; } - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleDelete; } - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { if (tableView.isEditing) { NSArray *selectedRows = [tableView indexPathsForSelectedRows]; _chatVC.navigationItem.leftBarButtonItem.title = (selectedRows.count == 0) ? NSLocalizedString(@"delete_all", nil) : [NSString stringWithFormat:NSLocalizedString(@"delete_n", nil), selectedRows.count]; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (tableView.isEditing) { NSArray *selectedRows = [tableView indexPathsForSelectedRows]; NSString *deleteButtonTitle = [NSString stringWithFormat:NSLocalizedString(@"delete_n", nil), selectedRows.count]; if (selectedRows.count == _chatVC.conversation.messages.count) { deleteButtonTitle = NSLocalizedString(@"delete_all", nil); } _chatVC.navigationItem.leftBarButtonItem.title = deleteButtonTitle; } } - (void)tableView:(UITableView *)tableView willDisplayCell:(nonnull UITableViewCell *)cell forRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if ([cell respondsToSelector:@selector(willDisplay)]) { [(ChatMessageCell *)cell willDisplay]; } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if ([cell respondsToSelector:@selector(didEndDisplaying)]) { [(ChatMessageCell *)cell didEndDisplaying]; } } - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)){ ChatMessageCell *messageCell = (ChatMessageCell *)[tableView cellForRowAtIndexPath:indexPath]; return [messageCell getContextMenu:indexPath point:point]; } - (UITargetedPreview *)tableView:(UITableView *)tableView previewForHighlightingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){ NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier; ChatMessageCell *messageCell = (ChatMessageCell *)[tableView cellForRowAtIndexPath:indexPath]; CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath]; UIView *superView = [tableView superview]; CGRect convertedRect = [tableView convertRect:cellRect toView:superView]; CGRect intersect = CGRectIntersection(tableView.frame, convertedRect); CGFloat height = intersect.size.height < _chatVC.chatContent.frame.size.height - 150.0 ? intersect.size.height : intersect.size.height - 150.0; CGRect visibleRect = CGRectMake(messageCell.frame.origin.x, intersect.origin.y - convertedRect.origin.y, messageCell.frame.size.width, height); NSMutableArray *clippingRectValuesInFrameCoordinates = [NSMutableArray new]; [clippingRectValuesInFrameCoordinates addObject:[NSValue valueWithCGRect:visibleRect]]; UIPreviewParameters *parameters = [[UIPreviewParameters alloc] initWithTextLineRects:clippingRectValuesInFrameCoordinates]; if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { if ([Colors getTheme] == ColorThemeDark || [Colors getTheme] == ColorThemeDarkWork) { parameters.backgroundColor = [UIColor colorWithRed:120.0/255.0 green:120.0/255.0 blue:120.0/255.0 alpha:0.9]; } else { parameters.backgroundColor = [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:0.9];; } } else { parameters.backgroundColor = [UIColor clearColor]; } return [[UITargetedPreview alloc] initWithView:messageCell parameters:parameters]; } - (UITargetedPreview *)tableView:(UITableView *)tableView previewForDismissingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){ return [self makeTargetedPreviewForConfiguration:configuration]; } - (UITargetedPreview *)makeTargetedPreviewForConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){ if (configuration.identifier == nil) { return nil; } NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier; if (indexPath == nil || self.chatVC.visible == false) { return nil; } UITableViewCell *cell = [self.chatVC.chatContent cellForRowAtIndexPath:indexPath]; if (cell != nil && [cell isKindOfClass:[ChatMessageCell class]]) { ChatMessageCell *messageCell = (ChatMessageCell *)cell; UIPreviewParameters *parameters = [[UIPreviewParameters alloc] init]; parameters.backgroundColor = [UIColor clearColor]; // The creation of UITargetedPreview crashes with the error message that it cannot find a window. if (cell.window != nil) { return [[UITargetedPreview alloc] initWithView:messageCell parameters:parameters]; } } return nil; } - (void)tableView:(UITableView *)tableView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)){ if ([animator.previewViewController isKindOfClass:[ThreemaSafariViewController class]]) { ThreemaSafariViewController *previewVc = (ThreemaSafariViewController *)animator.previewViewController; [animator addCompletion:^{ [[UIApplication sharedApplication] openURL:previewVc.url options:@{} completionHandler:nil]; }]; } else if ([animator.previewViewController isKindOfClass:[MWPhotoBrowser class]]) { NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier; ChatImageMessageCell *imageMessageCell = (ChatImageMessageCell *)[tableView cellForRowAtIndexPath:indexPath]; [animator addCompletion:^{ [self.chatVC imageMessageTapped:(ImageMessage*)imageMessageCell.message]; }]; } } #pragma mark - scroll view delegate // forward to chat view (table view delegate & scroll view delegate: there can only be one) - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { _forceShowSections = NO; [self refreshSectionHeadersInTableView:_chatVC.chatContent]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [_chatVC scrollViewDidScroll:scrollView]; [self refreshSectionHeadersInTableView:_chatVC.chatContent]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [_chatVC scrollViewWillBeginDragging:scrollView]; _forceShowSections = YES; } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { [_chatVC scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; if (fabs(velocity.y) < 0.2) { _forceShowSections = NO; [self refreshSectionHeadersInTableView:_chatVC.chatContent]; } } - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { return [_chatVC scrollViewShouldScrollToTop:scrollView]; } @end