ChatTableDataSource.m 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "ChatTableDataSource.h"
  21. #import "Contact.h"
  22. #import "ChatDefines.h"
  23. #import "UserSettings.h"
  24. #import "CachedCellHeight.h"
  25. #import "ChatSectionHeaderView.h"
  26. #import "UTIConverter.h"
  27. #import "SystemMessage.h"
  28. #import "TextMessage.h"
  29. #import "ImageMessage.h"
  30. #import "LocationMessage.h"
  31. #import "VideoMessage.h"
  32. #import "AudioMessage.h"
  33. #import "FileMessage.h"
  34. #import "LocationMessage.h"
  35. #import "BallotMessage.h"
  36. #import "UnreadMessageLine.h"
  37. #import "ChatTextMessageCell.h"
  38. #import "ChatVideoMessageCell.h"
  39. #import "ChatLocationMessageCell.h"
  40. #import "ChatContactCell.h"
  41. #import "ChatAudioMessageCell.h"
  42. #import "ChatBallotMessageCell.h"
  43. #import "ChatFileMessageCell.h"
  44. #import "UnreadMessageLineCell.h"
  45. #import "ChatCallMessageCell.h"
  46. #import "ValidationLogger.h"
  47. #import "Threema-Swift.h"
  48. #define SECTION_HEADER_PADDING 24.0
  49. @interface SectionHeaderCacheElement : NSObject
  50. @property NSInteger section;
  51. @property CGFloat minY;
  52. @property ChatSectionHeaderView *sectionHeaderView;
  53. @end
  54. @implementation SectionHeaderCacheElement
  55. @end
  56. @interface ChatTableDataSource ()
  57. // table sections
  58. @property NSMutableArray *dayArray;
  59. // rows per section
  60. @property NSMutableArray *messagesPerDayArray;
  61. // store section header views to show/hide later
  62. @property NSMutableSet *sectionHeaderViewCache;
  63. @property NSIndexPath *lastIndex;
  64. @property NSMutableDictionary *cellHeightCache;
  65. @property NSInteger loadedMessagesCount;
  66. @property NSIndexPath *unreadLineIndexPath;
  67. @end
  68. @implementation ChatTableDataSource
  69. - (instancetype)init
  70. {
  71. self = [super init];
  72. if (self) {
  73. _messagesPerDayArray = [NSMutableArray array];
  74. _dayArray = [NSMutableArray array];
  75. _cellHeightCache = [[NSMutableDictionary alloc] init];
  76. _sectionHeaderViewCache = [NSMutableSet set];
  77. _forceShowSections = NO;
  78. _loadedMessagesCount = 0;
  79. _unreadLineIndexPath = nil;
  80. _openTableView = NO;
  81. }
  82. return self;
  83. }
  84. - (BOOL)hasData {
  85. if (_dayArray.count > 0) {
  86. return YES;
  87. }
  88. return NO;
  89. }
  90. - (NSIndexPath *)indexPathForLastCell {
  91. NSInteger sectionCount = _dayArray.count;
  92. if (sectionCount > 0) {
  93. NSArray *sectionData = [_messagesPerDayArray objectAtIndex:sectionCount - 1];
  94. NSInteger rowCount = [sectionData count];
  95. if (rowCount > 0) {
  96. return [NSIndexPath indexPathForRow:rowCount - 1 inSection:sectionCount - 1];
  97. }
  98. }
  99. return nil;
  100. }
  101. - (NSIndexPath *)indexPathForMessage:(BaseMessage *)message {
  102. NSInteger section = 0;
  103. for (NSArray *sectionData in _messagesPerDayArray) {
  104. NSInteger row = 0;
  105. for (BaseMessage *indexMessage in sectionData) {
  106. if ([indexMessage isKindOfClass:[BaseMessage class]] && [indexMessage.id isEqual:message.id]) {
  107. return [NSIndexPath indexPathForRow:row inSection:section];
  108. }
  109. row++;
  110. }
  111. section++;
  112. }
  113. return nil;
  114. }
  115. - (id)objectForIndexPath:(NSIndexPath *)indexPath {
  116. if (_messagesPerDayArray.count) {
  117. NSArray *sectionData = [_messagesPerDayArray objectAtIndex:indexPath.section];
  118. return [sectionData objectAtIndex:indexPath.row];
  119. }
  120. return nil;
  121. }
  122. - (CGFloat)getSentDateFontSize {
  123. float fontSize = roundf([UserSettings sharedUserSettings].chatFontSize * 13.0 / 16.0);
  124. if (fontSize < kSentDateMinFontSize)
  125. fontSize = kSentDateMinFontSize;
  126. else if (fontSize > kSentDateMaxFontSize)
  127. fontSize = kSentDateMaxFontSize;
  128. return fontSize;
  129. }
  130. // appends added sections and rows to newSections or newRows collections
  131. - (void)addMessage:(BaseMessage *)message newSections:(NSMutableIndexSet *)newSections newRows:(NSMutableArray *)newRows visible:(BOOL)visible {
  132. NSDate *currentSentDate = message.remoteSentDate;
  133. NSString *dayString = [DateFormatter relativeMediumDateFor:currentSentDate];
  134. UIApplicationState applicationState = [[UIApplication sharedApplication] applicationState];
  135. BOOL showUnreadLine = YES;
  136. NSMutableArray *sectionData;
  137. NSInteger sectionIndex = [_dayArray indexOfObject:dayString];
  138. // 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
  139. if (_messagesPerDayArray.count) {
  140. id firstMessage = _messagesPerDayArray[0][0];
  141. if ([firstMessage isKindOfClass:[BaseMessage class]]) {
  142. if (!((BaseMessage *)firstMessage).read.boolValue && !((BaseMessage *)firstMessage).isOwn.boolValue) {
  143. showUnreadLine = NO;
  144. }
  145. }
  146. }
  147. if(sectionIndex != NSNotFound) {
  148. sectionData = [_messagesPerDayArray objectAtIndex:sectionIndex];
  149. id lastObject = [sectionData lastObject];
  150. if (lastObject && [lastObject isKindOfClass:[BaseMessage class]]) {
  151. BaseMessage * temp = (BaseMessage *)lastObject;
  152. BOOL addSender = NO;
  153. if (temp.sender != message.sender)
  154. addSender = YES;
  155. if ([temp isKindOfClass:[SystemMessage class]])
  156. addSender = YES;
  157. /* add sender name in group conversation if necessary */
  158. if (addSender && message.conversation.groupId != nil && !message.isOwn.boolValue && message.sender != nil) {
  159. BOOL isSystemMessage = NO;
  160. BOOL isCallSystemMessage = NO;
  161. if ([message isKindOfClass:[SystemMessage class]]) {
  162. isSystemMessage = YES;
  163. if ([((SystemMessage *)message) isCallType]) {
  164. isCallSystemMessage = YES;
  165. }
  166. }
  167. if (!isSystemMessage || (isSystemMessage && isCallSystemMessage)) {
  168. if (!_unreadLineIndexPath && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) {
  169. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  170. [sectionData addObject:[UnreadMessageLine new]];
  171. _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex];
  172. }
  173. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  174. [sectionData addObject:message.sender];
  175. }
  176. }
  177. }
  178. } else {
  179. sectionIndex = _dayArray.count;
  180. sectionData = [NSMutableArray new];
  181. [newSections addIndex:sectionIndex];
  182. if (message.conversation.groupId != nil && !message.isOwn.boolValue && message.sender != nil) {
  183. BOOL isSystemMessage = NO;
  184. BOOL isCallSystemMessage = NO;
  185. if ([message isKindOfClass:[SystemMessage class]]) {
  186. isSystemMessage = YES;
  187. if ([((SystemMessage *)message) isCallType]) {
  188. isCallSystemMessage = YES;
  189. }
  190. }
  191. if (!isSystemMessage || (isSystemMessage && isCallSystemMessage)) {
  192. if (!_unreadLineIndexPath && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) {
  193. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  194. [sectionData addObject:[UnreadMessageLine new]];
  195. _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex];
  196. }
  197. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  198. [sectionData addObject:message.sender];
  199. }
  200. }
  201. [_dayArray addObject:dayString];
  202. [_messagesPerDayArray addObject:sectionData];
  203. }
  204. if (!_unreadLineIndexPath && !message.isOwn.boolValue && !message.read.boolValue && showUnreadLine && (!visible || applicationState == UIApplicationStateBackground || applicationState == UIApplicationStateInactive)) {
  205. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  206. [sectionData addObject:[UnreadMessageLine new]];
  207. _unreadLineIndexPath = [NSIndexPath indexPathForRow:sectionData.count-1 inSection:sectionIndex];
  208. }
  209. [newRows addObject:[NSIndexPath indexPathForRow:sectionData.count inSection:sectionIndex]];
  210. [sectionData addObject:message];
  211. [_chatVC observeUpdatesForMessage:message];
  212. _loadedMessagesCount++;
  213. _lastIndex = [newRows lastObject];
  214. }
  215. - (void)removeObjectFromCellHeightCache:(NSIndexPath *)indexPath {
  216. [_cellHeightCache removeObjectForKey:indexPath];
  217. }
  218. - (void)cleanCellHeightCache {
  219. [_cellHeightCache removeAllObjects];
  220. }
  221. - (void)addObjectsFrom:(ChatTableDataSource *)otherDataSource {
  222. //note: other contains always later messages
  223. NSString *lastDay = [_dayArray lastObject];
  224. NSString *otherFirstDay = [otherDataSource.dayArray firstObject];
  225. if ([lastDay isEqualToString:otherFirstDay]) {
  226. // mix first day entries
  227. [_messagesPerDayArray.lastObject addObjectsFromArray:otherDataSource.messagesPerDayArray.firstObject];
  228. // append the rest
  229. [self array:_dayArray addArray:otherDataSource.dayArray startingAtIndex:1];
  230. [self array:_messagesPerDayArray addArray:otherDataSource.messagesPerDayArray startingAtIndex:1];
  231. } else {
  232. // just append everything
  233. [_dayArray addObjectsFromArray:otherDataSource.dayArray];
  234. [_messagesPerDayArray addObjectsFromArray:otherDataSource.messagesPerDayArray];
  235. }
  236. _loadedMessagesCount += otherDataSource.numberOfLoadedMessages;
  237. }
  238. - (void)array:(NSMutableArray *)array addArray:(NSMutableArray *)otherArray startingAtIndex:(NSInteger)index {
  239. for (NSInteger i = index; i < otherArray.count; i++) {
  240. id obj = [otherArray objectAtIndex:i];
  241. [array addObject:obj];
  242. }
  243. }
  244. - (void)refreshSectionHeadersInTableView:(UITableView *)tableView {
  245. for (SectionHeaderCacheElement *cacheElement in _sectionHeaderViewCache) {
  246. CGRect sectionRect = cacheElement.sectionHeaderView.frame;
  247. CGFloat alpha = 0.9;
  248. CGFloat delay = 0.0;
  249. NSArray *indexPaths = [tableView indexPathsForVisibleRows];
  250. if (!indexPaths || indexPaths.count == 0) {
  251. return;
  252. }
  253. NSIndexPath *firstVisibleIndexPath = [indexPaths objectAtIndex:0];
  254. if (_forceShowSections) {
  255. delay = 0.8;
  256. } else if (firstVisibleIndexPath.section == cacheElement.section && sectionRect.origin.y > cacheElement.minY && cacheElement.minY < MAXFLOAT) {
  257. // if y offset remains at minY the section header frame does not cover any table cells in the corresponding section
  258. alpha = 0.0;
  259. delay = 0.6;
  260. } else {
  261. // section 0 should be more persistent -> add offset
  262. if (cacheElement.section == 0) {
  263. if (_chatVC.loadEarlierMessages.hidden == NO) {
  264. cacheElement.minY = sectionRect.size.height + _chatVC.loadEarlierMessages.frame.size.height;
  265. } else {
  266. cacheElement.minY = sectionRect.size.height;
  267. }
  268. } else {
  269. cacheElement.minY = sectionRect.origin.y;
  270. }
  271. }
  272. if (cacheElement.sectionHeaderView.alpha == alpha) {
  273. continue;
  274. }
  275. UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
  276. [UIView animateWithDuration:0.3 delay:delay options:options animations:^{
  277. cacheElement.sectionHeaderView.alpha = alpha;
  278. } completion:^(BOOL finished) {
  279. ;//nop
  280. }];
  281. }
  282. }
  283. - (NSInteger)numberOfLoadedMessages {
  284. return _loadedMessagesCount;
  285. }
  286. - (NSIndexPath *)getUnreadLineIndexPath {
  287. return _unreadLineIndexPath;
  288. }
  289. - (BOOL)removeUnreadLine {
  290. // remove unread from array
  291. if (_messagesPerDayArray.count && _unreadLineIndexPath) {
  292. NSMutableArray *chatSection = [_messagesPerDayArray objectAtIndex:_unreadLineIndexPath.section];
  293. [chatSection removeObjectAtIndex:_unreadLineIndexPath.row];
  294. [_cellHeightCache removeAllObjects];
  295. _unreadLineIndexPath = nil;
  296. return YES;
  297. }
  298. return NO;
  299. }
  300. #pragma mark - UITableViewDataSource
  301. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  302. return _dayArray.count;
  303. }
  304. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  305. NSArray *chatSection = [_messagesPerDayArray objectAtIndex:section];
  306. return chatSection.count;
  307. }
  308. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  309. NSObject *object = [self objectForIndexPath:indexPath];
  310. if ([object isKindOfClass:[SystemMessage class]]) {
  311. if ([((SystemMessage *)object) isCallType]) {
  312. ChatMessageCell *cell = nil;
  313. static NSString *kCallMessageCell = @"CallMessageCell";
  314. cell = [tableView dequeueReusableCellWithIdentifier:kCallMessageCell];
  315. if (cell == nil) {
  316. cell = [[ChatCallMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCallMessageCell transparent:YES];
  317. }
  318. cell.chatVc = _chatVC;
  319. cell.message = (SystemMessage*)object;
  320. cell.typing = NO;
  321. if (indexPath == _lastIndex && _openTableView) {
  322. NSString *accessabilityText = [NSString stringWithFormat:@"%@%@", NSLocalizedString(@"new_message_accessibility", @""), cell.accessibilityLabelForContent];
  323. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, accessabilityText);
  324. }
  325. return cell;
  326. } else {
  327. if (((SystemMessage *)object).type.intValue == kSystemMessageContactOtherAppInfo) {
  328. static NSString *kSystemContactInfoMessageCell = @"SystemContactInfoMessageCell";
  329. ChatContactInfoSystemMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:kSystemContactInfoMessageCell];
  330. if (cell == nil) {
  331. cell = [[ChatContactInfoSystemMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kSystemContactInfoMessageCell];
  332. }
  333. [cell setMessageWithSystemMessage:(SystemMessage*)object];
  334. return cell;
  335. } else {
  336. static NSString *kSystemMessageCell = @"SystemMessageCell";
  337. ChatSystemMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:kSystemMessageCell];
  338. if (cell == nil) {
  339. cell = [[ChatSystemMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kSystemMessageCell];
  340. }
  341. [cell setMessageWithSystemMessage:(SystemMessage*)object];
  342. return cell;
  343. }
  344. }
  345. } else if ([object isKindOfClass:[BaseMessage class]]) {
  346. ChatMessageCell *cell = nil;
  347. if ([object isKindOfClass:[TextMessage class]]) {
  348. static NSString *kTextMessageCell = @"TextMessageCell";
  349. cell = [tableView dequeueReusableCellWithIdentifier:kTextMessageCell];
  350. if (cell == nil) {
  351. cell = [[ChatTextMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  352. reuseIdentifier:kTextMessageCell transparent:YES];
  353. }
  354. } else if ([object isKindOfClass:[ImageMessage class]]) {
  355. static NSString *kImageMessageCell = @"ImageMessageCell";
  356. cell = [tableView dequeueReusableCellWithIdentifier:kImageMessageCell];
  357. if (cell == nil) {
  358. cell = [[ChatImageMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  359. reuseIdentifier:kImageMessageCell transparent:YES];
  360. }
  361. } else if ([object isKindOfClass:[VideoMessage class]]) {
  362. static NSString *kVideoMessageCell = @"VideoMessageCell";
  363. cell = [tableView dequeueReusableCellWithIdentifier:kVideoMessageCell];
  364. if (cell == nil) {
  365. cell = [[ChatVideoMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  366. reuseIdentifier:kVideoMessageCell transparent:YES];
  367. }
  368. } else if ([object isKindOfClass:[LocationMessage class]]) {
  369. static NSString *kLocationMessageCell = @"LocationMessageCell";
  370. cell = [tableView dequeueReusableCellWithIdentifier:kLocationMessageCell];
  371. if (cell == nil) {
  372. cell = [[ChatLocationMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  373. reuseIdentifier:kLocationMessageCell transparent:YES];
  374. }
  375. } else if ([object isKindOfClass:[AudioMessage class]]) {
  376. static NSString *kAudioMessageCell = @"AudioMessageCell";
  377. cell = [tableView dequeueReusableCellWithIdentifier:kAudioMessageCell];
  378. if (cell == nil) {
  379. cell = [[ChatAudioMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  380. reuseIdentifier:kAudioMessageCell transparent:YES];
  381. }
  382. } else if ([object isKindOfClass:[BallotMessage class]]) {
  383. static NSString *kBallotMessageCell = @"BallotMessageCell";
  384. cell = [tableView dequeueReusableCellWithIdentifier:kBallotMessageCell];
  385. if (cell == nil) {
  386. cell = [[ChatBallotMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  387. reuseIdentifier:kBallotMessageCell transparent:YES];
  388. }
  389. } else if ([object isKindOfClass:[FileMessage class]]) {
  390. FileMessage *fileMessage = (FileMessage *)object;
  391. if ([fileMessage renderFileGifMessage] == true) {
  392. static NSString *kAnimGifMessageCell = @"AnimGifMessageCell";
  393. cell = [tableView dequeueReusableCellWithIdentifier:kAnimGifMessageCell];
  394. if (cell == nil) {
  395. cell = [[ChatAnimatedGifMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  396. reuseIdentifier:kAnimGifMessageCell transparent:YES];
  397. }
  398. }
  399. else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) {
  400. static NSString *kFileImageMessageCell = @"FileImageMessageCell";
  401. cell = [tableView dequeueReusableCellWithIdentifier:kFileImageMessageCell];
  402. if (cell == nil) {
  403. cell = [[ChatFileImageMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  404. reuseIdentifier:kFileImageMessageCell transparent:YES];
  405. }
  406. }
  407. else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) {
  408. static NSString *kFileVideoMessageCell = @"FileVideoMessageCell";
  409. cell = [tableView dequeueReusableCellWithIdentifier:kFileVideoMessageCell];
  410. if (cell == nil) {
  411. cell = [[ChatFileVideoMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  412. reuseIdentifier:kFileVideoMessageCell transparent:YES];
  413. }
  414. }
  415. else if ([fileMessage renderFileAudioMessage] == true) {
  416. static NSString *kFileAudioMessageCell = @"FileAudioMessageCell";
  417. cell = [tableView dequeueReusableCellWithIdentifier:kFileAudioMessageCell];
  418. if (cell == nil) {
  419. cell = [[ChatFileAudioMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  420. reuseIdentifier:kFileAudioMessageCell transparent:YES];
  421. }
  422. }
  423. else {
  424. static NSString *kFileMessageCell = @"FileMessageCell";
  425. cell = [tableView dequeueReusableCellWithIdentifier:kFileMessageCell];
  426. if (cell == nil) {
  427. cell = [[ChatFileMessageCell alloc] initWithStyle:UITableViewCellStyleDefault
  428. reuseIdentifier:kFileMessageCell transparent:YES];
  429. }
  430. }
  431. }
  432. cell.chatVc = _chatVC;
  433. cell.message = (BaseMessage*)object;
  434. if (cell.message.conversation.typing.boolValue && [indexPath isEqual:_lastIndex]) {
  435. cell.typing = YES;
  436. } else {
  437. cell.typing = NO;
  438. }
  439. if (_searching) {
  440. [cell highlightOccurencesOf:_searchPattern];
  441. }
  442. if (indexPath == _lastIndex && _openTableView) {
  443. NSString *accessabilityText = [NSString stringWithFormat:@"%@%@", NSLocalizedString(@"new_message_accessibility", @""), cell.accessibilityLabelForContent];
  444. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, accessabilityText);
  445. }
  446. return cell;
  447. } else if ([object isKindOfClass:[Contact class]]) {
  448. static NSString *kChatContactCell = @"ChatContactCell";
  449. ChatContactCell *cell = [tableView dequeueReusableCellWithIdentifier:kChatContactCell];
  450. if (cell == nil) {
  451. cell = [[ChatContactCell alloc] initWithStyle:UITableViewCellStyleDefault
  452. reuseIdentifier:kChatContactCell];
  453. cell.userInteractionEnabled = NO;
  454. }
  455. cell.contact = (Contact*)object;
  456. [cell setupColors];
  457. return cell;
  458. } else if ([object isKindOfClass:[UnreadMessageLine class]]) {
  459. static NSString *kUnreadMessageLineCell = @"UnreadMessageLineCell";
  460. UnreadMessageLineCell *cell = [tableView dequeueReusableCellWithIdentifier:kUnreadMessageLineCell];
  461. if (cell == nil) {
  462. cell = [[UnreadMessageLineCell alloc] initWithStyle:UITableViewCellStyleDefault
  463. reuseIdentifier:kUnreadMessageLineCell];
  464. cell.userInteractionEnabled = NO;
  465. }
  466. [cell setupColors];
  467. return cell;
  468. }
  469. return nil;
  470. }
  471. #pragma mark - UITableViewDelegate
  472. - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
  473. return UITableViewAutomaticDimension;
  474. }
  475. - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section {
  476. CGFloat sentDateFontSize = [self getSentDateFontSize];
  477. return sentDateFontSize + SECTION_HEADER_PADDING;
  478. }
  479. - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
  480. CGFloat sentDateFontSize = [self getSentDateFontSize];
  481. CGFloat viewHeight = sentDateFontSize + SECTION_HEADER_PADDING;
  482. CGRect frame = CGRectMake(0.0, 0.0, tableView.frame.size.width, viewHeight);
  483. ChatSectionHeaderView *headerView = [[ChatSectionHeaderView alloc] initWithFrame:frame];
  484. headerView.fontSize = sentDateFontSize;
  485. headerView.text = [_dayArray objectAtIndex:section];
  486. __block SectionHeaderCacheElement *foundObject;
  487. [_sectionHeaderViewCache enumerateObjectsUsingBlock:^(SectionHeaderCacheElement *cE, BOOL * _Nonnull stop) {
  488. if (cE.section == section) {
  489. foundObject = cE;
  490. *stop = YES;
  491. }
  492. }];
  493. if (foundObject) {
  494. [_sectionHeaderViewCache removeObject:foundObject];
  495. }
  496. SectionHeaderCacheElement *cacheElement = [SectionHeaderCacheElement new];
  497. cacheElement.sectionHeaderView = headerView;
  498. cacheElement.section = section;
  499. cacheElement.minY = MAXFLOAT;
  500. [_sectionHeaderViewCache addObject:cacheElement];
  501. return headerView;
  502. }
  503. - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
  504. CGFloat curTableWidth;
  505. if (@available(iOS 11.0, *)) {
  506. curTableWidth = tableView.safeAreaLayoutGuide.layoutFrame.size.width;
  507. } else {
  508. curTableWidth = tableView.frame.size.width;
  509. }
  510. if (_rotationOverrideTableWidth > 0) {
  511. curTableWidth = _rotationOverrideTableWidth;
  512. }
  513. CachedCellHeight *cachedHeight = [_cellHeightCache objectForKey:indexPath];
  514. if (cachedHeight != nil && cachedHeight.tableWidth == curTableWidth) {
  515. return cachedHeight.cellHeight;
  516. }
  517. NSObject *object = [self objectForIndexPath:indexPath];
  518. CGFloat height = 0;
  519. CGFloat additionalBubbleMarging = 22.0f;
  520. // Set SentDateCell height.
  521. if ([object isKindOfClass:[NSDate class]]) {
  522. height = [self getSentDateFontSize] + 48.0f;
  523. } else if ([object isKindOfClass:[TextMessage class]]) {
  524. height = [ChatTextMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  525. } else if ([object isKindOfClass:[ImageMessage class]]) {
  526. height = [ChatImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  527. } else if ([object isKindOfClass:[VideoMessage class]]) {
  528. height = [ChatVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  529. } else if ([object isKindOfClass:[LocationMessage class]]) {
  530. height = [ChatLocationMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  531. } else if ([object isKindOfClass:[AudioMessage class]]) {
  532. height = [ChatAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  533. } else if ([object isKindOfClass:[BallotMessage class]]) {
  534. height = [ChatBallotMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  535. } else if ([object isKindOfClass:[FileMessage class]]) {
  536. FileMessage *fileMessage = (FileMessage *)object;
  537. if ([fileMessage renderFileGifMessage] == true) {
  538. height = [ChatAnimatedGifMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  539. }
  540. else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) {
  541. height = [ChatFileImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  542. }
  543. else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) {
  544. height = [ChatFileVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  545. }
  546. else if ([fileMessage renderFileAudioMessage] == true) {
  547. height = [ChatFileAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  548. }
  549. else {
  550. height = [ChatFileMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + additionalBubbleMarging;
  551. }
  552. } else if ([object isKindOfClass:[Contact class]]) {
  553. height = 20;
  554. } else if ([object isKindOfClass:[SystemMessage class]]) {
  555. SystemMessage *message = (SystemMessage *)object;
  556. if ([message isCallType]) {
  557. height = [ChatCallMessageCell heightForMessage:message forTableWidth:curTableWidth] + additionalBubbleMarging;
  558. } else {
  559. if (message.type.intValue == kSystemMessageContactOtherAppInfo) {
  560. height = [ChatContactInfoSystemMessageCell heightFor:message forTableWidth:curTableWidth] + additionalBubbleMarging + 24.0;
  561. } else {
  562. height = [ChatSystemMessageCell heightFor:message forTableWidth:curTableWidth] + additionalBubbleMarging + 5.0;
  563. }
  564. }
  565. } else if ([object isKindOfClass:[UnreadMessageLine class]]) {
  566. height = 40;
  567. }
  568. if (height > 0) {
  569. cachedHeight = [[CachedCellHeight alloc] init];
  570. cachedHeight.cellHeight = height;
  571. cachedHeight.tableWidth = curTableWidth;
  572. [_cellHeightCache setObject:cachedHeight forKey:indexPath];
  573. }
  574. return height;
  575. }
  576. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  577. CGFloat curTableWidth;
  578. if (@available(iOS 11.0, *)) {
  579. curTableWidth = tableView.safeAreaLayoutGuide.layoutFrame.size.width;
  580. } else {
  581. curTableWidth = tableView.frame.size.width;
  582. }
  583. if (_rotationOverrideTableWidth > 0) {
  584. curTableWidth = _rotationOverrideTableWidth;
  585. }
  586. CachedCellHeight *cachedHeight = [_cellHeightCache objectForKey:indexPath];
  587. if (cachedHeight != nil && cachedHeight.tableWidth == curTableWidth) {
  588. return cachedHeight.cellHeight;
  589. }
  590. NSObject *object = [self objectForIndexPath:indexPath];
  591. CGFloat height = 0;
  592. // Set SentDateCell height.
  593. if ([object isKindOfClass:[NSDate class]]) {
  594. height = [self getSentDateFontSize] + 48.0f;
  595. } else if ([object isKindOfClass:[TextMessage class]]) {
  596. height = [ChatTextMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  597. } else if ([object isKindOfClass:[ImageMessage class]]) {
  598. height = [ChatImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  599. } else if ([object isKindOfClass:[VideoMessage class]]) {
  600. height = [ChatVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  601. } else if ([object isKindOfClass:[LocationMessage class]]) {
  602. height = [ChatLocationMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  603. } else if ([object isKindOfClass:[AudioMessage class]]) {
  604. height = [ChatAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  605. } else if ([object isKindOfClass:[BallotMessage class]]) {
  606. height = [ChatBallotMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  607. } else if ([object isKindOfClass:[FileMessage class]]) {
  608. FileMessage *fileMessage = (FileMessage *)object;
  609. if ([fileMessage renderFileGifMessage] == true) {
  610. height = [ChatAnimatedGifMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  611. }
  612. else if ([fileMessage renderFileImageMessage] == true && fileMessage.thumbnail != nil) {
  613. height = [ChatFileImageMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  614. }
  615. else if ([fileMessage renderFileVideoMessage] == true && fileMessage.thumbnail != nil) {
  616. height = [ChatFileVideoMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  617. }
  618. else if ([fileMessage renderFileAudioMessage] == true) {
  619. height = [ChatFileAudioMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  620. }
  621. else {
  622. height = [ChatFileMessageCell heightForMessage:(BaseMessage*)object forTableWidth:curTableWidth] + 17.0f;
  623. }
  624. } else if ([object isKindOfClass:[Contact class]]) {
  625. height = 20;
  626. } else if ([object isKindOfClass:[SystemMessage class]]) {
  627. SystemMessage *message = (SystemMessage *)object;
  628. if ([message isCallType]) {
  629. height = [ChatCallMessageCell heightForMessage:message forTableWidth:curTableWidth] + 17.0;
  630. } else {
  631. if (message.type.intValue == kSystemMessageContactOtherAppInfo) {
  632. height = [ChatContactInfoSystemMessageCell heightFor:message forTableWidth:curTableWidth] + 40.0f;
  633. } else {
  634. height = [ChatSystemMessageCell heightFor:message forTableWidth:curTableWidth] + 22.0;
  635. }
  636. }
  637. } else if ([object isKindOfClass:[UnreadMessageLine class]]) {
  638. height = 40;
  639. }
  640. if (height > 0) {
  641. cachedHeight = [[CachedCellHeight alloc] init];
  642. cachedHeight.cellHeight = height;
  643. cachedHeight.tableWidth = curTableWidth;
  644. [_cellHeightCache setObject:cachedHeight forKey:indexPath];
  645. }
  646. return height;
  647. }
  648. - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
  649. NSObject *object = [self objectForIndexPath:indexPath];
  650. if (![object isKindOfClass:[BaseMessage class]]) {
  651. return NO;
  652. }
  653. /* don't allow image messages in sending progress to be deleted */
  654. if ([object isKindOfClass:[ImageMessage class]]) {
  655. ImageMessage *message = (ImageMessage*)object;
  656. if (message.isOwn.boolValue && !message.sent.boolValue && !message.sendFailed.boolValue)
  657. return NO;
  658. }
  659. return YES;
  660. }
  661. - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
  662. return UITableViewCellEditingStyleDelete;
  663. }
  664. - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
  665. {
  666. if (tableView.isEditing) {
  667. NSArray *selectedRows = [tableView indexPathsForSelectedRows];
  668. _chatVC.navigationItem.leftBarButtonItem.title = (selectedRows.count == 0) ?
  669. NSLocalizedString(@"delete_all", nil) : [NSString stringWithFormat:NSLocalizedString(@"delete_n", nil), selectedRows.count];
  670. }
  671. }
  672. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  673. {
  674. if (tableView.isEditing) {
  675. NSArray *selectedRows = [tableView indexPathsForSelectedRows];
  676. NSString *deleteButtonTitle = [NSString stringWithFormat:NSLocalizedString(@"delete_n", nil), selectedRows.count];
  677. if (selectedRows.count == _chatVC.conversation.messages.count) {
  678. deleteButtonTitle = NSLocalizedString(@"delete_all", nil);
  679. }
  680. _chatVC.navigationItem.leftBarButtonItem.title = deleteButtonTitle;
  681. }
  682. }
  683. - (void)tableView:(UITableView *)tableView willDisplayCell:(nonnull UITableViewCell *)cell forRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
  684. if ([cell respondsToSelector:@selector(willDisplay)]) {
  685. [(ChatMessageCell *)cell willDisplay];
  686. }
  687. }
  688. - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  689. if ([cell respondsToSelector:@selector(didEndDisplaying)]) {
  690. [(ChatMessageCell *)cell didEndDisplaying];
  691. }
  692. }
  693. - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)){
  694. ChatMessageCell *messageCell = (ChatMessageCell *)[tableView cellForRowAtIndexPath:indexPath];
  695. return [messageCell getContextMenu:indexPath point:point];
  696. }
  697. - (UITargetedPreview *)tableView:(UITableView *)tableView previewForHighlightingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){
  698. NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier;
  699. ChatMessageCell *messageCell = (ChatMessageCell *)[tableView cellForRowAtIndexPath:indexPath];
  700. CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
  701. UIView *superView = [tableView superview];
  702. CGRect convertedRect = [tableView convertRect:cellRect toView:superView];
  703. CGRect intersect = CGRectIntersection(tableView.frame, convertedRect);
  704. CGFloat height = intersect.size.height < _chatVC.chatContent.frame.size.height - 150.0 ? intersect.size.height : intersect.size.height - 150.0;
  705. CGRect visibleRect = CGRectMake(messageCell.frame.origin.x, intersect.origin.y - convertedRect.origin.y, messageCell.frame.size.width, height);
  706. NSMutableArray *clippingRectValuesInFrameCoordinates = [NSMutableArray new];
  707. [clippingRectValuesInFrameCoordinates addObject:[NSValue valueWithCGRect:visibleRect]];
  708. UIPreviewParameters *parameters = [[UIPreviewParameters alloc] initWithTextLineRects:clippingRectValuesInFrameCoordinates];
  709. if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
  710. if ([Colors getTheme] == ColorThemeDark || [Colors getTheme] == ColorThemeDarkWork) {
  711. parameters.backgroundColor = [UIColor colorWithRed:120.0/255.0 green:120.0/255.0 blue:120.0/255.0 alpha:0.9];
  712. } else {
  713. parameters.backgroundColor = [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:0.9];;
  714. }
  715. } else {
  716. parameters.backgroundColor = [UIColor clearColor];
  717. }
  718. return [[UITargetedPreview alloc] initWithView:messageCell parameters:parameters];
  719. }
  720. - (UITargetedPreview *)tableView:(UITableView *)tableView previewForDismissingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){
  721. return [self makeTargetedPreviewForConfiguration:configuration];
  722. }
  723. - (UITargetedPreview *)makeTargetedPreviewForConfiguration:(UIContextMenuConfiguration *)configuration API_AVAILABLE(ios(13.0)){
  724. if (configuration.identifier == nil) {
  725. return nil;
  726. }
  727. NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier;
  728. if (indexPath == nil || self.chatVC.visible == false) {
  729. return nil;
  730. }
  731. UITableViewCell *cell = [self.chatVC.chatContent cellForRowAtIndexPath:indexPath];
  732. if (cell != nil && [cell isKindOfClass:[ChatMessageCell class]]) {
  733. ChatMessageCell *messageCell = (ChatMessageCell *)cell;
  734. UIPreviewParameters *parameters = [[UIPreviewParameters alloc] init];
  735. parameters.backgroundColor = [UIColor clearColor];
  736. // The creation of UITargetedPreview crashes with the error message that it cannot find a window.
  737. if (cell.window != nil) {
  738. return [[UITargetedPreview alloc] initWithView:messageCell parameters:parameters];
  739. }
  740. }
  741. return nil;
  742. }
  743. - (void)tableView:(UITableView *)tableView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionCommitAnimating>)animator API_AVAILABLE(ios(13.0)){
  744. if ([animator.previewViewController isKindOfClass:[ThreemaSafariViewController class]]) {
  745. ThreemaSafariViewController *previewVc = (ThreemaSafariViewController *)animator.previewViewController;
  746. [animator addCompletion:^{
  747. [[UIApplication sharedApplication] openURL:previewVc.url options:@{} completionHandler:nil];
  748. }];
  749. }
  750. else if ([animator.previewViewController isKindOfClass:[MWPhotoBrowser class]]) {
  751. NSIndexPath *indexPath = (NSIndexPath *)configuration.identifier;
  752. ChatImageMessageCell *imageMessageCell = (ChatImageMessageCell *)[tableView cellForRowAtIndexPath:indexPath];
  753. [animator addCompletion:^{
  754. [self.chatVC imageMessageTapped:(ImageMessage*)imageMessageCell.message];
  755. }];
  756. }
  757. }
  758. #pragma mark - scroll view delegate
  759. // forward to chat view (table view delegate & scroll view delegate: there can only be one)
  760. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  761. _forceShowSections = NO;
  762. [self refreshSectionHeadersInTableView:_chatVC.chatContent];
  763. }
  764. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  765. [_chatVC scrollViewDidScroll:scrollView];
  766. [self refreshSectionHeadersInTableView:_chatVC.chatContent];
  767. }
  768. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  769. [_chatVC scrollViewWillBeginDragging:scrollView];
  770. _forceShowSections = YES;
  771. }
  772. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
  773. [_chatVC scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
  774. if (fabs(velocity.y) < 0.2) {
  775. _forceShowSections = NO;
  776. [self refreshSectionHeadersInTableView:_chatVC.chatContent];
  777. }
  778. }
  779. - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
  780. return [_chatVC scrollViewShouldScrollToTop:scrollView];
  781. }
  782. @end