another.im-ios/Monal/Classes/ActiveChatsViewController.m
2024-11-18 15:53:52 +01:00

1559 lines
67 KiB
Objective-C
Executable file

//
// ActiveChatsViewController.m
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#include "metamacros.h"
#import <Contacts/Contacts.h>
#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 <Monal-Swift.h>
#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 () <UIGestureRecognizerDelegate>
@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<MLContact*>* newUnpinnedContacts = [[DataLayer sharedInstance] activeContactsWithPinned:NO];
NSMutableArray<MLContact*>* 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<CNPhoneNumber*>* 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<xmpp*>* 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<MLContact*>*) 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<UIViewControllerTransitionCoordinator>) 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