// // ActiveChatsViewController.m // Monal // // Created by Anurodh Pokharel on 6/14/13. // // #include "metamacros.h" #import #import "ActiveChatsViewController.h" #import "DataLayer.h" #import "xmpp.h" #import "MLContactCell.h" #import "chatViewController.h" #import "MonalAppDelegate.h" #import "MLImageManager.h" #import "MLXEPSlashMeHandler.h" #import "MLNotificationQueue.h" #import "MLSettingsAboutViewController.h" #import "MLVoIPProcessor.h" #import "MLCall.h" //for MLCallType #import "XMPPIQ.h" #import "MLIQProcessor.h" #import "Quicksy_Country.h" #import #define prependToViewQueue(firstArg, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self prependToViewQueue:firstArg withId:MLViewIDUnspecified andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_prependToViewQueue(firstArg, __VA_ARGS__)) #define _prependToViewQueue(ownId, block) [self prependToViewQueue:block withId:ownId andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__] #define appendToViewQueue(firstArg, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self appendToViewQueue:firstArg withId:MLViewIDUnspecified andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_appendToViewQueue(firstArg, __VA_ARGS__)) #define _appendToViewQueue(ownId, block) [self prependToViewQueue:block withId:ownId andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__] #define appendingReplaceOnViewQueue(firstArg, secondArg, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self replaceIdOnViewQueue:firstArg withBlock:secondArg havingId:MLViewIDUnspecified andAppendOnUnknown:YES withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_appendingReplaceOnViewQueue(firstArg, secondArg, __VA_ARGS__)) #define prependingReplaceOnViewQueue(firstArg, secondArg, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([self replaceIdOnViewQueue:firstArg withBlock:secondArg havingId:MLViewIDUnspecified andAppendOnUnknown:NO withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__])(_prependingReplaceOnViewQueue(firstArg, secondArg, __VA_ARGS__)) #define _appendingReplaceOnViewQueue(replaceId, ownId, block) [self replaceIdOnViewQueue:replaceId withBlock:block havingId:ownId andAppendOnUnknown:YES withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__] #define _prependingReplaceOnViewQueue(replaceId, ownId, block) [self replaceIdOnViewQueue:replaceId withBlock:block havingId:ownId andAppendOnUnknown:NO withFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__] typedef void (^view_queue_block_t)(PMKResolver _Nonnull); @import QuartzCore.CATransaction; @interface DZNEmptyDataSetView @property (atomic, strong) UIView* contentView; @property (atomic, strong) UIImageView* imageView; @property (atomic, strong) UILabel* titleLabel; @property (atomic, strong) UILabel* detailLabel; @end @interface UIScrollView () @property (nonatomic, readonly) DZNEmptyDataSetView* emptyDataSetView; @end @interface ActiveChatsViewController() { int _startedOrientation; double _portraitTop; double _landscapeTop; BOOL _loginAlreadyAutodisplayed; NSMutableArray* _blockQueue; dispatch_semaphore_t _blockQueueSemaphore; } @property (atomic, strong) NSMutableArray* unpinnedContacts; @property (atomic, strong) NSMutableArray* pinnedContacts; @end @implementation SizeClassWrapper @end @implementation ActiveChatsViewController enum activeChatsControllerSections { pinnedChats, unpinnedChats, activeChatsViewControllerSectionCnt }; typedef NS_ENUM(NSUInteger, MLViewID) { MLViewIDUnspecified, MLViewIDRegisterView, MLViewIDWelcomeLoginView, }; static NSMutableSet* _mamWarningDisplayed; static NSMutableSet* _smacksWarningDisplayed; static NSMutableSet* _pushWarningDisplayed; +(void) initialize { DDLogDebug(@"initializing active chats class"); _mamWarningDisplayed = [NSMutableSet new]; _smacksWarningDisplayed = [NSMutableSet new]; _pushWarningDisplayed = [NSMutableSet new]; } -(instancetype)initWithNibName:(NSString*) nibNameOrNil bundle:(NSBundle*) nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; [self commonInit]; return self; } -(instancetype) initWithStyle:(UITableViewStyle) style { self = [super initWithStyle:style]; [self commonInit]; return self; } -(instancetype) initWithCoder:(NSCoder*) coder { self = [super initWithCoder:coder]; [self commonInit]; return self; } -(void) commonInit { _blockQueue = [NSMutableArray new]; _blockQueueSemaphore = dispatch_semaphore_create(1); } -(void) resetViewQueue { [_blockQueue removeAllObjects]; } -(void) prependToViewQueue:(view_queue_block_t) block withId:(MLViewID) viewId andFile:(char*) file andLine:(int) line andFunc:(char*) func { @synchronized(_blockQueue) { DDLogDebug(@"Prepending block with id %lu defined in %s at %@:%d to queue...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); [_blockQueue insertObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) { DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); block(resolve); DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); }} atIndex:0]; } [self processViewQueue]; } -(void) appendToViewQueue:(view_queue_block_t) block withId:(MLViewID) viewId andFile:(char*) file andLine:(int) line andFunc:(char*) func { @synchronized(_blockQueue) { DDLogDebug(@"Appending block with id %lu defined in %s at %@:%d to queue...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); [_blockQueue addObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) { DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); block(resolve); DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); }}]; } [self processViewQueue]; } -(void) replaceIdOnViewQueue:(MLViewID) previousId withBlock:(view_queue_block_t) block havingId:(MLViewID) viewId andAppendOnUnknown:(BOOL) appendOnUnknown withFile:(char*) file andLine:(int) line andFunc:(char*) func { @synchronized(_blockQueue) { DDLogDebug(@"Replacing block with id %lu with new block having id %lu defined in %s at %@:%d to queue...", (unsigned long)previousId, (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); //search for old block to replace and remove it NSInteger index = -1; for(NSDictionary* blockInfo in _blockQueue) { index++; if(((NSNumber*)blockInfo[@"id"]).unsignedIntegerValue == previousId) { DDLogDebug(@"Found blockInfo at index %d: %@", (int)index, blockInfo); [self->_blockQueue removeObjectAtIndex:index]; break; } } if(index == -1) { if(appendOnUnknown) { DDLogDebug(@"Did not find block with id %lu on queue, appending block instead...", (unsigned long)previousId); [self appendToViewQueue:block withId:viewId andFile:file andLine:line andFunc:func]; } else { DDLogDebug(@"Did not find block with id %lu on queue, prepending block instead...", (unsigned long)previousId); [self prependToViewQueue:block withId:viewId andFile:file andLine:line andFunc:func]; } return; } //add replaement block at right position [_blockQueue insertObject:@{@"id":@(viewId), @"block":^(PMKResolver resolve) { DDLogDebug(@"Calling block with id %lu defined in %s at %@:%d...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); block(resolve); DDLogDebug(@"Block with id %lu defined in %s at %@:%d finished...", (unsigned long)viewId, func, [HelperTools sanitizeFilePath:file], line); }} atIndex:index]; } [self processViewQueue]; } -(void) processViewQueue { //we are using uikit api all over the place: make sure we always run in the main queue [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ NSMutableArray* viewControllerHierarchy = [self getCurrentViewControllerHierarchy]; //don't show the next entry if there is still the previous one //if(self.splitViewController.collapsed) if([viewControllerHierarchy count] > 0) { DDLogDebug(@"Ignoring call to processViewQueue, already showing: %@", viewControllerHierarchy); return; } //don't run the next block if the previous one did not yet complete if(dispatch_semaphore_wait(self->_blockQueueSemaphore, DISPATCH_TIME_NOW) != 0) { DDLogDebug(@"Ignoring call to processViewQueue, block still running, showing: %@", viewControllerHierarchy); return; } NSDictionary* blockInfo = nil; @synchronized(self->_blockQueue) { if(self->_blockQueue.count > 0) { blockInfo = [self->_blockQueue objectAtIndex:0]; [self->_blockQueue removeObjectAtIndex:0]; } else DDLogDebug(@"Queue is empty..."); } if(blockInfo) { //DDLogDebug(@"Calling next block, stacktrace: %@", [NSThread callStackSymbols]); monal_void_block_t looper = ^{ dispatch_semaphore_signal(self->_blockQueueSemaphore); DDLogDebug(@"Looping to next block..."); [self processViewQueue]; }; [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { ((view_queue_block_t)blockInfo[@"block"])(resolve); }].ensure(^{ looper(); }); } else { DDLogDebug(@"Not calling next block: there is none..."); dispatch_semaphore_signal(self->_blockQueueSemaphore); } }]; } #pragma mark view lifecycle -(void) configureComposeButton { UIImage* image = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.tintColor]; UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showContacts:)]; self.composeButton.customView = [HelperTools buttonWithNotificationBadgeForImage:image hasNotification:[[DataLayer sharedInstance] allContactRequests].count > 0 withTapHandler:tapRecognizer]; [self.composeButton setIsAccessibilityElement:YES]; if([[DataLayer sharedInstance] allContactRequests].count > 0) [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list (contact requests pending)", @"")]; else [self.composeButton setAccessibilityLabel:NSLocalizedString(@"Open contact list", @"")]; [self.composeButton setAccessibilityTraits:UIAccessibilityTraitButton]; } -(void) viewDidLoad { DDLogDebug(@"active chats view did load"); [super viewDidLoad]; _loginAlreadyAutodisplayed = NO; _startedOrientation = 0; self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; self.spinner.hidesWhenStopped = YES; self.view.backgroundColor = [UIColor lightGrayColor]; self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate]; appDelegate.activeChats = self; self.chatListTable = [UITableView new]; self.chatListTable.delegate = self; self.chatListTable.dataSource = self; self.view = self.chatListTable; self.sizeClass = [SizeClassWrapper new]; [self updateSizeClass]; NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalRefresh object:nil]; [nc addObserver:self selector:@selector(handleContactRemoved:) name:kMonalContactRemoved object:nil]; [nc addObserver:self selector:@selector(handleRefreshDisplayNotification:) name:kMonalMessageFiletransferUpdateNotice object:nil]; [nc addObserver:self selector:@selector(refreshContact:) name:kMonalContactRefresh object:nil]; [nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalNewMessageNotice object:nil]; [nc addObserver:self selector:@selector(handleNewMessage:) name:kMonalDeletedMessageNotice object:nil]; [nc addObserver:self selector:@selector(messageSent:) name:kMLMessageSentToContact object:nil]; [nc addObserver:self selector:@selector(handleDeviceRotation) name:UIDeviceOrientationDidChangeNotification object:nil]; [nc addObserver:self selector:@selector(showWarningsIfNeeded) name:kMonalFinishedCatchup object:nil]; [_chatListTable registerNib:[UINib nibWithNibName:@"MLContactCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"ContactCell"]; self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; #if !TARGET_OS_MACCATALYST self.splitViewController.primaryBackgroundStyle = UISplitViewControllerBackgroundStyleSidebar; #endif self.settingsButton.image = [UIImage systemImageNamed:@"gearshape.fill"]; [self configureComposeButton]; self.spinnerButton.customView = self.spinner; self.chatListTable.emptyDataSetSource = self; self.chatListTable.emptyDataSetDelegate = self; //has to be done here to not always prepend intro screens onto our view queue //once a fullscreen view is dismissed (or the app is switched to foreground) [self segueToIntroScreensIfNeeded]; } -(void) dealloc { DDLogDebug(@"active chats dealloc"); [[NSNotificationCenter defaultCenter] removeObserver:self]; } -(void) handleDeviceRotation { dispatch_async(dispatch_get_main_queue(), ^{ [self imageForEmptyDataSet:nil]; [self.chatListTable setNeedsDisplay]; }); } -(void) refreshDisplay { size_t unpinnedConCntBefore = self.unpinnedContacts.count; size_t pinnedConCntBefore = self.pinnedContacts.count; NSMutableArray* newUnpinnedContacts = [[DataLayer sharedInstance] activeContactsWithPinned:NO]; NSMutableArray* newPinnedContacts = [[DataLayer sharedInstance] activeContactsWithPinned:YES]; if(!newUnpinnedContacts || ! newPinnedContacts) return; int unpinnedCntDiff = (int)unpinnedConCntBefore - (int)newUnpinnedContacts.count; int pinnedCntDiff = (int)pinnedConCntBefore - (int)newPinnedContacts.count; void (^resizeSections)(UITableView*, size_t, int) = ^void(UITableView* table, size_t section, int diff){ if(diff > 0) { // remove rows for(int i = 0; i < diff; i++) { NSIndexPath* posInSection = [NSIndexPath indexPathForRow:i inSection:section]; [table deleteRowsAtIndexPaths:@[posInSection] withRowAnimation:UITableViewRowAnimationNone]; } } else if(diff < 0) { // add rows for(size_t i = (-1) * diff; i > 0; i--) { NSIndexPath* posInSectin = [NSIndexPath indexPathForRow:(i - 1) inSection:section]; [table insertRowsAtIndexPaths:@[posInSectin] withRowAnimation:UITableViewRowAnimationNone]; } } }; dispatch_async(dispatch_get_main_queue(), ^{ //make sure we don't display a chat view for a disabled account if([MLNotificationManager sharedInstance].currentContact != nil) { BOOL found = NO; for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList]) { NSNumber* accountID = accountDict[kAccountID]; if([MLNotificationManager sharedInstance].currentContact.accountID.intValue == accountID.intValue) found = YES; } if(!found) [self presentChatWithContact:nil]; } if(self.chatListTable.hasUncommittedUpdates) return; [CATransaction begin]; [UIView performWithoutAnimation:^{ [self.chatListTable beginUpdates]; resizeSections(self.chatListTable, unpinnedChats, unpinnedCntDiff); resizeSections(self.chatListTable, pinnedChats, pinnedCntDiff); self.unpinnedContacts = newUnpinnedContacts; self.pinnedContacts = newPinnedContacts; [self.chatListTable reloadSections:[NSIndexSet indexSetWithIndex:pinnedChats] withRowAnimation:UITableViewRowAnimationNone]; [self.chatListTable reloadSections:[NSIndexSet indexSetWithIndex:unpinnedChats] withRowAnimation:UITableViewRowAnimationNone]; [self.chatListTable endUpdates]; }]; [CATransaction commit]; [self.chatListTable reloadEmptyDataSet]; MonalAppDelegate* appDelegate = (MonalAppDelegate*)[UIApplication sharedApplication].delegate; [appDelegate updateUnread]; }); } -(void) refreshContact:(NSNotification*) notification { MLContact* contact = [notification.userInfo objectForKey:@"contact"]; DDLogInfo(@"Refreshing contact %@ at %@: unread=%lu", contact.contactJid, contact.accountID, (unsigned long)contact.unreadCount); //update red dot dispatch_async(dispatch_get_main_queue(), ^{ [self configureComposeButton]; }); // if pinning changed we have to move the user to a other section if([notification.userInfo objectForKey:@"pinningChanged"]) [self insertOrMoveContact:contact completion:nil]; else { dispatch_async(dispatch_get_main_queue(), ^{ NSIndexPath* indexPath = nil; for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++) { NSMutableArray* curContactArray = [self getChatArrayForSection:section]; // check if contact is already displayed -> get coresponding indexPath NSUInteger rowIdx = 0; for(MLContact* rowContact in curContactArray) { if([rowContact isEqualToContact:contact]) { //this MLContact instance is used in various ui parts, not just this file --> update all properties but keep the instance intact [rowContact updateWithContact:contact]; indexPath = [NSIndexPath indexPathForRow:rowIdx inSection:section]; break; } rowIdx++; } } // reload contact entry if we found it if(indexPath) { DDLogDebug(@"Reloading row at %@", indexPath); [self.chatListTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; } [self.chatListTable reloadEmptyDataSet]; }); } } -(void) handleRefreshDisplayNotification:(NSNotification*) notification { // filter notifcations from within this class if([notification.object isKindOfClass:[ActiveChatsViewController class]]) return; [self refresh]; } -(void) handleContactRemoved:(NSNotification*) notification { MLContact* removedContact = [notification.userInfo objectForKey:@"contact"]; if(removedContact == nil) unreachable(); dispatch_async(dispatch_get_main_queue(), ^{ DDLogInfo(@"Contact removed, refreshing active chats..."); //update red dot [self configureComposeButton]; // remove contact from activechats table [self refreshDisplay]; // open placeholder if the removed contact was "in foreground" if([removedContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) { DDLogInfo(@"Contact removed, closing chat view..."); [self presentChatWithContact:nil]; } }); } -(void) messageSent:(NSNotification*) notification { MLContact* contact = [notification.userInfo objectForKey:@"contact"]; if(!contact) unreachable(); [self insertOrMoveContact:contact completion:nil]; } -(void) handleNewMessage:(NSNotification*) notification { MLMessage* newMessage = notification.userInfo[@"message"]; MLContact* contact = notification.userInfo[@"contact"]; xmpp* msgAccount = (xmpp*)notification.object; if(!newMessage || !contact || !msgAccount) { unreachable(); return; } if([newMessage.messageType isEqualToString:kMessageTypeStatus]) return; // contact.statusMessage = newMessage; [self insertOrMoveContact:contact completion:nil]; } -(void) insertOrMoveContact:(MLContact*) contact completion:(void (^ _Nullable)(BOOL finished)) completion { dispatch_async(dispatch_get_main_queue(), ^{ [self.chatListTable performBatchUpdates:^{ __block NSIndexPath* indexPath = nil; for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++) { NSMutableArray* curContactArray = [self getChatArrayForSection:section]; // check if contact is already displayed -> get coresponding indexPath [curContactArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { MLContact* rowContact = (MLContact *) obj; if([rowContact isEqualToContact:contact]) { indexPath = [NSIndexPath indexPathForRow:idx inSection:section]; *stop = YES; } }]; } size_t insertInSection = unpinnedChats; if(contact.isPinned) { insertInSection = pinnedChats; } NSMutableArray* insertContactToArray = [self getChatArrayForSection:insertInSection]; NSIndexPath* insertAtPath = [NSIndexPath indexPathForRow:0 inSection:insertInSection]; if(indexPath && insertAtPath.section == indexPath.section && insertAtPath.row == indexPath.row) { [insertContactToArray replaceObjectAtIndex:insertAtPath.row withObject:contact]; [self.chatListTable reloadRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationNone]; return; } else if(indexPath) { // Contact is already in our active chats list NSMutableArray* removeContactFromArray = [self getChatArrayForSection:indexPath.section]; [self.chatListTable deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; [removeContactFromArray removeObjectAtIndex:indexPath.row]; [insertContactToArray insertObject:contact atIndex:0]; [self.chatListTable insertRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationNone]; } else { // Chats does not exists in active Chats yet NSUInteger oldCount = [insertContactToArray count]; [insertContactToArray insertObject:contact atIndex:0]; [self.chatListTable insertRowsAtIndexPaths:@[insertAtPath] withRowAnimation:UITableViewRowAnimationRight]; //make sure to fully refresh to remove the empty dataset (yes this will trigger on first chat pinning, too, but that does no harm) if(oldCount == 0) [self refreshDisplay]; } } completion:^(BOOL finished) { if(completion) completion(finished); }]; }); } -(void) viewWillAppear:(BOOL) animated { DDLogDebug(@"active chats view will appear"); [super viewWillAppear:animated]; [self presentSplitPlaceholder]; } -(void) viewWillDisappear:(BOOL) animated { DDLogDebug(@"active chats view will disappear"); [super viewWillDisappear:animated]; } -(void) viewDidAppear:(BOOL) animated { DDLogDebug(@"active chats view did appear"); [super viewDidAppear:animated]; [self refresh]; } -(void) sheetDismissed { [self refresh]; } -(void) refresh { dispatch_async(dispatch_get_main_queue(), ^{ [self refreshDisplay]; // load contacts [self processViewQueue]; }); } -(void) didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } -(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints { //check if contact is already known in any of our accounts and open a chat with the first contact we can find for(xmpp* checkAccount in [MLXMPPManager sharedInstance].connectedXMPP) { MLContact* checkContact = [MLContact createContactFromJid:jid andAccountID:checkAccount.accountID]; if(checkContact.isInRoster) { [self presentChatWithContact:checkContact]; return; } } appendToViewQueue((^(PMKResolver resolve) { UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewForJid:jid preauthToken:preauthToken prefillAccount:account andOmemoFingerprints:fingerprints withDismisser:^(MLContact* _Nonnull newContact) { [self presentChatWithContact:newContact]; }]; addContactMenuView.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:addContactMenuView animated:NO completion:^{resolve(nil);}]; }]; })); } -(void) showAddContact { appendToViewQueue((^(PMKResolver resolve) { UIViewController* addContactMenuView = [[SwiftuiInterface new] makeAddContactViewWithDismisser:^(MLContact* _Nonnull newContact) { [self presentChatWithContact:newContact]; }]; addContactMenuView.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:addContactMenuView animated:NO completion:^{resolve(nil);}]; }]; })); } -(void) segueToIntroScreensIfNeeded { DDLogDebug(@"segueToIntroScreensIfNeeded got called..."); //prepend in a prepend block to make sure we have prepended everything in order before showing the first view //(if we would not do this, the first view prepended would be shown regardless of other views prepended after it) //every entry in here is flipped, because we want to prepend all intro screens to our queue prependToViewQueue((^(PMKResolver resolve) { #ifdef IS_QUICKSY prependToViewQueue((^(PMKResolver resolve) { [self syncContacts]; resolve(nil); })); #else [self showWarningsIfNeeded]; #endif prependToViewQueue(MLViewIDWelcomeLoginView, (^(PMKResolver resolve) { #ifdef IS_QUICKSY if([[[DataLayer sharedInstance] accountList] count] == 0) { DDLogDebug(@"Showing account registration view..."); UIViewController* view = [[SwiftuiInterface new] makeAccountRegistration:@{}]; if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) view.modalPresentationStyle = UIModalPresentationFullScreen; else view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:view animated:NO completion:^{resolve(nil);}]; }]; } else resolve(nil); #else // display quick start if the user never seen it or if there are 0 enabled accounts if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0 && !self->_loginAlreadyAutodisplayed) { DDLogDebug(@"Showing WelcomeLogIn view..."); UIViewController* loginViewController = [[SwiftuiInterface new] makeViewWithName:@"WelcomeLogIn"]; loginViewController.ml_disposeCallback = ^{ self->_loginAlreadyAutodisplayed = YES; [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:loginViewController animated:YES completion:^{resolve(nil);}]; }]; } else resolve(nil); #endif })); prependToViewQueue((^(PMKResolver resolve) { if(![[HelperTools defaultsDB] boolForKey:@"hasCompletedOnboarding"]) { DDLogDebug(@"Showing onboarding view..."); UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"OnboardingView"]; if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) view.modalPresentationStyle = UIModalPresentationFullScreen; else view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:view animated:NO completion:^{resolve(nil);}]; }]; } else resolve(nil); })); prependToViewQueue((^(PMKResolver resolve) { //open password migration if needed NSArray* needingMigration = [[DataLayer sharedInstance] accountListNeedingPasswordMigration]; if(needingMigration.count > 0) { #ifdef IS_QUICKSY DDLogDebug(@"Showing account registration view to do password migration..."); UIViewController* view = [[SwiftuiInterface new] makeAccountRegistration:@{}]; if(UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPad) view.modalPresentationStyle = UIModalPresentationFullScreen; else view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:view animated:NO completion:^{resolve(nil);}]; }]; #else DDLogDebug(@"Showing password migration view..."); UIViewController* passwordMigration = [[SwiftuiInterface new] makePasswordMigration:needingMigration]; passwordMigration.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:passwordMigration animated:YES completion:^{resolve(nil);}]; }]; #endif } else resolve(nil); })); resolve(nil); })); } #ifdef IS_QUICKSY -(void) syncContacts { CNContactStore* store = [[CNContactStore alloc] init]; [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError* _Nullable error) { if(granted) { Quicksy_Country* country = [[HelperTools defaultsDB] objectForKey:@"Quicksy_country"]; NSString* countryCode = country.code; NSCharacterSet* allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] invertedSet]; NSMutableDictionary* numbers = [NSMutableDictionary new]; CNContactFetchRequest* request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactPhoneNumbersKey, CNContactNicknameKey, CNContactGivenNameKey, CNContactFamilyNameKey]]; NSError* error; [store enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact* _Nonnull contact, BOOL* _Nonnull stop) { if(!error) { NSString* name = [[NSString stringWithFormat:@"%@ %@", contact.givenName, contact.familyName] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for(CNLabeledValue* phone in contact.phoneNumbers) { //add country code if missing NSString* number = [[phone.value.stringValue componentsSeparatedByCharactersInSet:allowedCharacters] componentsJoinedByString:@""]; if(countryCode != nil && ![number hasPrefix:@"+"] && ![number hasPrefix:@"00"]) { DDLogVerbose(@"Adding country code '%@' to number: %@", countryCode, number); number = [NSString stringWithFormat:@"%@%@", countryCode, [number hasPrefix:@"0"] ? [number substringFromIndex:1] : number]; } numbers[number] = name; } } else DDLogWarn(@"Error fetching contacts: %@", error); }]; DDLogDebug(@"Got list of contact phone numbers: %@", numbers); NSArray* enabledAccounts = [MLXMPPManager sharedInstance].connectedXMPP; if(enabledAccounts.count == 0) { DDLogError(@"No connected account while trying to send quicksy phonebook!"); return; } else if(enabledAccounts.count > 1) DDLogWarn(@"More than 1 connected account while trying to send quicksy phonebook, using first one!"); XMPPIQ* iqNode = [[XMPPIQ alloc] initWithType:kiqGetType to:@"api.quicksy.im"]; [iqNode setQuicksyPhoneBook:numbers.allKeys]; [enabledAccounts[0] sendIq:iqNode withHandler:$newHandler(MLIQProcessor, handleQuicksyPhoneBook, $ID(numbers))]; } else DDLogError(@"Access to contacts not granted!"); }]; } #endif -(void) showWarningsIfNeeded { for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList]) { NSNumber* accountID = accountDict[kAccountID]; xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:accountID]; if(!account) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Connected xmpp* object for accountID is nil!" userInfo:accountDict]; prependToViewQueue((^(PMKResolver resolve) { if(![_mamWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) { if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:mam:2"]) { DDLogDebug(@"Showing MAM not supported warning..."); UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support MAM (XEP-0313). That means you could frequently miss incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_mamWarningDisplayed addObject:accountID]; resolve(nil); }]]; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:messageAlert animated:YES completion:nil]; }]; } else { [_mamWarningDisplayed addObject:accountID]; resolve(nil); } } else resolve(nil); })); prependToViewQueue((^(PMKResolver resolve) { if(![_smacksWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound) { if(!account.connectionProperties.supportsSM3) { DDLogDebug(@"Showing smacks not supported warning..."); UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support Stream Management (XEP-0198). That means your outgoing messages can get lost frequently!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_smacksWarningDisplayed addObject:accountID]; resolve(nil); }]]; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:messageAlert animated:YES completion:nil]; }]; } else { [_smacksWarningDisplayed addObject:accountID]; resolve(nil); } } else resolve(nil); })); prependToViewQueue((^(PMKResolver resolve) { if(![_pushWarningDisplayed containsObject:accountID] && account.accountState >= kStateBound && account.connectionProperties.accountDiscoDone) { if(![account.connectionProperties.accountDiscoFeatures containsObject:@"urn:xmpp:push:0"]) { DDLogDebug(@"Showing push not supported warning..."); UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Account %@", @""), account.connectionProperties.identity.jid] message:NSLocalizedString(@"Your server does not support PUSH (XEP-0357). That means you have to manually open the app to retrieve new incoming messages!! You should switch your server or talk to the server admin to enable this!", @"") preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) { [_pushWarningDisplayed addObject:accountID]; resolve(nil); }]]; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:messageAlert animated:YES completion:nil]; }]; } else { [_pushWarningDisplayed addObject:accountID]; resolve(nil); } } else resolve(nil); })); } } -(void) presentSplitPlaceholder { // only show placeholder if we use a split view if(!self.splitViewController.collapsed) { DDLogVerbose(@"Presenting Chat Placeholder..."); UIViewController* detailsViewController = [[SwiftuiInterface new] makeViewWithName:@"ChatPlaceholder"]; [self showDetailViewController:detailsViewController sender:self]; } [MLNotificationManager sharedInstance].currentContact = nil; } -(void) showNotificationSettings { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsNotificationSettings"]; view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self presentViewController:view animated:YES completion:nil]; }]; } -(void) prependGeneralSettings { prependToViewQueue((^(PMKResolver resolve) { UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"]; view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:view animated:YES completion:^{resolve(nil);}]; }]; })); } -(void) showGeneralSettings { appendToViewQueue((^(PMKResolver resolve) { UIViewController* view = [[SwiftuiInterface new] makeViewWithName:@"ActiveChatsGeneralSettings"]; view.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:view animated:YES completion:^{resolve(nil);}]; }]; })); } -(void) showSettings { appendToViewQueue((^(PMKResolver resolve) { [self performSegueWithIdentifier:@"showSettings" sender:self]; resolve(nil); })); } -(void) showCallContactNotFoundAlert:(NSString*) jid { UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Contact not found", @"") message:[NSString stringWithFormat:NSLocalizedString(@"You tried to call contact '%@' but this contact could not be found in your contact list.", @""), jid] preferredStyle:UIAlertControllerStyleAlert]; [messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {}]]; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:messageAlert animated:NO completion:nil]; }]; } -(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType { MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; MLCall* activeCall = [appDelegate.voipProcessor getActiveCallWithContact:contact]; if(activeCall != nil) [self presentCall:activeCall]; else [self presentCall:[appDelegate.voipProcessor initiateCallWithType:callType toContact:contact]]; } -(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender { MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; MLCall* activeCall = [appDelegate.voipProcessor getActiveCallWithContact:contact]; if(activeCall != nil) [self presentCall:activeCall]; else { UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Call Type", @"") message:NSLocalizedString(@"What call do you want to place?", @"") preferredStyle:UIAlertControllerStyleActionSheet]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"🎵 Audio", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; [self presentCall:[appDelegate.voipProcessor initiateCallWithType:MLCallTypeAudio toContact:contact]]; }]]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"🎥 Video", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; [self presentCall:[appDelegate.voipProcessor initiateCallWithType:MLCallTypeVideo toContact:contact]]; }]]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; }]]; UIPopoverPresentationController* popPresenter = [alert popoverPresentationController]; if(sender != nil) popPresenter.sourceItem = sender; else popPresenter.sourceView = self.view; [self presentViewController:alert animated:YES completion:nil]; } } -(void) presentAccountPickerForContacts:(NSArray*) contacts andCallType:(MLCallType) callType { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* accountPickerController = [[SwiftuiInterface new] makeAccountPickerForContacts:contacts andCallType:callType]; accountPickerController.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self presentViewController:accountPickerController animated:YES completion:^{}]; }]; } -(void) presentCall:(MLCall*) call { [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ UIViewController* callViewController = [[SwiftuiInterface new] makeCallScreenForCall:call]; callViewController.modalPresentationStyle = UIModalPresentationFullScreen; [self presentViewController:callViewController animated:NO completion:^{}]; }]; } -(void) presentChatWithContact:(MLContact*) contact { return [self presentChatWithContact:contact andCompletion:nil]; } -(void) presentChatWithContact:(MLContact*) contact andCompletion:(monal_id_block_t _Nullable) completion { DDLogVerbose(@"presenting chat with contact: %@, stacktrace: %@", contact, [NSThread callStackSymbols]); [HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ [self dismissCompleteViewChainWithAnimation:YES andCompletion:^{ // only open contact chat when it is not opened yet (needed for opening via notifications and for macOS) if([contact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) { // make sure the already open chat is reloaded and return [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; if(completion != nil) completion(@YES); return; } // clear old chat before opening a new one (but not for splitView == YES) if(self.splitViewController.collapsed) [self.navigationController popViewControllerAnimated:NO]; // show placeholder if contact is nil, open chat otherwise if(contact == nil) { [self presentSplitPlaceholder]; if(completion != nil) completion(@NO); return; } //open chat (make sure we have an active buddy for it and add it to our ui, if needed) //but don't animate this if the contact is already present in our list [[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountID]; if([[self getChatArrayForSection:pinnedChats] containsObject:contact] || [[self getChatArrayForSection:unpinnedChats] containsObject:contact]) { [self scrollToContact:contact]; [self performSegueWithIdentifier:@"showConversation" sender:contact]; if(completion != nil) completion(@YES); } else { [self insertOrMoveContact:contact completion:^(BOOL finished __unused) { [self scrollToContact:contact]; [self performSegueWithIdentifier:@"showConversation" sender:contact]; if(completion != nil) completion(@YES); }]; } }]; }]; } /* * return YES if no enabled account was found && a alert will open */ -(BOOL) showAccountNumberWarningIfNeeded { // Only open contacts list / roster if at least one account is enabled if([[DataLayer sharedInstance] enabledAccountCnts].intValue == 0) { // Show warning UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No enabled account found", @"") message:NSLocalizedString(@"Please add a new account under settings first. If you already added your account you may need to enable it under settings", @"") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { [alert dismissViewControllerAnimated:YES completion:nil]; }]]; [self presentViewController:alert animated:YES completion:nil]; return YES; } return NO; } -(BOOL) shouldPerformSegueWithIdentifier:(NSString*) identifier sender:(id) sender { return YES; } //this is needed to prevent segues invoked programmatically -(void) performSegueWithIdentifier:(NSString*) identifier sender:(id) sender { [super performSegueWithIdentifier:identifier sender:sender]; } -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender { DDLogInfo(@"Got segue identifier '%@'", segue.identifier); if([segue.identifier isEqualToString:@"showConversation"]) { UINavigationController* nav = segue.destinationViewController; chatViewController* chatVC = (chatViewController*)nav.topViewController; UIBarButtonItem* barButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; self.navigationItem.backBarButtonItem = barButtonItem; [chatVC setupWithContact:sender]; } else if([segue.identifier isEqualToString:@"showDetails"]) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:sender]; detailsViewController.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } } -(void) updateSizeClass { self.sizeClass.horizontal = self.view.traitCollection.horizontalSizeClass; } -(NSMutableArray*) getChatArrayForSection:(size_t) section { NSMutableArray* chatArray = nil; if(section == pinnedChats) { chatArray = self.pinnedContacts; } else if(section == unpinnedChats) { chatArray = self.unpinnedContacts; } return chatArray; } #pragma mark - tableview datasource -(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView { return activeChatsViewControllerSectionCnt; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if(section == pinnedChats) { return [self.pinnedContacts count]; } else if(section == unpinnedChats) { return [self.unpinnedContacts count]; } else { return 0; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MLContactCell* cell = (MLContactCell*)[tableView dequeueReusableCellWithIdentifier:@"ContactCell" forIndexPath:indexPath]; MLContact* chatContact = nil; // Select correct contact array if(indexPath.section == pinnedChats) chatContact = [self.pinnedContacts objectAtIndex:indexPath.row]; else chatContact = [self.unpinnedContacts objectAtIndex:indexPath.row]; // Display msg draft or last msg MLMessage* messageRow = [[DataLayer sharedInstance] lastMessageForContact:chatContact.contactJid forAccount:chatContact.accountID]; [cell initCell:chatContact withLastMessage:messageRow]; cell.selectionStyle = UITableViewCellSelectionStyleNone; // Highlight the selected chat if([MLNotificationManager sharedInstance].currentContact != nil && [chatContact isEqual:[MLNotificationManager sharedInstance].currentContact]) { cell.backgroundColor = [UIColor lightGrayColor]; cell.statusText.textColor = [UIColor whiteColor]; } else { cell.backgroundColor = [UIColor clearColor]; cell.statusText.textColor = [UIColor lightGrayColor]; } return cell; } #pragma mark - tableview delegate -(CGFloat) tableView:(UITableView*) tableView heightForRowAtIndexPath:(NSIndexPath*) indexPath { return 60.0f; } -(NSString*) tableView:(UITableView*) tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath*) indexPath { return NSLocalizedString(@"Archive chat", @""); } -(BOOL) tableView:(UITableView*) tableView canEditRowAtIndexPath:(NSIndexPath*) indexPath { return YES; } -(BOOL)tableView:(UITableView*) tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath*) indexPath { return YES; } -(void)tableView:(UITableView*) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath*) indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { MLContact* contact = nil; // Delete contact from view if(indexPath.section == pinnedChats) { contact = [self.pinnedContacts objectAtIndex:indexPath.row]; [self.pinnedContacts removeObjectAtIndex:indexPath.row]; } else { contact = [self.unpinnedContacts objectAtIndex:indexPath.row]; [self.unpinnedContacts removeObjectAtIndex:indexPath.row]; } [self.chatListTable deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; // removeActiveBuddy in db [[DataLayer sharedInstance] removeActiveBuddy:contact.contactJid forAccount:contact.accountID]; // remove contact from activechats table [self refreshDisplay]; // open placeholder [self presentChatWithContact:nil]; } } -(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath { MLContact* selected = nil; if(indexPath.section == pinnedChats) { selected = self.pinnedContacts[indexPath.row]; } else { selected = self.unpinnedContacts[indexPath.row]; } [self presentChatWithContact:selected]; } -(void) tableView:(UITableView*) tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath*) indexPath { MLContact* selected = nil; if(indexPath.section == pinnedChats) { selected = self.pinnedContacts[indexPath.row]; } else { selected = self.unpinnedContacts[indexPath.row]; } UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:selected]; detailsViewController.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self presentViewController:detailsViewController animated:YES completion:^{}]; } #pragma mark - empty data set -(void) viewWillTransitionToSize:(CGSize) size withTransitionCoordinator:(id) coordinator { //DDLogError(@"Transitioning to size: %@", NSStringFromCGSize(size)); [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; } -(UIImage*) imageForEmptyDataSet:(UIScrollView*) scrollView { int orientation; if(self.tableView.frame.size.height > self.tableView.frame.size.width) { orientation = 1; //portrait _portraitTop = self.navigationController.navigationBar.frame.size.height; } else { orientation = 2; //landscape _landscapeTop = self.navigationController.navigationBar.frame.size.height; } if(_startedOrientation == 0) _startedOrientation = orientation; //DDLogError(@"started orientation: %@", _startedOrientation == 1 ? @"portrait" : @"landscape"); //DDLogError(@"current orientation: %@", orientation == 1 ? @"portrait" : @"landscape"); DZNEmptyDataSetView* emptyDataSetView = self.tableView.emptyDataSetView; CGRect headerFrame = self.navigationController.navigationBar.frame; CGRect tableFrame = self.tableView.frame; //CGRect contentFrame = emptyDataSetView.contentView.frame; //DDLogError(@"headerFrame: %@", NSStringFromCGRect(headerFrame)); //DDLogError(@"tableFrame: %@", NSStringFromCGRect(tableFrame)); //DDLogError(@"contentFrame: %@", NSStringFromCGRect(contentFrame)); tableFrame.size.height *= 0.5; //started in landscape, moved to portrait if(_startedOrientation == 2 && orientation == 1) { tableFrame.origin.y += headerFrame.size.height - _landscapeTop - _portraitTop; } //started in portrait, moved to landscape else if(_startedOrientation == 1 && orientation == 2) { tableFrame.origin.y += (_portraitTop + _landscapeTop * 2); tableFrame.size.height -= _portraitTop; } //started in any orientation, moved to same orientation (or just started) else { tableFrame.origin.y += headerFrame.size.height; } emptyDataSetView.contentView.frame = tableFrame; emptyDataSetView.imageView.frame = tableFrame; [emptyDataSetView.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]-(32@750)-[titleLabel]-(16@750)-[detailLabel]|" options:0 metrics:nil views:@{ @"imageView": emptyDataSetView.imageView, @"titleLabel": emptyDataSetView.titleLabel, @"detailLabel": emptyDataSetView.detailLabel, }]]; emptyDataSetView.imageView.translatesAutoresizingMaskIntoConstraints = YES; if(self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) return [UIImage imageNamed:@"chat_dark"]; return [UIImage imageNamed:@"chat"]; /* DZNEmptyDataSetView* emptyDataSetView = self.chatListTable.emptyDataSetView; CGRect headerFrame = self.navigationController.navigationBar.frame; CGRect tableFrame = self.chatListTable.frame; CGRect contentFrame = emptyDataSetView.contentView.frame; DDLogError(@"headerFrame: %@", NSStringFromCGRect(headerFrame)); DDLogError(@"tableFrame: %@", NSStringFromCGRect(tableFrame)); DDLogError(@"contentFrame: %@", NSStringFromCGRect(contentFrame)); if(tableFrame.size.height > tableFrame.size.width) { DDLogError(@"height is bigger"); tableFrame.size.height *= 0.5; tableFrame.origin.y += headerFrame.size.height; } else { DDLogError(@"width is bigger"); tableFrame.size.height *= 2.0; } //tableFrame.size.height *= (tableFrame.size.width / tableFrame.size.height); emptyDataSetView.imageView.frame = tableFrame; emptyDataSetView.contentView.frame = tableFrame; [emptyDataSetView.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]-(48@750)-[titleLabel]-(16@750)-[detailLabel]|" options:0 metrics:nil views:@{ @"imageView": emptyDataSetView.imageView, @"titleLabel": emptyDataSetView.titleLabel, @"detailLabel": emptyDataSetView.detailLabel, }]]; emptyDataSetView.imageView.translatesAutoresizingMaskIntoConstraints = YES; return [UIImage imageNamed:@"chat"]; */ } -(CGFloat) spaceHeightForEmptyDataSet:(UIScrollView*) scrollView { return 480.0f; } -(NSAttributedString*) titleForEmptyDataSet:(UIScrollView*) scrollView { NSString* text = NSLocalizedString(@"No active conversations", @""); NSDictionary* attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0f], NSForegroundColorAttributeName: (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : [UIColor blackColor])}; return [[NSAttributedString alloc] initWithString:text attributes:attributes]; } - (NSAttributedString*)descriptionForEmptyDataSet:(UIScrollView*) scrollView { NSString* text = NSLocalizedString(@"When you start a conversation\nwith someone, they will\nshow up here.", @""); NSMutableParagraphStyle* paragraph = [NSMutableParagraphStyle new]; paragraph.lineBreakMode = NSLineBreakByWordWrapping; paragraph.alignment = NSTextAlignmentCenter; NSDictionary* attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f], NSForegroundColorAttributeName: (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? [UIColor whiteColor] : [UIColor blackColor]), NSParagraphStyleAttributeName: paragraph}; return [[NSAttributedString alloc] initWithString:text attributes:attributes]; } -(UIColor*) backgroundColorForEmptyDataSet:(UIScrollView*) scrollView { return [UIColor colorNamed:@"chats"]; } -(BOOL) emptyDataSetShouldDisplay:(UIScrollView*) scrollView { BOOL toreturn = (self.unpinnedContacts.count == 0 && self.pinnedContacts.count == 0) ? YES : NO; if(toreturn) { // A little trick for removing the cell separators self.tableView.tableFooterView = [UIView new]; } return toreturn; } #pragma mark - mac menu -(void) showContacts:(id) sender { // function definition for @selector [self showContacts]; } -(void) showContacts { if([self showAccountNumberWarningIfNeeded]) { return; } appendToViewQueue((^(PMKResolver resolve) { contactCompletion callback = ^(MLContact* selectedContact) { DDLogVerbose(@"Got selected contact from contactlist ui: %@", selectedContact); [self presentChatWithContact:selectedContact]; }; UIViewController* contactsView = [[SwiftuiInterface new] makeContactsViewWithDismisser: callback onButton: self.composeButton]; [self presentViewController:contactsView animated:YES completion:^{resolve(nil);}]; })); } //we can not call this var "completion" because then some dumb comiler check kicks in and tells us "completion handler is never called" //which is plainly wrong. "callback" on the other hand doesn't seem to be a word in the objc compiler's "bad words" dictionary, //so this makes it compile again -(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString*) token usingCompletion:(monal_id_block_t) callback { prependingReplaceOnViewQueue(MLViewIDWelcomeLoginView, MLViewIDRegisterView, (^(PMKResolver resolve) { UIViewController* registerViewController = [[SwiftuiInterface new] makeAccountRegistration:@{ @"host": nilWrapper(host), @"username": nilWrapper(username), @"token": nilWrapper(token), @"completion": nilDefault(callback, (^(id accountID) { DDLogWarn(@"Dummy reg completion called for accountID: %@", accountID); })), }]; registerViewController.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:registerViewController animated:YES completion:^{resolve(nil);}]; }]; })); } -(void) showDetails { appendToViewQueue((^(PMKResolver resolve) { if([MLNotificationManager sharedInstance].currentContact != nil) { UIViewController* detailsViewController = [[SwiftuiInterface new] makeContactDetails:[MLNotificationManager sharedInstance].currentContact]; detailsViewController.ml_disposeCallback = ^{ [self sheetDismissed]; }; [self dismissCompleteViewChainWithAnimation:NO andCompletion:^{ [self presentViewController:detailsViewController animated:YES completion:^{resolve(nil);}]; }]; } else resolve(nil); })); } -(void) deleteConversation { for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt; section++) { NSMutableArray* curContactArray = [self getChatArrayForSection:section]; // check if contact is already displayed -> get coresponding indexPath [curContactArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop __unused) { MLContact* rowContact = (MLContact*)obj; if([rowContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) { [self tableView:self.chatListTable commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:[NSIndexPath indexPathForRow:idx inSection:section]]; // remove contact from activechats table [self refreshDisplay]; // open placeholder [self presentChatWithContact:nil]; return; } }]; } } -(NSMutableArray*) getCurrentViewControllerHierarchy { MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; UIViewController* rootViewController = appDelegate.window.rootViewController; NSMutableArray* viewControllers = [NSMutableArray new]; while(rootViewController.presentedViewController) { [viewControllers addObject:rootViewController.presentedViewController]; rootViewController = rootViewController.presentedViewController; } return [[[viewControllers reverseObjectEnumerator] allObjects] mutableCopy]; } -(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion { NSMutableArray* viewControllers = [self getCurrentViewControllerHierarchy]; DDLogVerbose(@"Dismissing view controller hierarchy: %@", viewControllers); [self dismissRecursorWithViewControllers:viewControllers animation:animation andCompletion:completion]; } -(void) dismissRecursorWithViewControllers:(NSMutableArray*) viewControllers animation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion { if([viewControllers count] > 0) { UIViewController* viewController = viewControllers[0]; [viewControllers removeObjectAtIndex:0]; DDLogVerbose(@"Dismissing: %@", viewController); [viewController dismissViewControllerAnimated:animation completion:^{ [self dismissRecursorWithViewControllers:viewControllers animation:animation andCompletion:completion]; }]; } else { DDLogVerbose(@"View chain completely dismissed..."); completion(); } } -(chatViewController* _Nullable) currentChatView { NSArray* controllers = ((UINavigationController*)self.splitViewController.viewControllers[0]).viewControllers; chatViewController* chatView = nil; if(controllers.count > 1) chatView = [((UINavigationController*)controllers[1]).viewControllers firstObject]; if(![chatView isKindOfClass:NSClassFromString(@"chatViewController")]) chatView = nil; return chatView; } -(void) scrollToContact:(MLContact*) contact { __block NSIndexPath* indexPath = nil; for(size_t section = pinnedChats; section < activeChatsViewControllerSectionCnt && !indexPath; section++) { NSMutableArray* curContactArray = [self getChatArrayForSection:section]; // get indexPath [curContactArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { MLContact* rowContact = (MLContact*)obj; if([rowContact isEqualToContact:contact]) { indexPath = [NSIndexPath indexPathForRow:idx inSection:section]; *stop = YES; } }]; } [self.chatListTable selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; } @end