// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-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 "ConversationsViewController.h" #import "AppDelegate.h" #import "Conversation.h" #import "Contact.h" #import "ConversationCell.h" #import "ChatViewController.h" #import "ContactPickerViewController.h" #import "Utils.h" #import "NaClCrypto.h" #import "ProtocolDefines.h" #import "MyIdentityStore.h" #import "PortraitNavigationController.h" #import "EntityManager.h" #import "ErrorHandler.h" #import "GroupProxy.h" #import "ChatViewControllerCache.h" #import "DatabaseManager.h" #import "BrandingUtils.h" #import "MessageDraftStore.h" #import "MGSwipeTableCell.h" #import "ChatDefines.h" #import "MessageSender.h" #import "UIImage+ColoredImage.h" #import "ConversationUtils.h" #import "UserSettings.h" #import "NotificationManager.h" #import "LicenseStore.h" #import "BundleUtil.h" #import "SendMediaAction.h" #import "SendLocationAction.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif @interface ConversationsViewController () @property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController; @property id prevNavigationControllerDelegate; @property (copy) ChatViewControllerCompletionBlock chatViewCompletionBlock; @end @implementation ConversationsViewController { UIBarButtonItem *composeButtonItem; NSDate *lastAppearDate; Conversation *conversationToDelete; BOOL isEditing; BOOL viewLoadedInBackground; NSIndexPath *lastSelectedCell; BOOL canTransitionToLarge; BOOL canTransitionToSmall; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressbookSyncronized:) name:kNotificationAddressbookSyncronized object:nil]; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.leftBarButtonItem = self.editButtonItem; // Listen for unread messages count changes so we can update our title // and back button [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadMessagesCountChanged:) name:@"ThreemaUnreadMessagesCountChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshDirtyObjects:) name:kNotificationDBRefreshedDirtyObject object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(colorThemeChanged:) name:kNotificationColorThemeChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDraftForCell:) name:kNotificationUpdateDraftForCell object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showProfilePictureChanged:) name:kNotificationShowProfilePictureChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadTableView:) name:kNotificationBlockedContact object:nil]; self.clearsSelectionOnViewWillAppear = NO; [self registerForPreviewingWithDelegate:self sourceView:self.view]; [BrandingUtils updateTitleLogoOfNavigationItem:self.navigationItem navigationController:self.navigationController]; if (@available(iOS 11.0, *)) { self.navigationItem.largeTitleDisplayMode = [UserSettings sharedUserSettings].largeTitleDisplayMode; } canTransitionToLarge = false; canTransitionToSmall = true; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: NSLocalizedString(@"messages", nil) style: UIBarButtonItemStylePlain target: nil action: nil]; _createMessageBarButtonItem.accessibilityLabel = [BundleUtil localizedStringForKey:@"new_message_accessibility"]; if (SYSTEM_IS_IPAD == NO) { [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES]; } if (@available(iOS 11.0, *)) { self.tableView.estimatedSectionHeaderHeight = 0; self.tableView.estimatedSectionFooterHeight = 0; } self.tableView.estimatedRowHeight = 0.0; self.tableView.rowHeight = UITableViewAutomaticDimension; // iOS fix where the logo is moved to the right sometimes if (self.navigationController.navigationBar.frame.size.height == 44.0 && [LicenseStore requiresLicenseKey]) { [BrandingUtils updateTitleLogoOfNavigationItem:self.navigationItem navigationController:self.navigationController]; } else if (self.navigationController.navigationBar.frame.size.height == 44.0 && ![LicenseStore requiresLicenseKey] && self.navigationItem.titleView != nil) { [BrandingUtils updateTitleLogoOfNavigationItem:self.navigationItem navigationController:self.navigationController]; } // make sure conversations was updated if (![[AppDelegate sharedAppDelegate] isAppInBackground]) { viewLoadedInBackground = false; } else { viewLoadedInBackground = true; } // remove top space on tableview CGRect frame = CGRectZero; frame.size.height = CGFLOAT_MIN; [self.tableView setTableHeaderView:[[UIView alloc] initWithFrame:frame]]; [self refreshData]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self setEditing:NO animated:NO]; } - (void)refreshData { if (viewLoadedInBackground == false) { [_fetchedResultsController performFetch:nil]; [self.tableView reloadData]; } } - (void)colorThemeChanged:(NSNotification*)notification { [BrandingUtils updateTitleLogoOfNavigationItem:self.navigationItem navigationController:self.navigationController]; } - (void)checkDateAndUpdateTimestamps { NSDate *now = [NSDate date]; if (lastAppearDate != nil && ![Utils isSameDayWithDate1:lastAppearDate date2:now]) { DDLogInfo(@"Last appeared on a different date; updating timestamps"); if (viewLoadedInBackground == false) { [self.tableView reloadData]; } } lastAppearDate = now; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; self.fetchedResultsController = nil; } - (void)applicationWillEnterForeground:(NSNotification*)notification { [self checkDateAndUpdateTimestamps]; if (viewLoadedInBackground == true) { viewLoadedInBackground = false; [self refreshData]; } } - (void)unreadMessagesCountChanged:(NSNotification*)notification { int unread = ((NSNumber*)[notification.userInfo objectForKey:@"unread"]).intValue; NSString *backButtonTitle; if (unread > 0) { backButtonTitle = [NSString stringWithFormat:NSLocalizedString(@"bar_messages_x", nil), unread]; } else { backButtonTitle = NSLocalizedString(@"messages", nil); } UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:backButtonTitle style:UIBarButtonItemStylePlain target:nil action:nil]; dispatch_async(dispatch_get_main_queue(), ^{ self.navigationItem.title = NSLocalizedString(@"messages", nil); [[self navigationItem] setBackBarButtonItem:backButton]; }); } - (void)didReceiveMemoryWarning { DDLogWarn(@"Memory warning, removing cached chat view controllers"); [ChatViewControllerCache clearCache]; [super didReceiveMemoryWarning]; } - (void)addressbookSyncronized:(NSNotification*)notification { dispatch_async(dispatch_get_main_queue(), ^{ [ChatViewControllerCache clearCache]; [self.tableView reloadData]; }); } - (BOOL)shouldAutorotate { return YES; } -(UIInterfaceOrientationMask)supportedInterfaceOrientations { if (SYSTEM_IS_IPAD) { return UIInterfaceOrientationMaskAll; } return UIInterfaceOrientationMaskAllButUpsideDown; } - (IBAction)newMessage:(id)sender { /* have user pick a Contact for the new message/conversation first */ UINavigationController *contactPickerNavVc = [[self storyboard] instantiateViewControllerWithIdentifier:@"ContactPickerNav"]; [self presentViewController:contactPickerNavVc animated:YES completion:nil]; } - (void)setSelectionForConversation:(Conversation *)conversation { /* fix highlighted cell in our view */ NSIndexPath *selectedRow = [self.tableView indexPathForSelectedRow]; NSIndexPath *newRow = [self.fetchedResultsController indexPathForObject:conversation]; DDLogInfo(@"selectedRow: %@, newRow: %@", selectedRow, newRow); if (![selectedRow isEqual:newRow]) { if (selectedRow != nil) [self.tableView deselectRowAtIndexPath:selectedRow animated:NO]; if (newRow != nil) [self.tableView selectRowAtIndexPath:newRow animated:NO scrollPosition:UITableViewScrollPositionNone]; } } - (void)displayChat:(ChatViewController *)chatViewController animated:(BOOL)animated { [self setSelectionForConversation:chatViewController.conversation]; if (self.navigationController.topViewController == chatViewController) { return; } if ([self.navigationController.viewControllers containsObject:chatViewController]) { if (self.navigationController.topViewController.presentedViewController) { return; } [self.navigationController popToViewController:chatViewController animated:animated]; return; } [self.navigationController popToViewController:self animated:NO]; [self.navigationController pushViewController:chatViewController animated:animated]; } - (void)showConversationFor:(Contact*)contact overrideCompose:(BOOL)overrideCompose compose:(BOOL)compose defaultText:(NSString*)defaultText defaultImage:(UIImage *)defaultImage { UIViewController *topVc = [self.navigationController.viewControllers lastObject]; if (topVc.presentedViewController != nil && ![topVc.presentedViewController isKindOfClass:[PortraitNavigationController class]]) return; // modal view present and not passcode lock } - (Conversation *)getFirstConversation { if ([self hasData]) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; return [[self fetchedResultsController] objectAtIndexPath:indexPath]; } return nil; } - (BOOL)hasData { if ([self.fetchedResultsController.sections count] > 0) { id info = self.fetchedResultsController.sections[0]; if ([info numberOfObjects] > 0) { return YES; } } return NO; } - (void)setEditing:(BOOL)editing animated:(BOOL)animated { if (editing) { UIBarButtonItem *deleteButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"delete_all", nil) style:UIBarButtonItemStylePlain target:self action:@selector(deleteAllAction:)]; deleteButton.tintColor = [Colors red]; composeButtonItem = self.navigationItem.rightBarButtonItem; self.navigationItem.rightBarButtonItem = deleteButton; } else { if (composeButtonItem) { self.navigationItem.rightBarButtonItem = composeButtonItem; } } isEditing = editing; [super setEditing:editing animated:animated]; } - (void)deleteAllAction:(id)sender { UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"conversations_delete_all_confirm", nil) message:nil preferredStyle:UIAlertControllerStyleActionSheet]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"delete", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { EntityManager *entityManager = [[EntityManager alloc] init]; [entityManager performSyncBlockAndSafe:^{ NSArray *conversations = [entityManager.entityFetcher allConversations]; for (Conversation* conversation in conversations) { /* do not delete group conversations */ if (conversation.groupId == nil) [[entityManager entityDestroyer] deleteObjectWithObject:conversation]; } }]; self.editing = NO; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { }]]; if (!self.tabBarController) { actionSheet.popoverPresentationController.barButtonItem = self.navigationItem.rightBarButtonItem; actionSheet.popoverPresentationController.sourceView = self.view; } [self presentViewController:actionSheet animated:YES completion:nil]; } - (void)deleteConversation:(Conversation*)conversation { if (conversation == nil) return; if ([conversation isGroup]) { GroupProxy *group = [GroupProxy groupProxyForConversation:conversation]; [group adminDeleteGroup]; } [MessageDraftStore deleteDraftForConversation:conversation]; // Remove cached chat view controller for this conversation to avoid observer overload ChatViewController *oldController = [ChatViewControllerCache controllerForConversation:conversation]; [oldController removeConversationObservers]; [ChatViewControllerCache clearConversation:conversation]; /* Remove from Core Data */ EntityManager *entityManager = [[EntityManager alloc] init]; [entityManager performSyncBlockAndSafe:^{ [[entityManager entityDestroyer] deleteObjectWithObject:conversation]; }]; [[NotificationManager sharedInstance] updateUnreadMessagesCount:NO]; NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: conversation, kKeyConversation, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDeletedConversation object:nil userInfo:info]; } - (void)showAlertToDeleteConversation:(Conversation *)conversation cellRect:(CGRect)cellRect { /* If this was a group conversation with members, ask for confirmation */ GroupProxy *groupProxy = nil; BOOL isGroup = false; if (conversation.groupId != nil && conversation.members.count > 0) { groupProxy = [GroupProxy groupProxyForConversation:conversation]; isGroup = true; } if (isGroup && [groupProxy didLeaveGroup] == false) { conversationToDelete = conversation; NSString *message; if (conversation.contact == nil) { message = NSLocalizedString(@"group_admin_delete_confirm", nil); } else { message = NSLocalizedString(@"group_delete_confirm", nil); } UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleActionSheet]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"delete", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { [self deleteConversation:conversationToDelete]; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { }]]; if (!self.tabBarController) { actionSheet.popoverPresentationController.sourceRect = cellRect; actionSheet.popoverPresentationController.sourceView = self.view; } [self presentViewController:actionSheet animated:YES completion:nil]; } else { conversationToDelete = conversation; UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"conversation_delete_confirm", nil) message:nil preferredStyle:UIAlertControllerStyleActionSheet]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"delete", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { [self deleteConversation:conversationToDelete]; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { }]]; if (!self.tabBarController) { actionSheet.popoverPresentationController.sourceRect = cellRect; actionSheet.popoverPresentationController.sourceView = self.view; } [self presentViewController:actionSheet animated:YES completion:nil]; } } - (void)showAlertToLeaveGroup:(Conversation *)conversation cellRect:(CGRect)cellRect { /* If this was a group conversation with members, ask for confirmation */ GroupProxy *groupProxy = nil; BOOL isGroup = false; if (conversation.groupId != nil) { groupProxy = [GroupProxy groupProxyForConversation:conversation]; isGroup = true; } if (isGroup && [groupProxy isSelfMember] == true) { conversationToDelete = conversation; NSString *messageTitle = NSLocalizedString(@"leave_group", nil); NSString *message; if ([groupProxy isOwnGroup]) { message = [BundleUtil localizedStringForKey:@"group_admin_delete_confirm"]; } else { message = [BundleUtil localizedStringForKey:@"group_delete_confirm"]; } UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:messageTitle message:message preferredStyle:UIAlertControllerStyleActionSheet]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"leave", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { [groupProxy leaveGroup]; }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { }]]; if (!self.tabBarController) { actionSheet.popoverPresentationController.sourceRect = cellRect; actionSheet.popoverPresentationController.sourceView = self.view; } [self presentViewController:actionSheet animated:YES completion:nil]; } } #pragma mark - notification observer - (void)refreshDirtyObjects:(NSNotification*)notification { NSManagedObjectID *objectID = [notification.userInfo objectForKey:kKeyObjectID]; if (objectID && [objectID.entity.managedObjectClassName isEqualToString:@"Conversation"]) { dispatch_async(dispatch_get_main_queue(), ^{ [self refreshData]; }); } } - (void)updateDraftForCell:(NSNotification*)notification { ConversationCell *cell = [self.tableView cellForRowAtIndexPath:lastSelectedCell]; if (cell) [cell updateLastMessagePreview]; lastSelectedCell = self.tableView.indexPathForSelectedRow; } - (void)showProfilePictureChanged:(NSNotification*)notification { [self refresh]; } - (void)reloadTableView:(NSNotification *)notification { if (viewLoadedInBackground == false) { [self.tableView reloadData]; } } #pragma mark - Table view - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { //overwrite since conversation cells have custom UI } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return CGFLOAT_MIN; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[self.fetchedResultsController sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id sectionInfo = [self.fetchedResultsController sections][section]; return [sectionInfo numberOfObjects]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"ConversationCell"; ConversationCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; cell.conversation = [self.fetchedResultsController objectAtIndexPath:indexPath]; cell.delegate = self; cell.conversationCellDelegate = self; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { // MGSwipeTableCell already handles "swipe left" deletion of single conversation cells, // so we need to avoid triggering edit mode by swiping two cells left in succession. return isEditing; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { Conversation *conversation = [self.fetchedResultsController objectAtIndexPath:indexPath]; CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath]; [self showAlertToDeleteConversation:conversation cellRect:cellRect]; } } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (!lastSelectedCell) lastSelectedCell = indexPath; Conversation *conversation = [self.fetchedResultsController objectAtIndexPath:indexPath]; NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys: conversation, kKeyConversation, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationShowConversation object:nil userInfo:info]; } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section { return 0.0; } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForFooterInSection:(NSInteger)section { return 0.0; } - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)){ UITableViewCell *contextCell = [tableView cellForRowAtIndexPath:indexPath]; if ([contextCell isKindOfClass:[ConversationCell class]]) { ConversationCell *cell = (ConversationCell *)contextCell; ChatViewController *chatVc = [ChatViewControllerCache newControllerForConversation:cell.conversation forceTouch:YES]; UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{ return chatVc; } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { NSMutableArray *actionArray = [NSMutableArray new]; if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { NSString *actionTitle = [BundleUtil localizedStringForKey:@"take_photo_or_video"]; UIImage *actionImage = [[BundleUtil imageNamed:@"ActionCamera"] imageWithTintColor:[Colors fontNormal]]; UIAction *action = [UIAction actionWithTitle:actionTitle image:actionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController]; sendMediaAction.mediaPickerType = MediaPickerTakePhoto; [chatViewController setCurrentAction:sendMediaAction]; [sendMediaAction executeAction]; }]; [actionArray addObject:action]; } if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { NSString *actionTitle = [BundleUtil localizedStringForKey:@"choose_existing"]; UIImage *actionImage = [[BundleUtil imageNamed:@"ActionPhoto"] imageWithTintColor:[Colors fontNormal]]; UIAction *action = [UIAction actionWithTitle:actionTitle image:actionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; SendMediaAction *sendMediaAction = [SendMediaAction actionForChatViewController:chatViewController]; sendMediaAction.mediaPickerType = MediaPickerChooseExisting; [chatViewController setCurrentAction:sendMediaAction]; [sendMediaAction executeAction]; }]; [actionArray addObject:action]; } if ([CLLocationManager locationServicesEnabled]) { NSString *actionTitle = [BundleUtil localizedStringForKey:@"share_location"]; UIImage *actionImage = [[BundleUtil imageNamed:@"ActionLocation"] imageWithTintColor:[Colors fontNormal]]; UIAction *action = [UIAction actionWithTitle:actionTitle image:actionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; SendLocationAction *sendLocationAction = [SendLocationAction actionForChatViewController:chatViewController]; [chatViewController setCurrentAction:sendLocationAction]; [sendLocationAction executeAction]; }]; [actionArray addObject:action]; } if ([PlayRecordAudioViewController canRecordAudio]) { NSString *actionTitle = [BundleUtil localizedStringForKey:@"record_audio"]; UIImage *actionImage = [[BundleUtil imageNamed:@"ActionMicrophone"] imageWithTintColor:[Colors fontNormal]]; UIAction *action = [UIAction actionWithTitle:actionTitle image:actionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [chatViewController startRecordingAudio]; }); }]; [actionArray addObject:action]; } NSString *ballotActionTitle = NSLocalizedStringFromTable(@"ballot_create", @"Ballot", nil); UIImage *ballotActionImage = [[BundleUtil imageNamed:@"ActionBallot"] imageWithTintColor:[Colors fontNormal]]; UIAction *ballotAction = [UIAction actionWithTitle:ballotActionTitle image:ballotActionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; [chatViewController createBallot]; }]; [actionArray addObject:ballotAction]; NSString *shareActionTitle = [BundleUtil localizedStringForKey:@"share_file"]; UIImage *shareActionImage = [[BundleUtil imageNamed:@"ActionFile"] imageWithTintColor:[Colors fontNormal]]; UIAction *shareAction = [UIAction actionWithTitle:shareActionTitle image:shareActionImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { ChatViewController *chatViewController = [ChatViewControllerCache controllerForConversation:chatVc.conversation]; [chatViewController showContentAfterForceTouch]; [self displayChat:chatViewController animated:YES]; [chatViewController sendFile]; }]; [actionArray addObject:shareAction]; return [UIMenu menuWithTitle:chatVc.conversation.displayName image:nil identifier:nil options:UIMenuOptionsDisplayInline children:actionArray]; }]; return conf; } else { return nil; } } - (void)tableView:(UITableView *)tableView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)){ ChatViewController *previewVc = (ChatViewController *)animator.previewViewController; ChatViewController *chatVc = [ChatViewControllerCache controllerForConversation:previewVc.conversation]; [chatVc showContentAfterForceTouch]; [animator addCompletion:^{ [self displayChat:chatVc animated:YES]; }]; } #pragma mark - Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { if (_fetchedResultsController != nil) { return _fetchedResultsController; } EntityManager *entityManager = [[EntityManager alloc] init]; _fetchedResultsController = [entityManager.entityFetcher fetchedResultsControllerForConversations]; _fetchedResultsController.delegate = self; NSError *error = nil; if (![_fetchedResultsController performFetch:&error]) { DDLogError(@"Unresolved error %@, %@", error, [error userInfo]); [ErrorHandler abortWithError: error]; } return _fetchedResultsController; } - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; } - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { if (viewLoadedInBackground == false) { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } } - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { if (viewLoadedInBackground == false) { UITableView *tableView = self.tableView; switch(type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: { Conversation *conversation = anObject; [ChatViewControllerCache clearConversation:conversation]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; } case NSFetchedResultsChangeUpdate: { ConversationCell *cell = (ConversationCell*)[tableView cellForRowAtIndexPath:indexPath]; Conversation *conversation = anObject; if ([anObject changedValues].count != 0) { cell.conversation = conversation; } [cell refreshButtons:YES]; break; } case NSFetchedResultsChangeMove: if ([indexPath isEqual:newIndexPath] == NO) { [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } break; default: break; } } } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView endUpdates]; } #pragma mark - UIViewControllerPreviewingDelegate - (void)previewingContext:(id)previewingContext commitViewController:(UIViewController *)viewControllerToCommit { if ([viewControllerToCommit isKindOfClass:[ChatViewController class]]) { ChatViewController *previewVc = (ChatViewController *)viewControllerToCommit; ChatViewController *chatVc = [ChatViewControllerCache controllerForConversation:previewVc.conversation]; [chatVc showContentAfterForceTouch]; [self displayChat:chatVc animated:YES]; } } - (UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location { UIView *view = [self.view hitTest:location withEvent:nil]; if ([view.superview isKindOfClass:[ConversationCell class]]) { ConversationCell *cell = (ConversationCell *)view.superview; ChatViewController *chatVc = [ChatViewControllerCache newControllerForConversation:cell.conversation forceTouch:YES]; chatVc.delegate = self; return chatVc; } return nil; } #pragma mark - GroupDetailsViewControllerDelegate - (void)presentChatViewController:(ChatViewController *)chatViewController onCompletion:(ChatViewControllerCompletionBlock)onCompletion { _prevNavigationControllerDelegate = self.navigationController.delegate; self.navigationController.delegate = self; [chatViewController showContentAfterForceTouch]; _chatViewCompletionBlock = onCompletion; [self displayChat:chatViewController animated:NO]; } - (void)cancelSwipeGestureFromConversations { for (MGSwipeTableCell *cell in self.tableView.visibleCells) { if (cell.swipeState != MGSwipeStateNone) { [cell hideSwipeAnimated:true]; } } } - (void)pushSettingChanged:(Conversation *)conversation { NSIndexPath *indexPath = [self.fetchedResultsController indexPathForObject:conversation]; if (indexPath != nil) { ConversationCell *cell = (ConversationCell*)[self.tableView cellForRowAtIndexPath:indexPath]; cell.conversation = conversation; [cell refreshButtons:YES]; } } #pragma mark - UINavigationControllerDelegate - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { if (_chatViewCompletionBlock) { if ([viewController isKindOfClass:[ChatViewController class]]) { [((ChatViewController *)viewController) showContentAfterForceTouch]; _chatViewCompletionBlock((ChatViewController *)viewController); } _chatViewCompletionBlock = nil; } self.navigationController.delegate = _prevNavigationControllerDelegate; } #pragma mark Swipe Delegate -(BOOL)swipeTableCell:(MGSwipeTableCell*)cell canSwipe:(MGSwipeDirection)direction fromPoint:(CGPoint) point { if (UIAccessibilityIsVoiceOverRunning()) { return NO; } return YES; } -(NSArray*) swipeTableCell:(MGSwipeTableCell*) cell swipeButtonsForDirection:(MGSwipeDirection)direction swipeSettings:(MGSwipeSettings*) swipeSettings expansionSettings:(MGSwipeExpansionSettings*) expansionSettings { swipeSettings.transition = MGSwipeTransitionBorder; expansionSettings.buttonIndex = 0; NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; if (indexPath != nil) { Conversation *conversation = [self.fetchedResultsController objectAtIndexPath:indexPath]; if (direction == MGSwipeDirectionLeftToRight) { return [self swipeLeftToRightForTableCell:cell conversation:conversation swipeSettings:swipeSettings expansionSettings:expansionSettings]; } else { return [self swipeRightToLeftForTableCell:cell conversation:conversation swipeSettings:swipeSettings]; } } return nil; } - (NSArray *)swipeLeftToRightForTableCell:(MGSwipeTableCell *)cell conversation:(Conversation *)conversation swipeSettings:(MGSwipeSettings *)swipeSettings expansionSettings:(MGSwipeExpansionSettings *)expansionSettings { expansionSettings.fillOnTrigger = NO; expansionSettings.threshold = 1.5; swipeSettings.enableSwipeBounces = YES; __block NSString *buttonTitle; __block UIImage *buttonIcon; NSMutableArray *buttonArray = [NSMutableArray new]; if (conversation.unreadMessageCount.intValue > 0) { NSString *readTitle = NSLocalizedString(@"read", @""); buttonTitle = NSLocalizedString(@"read", @""); buttonIcon = [UIImage imageNamed:@"MessageStatus_read" inColor:[UIColor whiteColor]]; __block MGSwipeButton *read = [MGSwipeButton buttonWithTitle:buttonTitle icon:buttonIcon backgroundColor:[Colors workBlue] padding:10 callback:^BOOL(MGSwipeTableCell *sender) { [ConversationUtils unreadConversation:conversation]; [cell refreshButtons:YES]; [cell refreshContentView]; return YES; }]; CGRect readFrame = [readTitle boundingRectWithSize:CGSizeMake(self.view.frame.size.width/2, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName:read.titleLabel.font } context:nil]; [read setButtonWidth:readFrame.size.width + 30.0]; [read centerIconOverText]; [buttonArray addObject:read]; } NSString *markTitle = [conversation.marked isEqualToNumber:[NSNumber numberWithBool:YES]] ? NSLocalizedString(@"unpin", nil) : NSLocalizedString(@"pin", nil); UIImage *markImage = [conversation.marked isEqualToNumber:[NSNumber numberWithBool:YES]] ? [UIImage imageNamed:@"Unpin" inColor:[UIColor whiteColor]] : [UIImage imageNamed:@"Pin" inColor:[UIColor whiteColor]]; __block MGSwipeButton *mark = [MGSwipeButton buttonWithTitle:markTitle icon:markImage backgroundColor:[Colors markTag] padding:10 callback:^BOOL(MGSwipeTableCell *sender) { if ([conversation.marked isEqualToNumber:[NSNumber numberWithBool:YES]]) { [ConversationUtils unmarkConversation:conversation]; } else { [ConversationUtils markConversation:conversation]; } [cell refreshButtons:YES]; [cell refreshContentView]; return YES; }]; CGRect markFrame = [markTitle boundingRectWithSize:CGSizeMake(self.view.frame.size.width/2, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName:mark.titleLabel.font } context:nil]; [mark setButtonWidth:markFrame.size.width + 30.0]; [mark centerIconOverText]; [buttonArray addObject:mark]; return buttonArray; } - (NSArray *)swipeRightToLeftForTableCell:(MGSwipeTableCell *)cell conversation:(Conversation *)conversation swipeSettings:(MGSwipeSettings *)swipeSettings { swipeSettings.enableSwipeBounces = NO; MGSwipeButton *delete = [MGSwipeButton buttonWithTitle:NSLocalizedString(@"delete", @"") backgroundColor:[Colors red] padding:20 callback:^BOOL(MGSwipeTableCell *sender) { CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.view]; [self showAlertToDeleteConversation:conversation cellRect:cellRect]; return NO; }]; [delete centerIconOverText]; MGSwipeButton *leaveGroup; if (conversation.isGroup) { GroupProxy *group = [GroupProxy groupProxyForConversation:conversation]; if ([group isSelfMember]) { NSString *leaveGroupString = [BundleUtil localizedStringForKey:@"leave_group"]; NSString *strSpace = @" "; NSRange range = [leaveGroupString rangeOfString:strSpace]; if (NSNotFound != range.location) { leaveGroupString = [leaveGroupString stringByReplacingCharactersInRange:range withString:@"\n"]; } leaveGroup = [MGSwipeButton buttonWithTitle:leaveGroupString backgroundColor:[Colors orange] padding:20 callback:^BOOL(MGSwipeTableCell *sender) { CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.view]; [self showAlertToLeaveGroup:conversation cellRect:cellRect]; return NO; }]; [leaveGroup centerIconOverText]; } } if (leaveGroup != nil) { return @[leaveGroup, delete]; } else { return @[delete]; } } #pragma mark - ConversationCellDelegate - (void)voiceOverDeleteConversation:(ConversationCell *)cell { CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.view]; [self showAlertToDeleteConversation:cell.conversation cellRect:cellRect]; } - (void)voiceOverLeaveGroup:(ConversationCell *)cell { CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.view]; [self showAlertToLeaveGroup:cell.conversation cellRect:cellRect]; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (![LicenseStore requiresLicenseKey]) { if ([[self.navigationController navigationBar] frame].size.height < 60.0 && self.navigationItem.titleView != nil) { self.navigationItem.titleView = nil; self.navigationItem.title = NSLocalizedString(@"messages", nil); } else if ([[self.navigationController navigationBar] frame].size.height >= 59.5 && self.navigationItem.titleView == nil) { [BrandingUtils updateTitleLogoOfNavigationItem:self.navigationItem navigationController:self.navigationController]; } } } @end