diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 0cab6291e..1f24caf55 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -24,8 +24,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton; @property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton; @property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton; -@property (nonatomic, strong) chatViewController* currentChatViewController; @property (nonatomic, strong) UIActivityIndicatorView* spinner; +@property (atomic, readonly) chatViewController* _Nullable currentChatView; -(void) showCallContactNotFoundAlert:(NSString*) jid; -(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index b9628b7f1..0eb0ada15 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -361,13 +361,13 @@ -(void) refreshDisplay dispatch_async(dispatch_get_main_queue(), ^{ //make sure we don't display a chat view for a disabled account - if(self.currentChatViewController != nil && self.currentChatViewController.contact != nil) + if([MLNotificationManager sharedInstance].currentContact != nil) { BOOL found = NO; for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList]) { - NSNumber* accountNo = accountDict[kAccountID]; - if(self.currentChatViewController.contact.accountId.intValue == accountNo.intValue) + NSNumber* accountId = accountDict[kAccountID]; + if([MLNotificationManager sharedInstance].currentContact.accountId.intValue == accountId.intValue) found = YES; } if(!found) @@ -879,6 +879,7 @@ -(void) presentSplitPlaceholder UIViewController* detailsViewController = [[SwiftuiInterface new] makeViewWithName:@"ChatPlaceholder"]; [self showDetailViewController:detailsViewController sender:self]; } + [MLNotificationManager sharedInstance].currentContact = nil; } -(void) showNotificationSettings @@ -1094,7 +1095,6 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender UIBarButtonItem* barButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; self.navigationItem.backBarButtonItem = barButtonItem; [chatVC setupWithContact:sender]; - self.currentChatViewController = chatVC; } else if([segue.identifier isEqualToString:@"showDetails"]) { @@ -1513,6 +1513,17 @@ -(void) dismissRecursorWithViewControllers:(NSMutableArray*) viewControllers ani } } +-(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; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 6c1fcb886..aac8558eb 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -2352,8 +2352,12 @@ +(NSString*) encodeRandomResource u_int32_t i=arc4random(); #if TARGET_OS_MACCATALYST NSString* resource = [NSString stringWithFormat:@"Monal-macOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; +#else +#if IS_QUICKSY + NSString* resource = [NSString stringWithFormat:@"Quicksy-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; #else NSString* resource = [NSString stringWithFormat:@"Monal-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]]; +#endif #endif return resource; } diff --git a/Monal/Classes/MLDelayableTimer.m b/Monal/Classes/MLDelayableTimer.m index bf3ed5e69..ad7dceefc 100644 --- a/Monal/Classes/MLDelayableTimer.m +++ b/Monal/Classes/MLDelayableTimer.m @@ -52,7 +52,10 @@ -(void) start return; } DDLogDebug(@"Starting timer: %@", self); - [[HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer] addTimer:_wrappedTimer forMode:NSRunLoopCommonModes]; + //scheduling and unscheduling of a timer must be done from the same thread --> use our runloop + [self scheduleBlockInRunLoop:^{ + [[HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer] addTimer:self->_wrappedTimer forMode:NSRunLoopCommonModes]; + }]; } } @@ -131,8 +134,28 @@ -(void) invalidate return; } //DDLogVerbose(@"Invalidating timer: %@", self); - [_wrappedTimer invalidate]; + //scheduling and unscheduling of a timer must be done from the same thread --> use our runloop + [self scheduleBlockInRunLoop:^{ + [self->_wrappedTimer invalidate]; + }]; } } +-(void) scheduleBlockInRunLoop:(monal_void_block_t) block +{ + NSRunLoop* runLoop = [HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer]; +// NSCondition* condition = [NSCondition new]; +// [condition lock]; + CFRunLoopPerformBlock([runLoop getCFRunLoop], (__bridge CFStringRef)NSDefaultRunLoopMode, ^{ + block(); +// [condition lock]; +// [condition signal]; +// [condition unlock]; + }); + CFRunLoopWakeUp([runLoop getCFRunLoop]); //trigger wakeup of runloop to execute the block as soon as possible +// //wait for our block to finish executing +// [condition wait]; +// [condition unlock]; +} + @end diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 1070be770..2112a087f 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -50,7 +50,7 @@ @interface MLInputStream() //(mutexes can not be unlocked in a thread different from the one it got locked in and NSLock internally uses mutext --> both can not be used) dispatch_semaphore_t _read_sem; } -@property (atomic, readonly) void (^incoming_data_handler)(NSData* _Nullable, BOOL, NSError* _Nullable, BOOL allow_next_read); +@property (atomic, readonly) void (^incoming_data_handler)(NSData* _Nullable, BOOL, NSError* _Nullable, BOOL); @end @interface MLOutputStream() @@ -89,7 +89,7 @@ -(instancetype) initWithSharedState:(MLSharedStreamState*) shared //this handler will be called by the schedule_read method //since the framer swallows all data, nw_connection_receive() and the framer cannot race against each other and deliver reordered data weakify(self); - _incoming_data_handler = ^(NSData* _Nullable content, BOOL is_complete, NSError* _Nullable st_error, BOOL allow_next_read) { + _incoming_data_handler = ^(NSData* _Nullable content, BOOL is_complete, NSError* _Nullable st_error, BOOL polling_active) { strongify(self); if(self == nil) return; @@ -142,7 +142,7 @@ -(instancetype) initWithSharedState:(MLSharedStreamState*) shared [self generateEvent:NSStreamEventEndEncountered]; //try to read again - if(!is_complete && !generate_bytes_available_event && allow_next_read) + if(!is_complete && !generate_error_event && !generate_bytes_available_event && polling_active) [self schedule_read]; }; return self; @@ -235,8 +235,8 @@ -(void) schedule_read DDLogDebug(@"now calling nw_framer_parse_input inside framer queue"); nw_framer_parse_input(self.shared_state.framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) { DDLogDebug(@"nw_framer_parse_input got callback with is_complete:%@, length=%zu", bool2str(is_complete), (unsigned long)buffer_length); - //we only want to allow new calls to schedule_read if we received some data --> set last arg accordingly - self.incoming_data_handler([NSData dataWithBytes:buffer length:buffer_length], is_complete, nil, buffer_length > 0); + //we don't want to do "polling" here, our next nw_framer_parse_input will be triggered by the nw_framer_set_input_handler block + self.incoming_data_handler([NSData dataWithBytes:buffer length:buffer_length], is_complete, nil, NO); return buffer_length; }); }); @@ -249,7 +249,7 @@ -(void) schedule_read NSError* st_error = nil; if(receive_error) st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(receive_error)); - //we always want to allow new calls to schedule_read --> set last arg to YES + //we want to do "polling" here (e.g. start our next blocking nw_connection_receive call if we did not receive new data nor any error) self.incoming_data_handler((NSData*)content, is_complete, st_error, YES); }); } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index a32ddbb05..7769e3ac8 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -1972,10 +1972,10 @@ -(void) sendAllOutboxes monal_id_block_t cleanup = ^(NSDictionary* payload) { [[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - if(self.activeChats.currentChatViewController != nil) + if(self.activeChats.currentChatView != nil) { - [self.activeChats.currentChatViewController scrollToBottomAnimated:NO]; - [self.activeChats.currentChatViewController hideUploadHUD]; + [self.activeChats.currentChatView scrollToBottomAnimated:NO]; + [self.activeChats.currentChatView hideUploadHUD]; } //send next item (if there is one left) [self sendAllOutboxes]; @@ -2007,7 +2007,7 @@ -(void) sendAllOutboxes else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"]) { DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]); - [self.activeChats.currentChatViewController showUploadHUD]; + [self.activeChats.currentChatView showUploadHUD]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ $call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) { dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 8a689dbe5..b9ce26893 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -3560,7 +3560,8 @@ -(NSInteger)collectionView:(nonnull UICollectionView*) collectionView numberOfIt -(void) notifyUploadQueueRemoval:(NSUInteger) index { - MLAssert(index < self.uploadQueue.count, @"index is only allowed to be smaller than uploadQueue.count"); + if(index >= self.uploadQueue.count) + return; [self.uploadMenuView performBatchUpdates:^{ [self deleteQueueItemAtIndex:index]; } completion:^(BOOL finished) { diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index fa6e985ce..c3a3863f2 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -829,19 +829,21 @@ -(void) unfreeze //this operation has highest priority to make sure it will be executed first once unfrozen NSBlockOperation* unfreezeOperation = [NSBlockOperation blockOperationWithBlock:^{ //this has to be the very first thing even before unfreezing the parse or send queues - if(self.accountState < kStateReconnecting) - { - DDLogInfo(@"Reloading UNfrozen account %@", self.accountNo); - //(re)read persisted state (could be changed by appex) - [self readState]; + @synchronized(self->_stateLockObject) { + if(self.accountState < kStateReconnecting) + { + DDLogInfo(@"Reloading UNfrozen account %@", self.accountNo); + //(re)read persisted state (could be changed by appex) + [self readState]; + } + else + DDLogInfo(@"Not reloading UNfrozen account %@, already connected", self.accountNo); + + //this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system + [self unfreezeParseQueue]; + + [self unfreezeSendQueue]; } - else - DDLogInfo(@"Not reloading UNfrozen account %@, already connected", self.accountNo); - - //this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system - [self unfreezeParseQueue]; - - [self unfreezeSendQueue]; }]; unfreezeOperation.queuePriority = NSOperationQueuePriorityVeryHigh; //make sure this will become the first operation executed once unfrozen [self->_receiveQueue addOperations: @[unfreezeOperation] waitUntilFinished:NO]; @@ -962,11 +964,67 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicitLogout:(BOOL) explicitLogout { DDLogInfo(@"disconnect called..."); + //commonly used by shortcut outside of receive queue and called from inside the receive queue, too + monal_void_block_t doExplicitLogout = ^{ + @synchronized(self->_stateLockObject) { + DDLogVerbose(@"explicitLogout == YES --> clearing state"); + + //preserve unAckedStanzas even on explicitLogout and resend them on next connect + //if we don't do this, messages could get lost when logging out directly after sending them + //and: sending messages twice is less intrusive than silently loosing them + NSMutableArray* stanzas = self.unAckedStanzas; + + //reset smacks state to sane values (this can be done even if smacks is not supported) + [self initSM3]; + self.unAckedStanzas = stanzas; + + //inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards + @synchronized(self->_iqHandlers) { + for(NSString* iqid in [self->_iqHandlers allKeys]) + { + DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); + if(self->_iqHandlers[iqid][@"handler"] != nil) + $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect")); + else if(self->_iqHandlers[iqid][@"errorHandler"]) + ((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil); + } + self->_iqHandlers = [NSMutableDictionary new]; + } + + //invalidate pubsub queue (*after* iq handlers that also might invalidate a result handler of the queued operation) + [self.pubsub invalidateQueue]; + + //clear pipeline cache + self->_pipeliningState = kPipelinedNothing; + self->_cachedStreamFeaturesBeforeAuth = nil; + self->_cachedStreamFeaturesAfterAuth = nil; + + //clear all reconnection handlers + @synchronized(self->_reconnectionHandlers) { + [self->_reconnectionHandlers removeAllObjects]; + } + + //persist these changes + [self persistState]; + } + + [[DataLayer sharedInstance] resetContactsForAccount:self.accountNo]; + + //trigger view updates to make sure enabled/disabled account state propagates to all ui elements + [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; + }; //short-circuit common case without dispatching to receive queue //this allows calling a noop disconnect while the receive queue is frozen - if(self->_accountState an unfreeze can not happen half way through this explicit logout and therefore can't corrupt any state + //--> an unfreeze is needed to dispatch to the receive queue which is used by our connect method + if(self->_accountState_stateLockObject) { - DDLogVerbose(@"explicitLogout == YES --> clearing state"); - - //preserve unAckedStanzas even on explicitLogout and resend them on next connect - //if we don't do this, messages could get lost when logging out directly after sending them - //and: sending messages twice is less intrusive than silently loosing them - NSMutableArray* stanzas = self.unAckedStanzas; - - //reset smacks state to sane values (this can be done even if smacks is not supported) - [self initSM3]; - self.unAckedStanzas = stanzas; - - //inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards - @synchronized(self->_iqHandlers) { - for(NSString* iqid in [self->_iqHandlers allKeys]) - { - DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid); - if(self->_iqHandlers[iqid][@"handler"] != nil) - $invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect")); - else if(self->_iqHandlers[iqid][@"errorHandler"]) - ((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil); - } - self->_iqHandlers = [NSMutableDictionary new]; - } - - //invalidate pubsub queue (*after* iq handlers that also might invalidate a result handler of the queued operation) - [self.pubsub invalidateQueue]; - - //clear pipeline cache - self->_pipeliningState = kPipelinedNothing; - self->_cachedStreamFeaturesBeforeAuth = nil; - self->_cachedStreamFeaturesAfterAuth = nil; - - //clear all reconnection handlers - @synchronized(self->_reconnectionHandlers) { - [self->_reconnectionHandlers removeAllObjects]; - } - - //persist these changes - [self persistState]; - } - - [[DataLayer sharedInstance] resetContactsForAccount:self.accountNo]; - - //trigger view updates to make sure enabled/disabled account state propagates to all ui elements - [[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil]; - } + doExplicitLogout(); return; } DDLogInfo(@"disconnecting");