From b70a8d6d3ce88e0fe2d81f24ec961b81b91d8717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 17 Jun 2026 18:53:46 +0200 Subject: [PATCH 01/11] chore: Migrate SettingsController and NotificationsController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Marcel Müller --- NextcloudTalk.xcodeproj/project.pbxproj | 8 +- NextcloudTalk/AppDelegate.m | 12 +- NextcloudTalk/Calls/NCCameraController.swift | 4 +- NextcloudTalk/Contacts/NCContactsManager.m | 1 - .../DirectoryTableViewController.m | 1 - .../Login/AuthenticationViewController.m | 1 - NextcloudTalk/Network/OcsError.swift | 5 + ...NextcloudTalk-Bridging-Header-Extensions.h | 1 - NextcloudTalk/NextcloudTalk-Bridging-Header.h | 1 - .../Notifications/NCNotificationController.h | 35 - .../Notifications/NCNotificationController.m | 728 --------------- .../NCNotificationController.swift | 635 +++++++++++++ NextcloudTalk/Settings/NCSettingsController.h | 82 -- NextcloudTalk/Settings/NCSettingsController.m | 855 ------------------ .../Settings/NCSettingsController.swift | 705 ++++++++++++++- .../NCUserInterfaceController.h | 1 - .../NCUserInterfaceController.m | 3 +- 17 files changed, 1356 insertions(+), 1722 deletions(-) delete mode 100644 NextcloudTalk/Notifications/NCNotificationController.h delete mode 100644 NextcloudTalk/Notifications/NCNotificationController.m create mode 100644 NextcloudTalk/Notifications/NCNotificationController.swift delete mode 100644 NextcloudTalk/Settings/NCSettingsController.h delete mode 100644 NextcloudTalk/Settings/NCSettingsController.m diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 2dd8534a8..f7189a117 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -939,28 +939,28 @@ 1F9629C42E8E709D00EC9BEE /* Exceptions for "Notifications" folder in "NotificationServiceExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - NCNotificationController.m, + NCNotificationController.swift, ); target = 2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */; }; 1F9629C52E8E709D00EC9BEE /* Exceptions for "Notifications" folder in "ShareExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - NCNotificationController.m, + NCNotificationController.swift, ); target = 2C62AFA224C08845007E460A /* ShareExtension */; }; 1F9629C62E8E709D00EC9BEE /* Exceptions for "Notifications" folder in "BroadcastUploadExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - NCNotificationController.m, + NCNotificationController.swift, ); target = 1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */; }; 1F9629C72E8E709D00EC9BEE /* Exceptions for "Notifications" folder in "TalkIntents" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - NCNotificationController.m, + NCNotificationController.swift, ); target = 1FA93D9B2D70FCC200DF6CDF /* TalkIntents */; }; diff --git a/NextcloudTalk/AppDelegate.m b/NextcloudTalk/AppDelegate.m index 6e190e387..974f4179a 100644 --- a/NextcloudTalk/AppDelegate.m +++ b/NextcloudTalk/AppDelegate.m @@ -20,9 +20,7 @@ #import "NCAppBranding.h" #import "NCDatabaseManager.h" #import "NCKeyChainController.h" -#import "NCNotificationController.h" #import "NCPushNotification.h" -#import "NCSettingsController.h" #import "NCUserInterfaceController.h" #import "NextcloudTalk-Swift.h" @@ -295,7 +293,7 @@ - (void)application:(UIApplication *)application didRegisterForRemoteNotificatio - (void)registerInteractivePushNotification { // Reply directly to a chat notification action/category - UNTextInputNotificationAction *replyAction = [UNTextInputNotificationAction actionWithIdentifier:NCNotificationActionReplyToChat + UNTextInputNotificationAction *replyAction = [UNTextInputNotificationAction actionWithIdentifier:NCNotificationController.actionReplyToChat title:NSLocalizedString(@"Reply", nil) options:UNNotificationActionOptionAuthenticationRequired]; @@ -305,11 +303,11 @@ - (void)registerInteractivePushNotification options:UNNotificationCategoryOptionNone]; // Recording actions/category - UNNotificationAction *recordingShareAction = [UNNotificationAction actionWithIdentifier:NCNotificationActionShareRecording + UNNotificationAction *recordingShareAction = [UNNotificationAction actionWithIdentifier:NCNotificationController.actionShareRecording title:NSLocalizedString(@"Share to chat", nil) options:UNNotificationActionOptionAuthenticationRequired]; - UNNotificationAction *recordingDismissAction = [UNNotificationAction actionWithIdentifier:NCNotificationActionDismissRecordingNotification + UNNotificationAction *recordingDismissAction = [UNNotificationAction actionWithIdentifier:NCNotificationController.actionDismissRecordingNotification title:NSLocalizedString(@"Dismiss notification", nil) options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive]; @@ -319,11 +317,11 @@ - (void)registerInteractivePushNotification options:UNNotificationCategoryOptionNone]; // Federation invitation - UNNotificationAction *federationAccept = [UNNotificationAction actionWithIdentifier:NCNotificationActionFederationInvitationAccept + UNNotificationAction *federationAccept = [UNNotificationAction actionWithIdentifier:NCNotificationController.actionFederationInvitationAccept title:NSLocalizedString(@"Accept", nil) options:UNNotificationActionOptionAuthenticationRequired]; - UNNotificationAction *federationReject = [UNNotificationAction actionWithIdentifier:NCNotificationActionFederationInvitationReject + UNNotificationAction *federationReject = [UNNotificationAction actionWithIdentifier:NCNotificationController.actionFederationInvitationReject title:NSLocalizedString(@"Reject", nil) options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive]; diff --git a/NextcloudTalk/Calls/NCCameraController.swift b/NextcloudTalk/Calls/NCCameraController.swift index 1e205adfc..070e25947 100644 --- a/NextcloudTalk/Calls/NCCameraController.swift +++ b/NextcloudTalk/Calls/NCCameraController.swift @@ -122,8 +122,8 @@ class NCCameraController: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate let settings = NCSettingsController.sharedInstance().videoSettingsModel let formats = RTCCameraVideoCapturer.supportedFormats(for: device) - let targetWidth = settings?.currentVideoResolutionWidthFromStore() ?? 0 - let targetHeight = settings?.currentVideoResolutionHeightFromStore() ?? 0 + let targetWidth = settings.currentVideoResolutionWidthFromStore() + let targetHeight = settings.currentVideoResolutionHeightFromStore() var selectedFormat: AVCaptureDevice.Format? var currentDiff = INT_MAX diff --git a/NextcloudTalk/Contacts/NCContactsManager.m b/NextcloudTalk/Contacts/NCContactsManager.m index 087118c0a..6cd8adca6 100644 --- a/NextcloudTalk/Contacts/NCContactsManager.m +++ b/NextcloudTalk/Contacts/NCContactsManager.m @@ -8,7 +8,6 @@ #import #import "NCDatabaseManager.h" -#import "NCSettingsController.h" #import "ABContact.h" #import "NCContact.h" diff --git a/NextcloudTalk/File sharing/DirectoryTableViewController.m b/NextcloudTalk/File sharing/DirectoryTableViewController.m index e915d159c..c1f7b6768 100644 --- a/NextcloudTalk/File sharing/DirectoryTableViewController.m +++ b/NextcloudTalk/File sharing/DirectoryTableViewController.m @@ -11,7 +11,6 @@ #import "NCDatabaseManager.h" #import "NCAppBranding.h" -#import "NCSettingsController.h" #import "PlaceholderView.h" #import "NextcloudTalk-Swift.h" diff --git a/NextcloudTalk/Login/AuthenticationViewController.m b/NextcloudTalk/Login/AuthenticationViewController.m index cc60b9932..88437ff9e 100644 --- a/NextcloudTalk/Login/AuthenticationViewController.m +++ b/NextcloudTalk/Login/AuthenticationViewController.m @@ -11,7 +11,6 @@ #import "CCCertificate.h" #import "NCAppBranding.h" #import "NCDatabaseManager.h" -#import "NCSettingsController.h" NSString * const kNCAuthTokenFlowEndpoint = @"/index.php/login/flow"; diff --git a/NextcloudTalk/Network/OcsError.swift b/NextcloudTalk/Network/OcsError.swift index 1e131bbbb..b04f511bd 100644 --- a/NextcloudTalk/Network/OcsError.swift +++ b/NextcloudTalk/Network/OcsError.swift @@ -55,4 +55,9 @@ import Foundation self.underlyingError = error self.task = task } + + // Convenience factory for a generic error without an underlying network request + public static func genericError() -> OcsError { + return OcsError(withError: NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil), withTask: nil) + } } diff --git a/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h b/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h index b78cfb36c..a4fef9ab6 100644 --- a/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h +++ b/NextcloudTalk/NextcloudTalk-Bridging-Header-Extensions.h @@ -18,7 +18,6 @@ #import "SLKTextViewController.h" #import "NCMessageTextView.h" #import "ReplyMessageView.h" -#import "NCSettingsController.h" #import "NCKeyChainController.h" #import "CCCertificate.h" diff --git a/NextcloudTalk/NextcloudTalk-Bridging-Header.h b/NextcloudTalk/NextcloudTalk-Bridging-Header.h index 133519204..6dfe8e7c9 100644 --- a/NextcloudTalk/NextcloudTalk-Bridging-Header.h +++ b/NextcloudTalk/NextcloudTalk-Bridging-Header.h @@ -18,7 +18,6 @@ #import "NCDatabaseManager.h" #import "NCChatFileController.h" #import "NCSignalingController.h" -#import "NCSettingsController.h" #import "NCUserDefaults.h" #import "NCUserInterfaceController.h" #import "NCUserStatus.h" diff --git a/NextcloudTalk/Notifications/NCNotificationController.h b/NextcloudTalk/Notifications/NCNotificationController.h deleted file mode 100644 index 05c15211c..000000000 --- a/NextcloudTalk/Notifications/NCNotificationController.h +++ /dev/null @@ -1,35 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import - -#import "NCPushNotification.h" -#import "NCTypes.h" - -extern NSString * const NCNotificationControllerWillPresentNotification; -extern NSString * const NCLocalNotificationJoinChatNotification; - -extern NSString * const NCNotificationActionShareRecording; -extern NSString * const NCNotificationActionDismissRecordingNotification; -extern NSString * const NCNotificationActionReplyToChat; -extern NSString * const NCNotificationActionFederationInvitationAccept; -extern NSString * const NCNotificationActionFederationInvitationReject; - -typedef void (^CheckForNewNotificationsCompletionBlock)(NSError *error); -typedef void (^CheckNotificationExistanceCompletionBlock)(NSError *error); - -@interface NCNotificationController : NSObject - -+ (instancetype)sharedInstance; -- (void)requestAuthorization; -- (void)processBackgroundPushNotification:(NCPushNotification *)pushNotification; -- (void)showLocalNotification:(NCLocalNotificationType)type withUserInfo:(NSDictionary *)userInfo; -- (void)showLocalNotificationForIncomingCallWithPushNotificaion:(NCPushNotification *)pushNotification; -- (void)showIncomingCallForPushNotification:(NCPushNotification *)pushNotification; -- (void)showIncomingCallForOldAccount; -- (void)removeAllNotificationsForAccountId:(NSString *)accountId; -- (void)checkNotificationExistanceWithCompletionBlock:(CheckNotificationExistanceCompletionBlock)block; - -@end diff --git a/NextcloudTalk/Notifications/NCNotificationController.m b/NextcloudTalk/Notifications/NCNotificationController.m deleted file mode 100644 index addd2c3ae..000000000 --- a/NextcloudTalk/Notifications/NCNotificationController.m +++ /dev/null @@ -1,728 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import "NCNotificationController.h" - -#import - -#import "NCDatabaseManager.h" -#import "NCIntentController.h" -#import "NCSettingsController.h" -#import "NCUserInterfaceController.h" -#import "NCUserStatus.h" - -#import "NextcloudTalk-Swift.h" - -NSString * const NCNotificationControllerWillPresentNotification = @"NCNotificationControllerWillPresentNotification"; -NSString * const NCLocalNotificationJoinChatNotification = @"NCLocalNotificationJoinChatNotification"; - -NSString * const NCNotificationActionShareRecording = @"SHARE_RECORDING"; -NSString * const NCNotificationActionDismissRecordingNotification = @"DISMISS_RECORDING_NOTIFICATION"; -NSString * const NCNotificationActionReplyToChat = @"REPLY_CHAT"; -NSString * const NCNotificationActionFederationInvitationAccept = @"ACCEPT_FEDERATION_INVITATION"; -NSString * const NCNotificationActionFederationInvitationReject = @"REJECT_FEDERATION_INVITATION"; - -@interface NCNotificationController () - -@property (nonatomic, strong) UNUserNotificationCenter *notificationCenter; -@property (nonatomic, strong) NSMutableDictionary *serverNotificationsAttempts; // notificationId -> get attempts - -@end - -@implementation NCNotificationController - -+ (NCNotificationController *)sharedInstance -{ - static dispatch_once_t once; - static NCNotificationController *sharedInstance; - dispatch_once(&once, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (id)init -{ - self = [super init]; - if (self) { - _notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; - _notificationCenter.delegate = self; - _serverNotificationsAttempts = [[NSMutableDictionary alloc] init]; - } - - return self; -} - -- (void)requestAuthorization -{ - UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; - [_notificationCenter requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (granted) { - NSLog(@"User notifications permission granted."); - } else { - NSLog(@"User notifications permission denied."); - } - }]; -} - -- (void)processBackgroundPushNotification:(NCPushNotification *)pushNotification -{ - if (!pushNotification) { - return; - } - - if (pushNotification.type == NCPushNotificationTypeDelete) { - NSNumber *notificationId = @(pushNotification.notificationId); - [self removeNotificationWithNotificationIds:@[notificationId] forAccountId:pushNotification.accountId withCompletionBlock:nil]; - } else if (pushNotification.type == NCPushNotificationTypeDeleteAll) { - [self removeAllNotificationsForAccountId:pushNotification.accountId]; - } else if (pushNotification.type == NCPushNotificationTypeDeleteMultiple) { - [self removeNotificationWithNotificationIds:pushNotification.notificationIds forAccountId:pushNotification.accountId withCompletionBlock:nil]; - } else { - NSLog(@"Push Notification of an unknown type received"); - } -} - -- (void)showLocalNotification:(NCLocalNotificationType)type withUserInfo:(NSDictionary *)userInfo -{ - UNMutableNotificationContent *content = [UNMutableNotificationContent new]; - - switch (type) { - case kNCLocalNotificationTypeMissedCall: - { - NSString *missedCallString = NSLocalizedString(@"Missed call from", nil); - content.body = [NSString stringWithFormat:@"☎️ %@ %@", missedCallString, [userInfo objectForKey:@"displayName"]]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeCancelledCall: - { - NSString *cancelledCallString = NSLocalizedString(@"Cancelled call from another account", nil); - content.body = [NSString stringWithFormat:@"☎️ %@", cancelledCallString]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeFailedSendChat: - { - NSString *failedSendChatString = NSLocalizedString(@"Failed to send message", nil); - content.body = [NSString stringWithFormat:@"%@", failedSendChatString]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeCallFromOldAccount: - { - NSString *receivedCallFromOldAccountString = NSLocalizedString(@"Received call from an old account", nil); - content.body = [NSString stringWithFormat:@"%@", receivedCallFromOldAccountString]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeFailedToShareRecording: - { - NSString *failedToShareRecordingString = NSLocalizedString(@"Failed to share recording", nil); - content.body = [NSString stringWithFormat:@"%@", failedToShareRecordingString]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeFailedToAcceptInvitation: - { - NSString *failedToAcceptInvitationString = NSLocalizedString(@"Failed to accept invitation", nil); - content.body = [NSString stringWithFormat:@"%@", failedToAcceptInvitationString]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeRecordingConsentRequired: - { - NSString *consentRequiredString = NSLocalizedString(@"Recording consent required for joining the call", nil); - content.body = [NSString stringWithFormat:@"⚠️ %@ %@", consentRequiredString, [userInfo objectForKey:@"displayName"]]; - content.userInfo = userInfo; - } - break; - - case kNCLocalNotificationTypeEndToEndEncryptionUnsupported: - { - NSString *endToEndEncryptionUnsupported = NSLocalizedString(@"Calling is currently not supported because end-to-end-encryption is enabled on the server", nil); - content.body = [NSString stringWithFormat:@"⚠️ %@", endToEndEncryptionUnsupported]; - content.userInfo = userInfo; - } - break; - - default: - break; - } - - NSString *identifier = [NSString stringWithFormat:@"Notification-%f", [[NSDate date] timeIntervalSince1970]]; - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO]; - UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger]; - [_notificationCenter addNotificationRequest:request withCompletionHandler:nil]; - - NSString *accountId = [userInfo objectForKey:@"accountId"]; - [[NCDatabaseManager sharedInstance] increaseUnreadBadgeNumberForAccountId:accountId]; - [self updateAppIconBadgeNumber]; -} - -- (void)showLocalNotificationForIncomingCallWithPushNotificaion:(NCPushNotification *)pushNotification -{ - UNMutableNotificationContent *content = [UNMutableNotificationContent new]; - content.body = pushNotification.bodyForRemoteAlerts; - content.threadIdentifier = pushNotification.roomToken; - content.sound = [UNNotificationSound defaultSound]; - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; - [userInfo setObject:pushNotification.jsonString forKey:@"pushNotification"]; - [userInfo setObject:pushNotification.accountId forKey:@"accountId"]; - [userInfo setObject:@(pushNotification.notificationId) forKey:@"notificationId"]; - content.userInfo = userInfo; - - NSString *identifier = [NSString stringWithFormat:@"Notification-%f", [[NSDate date] timeIntervalSince1970]]; - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO]; - UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger]; - [_notificationCenter addNotificationRequest:request withCompletionHandler:nil]; - - [[NCDatabaseManager sharedInstance] increaseUnreadBadgeNumberForAccountId:pushNotification.accountId]; - [self updateAppIconBadgeNumber]; -} - -- (void)showIncomingCallForPushNotification:(NCPushNotification *)pushNotification -{ - if ([CallKitManager isCallKitAvailable]) { - [[CallKitManager sharedInstance] reportIncomingCall:pushNotification.roomToken withDisplayName:@"Incoming call" forAccountId:pushNotification.accountId]; - } else { - [[CallKitManager sharedInstance] reportIncomingCallForNonCallKitDevicesWithPushNotification:pushNotification]; - } -} - -- (void)showIncomingCallForOldAccount -{ - [[CallKitManager sharedInstance] reportIncomingCallForOldAccount]; -} - -- (void)showLocalNotificationForChatNotification:(NCNotification *)notification forAccountId:(NSString *)accountId -{ - UNMutableNotificationContent *content = [UNMutableNotificationContent new]; - content.title = notification.chatMessageTitle; - content.body = notification.message; - content.summaryArgument = notification.chatMessageAuthor; - content.threadIdentifier = notification.roomToken; - content.sound = [UNNotificationSound defaultSound]; - - // Currently not supported for local notifications - //content.categoryIdentifier = @"CATEGORY_CHAT"; - - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:notification.roomToken forKey:@"roomToken"]; - [userInfo setObject:accountId forKey:@"accountId"]; - [userInfo setObject:@(notification.notificationId) forKey:@"notificationId"]; - [userInfo setValue:@(kNCLocalNotificationTypeChatNotification) forKey:@"localNotificationType"]; - content.userInfo = userInfo; - - NSString *identifier = [NSString stringWithFormat:@"ChatNotification-%ld", notification.notificationId]; - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO]; - UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger]; - [_notificationCenter addNotificationRequest:request withCompletionHandler:nil]; - - [[NCDatabaseManager sharedInstance] increaseUnreadBadgeNumberForAccountId:accountId]; - [self updateAppIconBadgeNumber]; -} - -- (void)updateAppIconBadgeNumber -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [UIApplication sharedApplication].applicationIconBadgeNumber = [[NCDatabaseManager sharedInstance] numberOfUnreadNotifications]; - }); -} - -- (void)removeAllNotificationsForAccountId:(NSString *)accountId -{ - // Check in pending notifications - [_notificationCenter getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { - for (UNNotificationRequest *notificationRequest in requests) { - NSString *notificationAccountId = [notificationRequest.content.userInfo objectForKey:@"accountId"]; - if (notificationAccountId && [notificationAccountId isEqualToString:accountId]) { - [self->_notificationCenter removePendingNotificationRequestsWithIdentifiers:@[notificationRequest.identifier]]; - } - } - }]; - // Check in delivered notifications - [_notificationCenter getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { - for (UNNotification *notification in notifications) { - NSString *notificationAccountId = [notification.request.content.userInfo objectForKey:@"accountId"]; - if (notificationAccountId && [notificationAccountId isEqualToString:accountId]) { - [self->_notificationCenter removeDeliveredNotificationsWithIdentifiers:@[notification.request.identifier]]; - } - } - }]; - - [[NCDatabaseManager sharedInstance] resetUnreadBadgeNumberForAccountId:accountId]; - [self updateAppIconBadgeNumber]; -} - -- (void)removeNotificationWithNotificationIds:(NSArray *)notificationIds forAccountId:(NSString *)accountId withCompletionBlock:(void (^)(void))completion -{ - if (!notificationIds) { - if (completion) { - completion(); - } - return; - } - - void(^removeNotification)(UNNotificationRequest *, BOOL) = ^(UNNotificationRequest *notificationRequest, BOOL isPending) { - NSString *notificationAccountId = [notificationRequest.content.userInfo objectForKey:@"accountId"]; - NSInteger notificationId = [[notificationRequest.content.userInfo objectForKey:@"notificationId"] integerValue]; - - if (![notificationAccountId isEqualToString:accountId]) { - return; - } - - if ([notificationIds containsObject:@(notificationId)]) { - if (isPending) { - [self->_notificationCenter removePendingNotificationRequestsWithIdentifiers:@[notificationRequest.identifier]]; - } else { - [self->_notificationCenter removeDeliveredNotificationsWithIdentifiers:@[notificationRequest.identifier]]; - } - - [[NCDatabaseManager sharedInstance] decreaseUnreadBadgeNumberForAccountId:accountId]; - } - }; - - __block BOOL expired = NO; - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"decreaseUnreadBadgeNumberForAccountId" expirationHandler:^(BGTaskHelper *task) { - expired = YES; - }]; - - dispatch_group_t notificationsGroup = dispatch_group_create(); - - dispatch_group_enter(notificationsGroup); - // Check in pending notifications - [_notificationCenter getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { - for (UNNotificationRequest *notificationRequest in requests) { - if (expired) { - dispatch_group_leave(notificationsGroup); - return; - } - removeNotification(notificationRequest, YES); - } - - [self updateAppIconBadgeNumber]; - dispatch_group_leave(notificationsGroup); - }]; - - dispatch_group_enter(notificationsGroup); - // Check in delivered notifications - [_notificationCenter getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { - for (UNNotification *notification in notifications) { - if (expired) { - dispatch_group_leave(notificationsGroup); - return; - } - removeNotification(notification.request, NO); - } - - [self updateAppIconBadgeNumber]; - dispatch_group_leave(notificationsGroup); - }]; - - dispatch_group_notify(notificationsGroup, dispatch_get_main_queue(), ^{ - if (completion) { - completion(); - } - [bgTask stopBackgroundTask]; - }); -} - -- (void)checkNotificationExistanceWithCompletionBlock:(CheckNotificationExistanceCompletionBlock)block -{ - dispatch_group_t notificationsGroup = dispatch_group_create(); - - for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) { - if (![[NCDatabaseManager sharedInstance] serverHasNotificationsCapability:kNotificationsCapabilityExists forAccountId:account.accountId]) { - continue; - } - - dispatch_group_enter(notificationsGroup); - - [_notificationCenter getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { - NSMutableArray *notificationIdsOnDevice = [[NSMutableArray alloc] init]; - - // TODO: Instead of storing just the IDs, we can also store the identifier and directly - // remove the notification instead of iterating again removeNotificationWithNotificationIds - for (UNNotification *notification in notifications) { - UNNotificationRequest *notificationRequest = notification.request; - NSString *notificationAccountId = [notificationRequest.content.userInfo objectForKey:@"accountId"]; - NSInteger notificationId = [[notificationRequest.content.userInfo objectForKey:@"notificationId"] integerValue]; - - if (![notificationAccountId isEqualToString:account.accountId]) { - continue; - } - - [notificationIdsOnDevice addObject:@(notificationId)]; - } - - if ([notificationIdsOnDevice count] == 0) { - // No notifications for this account are currently shown on the system -> no need to check anything - - dispatch_group_leave(notificationsGroup); - return; - } - - [[NCAPIController sharedInstance] checkNotificationExistanceWithIds:notificationIdsOnDevice forAccount:account completionBlock:^(NSArray * _Nullable notificationIds, OcsError * _Nullable error) { - if (error) { - dispatch_group_leave(notificationsGroup); - return; - } - - // Remove all notificationIds which are still on the server - for (id notificationId in notificationIds) { - [notificationIdsOnDevice removeObject:notificationId]; - } - - // In case there are still notifications on the device (that are not on the server anymore) remove them - if ([notificationIdsOnDevice count] == 0) { - dispatch_group_leave(notificationsGroup); - return; - } - - [self removeNotificationWithNotificationIds:notificationIdsOnDevice forAccountId:account.accountId withCompletionBlock:^{ - dispatch_group_leave(notificationsGroup); - }]; - }]; - }]; - } - - dispatch_group_notify(notificationsGroup, dispatch_get_main_queue(), ^{ - // Notify backgroundFetch that we're finished - if (block) { - block(nil); - } - }); -} - -#pragma mark - UNUserNotificationCenter delegate - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler -{ - // Called when a notification is delivered to a foreground app. - [[NSNotificationCenter defaultCenter] postNotificationName:NCNotificationControllerWillPresentNotification object:self userInfo:nil]; - completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner); - - // Remove the notification from Notification Center if it is from the active account - NSString *notificationAccountId = [notification.request.content.userInfo objectForKey:@"accountId"]; - if (notificationAccountId && [[[NCDatabaseManager sharedInstance] activeAccount].accountId isEqualToString:notificationAccountId]) { - [self removeAllNotificationsForAccountId:notificationAccountId]; - } -} - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler -{ - UNNotificationRequest *notificationRequest = response.notification.request; - NSDictionary *userInfo = notificationRequest.content.userInfo; - - // Local notification - NCLocalNotificationType localNotificationType = (NCLocalNotificationType)[[userInfo objectForKey:@"localNotificationType"] integerValue]; - - // Push notification - NSString *notificationString = [userInfo objectForKey:@"pushNotification"]; - NSString *notificationAccountId = [userInfo objectForKey:@"accountId"]; - NCPushNotification *pushNotification = [NCPushNotification pushNotificationFromDecryptedString:notificationString withAccountId:notificationAccountId]; - - // Server notification (only available if the Notification Service Extension was able to fetch it) - NSDictionary *serverNotificationDict = [userInfo objectForKey:@"serverNotification"]; - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:notificationAccountId]; - NCNotification *serverNotification = [[NCNotification alloc] initWithDictionary:serverNotificationDict]; - - // Update push notification with server notification - pushNotification.threadId = serverNotification.threadId; - - // Handle notification response - if (pushNotification) { - if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { - UNTextInputNotificationResponse *textInputResponse = (UNTextInputNotificationResponse *)response; - pushNotification.responseUserText = textInputResponse.userText; - - [self handlePushNotificationResponseWithUserText:pushNotification]; - } else if (pushNotification.type == NCPushNotificationTypeRecording) { - [self handlePushNotificationResponseForRecording:serverNotification withActionIdentifier:response.actionIdentifier forAccount:account]; - } else if (pushNotification.type == NCPUshNotificationTypeFederation) { - [self handlePushNotificationResponseForFederation:serverNotification withActionIdentifier:response.actionIdentifier forAccount:account]; - } else if (pushNotification.type == NCPushNotificationTypeReminder) { - [self handlePushNotificationResponseForReminder:serverNotification withActionIdentifier:response.actionIdentifier forAccount:account]; - } else { - [self handlePushNotificationResponse:pushNotification]; - } - } else if (localNotificationType > 0) { - [self handleLocalNotificationResponse:notificationRequest.content.userInfo]; - } - - completionHandler(); -} - -- (void)handlePushNotificationResponseWithUserText:(NCPushNotification *)pushNotification -{ - NSLog(@"Recevied push-notification with user input -> sending chat message"); - - TalkAccount *pushAccount = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:pushNotification.accountId]; - - UIApplication *application = [UIApplication sharedApplication]; - __block UIBackgroundTaskIdentifier sendTask = [application beginBackgroundTaskWithExpirationHandler:^{ - [application endBackgroundTask:sendTask]; - sendTask = UIBackgroundTaskInvalid; - }]; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [[NCAPIController sharedInstance] sendChatMessage:pushNotification.responseUserText toRoom:pushNotification.roomToken threadTitle:nil replyTo:-1 referenceId:nil silently:NO forAccount:pushAccount completionBlock:^(OcsError *error) { - - if (error) { - NSLog(@"Could not send chat message. Error: %@", error.description); - - // Display local push-notification to inform user - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:pushNotification.roomToken forKey:@"roomToken"]; - [userInfo setValue:@(kNCLocalNotificationTypeFailedSendChat) forKey:@"localNotificationType"]; - [userInfo setObject:pushNotification.accountId forKey:@"accountId"]; - [userInfo setObject:pushNotification.responseUserText forKey:@"responseUserText"]; - - [[NCNotificationController sharedInstance] showLocalNotification:kNCLocalNotificationTypeFailedSendChat withUserInfo:userInfo]; - } else { - // We replied to the message, so we can assume, we read it as well - [[NCDatabaseManager sharedInstance] decreaseUnreadBadgeNumberForAccountId:pushNotification.accountId]; - [self updateAppIconBadgeNumber]; - NCRoom *room = [[NCDatabaseManager sharedInstance] roomWithToken:pushNotification.roomToken forAccountId:pushNotification.accountId]; - if (room) { - [[NCIntentController sharedInstance] donateSendMessageIntentForRoom:room]; - } - } - - [application endBackgroundTask:sendTask]; - sendTask = UIBackgroundTaskInvalid; - }]; - }); -} - -- (void)handlePushNotificationResponseForFederation:(NCNotification *)serverNotification withActionIdentifier:(NSString *)actionIdentifier forAccount:(TalkAccount *)account -{ - if (!account || !serverNotification) { - return; - } - - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"handlePushNotificationResponseForFederation" expirationHandler:^(BGTaskHelper *task) { - [NCLog log:@"ExpirationHandler called - handlePushNotificationResponseForFederation"]; - }]; - - if ([actionIdentifier isEqualToString:NCNotificationActionFederationInvitationAccept]) { - FederationInvitation *invitation = [[FederationInvitation alloc] initWithNotification:serverNotification for:account.accountId]; - - [[NCAPIController sharedInstance] acceptFederationInvitationFor:account.accountId with:invitation.invitationId completionBlock:^(BOOL success) { - if (!success) { - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:serverNotification.roomToken forKey:@"roomToken"]; - [userInfo setValue:@(kNCLocalNotificationTypeFailedToAcceptInvitation) forKey:@"localNotificationType"]; - [userInfo setObject:account.accountId forKey:@"accountId"]; - - [self showLocalNotification:kNCLocalNotificationTypeFailedToAcceptInvitation withUserInfo:userInfo]; - } - - [[NCDatabaseManager sharedInstance] decreasePendingFederationInvitationForAccountId:account.accountId]; - - [bgTask stopBackgroundTask]; - }]; - - } else if ([actionIdentifier isEqualToString:NCNotificationActionFederationInvitationReject]) { - FederationInvitation *invitation = [[FederationInvitation alloc] initWithNotification:serverNotification for:account.accountId]; - - [[NCAPIController sharedInstance] rejectFederationInvitationFor:account.accountId with:invitation.invitationId completionBlock:^(BOOL success) { - [[NCDatabaseManager sharedInstance] decreasePendingFederationInvitationForAccountId:account.accountId]; - [bgTask stopBackgroundTask]; - }]; - } else { - [bgTask stopBackgroundTask]; - - UIAlertController *alert = [UIAlertController alertControllerWithTitle:serverNotification.subject - message:serverNotification.message - preferredStyle:UIAlertControllerStyleAlert]; - - for (NCNotificationAction *notificationAction in [serverNotification notificationActions]) { - UIAlertAction* tempButton = [UIAlertAction actionWithTitle:notificationAction.actionLabel - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - [[NCDatabaseManager sharedInstance] decreasePendingFederationInvitationForAccountId:account.accountId]; - [[NCAPIController sharedInstance] executeNotificationAction:notificationAction forAccount:account completionBlock:nil]; - }]; - - [alert addAction:tempButton]; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - [[NCUserInterfaceController sharedInstance] presentAlertViewController:alert]; - }); - } -} - -- (void)handlePushNotificationResponseForRecording:(NCNotification *)serverNotification withActionIdentifier:(NSString *)actionIdentifier forAccount:(TalkAccount *)account -{ - if (!account || !serverNotification) { - return; - } - - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"handlePushNotificationResponseForRecording" expirationHandler:^(BGTaskHelper *task) { - [NCLog log:@"ExpirationHandler called - handlePushNotificationResponseForRecording"]; - }]; - - NSTimeInterval notificationTimeInterval = [serverNotification.datetime timeIntervalSince1970]; - NSString *notificationTimestamp = [NSString stringWithFormat:@"%.0f", notificationTimeInterval]; - - if ([actionIdentifier isEqualToString:NCNotificationActionShareRecording]) { - NSDictionary *fileParameters = [serverNotification.messageRichParameters objectForKey:@"file"]; - - if (!fileParameters || ![fileParameters objectForKey:@"id"]) { - [bgTask stopBackgroundTask]; - return; - } - - NSString *fileId = [fileParameters objectForKey:@"id"]; - - [[NCAPIController sharedInstance] shareStoredRecordingWithTimestamp:notificationTimestamp - withFileId:fileId - forRoom:serverNotification.roomToken - forAccount:account - completionBlock:^(NSError *error) { - if (error) { - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:serverNotification.roomToken forKey:@"roomToken"]; - [userInfo setValue:@(kNCLocalNotificationTypeFailedToShareRecording) forKey:@"localNotificationType"]; - [userInfo setObject:account.accountId forKey:@"accountId"]; - - [self showLocalNotification:kNCLocalNotificationTypeFailedToShareRecording withUserInfo:userInfo]; - } - - [bgTask stopBackgroundTask]; - }]; - - } else if ([actionIdentifier isEqualToString:NCNotificationActionDismissRecordingNotification]) { - [[NCAPIController sharedInstance] dismissStoredRecordingNotificationWithTimestamp:notificationTimestamp - forRoom:serverNotification.roomToken - forAccount:account - completionBlock:^(NSError *error) { - [bgTask stopBackgroundTask]; - }]; - } else { - [bgTask stopBackgroundTask]; - - UIAlertController *alert = [UIAlertController alertControllerWithTitle:serverNotification.subject - message:serverNotification.message - preferredStyle:UIAlertControllerStyleAlert]; - - NSArray *notificationActions = [serverNotification notificationActions]; - for (NCNotificationAction *notificationAction in notificationActions) { - UIAlertAction* tempButton = [UIAlertAction actionWithTitle:notificationAction.actionLabel - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - [[NCAPIController sharedInstance] executeNotificationAction:notificationAction forAccount:account completionBlock:nil]; - }]; - - [alert addAction:tempButton]; - } - - if ([notificationActions count] == 0) { - // Make sure that we have at least a way to dismiss the notification, if there are no actions - UIAlertAction* okButton = [UIAlertAction - actionWithTitle:NSLocalizedString(@"OK", nil) - style:UIAlertActionStyleDefault - handler:nil]; - - [alert addAction:okButton]; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - [[NCUserInterfaceController sharedInstance] presentAlertViewController:alert]; - }); - } -} - -- (void)handlePushNotificationResponseForReminder:(NCNotification *)serverNotification withActionIdentifier:(NSString *)actionIdentifier forAccount:(TalkAccount *)account -{ - if (!account || !serverNotification) { - return; - } - - if ([NCRoomsManager shared].callViewController) { - return; - } - - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"handlePushNotificationResponseForReminder" expirationHandler:^(BGTaskHelper *task) { - [NCLog log:@"ExpirationHandler called - handlePushNotificationResponseForReminder"]; - }]; - - // Open the conversation for the reminder - [[NCRoomsManager shared] startChatWithRoomToken:serverNotification.roomToken]; - - // After opening the notification, we need to execute the DELETE action - for (NSDictionary *dict in serverNotification.actions) { - NCNotificationAction *notificationAction = [[NCNotificationAction alloc] initWithDictionary:dict]; - - if (notificationAction && notificationAction.actionType == NCNotificationActionTypeKNotificationActionTypeDelete) { - [[NCAPIController sharedInstance] executeNotificationAction:notificationAction forAccount:account completionBlock:^(NSError * _Nullable error) { - [bgTask stopBackgroundTask]; - }]; - - break; - } - } -} - -- (void)handlePushNotificationResponse:(NCPushNotification *)pushNotification -{ - if ([NCRoomsManager shared].callViewController) { - return; - } - - if (pushNotification) { - switch (pushNotification.type) { - case NCPushNotificationTypeCall: - { - [[NCUserInterfaceController sharedInstance] presentAlertForPushNotification:pushNotification]; - } - break; - case NCPushNotificationTypeRoom: - case NCPushNotificationTypeChat: - { - [[NCUserInterfaceController sharedInstance] presentChatForPushNotification:pushNotification]; - } - break; - default: - break; - } - } -} - -- (void)handleLocalNotificationResponse:(NSDictionary *)notificationUserInfo -{ - if ([NCRoomsManager shared].callViewController) { - return; - } - - NCLocalNotificationType localNotificationType = (NCLocalNotificationType)[[notificationUserInfo objectForKey:@"localNotificationType"] integerValue]; - if (localNotificationType > 0) { - switch (localNotificationType) { - case kNCLocalNotificationTypeMissedCall: - case kNCLocalNotificationTypeCancelledCall: - case kNCLocalNotificationTypeFailedSendChat: - case kNCLocalNotificationTypeChatNotification: - case kNCLocalNotificationTypeRecordingConsentRequired: - { - [[NCUserInterfaceController sharedInstance] presentChatForLocalNotification:notificationUserInfo]; - } - break; - case kNCLocalNotificationTypeCallFromOldAccount: - { - [[NCUserInterfaceController sharedInstance] presentSettingsViewController]; - } - break; - default: - break; - } - } -} - -@end diff --git a/NextcloudTalk/Notifications/NCNotificationController.swift b/NextcloudTalk/Notifications/NCNotificationController.swift new file mode 100644 index 000000000..9cb9f0de1 --- /dev/null +++ b/NextcloudTalk/Notifications/NCNotificationController.swift @@ -0,0 +1,635 @@ +// +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation +import UserNotifications + +// MARK: - Notification names + +extension NSNotification.Name { + static let NCNotificationControllerWillPresent = Notification.Name(rawValue: "NCNotificationControllerWillPresentNotification") + static let NCLocalNotificationJoinChat = Notification.Name(rawValue: "NCLocalNotificationJoinChatNotification") +} + +@objc extension NSNotification { + public static let NCNotificationControllerWillPresent = Notification.Name.NCNotificationControllerWillPresent + public static let NCLocalNotificationJoinChat = Notification.Name.NCLocalNotificationJoinChat +} + +@objcMembers +public class NCNotificationController: NSObject, UNUserNotificationCenterDelegate { + + static let shared = NCNotificationController() + + @available(*, renamed: "shared") + static func sharedInstance() -> NCNotificationController { + return NCNotificationController.shared + } + + // MARK: - Notification action identifiers + + public static let actionShareRecording = "SHARE_RECORDING" + public static let actionDismissRecordingNotification = "DISMISS_RECORDING_NOTIFICATION" + public static let actionReplyToChat = "REPLY_CHAT" + public static let actionFederationInvitationAccept = "ACCEPT_FEDERATION_INVITATION" + public static let actionFederationInvitationReject = "REJECT_FEDERATION_INVITATION" + + private let notificationCenter = UNUserNotificationCenter.current() + private var serverNotificationsAttempts: [AnyHashable: Any] = [:] // notificationId -> get attempts + + override init() { + super.init() + + self.notificationCenter.delegate = self + } + + public func requestAuthorization() { + let authOptions: UNAuthorizationOptions = [.alert, .sound, .badge] + self.notificationCenter.requestAuthorization(options: authOptions) { granted, _ in + if granted { + NSLog("User notifications permission granted.") + } else { + NSLog("User notifications permission denied.") + } + } + } + + public func processBackgroundPushNotification(_ pushNotification: NCPushNotification?) { + guard let pushNotification else { + return + } + + switch pushNotification.type { + case .NCPushNotificationTypeDelete: + self.removeNotification(withNotificationIds: [NSNumber(value: pushNotification.notificationId)], forAccountId: pushNotification.accountId, withCompletionBlock: nil) + case .NCPushNotificationTypeDeleteAll: + self.removeAllNotifications(forAccountId: pushNotification.accountId) + case .NCPushNotificationTypeDeleteMultiple: + self.removeNotification(withNotificationIds: pushNotification.notificationIds as? [NSNumber], forAccountId: pushNotification.accountId, withCompletionBlock: nil) + default: + NSLog("Push Notification of an unknown type received") + } + } + + @objc(showLocalNotification:withUserInfo:) + public func show(_ type: NCLocalNotificationType, withUserInfo userInfo: [AnyHashable: Any]) { + let content = UNMutableNotificationContent() + + switch type { + case .missedCall: + let missedCallString = NSLocalizedString("Missed call from", comment: "") + content.body = "☎️ \(missedCallString) \(userInfo["displayName"] ?? "")" + content.userInfo = userInfo + case .cancelledCall: + let cancelledCallString = NSLocalizedString("Cancelled call from another account", comment: "") + content.body = "☎️ \(cancelledCallString)" + content.userInfo = userInfo + case .failedSendChat: + content.body = NSLocalizedString("Failed to send message", comment: "") + content.userInfo = userInfo + case .callFromOldAccount: + content.body = NSLocalizedString("Received call from an old account", comment: "") + content.userInfo = userInfo + case .failedToShareRecording: + content.body = NSLocalizedString("Failed to share recording", comment: "") + content.userInfo = userInfo + case .failedToAcceptInvitation: + content.body = NSLocalizedString("Failed to accept invitation", comment: "") + content.userInfo = userInfo + case .recordingConsentRequired: + let consentRequiredString = NSLocalizedString("Recording consent required for joining the call", comment: "") + content.body = "⚠️ \(consentRequiredString) \(userInfo["displayName"] ?? "")" + content.userInfo = userInfo + case .endToEndEncryptionUnsupported: + let endToEndEncryptionUnsupported = NSLocalizedString("Calling is currently not supported because end-to-end-encryption is enabled on the server", comment: "") + content.body = "⚠️ \(endToEndEncryptionUnsupported)" + content.userInfo = userInfo + default: + break + } + + let identifier = String(format: "Notification-%f", Date().timeIntervalSince1970) + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + self.notificationCenter.add(request, withCompletionHandler: nil) + + let accountId = userInfo["accountId"] as? String ?? "" + NCDatabaseManager.sharedInstance().increaseUnreadBadgeNumber(forAccountId: accountId) + self.updateAppIconBadgeNumber() + } + + public func showLocalNotificationForIncomingCall(withPushNotificaion pushNotification: NCPushNotification) { + let content = UNMutableNotificationContent() + content.body = pushNotification.bodyForRemoteAlerts() + content.threadIdentifier = pushNotification.roomToken + content.sound = .default + + var userInfo: [AnyHashable: Any] = [:] + userInfo["pushNotification"] = pushNotification.jsonString + userInfo["accountId"] = pushNotification.accountId + userInfo["notificationId"] = pushNotification.notificationId + content.userInfo = userInfo + + let identifier = String(format: "Notification-%f", Date().timeIntervalSince1970) + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + self.notificationCenter.add(request, withCompletionHandler: nil) + + NCDatabaseManager.sharedInstance().increaseUnreadBadgeNumber(forAccountId: pushNotification.accountId) + self.updateAppIconBadgeNumber() + } + + public func showIncomingCall(forPushNotification pushNotification: NCPushNotification) { + if CallKitManager.isCallKitAvailable() { + CallKitManager.sharedInstance().reportIncomingCall(pushNotification.roomToken, withDisplayName: "Incoming call", forAccountId: pushNotification.accountId) + } else { + CallKitManager.sharedInstance().reportIncomingCallForNonCallKitDevices(withPushNotification: pushNotification) + } + } + + public func showIncomingCallForOldAccount() { + CallKitManager.sharedInstance().reportIncomingCallForOldAccount() + } + + public func showLocalNotification(forChatNotification notification: NCNotification, forAccountId accountId: String) { + let content = UNMutableNotificationContent() + content.title = notification.chatMessageTitle + content.body = notification.message + content.summaryArgument = notification.chatMessageAuthor + content.threadIdentifier = notification.roomToken + content.sound = .default + + // Currently not supported for local notifications + // content.categoryIdentifier = "CATEGORY_CHAT" + + var userInfo: [AnyHashable: Any] = [:] + userInfo["roomToken"] = notification.roomToken + userInfo["accountId"] = accountId + userInfo["notificationId"] = notification.notificationId + userInfo["localNotificationType"] = NCLocalNotificationType.chatNotification.rawValue + content.userInfo = userInfo + + let identifier = "ChatNotification-\(notification.notificationId)" + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + self.notificationCenter.add(request, withCompletionHandler: nil) + + NCDatabaseManager.sharedInstance().increaseUnreadBadgeNumber(forAccountId: accountId) + self.updateAppIconBadgeNumber() + } + + private func updateAppIconBadgeNumber() { + DispatchQueue.main.async { + UIApplication.shared.applicationIconBadgeNumber = NCDatabaseManager.sharedInstance().numberOfUnreadNotifications() + } + } + + public func removeAllNotifications(forAccountId accountId: String) { + // Check in pending notifications + self.notificationCenter.getPendingNotificationRequests { requests in + for notificationRequest in requests { + let notificationAccountId = notificationRequest.content.userInfo["accountId"] as? String + if let notificationAccountId, notificationAccountId == accountId { + self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [notificationRequest.identifier]) + } + } + } + // Check in delivered notifications + self.notificationCenter.getDeliveredNotifications { notifications in + for notification in notifications { + let notificationAccountId = notification.request.content.userInfo["accountId"] as? String + if let notificationAccountId, notificationAccountId == accountId { + self.notificationCenter.removeDeliveredNotifications(withIdentifiers: [notification.request.identifier]) + } + } + } + + NCDatabaseManager.sharedInstance().resetUnreadBadgeNumber(forAccountId: accountId) + self.updateAppIconBadgeNumber() + } + + private func removeNotification(withNotificationIds notificationIds: [NSNumber]?, forAccountId accountId: String, withCompletionBlock completion: (() -> Void)?) { + guard let notificationIds else { + completion?() + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "decreaseUnreadBadgeNumberForAccountId") + + let removeNotification: (UNNotificationRequest, Bool) -> Void = { notificationRequest, isPending in + let notificationAccountId = notificationRequest.content.userInfo["accountId"] as? String + let notificationId = (notificationRequest.content.userInfo["notificationId"] as? NSNumber)?.intValue ?? 0 + + guard notificationAccountId == accountId else { + return + } + + if notificationIds.contains(NSNumber(value: notificationId)) { + if isPending { + self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [notificationRequest.identifier]) + } else { + self.notificationCenter.removeDeliveredNotifications(withIdentifiers: [notificationRequest.identifier]) + } + + NCDatabaseManager.sharedInstance().decreaseUnreadBadgeNumber(forAccountId: accountId) + } + } + + let notificationsGroup = DispatchGroup() + + notificationsGroup.enter() + // Check in pending notifications + self.notificationCenter.getPendingNotificationRequests { requests in + for notificationRequest in requests { + if bgTask.isExpired { + notificationsGroup.leave() + return + } + removeNotification(notificationRequest, true) + } + + self.updateAppIconBadgeNumber() + notificationsGroup.leave() + } + + notificationsGroup.enter() + // Check in delivered notifications + self.notificationCenter.getDeliveredNotifications { notifications in + for notification in notifications { + if bgTask.isExpired { + notificationsGroup.leave() + return + } + removeNotification(notification.request, false) + } + + self.updateAppIconBadgeNumber() + notificationsGroup.leave() + } + + notificationsGroup.notify(queue: .main) { + completion?() + bgTask.stopBackgroundTask() + } + } + + public func checkNotificationExistance(completionBlock block: ((_ error: Error?) -> Void)?) { + let notificationsGroup = DispatchGroup() + + for account in NCDatabaseManager.sharedInstance().allAccounts() { + if !NCDatabaseManager.sharedInstance().serverHasNotificationsCapability(kNotificationsCapabilityExists, forAccountId: account.accountId) { + continue + } + + notificationsGroup.enter() + + self.notificationCenter.getDeliveredNotifications { notifications in + var notificationIdsOnDevice: [NSNumber] = [] + + // TODO: Instead of storing just the IDs, we can also store the identifier and directly + // remove the notification instead of iterating again removeNotificationWithNotificationIds + for notification in notifications { + let notificationRequest = notification.request + let notificationAccountId = notificationRequest.content.userInfo["accountId"] as? String + let notificationId = (notificationRequest.content.userInfo["notificationId"] as? NSNumber)?.intValue ?? 0 + + if notificationAccountId != account.accountId { + continue + } + + notificationIdsOnDevice.append(NSNumber(value: notificationId)) + } + + if notificationIdsOnDevice.isEmpty { + // No notifications for this account are currently shown on the system -> no need to check anything + notificationsGroup.leave() + return + } + + NCAPIController.sharedInstance().checkNotificationExistance(withIds: notificationIdsOnDevice.map { $0.intValue }, forAccount: account) { notificationIds, error in + if error != nil { + notificationsGroup.leave() + return + } + + // Remove all notificationIds which are still on the server + if let notificationIds { + for notificationId in notificationIds { + notificationIdsOnDevice.removeAll { $0.intValue == notificationId } + } + } + + // In case there are still notifications on the device (that are not on the server anymore) remove them + if notificationIdsOnDevice.isEmpty { + notificationsGroup.leave() + return + } + + self.removeNotification(withNotificationIds: notificationIdsOnDevice, forAccountId: account.accountId) { + notificationsGroup.leave() + } + } + } + } + + notificationsGroup.notify(queue: .main) { + // Notify backgroundFetch that we're finished + block?(nil) + } + } + + @nonobjc + public func checkNotificationExistance() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.checkNotificationExistance { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + + // MARK: - UNUserNotificationCenter delegate + + public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + // Called when a notification is delivered to a foreground app. + NotificationCenter.default.post(name: .NCNotificationControllerWillPresent, object: self, userInfo: nil) + completionHandler([.list, .banner]) + + // Remove the notification from Notification Center if it is from the active account + let notificationAccountId = notification.request.content.userInfo["accountId"] as? String + if let notificationAccountId, NCDatabaseManager.sharedInstance().activeAccount().accountId == notificationAccountId { + self.removeAllNotifications(forAccountId: notificationAccountId) + } + } + + public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + let notificationRequest = response.notification.request + let userInfo = notificationRequest.content.userInfo + + // Local notification + let localNotificationType = NCLocalNotificationType(rawValue: (userInfo["localNotificationType"] as? NSNumber)?.intValue ?? 0) + + // Push notification + let notificationString = userInfo["pushNotification"] as? String + let notificationAccountId = userInfo["accountId"] as? String + var pushNotification: NCPushNotification? + if let notificationString { + pushNotification = NCPushNotification(fromDecryptedString: notificationString, withAccountId: notificationAccountId) + } + + // Server notification (only available if the Notification Service Extension was able to fetch it) + let serverNotificationDict = userInfo["serverNotification"] as? [String: Any] + let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: notificationAccountId ?? "") + let serverNotification = NCNotification(dictionary: serverNotificationDict) + + // Update push notification with server notification + pushNotification?.threadId = serverNotification?.threadId ?? 0 + + // Handle notification response + if let pushNotification { + if let textInputResponse = response as? UNTextInputNotificationResponse { + pushNotification.responseUserText = textInputResponse.userText + self.handlePushNotificationResponse(withUserText: pushNotification) + } else if pushNotification.type == .NCPushNotificationTypeRecording { + self.handlePushNotificationResponseForRecording(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) + } else if pushNotification.type == .NCPUshNotificationTypeFederation { + self.handlePushNotificationResponseForFederation(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) + } else if pushNotification.type == .NCPushNotificationTypeReminder { + self.handlePushNotificationResponseForReminder(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) + } else { + self.handlePushNotificationResponse(pushNotification) + } + } else if let localNotificationType, localNotificationType.rawValue > 0 { + self.handleLocalNotificationResponse(notificationRequest.content.userInfo) + } + + completionHandler() + } + + private func handlePushNotificationResponse(withUserText pushNotification: NCPushNotification) { + NSLog("Received push-notification with user input -> sending chat message") + + guard let pushAccount = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: pushNotification.accountId) else { + return + } + + let application = UIApplication.shared + var sendTask: UIBackgroundTaskIdentifier = .invalid + sendTask = application.beginBackgroundTask { + application.endBackgroundTask(sendTask) + sendTask = .invalid + } + + DispatchQueue.global().async { + NCAPIController.sharedInstance().sendChatMessage(pushNotification.responseUserText, toRoom: pushNotification.roomToken, threadTitle: nil, replyTo: -1, referenceId: nil, silently: false, forAccount: pushAccount) { error in + if let error { + NSLog("Could not send chat message. Error: %@", error.description) + + // Display local push-notification to inform user + var userInfo: [AnyHashable: Any] = [:] + userInfo["roomToken"] = pushNotification.roomToken + userInfo["localNotificationType"] = NCLocalNotificationType.failedSendChat.rawValue + userInfo["accountId"] = pushNotification.accountId + userInfo["responseUserText"] = pushNotification.responseUserText + + NCNotificationController.sharedInstance().show(.failedSendChat, withUserInfo: userInfo) + } else { + // We replied to the message, so we can assume, we read it as well + NCDatabaseManager.sharedInstance().decreaseUnreadBadgeNumber(forAccountId: pushNotification.accountId) + self.updateAppIconBadgeNumber() + if let room = NCDatabaseManager.sharedInstance().room(withToken: pushNotification.roomToken, forAccountId: pushNotification.accountId) { + NCIntentController.sharedInstance().donateSendMessageIntent(for: room) + } + } + + application.endBackgroundTask(sendTask) + sendTask = .invalid + } + } + } + + private func handlePushNotificationResponseForFederation(_ serverNotification: NCNotification?, withActionIdentifier actionIdentifier: String, forAccount account: TalkAccount?) { + guard let account, let serverNotification else { + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "handlePushNotificationResponseForFederation") { _ in + NCLog.log("ExpirationHandler called - handlePushNotificationResponseForFederation") + } + + if actionIdentifier == NCNotificationController.actionFederationInvitationAccept { + let invitation = FederationInvitation(notification: serverNotification, for: account.accountId) + + NCAPIController.sharedInstance().acceptFederationInvitation(for: account.accountId, with: invitation.invitationId) { success in + if !success { + var userInfo: [AnyHashable: Any] = [:] + userInfo["roomToken"] = serverNotification.roomToken + userInfo["localNotificationType"] = NCLocalNotificationType.failedToAcceptInvitation.rawValue + userInfo["accountId"] = account.accountId + + self.show(.failedToAcceptInvitation, withUserInfo: userInfo) + } + + NCDatabaseManager.sharedInstance().decreasePendingFederationInvitation(forAccountId: account.accountId) + + bgTask.stopBackgroundTask() + } + } else if actionIdentifier == NCNotificationController.actionFederationInvitationReject { + let invitation = FederationInvitation(notification: serverNotification, for: account.accountId) + + NCAPIController.sharedInstance().rejectFederationInvitation(for: account.accountId, with: invitation.invitationId) { _ in + NCDatabaseManager.sharedInstance().decreasePendingFederationInvitation(forAccountId: account.accountId) + bgTask.stopBackgroundTask() + } + } else { + bgTask.stopBackgroundTask() + + let alert = UIAlertController(title: serverNotification.subject, message: serverNotification.message, preferredStyle: .alert) + + for notificationAction in serverNotification.notificationActions { + let tempButton = UIAlertAction(title: notificationAction.actionLabel, style: .default) { _ in + NCDatabaseManager.sharedInstance().decreasePendingFederationInvitation(forAccountId: account.accountId) + NCAPIController.sharedInstance().executeNotificationAction(notificationAction, forAccount: account, completionBlock: nil) + } + + alert.addAction(tempButton) + } + + DispatchQueue.main.async { + NCUserInterfaceController.sharedInstance().presentAlertViewController(alert) + } + } + } + + private func handlePushNotificationResponseForRecording(_ serverNotification: NCNotification?, withActionIdentifier actionIdentifier: String, forAccount account: TalkAccount?) { + guard let account, let serverNotification else { + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "handlePushNotificationResponseForRecording") { _ in + NCLog.log("ExpirationHandler called - handlePushNotificationResponseForRecording") + } + + let notificationTimeInterval = serverNotification.datetime?.timeIntervalSince1970 ?? 0 + let notificationTimestamp = String(format: "%.0f", notificationTimeInterval) + + if actionIdentifier == NCNotificationController.actionShareRecording { + let fileParameters = serverNotification.messageRichParameters["file"] as? [AnyHashable: Any] + + guard let fileParameters, let fileId = fileParameters["id"] as? String else { + bgTask.stopBackgroundTask() + return + } + + NCAPIController.sharedInstance().shareStoredRecording(withTimestamp: notificationTimestamp, withFileId: fileId, forRoom: serverNotification.roomToken, forAccount: account) { error in + if error != nil { + var userInfo: [AnyHashable: Any] = [:] + userInfo["roomToken"] = serverNotification.roomToken + userInfo["localNotificationType"] = NCLocalNotificationType.failedToShareRecording.rawValue + userInfo["accountId"] = account.accountId + + self.show(.failedToShareRecording, withUserInfo: userInfo) + } + + bgTask.stopBackgroundTask() + } + } else if actionIdentifier == NCNotificationController.actionDismissRecordingNotification { + NCAPIController.sharedInstance().dismissStoredRecordingNotification(withTimestamp: notificationTimestamp, forRoom: serverNotification.roomToken, forAccount: account) { _ in + bgTask.stopBackgroundTask() + } + } else { + bgTask.stopBackgroundTask() + + let alert = UIAlertController(title: serverNotification.subject, message: serverNotification.message, preferredStyle: .alert) + + let notificationActions = serverNotification.notificationActions + for notificationAction in notificationActions { + let tempButton = UIAlertAction(title: notificationAction.actionLabel, style: .default) { _ in + NCAPIController.sharedInstance().executeNotificationAction(notificationAction, forAccount: account, completionBlock: nil) + } + + alert.addAction(tempButton) + } + + if notificationActions.isEmpty { + // Make sure that we have at least a way to dismiss the notification, if there are no actions + let okButton = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil) + alert.addAction(okButton) + } + + DispatchQueue.main.async { + NCUserInterfaceController.sharedInstance().presentAlertViewController(alert) + } + } + } + + private func handlePushNotificationResponseForReminder(_ serverNotification: NCNotification?, withActionIdentifier actionIdentifier: String, forAccount account: TalkAccount?) { + guard let account, let serverNotification else { + return + } + + if NCRoomsManager.shared.callViewController != nil { + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "handlePushNotificationResponseForReminder") { _ in + NCLog.log("ExpirationHandler called - handlePushNotificationResponseForReminder") + } + + // Open the conversation for the reminder + NCRoomsManager.shared.startChat(withRoomToken: serverNotification.roomToken) + + // After opening the notification, we need to execute the DELETE action + for dict in serverNotification.actions { + let notificationAction = NCNotificationAction(dictionary: dict) + + if notificationAction.actionType == .kNotificationActionTypeDelete { + NCAPIController.sharedInstance().executeNotificationAction(notificationAction, forAccount: account) { _ in + bgTask.stopBackgroundTask() + } + + break + } + } + } + + private func handlePushNotificationResponse(_ pushNotification: NCPushNotification) { + if NCRoomsManager.shared.callViewController != nil { + return + } + + switch pushNotification.type { + case .NCPushNotificationTypeCall: + NCUserInterfaceController.sharedInstance().presentAlert(for: pushNotification) + case .NCPushNotificationTypeRoom, .NCPushNotificationTypeChat: + NCUserInterfaceController.sharedInstance().presentChat(for: pushNotification) + default: + break + } + } + + private func handleLocalNotificationResponse(_ notificationUserInfo: [AnyHashable: Any]) { + if NCRoomsManager.shared.callViewController != nil { + return + } + + let localNotificationType = NCLocalNotificationType(rawValue: (notificationUserInfo["localNotificationType"] as? NSNumber)?.intValue ?? 0) + guard let localNotificationType, localNotificationType.rawValue > 0 else { + return + } + + switch localNotificationType { + case .missedCall, .cancelledCall, .failedSendChat, .chatNotification, .recordingConsentRequired: + NCUserInterfaceController.sharedInstance().presentChat(forLocalNotification: notificationUserInfo) + case .callFromOldAccount: + NCUserInterfaceController.sharedInstance().presentSettingsViewController() + default: + break + } + } +} diff --git a/NextcloudTalk/Settings/NCSettingsController.h b/NextcloudTalk/Settings/NCSettingsController.h deleted file mode 100644 index 8d03c72ac..000000000 --- a/NextcloudTalk/Settings/NCSettingsController.h +++ /dev/null @@ -1,82 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import -#import - -#import "ARDSettingsModel.h" -#import "NCTypes.h" - -extern NSString * const kUserProfileDisplayName; -extern NSString * const kUserProfileDisplayNameScope; -extern NSString * const kUserProfileEmail; -extern NSString * const kUserProfileEmailScope; -extern NSString * const kUserProfilePhone; -extern NSString * const kUserProfilePhoneScope; -extern NSString * const kUserProfileAddress; -extern NSString * const kUserProfileAddressScope; -extern NSString * const kUserProfileWebsite; -extern NSString * const kUserProfileWebsiteScope; -extern NSString * const kUserProfileTwitter; -extern NSString * const kUserProfileTwitterScope; -extern NSString * const kUserProfileAvatarScope; - -extern NSString * const kUserProfileScopePrivate; -extern NSString * const kUserProfileScopeLocal; -extern NSString * const kUserProfileScopeFederated; -extern NSString * const kUserProfileScopePublished; - -extern NSString * const NCSettingsControllerDidChangeActiveAccountNotification; - -@class NCExternalSignalingController; -@class SignalingSettings; -@class OcsError; - -typedef void (^UpdatedProfileCompletionBlock)(OcsError *error); -typedef void (^LogoutCompletionBlock)(NSError *error); -typedef void (^GetCapabilitiesCompletionBlock)(OcsError *error); -typedef void (^UpdateSignalingConfigCompletionBlock)(NCExternalSignalingController * _Nullable signalingServer, NSError * _Nullable error); -typedef void (^SubscribeForPushNotificationsCompletionBlock)(BOOL success); -typedef void (^EnsureSignalingConfigCompletionBlock)(NCExternalSignalingController * _Nullable signalingServer); - - -@interface NCSettingsController : NSObject - -@property (nonatomic, copy) ARDSettingsModel *videoSettingsModel; -@property (nonatomic, strong) UIAlertController *updateAlertController; -@property (nonatomic, strong) NSString *updateAlertControllerAccountId; -@property (nonatomic, strong) NSMutableDictionary *signalingConfigurations; // accountId -> signalingConfigutation -@property (nonatomic, strong) NSMutableDictionary *externalSignalingControllers; // accountId -> externalSignalingController - -+ (instancetype)sharedInstance; -- (void)addNewAccountForUser:(NSString *)user withToken:(NSString *)token inServer:(NSString *)server; -- (void)setActiveAccountWithAccountId:(NSString *)accountId; -- (void)getUserProfileForAccountId:(NSString *)accountId withCompletionBlock:(UpdatedProfileCompletionBlock _Nonnull)block; -- (void)getUserGroupsAndTeamsForAccountId:(NSString *)accountId; -- (void)logoutAccountWithAccountId:(NSString *)accountId withCompletionBlock:(LogoutCompletionBlock)block; -- (void)getCapabilitiesForAccountId:(NSString *)accountId withCompletionBlock:(GetCapabilitiesCompletionBlock)block; -- (void)updateSignalingConfigurationForAccountId:(NSString * _Nonnull)accountId withCompletionBlock:(UpdateSignalingConfigCompletionBlock _Nonnull)block; -- (NCExternalSignalingController * _Nullable)setSignalingConfigurationForAccountId:(NSString * _Nonnull)accountId withSettings:(SignalingSettings * _Nonnull)settings; -- (void)ensureSignalingConfigurationForAccountId:(NSString * _Nonnull)accountId withSettings:(SignalingSettings * _Nullable)settings withCompletionBlock:(EnsureSignalingConfigCompletionBlock _Nonnull)block; -- (NCExternalSignalingController * _Nullable)externalSignalingControllerForAccountId:(NSString * _Nonnull)accountId; -- (void)connectDisconnectedExternalSignalingControllers; -- (void)disconnectAllExternalSignalingControllers; -- (void)subscribeForPushNotificationsForAccountId:(NSString *)accountId withCompletionBlock:(SubscribeForPushNotificationsCompletionBlock)block; -- (BOOL)canCreateGroupAndPublicRooms; -- (BOOL)isGuestsAppEnabled; -- (BOOL)isReferenceApiSupported; -- (BOOL)isRecordingEnabled; -- (NSString * _Nullable)passwordPolicyGenerateAPIEndpoint; -- (NSString * _Nullable)passwordPolicyValidateAPIEndpoint; -- (NSInteger)passwordPolicyMinLength; -- (NCPreferredFileSorting)getPreferredFileSorting; -- (void)setPreferredFileSorting:(NCPreferredFileSorting)sorting; -- (BOOL)isContactSyncEnabled; -- (void)setContactSync:(BOOL)enabled; -- (BOOL)didReceiveCallsFromOldAccount; -- (void)setDidReceiveCallsFromOldAccount:(BOOL)receivedOldCalls; -- (void)createAccountsFile; - -@end diff --git a/NextcloudTalk/Settings/NCSettingsController.m b/NextcloudTalk/Settings/NCSettingsController.m deleted file mode 100644 index 280c2fcae..000000000 --- a/NextcloudTalk/Settings/NCSettingsController.m +++ /dev/null @@ -1,855 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import "NCSettingsController.h" - -@import NextcloudKit; - -#import "JDStatusBarNotification.h" - -#import "NCAppBranding.h" -#import "NCDatabaseManager.h" -#import "NCKeyChainController.h" -#import "NCUserInterfaceController.h" -#import "NCUserDefaults.h" -#import "NCChatFileController.h" -#import "NotificationCenterNotifications.h" - -#import "NextcloudTalk-Swift.h" - -NSString * const NCSettingsControllerDidChangeActiveAccountNotification = @"NCSettingsControllerDidChangeActiveAccountNotification"; - -@implementation NCSettingsController - -NSString * const kUserProfileUserId = @"id"; -NSString * const kUserProfileDisplayName = @"displayname"; -NSString * const kUserProfileDisplayNameScope = @"displaynameScope"; -NSString * const kUserProfileEmail = @"email"; -NSString * const kUserProfileEmailScope = @"emailScope"; -NSString * const kUserProfilePhone = @"phone"; -NSString * const kUserProfilePhoneScope = @"phoneScope"; -NSString * const kUserProfileAddress = @"address"; -NSString * const kUserProfileAddressScope = @"addressScope"; -NSString * const kUserProfileWebsite = @"website"; -NSString * const kUserProfileWebsiteScope = @"websiteScope"; -NSString * const kUserProfileTwitter = @"twitter"; -NSString * const kUserProfileTwitterScope = @"twitterScope"; -NSString * const kUserProfileAvatarScope = @"avatarScope"; - -NSString * const kUserProfileScopePrivate = @"v2-private"; -NSString * const kUserProfileScopeLocal = @"v2-local"; -NSString * const kUserProfileScopeFederated = @"v2-federated"; -NSString * const kUserProfileScopePublished = @"v2-published"; - -NSString * const kPreferredFileSorting = @"preferredFileSorting"; -NSString * const kContactSyncEnabled = @"contactSyncEnabled"; - -NSString * const kDidReceiveCallsFromOldAccount = @"receivedCallsFromOldAccount"; - -+ (NCSettingsController *)sharedInstance -{ - static dispatch_once_t once; - static NCSettingsController *sharedInstance; - dispatch_once(&once, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (id)init -{ - self = [super init]; - if (self) { - _videoSettingsModel = [[ARDSettingsModel alloc] init]; - _signalingConfigurations = [NSMutableDictionary new]; - _externalSignalingControllers = [NSMutableDictionary new]; - - [self configureDatabase]; - [self checkStoredDataInKechain]; - [self resetPerAppLaunchSettings]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tokenRevokedResponseReceived:) name:NCTokenRevokedResponseReceivedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(upgradeRequiredResponseReceived:) name:NCUpgradeRequiredResponseReceivedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(talkConfigurationHasChanged:) name:NCTalkConfigurationHashChangedNotification object:nil]; - } - return self; -} - -#pragma mark - Database - -- (void)configureDatabase -{ - // Init database - [NCDatabaseManager sharedInstance]; -} - -- (void)checkStoredDataInKechain -{ - // Removed data stored in the Keychain if there are no accounts configured - // This step should be always done before the possible account migration - if ([[NCDatabaseManager sharedInstance] numberOfAccounts] == 0) { - NSLog(@"Removing all data stored in Keychain"); - [[NCKeyChainController sharedInstance] removeAllItems]; - } -} - -- (void)resetPerAppLaunchSettings -{ - // Reset "threadsLastCheckTimestamp" on every app fresh launch - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm beginWriteTransaction]; - for (TalkAccount *account in [TalkAccount allObjects]) { - account.threadsLastCheckTimestamp = 0; - } - [realm commitWriteTransaction]; -} - -#pragma mark - User accounts - -- (void)addNewAccountForUser:(NSString *)user withToken:(NSString *)token inServer:(NSString *)server -{ - NSString *accountId = [[NCDatabaseManager sharedInstance] accountIdForUser:user inServer:server]; - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - if (!account) { - [[NCDatabaseManager sharedInstance] createAccountForUser:user inServer:server]; - [[NCDatabaseManager sharedInstance] setActiveAccountWithAccountId:accountId]; - [[NCKeyChainController sharedInstance] setToken:token forAccountId:accountId]; - [self subscribeForPushNotificationsForAccountId:accountId withCompletionBlock:nil]; - [self createAccountsFile]; - } else { - [self setActiveAccountWithAccountId:accountId]; - [[JDStatusBarNotificationPresenter sharedPresenter] presentWithText:NSLocalizedString(@"Account already added", nil) dismissAfterDelay:4.0f includedStyle:JDStatusBarNotificationIncludedStyleSuccess]; - } -} - -- (void)setActiveAccountWithAccountId:(NSString *)accountId -{ - [[NCUserInterfaceController sharedInstance] presentConversationsList]; - [[NCDatabaseManager sharedInstance] setActiveAccountWithAccountId:accountId]; - [[NCDatabaseManager sharedInstance] resetUnreadBadgeNumberForAccountId:accountId]; - [[NCNotificationController sharedInstance] removeAllNotificationsForAccountId:accountId]; - [[NCConnectionController shared] checkAppState]; - - NSMutableDictionary *userInfo = [NSMutableDictionary new]; - [userInfo setObject:accountId forKey:@"accountId"]; - [[NSNotificationCenter defaultCenter] postNotificationName:NCSettingsControllerDidChangeActiveAccountNotification - object:self - userInfo:userInfo]; -} - -- (void)createAccountsFile -{ - if (!useAppsGroup) { - return; - } - - // Create accounts data - NSURL *appsGroupFolderURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appsGroupIdentifier]; - NSMutableArray *accounts = [NSMutableArray new]; - for (TalkAccount *account in [[NCDatabaseManager sharedInstance] allAccounts]) { - UIImage *accountImage = [[NCAPIController sharedInstance] userProfileImageForAccount:account withStyle:UIUserInterfaceStyleLight]; - if (accountImage) { - accountImage = [NCUtils roundedImageFromImage:accountImage]; - } - DataAccounts *accountData = [[DataAccounts alloc] initWithUrl:account.server user:account.user name:account.userDisplayName image:accountImage]; - [accounts addObject:accountData]; - } - - NKShareAccounts *shareAccounts = [[NKShareAccounts alloc] init]; - NSError *error = [shareAccounts putShareAccountsAt:appsGroupFolderURL app:@"nextcloudtalk" dataAccounts:accounts]; - NSLog(@"Created accounts file. Error: %@", error); -} - -#pragma mark - Notifications - -- (void)tokenRevokedResponseReceived:(NSNotification *)notification -{ - NSString *accountId = [notification.userInfo objectForKey:@"accountId"]; - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - // Always remove the account, whether the token has been revoked or marked for remote wipe - [self logoutAccountWithAccountId:accountId withCompletionBlock:^(NSError *error) { - if (!error) { - [[NCUserInterfaceController sharedInstance] presentConversationsList]; - [[NCUserInterfaceController sharedInstance] presentLoggedOutInvalidCredentialsAlert]; - [[NCConnectionController shared] checkAppState]; - - // If the token was marked for remote wipe, confirm the wipe - [[NCAPIController sharedInstance] checkWipeStatusForAccount:account completionBlock:^(BOOL wipe, NSError *error) { - if (wipe) { - [[NCAPIController sharedInstance] confirmWipeForAccount:account completionBlock:nil]; - } - }]; - } - }]; -} - -- (void)upgradeRequiredResponseReceived:(NSNotification *)notification -{ - NSString *accountId = [notification.userInfo objectForKey:@"accountId"]; - if (!_updateAlertController || ![_updateAlertControllerAccountId isEqualToString:accountId]) { - [self createUpdateAlertContollerForAccountId:accountId]; - } - - [[NCUserInterfaceController sharedInstance] presentAlertIfNotPresentedAlready:_updateAlertController]; - -} - -- (void)createUpdateAlertContollerForAccountId:(NSString *)accountId -{ - NSString *appStoreURLString = @"itms-apps://itunes.apple.com/app/id"; - BOOL canOpenAppStore = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:appStoreURLString]]; - - NSString *messageNotification = NSLocalizedString(@"The app is too old and no longer supported by this server.", nil); - NSString *messageAction = canOpenAppStore ? NSLocalizedString(@"Please update.", nil) : NSLocalizedString(@"Please contact your system administrator.", nil); - NSString *message = [NSString stringWithFormat:@"%@ %@", messageNotification, messageAction]; - - _updateAlertController = [UIAlertController - alertControllerWithTitle:NSLocalizedString(@"App is outdated", nil) - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - _updateAlertControllerAccountId = accountId; - - if (canOpenAppStore) { - UIAlertAction* updateButton = [UIAlertAction - actionWithTitle:NSLocalizedString(@"Update", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - - [[NCAPIController sharedInstance] getAppStoreAppIdWithCompletionBlock:^(NSString *appId, NSError *error) { - if (appId.length > 0) { - NSURL *appStoreURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", appStoreURLString, appId]]; - [[UIApplication sharedApplication] openURL:appStoreURL options:@{} completionHandler:nil]; - } - - self->_updateAlertControllerAccountId = nil; - }]; - }]; - - [_updateAlertController addAction:updateButton]; - } - - NSArray *inactiveAccounts = [[NCDatabaseManager sharedInstance] inactiveAccounts]; - if (inactiveAccounts.count > 0) { - UIAlertAction* switchAccountButton = [UIAlertAction - actionWithTitle:NSLocalizedString(@"Switch account", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - - [self switchToAnyInactiveAccount]; - self->_updateAlertControllerAccountId = nil; - }]; - - [_updateAlertController addAction:switchAccountButton]; - } - - UIAlertAction* logoutButton = [UIAlertAction - actionWithTitle:NSLocalizedString(@"Log out", nil) - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * _Nonnull action) { - - [[NCUserInterfaceController sharedInstance] logOutAccountWithAccountId:accountId]; - self->_updateAlertControllerAccountId = nil; - }]; - - [_updateAlertController addAction:logoutButton]; -} - -- (void)talkConfigurationHasChanged:(NSNotification *)notification -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - NSString *accountId = [notification.userInfo objectForKey:@"accountId"]; - NSString *configurationHash = [notification.userInfo objectForKey:@"configurationHash"]; - - if (!accountId || !configurationHash || ![activeAccount.accountId isEqualToString:accountId]) { - return; - } - - [self getCapabilitiesForAccountId:accountId withCompletionBlock:^(OcsError *error) { - if (error) { - return; - } - - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCUpdateSignalingConfiguration" expirationHandler:nil]; - - [self updateSignalingConfigurationForAccountId:accountId withCompletionBlock:^(NCExternalSignalingController * _Nullable signalingServer, NSError *error) { - if (!error) { - [[NCDatabaseManager sharedInstance] updateTalkConfigurationHashForAccountId:accountId withHash:configurationHash]; - } - - [bgTask stopBackgroundTask]; - }]; - }]; -} - -#pragma mark - User defaults - -- (NCPreferredFileSorting)getPreferredFileSorting -{ - NCPreferredFileSorting sorting = (NCPreferredFileSorting)[[[NSUserDefaults standardUserDefaults] objectForKey:kPreferredFileSorting] integerValue]; - if (!sorting) { - sorting = NCModificationDateSorting; - [[NSUserDefaults standardUserDefaults] setObject:@(sorting) forKey:kPreferredFileSorting]; - } - return sorting; -} - -- (void)setPreferredFileSorting:(NCPreferredFileSorting)sorting -{ - [[NSUserDefaults standardUserDefaults] setObject:@(sorting) forKey:kPreferredFileSorting]; -} - -- (BOOL)isContactSyncEnabled -{ - // Migration from global setting to per-account setting - if ([[[NSUserDefaults standardUserDefaults] objectForKey:kContactSyncEnabled] boolValue]) { - // If global setting was enabled then we enable contact sync for all accounts - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm beginWriteTransaction]; - for (TalkAccount *account in [TalkAccount allObjects]) { - account.hasContactSyncEnabled = YES; - } - [realm commitWriteTransaction]; - // Remove global setting - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kContactSyncEnabled]; - [[NSUserDefaults standardUserDefaults] synchronize]; - return YES; - } - - return [[NCDatabaseManager sharedInstance] activeAccount].hasContactSyncEnabled; -} - -- (void)setContactSync:(BOOL)enabled -{ - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm beginWriteTransaction]; - TalkAccount *account = [TalkAccount objectsWhere:(@"active = true")].firstObject; - account.hasContactSyncEnabled = enabled; - [realm commitWriteTransaction]; -} - -- (BOOL)didReceiveCallsFromOldAccount -{ - BOOL didReceiveCallsFromOldAccount = [[[NSUserDefaults standardUserDefaults] objectForKey:kDidReceiveCallsFromOldAccount] boolValue]; - - return didReceiveCallsFromOldAccount; -} - -- (void)setDidReceiveCallsFromOldAccount:(BOOL)receivedOldCalls -{ - [[NSUserDefaults standardUserDefaults] setObject:@(receivedOldCalls) forKey:kDidReceiveCallsFromOldAccount]; -} - -#pragma mark - User Profile - -- (void)getUserProfileForAccountId:(NSString *)accountId withCompletionBlock:(UpdatedProfileCompletionBlock)block -{ - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - if (!account) { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - block(error); - - return; - } - - [[NCAPIController sharedInstance] getUserProfileForAccount:account completionBlock:^(NSDictionary *userProfile, OcsError *error) { - if (!error) { - id emailObject = [userProfile objectForKey:kUserProfileEmail]; - NSString *email = emailObject; - if (!emailObject || [emailObject isEqual:[NSNull null]]) { - email = @""; - } - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCSetUserProfile" expirationHandler:nil]; - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm transactionWithBlock:^{ - NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", account.accountId]; - TalkAccount *managedActiveAccount = [TalkAccount objectsWithPredicate:query].firstObject; - - if (!managedActiveAccount) { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - block(error); - - return; - } - - managedActiveAccount.userId = [userProfile objectForKey:kUserProfileUserId]; - // "display-name" is returned by /cloud/user endpoint - // change to kUserProfileDisplayName ("displayName") when using /cloud/users/{userId} endpoint - managedActiveAccount.userDisplayName = [userProfile objectForKey:@"display-name"]; - managedActiveAccount.userDisplayNameScope = [userProfile objectForKey:kUserProfileDisplayNameScope]; - managedActiveAccount.phone = [userProfile objectForKey:kUserProfilePhone]; - managedActiveAccount.phoneScope = [userProfile objectForKey:kUserProfilePhoneScope]; - managedActiveAccount.email = email; - managedActiveAccount.emailScope = [userProfile objectForKey:kUserProfileEmailScope]; - managedActiveAccount.address = [userProfile objectForKey:kUserProfileAddress]; - managedActiveAccount.addressScope = [userProfile objectForKey:kUserProfileAddressScope]; - managedActiveAccount.website = [userProfile objectForKey:kUserProfileWebsite]; - managedActiveAccount.websiteScope = [userProfile objectForKey:kUserProfileWebsiteScope]; - managedActiveAccount.twitter = [userProfile objectForKey:kUserProfileTwitter]; - managedActiveAccount.twitterScope = [userProfile objectForKey:kUserProfileTwitterScope]; - managedActiveAccount.avatarScope = [userProfile objectForKey:kUserProfileAvatarScope]; - - TalkAccount *unmanagedUpdatedAccount = [[TalkAccount alloc] initWithValue:managedActiveAccount]; - [[NCAPIController sharedInstance] saveProfileImageForAccount:unmanagedUpdatedAccount]; - - block(nil); - }]; - [bgTask stopBackgroundTask]; - } else { - NSLog(@"Error while getting the user profile"); - block(error); - } - }]; -} - -- (void)getUserGroupsAndTeamsForAccountId:(NSString *)accountId -{ - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - if (!account) { - return; - } - - [[NCAPIController sharedInstance] getUserGroupsForAccount:account completionBlock:^(NSArray * _Nullable groupIds, NSError * _Nullable error) { - if (!error) { - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCSetUserGroups" expirationHandler:nil]; - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm transactionWithBlock:^{ - NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", account.accountId]; - TalkAccount *managedActiveAccount = [TalkAccount objectsWithPredicate:query].firstObject; - - if (!managedActiveAccount) { - return; - } - - managedActiveAccount.groupIds = groupIds; - }]; - [bgTask stopBackgroundTask]; - } else { - NSLog(@"Error while getting user's groups"); - } - }]; - - [[NCAPIController sharedInstance] getUserTeamsForAccount:account completionBlock:^(NSArray * _Nullable teamIds, NSError * _Nullable error) { - if (!error) { - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCSetUserTeams" expirationHandler:nil]; - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm transactionWithBlock:^{ - NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", account.accountId]; - TalkAccount *managedActiveAccount = [TalkAccount objectsWithPredicate:query].firstObject; - - if (!managedActiveAccount) { - return; - } - - managedActiveAccount.teamIds = teamIds; - }]; - [bgTask stopBackgroundTask]; - } else { - NSLog(@"Error while getting user' teams"); - } - }]; -} - -- (void)logoutAccountWithAccountId:(NSString *)accountId withCompletionBlock:(LogoutCompletionBlock)block -{ - TalkAccount *removingAccount = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - if (!removingAccount) { - if (block) { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - block(error); - } - return; - } - - if (removingAccount.deviceIdentifier) { - [[NCAPIController sharedInstance] unsubscribeAccount:removingAccount fromNextcloudServerWithCompletionBlock:^(OcsError *error) { - if (!error) { - NSLog(@"Unsubscribed from NC server!!!"); - } else { - NSLog(@"Error while unsubscribing from NC server."); - } - }]; - [[NCAPIController sharedInstance] unsubscribeAccount:removingAccount fromPushServerWithCompletionBlock:^(NSError *error) { - if (!error) { - NSLog(@"Unsubscribed from Push Notification server!!!"); - } else { - NSLog(@"Error while unsubscribing from Push Notification server."); - } - }]; - } - NCExternalSignalingController *extSignalingController = [self externalSignalingControllerForAccountId:removingAccount.accountId]; - [extSignalingController disconnect]; - [[NCAPIController sharedInstance] removeProfileImageForAccount:removingAccount]; - [[NCAPIController sharedInstance] removeAPISessionManagerForAccount:removingAccount]; - [[NCDatabaseManager sharedInstance] removeAccountWithAccountId:removingAccount.accountId]; - [[[NCChatFileController alloc] init] deleteDownloadDirectoryForAccount:removingAccount]; - [[[NCRoomsManager shared] chatViewController] leaveChat]; - [self createAccountsFile]; - - // Activate any of the inactive accounts - [self switchToAnyInactiveAccount]; - - if (block) block(nil); -} - -- (void)switchToAnyInactiveAccount -{ - NSArray *inactiveAccounts = [[NCDatabaseManager sharedInstance] inactiveAccounts]; - if (inactiveAccounts.count > 0) { - TalkAccount *inactiveAccount = [inactiveAccounts objectAtIndex:0]; - [self setActiveAccountWithAccountId:inactiveAccount.accountId]; - } -} - -#pragma mark - Signaling Configuration - -- (void)updateSignalingConfigurationForAccountId:(NSString *)accountId withCompletionBlock:(UpdateSignalingConfigCompletionBlock)block -{ - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - if (!account) { - if (block) { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - block(nil, error); - } - - return; - } - - [[NCAPIController sharedInstance] getSignalingSettingsFor:account forRoom:nil completionBlock:^(SignalingSettings * _Nullable settings, NSError * _Nullable error) { - if (!error) { - if (settings && account && account.accountId) { - NCExternalSignalingController *extSignalingController = [self setSignalingConfigurationForAccountId:account.accountId withSettings:settings]; - - if (block) { - block(extSignalingController, nil); - } - } else { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - - if (block) { - block(nil, error); - } - } - } else { - NSLog(@"Error while getting signaling configuration"); - if (block) { - block(nil, error); - } - } - }]; -} - -- (NCExternalSignalingController * _Nullable)setSignalingConfigurationForAccountId:(NSString *)accountId withSettings:(SignalingSettings * _Nonnull)signalingSettings -{ - [self->_signalingConfigurations setObject:signalingSettings forKey:accountId]; - - if (signalingSettings.server && signalingSettings.server.length > 0 && signalingSettings.ticket && signalingSettings.ticket.length > 0) { - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCSetSignalingConfiguration" expirationHandler:nil]; - NCExternalSignalingController *extSignalingController = [self->_externalSignalingControllers objectForKey:accountId]; - - if (extSignalingController) { - [extSignalingController disconnect]; - } - - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - extSignalingController = [[NCExternalSignalingController alloc] initWithAccount:account serverUrl:signalingSettings.server ticket:signalingSettings.ticket]; - [self->_externalSignalingControllers setObject:extSignalingController forKey:accountId]; - - [bgTask stopBackgroundTask]; - - return extSignalingController; - } - - return nil; -} - -- (void)ensureSignalingConfigurationForAccountId:(NSString *)accountId withSettings:(SignalingSettings *)settings withCompletionBlock:(EnsureSignalingConfigCompletionBlock)block -{ - SignalingSettings *currentSignalingSettings = [_signalingConfigurations objectForKey:accountId]; - - if (currentSignalingSettings) { - block([self->_externalSignalingControllers objectForKey:accountId]); - } else { - [NCLog log:@"Ensure signaling configuration -> Setting configuration"]; - - if (settings) { - // In case settings are provided, we use these provided settings - NCExternalSignalingController *extSignalingController = [self setSignalingConfigurationForAccountId:accountId withSettings:settings]; - block(extSignalingController); - } else { - // There were no settings provided for that call, we have to update the settings - [self updateSignalingConfigurationForAccountId:accountId withCompletionBlock:^(NCExternalSignalingController * _Nullable signalingServer, NSError *error) { - block(signalingServer); - }]; - } - } -} - -- (NCExternalSignalingController *)externalSignalingControllerForAccountId:(NSString *)accountId -{ - return [_externalSignalingControllers objectForKey:accountId]; -} - -- (void)connectDisconnectedExternalSignalingControllers -{ - for (NCExternalSignalingController *extSignalingController in self->_externalSignalingControllers.allValues) { - if (extSignalingController.disconnected) { - [extSignalingController connect]; - } - } -} - -- (void)disconnectAllExternalSignalingControllers -{ - for (NCExternalSignalingController *extSignalingController in self->_externalSignalingControllers.allValues) { - [extSignalingController disconnect]; - } -} - -#pragma mark - Server Capabilities - -- (void)getCapabilitiesForAccountId:(NSString *)accountId withCompletionBlock:(GetCapabilitiesCompletionBlock)block -{ - TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - - if (!account) { - if (block) { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - block(error); - } - - return; - } - - [[NCAPIController sharedInstance] getServerCapabilitiesForAccount:account completionBlock:^(NSDictionary *serverCapabilities, OcsError *error) { - if (!error && [serverCapabilities isKindOfClass:[NSDictionary class]]) { - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCUpdateCapabilitiesTransaction" expirationHandler:nil]; - [[NCDatabaseManager sharedInstance] setServerCapabilities:serverCapabilities forAccountId:account.accountId]; - [self checkServerCapabilitiesForAccount:account]; - [bgTask stopBackgroundTask]; - - [[NSNotificationCenter defaultCenter] postNotificationName:NCServerCapabilitiesUpdatedNotification - object:self - userInfo:nil]; - if (block) { - block(nil); - } - } else { - NSLog(@"Error while getting server capabilities"); - if (block) { - block(error); - } - } - }]; -} - -- (void)checkServerCapabilitiesForAccount:(TalkAccount *)account -{ - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:account.accountId]; - if (serverCapabilities) { - NSArray *talkFeatures = [serverCapabilities.talkCapabilities valueForKey:@"self"]; - if (!talkFeatures || [talkFeatures count] == 0) { - [[NSNotificationCenter defaultCenter] postNotificationName:NCTalkNotInstalledNotification - object:self - userInfo:nil]; - } else if (![talkFeatures containsObject:kMinimumRequiredTalkCapability]) { - [[NSNotificationCenter defaultCenter] postNotificationName:NCOutdatedTalkVersionNotification - object:self - userInfo:nil]; - } - } -} - -- (BOOL)canCreateGroupAndPublicRooms -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.canCreate; - } - return YES; -} - -- (BOOL)isGuestsAppEnabled -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.guestsAppEnabled; - } - return NO; -} - -- (BOOL)isReferenceApiSupported -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.referenceApiSupported; - } - return NO; -} - -- (BOOL)isRecordingEnabled -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities && [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityRecordingV1]) { - return serverCapabilities.recordingEnabled; - } - return NO; -} - -- (NSString * _Nullable)passwordPolicyGenerateAPIEndpoint -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.passwordPolicyGenerateAPIEndpoint; - } - return nil; -} - -- (NSString * _Nullable)passwordPolicyValidateAPIEndpoint -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.passwordPolicyValidateAPIEndpoint; - } - return nil; -} - -- (NSInteger)passwordPolicyMinLength -{ - TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; - ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; - if (serverCapabilities) { - return serverCapabilities.passwordPolicyMinLength; - } - return -1; -} - -#pragma mark - Push Notifications - -- (void)subscribeForPushNotificationsForAccountId:(NSString *)accountId withCompletionBlock:(SubscribeForPushNotificationsCompletionBlock)block; -{ -#if !TARGET_IPHONE_SIMULATOR - NCPushNotificationKeyPair *keyPair = nil; - NSData *pushNotificationPublicKey = [[NCKeyChainController sharedInstance] pushNotificationPublicKeyForAccountId:accountId]; - NSData *pushNotificationPrivateKey = [[NCKeyChainController sharedInstance] pushNotificationPrivateKeyForAccountId:accountId]; - - if (pushNotificationPublicKey && pushNotificationPrivateKey) { - keyPair = [[NCPushNotificationKeyPair alloc] initWithPrivateKey:pushNotificationPrivateKey publicKey:pushNotificationPublicKey]; - } else { - keyPair = [NCPushNotificationsUtils generatePushNotificationKeyPair]; - } - - if (!keyPair) { - [NCLog log:@"Error while subscribing: Unable to generate push notifications key pair."]; - - if (block) { - block(NO); - } - - return; - } - - NSString *pushToken = [[NCKeyChainController sharedInstance] combinedPushToken]; - - if (!pushToken) { - [NCLog log:@"Error while subscribing: Push token is not available."]; - - if (block) { - block(NO); - } - - return; - } - - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"PushProxySubscription" expirationHandler:nil]; - - [[NCAPIController sharedInstance] subscribeAccount:[[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId] withPublicKey:keyPair.publicKey toNextcloudServerWithCompletionBlock:^(NSDictionary *responseDict, OcsError *error) { - if (!error) { - [NCLog log:@"Subscribed to NC server successfully."]; - - NSString *publicKey = [responseDict objectForKey:@"publicKey"]; - NSString *deviceIdentifier = [responseDict objectForKey:@"deviceIdentifier"]; - NSString *signature = [responseDict objectForKey:@"signature"]; - - if (!publicKey || !deviceIdentifier || !signature) { - [NCLog log:@"Something went wrong subscribing to NC server. Aborting subscribe to Push Notification server."]; - - if (block) { - block(NO); - } - - [bgTask stopBackgroundTask]; - return; - } - - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm beginWriteTransaction]; - NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; - TalkAccount *managedAccount = [TalkAccount objectsWithPredicate:query].firstObject; - managedAccount.userPublicKey = publicKey; - managedAccount.deviceIdentifier = deviceIdentifier; - managedAccount.deviceSignature = signature; - [realm commitWriteTransaction]; - - [[NCAPIController sharedInstance] subscribeAccount:[[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId] toPushServerWithCompletionBlock:^(NSError *error) { - if (!error) { - RLMRealm *realm = [RLMRealm defaultRealm]; - [realm beginWriteTransaction]; - NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; - TalkAccount *managedAccount = [TalkAccount objectsWithPredicate:query].firstObject; - managedAccount.lastPushSubscription = [[NSDate date] timeIntervalSince1970]; - [realm commitWriteTransaction]; - [[NCKeyChainController sharedInstance] setPushNotificationPublicKey:keyPair.publicKey forAccountId:accountId]; - [[NCKeyChainController sharedInstance] setPushNotificationPrivateKey:keyPair.privateKey forAccountId:accountId]; - [NCLog log:@"Subscribed to Push Notification server successfully."]; - - if (block) { - block(YES); - } - - [bgTask stopBackgroundTask]; - } else { - [NCLog log:[NSString stringWithFormat:@"Error while subscribing to Push Notification server. Error: %@", error.description]]; - [NCLog log:[NSString stringWithFormat:@"Push notification, public key: %@", publicKey]]; - [NCLog log:[NSString stringWithFormat:@"Push notification, device signature: %@", signature]]; - [NCLog log:[NSString stringWithFormat:@"Push notification, device identifier: %@", deviceIdentifier]]; - [[NCKeyChainController sharedInstance] logCombinedPushToken]; - - if (block) { - block(NO); - } - - [bgTask stopBackgroundTask]; - } - }]; - } else { - [NCLog log:[NSString stringWithFormat:@"Error while subscribing to NC server. Error: %@", error.description]]; - - if (block) { - block(NO); - } - - [bgTask stopBackgroundTask]; - } - }]; -#else - if (block) { - block(YES); - } -#endif -} - -@end diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index c19808738..04e4e5117 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -1,9 +1,712 @@ // -// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors // SPDX-License-Identifier: GPL-3.0-or-later // import Foundation +import NextcloudKit + +// MARK: - Notification names + +extension NSNotification.Name { + static let NCSettingsControllerDidChangeActiveAccount = Notification.Name(rawValue: "NCSettingsControllerDidChangeActiveAccountNotification") +} + +@objc extension NSNotification { + public static let NCSettingsControllerDidChangeActiveAccount = Notification.Name.NCSettingsControllerDidChangeActiveAccount +} + +// MARK: - User profile keys + +let kUserProfileUserId = "id" +let kUserProfileDisplayName = "displayname" +let kUserProfileDisplayNameScope = "displaynameScope" +let kUserProfileEmail = "email" +let kUserProfileEmailScope = "emailScope" +let kUserProfilePhone = "phone" +let kUserProfilePhoneScope = "phoneScope" +let kUserProfileAddress = "address" +let kUserProfileAddressScope = "addressScope" +let kUserProfileWebsite = "website" +let kUserProfileWebsiteScope = "websiteScope" +let kUserProfileTwitter = "twitter" +let kUserProfileTwitterScope = "twitterScope" +let kUserProfileAvatarScope = "avatarScope" + +let kUserProfileScopePrivate = "v2-private" +let kUserProfileScopeLocal = "v2-local" +let kUserProfileScopeFederated = "v2-federated" +let kUserProfileScopePublished = "v2-published" + +private let kPreferredFileSorting = "preferredFileSorting" +private let kContactSyncEnabled = "contactSyncEnabled" +private let kDidReceiveCallsFromOldAccount = "receivedCallsFromOldAccount" + +@objcMembers +public class NCSettingsController: NSObject { + + static let shared = NCSettingsController() + + @available(*, renamed: "shared") + static func sharedInstance() -> NCSettingsController { + return NCSettingsController.shared + } + + public var videoSettingsModel = ARDSettingsModel() + public var signalingConfigurations = NSMutableDictionary() // accountId -> signalingConfiguration + public var externalSignalingControllers = NSMutableDictionary() // accountId -> externalSignalingController + + private var updateAlertController: UIAlertController? + private var updateAlertControllerAccountId: String? + + override init() { + super.init() + + self.configureDatabase() + self.checkStoredDataInKeychain() + self.resetPerAppLaunchSettings() + + NotificationCenter.default.addObserver(self, selector: #selector(tokenRevokedResponseReceived(_:)), name: .NCTokenRevokedResponseReceived, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(upgradeRequiredResponseReceived(_:)), name: .NCUpgradeRequiredResponseReceived, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(talkConfigurationHasChanged(_:)), name: .NCTalkConfigurationHashChanged, object: nil) + } + + // MARK: - Database + + private func configureDatabase() { + // Init database + _ = NCDatabaseManager.sharedInstance() + } + + private func checkStoredDataInKeychain() { + // Remove data stored in the Keychain if there are no accounts configured + // This step should always be done before the possible account migration + if NCDatabaseManager.sharedInstance().numberOfAccounts() == 0 { + NSLog("Removing all data stored in Keychain") + NCKeyChainController.sharedInstance().removeAllItems() + } + } + + private func resetPerAppLaunchSettings() { + // Reset "threadsLastCheckTimestamp" on every app fresh launch + let realm = RLMRealm.default() + try? realm.transaction { + for case let account as TalkAccount in TalkAccount.allObjects() { + account.threadsLastCheckTimestamp = 0 + } + } + } + + // MARK: - User accounts + + public func addNewAccount(forUser user: String, withToken token: String, inServer server: String) { + let accountId = NCDatabaseManager.sharedInstance().accountId(forUser: user, inServer: server) + let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) + + if account == nil { + NCDatabaseManager.sharedInstance().createAccount(forUser: user, inServer: server) + NCDatabaseManager.sharedInstance().setActiveAccountWithAccountId(accountId) + NCKeyChainController.sharedInstance().setToken(token, forAccountId: accountId) + self.subscribeForPushNotifications(forAccountId: accountId, withCompletionBlock: nil) + self.createAccountsFile() + } else { + self.setActiveAccountWithAccountId(accountId) + NotificationPresenter.shared().present(text: NSLocalizedString("Account already added", comment: ""), dismissAfterDelay: 4.0, includedStyle: .success) + } + } + + public func setActiveAccountWithAccountId(_ accountId: String) { + NCUserInterfaceController.sharedInstance().presentConversationsList() + NCDatabaseManager.sharedInstance().setActiveAccountWithAccountId(accountId) + NCDatabaseManager.sharedInstance().resetUnreadBadgeNumber(forAccountId: accountId) + NCNotificationController.sharedInstance().removeAllNotifications(forAccountId: accountId) + NCConnectionController.shared.checkAppState() + + let userInfo = ["accountId": accountId] + NotificationCenter.default.post(name: .NCSettingsControllerDidChangeActiveAccount, object: self, userInfo: userInfo) + } + + public func createAccountsFile() { + guard useAppsGroup.boolValue else { + return + } + + // Create accounts data + guard let appsGroupFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appsGroupIdentifier) else { + return + } + + var accounts: [NKShareAccounts.DataAccounts] = [] + for account in NCDatabaseManager.sharedInstance().allAccounts() { + var accountImage = NCAPIController.sharedInstance().userProfileImage(forAccount: account, withStyle: .light) + if let image = accountImage { + accountImage = NCUtils.roundedImage(fromImage: image) + } + let accountData = NKShareAccounts.DataAccounts(withUrl: account.server, user: account.user, name: account.userDisplayName, image: accountImage) + accounts.append(accountData) + } + + let error = NKShareAccounts().putShareAccounts(at: appsGroupFolderURL, app: "nextcloudtalk", dataAccounts: accounts) + NSLog("Created accounts file. Error: %@", error?.localizedDescription ?? "nil") + } + + // MARK: - Notifications + + func tokenRevokedResponseReceived(_ notification: Notification) { + guard let accountId = notification.userInfo?["accountId"] as? String else { return } + let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) + + // Always remove the account, whether the token has been revoked or marked for remote wipe + self.logoutAccount(withAccountId: accountId) { error in + guard error == nil, let account else { return } + + NCUserInterfaceController.sharedInstance().presentConversationsList() + NCUserInterfaceController.sharedInstance().presentLoggedOutInvalidCredentialsAlert() + NCConnectionController.shared.checkAppState() + + // If the token was marked for remote wipe, confirm the wipe + NCAPIController.sharedInstance().checkWipeStatus(forAccount: account) { wipe, _ in + if wipe { + NCAPIController.sharedInstance().confirmWipe(forAccount: account, completionBlock: nil) + } + } + } + } + + func upgradeRequiredResponseReceived(_ notification: Notification) { + guard let accountId = notification.userInfo?["accountId"] as? String else { return } + + if self.updateAlertController == nil || self.updateAlertControllerAccountId != accountId { + self.createUpdateAlertController(forAccountId: accountId) + } + + if let updateAlertController = self.updateAlertController { + NCUserInterfaceController.sharedInstance().presentAlertIfNotPresentedAlready(updateAlertController) + } + } + + private func createUpdateAlertController(forAccountId accountId: String) { + let appStoreURLString = "itms-apps://itunes.apple.com/app/id" + let canOpenAppStore = URL(string: appStoreURLString).map { UIApplication.shared.canOpenURL($0) } ?? false + + let messageNotification = NSLocalizedString("The app is too old and no longer supported by this server.", comment: "") + let messageAction = canOpenAppStore ? NSLocalizedString("Please update.", comment: "") : NSLocalizedString("Please contact your system administrator.", comment: "") + let message = "\(messageNotification) \(messageAction)" + + let alertController = UIAlertController(title: NSLocalizedString("App is outdated", comment: ""), message: message, preferredStyle: .alert) + + self.updateAlertController = alertController + self.updateAlertControllerAccountId = accountId + + if canOpenAppStore { + let updateButton = UIAlertAction(title: NSLocalizedString("Update", comment: ""), style: .default) { _ in + NCAPIController.sharedInstance().getAppStoreAppId { appId, _ in + if let appId, !appId.isEmpty, let appStoreURL = URL(string: "\(appStoreURLString)\(appId)") { + UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) + } + + self.updateAlertControllerAccountId = nil + } + } + + alertController.addAction(updateButton) + } + + if !NCDatabaseManager.sharedInstance().inactiveAccounts().isEmpty { + let switchAccountButton = UIAlertAction(title: NSLocalizedString("Switch account", comment: ""), style: .default) { _ in + self.switchToAnyInactiveAccount() + self.updateAlertControllerAccountId = nil + } + + alertController.addAction(switchAccountButton) + } + + let logoutButton = UIAlertAction(title: NSLocalizedString("Log out", comment: ""), style: .destructive) { _ in + NCUserInterfaceController.sharedInstance().logOutAccount(withAccountId: accountId) + self.updateAlertControllerAccountId = nil + } + + alertController.addAction(logoutButton) + } + + func talkConfigurationHasChanged(_ notification: Notification) { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + guard let accountId = notification.userInfo?["accountId"] as? String, + let configurationHash = notification.userInfo?["configurationHash"] as? String, + activeAccount.accountId == accountId + else { return } + + self.getCapabilitiesForAccountId(accountId) { error in + if error != nil { + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCUpdateSignalingConfiguration") + + self.updateSignalingConfiguration(forAccountId: accountId) { _, error in + if error == nil { + NCDatabaseManager.sharedInstance().updateTalkConfigurationHash(forAccountId: accountId, withHash: configurationHash) + } + + bgTask.stopBackgroundTask() + } + } + } + + // MARK: - User defaults + + public func getPreferredFileSorting() -> NCPreferredFileSorting { + let rawValue = (UserDefaults.standard.object(forKey: kPreferredFileSorting) as? NSNumber)?.intValue ?? 0 + + guard let sorting = NCPreferredFileSorting(rawValue: rawValue), rawValue != 0 else { + UserDefaults.standard.set(NSNumber(value: NCPreferredFileSorting.modificationDateSorting.rawValue), forKey: kPreferredFileSorting) + return .modificationDateSorting + } + + return sorting + } + + public func setPreferredFileSorting(_ sorting: NCPreferredFileSorting) { + UserDefaults.standard.set(NSNumber(value: sorting.rawValue), forKey: kPreferredFileSorting) + } + + public func isContactSyncEnabled() -> Bool { + // Migration from global setting to per-account setting + if (UserDefaults.standard.object(forKey: kContactSyncEnabled) as? NSNumber)?.boolValue == true { + // If global setting was enabled then we enable contact sync for all accounts + let realm = RLMRealm.default() + try? realm.transaction { + for case let account as TalkAccount in TalkAccount.allObjects() { + account.hasContactSyncEnabled = true + } + } + // Remove global setting + UserDefaults.standard.removeObject(forKey: kContactSyncEnabled) + UserDefaults.standard.synchronize() + return true + } + + return NCDatabaseManager.sharedInstance().activeAccount().hasContactSyncEnabled + } + + public func setContactSync(_ enabled: Bool) { + let realm = RLMRealm.default() + try? realm.transaction { + let account = TalkAccount.objects(where: "active = true").firstObject() as? TalkAccount + account?.hasContactSyncEnabled = enabled + } + } + + public func didReceiveCallsFromOldAccount() -> Bool { + return (UserDefaults.standard.object(forKey: kDidReceiveCallsFromOldAccount) as? NSNumber)?.boolValue ?? false + } + + public func setDidReceiveCallsFromOldAccount(_ receivedOldCalls: Bool) { + UserDefaults.standard.set(NSNumber(value: receivedOldCalls), forKey: kDidReceiveCallsFromOldAccount) + } + + // MARK: - User Profile + + public func getUserProfile(forAccountId accountId: String, withCompletionBlock block: @escaping (_ error: OcsError?) -> Void) { + guard let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + block(OcsError.genericError()) + return + } + + NCAPIController.sharedInstance().getUserProfile(forAccount: account) { userProfile, error in + guard error == nil, let userProfile else { + NSLog("Error while getting the user profile") + block(error) + return + } + + let email = (userProfile[kUserProfileEmail] as? String) ?? "" + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetUserProfile") + let realm = RLMRealm.default() + try? realm.transaction { + guard let managedActiveAccount = TalkAccount.objects(where: "accountId = %@", account.accountId).firstObject() as? TalkAccount else { + block(OcsError.genericError()) + return + } + + managedActiveAccount.userId = (userProfile[kUserProfileUserId] as? String) ?? "" + // "display-name" is returned by /cloud/user endpoint + // change to kUserProfileDisplayName ("displayName") when using /cloud/users/{userId} endpoint + managedActiveAccount.userDisplayName = (userProfile["display-name"] as? String) ?? "" + managedActiveAccount.userDisplayNameScope = (userProfile[kUserProfileDisplayNameScope] as? String) ?? "" + managedActiveAccount.phone = (userProfile[kUserProfilePhone] as? String) ?? "" + managedActiveAccount.phoneScope = (userProfile[kUserProfilePhoneScope] as? String) ?? "" + managedActiveAccount.email = email + managedActiveAccount.emailScope = (userProfile[kUserProfileEmailScope] as? String) ?? "" + managedActiveAccount.address = (userProfile[kUserProfileAddress] as? String) ?? "" + managedActiveAccount.addressScope = (userProfile[kUserProfileAddressScope] as? String) ?? "" + managedActiveAccount.website = (userProfile[kUserProfileWebsite] as? String) ?? "" + managedActiveAccount.websiteScope = (userProfile[kUserProfileWebsiteScope] as? String) ?? "" + managedActiveAccount.twitter = (userProfile[kUserProfileTwitter] as? String) ?? "" + managedActiveAccount.twitterScope = (userProfile[kUserProfileTwitterScope] as? String) ?? "" + managedActiveAccount.avatarScope = (userProfile[kUserProfileAvatarScope] as? String) ?? "" + + let unmanagedUpdatedAccount = TalkAccount(value: managedActiveAccount) + NCAPIController.sharedInstance().saveProfileImage(forAccount: unmanagedUpdatedAccount) + + block(nil) + } + bgTask.stopBackgroundTask() + } + } + + public func getUserGroupsAndTeams(forAccountId accountId: String) { + guard let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + return + } + + NCAPIController.sharedInstance().getUserGroups(forAccount: account) { groupIds, error in + guard error == nil else { + NSLog("Error while getting user's groups") + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetUserGroups") + let realm = RLMRealm.default() + try? realm.transaction { + guard let managedActiveAccount = TalkAccount.objects(where: "accountId = %@", account.accountId).firstObject() as? TalkAccount else { + return + } + + managedActiveAccount.groupIds.removeAllObjects() + managedActiveAccount.groupIds.addObjects((groupIds ?? []) as NSArray) + } + bgTask.stopBackgroundTask() + } + + NCAPIController.sharedInstance().getUserTeams(forAccount: account) { teamIds, error in + guard error == nil else { + NSLog("Error while getting user's teams") + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetUserTeams") + let realm = RLMRealm.default() + try? realm.transaction { + guard let managedActiveAccount = TalkAccount.objects(where: "accountId = %@", account.accountId).firstObject() as? TalkAccount else { + return + } + + managedActiveAccount.teamIds.removeAllObjects() + managedActiveAccount.teamIds.addObjects((teamIds ?? []) as NSArray) + } + bgTask.stopBackgroundTask() + } + } + + public func logoutAccount(withAccountId accountId: String, withCompletionBlock block: ((_ error: NSError?) -> Void)?) { + guard let removingAccount = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + block?(NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil)) + return + } + + if removingAccount.deviceIdentifier != nil { + NCAPIController.sharedInstance().unsubscribeAccount(removingAccount, fromNextcloudServerWithCompletionBlock: { error in + if error == nil { + NSLog("Unsubscribed from NC server!!!") + } else { + NSLog("Error while unsubscribing from NC server.") + } + }) + NCAPIController.sharedInstance().unsubscribeAccount(removingAccount, fromPushServerWithCompletionBlock: { error in + if error == nil { + NSLog("Unsubscribed from Push Notification server!!!") + } else { + NSLog("Error while unsubscribing from Push Notification server.") + } + }) + } + + let extSignalingController = self.externalSignalingController(forAccountId: removingAccount.accountId) + extSignalingController?.disconnect() + NCAPIController.sharedInstance().removeProfileImage(forAccount: removingAccount) + NCAPIController.sharedInstance().removeAPISessionManager(forAccount: removingAccount) + NCDatabaseManager.sharedInstance().removeAccount(withAccountId: removingAccount.accountId) + NCChatFileController().deleteDownloadDirectory(for: removingAccount) + NCRoomsManager.shared.chatViewController?.leaveChat() + self.createAccountsFile() + + // Activate any of the inactive accounts + self.switchToAnyInactiveAccount() + + block?(nil) + } + + private func switchToAnyInactiveAccount() { + if let inactiveAccount = NCDatabaseManager.sharedInstance().inactiveAccounts().first { + self.setActiveAccountWithAccountId(inactiveAccount.accountId) + } + } + + // MARK: - Signaling Configuration + + public func updateSignalingConfiguration(forAccountId accountId: String, withCompletionBlock block: @escaping (_ signalingServer: NCExternalSignalingController?, _ error: NSError?) -> Void) { + guard let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + block(nil, NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil)) + return + } + + NCAPIController.sharedInstance().getSignalingSettings(for: account, forRoom: nil) { settings, error in + guard error == nil else { + NSLog("Error while getting signaling configuration") + block(nil, error as NSError?) + return + } + + guard let settings, !account.accountId.isEmpty else { + block(nil, NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil)) + return + } + + let extSignalingController = self.setSignalingConfiguration(forAccountId: account.accountId, withSettings: settings) + block(extSignalingController, nil) + } + } + + @discardableResult + public func setSignalingConfiguration(forAccountId accountId: String, withSettings signalingSettings: SignalingSettings) -> NCExternalSignalingController? { + self.signalingConfigurations[accountId] = signalingSettings + + guard let server = signalingSettings.server, !server.isEmpty, + let ticket = signalingSettings.ticket, !ticket.isEmpty + else { return nil } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetSignalingConfiguration") + + if let extSignalingController = self.externalSignalingControllers[accountId] as? NCExternalSignalingController { + extSignalingController.disconnect() + } + + let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) + let extSignalingController = account.map { NCExternalSignalingController(account: $0, serverUrl: server, ticket: ticket) } + self.externalSignalingControllers[accountId] = extSignalingController + + bgTask.stopBackgroundTask() + + return extSignalingController + } + + public func ensureSignalingConfiguration(forAccountId accountId: String, with settings: SignalingSettings?, withCompletionBlock block: @escaping (_ signalingServer: NCExternalSignalingController?) -> Void) { + if self.signalingConfigurations[accountId] != nil { + block(self.externalSignalingControllers[accountId] as? NCExternalSignalingController) + return + } + + NCLog.log("Ensure signaling configuration -> Setting configuration") + + if let settings { + // In case settings are provided, we use these provided settings + let extSignalingController = self.setSignalingConfiguration(forAccountId: accountId, withSettings: settings) + block(extSignalingController) + } else { + // There were no settings provided for that call, we have to update the settings + self.updateSignalingConfiguration(forAccountId: accountId) { signalingServer, _ in + block(signalingServer) + } + } + } + + public func externalSignalingController(forAccountId accountId: String) -> NCExternalSignalingController? { + return self.externalSignalingControllers[accountId] as? NCExternalSignalingController + } + + public func connectDisconnectedExternalSignalingControllers() { + for case let extSignalingController as NCExternalSignalingController in self.externalSignalingControllers.allValues { + if extSignalingController.disconnected { + extSignalingController.connect() + } + } + } + + public func disconnectAllExternalSignalingControllers() { + for case let extSignalingController as NCExternalSignalingController in self.externalSignalingControllers.allValues { + extSignalingController.disconnect() + } + } + + // MARK: - Server Capabilities + + public func getCapabilitiesForAccountId(_ accountId: String, withCompletionBlock block: ((_ error: OcsError?) -> Void)?) { + guard let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + block?(OcsError.genericError()) + return + } + + NCAPIController.sharedInstance().getServerCapabilities(forAccount: account) { serverCapabilities, error in + guard error == nil, let serverCapabilities else { + NSLog("Error while getting server capabilities") + block?(error) + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCUpdateCapabilitiesTransaction") + NCDatabaseManager.sharedInstance().setServerCapabilities(serverCapabilities, forAccountId: account.accountId) + self.checkServerCapabilities(forAccount: account) + bgTask.stopBackgroundTask() + + NotificationCenter.default.post(name: .NCServerCapabilitiesUpdated, object: self, userInfo: nil) + + block?(nil) + } + } + + private func checkServerCapabilities(forAccount account: TalkAccount) { + guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: account.accountId) else { + return + } + + let talkFeatures = serverCapabilities.talkCapabilities.value(forKey: "self") as? [String] + if talkFeatures == nil || talkFeatures?.isEmpty == true { + NotificationCenter.default.post(name: .NCTalkNotInstalled, object: self, userInfo: nil) + } else if talkFeatures?.contains(kMinimumRequiredTalkCapability) == false { + NotificationCenter.default.post(name: .NCOutdatedTalkVersion, object: self, userInfo: nil) + } + } + + public func canCreateGroupAndPublicRooms() -> Bool { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) { + return serverCapabilities.canCreate + } + return true + } + + public func isGuestsAppEnabled() -> Bool { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) { + return serverCapabilities.guestsAppEnabled + } + return false + } + + public func isReferenceApiSupported() -> Bool { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) { + return serverCapabilities.referenceApiSupported + } + return false + } + + public func isRecordingEnabled() -> Bool { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId), + NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityRecordingV1) { + return serverCapabilities.recordingEnabled + } + return false + } + + public func passwordPolicyGenerateAPIEndpoint() -> String? { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + return NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId)?.passwordPolicyGenerateAPIEndpoint + } + + public func passwordPolicyValidateAPIEndpoint() -> String? { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + return NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId)?.passwordPolicyValidateAPIEndpoint + } + + public func passwordPolicyMinLength() -> Int { + let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() + if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId) { + return serverCapabilities.passwordPolicyMinLength + } + return -1 + } + + // MARK: - Push Notifications + + public func subscribeForPushNotifications(forAccountId accountId: String, withCompletionBlock block: ((_ success: Bool) -> Void)?) { +#if !targetEnvironment(simulator) + var keyPair: NCPushNotificationKeyPair? + let pushNotificationPublicKey = NCKeyChainController.sharedInstance().pushNotificationPublicKey(forAccountId: accountId) + let pushNotificationPrivateKey = NCKeyChainController.sharedInstance().pushNotificationPrivateKey(forAccountId: accountId) + + if let pushNotificationPublicKey, let pushNotificationPrivateKey { + keyPair = NCPushNotificationKeyPair(privateKey: pushNotificationPrivateKey, publicKey: pushNotificationPublicKey) + } else { + keyPair = NCPushNotificationsUtils.generatePushNotificationKeyPair() + } + + guard let keyPair else { + NCLog.log("Error while subscribing: Unable to generate push notifications key pair.") + block?(false) + return + } + + guard NCKeyChainController.sharedInstance().combinedPushToken() != nil else { + NCLog.log("Error while subscribing: Push token is not available.") + block?(false) + return + } + + let bgTask = BGTaskHelper.startBackgroundTask(withName: "PushProxySubscription") + + NCAPIController.sharedInstance().subscribeAccount(NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId), withPublicKey: keyPair.publicKey, toNextcloudServerWithCompletionBlock: { responseDict, error in + guard error == nil else { + NCLog.log("Error while subscribing to NC server. Error: \(error?.description ?? "")") + block?(false) + bgTask.stopBackgroundTask() + return + } + + NCLog.log("Subscribed to NC server successfully.") + + guard let publicKey = responseDict?["publicKey"] as? String, + let deviceIdentifier = responseDict?["deviceIdentifier"] as? String, + let signature = responseDict?["signature"] as? String + else { + NCLog.log("Something went wrong subscribing to NC server. Aborting subscribe to Push Notification server.") + block?(false) + bgTask.stopBackgroundTask() + return + } + + let realm = RLMRealm.default() + realm.beginWriteTransaction() + let managedAccount = TalkAccount.objects(where: "accountId = %@", accountId).firstObject() as? TalkAccount + managedAccount?.userPublicKey = publicKey + managedAccount?.deviceIdentifier = deviceIdentifier + managedAccount?.deviceSignature = signature + try? realm.commitWriteTransaction() + + NCAPIController.sharedInstance().subscribeAccount(NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId), toPushServerWithCompletionBlock: { error in + guard error == nil else { + NCLog.log("Error while subscribing to Push Notification server. Error: \(error?.localizedDescription ?? "")") + NCLog.log("Push notification, public key: \(publicKey)") + NCLog.log("Push notification, device signature: \(signature)") + NCLog.log("Push notification, device identifier: \(deviceIdentifier)") + NCKeyChainController.sharedInstance().logCombinedPushToken() + block?(false) + bgTask.stopBackgroundTask() + return + } + + let realm = RLMRealm.default() + realm.beginWriteTransaction() + let managedAccount = TalkAccount.objects(where: "accountId = %@", accountId).firstObject() as? TalkAccount + managedAccount?.lastPushSubscription = Int(Date().timeIntervalSince1970) + try? realm.commitWriteTransaction() + NCKeyChainController.sharedInstance().setPushNotificationPublicKey(keyPair.publicKey, forAccountId: accountId) + NCKeyChainController.sharedInstance().setPushNotificationPrivateKey(keyPair.privateKey, forAccountId: accountId) + NCLog.log("Subscribed to Push Notification server successfully.") + block?(true) + bgTask.stopBackgroundTask() + }) + }) +#else + block?(true) +#endif + } +} + +// MARK: - Capability helpers @objc public extension NCSettingsController { diff --git a/NextcloudTalk/User Interface/NCUserInterfaceController.h b/NextcloudTalk/User Interface/NCUserInterfaceController.h index a1f112840..567f93b75 100644 --- a/NextcloudTalk/User Interface/NCUserInterfaceController.h +++ b/NextcloudTalk/User Interface/NCUserInterfaceController.h @@ -6,7 +6,6 @@ #import #import -#import "NCNotificationController.h" #import "NCPushNotification.h" #import "NCRoom.h" diff --git a/NextcloudTalk/User Interface/NCUserInterfaceController.m b/NextcloudTalk/User Interface/NCUserInterfaceController.m index 25a06e443..98ec93e0d 100644 --- a/NextcloudTalk/User Interface/NCUserInterfaceController.m +++ b/NextcloudTalk/User Interface/NCUserInterfaceController.m @@ -11,7 +11,6 @@ #import "AuthenticationViewController.h" #import "NCAppBranding.h" #import "NCDatabaseManager.h" -#import "NCSettingsController.h" #import "NotificationCenterNotifications.h" #import "NextcloudTalk-Swift.h" @@ -219,7 +218,7 @@ - (void)presentChatForLocalNotification:(NSDictionary *)userInfo _pendingLocalNotification = userInfo; return; } - [[NSNotificationCenter defaultCenter] postNotificationName:NCLocalNotificationJoinChatNotification + [[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.NCLocalNotificationJoinChat object:self userInfo:userInfo]; } From 0d15657417904d895a8395dd1b40233f138b4ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 17 Jun 2026 23:38:58 +0200 Subject: [PATCH 02/11] chore: fix enum typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Marcel Müller --- NextcloudTalk/NCTypes.h | 2 +- .../Notifications/NCNotificationController.swift | 16 ++++++++-------- NextcloudTalk/Notifications/NCPushNotification.m | 2 +- .../NotificationService.swift | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/NextcloudTalk/NCTypes.h b/NextcloudTalk/NCTypes.h index 3259f0efd..bdb34fa57 100644 --- a/NextcloudTalk/NCTypes.h +++ b/NextcloudTalk/NCTypes.h @@ -176,7 +176,7 @@ typedef NS_ENUM(NSInteger, NCPushNotificationType) { NCPushNotificationTypeDeleteMultiple, NCPushNotificationTypeAdminNotification, NCPushNotificationTypeRecording, - NCPUshNotificationTypeFederation, + NCPushNotificationTypeFederation, NCPushNotificationTypeReminder }; diff --git a/NextcloudTalk/Notifications/NCNotificationController.swift b/NextcloudTalk/Notifications/NCNotificationController.swift index 9cb9f0de1..a108f5ab7 100644 --- a/NextcloudTalk/Notifications/NCNotificationController.swift +++ b/NextcloudTalk/Notifications/NCNotificationController.swift @@ -62,11 +62,11 @@ public class NCNotificationController: NSObject, UNUserNotificationCenterDelegat } switch pushNotification.type { - case .NCPushNotificationTypeDelete: + case .delete: self.removeNotification(withNotificationIds: [NSNumber(value: pushNotification.notificationId)], forAccountId: pushNotification.accountId, withCompletionBlock: nil) - case .NCPushNotificationTypeDeleteAll: + case .deleteAll: self.removeAllNotifications(forAccountId: pushNotification.accountId) - case .NCPushNotificationTypeDeleteMultiple: + case .deleteMultiple: self.removeNotification(withNotificationIds: pushNotification.notificationIds as? [NSNumber], forAccountId: pushNotification.accountId, withCompletionBlock: nil) default: NSLog("Push Notification of an unknown type received") @@ -395,11 +395,11 @@ public class NCNotificationController: NSObject, UNUserNotificationCenterDelegat if let textInputResponse = response as? UNTextInputNotificationResponse { pushNotification.responseUserText = textInputResponse.userText self.handlePushNotificationResponse(withUserText: pushNotification) - } else if pushNotification.type == .NCPushNotificationTypeRecording { + } else if pushNotification.type == .recording { self.handlePushNotificationResponseForRecording(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) - } else if pushNotification.type == .NCPUshNotificationTypeFederation { + } else if pushNotification.type == .federation { self.handlePushNotificationResponseForFederation(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) - } else if pushNotification.type == .NCPushNotificationTypeReminder { + } else if pushNotification.type == .reminder { self.handlePushNotificationResponseForReminder(serverNotification, withActionIdentifier: response.actionIdentifier, forAccount: account) } else { self.handlePushNotificationResponse(pushNotification) @@ -604,9 +604,9 @@ public class NCNotificationController: NSObject, UNUserNotificationCenterDelegat } switch pushNotification.type { - case .NCPushNotificationTypeCall: + case .call: NCUserInterfaceController.sharedInstance().presentAlert(for: pushNotification) - case .NCPushNotificationTypeRoom, .NCPushNotificationTypeChat: + case .room, .chat: NCUserInterfaceController.sharedInstance().presentChat(for: pushNotification) default: break diff --git a/NextcloudTalk/Notifications/NCPushNotification.m b/NextcloudTalk/Notifications/NCPushNotification.m index 83b683bbe..62b30c85b 100644 --- a/NextcloudTalk/Notifications/NCPushNotification.m +++ b/NextcloudTalk/Notifications/NCPushNotification.m @@ -61,7 +61,7 @@ + (instancetype)pushNotificationFromDecryptedString:(NSString *)decryptedString } else if ([type isEqualToString:kNCPNTypeRecording]) { pushNotification.type = NCPushNotificationTypeRecording; } else if ([type isEqualToString:kNCPNTypeFederation]) { - pushNotification.type = NCPUshNotificationTypeFederation; + pushNotification.type = NCPushNotificationTypeFederation; } else if ([type isEqualToString:kNCPNTypeReminder]) { pushNotification.type = NCPushNotificationTypeReminder; } diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index da92aa385..c1f3e8809 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -103,7 +103,7 @@ class NotificationService: UNNotificationServiceExtension { return } - if pushNotification.type == .NCPushNotificationTypeAdminNotification { + if pushNotification.type == .adminNotification { // Test notification send through "occ notification:test-push --talk " // No need to increase the badge or query the server about it @@ -131,7 +131,7 @@ class NotificationService: UNNotificationServiceExtension { self.bestAttemptContent?.sound = .default self.bestAttemptContent?.badge = unreadNotifications as NSNumber - if pushNotification.type == .NCPushNotificationTypeChat { + if pushNotification.type == .chat { // Set category for chat messages to allow interactive notifications self.bestAttemptContent?.categoryIdentifier = "CATEGORY_CHAT" } From 5ebbc4fedbb3516d6592e62c7d38e8cd9d902592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 17 Jun 2026 23:58:06 +0200 Subject: [PATCH 03/11] chore: Use enum for user profile fields/copes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Marcel Müller --- .../Settings/NCSettingsController.swift | 70 ++++++++++--------- .../SettingsTableViewController.swift | 2 +- ...erProfileTableViewController+Actions.swift | 2 +- ...eTableViewController+DelegateMethods.swift | 10 +-- ...UserProfileTableViewController+Utils.swift | 40 +++++------ .../UserProfileTableViewController.swift | 10 +-- 6 files changed, 69 insertions(+), 65 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 04e4e5117..3ebf77071 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -18,25 +18,29 @@ extension NSNotification.Name { // MARK: - User profile keys -let kUserProfileUserId = "id" -let kUserProfileDisplayName = "displayname" -let kUserProfileDisplayNameScope = "displaynameScope" -let kUserProfileEmail = "email" -let kUserProfileEmailScope = "emailScope" -let kUserProfilePhone = "phone" -let kUserProfilePhoneScope = "phoneScope" -let kUserProfileAddress = "address" -let kUserProfileAddressScope = "addressScope" -let kUserProfileWebsite = "website" -let kUserProfileWebsiteScope = "websiteScope" -let kUserProfileTwitter = "twitter" -let kUserProfileTwitterScope = "twitterScope" -let kUserProfileAvatarScope = "avatarScope" - -let kUserProfileScopePrivate = "v2-private" -let kUserProfileScopeLocal = "v2-local" -let kUserProfileScopeFederated = "v2-federated" -let kUserProfileScopePublished = "v2-published" +enum UserProfileField { + static let userId = "id" + static let displayName = "displayname" + static let displayNameScope = "displaynameScope" + static let email = "email" + static let emailScope = "emailScope" + static let phone = "phone" + static let phoneScope = "phoneScope" + static let address = "address" + static let addressScope = "addressScope" + static let website = "website" + static let websiteScope = "websiteScope" + static let twitter = "twitter" + static let twitterScope = "twitterScope" + static let avatarScope = "avatarScope" +} + +enum UserProfileScope { + static let `private` = "v2-private" + static let local = "v2-local" + static let federated = "v2-federated" + static let published = "v2-published" +} private let kPreferredFileSorting = "preferredFileSorting" private let kContactSyncEnabled = "contactSyncEnabled" @@ -320,7 +324,7 @@ public class NCSettingsController: NSObject { return } - let email = (userProfile[kUserProfileEmail] as? String) ?? "" + let email = (userProfile[UserProfileField.email] as? String) ?? "" let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetUserProfile") let realm = RLMRealm.default() @@ -330,22 +334,22 @@ public class NCSettingsController: NSObject { return } - managedActiveAccount.userId = (userProfile[kUserProfileUserId] as? String) ?? "" + managedActiveAccount.userId = (userProfile[UserProfileField.userId] as? String) ?? "" // "display-name" is returned by /cloud/user endpoint - // change to kUserProfileDisplayName ("displayName") when using /cloud/users/{userId} endpoint + // change to UserProfileField.displayName ("displayName") when using /cloud/users/{userId} endpoint managedActiveAccount.userDisplayName = (userProfile["display-name"] as? String) ?? "" - managedActiveAccount.userDisplayNameScope = (userProfile[kUserProfileDisplayNameScope] as? String) ?? "" - managedActiveAccount.phone = (userProfile[kUserProfilePhone] as? String) ?? "" - managedActiveAccount.phoneScope = (userProfile[kUserProfilePhoneScope] as? String) ?? "" + managedActiveAccount.userDisplayNameScope = (userProfile[UserProfileField.displayNameScope] as? String) ?? "" + managedActiveAccount.phone = (userProfile[UserProfileField.phone] as? String) ?? "" + managedActiveAccount.phoneScope = (userProfile[UserProfileField.phoneScope] as? String) ?? "" managedActiveAccount.email = email - managedActiveAccount.emailScope = (userProfile[kUserProfileEmailScope] as? String) ?? "" - managedActiveAccount.address = (userProfile[kUserProfileAddress] as? String) ?? "" - managedActiveAccount.addressScope = (userProfile[kUserProfileAddressScope] as? String) ?? "" - managedActiveAccount.website = (userProfile[kUserProfileWebsite] as? String) ?? "" - managedActiveAccount.websiteScope = (userProfile[kUserProfileWebsiteScope] as? String) ?? "" - managedActiveAccount.twitter = (userProfile[kUserProfileTwitter] as? String) ?? "" - managedActiveAccount.twitterScope = (userProfile[kUserProfileTwitterScope] as? String) ?? "" - managedActiveAccount.avatarScope = (userProfile[kUserProfileAvatarScope] as? String) ?? "" + managedActiveAccount.emailScope = (userProfile[UserProfileField.emailScope] as? String) ?? "" + managedActiveAccount.address = (userProfile[UserProfileField.address] as? String) ?? "" + managedActiveAccount.addressScope = (userProfile[UserProfileField.addressScope] as? String) ?? "" + managedActiveAccount.website = (userProfile[UserProfileField.website] as? String) ?? "" + managedActiveAccount.websiteScope = (userProfile[UserProfileField.websiteScope] as? String) ?? "" + managedActiveAccount.twitter = (userProfile[UserProfileField.twitter] as? String) ?? "" + managedActiveAccount.twitterScope = (userProfile[UserProfileField.twitterScope] as? String) ?? "" + managedActiveAccount.avatarScope = (userProfile[UserProfileField.avatarScope] as? String) ?? "" let unmanagedUpdatedAccount = TalkAccount(value: managedActiveAccount) NCAPIController.sharedInstance().saveProfileImage(forAccount: unmanagedUpdatedAccount) diff --git a/NextcloudTalk/Settings/SettingsTableViewController.swift b/NextcloudTalk/Settings/SettingsTableViewController.swift index 41e92d921..913f5b8af 100644 --- a/NextcloudTalk/Settings/SettingsTableViewController.swift +++ b/NextcloudTalk/Settings/SettingsTableViewController.swift @@ -336,7 +336,7 @@ class SettingsTableViewController: UITableViewController, UITextFieldDelegate, U setPhoneAction = UIAlertAction(title: NSLocalizedString("Set", comment: ""), style: .default, handler: { _ in guard let phoneNumber = setPhoneNumberDialog.textFields?[0].text else { return } - NCAPIController.sharedInstance().setUserProfileField(kUserProfilePhone, withValue: phoneNumber, forAccount: self.activeAccount) { error in + NCAPIController.sharedInstance().setUserProfileField(UserProfileField.phone, withValue: phoneNumber, forAccount: self.activeAccount) { error in if error != nil { self.presentPhoneNumberErrorDialog(phoneNumber: phoneNumber) print("Error setting phone number ", error ?? "") diff --git a/NextcloudTalk/Settings/UserProfileTableViewController+Actions.swift b/NextcloudTalk/Settings/UserProfileTableViewController+Actions.swift index a25519ff1..30332f986 100644 --- a/NextcloudTalk/Settings/UserProfileTableViewController+Actions.swift +++ b/NextcloudTalk/Settings/UserProfileTableViewController+Actions.swift @@ -95,7 +95,7 @@ extension UserProfileTableViewController { func setPhoneNumber(_ phoneNumber: String) { self.setModifyingProfileUI() - NCAPIController.sharedInstance().setUserProfileField(kUserProfilePhone, withValue: phoneNumber, forAccount: account) { error in + NCAPIController.sharedInstance().setUserProfileField(UserProfileField.phone, withValue: phoneNumber, forAccount: account) { error in if error != nil { self.showProfileModificationErrorForField(inTextField: self.kPhoneTextFieldTag, textField: nil) } else { diff --git a/NextcloudTalk/Settings/UserProfileTableViewController+DelegateMethods.swift b/NextcloudTalk/Settings/UserProfileTableViewController+DelegateMethods.swift index 5e36b0a5f..3d2639fdf 100644 --- a/NextcloudTalk/Settings/UserProfileTableViewController+DelegateMethods.swift +++ b/NextcloudTalk/Settings/UserProfileTableViewController+DelegateMethods.swift @@ -72,21 +72,21 @@ extension UserProfileTableViewController: UINavigationControllerDelegate, UIText self.waitingForModification = false activeTextField = nil if tag == kNameTextFieldTag { - field = kUserProfileDisplayName + field = UserProfileField.displayName currentValue = account.userDisplayName } else if tag == kEmailTextFieldTag { - field = kUserProfileEmail + field = UserProfileField.email currentValue = account.email } else if tag == kPhoneTextFieldTag { return } else if tag == kAddressTextFieldTag { - field = kUserProfileAddress + field = UserProfileField.address currentValue = account.address } else if tag == kWebsiteTextFieldTag { - field = kUserProfileWebsite + field = UserProfileField.website currentValue = account.website } else if tag == kTwitterTextFieldTag { - field = kUserProfileTwitter + field = UserProfileField.twitter currentValue = account.twitter } textField.text = newValue diff --git a/NextcloudTalk/Settings/UserProfileTableViewController+Utils.swift b/NextcloudTalk/Settings/UserProfileTableViewController+Utils.swift index 613a517b8..345c855fe 100644 --- a/NextcloudTalk/Settings/UserProfileTableViewController+Utils.swift +++ b/NextcloudTalk/Settings/UserProfileTableViewController+Utils.swift @@ -141,13 +141,13 @@ extension UserProfileTableViewController { } func imageForScope(scope: String) -> UIImage? { - if scope == kUserProfileScopePrivate { + if scope == UserProfileScope.private { return UIImage(systemName: "iphone") - } else if scope == kUserProfileScopeLocal { + } else if scope == UserProfileScope.local { return UIImage(systemName: "lock") - } else if scope == kUserProfileScopeFederated { + } else if scope == UserProfileScope.federated { return UIImage(systemName: "person.2") - } else if scope == kUserProfileScopePublished { + } else if scope == UserProfileScope.published { return UIImage(systemName: "network") } return nil @@ -163,31 +163,31 @@ extension UserProfileTableViewController { let title: String if sender.tag == kNameTextFieldTag { - field = kUserProfileDisplayNameScope + field = UserProfileField.displayNameScope currentValue = account.userDisplayNameScope title = NSLocalizedString("Full name", comment: "") } else if sender.tag == kEmailTextFieldTag { - field = kUserProfileEmailScope + field = UserProfileField.emailScope currentValue = account.emailScope title = NSLocalizedString("Email", comment: "") } else if sender.tag == kPhoneTextFieldTag { - field = kUserProfilePhoneScope + field = UserProfileField.phoneScope currentValue = account.phoneScope title = NSLocalizedString("Phone number", comment: "") } else if sender.tag == kAddressTextFieldTag { - field = kUserProfileAddressScope + field = UserProfileField.addressScope currentValue = account.addressScope title = NSLocalizedString("Address", comment: "") } else if sender.tag == kWebsiteTextFieldTag { - field = kUserProfileWebsiteScope + field = UserProfileField.websiteScope currentValue = account.websiteScope title = NSLocalizedString("Website", comment: "") } else if sender.tag == kTwitterTextFieldTag { - field = kUserProfileTwitterScope + field = UserProfileField.twitterScope currentValue = account.twitterScope title = NSLocalizedString("Twitter", comment: "") } else if sender.tag == kAvatarScopeButtonTag { - field = kUserProfileAvatarScope + field = UserProfileField.avatarScope currentValue = account.avatarScope title = NSLocalizedString("Profile picture", comment: "") } else { @@ -200,28 +200,28 @@ extension UserProfileTableViewController { func presentScopeSelector(field: String, currentValue: String, title: String) { var options = [DetailedOption]() - let privateOption = setupDetailedOption(identifier: kUserProfileScopePrivate, + let privateOption = setupDetailedOption(identifier: UserProfileScope.private, image: UIImage(systemName: "iphone")?.applyingSymbolConfiguration(iconConfiguration), title: NSLocalizedString("Private", comment: ""), subtitle: NSLocalizedString("Only visible to people matched via phone number integration", comment: ""), - selected: currentValue == kUserProfileScopePrivate) - let localOption = setupDetailedOption(identifier: kUserProfileScopeLocal, + selected: currentValue == UserProfileScope.private) + let localOption = setupDetailedOption(identifier: UserProfileScope.local, image: UIImage(systemName: "lock")?.applyingSymbolConfiguration(iconConfiguration), title: NSLocalizedString("Local", comment: ""), subtitle: NSLocalizedString("Only visible to people on this instance and guests", comment: ""), - selected: currentValue == kUserProfileScopeLocal) - let federatedOption = setupDetailedOption(identifier: kUserProfileScopeFederated, + selected: currentValue == UserProfileScope.local) + let federatedOption = setupDetailedOption(identifier: UserProfileScope.federated, image: UIImage(systemName: "person.2")?.applyingSymbolConfiguration(iconConfiguration), title: NSLocalizedString("Federated", comment: ""), subtitle: NSLocalizedString("Only synchronize to trusted servers", comment: ""), - selected: currentValue == kUserProfileScopeFederated) - let publishedOption = setupDetailedOption(identifier: kUserProfileScopePublished, + selected: currentValue == UserProfileScope.federated) + let publishedOption = setupDetailedOption(identifier: UserProfileScope.published, image: UIImage(systemName: "network")?.applyingSymbolConfiguration(iconConfiguration), title: NSLocalizedString("Published", comment: ""), subtitle: NSLocalizedString("Synchronize to trusted servers and the global and public address book", comment: ""), - selected: currentValue == kUserProfileScopePublished) + selected: currentValue == UserProfileScope.published) - if field != kUserProfileDisplayNameScope && field != kUserProfileEmailScope { + if field != UserProfileField.displayNameScope && field != UserProfileField.emailScope { options.append(privateOption) } diff --git a/NextcloudTalk/Settings/UserProfileTableViewController.swift b/NextcloudTalk/Settings/UserProfileTableViewController.swift index e3f145a15..cc009edb4 100644 --- a/NextcloudTalk/Settings/UserProfileTableViewController.swift +++ b/NextcloudTalk/Settings/UserProfileTableViewController.swift @@ -162,11 +162,11 @@ class UserProfileTableViewController: UITableViewController, DetailedOptionsSele case ProfileSection.kProfileSectionName.rawValue: return textInputCellWith(text: account.userDisplayName, tag: kNameTextFieldTag, - interactionEnabled: editableFields.contains(kUserProfileDisplayName)) + interactionEnabled: editableFields.contains(UserProfileField.displayName)) case ProfileSection.kProfileSectionEmail.rawValue: return textInputCellWith(text: account.email, tag: kEmailTextFieldTag, - interactionEnabled: editableFields.contains(kUserProfileEmail), + interactionEnabled: editableFields.contains(UserProfileField.email), keyBoardType: .emailAddress, autocapitalizationType: .none, placeHolder: NSLocalizedString("Your email address", comment: "")) @@ -182,19 +182,19 @@ class UserProfileTableViewController: UITableViewController, DetailedOptionsSele case ProfileSection.kProfileSectionAddress.rawValue: return textInputCellWith(text: account.address, tag: kAddressTextFieldTag, - interactionEnabled: editableFields.contains(kUserProfileAddress), + interactionEnabled: editableFields.contains(UserProfileField.address), placeHolder: NSLocalizedString("Your postal address", comment: "")) case ProfileSection.kProfileSectionWebsite.rawValue: return textInputCellWith(text: account.website, tag: kWebsiteTextFieldTag, - interactionEnabled: editableFields.contains(kUserProfileWebsite), + interactionEnabled: editableFields.contains(UserProfileField.website), keyBoardType: .URL, autocapitalizationType: .none, placeHolder: NSLocalizedString("Link https://…", comment: "")) case ProfileSection.kProfileSectionTwitter.rawValue: return textInputCellWith(text: account.twitter, tag: kTwitterTextFieldTag, - interactionEnabled: editableFields.contains(kUserProfileTwitter), + interactionEnabled: editableFields.contains(UserProfileField.twitter), keyBoardType: .emailAddress, autocapitalizationType: .none, placeHolder: NSLocalizedString("Twitter handle @…", comment: "")) From 3caacab8862faf02db3ca1f2f9f083cf4d840285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:10:39 +0200 Subject: [PATCH 04/11] chore: Use swift dictionaries instead NSMutableDictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Marcel Müller --- .../Network/NCConnectionController.swift | 2 +- .../RoomInfo/RoomInfoSIPInfoSection.swift | 2 +- .../DiagnosticsTableViewController.swift | 2 +- .../Settings/NCSettingsController.swift | 20 ++++++++++++------- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/NextcloudTalk/Network/NCConnectionController.swift b/NextcloudTalk/Network/NCConnectionController.swift index 03db34612..4dfdeeb68 100644 --- a/NextcloudTalk/Network/NCConnectionController.swift +++ b/NextcloudTalk/Network/NCConnectionController.swift @@ -86,7 +86,7 @@ class NCConnectionController: NSObject { } let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() - let signalingConfig = NCSettingsController.sharedInstance().signalingConfigurations.object(forKey: activeAccount.accountId) + let signalingConfig = NCSettingsController.sharedInstance().signalingConfiguration(forAccountId: activeAccount.accountId) if activeAccount.user.isEmpty || activeAccount.userDisplayName.isEmpty { self.appState = .missingUserProfile diff --git a/NextcloudTalk/Rooms/RoomInfo/RoomInfoSIPInfoSection.swift b/NextcloudTalk/Rooms/RoomInfo/RoomInfoSIPInfoSection.swift index 6bcb3a25d..3813780a1 100644 --- a/NextcloudTalk/Rooms/RoomInfo/RoomInfoSIPInfoSection.swift +++ b/NextcloudTalk/Rooms/RoomInfo/RoomInfoSIPInfoSection.swift @@ -16,7 +16,7 @@ struct RoomInfoSIPInfoSection: View { return Section(header: Text("SIP dial-in")) { // TODO: SwiftUI Text does not support data detectors? - let signalingConfig = NCSettingsController.sharedInstance().signalingConfigurations.object(forKey: room.account!.accountId) as? SignalingSettings + let signalingConfig = NCSettingsController.sharedInstance().signalingConfiguration(forAccountId: room.account!.accountId) Text(signalingConfig?.sipDialinInfo ?? "") HStack { diff --git a/NextcloudTalk/Settings/DiagnosticsTableViewController.swift b/NextcloudTalk/Settings/DiagnosticsTableViewController.swift index aab689e9d..f8723d7cf 100644 --- a/NextcloudTalk/Settings/DiagnosticsTableViewController.swift +++ b/NextcloudTalk/Settings/DiagnosticsTableViewController.swift @@ -109,7 +109,7 @@ class DiagnosticsTableViewController: UITableViewController { self.account = account self.serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: account.accountId)! - self.signalingConfiguration = NCSettingsController.sharedInstance().signalingConfigurations[account.accountId] as? SignalingSettings + self.signalingConfiguration = NCSettingsController.sharedInstance().signalingConfiguration(forAccountId: account.accountId) self.externalSignalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: account.accountId) self.signalingVersion = NCAPIVersion(forType: .signaling, withAccount: account).rawValue diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 3ebf77071..379e3fcf6 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -56,9 +56,11 @@ public class NCSettingsController: NSObject { return NCSettingsController.shared } + private typealias AccountId = String + public var videoSettingsModel = ARDSettingsModel() - public var signalingConfigurations = NSMutableDictionary() // accountId -> signalingConfiguration - public var externalSignalingControllers = NSMutableDictionary() // accountId -> externalSignalingController + private var signalingConfigurations: [AccountId: SignalingSettings] = [:] + private var externalSignalingControllers: [AccountId: NCExternalSignalingController] = [:] private var updateAlertController: UIAlertController? private var updateAlertControllerAccountId: String? @@ -483,7 +485,7 @@ public class NCSettingsController: NSObject { let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCSetSignalingConfiguration") - if let extSignalingController = self.externalSignalingControllers[accountId] as? NCExternalSignalingController { + if let extSignalingController = self.externalSignalingControllers[accountId] { extSignalingController.disconnect() } @@ -498,7 +500,7 @@ public class NCSettingsController: NSObject { public func ensureSignalingConfiguration(forAccountId accountId: String, with settings: SignalingSettings?, withCompletionBlock block: @escaping (_ signalingServer: NCExternalSignalingController?) -> Void) { if self.signalingConfigurations[accountId] != nil { - block(self.externalSignalingControllers[accountId] as? NCExternalSignalingController) + block(self.externalSignalingControllers[accountId]) return } @@ -516,12 +518,16 @@ public class NCSettingsController: NSObject { } } + public func signalingConfiguration(forAccountId accountId: String) -> SignalingSettings? { + return self.signalingConfigurations[accountId] + } + public func externalSignalingController(forAccountId accountId: String) -> NCExternalSignalingController? { - return self.externalSignalingControllers[accountId] as? NCExternalSignalingController + return self.externalSignalingControllers[accountId] } public func connectDisconnectedExternalSignalingControllers() { - for case let extSignalingController as NCExternalSignalingController in self.externalSignalingControllers.allValues { + for extSignalingController in self.externalSignalingControllers.values { if extSignalingController.disconnected { extSignalingController.connect() } @@ -529,7 +535,7 @@ public class NCSettingsController: NSObject { } public func disconnectAllExternalSignalingControllers() { - for case let extSignalingController as NCExternalSignalingController in self.externalSignalingControllers.allValues { + for extSignalingController in self.externalSignalingControllers.values { extSignalingController.disconnect() } } From 7757cdbea11ebe93499b0783dae562e7df74bb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:21:35 +0200 Subject: [PATCH 05/11] fix: Correctly guard signaling controller creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Settings/NCSettingsController.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 379e3fcf6..99ffefdd2 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -487,11 +487,15 @@ public class NCSettingsController: NSObject { if let extSignalingController = self.externalSignalingControllers[accountId] { extSignalingController.disconnect() + self.externalSignalingControllers.removeValue(forKey: accountId) } - let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) - let extSignalingController = account.map { NCExternalSignalingController(account: $0, serverUrl: server, ticket: ticket) } - self.externalSignalingControllers[accountId] = extSignalingController + var extSignalingController: NCExternalSignalingController? + + if let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) { + extSignalingController = NCExternalSignalingController(account: account, serverUrl: server, ticket: ticket) + self.externalSignalingControllers[accountId] = extSignalingController + } bgTask.stopBackgroundTask() From 28ca618fe01a1c882655bfccde887666666e8c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:23:34 +0200 Subject: [PATCH 06/11] fix: Style fix for ensuring signalingController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Settings/NCSettingsController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 99ffefdd2..768e57caf 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -503,8 +503,8 @@ public class NCSettingsController: NSObject { } public func ensureSignalingConfiguration(forAccountId accountId: String, with settings: SignalingSettings?, withCompletionBlock block: @escaping (_ signalingServer: NCExternalSignalingController?) -> Void) { - if self.signalingConfigurations[accountId] != nil { - block(self.externalSignalingControllers[accountId]) + if let signalingController = self.externalSignalingControllers[accountId] { + block(signalingController) return } From 20a048638be1b7e3baea5a097b71e71eefd91c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:24:37 +0200 Subject: [PATCH 07/11] chore: Prefer where in for-loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Settings/NCSettingsController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 768e57caf..0192513a4 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -531,10 +531,8 @@ public class NCSettingsController: NSObject { } public func connectDisconnectedExternalSignalingControllers() { - for extSignalingController in self.externalSignalingControllers.values { - if extSignalingController.disconnected { - extSignalingController.connect() - } + for extSignalingController in self.externalSignalingControllers.values where extSignalingController.disconnected { + extSignalingController.connect() } } From 03e7459e6a9851b9d0cf4a1e0072b6f4d5b6a53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:31:14 +0200 Subject: [PATCH 08/11] chore: Move extension methods inside main class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- .../Settings/NCSettingsController.swift | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 0192513a4..6b4d7935e 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -614,6 +614,18 @@ public class NCSettingsController: NSObject { return false } + func isEndToEndEncryptedCallingEnabled(forAccount accountId: String) -> Bool { + return NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: accountId)?.e2eeCallsEnabled ?? false + } + + func isRoomsSortingSupported(forAccountId accountId: String) -> Bool { + guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: accountId) + else { return false } + + return NCRoomSortOrder(rawValue: serverCapabilities.roomsSortOrder) != .unsupported && + NCRoomGroupMode(rawValue: serverCapabilities.roomsGroupMode) != .unsupported + } + public func passwordPolicyGenerateAPIEndpoint() -> String? { let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() return NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: activeAccount.accountId)?.passwordPolicyGenerateAPIEndpoint @@ -717,21 +729,3 @@ public class NCSettingsController: NSObject { #endif } } - -// MARK: - Capability helpers - -@objc public extension NCSettingsController { - - func isEndToEndEncryptedCallingEnabled(forAccount accountId: String) -> Bool { - return NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: accountId)?.e2eeCallsEnabled ?? false - } - - func isRoomsSortingSupported(forAccountId accountId: String) -> Bool { - guard let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: accountId) - else { return false } - - return NCRoomSortOrder(rawValue: serverCapabilities.roomsSortOrder) != .unsupported && - NCRoomGroupMode(rawValue: serverCapabilities.roomsGroupMode) != .unsupported - } - -} From 7bff8b2f7400c1b66e8c25ab19867151b2048265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 00:49:55 +0200 Subject: [PATCH 09/11] chore: Translate "Incoming call" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Notifications/NCNotificationController.swift | 2 +- NextcloudTalk/en.lproj/Localizable.strings | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NextcloudTalk/Notifications/NCNotificationController.swift b/NextcloudTalk/Notifications/NCNotificationController.swift index a108f5ab7..9b66a5b5c 100644 --- a/NextcloudTalk/Notifications/NCNotificationController.swift +++ b/NextcloudTalk/Notifications/NCNotificationController.swift @@ -143,7 +143,7 @@ public class NCNotificationController: NSObject, UNUserNotificationCenterDelegat public func showIncomingCall(forPushNotification pushNotification: NCPushNotification) { if CallKitManager.isCallKitAvailable() { - CallKitManager.sharedInstance().reportIncomingCall(pushNotification.roomToken, withDisplayName: "Incoming call", forAccountId: pushNotification.accountId) + CallKitManager.sharedInstance().reportIncomingCall(pushNotification.roomToken, withDisplayName: NSLocalizedString("Incoming call", comment: ""), forAccountId: pushNotification.accountId) } else { CallKitManager.sharedInstance().reportIncomingCallForNonCallKitDevices(withPushNotification: pushNotification) } diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index d79043980..a8061c9a6 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -1264,6 +1264,9 @@ /* No comment provided by engineer. */ "Include calls in call history" = "Include calls in call history"; +/* No comment provided by engineer. */ +"Incoming call" = "Incoming call"; + /* Message is pinned indefinitely */ "Indefinitely" = "Indefinitely"; From 5c8ff00e97338ce45cc1c0d529a45fced0806392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 09:51:20 +0200 Subject: [PATCH 10/11] chore: Correctly guard appStoreUrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Settings/NCSettingsController.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 6b4d7935e..624ffc5bf 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -192,8 +192,13 @@ public class NCSettingsController: NSObject { } private func createUpdateAlertController(forAccountId accountId: String) { - let appStoreURLString = "itms-apps://itunes.apple.com/app/id" - let canOpenAppStore = URL(string: appStoreURLString).map { UIApplication.shared.canOpenURL($0) } ?? false + let appStoreUrlString = "itms-apps://itunes.apple.com/app/id" + + guard let appStoreUrl = URL(string: appStoreUrlString) else { + return + } + + let canOpenAppStore = UIApplication.shared.canOpenURL(appStoreUrl) let messageNotification = NSLocalizedString("The app is too old and no longer supported by this server.", comment: "") let messageAction = canOpenAppStore ? NSLocalizedString("Please update.", comment: "") : NSLocalizedString("Please contact your system administrator.", comment: "") @@ -207,7 +212,7 @@ public class NCSettingsController: NSObject { if canOpenAppStore { let updateButton = UIAlertAction(title: NSLocalizedString("Update", comment: ""), style: .default) { _ in NCAPIController.sharedInstance().getAppStoreAppId { appId, _ in - if let appId, !appId.isEmpty, let appStoreURL = URL(string: "\(appStoreURLString)\(appId)") { + if let appId, !appId.isEmpty, let appStoreURL = URL(string: "\(appStoreUrlString)\(appId)") { UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) } From 408436126a5afa87461cd53bb05ac760d6af6897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 16:08:36 +0200 Subject: [PATCH 11/11] fix: Correctly guard account in push registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk/Settings/NCKeyChainController.h | 2 +- NextcloudTalk/Settings/NCSettingsController.swift | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/NextcloudTalk/Settings/NCKeyChainController.h b/NextcloudTalk/Settings/NCKeyChainController.h index 3a29c25d6..4720ed7dd 100644 --- a/NextcloudTalk/Settings/NCKeyChainController.h +++ b/NextcloudTalk/Settings/NCKeyChainController.h @@ -25,7 +25,7 @@ extern NSString * const kNCPushKitTokenKey; - (NSData * _Nullable)pushNotificationPrivateKeyForAccountId:(NSString *)accountId; - (NSString *)pushTokenSHA512; - (void)logCombinedPushToken; -- (NSString *)combinedPushToken; +- (NSString * _Nullable)combinedPushToken; - (void)removeAllItems; @end diff --git a/NextcloudTalk/Settings/NCSettingsController.swift b/NextcloudTalk/Settings/NCSettingsController.swift index 624ffc5bf..57a165a1e 100644 --- a/NextcloudTalk/Settings/NCSettingsController.swift +++ b/NextcloudTalk/Settings/NCSettingsController.swift @@ -675,9 +675,15 @@ public class NCSettingsController: NSObject { return } + guard let account = NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId) else { + NCLog.log("Error while subscribing: Account not available") + block?(false) + return + } + let bgTask = BGTaskHelper.startBackgroundTask(withName: "PushProxySubscription") - NCAPIController.sharedInstance().subscribeAccount(NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId), withPublicKey: keyPair.publicKey, toNextcloudServerWithCompletionBlock: { responseDict, error in + NCAPIController.sharedInstance().subscribeAccount(account, withPublicKey: keyPair.publicKey, toNextcloudServerWithCompletionBlock: { responseDict, error in guard error == nil else { NCLog.log("Error while subscribing to NC server. Error: \(error?.description ?? "")") block?(false) @@ -705,7 +711,7 @@ public class NCSettingsController: NSObject { managedAccount?.deviceSignature = signature try? realm.commitWriteTransaction() - NCAPIController.sharedInstance().subscribeAccount(NCDatabaseManager.sharedInstance().talkAccount(forAccountId: accountId), toPushServerWithCompletionBlock: { error in + NCAPIController.sharedInstance().subscribeAccount(account, toPushServerWithCompletionBlock: { error in guard error == nil else { NCLog.log("Error while subscribing to Push Notification server. Error: \(error?.localizedDescription ?? "")") NCLog.log("Push notification, public key: \(publicKey)")