From f6c8ed171c0954e1f36f100ca3df678d6241a438 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 08:18:49 +0100 Subject: [PATCH 01/22] added notificationCenterNetworkProcess Signed-off-by: Marino Faggiana --- iOSClient/NCGlobal.swift | 1 + iOSClient/Networking/NCNetworkingProcess.swift | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index b6c5f21d76..a935dc2314 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -278,6 +278,7 @@ final class NCGlobal: Sendable { let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterNetworkProcess = "networkProcess" let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 992f25c24b..4ea48b99f4 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -57,6 +57,14 @@ actor NCNetworkingProcess { } } + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterNetworkProcess), object: nil, queue: nil) { [weak self] _ in + guard let self else { return } + + Task { @MainActor in + await self.handleTimerTick() + } + } + NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in guard let self else { return } From b534fd50b324d9d544e4f7480d504ebe44f1c920 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 08:44:08 +0100 Subject: [PATCH 02/22] clean Signed-off-by: Marino Faggiana --- iOSClient/Main/NCMainNavigationController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/iOSClient/Main/NCMainNavigationController.swift b/iOSClient/Main/NCMainNavigationController.swift index 3b7a5d7a1d..b23b133157 100644 --- a/iOSClient/Main/NCMainNavigationController.swift +++ b/iOSClient/Main/NCMainNavigationController.swift @@ -911,11 +911,9 @@ class NCMainNavigationController: UINavigationController, UINavigationController @MainActor private func applyTint(_ button: UIButton, color: UIColor) { if var cfg = button.configuration { - // Se in futuro userai UIButton.Configuration, tieni il colore allineato qui cfg.baseForegroundColor = color button.configuration = cfg } else { - // Config attuale (nessuna configuration): SF Symbols sono template, quindi basta tintColor button.tintColor = color button.setTitleColor(color, for: .normal) } From 4a4c7e221e66052b724ed7f9714b57f2caab812a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 08:55:44 +0100 Subject: [PATCH 03/22] fix error Signed-off-by: Marino Faggiana --- iOSClient/Supporting Files/en.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 59cf1839df..53259cb481 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -646,7 +646,7 @@ "_create_folder_error_" = "An error has occurred while creating the folder:\n%@.\n\nPlease resolve the issue as soon as possible.\n\nAll uploads are suspended until the problem is resolved.\n"; "_creating_dir_progress_" = "Creating directories in progress … keep the application active."; "_creating_db_photo_progress_" = "Creating photo archive in progress … keep the application active."; -"_account_unauthorized_" = "There was an issue authorizing the account %@. Please log in again."; +"_account_unauthorized_" = "There was an issue authorizing the account. Please log in again."; "_folder_offline_desc_" = "Even without an internet connection, you can organize your folders, create files. Once you're back online, your pending actions will automatically sync."; "_offline_not_allowed_" = "This operation is not allowed in offline mode"; "_Upload_native_format_yes_"= "Upload in native format: yes"; From 63ba23881402cb56ede55504c8db04beb5497802 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 09:25:53 +0100 Subject: [PATCH 04/22] code Signed-off-by: Marino Faggiana --- iOSClient/Extensions/NotificationCenter+MainThread.swift | 5 +++++ iOSClient/GUI/Lucid Banner/BannerView.swift | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/iOSClient/Extensions/NotificationCenter+MainThread.swift b/iOSClient/Extensions/NotificationCenter+MainThread.swift index 3286a76f50..76c9982c4b 100644 --- a/iOSClient/Extensions/NotificationCenter+MainThread.swift +++ b/iOSClient/Extensions/NotificationCenter+MainThread.swift @@ -30,4 +30,9 @@ extension NotificationCenter { NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: anObject, userInfo: aUserInfo) } } + func postOnGlobal(name: String, object anObject: Any? = nil, userInfo aUserInfo: [AnyHashable: Any]? = nil, second: Double = 0) { + DispatchQueue.global().asyncAfter(deadline: .now() + second) { + NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: anObject, userInfo: aUserInfo) + } + } } diff --git a/iOSClient/GUI/Lucid Banner/BannerView.swift b/iOSClient/GUI/Lucid Banner/BannerView.swift index 90ad6db9c8..3661314136 100644 --- a/iOSClient/GUI/Lucid Banner/BannerView.swift +++ b/iOSClient/GUI/Lucid Banner/BannerView.swift @@ -309,7 +309,8 @@ func bannerContainsError(errorCode: Int?, afError: AFError? = nil) -> Bool { // The same error code is shown to the user only once. // Error 401 (maintenance mode) // Error 507 (insufficient storage) - if errorCode == 401 || errorCode == 507 { + // Error -1009 (not connected to internet) + if errorCode == 401 || errorCode == 507 || errorCode == URLError.notConnectedToInternet.rawValue { shownErrors.insert(errorCode) } return false From 599df0c771848deb9fad4376c7275fe5aea6f6bf Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 09:54:59 +0100 Subject: [PATCH 05/22] create folder Signed-off-by: Marino Faggiana --- iOSClient/Extensions/UIAlertController+Extension.swift | 2 ++ .../Main/Collection Common/NCCollectionViewCommon.swift | 3 ++- iOSClient/Networking/NCNetworkingProcess.swift | 8 +++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index e775ce1ee5..17bd1a5b3d 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -99,6 +99,8 @@ extension UIAlertController { metadata.sessionDate = Date() NCManageDatabase.shared.addMetadata(metadata) + + NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) #endif } }) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index e1a560361c..6b5b87b8e9 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -880,7 +880,8 @@ extension NCCollectionViewCommon: NCTransferDelegate { } if status == self.global.networkingStatusCreateFolder { - if serverUrl == self.serverUrl, + if error == .success, + serverUrl == self.serverUrl, selector != self.global.selectorUploadAutoUpload, let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { self.pushMetadata(metadata) diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 4ea48b99f4..f853b1dd15 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -33,6 +33,7 @@ actor NCNetworkingProcess { private var timer: DispatchSourceTimer? private let timerQueue = DispatchQueue(label: "com.nextcloud.timerProcess", qos: .utility) private var lastUsedInterval: TimeInterval = 3.5 + private let offlineInterval: TimeInterval = 10.0 private let maxInterval: TimeInterval = 3.5 private let minInterval: TimeInterval = 2.5 @@ -195,8 +196,7 @@ actor NCNetworkingProcess { return } - guard networking.isOnline, - !currentAccount.isEmpty, + guard !currentAccount.isEmpty, networking.noServerErrorAccount(currentAccount) else { return @@ -251,7 +251,9 @@ actor NCNetworkingProcess { // TODO: Check temperature - if lastUsedInterval != minInterval { + if !networking.isOnline { + await startTimer(interval: offlineInterval) + } else if lastUsedInterval != minInterval { await startTimer(interval: minInterval) } } else { From f7bd1ae74ffc6500f45ae93da710ab82cea544e0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 10:03:58 +0100 Subject: [PATCH 06/22] Delete Signed-off-by: Marino Faggiana --- iOSClient/Extensions/UIAlertController+Extension.swift | 1 + iOSClient/Networking/NCNetworking+WebDAV.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 17bd1a5b3d..d981404b8a 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -100,6 +100,7 @@ extension UIAlertController { NCManageDatabase.shared.addMetadata(metadata) + // START Network process NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) #endif } diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 9e761710d7..ad198000c6 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -464,6 +464,9 @@ extension NCNetworking { serverUrlss.forEach { serverUrl in delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitDelete) } + + // START Network process + NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) } } } From 2b2b3cef6ba3c8aee85846e97f7967e4726485eb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 10:41:12 +0100 Subject: [PATCH 07/22] code Signed-off-by: Marino Faggiana --- iOSClient/Networking/NCNetworking+WebDAV.swift | 3 +++ iOSClient/Networking/NCNetworkingProcess.swift | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index ad198000c6..27fc1d093c 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -548,6 +548,9 @@ extension NCNetworking { await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: ocId, status: self.global.metadataStatusWaitRename) delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitRename) } + + // START Network process + NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) } } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index f853b1dd15..745b1f1b9e 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -196,7 +196,8 @@ actor NCNetworkingProcess { return } - guard !currentAccount.isEmpty, + guard networking.isOnline, + !currentAccount.isEmpty, networking.noServerErrorAccount(currentAccount) else { return From f15b9c2693f543d90ca0bbde8f52e8983541736c Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 16 Feb 2026 10:40:58 +0100 Subject: [PATCH 08/22] Assistant Chat (#3978) * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Send message Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * Refactor Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * WIP Signed-off-by: Milen Pivchev * NCKit Signed-off-by: Milen Pivchev * Refactor Signed-off-by: Milen Pivchev * Add error handling Signed-off-by: Milen Pivchev * fix assistantButtonItem Signed-off-by: Marino Faggiana --------- Signed-off-by: Milen Pivchev Signed-off-by: Marino Faggiana Co-authored-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 46 +++- Tests/NextcloudUITests/AssistantUITests.swift | 2 +- iOSClient/Account/NCAccount.swift | 2 +- .../NCAssistantChatConversations.swift | 52 ++++ .../NCAssistantChatConversationsModel.swift | 38 +++ .../Assistant/Chat/NCAssistantChat.swift | 222 ++++++++++++++++++ .../Assistant/Chat/NCAssistantChatModel.swift | 184 +++++++++++++++ .../Assistant/Components/ChatInputField.swift | 63 +++++ .../Components/NCAssistantEmptyView.swift | 14 +- .../NCAssistantCreateNewTask.swift | 4 +- iOSClient/Assistant/NCAssistant.swift | 110 ++++++--- .../{Models => }/NCAssistantModel.swift | 152 ++++++------ .../Task Detail/NCAssistantTaskDetail.swift | 18 +- .../Cell/UIView+BlurVibrancy.swift | 69 +----- .../Main/NCMainNavigationController.swift | 3 +- .../More/Cells/NCMoreAppSuggestionsCell.swift | 12 - .../More/Cells/NCMoreAppSuggestionsCell.xib | 57 +---- iOSClient/More/NCMore.swift | 5 - .../en.lproj/Localizable.strings | 15 +- 19 files changed, 793 insertions(+), 275 deletions(-) create mode 100644 iOSClient/Assistant/Chat Sessions/NCAssistantChatConversations.swift create mode 100644 iOSClient/Assistant/Chat Sessions/NCAssistantChatConversationsModel.swift create mode 100644 iOSClient/Assistant/Chat/NCAssistantChat.swift create mode 100644 iOSClient/Assistant/Chat/NCAssistantChatModel.swift create mode 100644 iOSClient/Assistant/Components/ChatInputField.swift rename iOSClient/Assistant/{Models => }/NCAssistantModel.swift (62%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 4b68394076..696786d48e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -160,7 +160,12 @@ F3BB46522A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */; }; F3BB46542A3A1E9D00461F6E /* CCCellMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */; }; F3C587AE2D47E4FE004532DB /* PHAssetCollectionThumbnailLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C587AD2D47E4FE004532DB /* PHAssetCollectionThumbnailLoader.swift */; }; + F3C6F6F62F34CC0900C531B6 /* NCAssistantChatConversationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C6F6F52F34CC0900C531B6 /* NCAssistantChatConversationsModel.swift */; }; F3CA337D2D0B2B6C00672333 /* AlbumModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */; }; + F3DDFE0F2F15453900A784C8 /* NCAssistantChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DDFE0E2F15453900A784C8 /* NCAssistantChat.swift */; }; + F3DDFE1E2F1F8EC600A784C8 /* ChatInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DDFE1D2F1F8EC600A784C8 /* ChatInputField.swift */; }; + F3DDFE212F1F953000A784C8 /* NCAssistantChatConversations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DDFE202F1F953000A784C8 /* NCAssistantChatConversations.swift */; }; + F3DDFE232F1FB4C300A784C8 /* NCAssistantChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DDFE222F1FB4C300A784C8 /* NCAssistantChatModel.swift */; }; F3E173B02C9AF637006D177A /* ScreenAwakeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */; }; F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; F3E173C12C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; @@ -1267,7 +1272,12 @@ F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreAppSuggestionsCell.swift; sourceTree = ""; }; F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CCCellMore.swift; sourceTree = ""; }; F3C587AD2D47E4FE004532DB /* PHAssetCollectionThumbnailLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHAssetCollectionThumbnailLoader.swift; sourceTree = ""; }; + F3C6F6F52F34CC0900C531B6 /* NCAssistantChatConversationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantChatConversationsModel.swift; sourceTree = ""; }; F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumModel.swift; sourceTree = ""; }; + F3DDFE0E2F15453900A784C8 /* NCAssistantChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantChat.swift; sourceTree = ""; }; + F3DDFE1D2F1F8EC600A784C8 /* ChatInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputField.swift; sourceTree = ""; }; + F3DDFE202F1F953000A784C8 /* NCAssistantChatConversations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantChatConversations.swift; sourceTree = ""; }; + F3DDFE222F1FB4C300A784C8 /* NCAssistantChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantChatModel.swift; sourceTree = ""; }; F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAwakeManager.swift; sourceTree = ""; }; F3E173BF2C9B1067006D177A /* AwakeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwakeMode.swift; sourceTree = ""; }; F3F442ED2DDE292600FD701F /* NCMetadataPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMetadataPermissions.swift; sourceTree = ""; }; @@ -2107,6 +2117,7 @@ F3374A7F2D64AB40002A38F9 /* Components */ = { isa = PBXGroup; children = ( + F3DDFE1D2F1F8EC600A784C8 /* ChatInputField.swift */, F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */, F3374A802D64AB9E002A38F9 /* StatusInfo.swift */, F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */, @@ -2138,10 +2149,12 @@ isa = PBXGroup; children = ( F3A047962BD2668800658E7B /* NCAssistant.swift */, + F3A047932BD2668800658E7B /* NCAssistantModel.swift */, + F3DDFE0D2F15452F00A784C8 /* Chat */, + F3DDFE1F2F1F951000A784C8 /* Chat Sessions */, F3A047902BD2668800658E7B /* Create Task */, F3A047942BD2668800658E7B /* Task Detail */, F3374A7F2D64AB40002A38F9 /* Components */, - F3A047922BD2668800658E7B /* Models */, ); path = Assistant; sourceTree = ""; @@ -2154,14 +2167,6 @@ path = "Create Task"; sourceTree = ""; }; - F3A047922BD2668800658E7B /* Models */ = { - isa = PBXGroup; - children = ( - F3A047932BD2668800658E7B /* NCAssistantModel.swift */, - ); - path = Models; - sourceTree = ""; - }; F3A047942BD2668800658E7B /* Task Detail */ = { isa = PBXGroup; children = ( @@ -2181,6 +2186,24 @@ path = Cells; sourceTree = ""; }; + F3DDFE0D2F15452F00A784C8 /* Chat */ = { + isa = PBXGroup; + children = ( + F3DDFE0E2F15453900A784C8 /* NCAssistantChat.swift */, + F3DDFE222F1FB4C300A784C8 /* NCAssistantChatModel.swift */, + ); + path = Chat; + sourceTree = ""; + }; + F3DDFE1F2F1F951000A784C8 /* Chat Sessions */ = { + isa = PBXGroup; + children = ( + F3DDFE202F1F953000A784C8 /* NCAssistantChatConversations.swift */, + F3C6F6F52F34CC0900C531B6 /* NCAssistantChatConversationsModel.swift */, + ); + path = "Chat Sessions"; + sourceTree = ""; + }; F3E173BE2C9B1057006D177A /* ScreenAwakeManager */ = { isa = PBXGroup; children = ( @@ -4412,6 +4435,7 @@ F7AE00F8230E81CB007ACF8A /* NCBrowserWeb.swift in Sources */, F77DD6A82C5CC093009448FB /* NCSession.swift in Sources */, F702F30825EE5D47008F8E80 /* NCPopupViewController.swift in Sources */, + F3C6F6F62F34CC0900C531B6 /* NCAssistantChatConversationsModel.swift in Sources */, F76340FC2EBDF64D0056F538 /* NCManageDatabase+Tag.swift in Sources */, F733598125C1C188002ABA72 /* NCAskAuthorization.swift in Sources */, 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */, @@ -4583,6 +4607,7 @@ F7816EF22C2C3E1F00A52517 /* NCPushNotification.swift in Sources */, F76882342C0DD1E7001CF441 /* NCDisplayView.swift in Sources */, F7C30DF6291BC0CA0017149B /* NCNetworkingE2EEUpload.swift in Sources */, + F3DDFE232F1FB4C300A784C8 /* NCAssistantChatModel.swift in Sources */, F7501C332212E57500FB1415 /* NCMedia.swift in Sources */, F7411C552D7B26D700F57358 /* NCNetworking+ServerError.swift in Sources */, F72944F22A84246400246839 /* NCEndToEndMetadataV20.swift in Sources */, @@ -4699,7 +4724,9 @@ F7A03E2F2D425A14007AA677 /* NCFavoriteNavigationController.swift in Sources */, F343A4BB2A1E734600DDA874 /* Optional+Extension.swift in Sources */, F76882232C0DD1E7001CF441 /* NCCapabilitiesModel.swift in Sources */, + F3DDFE212F1F953000A784C8 /* NCAssistantChatConversations.swift in Sources */, F7E2B64F2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift in Sources */, + F3DDFE0F2F15453900A784C8 /* NCAssistantChat.swift in Sources */, F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, F799DF8B2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift in Sources */, @@ -4723,6 +4750,7 @@ F76882272C0DD1E7001CF441 /* NCManageE2EEView.swift in Sources */, F7864ACC2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F7327E302B73A86700A462C7 /* NCNetworking+WebDAV.swift in Sources */, + F3DDFE1E2F1F8EC600A784C8 /* ChatInputField.swift in Sources */, F7D61EA82EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, F79FFB262A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */, F70D8D8124A4A9BF000A5756 /* NCNetworkingProcess.swift in Sources */, diff --git a/Tests/NextcloudUITests/AssistantUITests.swift b/Tests/NextcloudUITests/AssistantUITests.swift index bce341aed9..51835393ea 100644 --- a/Tests/NextcloudUITests/AssistantUITests.swift +++ b/Tests/NextcloudUITests/AssistantUITests.swift @@ -50,7 +50,7 @@ final class AssistantUITests: BaseUIXCTestCase { } private func createTask(input: String) { - app.navigationBars["Assistant"].buttons["CreateButton"].tap() + app.navigationBars["Assistant"].buttons["ConversationsButton"].tap() let inputTextEditor = app.textViews["InputTextEditor"] inputTextEditor.await() diff --git a/iOSClient/Account/NCAccount.swift b/iOSClient/Account/NCAccount.swift index dd13742efe..e9d8d20190 100644 --- a/iOSClient/Account/NCAccount.swift +++ b/iOSClient/Account/NCAccount.swift @@ -194,7 +194,7 @@ class NCAccount: NSObject { return } - await showErrorBanner(controller: controller, text: "_account_unauthorized_", errorCode: NCGlobal.shared.errorUnauthorized401) + await showErrorBanner(controller: controller, text: String(format: NSLocalizedString("_account_unauthorized_", comment: ""), account), errorCode: NCGlobal.shared.errorUnauthorized401) let resultsWipe = await NextcloudKit.shared.getRemoteWipeStatusAsync(serverUrl: tblAccount.urlBase, token: token, account: account) { task in Task { diff --git a/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversations.swift b/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversations.swift new file mode 100644 index 0000000000..5fd16cf93d --- /dev/null +++ b/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversations.swift @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import SwiftUI +import NextcloudKit + +struct NCAssistantChatConversations: View { + var conversationsModel: NCAssistantChatConversationsModel + var selectedConversation: AssistantConversation? + var onConversationSelected: (AssistantConversation?) -> Void + + @Environment(\.dismiss) private var dismiss + + var body: some View { + Group { + List(conversationsModel.conversations, id: \.id) { conversation in + Button { + onConversationSelected(conversation) + dismiss() + } label: { + HStack { + Text(conversation.validTitle) + Spacer() + if selectedConversation?.id == conversation.id { + Image(systemName: "checkmark") + .foregroundStyle(.blue) + } + } + .contentShape(Rectangle()) + } + } + } + .navigationTitle(NSLocalizedString("_conversations_", comment: "")) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("_new_conversation_", systemImage: "plus.message.fill") { + Task { + let session = await conversationsModel.createNewConversation() + onConversationSelected(session) + dismiss() + } + } + } + } + } +} + +#Preview { + NCAssistantChatConversations(conversationsModel: NCAssistantChatConversationsModel(controller: nil), selectedConversation: nil, onConversationSelected: { _ in }) +} diff --git a/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversationsModel.swift b/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversationsModel.swift new file mode 100644 index 0000000000..4ad1bb1d70 --- /dev/null +++ b/iOSClient/Assistant/Chat Sessions/NCAssistantChatConversationsModel.swift @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit + +@Observable class NCAssistantChatConversationsModel { + var conversations: [AssistantConversation] = [] + var isLoading: Bool = false + var hasError: Bool = false + + private let ncSession: NCSession.Session + + init(controller: NCMainTabBarController?) { + self.ncSession = NCSession.shared.getSession(controller: controller) + loadAllSessions() + } + + func loadAllSessions() { + Task { + let result = await NextcloudKit.shared.getAssistantChatConversations(account: ncSession.account) + conversations = result.sessions ?? [] + } + } + + func createNewConversation(title: String? = nil) async -> AssistantConversation? { + let timestamp = Int(Date().timeIntervalSince1970) + let result = await NextcloudKit.shared.createAssistantChatConversation(title: title, timestamp: timestamp, account: ncSession.account) + if result.error == .success, let newConversation = result.conversation?.conversation { + conversations.insert(newConversation, at: 0) + return newConversation + } else { + hasError = true + return nil + } + } +} diff --git a/iOSClient/Assistant/Chat/NCAssistantChat.swift b/iOSClient/Assistant/Chat/NCAssistantChat.swift new file mode 100644 index 0000000000..184754418f --- /dev/null +++ b/iOSClient/Assistant/Chat/NCAssistantChat.swift @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import NextcloudKit + +struct NCAssistantChat: View { + @Environment(NCAssistantChatModel.self) var chatModel + @Binding var conversationsModel: NCAssistantChatConversationsModel + + var body: some View { + @Bindable var chatModel = chatModel + + if chatModel.messages.isEmpty { + NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_no_chat_subtitle_") + } + + ZStack { + VStack(spacing: 0) { + messageListView + } + + } + .safeAreaInset(edge: .bottom) { + ChatInputField(isLoading: $chatModel.isSending, isDisabled: $chatModel.isSendingDisabled) { input in + if chatModel.selectedConversation != nil { + chatModel.sendMessage(input: input) + } else { + chatModel.startNewConversationViaMessage(input: input, sessionsModel: conversationsModel) + } + } + } + .navigationTitle(NSLocalizedString("_assistant_chat_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + } + + private var messageListView: some View { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(chatModel.messages) { message in + MessageBubbleView(message: message, account: chatModel.controller?.account ?? "") + .id(message.id) + } + + if chatModel.isThinking { + ThinkingBubbleView() + .id("thinking") + } + + if chatModel.showRetryResponseGenerationButton { + let button = Button("_retry_response_generation_") { + chatModel.onRetryResponseGeneration() + } + .frame(maxWidth: .infinity) + .padding() + + if #available(iOS 26.0, *) { + button + .buttonStyle(.glass) + } else { + button + .buttonStyle(.bordered) + } + } + } + .padding(.vertical) + } + .onChange(of: chatModel.messages.count) { _, _ in + withAnimation { + if let lastMessage = chatModel.messages.last { + proxy.scrollTo(lastMessage.id, anchor: .bottom) + } + } + } + .onChange(of: chatModel.isThinking) { _, isThinking in + if isThinking { + withAnimation { + proxy.scrollTo("thinking", anchor: .bottom) + } + } + } + } + } +} + +// MARK: - Message Bubble View + +struct MessageBubbleView: View { + let message: AssistantChatMessage + let account: String + + var body: some View { + HStack { + if message.isFromHuman { + Spacer(minLength: 50) + } + + VStack(alignment: message.isFromHuman ? .trailing : .leading, spacing: 4) { + Text(message.content) + .font(.body) + .foregroundStyle(message.isFromHuman ? .white : .primary) + .padding() + .background(bubbleBackground) + .clipShape(.rect(cornerRadius: 16)) + + Text(NCUtility().getRelativeDateTitle(Date(timeIntervalSince1970: TimeInterval(message.timestamp)))) + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal, 4) + } + .frame(maxWidth: .infinity, alignment: message.isFromHuman ? .trailing : .leading) + .padding(.horizontal) + + if !message.isFromHuman { + Spacer(minLength: 50) + } + } + } + + private var bubbleBackground: Color { + if message.isFromHuman { + return Color(NCBrandColor.shared.getElement(account: account)) + } else { + return Color(NCBrandColor.shared.textColor2).opacity(0.1) + } + } +} + +// MARK: - Thinking Bubble View + +struct ThinkingBubbleView: View { + @State private var scale1: CGFloat = 1.0 + @State private var scale2: CGFloat = 1.0 + @State private var scale3: CGFloat = 1.0 + + var body: some View { + HStack(alignment: .center, spacing: 4) { + Circle() + .fill(Color.secondary) + .frame(width: 8, height: 8) + .scaleEffect(scale1) + + Circle() + .fill(Color.secondary) + .frame(width: 8, height: 8) + .scaleEffect(scale2) + + Circle() + .fill(Color.secondary) + .frame(width: 8, height: 8) + .scaleEffect(scale3) + } + .padding() + .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) + .clipShape(.rect(cornerRadius: 16)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .onAppear { + startAnimation() + } + } + + private func startAnimation() { + withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true)) { + scale1 = 1.3 + } + withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true).delay(0.15)) { + scale2 = 1.3 + } + withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true).delay(0.3)) { + scale3 = 1.3 + } + } +} + +// MARK: - Empty Chat View + +struct EmptyChatView: View { + @Environment(NCAssistantChatModel.self) var chatModel + + var body: some View { + VStack(spacing: 16) { + Image(systemName: "bubble.left.and.bubble.right") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: chatModel.controller?.account))) + .font(Font.system(.body).weight(.light)) + .frame(height: 100) + + Text(NSLocalizedString("_start_conversation_", comment: "")) + .font(.system(size: 22, weight: .bold)) + .padding(.bottom, 5) + + Text(NSLocalizedString("_ask_assistant_anything_", comment: "")) + .font(.system(size: 14)) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + } +} + +// MARK: - Preview + +#Preview { + NavigationStack { + NCAssistantChat(conversationsModel: .constant(NCAssistantChatConversationsModel(controller: nil))) + .environment(NCAssistantChatModel(controller: nil)) + .environment(NCAssistantModel(controller: nil)) + } +} + +#Preview("With Messages") { + NavigationStack { + NCAssistantChat(conversationsModel: .constant(NCAssistantChatConversationsModel(controller: nil))) + .environment(NCAssistantChatModel.example) + .environment(NCAssistantModel(controller: nil)) + } +} diff --git a/iOSClient/Assistant/Chat/NCAssistantChatModel.swift b/iOSClient/Assistant/Chat/NCAssistantChatModel.swift new file mode 100644 index 0000000000..140f50870d --- /dev/null +++ b/iOSClient/Assistant/Chat/NCAssistantChatModel.swift @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import NextcloudKit + +@Observable class NCAssistantChatModel { + var messages: [AssistantChatMessage] = [] + var isSending: Bool = false + var isThinking: Bool = false + var isSendingDisabled = false + var hasError: Bool = false + var showRetryResponseGenerationButton = false + var showMessageNotSentError: Bool = false + + public private(set) var selectedConversation: AssistantConversation? + + var currentSession: AssistantSession? + + private let ncSession: NCSession.Session + private var pollingTask: Task? + + @ObservationIgnored var controller: NCMainTabBarController? + @ObservationIgnored private var chatMessageTaskId: Int? + + init(controller: NCMainTabBarController?, messages: [AssistantChatMessage] = []) { + self.controller = controller + self.ncSession = NCSession.shared.getSession(controller: controller) + self.messages = messages + } + + func startPollingForResponse(interval: TimeInterval = 4.0) { + stopPolling() + isSendingDisabled = true + isThinking = true + showRetryResponseGenerationButton = false + + pollingTask = Task { + while !Task.isCancelled { + + await loadLastMessage() + try? await Task.sleep(for: .seconds(interval)) + } + } + } + + func stopPolling() { + pollingTask?.cancel() + pollingTask = nil + isThinking = false + isSendingDisabled = false + } + + func selectConversation(selectedConversation: AssistantConversation) async { + self.selectedConversation = selectedConversation + + stopPolling() + showRetryResponseGenerationButton = false + currentSession = nil + + await loadAllMessages() + currentSession = await checkChatSession(sessionId: selectedConversation.id) + chatMessageTaskId = currentSession?.messageTaskId + + if messages.last?.isFromHuman == true, chatMessageTaskId == nil, isSending == false { + showRetryResponseGenerationButton = true + } else if chatMessageTaskId != nil { + startPollingForResponse() + } + } + + func generateChatSession() async { + guard let sessionId = selectedConversation?.id else { return } + + let result = await NextcloudKit.shared.generateAssistantChatSession(sessionId: sessionId, account: ncSession.account) + chatMessageTaskId = result.sessionTask?.taskId + } + + func onRetryResponseGeneration() { + Task { + await generateChatSession() + startPollingForResponse() + } + } + + private func checkChatSession(sessionId: Int) async -> AssistantSession? { + let result = await NextcloudKit.shared.checkAssistantChatSession(sessionId: sessionId, account: ncSession.account) + return result.session + } + + private func loadAllMessages() async { + guard let sessionId = selectedConversation?.id else { return } + + let result = await NextcloudKit.shared.getAssistantChatMessages(sessionId: sessionId, account: ncSession.account) + + if result.error == .success { + messages = result.chatMessages ?? [] + } else { + await showErrorBanner(controller: controller, title: "_error_", text: "_assistant_error_load_messages_", errorCode: result.error.errorCode) + } + } + + private func loadLastMessage() async { + guard let chatMessageTaskId else { return } + + let result = await NextcloudKit.shared.checkAssistantChatGeneration(taskId: chatMessageTaskId, sessionId: selectedConversation?.id ?? 0, account: ncSession.account) + + if result.error != .success { + stopPolling() + await showErrorBanner(controller: controller, title: "_error_", text: "_assistant_error_generate_response_", errorCode: result.error.errorCode) + return + } + + if let lastMessage = result.chatMessage, lastMessage.role == "assistant" { + stopPolling() + messages.append(lastMessage) + } + } + + func sendMessage(input: String) { + guard let selectedConversation else { return } + + let request = AssistantChatMessageRequest(sessionId: selectedConversation.id, role: "human", content: input, timestamp: Int(Date().timeIntervalSince1970), firstHumanMessage: messages.isEmpty) + isSending = true + isSendingDisabled = true + + Task { + let result = await NextcloudKit.shared.createAssistantChatMessage(messageRequest: request, account: ncSession.account) + if result.error == .success { + guard let chatMessage = result.chatMessage else { return } + messages.append(chatMessage) + + stopPolling() + await generateChatSession() + startPollingForResponse() + } else { + await showErrorBanner(controller: controller, title: "_error_", text: "_assistant_error_send_message_", errorCode: 20) + } + + isSending = false + } + } + + func startNewConversationViaMessage(input: String, sessionsModel: NCAssistantChatConversationsModel) { + Task { + isSending = true + guard let conversation = await sessionsModel.createNewConversation(title: input) else { return } + await selectConversation(selectedConversation: conversation) + sendMessage(input: input) + } + } +} + +extension NCAssistantChatModel { + static var example = NCAssistantChatModel(controller: nil, messages: [ + AssistantChatMessage( + id: 1, + sessionId: 0, + role: "human", + content: "Hello! Can you help me summarize this document?", + timestamp: Int(Date().addingTimeInterval(-300).timeIntervalSince1970 * 1000) + ), + AssistantChatMessage( + id: 2, + sessionId: 0, + role: "assistant", + content: "Of course! I'd be happy to help you summarize your document. Please share the document or paste the text you'd like me to summarize.", + timestamp: Int(Date().addingTimeInterval(-240).timeIntervalSince1970 * 1000) + ), + AssistantChatMessage( + id: 3, + sessionId: 0, + role: "human", + content: "Here is the text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + timestamp: Int(Date().addingTimeInterval(-180).timeIntervalSince1970 * 1000) + ), + AssistantChatMessage( + id: 4, + sessionId: 0, + role: "assistant", + content: "Based on the text you provided, here's a concise summary: The document discusses the classic Lorem Ipsum placeholder text, which has been used in the printing and typesetting industry for centuries as a standard dummy text.", + timestamp: Int(Date().addingTimeInterval(-120).timeIntervalSince1970 * 1000) + )]) +} diff --git a/iOSClient/Assistant/Components/ChatInputField.swift b/iOSClient/Assistant/Components/ChatInputField.swift new file mode 100644 index 0000000000..907d06e339 --- /dev/null +++ b/iOSClient/Assistant/Components/ChatInputField.swift @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ChatInputField: View { + @FocusState private var isInputFocused: Bool + @State var text: String = "" + @Binding var isLoading: Bool + @Binding var isDisabled: Bool + var onSend: ((_ input: String) -> Void)? + + init(isLoading: Binding = .constant(false), isDisabled: Binding = .constant(false), onSend: ((_: String) -> Void)? = nil) { + _isLoading = isLoading + _isDisabled = isDisabled + self.onSend = onSend + } + + var body: some View { + VStack { + Text("_assistant_ai_warning_") + .lineLimit(1) + .allowsTightening(true) + .minimumScaleFactor(0.5) + + HStack(spacing: 8) { + TextField(NSLocalizedString("_type_message_", comment: ""), text: $text, axis: .vertical) + .textFieldStyle(.plain) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(.primary.opacity(0.1)) + .clipShape(.rect(cornerRadius: 20)) + .focused($isInputFocused) + .lineLimit(1...5) + + Button(action: { + isInputFocused = false + onSend?(text.trimmingCharacters(in: .whitespaces)) + text = "" + }) { + if isLoading { + ProgressView() + .frame(width: 28, height: 28) + } else { + Image(systemName: "arrow.up.circle.fill") + .font(.system(size: 28)) + } + } + .disabled(text.trimmingCharacters(in: .whitespaces).isEmpty || isDisabled || isLoading) + } + } + .padding(.horizontal) + .padding(.top, 16) + .padding(.bottom, 16) + .background(.background) + } +} + +#Preview { + ChatInputField(isLoading: .constant(false)) + ChatInputField(isLoading: .constant(true)) +} diff --git a/iOSClient/Assistant/Components/NCAssistantEmptyView.swift b/iOSClient/Assistant/Components/NCAssistantEmptyView.swift index d24cca06ca..0c6f424a9d 100644 --- a/iOSClient/Assistant/Components/NCAssistantEmptyView.swift +++ b/iOSClient/Assistant/Components/NCAssistantEmptyView.swift @@ -1,15 +1,11 @@ -// -// EmptyTasksView.swift -// Nextcloud -// -// Created by Milen on 16.04.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI struct NCAssistantEmptyView: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var assistantModel let titleKey, subtitleKey: String var body: some View { @@ -18,7 +14,7 @@ struct NCAssistantEmptyView: View { .renderingMode(.template) .resizable() .aspectRatio(contentMode: .fit) - .foregroundStyle(Color(NCBrandColor.shared.getElement(account: model.controller?.account))) + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: assistantModel.controller?.account))) .font(Font.system(.body).weight(.light)) .frame(height: 100) diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index cd3250d38c..a172e724f7 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -9,7 +9,7 @@ import SwiftUI struct NCAssistantCreateNewTask: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var model @State var text = "" @FocusState private var inFocus: Bool @Environment(\.presentationMode) var presentationMode @@ -60,7 +60,7 @@ struct NCAssistantCreateNewTask: View { let model = NCAssistantModel(controller: nil) NCAssistantCreateNewTask() - .environmentObject(model) + .environment(model) .onAppear { model.loadDummyData() } diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 673f37e6e5..07dea8b63d 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -1,36 +1,33 @@ -// -// NCAssistant.swift -// Nextcloud -// -// Created by Milen on 03.04.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI import NextcloudKit import PopupView struct NCAssistant: View { - @EnvironmentObject var model: NCAssistantModel + @State var assistantModel: NCAssistantModel + @State var chatModel: NCAssistantChatModel + @State var conversationsModel: NCAssistantChatConversationsModel @State var input = "" @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { ZStack { - TaskList() + if assistantModel.types.isEmpty, !assistantModel.isLoading { + NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") + } else if assistantModel.isSelectedTypeChat { + NCAssistantChat(conversationsModel: $conversationsModel) + } else { + TaskList() + } - if model.isLoading, !model.isRefreshing { + if assistantModel.isLoading, !assistantModel.isRefreshing { ProgressView() .controlSize(.regular) } - - if model.types.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") - } else if model.filteredTasks.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") - } - } .toolbar { ToolbarItem(placement: .topBarLeading) { @@ -41,17 +38,27 @@ struct NCAssistant: View { } } ToolbarItem(placement: .topBarTrailing) { - NavigationLink(destination: NCAssistantCreateNewTask()) { - Image(systemName: "plus") + NavigationLink(destination: NCAssistantChatConversations(conversationsModel: conversationsModel, selectedConversation: chatModel.selectedConversation) { conversation in + guard let conversation else { return } + + Task { + await chatModel.selectConversation(selectedConversation: conversation) + assistantModel.selectChatTaskType() + } + }) { + Image(systemName: "clock.arrow.trianglehead.counterclockwise.rotate.90") .font(Font.system(.body).weight(.light)) .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) } - .disabled(model.selectedType == nil) - .accessibilityIdentifier("CreateButton") + .disabled(assistantModel.selectedType == nil) + .accessibilityIdentifier("ConversationsButton") } } .navigationBarTitleDisplayMode(.inline) .navigationTitle(NSLocalizedString("_assistant_", comment: "")) + .modifier(NavigationSubtitleModifier(subtitle: assistantModel.isSelectedTypeChat ? + chatModel.currentSession?.sessionTitle ?? chatModel.selectedConversation?.validTitle + : "")) .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .top, spacing: -10) { TypeList() @@ -59,7 +66,7 @@ struct NCAssistant: View { } .navigationViewStyle(.stack) - .popup(isPresented: $model.hasError) { + .popup(isPresented: $assistantModel.hasError) { Text(NSLocalizedString("_error_occurred_", comment: "")) .padding() .background(.red) @@ -71,22 +78,27 @@ struct NCAssistant: View { .position(.bottom) } .accentColor(Color(NCBrandColor.shared.iconImageColor)) - .environmentObject(model) + .environment(assistantModel) + .environment(chatModel) + .onDisappear { + chatModel.stopPolling() + } } } #Preview { + @Previewable @State var chatModel = NCAssistantChatModel(controller: nil) let model = NCAssistantModel(controller: nil) + let conversationsModel = NCAssistantChatConversationsModel(controller: nil) - NCAssistant() - .environmentObject(model) + NCAssistant(assistantModel: model, chatModel: chatModel, conversationsModel: conversationsModel) .onAppear { model.loadDummyData() } } struct TaskList: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var assistantModel @State var presentEditTask = false @State var showDeleteConfirmation = false @@ -94,11 +106,13 @@ struct TaskList: View { @State var taskToDelete: AssistantTask? var body: some View { - List(model.filteredTasks, id: \.id) { task in + @Bindable var assistantModel = assistantModel + + List(assistantModel.filteredTasks, id: \.id) { task in TaskItem(showDeleteConfirmation: $showDeleteConfirmation, taskToDelete: $taskToDelete, task: task) .contextMenu { Button { - model.shareTask(task) + assistantModel.shareTask(task) } label: { Label { Text("_share_") @@ -108,7 +122,7 @@ struct TaskList: View { } Button { - model.scheduleTask(input: task.input?.input ?? "") + assistantModel.scheduleTask(input: task.input?.input ?? "") } label: { Label { Text("_retry_") @@ -144,16 +158,16 @@ struct TaskList: View { } .accessibilityIdentifier("TaskContextMenu") } - .if(!model.types.isEmpty) { view in + .if(!assistantModel.types.isEmpty) { view in view.refreshable { - model.refresh() + assistantModel.refresh() } } .confirmationDialog("", isPresented: $showDeleteConfirmation) { Button(NSLocalizedString("_delete_", comment: ""), role: .destructive) { withAnimation { guard let taskToDelete else { return } - model.deleteTask(taskToDelete) + assistantModel.deleteTask(taskToDelete) } } } @@ -162,11 +176,20 @@ struct TaskList: View { NCAssistantCreateNewTask(text: taskToEdit?.input?.input ?? "", editMode: true) } } + .safeAreaInset(edge: .bottom) { + ChatInputField(isLoading: $assistantModel.isLoading) { input in + assistantModel.scheduleTask(input: input) + } + } + + if assistantModel.filteredTasks.isEmpty, !assistantModel.isLoading { + NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") + } } } struct TypeButton: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var model let taskType: TaskTypeData? var scrollProxy: ScrollViewProxy @@ -201,7 +224,7 @@ struct TypeButton: View { } struct TaskItem: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var model @Binding var showDeleteConfirmation: Bool @Binding var taskToDelete: AssistantTask? var task: AssistantTask @@ -245,8 +268,20 @@ struct TaskItem: View { } } +struct NavigationSubtitleModifier: ViewModifier { + let subtitle: String? + + func body(content: Content) -> some View { + if #available(iOS 26.0, *) { + content.navigationSubtitle(subtitle ?? "") + } else { + content + } + } +} + struct TypeList: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var model var body: some View { ScrollViewReader { scrollProxy in @@ -260,6 +295,11 @@ struct TypeList: View { .frame(height: 50) } .background(.ultraThinMaterial) + .onChange(of: model.scrollTypeListToTop) { + withAnimation(.easeInOut(duration: 0.7)) { + scrollProxy.scrollTo(model.types.first?.id, anchor: .center) + } + } } } } diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/NCAssistantModel.swift similarity index 62% rename from iOSClient/Assistant/Models/NCAssistantModel.swift rename to iOSClient/Assistant/NCAssistantModel.swift index dd99f16704..3a6640e376 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/NCAssistantModel.swift @@ -7,22 +7,24 @@ import UIKit import NextcloudKit import SwiftUI -class NCAssistantModel: ObservableObject { - @Published var types: [TaskTypeData] = [] - @Published var filteredTasks: [AssistantTask] = [] - @Published var selectedType: TaskTypeData? - @Published var selectedTask: AssistantTask? - - @Published var hasError: Bool = false - @Published var isLoading: Bool = false - @Published var isRefreshing: Bool = false - @Published var controller: NCMainTabBarController? - - private var tasks: [AssistantTask] = [] - - private let session: NCSession.Session - - private let useV2: Bool +@Observable +class NCAssistantModel { + var types: [TaskTypeData] = [] + var filteredTasks: [AssistantTask] = [] + var selectedType: TaskTypeData? + var selectedTask: AssistantTask? + + var hasError: Bool = false + var isLoading: Bool = false + var isRefreshing: Bool = false + var scrollTypeListToTop: Bool = false + + @ObservationIgnored let controller: NCMainTabBarController? + @ObservationIgnored private var tasks: [AssistantTask] = [] + @ObservationIgnored private let session: NCSession.Session + @ObservationIgnored private let useV2: Bool + @ObservationIgnored private let chatTypeId = "core:text2text:chat" + @ObservationIgnored var isSelectedTypeChat: Bool { selectedType?.id == chatTypeId } init(controller: NCMainTabBarController?) { self.controller = controller @@ -30,7 +32,6 @@ class NCAssistantModel: ObservableObject { let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() useV2 = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion30 - // useV2 = false loadAllTypes() } @@ -49,6 +50,11 @@ class NCAssistantModel: ObservableObject { self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) } + func selectChatTaskType() { + selectTaskType(types.first) + scrollTypeListToTop.toggle() + } + func selectTaskType(_ type: TaskTypeData?) { selectedType = type @@ -60,19 +66,18 @@ class NCAssistantModel: ObservableObject { selectedTask = task isLoading = true - /* - if useV2 { - NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { _, _, _, error in - handle(task: task, error: error) - }) - } else { - NextcloudKit.shared.textProcessingGetTask(taskId: Int(task.id), account: session.account) { _, task, _, error in - guard let task else { return } - let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first - handle(task: taskV2, error: error) + Task { + if useV2 { + let result = await NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account) + handle(task: task, error: result.error) + } else { + NextcloudKit.shared.textProcessingGetTask(taskId: Int(task.id), account: session.account) { _, task, _, error in + guard let task else { return } + let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first + handle(task: taskV2, error: error) + } } } - */ func handle(task: AssistantTask?, error: NKError?) { self.isLoading = false @@ -89,19 +94,18 @@ class NCAssistantModel: ObservableObject { func scheduleTask(input: String) { isLoading = true - /* - if useV2 { - guard let selectedType else { return } - NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType, account: session.account) { _, task, _, error in - handle(task: task, error: error) - } - } else { - NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: session.account) { _, task, _, error in - guard let task, let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first else { return } - handle(task: taskV2, error: error) + Task { + if useV2 { + guard let selectedType else { return } + let result = await NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType, account: session.account) + handle(task: result.task, error: result.error) + } else { + NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: session.account) { _, task, _, error in + guard let task, let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first else { return } + handle(task: taskV2, error: error) + } } } - */ func handle(task: AssistantTask?, error: NKError?) { self.isLoading = false @@ -121,17 +125,16 @@ class NCAssistantModel: ObservableObject { func deleteTask(_ task: AssistantTask) { isLoading = true - /* - if useV2 { - NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { _, _, error in - handle(task: task, error: error) - } - } else { - NextcloudKit.shared.textProcessingDeleteTask(taskId: Int(task.id), account: session.account) { _, _, _, error in - handle(task: task, error: error) + Task { + if useV2 { + let result = await NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) + handle(task: task, error: result.error) + } else { + NextcloudKit.shared.textProcessingDeleteTask(taskId: Int(task.id), account: session.account) { _, _, _, error in + handle(task: task, error: error) + } } } - */ func handle(task: AssistantTask, error: NKError?) { self.isLoading = false @@ -154,20 +157,19 @@ class NCAssistantModel: ObservableObject { private func loadAllTypes() { isLoading = true - /* - if useV2 { - NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { _, types, _, error in - handle(types: types, error: error) - } - } else { - NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in - guard let types else { return } - let typesV2 = NKTextProcessingTaskType.toV2(type: types).types - - handle(types: typesV2, error: error) + Task { + if useV2 { + let result = await NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) + handle(types: result.types, error: result.error) + } else { + NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in + guard let types else { return } + let typesV2 = NKTextProcessingTaskType.toV2(type: types).types + + handle(types: typesV2, error: error) + } } } - */ func handle(types: [TaskTypeData]?, error: NKError) { self.isLoading = false @@ -179,10 +181,10 @@ class NCAssistantModel: ObservableObject { guard let types else { return } - self.types = types + self.types = types.sorted { $0.id == chatTypeId && $1.id != chatTypeId } if self.selectedType == nil { - self.selectTaskType(types.first) + self.selectTaskType(self.types.first) } self.loadAllTasks(type: selectedType) @@ -192,19 +194,18 @@ class NCAssistantModel: ObservableObject { private func loadAllTasks(appId: String = "assistant", type: TaskTypeData?) { isLoading = true - /* - if useV2 { - NextcloudKit.shared.textProcessingGetTasksV2(taskType: type?.id ?? "", account: session.account) { _, tasks, _, error in - guard let tasks = tasks?.tasks.filter({ $0.appId == "assistant" }) else { return } - handle(tasks: tasks, error: error) - } - } else { - NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in - guard let tasks else { return } - handle(tasks: NKTextProcessingTask.toV2(tasks: tasks).tasks, error: error) + Task { + if useV2 { + let result = await NextcloudKit.shared.textProcessingGetTasksV2(taskType: type?.id ?? "", account: session.account) + guard let tasks = result.tasks?.tasks.filter({ $0.appId == "assistant" }) else { return } + handle(tasks: tasks, error: result.error) + } else { + NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in + guard let tasks else { return } + handle(tasks: NKTextProcessingTask.toV2(tasks: tasks).tasks, error: error) + } } } - */ func handle(tasks: [AssistantTask], error: NKError?) { isLoading = false @@ -232,10 +233,11 @@ extension NCAssistantModel { } self.types = [ - TaskTypeData(id: "1", name: "Free Prompt", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "1", name: "Chat", description: "", inputShape: nil, outputShape: nil), TaskTypeData(id: "2", name: "Summarize", description: "", inputShape: nil, outputShape: nil), TaskTypeData(id: "3", name: "Generate headline", description: "", inputShape: nil, outputShape: nil), - TaskTypeData(id: "4", name: "Reformulate", description: "", inputShape: nil, outputShape: nil) + TaskTypeData(id: "4", name: "Reformulate", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "5", name: "Free Prompt", description: "", inputShape: nil, outputShape: nil) ] self.tasks = tasks diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index 3a2e9b9bba..7533dfc588 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -10,7 +10,7 @@ import SwiftUI import NextcloudKit struct NCAssistantTaskDetail: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var assistantModel let task: AssistantTask var body: some View { @@ -21,7 +21,7 @@ struct NCAssistantTaskDetail: View { } .toolbar { Button(action: { - model.shareTask(task) + assistantModel.shareTask(task) }, label: { Image(systemName: "square.and.arrow.up") }) @@ -29,23 +29,23 @@ struct NCAssistantTaskDetail: View { .navigationBarTitleDisplayMode(.inline) .navigationTitle(NSLocalizedString("_task_details_", comment: "")) .onAppear { - model.selectTask(task) + assistantModel.selectTask(task) } } } #Preview { - let model = NCAssistantModel(controller: nil) + let assistantModel = NCAssistantModel(controller: nil) - NCAssistantTaskDetail(task: model.selectedTask!) - .environmentObject(model) + NCAssistantTaskDetail(task: assistantModel.selectedTask!) + .environment(assistantModel) .onAppear { - model.loadDummyData() + assistantModel.loadDummyData() } } struct InputOutputScrollView: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var model let task: AssistantTask var body: some View { @@ -79,7 +79,7 @@ struct InputOutputScrollView: View { } struct BottomDetailsBar: View { - @EnvironmentObject var model: NCAssistantModel + @Environment(NCAssistantModel.self) var assistantModel let task: AssistantTask var body: some View { diff --git a/iOSClient/Main/Collection Common/Cell/UIView+BlurVibrancy.swift b/iOSClient/Main/Collection Common/Cell/UIView+BlurVibrancy.swift index e48bf71263..d31c53262a 100644 --- a/iOSClient/Main/Collection Common/Cell/UIView+BlurVibrancy.swift +++ b/iOSClient/Main/Collection Common/Cell/UIView+BlurVibrancy.swift @@ -1,8 +1,6 @@ -// -// UIView+BlurVibrancy.swift -// -// Created by Xcode Assistant. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit import ObjectiveC @@ -85,65 +83,4 @@ public extension UIView { blurEffectView = blurView return blurView } - - /// Adds a vibrancy overlay tied to a blur view and returns the vibrancy effect view. - /// If `blurView` is `nil`, this method will use the previously added blur view or create a new one with default style. - /// The vibrancy view is added inside the blur view's contentView and pinned to its edges. - /// - /// - Parameters: - /// - using: The blur view to attach vibrancy to. If `nil`, uses/creates one. - /// - style: The `UIVibrancyEffectStyle` to use. Defaults to `.label`. - /// - insets: Edge insets to apply when pinning the vibrancy view. Defaults to `.zero`. - /// - Returns: The configured and inserted `UIVisualEffectView` for vibrancy, or `nil` if a blur effect could not be determined. - @discardableResult - func addVibrancyOverlay(using blurView: UIVisualEffectView? = nil, - style: UIVibrancyEffectStyle = .label, - insets: UIEdgeInsets = .zero) -> UIVisualEffectView? { - // Ensure we have a blur view - let blur: UIVisualEffectView - if let provided = blurView { - blur = provided - } else if let existing = blurEffectView { - blur = existing - } else { - // Create a default blur if none exists - blur = addBlurBackground() - } - - guard let blurEffect = blur.effect as? UIBlurEffect else { return nil } - - // Remove existing vibrancy (if any) that was previously added via this extension - if let existingVibrancy = vibrancyEffectView { - existingVibrancy.removeFromSuperview() - vibrancyEffectView = nil - } - - let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: style) - let vibrancyView = UIVisualEffectView(effect: vibrancyEffect) - vibrancyView.isUserInteractionEnabled = false - vibrancyView.translatesAutoresizingMaskIntoConstraints = false - - blur.contentView.addSubview(vibrancyView) - NSLayoutConstraint.activate([ - vibrancyView.leadingAnchor.constraint(equalTo: blur.contentView.leadingAnchor, constant: insets.left), - vibrancyView.trailingAnchor.constraint(equalTo: blur.contentView.trailingAnchor, constant: -insets.right), - vibrancyView.topAnchor.constraint(equalTo: blur.contentView.topAnchor, constant: insets.top), - vibrancyView.bottomAnchor.constraint(equalTo: blur.contentView.bottomAnchor, constant: -insets.bottom) - ]) - - vibrancyEffectView = vibrancyView - return vibrancyView - } - - /// Removes the blur and vibrancy effect views previously added via this extension. - func removeBlurAndVibrancy() { - if let vibrancy = vibrancyEffectView { - vibrancy.removeFromSuperview() - vibrancyEffectView = nil - } - if let blur = blurEffectView { - blur.removeFromSuperview() - blurEffectView = nil - } - } } diff --git a/iOSClient/Main/NCMainNavigationController.swift b/iOSClient/Main/NCMainNavigationController.swift index b23b133157..c3c552613a 100644 --- a/iOSClient/Main/NCMainNavigationController.swift +++ b/iOSClient/Main/NCMainNavigationController.swift @@ -80,8 +80,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController assistantButtonItem.title = NSLocalizedString("_assistant_", comment: "") assistantButtonItem.tintColor = NCBrandColor.shared.iconImageColor assistantButtonItem.primaryAction = UIAction(handler: { _ in - let assistant = NCAssistant() - .environmentObject(NCAssistantModel(controller: self.controller)) + let assistant = NCAssistant(assistantModel: NCAssistantModel(controller: self.controller), chatModel: NCAssistantChatModel(controller: self.controller), conversationsModel: NCAssistantChatConversationsModel(controller: self.controller)) let hostingController = UIHostingController(rootView: assistant) self.present(hostingController, animated: true, completion: nil) }) diff --git a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift index 52dfb9e3f7..0fe44f09b4 100644 --- a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift +++ b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift @@ -28,7 +28,6 @@ import SwiftUI import NextcloudKit class NCMoreAppSuggestionsCell: BaseNCMoreCell { - @IBOutlet weak var assistantView: UIStackView! @IBOutlet weak var talkView: UIStackView! @IBOutlet weak var notesView: UIStackView! @IBOutlet weak var moreAppsView: UIStackView! @@ -44,7 +43,6 @@ class NCMoreAppSuggestionsCell: BaseNCMoreCell { super.awakeFromNib() backgroundColor = .clear - assistantView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(assistantTapped(_:)))) talkView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(talkTapped(_:)))) notesView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(notesTapped(_:)))) moreAppsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(moreAppsTapped(_:)))) @@ -55,19 +53,9 @@ class NCMoreAppSuggestionsCell: BaseNCMoreCell { return } - assistantView.isHidden = !capabilities.assistantEnabled self.controller = controller } - @objc func assistantTapped(_ sender: Any?) { - if let viewController = self.window?.rootViewController { - let assistant = NCAssistant() - .environmentObject(NCAssistantModel(controller: self.controller)) - let hostingController = UIHostingController(rootView: assistant) - viewController.present(hostingController, animated: true, completion: nil) - } - } - @objc func talkTapped(_ sender: Any?) { guard let url = URL(string: NCGlobal.shared.talkSchemeUrl) else { return } diff --git a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.xib b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.xib index b3ec728c77..3bc0d1a9eb 100644 --- a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.xib +++ b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.xib @@ -1,9 +1,8 @@ - + - - + @@ -20,41 +19,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -62,7 +31,7 @@ - + - + @@ -92,7 +61,7 @@ - + - + @@ -122,7 +91,7 @@ - - @@ -163,7 +130,6 @@ - @@ -174,7 +140,6 @@ - diff --git a/iOSClient/More/NCMore.swift b/iOSClient/More/NCMore.swift index 43301a4fd4..57c3f016cc 100644 --- a/iOSClient/More/NCMore.swift +++ b/iOSClient/More/NCMore.swift @@ -407,11 +407,6 @@ class NCMore: UIViewController, UITableViewDelegate, UITableViewDataSource { alertController.addAction(actionYes) alertController.addAction(actionNo) self.present(alertController, animated: true, completion: nil) - } else if item.url == "openAssistant" { - let assistant = NCAssistant() - .environmentObject(NCAssistantModel(controller: self.controller)) - let hostingController = UIHostingController(rootView: assistant) - present(hostingController, animated: true, completion: nil) } else if item.url == "openSettings" { let settingsView = NCSettingsView(model: NCSettingsModel(controller: self.controller)) let settingsController = UIHostingController(rootView: settingsView) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 53259cb481..26990a62c5 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -711,12 +711,21 @@ You can stop it at any time, adjust the settings, and enable it again."; "_task_details_" = "Task details"; "_assistant_" = "Assistant"; "_new_task_" = "New %@ task"; -"_no_tasks_" = "No tasks in here"; -"_create_task_subtitle_" = "Use the + button to create one"; +"_no_tasks_" = "Hello there! What can I help you with today?"; +"_create_task_subtitle_" = "Use the + button to create a new task"; "_edit_task_" = "Edit %@ task"; "_no_types_" = "No provider found"; "_no_types_subtitle_" = "AI Providers need to be installed to use the Assistant."; - +"_assistant_ai_warning_" = "Output shown here is generated by AI. Make sure to always double-check."; +"_no_chat_subtitle_" = "Try sending a message to spark a conversation."; +"_retry_response_generation_" = "Retry response generation"; +"_conversations_" = "Conversations"; +"_new_conversation_" = "New conversation"; +"_type_message_" = "Type a message..."; +"_assistant_chat_" = "Assistant Chat"; +"_assistant_error_send_message_" = "Could not send message. Please try again."; +"_assistant_error_load_messages_" = "Could not load messages. Please try again."; +"_assistant_error_generate_response_" = "Could not generate response. Please try again."; // MARK: Client certificate "_no_client_cert_found_" = "The server is requesting a client certificate."; "_no_client_cert_found_desc_" = "Do you want to install a TLS client certificate? \n Note that the .p12 certificate must be installed on your device first by clicking on it and installing it as an Identitity Certificate Profile in Settings. The certificate MUST also have a password as that is a requirement by iOS."; From b7333a0dbfe7961a4faa7190bd627f662bc4db80 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 15:52:29 +0100 Subject: [PATCH 09/22] fix offline menu Signed-off-by: Marino Faggiana --- iOSClient/Menu/NCContextMenuMain.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index c57af4a957..383a587f28 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -47,10 +47,6 @@ class NCContextMenuMain: NSObject { let deleteMenu = buildDeleteMenu(metadata: metadata) - if !NCNetworking.shared.isOnline { - return UIMenu() - } - // Assemble final menu let baseChildren = [ UIMenu(title: "", options: .displayInline, children: mainActionsMenu), From 1db056d5e673fb4e61868f3e4b83741dcb7a346c Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:21:09 +0100 Subject: [PATCH 10/22] preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --- iOSClient/Main/Collection Common/NCCollectionViewCommon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 6b5b87b8e9..8076130d80 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -190,10 +190,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, let searchBar = searchController?.searchBar searchBar?.delegate = self searchBar?.autocapitalizationType = .none - searchBar?.backgroundImage = UIImage() navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.preferredSearchBarPlacement = .inline } // Cell From 8b20cfff4106bc6feeec06b80a3f26eaa7dee080 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 16 Feb 2026 17:41:09 +0100 Subject: [PATCH 11/22] Fix files-lock selection option (#3984) Signed-off-by: Milen Pivchev --- .../NCCollectionViewCommonSelectTabBar.swift | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift index d43da0c46a..f58f75dc19 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift @@ -31,6 +31,11 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { @Published var isSelectedEmpty = true @Published var metadatas: [tableMetadata] = [] + var isFilesLockCapabilityEnabled: Bool { + let capabilities = NCNetworking.shared.capabilities[controller?.account ?? ""] ?? NKCapabilities.Capabilities() + return !capabilities.filesLockVersion.isEmpty + } + init(controller: NCMainTabBarController? = nil, viewController: UIViewController, delegate: NCCollectionViewCommonSelectTabBarDelegate? = nil) { guard let controller else { return @@ -127,7 +132,7 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { } // else: file is not offline, continue } let capabilities = NCNetworking.shared.capabilities[controller?.account ?? ""] ?? NKCapabilities.Capabilities() - enableLock = !isAnyDirectory && canUnlock && !capabilities.filesLockVersion.isEmpty + enableLock = !isAnyDirectory && canUnlock && isFilesLockCapabilityEnabled } self.isSelectedEmpty = fileSelect.isEmpty } @@ -187,17 +192,18 @@ struct NCCollectionViewCommonSelectTabBarView: View { }) .disabled(!tabBarSelect.isAnyOffline && (!tabBarSelect.canSetAsOffline || tabBarSelect.isSelectedEmpty)) - Button(action: { - tabBarSelect.delegate?.lock(isAnyLocked: tabBarSelect.isAnyLocked) - }, label: { - Label(NSLocalizedString(tabBarSelect.isAnyLocked ? "_unlock_" : "_lock_", comment: ""), systemImage: tabBarSelect.isAnyLocked ? "lock.open" : "lock") - - if !tabBarSelect.enableLock { - Text(NSLocalizedString("_lock_no_permissions_selected_", comment: "")) - } - }) - .disabled(!tabBarSelect.enableLock || tabBarSelect.isSelectedEmpty) - + if tabBarSelect.isFilesLockCapabilityEnabled { + Button(action: { + tabBarSelect.delegate?.lock(isAnyLocked: tabBarSelect.isAnyLocked) + }, label: { + Label(NSLocalizedString(tabBarSelect.isAnyLocked ? "_unlock_" : "_lock_", comment: ""), systemImage: tabBarSelect.isAnyLocked ? "lock.open" : "lock") + + if !tabBarSelect.enableLock { + Text(NSLocalizedString("_lock_no_permissions_selected_", comment: "")) + } + }) + .disabled(!tabBarSelect.enableLock || tabBarSelect.isSelectedEmpty) + } Button(action: { tabBarSelect.delegate?.selectAll() }, label: { From c622aba6a067120a10cb9b3b2123a98931ae3267 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:37:29 +0100 Subject: [PATCH 12/22] Fix gui svg (#3989) * code Signed-off-by: Marino Faggiana * test Signed-off-by: Marino Faggiana * preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana Co-authored-by: Milen Pivchev --- iOSClient/Extensions/UIImage+Extension.swift | 11 ++-- iOSClient/Menu/NCContextMenuMain.swift | 7 +- iOSClient/Utility/NCSVGRenderer.swift | 67 +++++++++++++++++++- iOSClient/Utility/NCUtility+Image.swift | 5 +- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/iOSClient/Extensions/UIImage+Extension.swift b/iOSClient/Extensions/UIImage+Extension.swift index 0502b7f08a..40b8be463a 100644 --- a/iOSClient/Extensions/UIImage+Extension.swift +++ b/iOSClient/Extensions/UIImage+Extension.swift @@ -41,10 +41,13 @@ extension UIImage { } } - UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - self.draw(in: CGRect(origin: .zero, size: newSize)) - defer { UIGraphicsEndImageContext() } - return UIGraphicsGetImageFromCurrentImageContext() + let format = UIGraphicsImageRendererFormat.default() + format.opaque = false + format.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: newSize, format: format) + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: newSize)) + } } func fixedOrientation() -> UIImage? { diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 383a587f28..820a4c9c61 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -503,7 +503,12 @@ class NCContextMenuMain: NSObject { if let image = await NCUtility().convertSVGtoPNGWriteToUserData(serverUrl: metadata.urlBase + iconUrl, rewrite: false, account: metadata.account).image { - iconImage = image + if let image = image.withTintColor( + NCBrandColor.shared.iconImageColor, + renderingMode: .alwaysOriginal + ).resizeImage(size: CGSize(width: 20, height: 20)) { + iconImage = image + } } } diff --git a/iOSClient/Utility/NCSVGRenderer.swift b/iOSClient/Utility/NCSVGRenderer.swift index be88278e0c..95b7d75c7a 100644 --- a/iOSClient/Utility/NCSVGRenderer.swift +++ b/iOSClient/Utility/NCSVGRenderer.swift @@ -12,8 +12,10 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { private let utilityFileSystem = NCUtilityFileSystem() func renderSVGToUIImage(svgData: Data?, - size: CGSize = CGSize(width: 128, height: 128), - backgroundColor: UIColor = .clear) async throws -> UIImage? { + size: CGSize = CGSize(width: 256, height: 256), + backgroundColor: UIColor = .clear, + trimTransparentPixels: Bool = true, + alphaThreshold: UInt8 = 8) async throws -> UIImage? { guard let svgData else { return nil } @@ -78,9 +80,70 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { let scaled = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: targetSize)) } + + if trimTransparentPixels, + let trimmed = Self.trimTransparentPixels(in: scaled, alphaThreshold: alphaThreshold) { + return trimmed + } + return scaled } + private static func trimTransparentPixels(in image: UIImage, alphaThreshold: UInt8) -> UIImage? { + guard let cgImage = image.cgImage else { return nil } + + let width = cgImage.width + let height = cgImage.height + let bytesPerRow = width * 4 + let colorSpace = CGColorSpaceCreateDeviceRGB() + + guard let context = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ), let data = context.data else { + return nil + } + + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + let buffer = data.bindMemory(to: UInt8.self, capacity: width * height * 4) + var minX = width + var minY = height + var maxX = 0 + var maxY = 0 + var found = false + + for y in 0.. alphaThreshold { + found = true + if x < minX { minX = x } + if y < minY { minY = y } + if x > maxX { maxX = x } + if y > maxY { maxY = y } + } + } + } + + guard found else { return nil } + + let cropRect = CGRect( + x: minX, + y: minY, + width: maxX - minX + 1, + height: maxY - minY + 1 + ) + + guard let cropped = cgImage.cropping(to: cropRect) else { return nil } + return UIImage(cgImage: cropped, scale: image.scale, orientation: .up) + } + private func loadHTMLAsync(webView: WKWebView, html: String) async throws { // Cancel any in-flight load to avoid overlapping delegates/continuations webView.stopLoading() diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index 0f495107f4..583789726f 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -264,8 +264,9 @@ extension NCUtility { #if !EXTENSION func convertSVGtoPNGWriteToUserData(serverUrl: String, - size: CGFloat = 128, + size: CGFloat = 256, rewrite: Bool, + trimTransparentPixels: Bool = true, account: String, id: Int? = nil) async -> (image: UIImage?, id: Int?) { var serverUrl = serverUrl @@ -313,7 +314,7 @@ extension NCUtility { // is a SVG do { - let image = try await NCSVGRenderer().renderSVGToUIImage(svgData: data, size: CGSize(width: size, height: size)) + let image = try await NCSVGRenderer().renderSVGToUIImage(svgData: data, size: CGSize(width: size, height: size), trimTransparentPixels: trimTransparentPixels) guard let image, let pngImageData = image.pngData() else { return(nil, id) From 76b65865e651cdcbd4567654066a945ea9e71ef6 Mon Sep 17 00:00:00 2001 From: Nextcloud bot Date: Tue, 17 Feb 2026 02:55:38 +0000 Subject: [PATCH 13/22] fix(l10n): Update translations from Transifex Signed-off-by: Nextcloud bot --- .../af.lproj/Localizable.strings | Bin 92042 -> 93566 bytes .../an.lproj/Localizable.strings | Bin 91802 -> 93326 bytes .../ar.lproj/Localizable.strings | Bin 90852 -> 92366 bytes .../ast.lproj/Localizable.strings | Bin 92620 -> 94144 bytes .../az.lproj/Localizable.strings | Bin 92034 -> 93558 bytes .../be.lproj/Localizable.strings | Bin 92420 -> 93930 bytes .../bg_BG.lproj/Localizable.strings | Bin 94906 -> 96430 bytes .../bn_BD.lproj/Localizable.strings | Bin 91920 -> 93444 bytes .../br.lproj/Localizable.strings | Bin 93326 -> 94850 bytes .../bs.lproj/Localizable.strings | Bin 91922 -> 93446 bytes .../ca.lproj/Localizable.strings | Bin 94536 -> 96054 bytes .../cs-CZ.lproj/Localizable.strings | Bin 94808 -> 96310 bytes .../cy_GB.lproj/Localizable.strings | Bin 91902 -> 93426 bytes .../da.lproj/Localizable.strings | Bin 92588 -> 94080 bytes .../de.lproj/Localizable.strings | Bin 100774 -> 102248 bytes .../el.lproj/Localizable.strings | Bin 102244 -> 103702 bytes .../en-GB.lproj/Localizable.strings | Bin 91888 -> 93412 bytes .../eo.lproj/Localizable.strings | Bin 92188 -> 93712 bytes .../es-419.lproj/Localizable.strings | Bin 93110 -> 94634 bytes .../es-AR.lproj/Localizable.strings | Bin 92646 -> 94174 bytes .../es-CL.lproj/Localizable.strings | Bin 93334 -> 94858 bytes .../es-CO.lproj/Localizable.strings | Bin 93178 -> 94702 bytes .../es-CR.lproj/Localizable.strings | Bin 93194 -> 94718 bytes .../es-DO.lproj/Localizable.strings | Bin 93174 -> 94698 bytes .../es-EC.lproj/Localizable.strings | Bin 95322 -> 96850 bytes .../es-GT.lproj/Localizable.strings | Bin 93180 -> 94704 bytes .../es-HN.lproj/Localizable.strings | Bin 93090 -> 94614 bytes .../es-MX.lproj/Localizable.strings | Bin 93380 -> 94904 bytes .../es-NI.lproj/Localizable.strings | Bin 93096 -> 94620 bytes .../es-PA.lproj/Localizable.strings | Bin 93080 -> 94604 bytes .../es-PE.lproj/Localizable.strings | Bin 93070 -> 94594 bytes .../es-PR.lproj/Localizable.strings | Bin 93082 -> 94606 bytes .../es-PY.lproj/Localizable.strings | Bin 93106 -> 94630 bytes .../es-SV.lproj/Localizable.strings | Bin 93168 -> 94692 bytes .../es-UY.lproj/Localizable.strings | Bin 93090 -> 94614 bytes .../es.lproj/Localizable.strings | Bin 99534 -> 101052 bytes .../et.lproj/Localizable.strings | Bin 96780 -> 98280 bytes .../eu.lproj/Localizable.strings | Bin 95628 -> 97146 bytes .../fa.lproj/Localizable.strings | Bin 92534 -> 94048 bytes .../fi-FI.lproj/Localizable.strings | Bin 93160 -> 94682 bytes .../fo.lproj/Localizable.strings | Bin 91782 -> 93306 bytes .../fr.lproj/Localizable.strings | Bin 102658 -> 104176 bytes .../ga.lproj/Localizable.strings | Bin 99870 -> 101346 bytes .../gd.lproj/Localizable.strings | Bin 92684 -> 94208 bytes .../gl.lproj/Localizable.strings | Bin 98984 -> 100496 bytes .../he.lproj/Localizable.strings | Bin 91232 -> 92740 bytes .../hi_IN.lproj/Localizable.strings | Bin 91790 -> 93314 bytes .../hr.lproj/Localizable.strings | Bin 95808 -> 97274 bytes .../hsb.lproj/Localizable.strings | Bin 91792 -> 93316 bytes .../hu.lproj/Localizable.strings | Bin 95160 -> 96658 bytes .../hy.lproj/Localizable.strings | Bin 91934 -> 93458 bytes .../ia.lproj/Localizable.strings | Bin 92110 -> 93634 bytes .../id.lproj/Localizable.strings | Bin 92296 -> 93820 bytes .../ig.lproj/Localizable.strings | Bin 91766 -> 93290 bytes .../is.lproj/Localizable.strings | Bin 93808 -> 95310 bytes .../it.lproj/Localizable.strings | Bin 98946 -> 100448 bytes .../ja-JP.lproj/Localizable.strings | Bin 75022 -> 76556 bytes .../ka-GE.lproj/Localizable.strings | Bin 92778 -> 94302 bytes .../ka.lproj/Localizable.strings | Bin 91798 -> 93322 bytes .../kab.lproj/Localizable.strings | Bin 91794 -> 93318 bytes .../km.lproj/Localizable.strings | Bin 91900 -> 93424 bytes .../kn.lproj/Localizable.strings | Bin 92030 -> 93554 bytes .../ko.lproj/Localizable.strings | Bin 77872 -> 79372 bytes .../la.lproj/Localizable.strings | Bin 91780 -> 93304 bytes .../lb.lproj/Localizable.strings | Bin 92084 -> 93608 bytes .../lo.lproj/Localizable.strings | Bin 91540 -> 93064 bytes .../lt_LT.lproj/Localizable.strings | Bin 93120 -> 94642 bytes .../lv.lproj/Localizable.strings | Bin 92552 -> 94074 bytes .../mk.lproj/Localizable.strings | Bin 92554 -> 94070 bytes .../mn.lproj/Localizable.strings | Bin 92174 -> 93698 bytes .../mr.lproj/Localizable.strings | Bin 91744 -> 93268 bytes .../ms_MY.lproj/Localizable.strings | Bin 91812 -> 93336 bytes .../my.lproj/Localizable.strings | Bin 91820 -> 93344 bytes .../nb-NO.lproj/Localizable.strings | Bin 92848 -> 94354 bytes .../ne.lproj/Localizable.strings | Bin 91786 -> 93310 bytes .../nl.lproj/Localizable.strings | Bin 94226 -> 95744 bytes .../nn_NO.lproj/Localizable.strings | Bin 91816 -> 93340 bytes .../oc.lproj/Localizable.strings | Bin 92484 -> 94008 bytes .../pl.lproj/Localizable.strings | Bin 95550 -> 97062 bytes .../ps.lproj/Localizable.strings | Bin 91770 -> 93294 bytes .../pt-BR.lproj/Localizable.strings | Bin 97998 -> 99514 bytes .../pt-PT.lproj/Localizable.strings | Bin 93288 -> 94812 bytes .../ro.lproj/Localizable.strings | Bin 93084 -> 94608 bytes .../ru.lproj/Localizable.strings | Bin 96304 -> 97816 bytes .../sc.lproj/Localizable.strings | Bin 95334 -> 96850 bytes .../si.lproj/Localizable.strings | Bin 92266 -> 93790 bytes .../sk-SK.lproj/Localizable.strings | Bin 95442 -> 96962 bytes .../sl.lproj/Localizable.strings | Bin 93894 -> 95408 bytes .../sq.lproj/Localizable.strings | Bin 92576 -> 94090 bytes .../sr.lproj/Localizable.strings | Bin 95612 -> 97120 bytes .../sr@latin.lproj/Localizable.strings | Bin 91972 -> 93490 bytes .../sv.lproj/Localizable.strings | Bin 93520 -> 95032 bytes .../sw.lproj/Localizable.strings | Bin 95586 -> 97096 bytes .../ta.lproj/Localizable.strings | Bin 91970 -> 93494 bytes .../th_TH.lproj/Localizable.strings | Bin 91938 -> 93462 bytes .../tk.lproj/Localizable.strings | Bin 92180 -> 93704 bytes .../tr.lproj/Localizable.strings | Bin 95750 -> 97224 bytes .../ug.lproj/Localizable.strings | Bin 95276 -> 96732 bytes .../uk.lproj/Localizable.strings | Bin 96874 -> 98364 bytes .../ur_PK.lproj/Localizable.strings | Bin 91798 -> 93322 bytes .../uz.lproj/Localizable.strings | Bin 91932 -> 93456 bytes .../vi.lproj/Localizable.strings | Bin 92518 -> 94044 bytes .../zh-Hans.lproj/Localizable.strings | Bin 66000 -> 67532 bytes .../zh-Hant-TW.lproj/Localizable.strings | Bin 80648 -> 82146 bytes .../zh_HK.lproj/Localizable.strings | Bin 66508 -> 68058 bytes .../zu_ZA.lproj/Localizable.strings | Bin 91790 -> 93314 bytes 106 files changed, 0 insertions(+), 0 deletions(-) diff --git a/iOSClient/Supporting Files/af.lproj/Localizable.strings b/iOSClient/Supporting Files/af.lproj/Localizable.strings index 98442d0074e6e07054ab5a8e8d158cbc429a7068..b9315db5e39575acd18682f57e467a14539505fc 100644 GIT binary patch delta 967 zcmaJ=O=}ZT6nzhyG!DU*q)ESM^NgsJCb4lRDk|CN!ZHnoieTe39csv!luSyfn^bWn zBEns~wLd`+++?H)uHn9c!x5fX$JZuuHgLB@#bk1RU92K@Q(U3L zopC)CtumT|o`BrO_#V9{J6qHo*MYAfM` zrw(Lh?P*fr@nNq)LCxr7buJi{SOKRJ*Pt+m`zAD0@eAW@n0k)1Dfm60vO-2$H5IJD z=ZY+%IM+#D9v98)9}@SQ_U`MFut!7^zu=HE_KEz zxqF6eD@@ z6J)7;!HmN*1Fhkk6MNkb_L#?P(?Mod@7Z$XPCq2?vICG(U@Sg7X{24oNkb8D@1JD7 z%m5jEImqkxwoJDBX@YB5LOsEO{+}+0p#DNIRhsp5xD)g}Go^Q2{Flxz%3JYYRQlZ} delta 59 zcmV-B0L1_P+69Ww1%R{x+)*_)Vc=TP$O#EF(MwcTXOUuHT0r^*ewbLJ;coq+K_cqvW+DdZ4^8f z|;`{YPe7&fg{XW&>Zc=4<`N&v&XHGc}G)3TO0#z6X z{b+DR*e`-TG(AdZHLz(agHKZ)-MN^H;BVkWaN0)8c3&Ft0I1TPAXQEGWyESU1%<*? zp~gOPIrlJibX$7KfRy>&_vyKt0287Ji6P4C_%CAbW86h7!snX?2ZRx9@=?|3XH`HM z(mmw1DB|gKn!jAMGh(-7%9&u7hDGx0!Z^EIW1O5yoYF?laPE#F{I;6r!dY8IJo7Qf zPh6Wn^c-G#VOwI?Dz|%=;x$htRfT)K4113fRtKat?*2~ktE~ikeXd5}*}xni732=xISq->p84ePPzv9+-;@v6kmGaCF2+ z?mkYj{p)mGD5~SPhe74F`hX~n7#|^$1>j|jEP9gnxtnTGxr>UweoF5UHHV2tpId3q Z4g8OA6k*RD_EnzbZ*QEu}w)L#|x)r9~6 delta 63 zcmeCX$vW#SYr_`CR{@h1=CbjZFeEY*Gh_p?0z)Q4-ektTqSJi>8LL?H8S)rXx8Dq8 SoXI$y%ac)LyGbNtlrR9GxD!(V diff --git a/iOSClient/Supporting Files/ar.lproj/Localizable.strings b/iOSClient/Supporting Files/ar.lproj/Localizable.strings index c307c24ddaa224a4910e81952f6d70cf74421cff..3ada0df3657712fd411ffed0a2a196d56ae07658 100644 GIT binary patch delta 1128 zcmaJ=&ubGw6n=vwwh}~5s!71GUC{b7bUjE#L`po^Kx2_g!OOZyN)u8yA-gpMgsN9h z`VMoG96Wf^i;zPLp1f-NA9(Ok!Gm5r`@Lxv%BGYsGdnZyec$)KH~INB@ohKp;Gx%oJ?2qU zJFZHzbt?1rcyjZBLF|~>LUaR|JtI0Q$%4!PXlp&u3_0QF*RnYgEeUcU6mC>D zJNMGO(@1i`PR=~UO*j$~&p2Qd&pT7RXQ%kjR*L`C@_Of%%J8M_vpiQ%@!Y2r?`_>c z6DBLblZMrJX8Zi4R3D3;A}>}* z>q3<8W45TY@8*7VS`6r-(k}Q$cIv;bI>!TNqEy1g20A2NmR3l5HU&)6GRdt z&ZA5E^{mS24`r3vJV+&f84tK_tX~06Y~h!ph o_=|R-I^O=H6~z&(F@%^m`N*6|nt8zH-Rmz4YHCUL`CSOib^l>Pzq+T$8?))n~?3M?52jED(Jn! zLk6KoPo63WJq3F3*h}yrDC$x0(*6bF_a+I3jVv>>^X7Zs_ue;i@;Pz*Gx6f1Dmtqr z^2o>6BAeP&$1cRkVYiB%iYOhRKxt7S({=rH8!#X-022 z%Fnuh1W4*&ZR0Sa=Ft~Ny;VA2R@3KPYVdyMCTCLT_4}tPy=Mba2T{jJ4hym&5`ke8 zOu`nL(mcr7v<^X=9;3V>)@7VDfe@Y@(@eiG8*v}3Ql2o?O!@1Gc~n4)qEw~kEJ*US zNGZ$}NVPD_A)L@#j+)nJmdY%LMlwQ50HOxwO;Ad!TZl#YTvO^$I)Y;^ifIl|RqzJ% z9N3T|?rm87x{#g|=LIQe!d>~-E`NAw@sCHTYi(pmyK(G@HLSi~;&i7DKc*Fwkh;=_S>Y&dsZ=HS{&+ zCVxM@t*@8W)V)y-dAcqxp%Sw`LL?9I2N-!8Fb~0T=5MEW4fEnCbNsDU#!Meq$iU*5}V3 delta 67 zcmX?bpY_aT)`l&NdLh#v=rOYKmoOwU6fwOTL*C?r4Wg509pRfkFN86NHJ>4m XA$7ZKDC11V$*b0gY_Eu6^biIB8}k;W diff --git a/iOSClient/Supporting Files/az.lproj/Localizable.strings b/iOSClient/Supporting Files/az.lproj/Localizable.strings index 957afe380da7d34c39cfafdb1c72d3d56ff3bdab..ceba3dd3d94c28fed047ccfa131e66b79dc616aa 100644 GIT binary patch delta 942 zcmaJ=L2DC16n=w7S6L*Qlvbp=sR|9&fG0gDt@dJ-$fN`dUg9QM+K_Y;vRgy(Pzids zyu;vM@Zv@2B_O>h2qJj&FL)3^J^Q^$Lc^+M+04$H?|t9<-n{fbr+$A;J$tLZl-BN2 zKrK9N@+hVa?7Db!*sWpbBj!;JYlw9PuOM2~!Pho&wy^f7VKXJaO?6;BsTUa^ z#~~7WSYsSS)ZBZzs$MDcUaOfSJ~eo=n&X!@T=PIHcgF)_10+2wH!jA5Km>+$FbP}N zmK8wCqbkHax{cBbSc^Dm0wFwG*2u6Z5AhaQr8Hrx+tRCu1#}hmM5#ti^S!L5Qr5LO zy{huu(WjQn;1P`om=Ku_{H|j!-Zl}7@c6d2u~vlk_{eIGP&M`k)BJkZHO?L7o|FCx zqzxh=tsEHR#r`yhpH5zkN1{pWL=$-b+LRhn&p*7x`iaAj2Qz&0zQfPoq`5xG^1c3f z)~;hZC3Va~XL(5SQUPoQZvbupWw*gn Q0hs}l6iF1f99jWd6Pz9teEZ`k=hm88vpqB->j(uHrmX(7o*i6tvat4JGQi_P zE+YcRk5i(vz-Z7iI1Rds2vay0Kv%(w;I#aFg4hh`t3Z_q1!=|4U>Q1#u1+FNp(?>O zMG3k=I`$%{YS`rsbl!eWP4lm0G?N3aaJq1DfMjDYLpHHqKRq0uk z0PN69*fz-JPtQiZLRt-vOZmlA9tFJEPuSZms~G>bkMgl?J=yH>hs_7-!>gT>e4#tW zdVZ4YU-dIH+BNN_c1xSp=C#2DEn4Gb_c||jqC>bNz7pHrI2+$Iw62X{EPnU+65n}r zB9RN^Lo-dGa4GUOGU{B!FBnf;p5nK2(bG~5>7ERN<99e66g04oNevoExq)jiBP8I`i({wD%^XPAv_B$zYpsFNT`pLX!aK99Du3qv?D8L&KlH ykG|hO>+`G+@`Q)7h*ZLT{6Ak9!8m{*qrK=4tNcwC|JjRs_vh8f*2kx+5c>-j|JUvS delta 63 zcmaELm$l^*Yr_^sr{Kv+^MnM+TS7rFf$mwweYoxnhn?v)l8+-H)#dAn{-bni|RFh zxNc4Q2zXTI!ROe{UQxYL`d2|E&N@`n>oaPOzn+h5O@?*1psW`*GS>joF?1s_HV8Zr zt%BM$0!3C8R5q1CW>Xf@(y$M~RmUH}*)WZqh_VrH09B+3(u$E@MyyJUXiu0bRM!hn zRV-w>*4d)E!Fz{Oxjb-$!$XdX${OCQ@Ex3IhcgxG5BhOlN?7{s zlCmz!d?lp`AOS7?ZH(7@ah}SAx15Q~{*X7AJ~ju=_F}wy#o{l0OaI8IX>Kjf@_s)N z^C(5PDS~&&jI0BDBI9{)UdFxsSe;W6nd9!0iI9<m0@I$%py*WL`WdJFUqy2A$SN z1+@^8xj8%tU)B59R3z@1HOabNtjDID{3hSzM&(+y(Q+F?hLhy?2Q$2#3E#|_Z8Xe2 zU3^RNXxf1p>u+~el;0d*(!UGpLU9~buCN1)QeZ0{A}Ju=!AU`sT-txRMwJ0BetCS$ k&@{(sf|sy_mV*2IKV1;P4kC=2oYg;DYI^5UNtL620QX_u-~a#s delta 70 zcmZ4Yk#*No)`l&NOtF(Ubg+q+FeEY*Gh_p?0z)Q49+1pnNM$IR{83PPdQmK64r@M8 XFm?OKSjL%*lRY+yZ1+iJOcDkFDC8C? diff --git a/iOSClient/Supporting Files/bn_BD.lproj/Localizable.strings b/iOSClient/Supporting Files/bn_BD.lproj/Localizable.strings index ac713477257e3f18face9e37d1103d4d9a55cbc7..4f0d69d4d0149a3751848fa85bb52712c9ab61ac 100644 GIT binary patch delta 953 zcmaJ$AsZ`V z_oa97aHAXdx)8c(e}mesxX6!i;i`DAW}ny;IzYmPMfpfuLD(D6r{~?gbnyAw16>%sZ6yD zK=O2plGq(kHL*(&YUuYxb(PcWsnrs2h0{k(14vcuTR6+i&*Ag&xnZw;ZXdF_sH#O- zWdJwm1#&&|_2M%%$+lAD8XMLpwlY+iZysIZ>raLf0p@G5`j+F?cs5s( z6WTdz!U6;<=oMSdBto6|*to*!`@{O3t<)Z*@&LSzl}A7SQuQliROJ6f)*K{@ aKST#J#qaL_q;Ui*ju4S&4n90ob@Mk3htfI# delta 59 zcmV-B0L1@<+69o#1%R{xU{RCMf(8e40AT=g0Be(JogI^S5e}EoQ2}fPZvbupWw%dK R0hs}pI%@$Gx2jnIS`&s(6bb+U diff --git a/iOSClient/Supporting Files/br.lproj/Localizable.strings b/iOSClient/Supporting Files/br.lproj/Localizable.strings index 4e00dedbbef86aa7f94d65efc90555ae76432819..c79fa6d5d3b3aeafeb8434876bf30a11cb8be079 100644 GIT binary patch delta 927 zcmah{L2DC16n=wANy8%8QcV=hbgN#Zn^t=%BDM5j6qG__g9nM*O^Bu2Y}idGfkJ~& z@Klg@n43SqgP;iYB8xx4TfGP(UcA|}px;azj9Wp5+1YvX-uHd)o7wx6efljscq_iU zwF)&Uzz9iFm!4o3VNPRL!_GrYl8?2G^$w=a2&jXiUox$nDb-m(LWH zT@%5$hm#LXFfg9jqbNq*?N9L*=8? zA)9Vc9^X2sLVSIk!3_2Ox+wGN`TSC37-OTQ0cCCI()f}#5sNWAqjbm|qc;y)Hjc75 zOp07-St@^9SQoVAIw1s%hOGZ-lD~XC$Df~#apy{ArV9YwXgYty6#lV0#RoGMZzmQX zxR)R{$B&&cewR$BmyWQM(-JuzyvcAkxuyoLIL)1;z(04bY#$Pu?9CO~`E`+HHCHl` zfA;0TBu6b*MJa2%Q6C?O%SyVUFfM83*e!iLsc$MmYZ2Ny_6r=Pt1HI+z}%lUS--TQ zpD7pl!(mz7t%|dcM#1!>^-x|mKR=4?B9AQ?oa*(+4Rz; delta 68 zcmZpA%G!66wP6e6tFX!YTG%8@7!nzZ8M1*`fgzJ24@hP(q%ssSq)vAXXKbBr;=?Gg V{Yf}uALH~>$&4b~P2w5-gaHKa6$bzS diff --git a/iOSClient/Supporting Files/bs.lproj/Localizable.strings b/iOSClient/Supporting Files/bs.lproj/Localizable.strings index eeed23d60ea1c957a22cb6343f0f8a3691dd8db5..8b99feab2d1b934547ff382ebd28a84defa4e25c 100644 GIT binary patch delta 932 zcmaJ=L2DCH5T2)|)))&W*i>!kOAmrbQ|z^fExp)-WLJX~J%mlVv>|a@vRgwA5)~B$ z58{ANFQPY3dJ!tLcQ2*%EC~LA+&w7x&1+)YRb+XYoq03gH{a~!#dhM!*Tnr#s^TtO zA)gv}R!CDs%kY{wd3X!(Jj66rVFTEC9LZ?VD&E?tS%Ym+%~neN3N0eHO_$9BS3S;n zxDHUzf{kzyQhojSHT7DV8;%+|>`~498d5oa`##Ce^q~1(RLPD;+A@$@mb2fH21^K- zCBO+})Ar_rPg4bqnr6{o0ro0x>c|kBHLGT~GmUrysM4PxE!zGoi1{=Djlxu=y6KeE zsf3ko-nr^L>k~u85^#hgL{5Op8h)4Hi@-aGg?K#MTc0b$j677;ds$Vm1#}m=O$zy& zpEa*$RqCt^TT3v+hj4g1XkyvuI&CmNe0x4V<( zrlW>$?+27M^-x}p4Lw9;9AKAl#?g-~=0Tc%JFFEQ2}fPZvbupWw%jM Q0hs}l8c7tltXTnC6QU9oH2?qr diff --git a/iOSClient/Supporting Files/ca.lproj/Localizable.strings b/iOSClient/Supporting Files/ca.lproj/Localizable.strings index 43d4d6f9245e6d75459f8fd9d6b6f7c2732086a6..2385b4a3b578f1240c8aae011708f631154e4a17 100644 GIT binary patch delta 923 zcmZuvO=}ZT6ul2gt#Q=Y^dkw1lZXwi7_BagXvs!Fp^HLcQW>UcOhcxbFq5{#jX_*W zq29$?i+?~t?LN2=6vp+;i?d?|nH+eEON_eNe9|s}HC~ zE*_6e>d+=4A8#JfDk29tld9NT*dO8*giCFFby2g8eU}+?XD>l=0l80Q?>pMJ?*Z1b=|I4iX{q z!>(#jGHD%JCM}_}686XV(*#0zw!=A&doq!C!77~z(?;0&I&w9-f$2o4O3fTd3baT` ztTv=Ptnw2k=&uzu$EoS_O8?h-J(zQZCEf5bxLTX2Y>O)VZx#y1Y-cs^vXQ&&8kNCpBZ%^@jKf^!1&+=f9pg3SfB+)SWuniNZ>CChtt7PXw zT$H;Q|Ah)-*ZB>i2wfH2TZOLNc>4DxA%+T>ytaeIQB1)D)l><;!-Eb-`452pcQwz2oAqa!7LlUnc{P$k%^sfVkN z$%A_YY|sME4FvhxRUjoq2!vF~#M{DhaO&%K3mEp^)HAiOf*VyeJ7rNPI6JE@viUU4 z?=z{P;Po`B0pATE_O;w783O`6V66jFaNUrp1zQ7R4Z4fe%Ge*^#nu`;;EAS4p@pRT zcpYe@mQK9^TbmZskS|nCviYEpot7I?33hAh3g4+@s%tlI~HPx4z_g z^c4!`=89{7ZO0~GMhnWpwW=y4bqRTQ;SwK?QhBM5bkIjl9DEI02}>-_St*K(++A>n zB?f1qvI5&SS|n|DaF)QR1@Xd5^G)4O88_W_{&6%DELYWq;r|5kN3j4|k9E$2r38>$ zSS9G2q>duv)SjNa7n0QWkZSf4-a9 K`}JJC$o>HldhZbc delta 99 zcmdn?f%V1{)`l&NZZVS^7AbN4V#sF5XUJiwWSA~s#V9d-TMVPw{Q8{x@g?{0ox1KW zUZ#LTj5X5KrxomC%o=u!*!hTQYGaMCUcr=%knZAdfSPrz8`QOxQolwW08 ztdEYFC$0+lYNMF%<0PTp)=yWxQ0BFxDu;aPvUj9mc5bL+4>V3!fUse?hMj1jCtz6y zZ)CfZo&xY_S^|@%dGyu9>Hxoo6Tw-xD*kk&5f6bXeF;*>_PvByK(o*$Ol|7%e6^I9 z9q`RlSIzLw-h>ldiUcVU3cL6&1H#8#MJ&PaZEHiS1lIT<@12A%zGay{QlEv*4xwO%#13U zm%b_>dXB$7D&zx*$0c%O`y{`5Jf0hXH{jjY6yHBQ%{TLfs*R)_|F5eFYnx`%QPrIF zhTXTP_*XyY5+?{cB&1}?00N52KqU|^jVJ;{aFeL5lDd)Y5@n%b zV?}Q;8)9cf>VPW2z`#^hWnn7(2h0qJ?_FcjIs>xKclYjn-}~N=_xp+7L8A9T{aIYP zO$}<{+aQYqTE)%7%HpI$Bkv*@6xqG%KJJN>8hAK#_#(;(rqv6tUmMOA{W#v#Ikv)NaAv0;^;o zOm&^%GHeaHltLLKRE?VaIb)ub8_E;zT~;%pTT#RFLMNf?5aL3H&{?z&n~%@Y!lFPv zQgINv+4rg;luOUy>ygh>6BGQaa4IdW5Knm%?rM}Azr1Dg?017->>Jbf@e%+hDaH{; z%E9kS<6PdEMa7PnSUPO;mqI>#Vym&Oqm(k{4oIr8uK2Lz0K0~j zgB}^|HClVM%wmZcz+6e;yg!mns diff --git a/iOSClient/Supporting Files/de.lproj/Localizable.strings b/iOSClient/Supporting Files/de.lproj/Localizable.strings index 7c42fa6b55cde7ed287c91baa67508d755f45bfb..0238b09d4311119c95921d0df3da5081b946010e 100644 GIT binary patch delta 1124 zcmaJ=O=}ZT6ul3}F{TT(W6_|{X|xuRrgReokt$tPDb$jp;3B?!(8k0`oHsFKA$8$e zgo_MrT=o};F5S72`~Y!b-T5;-_hkm;waW13&Aad3bI$#k1NMJ^@LzpYH{+c)_3-p5 zq=fdUO)=I2P7iU?u!Ypb9%0|W5|17Y@HIxv5c`NaHd5mI)PnDr*7)3PDSaKQ>c$Qk zgyi5M-N1JPfr)h&SP2snAr?aM)>sj~M)qCbpw4NL@Ukslz{JmUwHnurE*p z3mC#}4`3s!HqRx5J_DFtzzJlWk+fiIKx;^Mky!=%CT_Zx-T+VJe$L3x^DQz)R6{$-nZ54TSd65s#6w=;vRSaYzWj5ER1(w2F-$2R|LthNa ztjM4&@nO|p6Wv3!O0v@6um<<#zsV|Y2pWU3}`9pqlg%CAvIhj*SD#L+%A>K&V=IO z!V}WSkC7!^!FL5!fy-q~u3-jdWj4-Mcv|=h-MIor)4b{D_P&SWy|CA{D5MlMOfNfS zZe`LNiw>~kR*B>lK!#3m+>V4q4X|Z^GO!;})ex&duB6*&&cpiwPMS`H1|ExwB`UNX z;g?ic*h?G)rSx%b;pwThAUJxqpT``TeOZ`-{l6t;E36uDV$@`T)+XQDF1HT^qP4ySxQem zMm&gQB=Zm0WKU*A-rOHd*up(AZsdov)ARqJ)_?Sk*_k_on`Zx5=E&>RZc5Te^Ky{S zvMT|a?@--{8)wYcw-aWwf4aCdjQ!OF51x8tuWB)Q*m-Gs-X$Kq?fYVUOw#<&;9<%s z_QZ!sA5@XUpt?|E;cJ)|&zuLt7dxIn(s#ZIk9f9lopUe;`za%G*3;tr#z6-D<0xR3 z6O+@36j3*arwE0k+;L@+Tf2LhV2?({w?f&^8F6$}N%C%&QIos~|5ubmkRu2QUk?2; MM$GQ*cE#J=PlZ0|GXMYp delta 116 zcmV-)0E_>Yss`kq27t5y{&tgqkQx`Y1Hl8c1F-|c1Fr+M1HuC!0JQ_O1J0At5gC_m zcL6q+U=#rqm#!fJ43j_@1QogizXQbsyaT`kuLB?eD*zw>vje%4XQ(FzyaTTTu>+H^ W!W6dxcmcWrmnxG16t{SP0iG4E&M0;O diff --git a/iOSClient/Supporting Files/en-GB.lproj/Localizable.strings b/iOSClient/Supporting Files/en-GB.lproj/Localizable.strings index e69532e62a928aea957dc4787bc69af3e6495094..2738d961791a6a0d81a6dcd54af1ddc1dc9d3a21 100644 GIT binary patch delta 907 zcmaJ=O=uHQ5T1v|#x-c$B>jmdJUj~CJcuCl5Yb~Via!UzyB7~ayn3nMtO>STkmbF%Z+7PUX1_$sZ#w?#LM z>!`<^nu-n7VeqN6`@>OBmAPcAv10`)^1`Vpw&$bf?Sit}8XlVnZU!{{Bs7415Nv?5 z5+o++c%aa<3KC7XkWvok9dMNJMQ|EH0e_Rxu$O@Q9%WIMFs)H31&}md zBMZ9=sw#H*2s+bt)HLg%)5%5P3a5{l3XqD}H+X+%G%00^eG36K1POrquP81&9OWnJ6MS*y_+-6D681kN0{>c$^Ow#TKi|8+-E53gZ-$5b zLlt<`{)@c2dM-0q{hsvQ3V>Ojoq delta 59 zcmV-B0L1^~*#+>;1%R{xKv9#>+6D)70AT=g0Be(pogI^o!VZ_PQ2}fPZvbupWw$R< R0hs}p8fyU*w~AQ-S`*I}6=whd diff --git a/iOSClient/Supporting Files/eo.lproj/Localizable.strings b/iOSClient/Supporting Files/eo.lproj/Localizable.strings index 8adc86a084f70ab1932b81c5074c1bdaf1e860eb..dcf3f284fdf64aed3fac9bef904515d96066f777 100644 GIT binary patch delta 927 zcmaJ$tLkJfhwQ?3S3G;Lw9d z;T?v9!Du`iOib{w2g1ptM-L`?_E-45p$&C4W-~kcX5ah1_kHtTe|27cceXysXklqi zgd)Pz5L&dvQ$$TH7ts=;0B2gPV6S7pi^Um{SjSfzH5=Gph^npR`i59VZd**#^B*JT z{ep~W{faWJMVY1L{egq4i?Sol15Xa03PhFOR9qT#mHAncYEz3C=ndd8SFaNd^ce9P zc*t!$+AJA3#hJBdLzkD&A{|1FUB_i}3`uxgJ>za{^S=jUif-rC zC|k?_8$e;i@`)_k{&tb}if3pmIX%_tvZnLinRLQ@lZx;|0y=ERLzYjk9c)`l&NIYE;T@Uw}RFeEY*Gh_p?0z)Q49+1pnNM$IR{PCXj^gBU}In$50 cGYU+O*}%2EAega*ak^FlqsaDcQH)W-0GGHJM*si- diff --git a/iOSClient/Supporting Files/es-419.lproj/Localizable.strings b/iOSClient/Supporting Files/es-419.lproj/Localizable.strings index 3dfde505da232beaa46b9cd673747907d907ddbb..6e711ce883a7782eccf64eeec81bb14ff8413962 100644 GIT binary patch delta 908 zcmZuvL2DC16rNFc8_S|0yKNM)-L$n*%BJnD7nN891zS*(t)LjDo23mKH#VD6;>8N- z#e-PhVLkR-P^5@G<`0N?^D21rD4z7(``-8F<5%tAySDRzpL17l zGoLl^G?~rXYy+njRsp9~oCIPv^RS25?_kM{278LH4r;crZ?n3pl=V$kLvDxNh#i;j z(&wwjP#b}W1+;fKa`egNT}~^39%m{%cUrJIiBXewvKGA`(&Lw9ZalY>Abtw8;B8NRw0!(9q_WJUw&9V!gtx j*?yw*ZL1EBf=V~dg)~LF=EV5WlS9AW=8kF)_0w85S>dK8|vFgXw!t&+{Qw|(rAhs5nDnx6)Aosn2qp!O1yZh0sk2y6_jcbD=xki9bNZGq;JvXDN?+@0+D-7NpT~9{@!V-SH~UF5e@@r?DHT5Ari z%Y`dNm5JGoasKd<2ngS2)MV1w3Cas-Vse@`hfS6Egiy9X-$L=jrh+uHvI$=YBa$)8 zEF^K%^hm3@@I1j=Kj*`1MK$Y=1InZJ0O3YvW8ldG@FrFkdj6*xWk#sbopG{YoGki4 YUDOmE@7bhL1RX^flBbWJJy6xeA5zuTXaE2J delta 57 zcmV-90LK5`-v#E=1%R{xK2($7p9TkX0AT=g0Be)+pB)w>VV+ PjscVCNfft?Tme!Oxn34n diff --git a/iOSClient/Supporting Files/es-CL.lproj/Localizable.strings b/iOSClient/Supporting Files/es-CL.lproj/Localizable.strings index 813d39b8e9b5807a00fc3833a66c4fe5db0b6ba6..7ed7a063cf0b5231a8cc9d587d02405e9b8143a1 100644 GIT binary patch delta 941 zcmaJ=O=uHA6rMr11|1ZeN>ZeB609g~0y&EKQ+lupN)IM$dkNcSZ9~#c&8CESX+7A3 zc#uADD1xGQJqbOoM-N^EJ$Uk>9(t*G^XB&^jjd}b%k1pDdGGt)_ukv5Uz|tZoz=JU zSIM0ezNq7A2rXJ-0jDNb1}7IM4>m34um{+$W3fkFEa9tzm}Ts3QMHjA-w+k}cElBP z(v=Ts=A1gxf*};O&1bIMkmg5GrVn|dN`*>>uHIKBC`z@W;jw_=wx#JMp#dC%U>=l# zO^nm=L7~MgNVFJ7N_p%z!BN8v<1AYN-DEWEb)a%ej8w5H&%)-59Aq)moT!--GxC&U z)tZ7U&(mu5`1oz$Fh>ZV0FhO^=W+J17GVqVcs92lSBSEBh^lqH=w&oWN7KqYz9!Xa zt|~h;0K<^wU0L+tTZ*1<_pM(%oN4W_#@+u`fj-_%(Va7j-b9K@AI{Rxq@t~;-@MOD z<#3tW^dh>jzLqdgO0th$MQIvaQ%(omzPVSGDVqA6Aib!Et!ZCHc~muTz82-M!$tDQ zdp$z$jwJ?JG&hBt8sM$P1mBC6bx!i|G$FAGc{_FMQeOU)C`{Pm*KPX(d~SPLgf fVA?o>{tv;bwL{6jici0DL)5&{zd1QAZzcZ#XjRuB delta 57 zcmV-90LK4{Y@%1_yKiVE}UgYm-T#9g~a^4wqtA0c-_t0B!(fx8YX- PnE{i8t`xUCUja@NSacJ+ diff --git a/iOSClient/Supporting Files/es-CO.lproj/Localizable.strings b/iOSClient/Supporting Files/es-CO.lproj/Localizable.strings index 094e6f64643caf69c12f0c612759bfed7b1a78d2..17618356e98f5d2cd3d532ffa3346142e1bbbd26 100644 GIT binary patch delta 943 zcmah|&ubGw6rRy!6G9Miv2Bo&X{kS=bWM&Pq?AIbh@rI2){De#yR;#36L(WWPpzPL z%R3C-^ybln(3|NW;Gv-4p+|2b9&%Jszc+0#ZVR%^&c2!VzVChSz5V_v`R#LZ=dE0D zm*<2p8hDyQiMH5)6=IFST88D}Oo>(O0rops?9mV{d~G3S6MIM0OeDuQ#TtCK#FT#Q z%4fER>i_{A>}^~`qQ3XTl`o{8t;mtHo~V&`{;DoslWBdhBs1FzE*n7U7`}cs3Jeh- z)&a$Ep~=e!ml7*rQ(_vqIoR*vrVbCr*)&RyvQjv20F`rNq&1WK3eJ2n34P48D(Y0q z+bOo7TkpEEKnK^)O)mk6Ng_A}2(9704x80I!dZmJGs*SIA{59&RQG!)^^d@D|&Xu$Y$sT4>ZK!GqpPrNpR9qa&8|zeE4vOpO zsfx@eO?7CuaG4(3=^RVv-f(jRyp@m>ClG6Nz5dKZfq-Y zBknHvmePMvM5w#$+_-V+!bPFKz^p|*_cg&d6=Znx=G}MiIrrRq=k?dv&%@aE2U&FU z_k}O2cxpn4rr5x#fpre2JWd{LN|dk%*dJiAM^)7EwTYM(_O>XSNRF?GHTZ6dyL!i! zuN)880Rr0Co45!?h2Fg%+gEwnk=o74$rGL^)AQ4p^ulFn>4RmN+){Ac08-m<4l+_; z2?4VXIEHMP+%_&{t!16crZ@Os5#0^!CnO_=f_BECjTOAzL?}rmKawAULsgv|&K0c+4hGFlaD$q`6j7GbY^y%|OT9{7K z?ei9`c5Qt#Bkh=}J)P>_qScP|)|GMN`lgdiG@+(0e9A64^x|5a_I^#%UcyQn>pyj6 zP~41uoRjI8sSfSVUZA>TrC2)mh8rB_ZG%>(8 z2M;P7sVn)tt9m6T$Da-Z^4xj|FGQLU!ZHJ}J6JQwhbQ%Knn7fgrqsJ`XtsuFqC;0W j){_6xMG=lSl}C5U3?u(1KDByN`sE`zy8m%SK5>2nduG-k delta 58 zcmV-A0LB0Q;{}S?1%R{xVpfyzga!w60AT=g0Be)MpdFKX5e}EqRsn1UZvbupWw%jR Q0hs}l=t&f}tX=_96Rfors{jB1 diff --git a/iOSClient/Supporting Files/es-DO.lproj/Localizable.strings b/iOSClient/Supporting Files/es-DO.lproj/Localizable.strings index 3bbd81e3ecda4f8fa0d4bb93200430341c437816..b7ec834e0194b23b9a83b4cbe73bb5703afc186e 100644 GIT binary patch delta 940 zcmah{O=}ZT6ul2k8)_HP!A@ha83R&UO-kGr(O3~(Xc0{6MyAtAY(l1!Fq0CxvKw8s zpx(tpe}~Xz{Q=#$@&~w)F1yg3cJ!;|t zWKwHD&Y=wmI#fY=CD_NfX#*iV+oqU7Vh;QPSS3ASs+;sT;A>D3C5n@p&e-kyMNhc@S?8Q9f4=Z9cN6_4fyAIwvg_ zNC!kh>i(^Z^S;f$$`jnpp6AcSiJW6<7)Sr<0+(Of%24a$qPoQ0n^*Ym;q2v@3bag9 z*j>}CKI|ZaMSqUhU#BMr;}GA~3xevQzAiQNm!`7yTn=2BU?y*TXVzIlt4n&hs`4q* zD}J|do!6~&R$3vGl1Ycy9a6?Zn!I-_ePhK;Brr1#@h{P%mM0p` zPfwQg{i>RIG0rHj*h9FRm`V&!0c5wa3P?!a?Y}ak$fV5bN!iGB#$}Rk&^d_m1v9x%L z0;=O_kV9=+!>NTejng7dK4K14u!q=hV`)U4nt0np&N}uERZXVOZ_o;`n^aKq1-51P za6o-t;=1Jn7b3HRy$wV}wY_4QpR;=A^28~hs_J2rEqNJ?sNG>sE63&ZmIK@xI6Fo? zuFZj71fEs!Y0j3ZGJvu}%g}ac9+l>?7xASAgyvZ{ef4T|5U+z(SE`v-Oef2T1#|`7 zYNZO*)V|Ml%9x<$Jf4w^J0zud0|l)S0TUv#ir-b7b%zfSi}3iSwpc5ITRzllC#Z@X zAB@Y-@wB>hm(y8ow}v!8q)E$hW3qqKlG);*gpUWs%b%TeObh+kJ`?1_HJc5!+PTi7 zvVHD?d_9=pNFBRu^}fjaqcJHBk7Vz_j(|2y(G{b-fpd!@DSP(mHf(89Airj_>TQlQ zvTdKAzSj%>v(r4>&Z{RbPo=bZzOZ*uUS)>V)BBt`L3!R>r2&JjZ z3+58)<{Cr1>%ognPFO2nx-)9Wt;mMYexRV@wbR0Fi1I%K!iX diff --git a/iOSClient/Supporting Files/es-GT.lproj/Localizable.strings b/iOSClient/Supporting Files/es-GT.lproj/Localizable.strings index 003db62acf851be16c0a05e7d77a6a1d9cecca72..7632f31d53a97c38f011dd69fcdf8800b8b7a526 100644 GIT binary patch delta 955 zcmah|O=uHQ5PnbHbPao`F0l>Lb}jx?v}pq3O(aw#3YO4z8$C$6ZI>FdZqn?AQV{AP z2M>bE9NyKlcn~S}81H)bEIkX7v#8&^G#IynEN|bwc{B5U-^}cfFUI!+UPIqNXD`z9BZ?yCZH=^;A|p zUy*_B!6ih{Hue~ok*L#)_ZhY4%4_s;$=qM4$dXhgSB@X|M2)=BODaDj&D}QKn?PU& z5pWww!7K(G9pFYdZz4GZv@}~E3>>ir4vr{-aSr=E+|=R0dD>cEpQ{6VfUL}zGi_)^ zYq0rZHVxSfwJz#v>w(M|x*FsZ6yzES&tz9D?a(X2^BBM)BA7k%SHP)2_M&M delta 62 zcmV-E0Kxz8;|2WJ1%R{xR92JVga!w60AT=g0Be)6pdFKH5e}EcRsn1UZvbupWs{Gx U6}MPd0fYgQ*hv((o?Zb`6Bwfxe*gdg diff --git a/iOSClient/Supporting Files/es-HN.lproj/Localizable.strings b/iOSClient/Supporting Files/es-HN.lproj/Localizable.strings index 99ed77cd953a63608128991c45aa306796f4be4b..114968b6ca658693c65751afc46c866516b8e228 100644 GIT binary patch delta 938 zcmah{L2FY%5Z=}1nTBTwiES*J^tEV3v}v2GXwZtNptPZBi-m@5`lun#B;;92PR&7Y zB9#I6tamR(s3+4O;K767rI)HdKzl4IezPW+rUhB{?asWJ@0d0Bexh9Gxlk+QL8L@SdA@yZI zr)Qh4UW%ln}up!t8Tk=%*X?+J^l13k1oY9$=gMrU+~5H!8mM?c@6rl31Q zoD54YB0(EZtw0^zF74irQR9|K(^vJvn_c~&-r5K0A_<3sRtcI>yg0+||dY-l}O&aMv z^LS<4dBobncF9SdIfI)ZUAMIfwC31 T{#F5m0hiih0Tj1HUI9`Qec#Nyw;vKaUlVsA?#J~ zb|Q76sRgSx8{@V zzdyjg`e|Oek>Z`_Sp5obImwA8+5koTqkoj`d;2H4sK%!zKda=;w@GF5cRM>>9XN;b z*CU-FN|6c7$!8AM)Knts4L`kp!aOM{D;Gv{>LRX#`Dj`gwoYO*QhR_>1C%-J9XO{> zEx^~qAY_=*aY-EYR?{A8-aVh;$Fs*wwx+V~IH0`UIw0J=2>^02N=;|6O3?E!)hIJY lg)xnjdE;d9XXv7)_#ppJ8b{D^gdur>D;Lw-!BurR^9Pvw+57+i delta 76 zcmdn-l=a9>)`l&NGU3w?L@=_6moOwU6fwOTLmrUKU`S;snq2r;dU{VdV-9OR dP%w3};%3q93K5JgjMJ_B8AZ0I#WMy80|1%f7OnsQ diff --git a/iOSClient/Supporting Files/es-NI.lproj/Localizable.strings b/iOSClient/Supporting Files/es-NI.lproj/Localizable.strings index 154884bb105f6e7fc09101ca1228ca1fa458474d..158b9ee0452677694b5b76f725ee99681ace0651 100644 GIT binary patch delta 897 zcmZuv&ubG=5T2)QTFMf|Xl#nKFR4FJX)t=J6e*z|RPZ1yTMuH`HX+RqlbTHl1;G^W zy^Qb>L_B!VTk+DH@h|Wsc+&3xa?_uiY&&WmqO|F!a$ zZ&HPt_}ZkYOKb2tSXp?>@B+j%t-`ioZ(&JBlh*OKiJA@A9@U~ssc%ymxtp}eU%qC{ zysw)4@w_|MMKGks?pt3ySLUauCJzTx;~<^l{;?ChGnO!qJ>@>p$XWwPFQSW^(I5yx zR0VY_YEas$fJ)N}$TStvS|0Wecp5knoQ-IZgQhg%O`u9^f>e%LUqP%wa~MyUR;e)s zkQ^=01ojfB+Sug~bY{_47r1{kS@eJ_oFQsjK&oM{!Vj<>A{OEcqF!U}5OM{mY8+&- z8f4h5y1YH-W<+c`Zv#aTOYxMZSXWS|p5O<65`6jMk=gE$*e)3nhVSm33Bw_mRpjWZeiE7G!#yfQ}U`h7_x%_hr;pJl^L~U3YS#s zRIH_?pxK&-2I>9N=9j0^S4TnQ!3I#jWN98Ek^}p7tQ-yD<75k-)qWvDUKmBTUs(xF W+jnF$WbGnt7saDY?cTVj%Bg?3BG2Oh delta 73 zcmbR9n03W<)`lgFVe)(9*d&MyZ2?GEanHC2C diff --git a/iOSClient/Supporting Files/es-PA.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PA.lproj/Localizable.strings index 42e2fc0eb6943b62e6071097ccd640934d16cf04..68c9f47a9ee3855b19eb69a312e46c6e323a02f7 100644 GIT binary patch delta 897 zcmZuvL2DC16n=wdTgsx*ZEI7colUjchDM@ZR77GC6cjA1X>VcEEN#fTiP@A;5Zi-@ z2XF7N{t8hMdfWa6LGThUDjvLO`U8sJYmzVAaiVaxqGpbSNyJ^CDtQX29?7pE=Fk#aR7~sC zoCZjqZqXR#DyUkR)gkDDD;2T8yQfc;E#N9nA2lA38kje*w=td~=Hs&?tqHde%WPCN zkFt0W=7N4r==@?y&nd@MLJKeovYNEXEB6e}?rVJLX)G5{&UItrbj6S{T<%Zs?>U_h zL!C8ep5?gCH}}t;@sIW8P>=tFH`(pCTf delta 72 zcmeBq%sS&bYr_`C522F{qS+)%7!nzZ8M1*`fgzJ24@hP(q%ssSq)xv0NOF2Y7-J4= ZK2SJy`~5J+nT(S!d5Ubeien5C1^`nn7T*8> diff --git a/iOSClient/Supporting Files/es-PE.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PE.lproj/Localizable.strings index 3d1f7a9bc6989c92b743f41d478041d81fa4630e..708a0c16569ee5052475747ea23129ac10ea796c 100644 GIT binary patch delta 962 zcmah|&ubG=5T1ukyM#Sx6I+9%SyQ!O>9+OYMa4=ldMK#DM$i(r>9&TXo4A`Oxg_Er zpvW8+{{xR6gnDZJ053fV9y|v8b+F{KJyHg3SolNq|7o*)kbPagI7l5S-T;UvM zWPv5XeFyxW&Qfx7!DrDX7%jSq{A`?SAS>fVa2mRrer6Wz6`)Ffg0!Xc--OMjh4Uy< zsEP=+DN9!=j=cb?Dt37TUD(d6MXsgeYrbX-5a0o)gnb8y4%RMg0X|1(I%Ez|n}b}+ z1Fs@rJ$edXp8`JIp61qqc~+DcPk9sU!oO*Ba7@_Z-F2i-9=stJCVpIlDy%AXm2 zH(_M7<=EFPj1aZX6pKC1Ll;Xp$$X69F6 zYoH&}QxU#D3w@0v%JPriVi?bjP17Ew oiOyi*NF)A77ezSQN}1$4dN$>+x8PO* PjscU%Nffs*UI9`QmfaPX diff --git a/iOSClient/Supporting Files/es-PR.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PR.lproj/Localizable.strings index 56f6ebe331e3456027f3684f0d32fa36e19d5f6e..68e2a301368c0e59a647d27f941c7cf058699d4e 100644 GIT binary patch delta 919 zcmZuvL2DCH5Pna)ah0WNw<-3Z-Lz<_C9CPhlZqChA_|dh>cxibc4@=LZJJFjxo8XN zRchz3;K@HAND+E#{z5!?^q>b3FHs5}JowEs3+W=u+xKSPeDi%X^WJ>YUVYKFKFFW* z3vBj;M+nPEBAouy2ZrLNb0$EF-olZpY_K za)%BljZ+OcA`#H`e)iyfNwy^23NrBzM-H8IMTLqZCe_Xj$DK*3$2CWud1)bh6`3{_ zXQIdgO9ZwR@P|qzYxBWpi6t;v;turL*dKy0zzyfwP}TfXX2D-YR@TRvmX-b`_|dRJ>=B?PX~2V}`7&0~3v&hBTtDD^y&}()S5t zuWgX-U!m*mOZ4Y~9(VF`fG+GC^!spzIxRikotGKu(T(;++TF=$O$hL5`_UA=Jw2Rr zP%7;ImS>~McKq3qV;c9&rCj;)o~w^?19(I{?+|}W%<{Eps92M$uA{~}>g)B8)@R11 zXVny{YK|e!3`}~Q>MR{~uf^XSIr6kW89!YY@N#Ml5j^8ab`xtHTKKUZr%8}$nqS?5 pqM7ceNx!=?D(RaW74DjaG(j3;9mk;IN8LPqeK&M4_e4H6{{l9@*OdSO delta 58 zcmV-A0LA}~;{}@61%R{x@>P?bk_HEK0AT=g0Be({pdFK1#txTaRsn1UZvbupWw+s0 Q0hs}l)JYV#JYE4(6Ri{#s{jB1 diff --git a/iOSClient/Supporting Files/es-PY.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PY.lproj/Localizable.strings index 40c10678bdea4d7fd35bd78f1c18e9b6285298a1..0431501442b51e38508815edb0bb02588c64cccd 100644 GIT binary patch delta 927 zcmZuv&rcIk5PnZ-1I*bg3l2i?hyAw9F7RY7QR}@*~a}!R81!5*Tp(wTjCD6UqB)JUwuW+gXGLaTJ0b!?9W@y*X5#=M zg1rIk&@g0PKCm270h%M`!L5LM3FtNa;X2!~lwryQWBmc7ILN+s~OL28s|EBE5?D25g11315Uq8Gq|>&?OZo6Ea#pQP^EblXeQ=(jXESNkTLNM#bqj2>l3JsqaslNJqi zExO=ern&AYS&#blkG$-oD;Qu=*6|M*+rv%VCEO% zn|CZ0HYq1h^)YScXLL}MIls3gFRelmH!*<-o(ZJg!kGXgUel9cab^-MGE4LiZl)J5 gxqa@*InexqeKQG^%sD-~xW6g-^D%q)Y)uyJzknCfeES6&Dw^UvMQWKWJ6uSTb diff --git a/iOSClient/Supporting Files/es-SV.lproj/Localizable.strings b/iOSClient/Supporting Files/es-SV.lproj/Localizable.strings index e69a0906772ed0993a464350e6c80c823890d025..7e75435e89e2c0340b7e3c2ef34a45ce90dcc14f 100644 GIT binary patch delta 915 zcmZuvL1+_E5S@=^6H5-ZYnw)?-2_xTBsKJ;!HN{5rD$oWi6`SW30e|2Wj8eh1na@8 zMCPzJ?a@Pp9)#XnJ$v-fiwMQuD+R%Wh;P0d3>#Vg{+<74-h1Zr zimGwSMv8-?wtsY2jqdy^s+Wpq>W05nP$MU7T4nq6CEku2&lUmK0<^p>@<7!DxDfYb zS)lR|KLnm&x>{fjq!z7!)}osbnZ3>m!2$oEq^5ZLT5NF{NW$W2{wmZs&?1-?J;s@b$JXk4Y##ctQB@DJD&TYI z336TX_+ujBKfR^m=frbKl}iCH|7(%$FA4sgGk7g|l0RgO2}`%rpWSB&FWn#IpS$C{ z_30vCIX}cRduN6{N>iSO@GfiH+WcepDt9+yQH%w`DxBY&;O+|}(t(B=AKWpy``Pfn zE~q$f8sn*Dbd8JucAACXS^rHzO-8g|{Oa-reqhFuVup;X1p^#C(%HaINjAj+7Y(}* z75Ett$JMO={H!t)J)1-3^1|_y|F)pUru#AE7q&6aa%d7BPSQ|*9V<-%axXcIJb#?i h$oYQc@ZVM*7{DMyDqJ)$HFS~T!*?S)ukWdX`3L^0*608L delta 67 zcmaFznDxVT)`l&N9$}N;@w4%lFeEY*Gh_p?0z)Q4-sHpw;*;av^Gx3n#+bvJ&ydHE Wy4^gSaVFz*sRBlk?UUjdgMd76)`LBG zk-|XVwTNdaBJ?8d)njj-)jwc+6a~L|NiaAg)^Jczp=6mz@OK9VJ=;cTCvoL>$ zD&*nw$)*-Pfz`xHz?z5U!e>(nXC3EVEQ#=_fxl(sEa7}kRg)?CK9v!>Oj&lnjp@vs z@_6TxrQa1)Wc&S`dZTnPuj0pCs&Z~L%8ijzx-qLFt2PBd_JJil-70J#2e22xSvQqQ zJr!`+v;Y>HZlk6&&ii1k;YM(lOb6|1Z1_E(N=<@PHZ?E6SD_U85~dQ>#sQL~8#IL7 z0hNzkEc+m{E U{xXblCgbE&o+8^_;uwR30s6QV4gdfE diff --git a/iOSClient/Supporting Files/es.lproj/Localizable.strings b/iOSClient/Supporting Files/es.lproj/Localizable.strings index 3bd368c3435bc3ae1d395ea60002682527ab1723..20fb4ec6acc9f202c8af0fb5d0c8d83471c4ddf9 100644 GIT binary patch delta 1113 zcmaJ=&ubG=5S|z7y4D^_ZL}$rZlmHs8;vJHrN)BQgQ5`V!9(2qpv|w%lD8$~V!evD z46+A3c=YH&=s^$l)_{nfyy>4HJuBijZ?h1#TEcs8-@KV`zWHYI`I+_Mh4u8Esovnb;zNBHgW4>E#bC~n+uym6`UT*3>1wdCM*ofh##h6Ax0=v2Wlm z^|oQt__>B_oLi$h7g5c~i+}B9`1;&b_~wzCS`d=r&;g7SF!T(b(7pjI@JZd!Jua$G`r|pagAPoypw1?fH)scqc zd{X+?1;s8(9chT9iw{>}+d@aAt0KJw#u_J;m0mwE-IKYBx*T;yl*ecRA>y_@o=*ND zE)tXyjnc4W0eJ%}3$_P|#F0rN+Kk;HRILATNe*)ff+Q#5a85}C=Kw-Nrg@YX57C%T QGV)=*rqVkpUwxYW3opFv(EtDd delta 120 zcmdlpmF-+7Tf-K{l-$V+y7{pE^V+%(=iYT}G>KE@J{J?h}AkC+3TJ&H`E#J4GcZ=cq9FjLMR2I}sk z1OYj;M~nDwAS^Y`0xCh0Lefe)@zz)|zIw*4DWMh*+}YzVf%>H~SF5=Tnj-#u`4*oP z3r7OiF`yn?cLCTleDi8NFiZfo14x1GjuQoVYk;js>!{7g`2?1hp^yMiDCGn-s_wzs zH%Ni0y@9uoZZCknq-rA6rz+i}DeQHmYGW50>dFS8y2I1+h4mM}m7F3>480=kJGe{H z`*=(6(}ruFJ3)0CQLT{|w}xf@?$2Eleqw6_LD1@x74l3x%TMpSOV43R$2si?Ta!qz zSPyUT=F5d_dQnaBhv5}oSt~lG9~=!=v-eF^bPgpqzx`Rdl%kprh1u(hD)Gryk-wJb zE9U8+MfuRk&rY{h#WB+GH}_`tJ5+_T6fTWQQEZIeqm8k4;$4!yb-}NTI>*}OIbGgf zcDFWQJU~mNt)jgk^&) zDrlc2l1JJ^5;J$_P;vjq#h&Iml(FaGd`>okcLu?d=_<<0M`=PQnfc>SuT-t{7pVU1 Ac>n+a delta 98 zcmaFypS9-;Yr_^smXyg3?QD}Xmh%b!VaQ=fWhiDyWXNMk0kSd~QYIf1l$>6Y!f3%< z%8)ww{6q1{tG@FHDllj>C;+9i8S;TJk0EpNK{nCt9I1@&7$b AIRF3v diff --git a/iOSClient/Supporting Files/eu.lproj/Localizable.strings b/iOSClient/Supporting Files/eu.lproj/Localizable.strings index e9152307c1a8fd81916a211987066debd8746752..18c04ff0c676957f0245adfa4151d4ad0e532771 100644 GIT binary patch delta 996 zcmaJ<&ubG=5T2(dhO$UW8iE$;ruYks@yAJ!Qc5ohYOEA0LUyY}0($I0 zK^gHu@dxTzR4DeU#FHnF9`p|oLGd%JPTw$mY?iT338=C9NM9knw40e<0FaP`hzXF|z`hLI#ae+c#OE4u zF63a6>uRoWSH7>)DV7V4}=WmUy)(ZUnMEdY$_@jY@Wc*N1 z^vgiyxp%}u*`xAWnnZ>;~7O(YB@sH=3ybn#Htb;0Um{vv5Q~tVlcZ3?- z<6DBRB08fHtrNe7iBXZnBEB!Znph}rk)_o2#eesa}D?hi96 B;|~A; delta 96 zcmezMjkV`BYr_`Cg!st~QUbhH45Zz@6cE>D vP+&-6$Y&@4;!K7_FfWmzWU}Kb;mI~qT-&1(7?&_kkH}#Z*?ugYaf>hj)43f) diff --git a/iOSClient/Supporting Files/fa.lproj/Localizable.strings b/iOSClient/Supporting Files/fa.lproj/Localizable.strings index 48957c579b9ec219c55c0e36e77391706e1d3136..278509d96c6cecf6c4fb67ae55eb821b495b6dc9 100644 GIT binary patch delta 968 zcmah|L2DCH5T1u*wP7hHv8J($gXTwXL!HxcxxdeF_O*UJB#RkNpTs_^@n2)`;!^VU<-f0$Kf*FshqAZ<-I6k~zF z1JN$1T`f>#IiRwr05Xf#AT5n^6FgOX5uBE8Y13BA(NBoyVxySADAh*5pke_h8t*h;3p{K zSJn}0VKg#cnSm4z{T_3f7yHZpaaNt*83mKeY@<9sHWCk!B*0d&lF%|qGprn^S?Fgp rO?Q-L{5s|*M)Nu@WIjpbvyDKbU)RH delta 73 zcmaEGkM-Ln)`l&N*McW2G_y&TFeEY*Gh_p?0z)Q49+1pnNM$HuNS&ch~w!L{O z>OTy4ked?OGoysaW;1?z3m(2*P;iUr(V6=$jc zDW_Uh8PJcTruuwEW?p`)%1ue%eM3E|$jlx`G-!FuQrnhH)B2sX>dwi`nk^zwgaBp2 zSdI;f2>V5lH?>3-=7G!>^PscEd59}wy##t6Ul?aar}A5tjq?DgERK;DwD@_Pd14v~ zGLtL(0{|(A(;|(z4yq6{e*~R+SCb{`PVFtv0hc)=#594_z`TgPgK-OI5gtctjkzNj z<{-)++;Zt|FH1XFleVYL92?FzLf|lFJ@G}@J%2S1RW*7DRnMBV3^30C66xk&pP;O&nc9c0}S z9zC1M(Y;suXzObxuTTD&dl6O^)x(O+r?i7=`LaBsw#H>T8|iX(U`Z1+wkQuA=F1j0 zU>jbvp>5zW81u)ONDB@yhu5V+H4b?-Xt0ln`ZR3kU=k8NI+*Q{Uc C@Z$IY delta 72 zcmcchnDxbV)`l&N7Gaa)TG=E^7!nzZ8M1*`fgzJ24@hP(q%ssSq)z_$P;&Z;Fvc9# Ze4udZcJ*+^nT(UQyhOG)#4$z*0{~&*7Igps diff --git a/iOSClient/Supporting Files/fo.lproj/Localizable.strings b/iOSClient/Supporting Files/fo.lproj/Localizable.strings index b211be7f05cf416aaf7e68ee1c65c86da4350a88..b705fbf26ab07387456861157f47144cd6f06738 100644 GIT binary patch delta 952 zcmaJ=L2DCH5T1ukO~X5J<5QP)pb*p@yv4m`x}VA$rie z&S5cL^ysM|g&y{xf>-sXUi<|{(3=z#zj;X@t|H6ZmzjO@ee->9^5AXg;8W=Cp8Ak4 zT&4=ycpNgRMK=&Nv66@ih%DqxDq*i9jfX|sduQ1vmLs~&gbLa z^t!Tn{-+eUc~=P81B5mXM|@(py}%Vo8~=KXMX1GvKJqNEO_D&E_OEv%c!xp*wy>xkQhItx{` z;i(eu`m=m)*5G$bMnX)N6Ao|$vpBAkANFGWt31h>&54t(A#<=|%o(;m#@P1b-t99g z${Bx-U)`DHJwM9VpBPhO-i~O}4fJ~RB8cO=2EXv<9$lLVwV?}Y5%7SBA9<6$oS^C-J-9#)%X=-Q>DikRmL_vyJum>S*K1@Sm60=Fys9^Ok zsJz2~2L^04Pp5px6c9So_cQwwiB^mH)?R5hK_-=q?1dvueZPNnqw zp1OA1M?(nh(mdV^XzQ3_0hK195L%&=Y7e7p&6~38R6Gs(4ng7NbwF z=i}!Ybs?LNj6HPKhE+xIHE0X9ZSr~dzQa4oF@AT|z91T%U`!oDIuz3HH zmHw|FT0N2puU4k{#+=O)eOv$Rs5w6PYmW~p_3o0IiJ6bWs()P%o~h*d%eA< z7Z20CoxPY|mlY(PW;Bbqv-)#a&BjbzIv1$RdIzv)UAi?4P>vn3+=jh2tPKO?++m7; zEyb_M6|5srxi3xJo}-8(!Rpr=%7Hwjpg(M=l>XjTiTo(E{0BTh-GPP$YHo|HGo&v#J;mF;4d= NWE9!{qml842mnElB~1VT diff --git a/iOSClient/Supporting Files/ga.lproj/Localizable.strings b/iOSClient/Supporting Files/ga.lproj/Localizable.strings index bd9299f151aab7eef8b1bfe551bf12cc4f1a1af1..b48d5cae1ca62be4e2ff89c685f7d6693a1fa806 100644 GIT binary patch delta 1133 zcmah|O=}ZT6ul31C^aB7v1t@^60DnGs*4tih=`khAgxs?xEWqPXhM=nIx{h3W2I|# zbA{OyAwQu|?0;w%ZpD@D%4NVm;JI%)v`qXUy!Ynackj99o_q88y>%K|?+(W>Hi*p%EJX+Mn*8nj+oPBDTNQv)I6}|&{%wI>j@Q14w zmS2&NkQm&iBEHKA42-+LN|+>(#F9|FHI|F7zIoTcXddd0I#%JQwwk%5sSyr$RhqwF z&G7n!wXBO|YHwB6(QP^2a;YgALdN zL`nXne^Qi&AhDj!^5`hVE8p(2eLQttx?H0IJ)}wO4sxMAd~&oXuQ_Q~4_wdR1LW#b zE!^K#S$_LsitkTn@-HLfAB!$Q{Zja$t!`g2>hbg9ESfu!6J4Zv0q|YyHa(5hHO^@f z!vkf{h;5{elHQu1T$34XL2KdHF|o1wNzJA8>(WeQiAOflOL~Hd142Y^=VC_r>u4l! z5-s#$DFE^jRsn>^iNujfBEFfze^9ah#w9u1&RDDe$0><0J}e2DZlJt)t}f^#b8uMP IR=+a80kMwo2mk;8 delta 99 zcmaDfoo!wZTf-K{IeF6;xH1axmoOwU6f-0P$vhxToc!>D_~ct3_@?vZGn!4;v1Q~E wR{+Z?0M!8H88R6P7*ZJ&7__G=CNoM-PSF(F-jmOGig9w6pU8HWa>jci00h7tSO5S3 diff --git a/iOSClient/Supporting Files/gd.lproj/Localizable.strings b/iOSClient/Supporting Files/gd.lproj/Localizable.strings index c8028ea352b52d06dce43064979619b03e9f6212..67b5ab107ce8d1651c8006ea44e5c8dec1999302 100644 GIT binary patch delta 943 zcmah|L2DCH5T2)Qh;@-_gs8>3En2mj#gm@Iial6C3}$Pg2w|J7X~=fdWLqT%X%F?F zhzKJ*(2HI@wIX<1JgWy0y?F8mNY5Vq=9$>ARb+X2JF_$2H{Zt)&{-^&azSPyBY`n5U5gxijZ5!ZwX7RRLc_cM#jAm=|h! z{c}>;L(*aim5%~m-jc|zB+r{aGJMt@7;=o1{>VSg;Fq`aJbTRM#l+^T@5lH>GOWkX zD4Rb%$Z|C~$Dh`+yg8QPcN@F-RdSf0+|O7YkOsU_%<=kXo7dgp5f2LC`>QG!y><0e zNgcJMRX(@IkMo3=)z3<5aJNI3#YsyhA}12tP}GK+Zo|4nne!I_KZ?i zueIrh%212^p>SGzC3PU^2bW#-0a8xQAckiYY5ykcWv0nuvqM{SEB$oo)S9LAr6c^ca70he<3IEIrkc(D0>{VJt^fc4 delta 66 zcmZp8z}j<#wP6cmOvoewHvSTZM22F9Y#>%($YjWyys%Mpa*iP1^fMuhIjs2%c?_xB Vy+aviGESFDVieiFEQT>k7y!8j6c_*i diff --git a/iOSClient/Supporting Files/gl.lproj/Localizable.strings b/iOSClient/Supporting Files/gl.lproj/Localizable.strings index 491c519dc93100cf18220af6c30383add3ea1acb..ce72891647cd2a93bc8de055886a0f15806b339d 100644 GIT binary patch delta 1165 zcmaJ=J7^S96ul40u&gKq6Lm#&cB7;SjjCPIrlT~cAQtQo$Zh6XV7>+9SZUF z$)$w4IK>!MoEkW3+_}`k8ex5i!4@G6up7bW1=b<;#FPE|)Q0Vd7VX#RO6rB`?s5j| z(gIAIa7e_A;n>mG8F8+3Dq~1{_d)ar!@8Km(%n1?c>szp#}oz z!mJChp{UGbaG}cpW*u-08Ba*sxNAbAOUsDJ!@7c(KRYu%T|+CBHOY0}zuK*hEN65A!1<>Fmi^?H=QkzTVi@~iSTTMV;#0=BVPxrG%8H1 z;UFu>mFYZ&6{Ql@^nO7ltcz6%kBHolng8vBDd?|ZPUD*QxJmVGG!ApH8D@mGv56$sphqsQt`3A zjfQ#b-iq_n%!JP5RN~Sghy186d@mYq`^cmrejO`KhwQ07*kC9BAO35D{N`U(MCazc Xm^zMGWSd1jy>WiE5ZfCssN3;B?h($j delta 73 zcmX?dh4sM+)`l&NNBkx$G_vuRFeEY*Gh_p?0z)Q4-sJteMW^fNGjdHg@MkPw%V)@A dNM)F8uuf$A34g{G#_6xz7)7@K3uAN>1^|^P7ytkO diff --git a/iOSClient/Supporting Files/hi_IN.lproj/Localizable.strings b/iOSClient/Supporting Files/hi_IN.lproj/Localizable.strings index 4acf7722bdcf309dcdc92667db2a0f69c6dbd66a..8b533151908df41423224eed41597859556fb67c 100644 GIT binary patch delta 923 zcmah{L2DC16n=wEZP%cc)TWIZrx7i!HQsuWqEZkQ!Zs)rY)jatp$%D+x|>*Xs`S!Z zLEd5TPiPB;f&umBA@-mL(S!a15A`4Ty=`LSMv!G@cHX@Aec$_Lwhtnk-y+ZV)Q{P@ zdsHA7UxhSPX%#yUV-&kN>>R{2<*}Br-p7y(m)7uCM@N~B}mJG>?OnsbRDe;Q=Uo*fTZXq zS(tO6s$iBw(3y)_HO{xc4rIK5(MLrYI7Q4WKy)ykAm-z90;O$cA4WOQQfg)80b8bL z$o0rK?-o^@KTbx^i{p|ihXS5EZkJy;akD+7tjkr@NWbCa4sUpO^Bh+j!+iI{Ro-bN z`R9W^GvTNxf>V5G^|F2)*lsY&I{4_Jp`pn?yqoe&l8%o2Sv}xMvQY0{i zcPFlxwF}BJ$ys%}U(-y>DoKe+dmg&+&`HZIKARcgw>NsP%^+Tb`Er5cvb?Xhp&=Xj zQ!~b|CX?oitp?{iLFKtRfXIb>_Yp|}a2g{8P2E)O%D<@ipc(%|ROuuNKf7F){r@8j RBXnJ0tINcG>Y;LDzX8>x(&7LB delta 59 zcmV-B0L1@-*#(Zx1%R{x-cXYug9Zn50AT=g0Be&eogI_>-42&dQ2}fPZvbupWw*^y R0hs}pxJ&^Qw>V-?@2@%1bGa`}>^lJLfy+ef=5x`zQ8kD{F3_PnIOj z(_NC(p&I7#-lbLSySVd6V_u^s6>(FuRtjfrE$XCR?`21OS>fDYd5Ehvz77mEHE`-- z;2SjG?!8E|}l-i{{2u z{PN=A^q|Y5yONk4kfT=S!_F;#*Ab_4tnn*E(m~W65FZ$7XwjurGhE7<&8wN*^7wqH z9tBjT;H$Eji&<68a5Qh;wG(NUjEky4<6@Sy7^u$1Db}fHc^#_>R0m($d|Zf^IGH{w z$JKYSXXmgo(Ry%CJh4^g2DIH^-tJHPwX(=;{7=F^j{>9bf z0a+k5sSFhinLv>e1|1-;fFTDck_hBO%u8WN0g7falr-O#-F{n^G2Usi!%`l}JfL(g zP?-Wl6_88;;u0XvW=NbqF@Z5~5)X@N2}2Q3TNY4V5=cAPxOAYya)EpW25le)>YD6$ iLX;n5bs5kxAoV#6ncFoJ8TT?y&hio2z9@q+LKpx`@HD#s diff --git a/iOSClient/Supporting Files/hsb.lproj/Localizable.strings b/iOSClient/Supporting Files/hsb.lproj/Localizable.strings index 27113a9dd101a0897ae7c6f661c4acbafecb8c0a..dfc0a57525436144a07e8e8fb915012b0e4b0642 100644 GIT binary patch delta 941 zcmah|&ubGw6n=wEOk==mE2)*X)A*xQ(-bdWL_~V2Lf9<|1%HKYy0kGdX?K&BT&#$q zUPO6^#iM6ICOz|kuD!%H7sbSxsf{B#)3N696P8Wk; zwtB=PlZpLR7x)^DNn@_fTZX= z#j)lH@lWB+9he!@`DYm`xfGyKH zd_D4m54Tj3Kc^GNL~U`EO99XQDVJY3Nq&}!a`R^2@hZZkw`lR*Y?i;tSkeW zGQqB;#v?`@-a37fTk%*hW~+gIO|xyu#A8s(^iY+DirS*_q3luKnTejAg}sI*NFzmU zd1D=6!*WV{ZGM$L6MVDO(2Z_bd9)5da-k0Ru%r-n0W*cHddWJGf06N*Pf3209y3In j-9+IBnCt1n{|Lhfy?r!wnujUBi_5RmquVF0s?y+Z`1;iu delta 59 zcmV-B0L1@<*#(fz1%R{x;82qwg9Zn50AT=g0Be&gogI@1>JFDsQ2}fPZvbupWw*~! R0hs}px@rLww<=iyS`&WP6afGL diff --git a/iOSClient/Supporting Files/hu.lproj/Localizable.strings b/iOSClient/Supporting Files/hu.lproj/Localizable.strings index 39f252fc0792b43fcfef19b54310fcd054a27480..388fa258d8e72e396bbadcbe9ac3d7cb3e6d43dd 100644 GIT binary patch delta 1029 zcmah|O=}ZT6ul3VSYyP{57SUfriqGBI;{xZsMOMxi-HA33{A&r5}J^7lFm%6>8AY& zlDimiBVg{9QIymoPNk)^}cl0^mxp@N@~M0S z@g}ND6H=+7o7_aKN^@7>giw_MH7QTGD2d$yRSUbkgI~c*OU<)!Il1a;M(N%`ojUd{ zRJ5_S5%ch~wbGE;LvJ>;H2PU(ly&GSa$WLxc{0Pd&D4}=l}vdjwXMJ1@=hhg&X+`x zzMx`rK48*v6cDwiE~WVQhXOZ0UF$BSF7_Fv8K`arxsu9o?BgQ0%_RRgNfbQVr*E)C zZ1(Zw4S3;$vrScAyP4&~<22t*jn6zdjs3HE6Hc1JPEqCJ+CcuCxXKH!MuK!n#U^B& zaycCA2CeiRmh3Tc#f5Yi4)i?+X>N7}v0aFhnaf}V7|tN#JfH8)2m3{JsWezjz6Bd_ zD>5bzkvxjs!OBC+FinVz(wytv&@=~wG|@Y14GrplbkPdKqn4X~U*BE%zN$Rcn-02< M)#%as6SY3}2Ri2AYybcN delta 107 zcmbRAnRUl=)`l&Nw_+!IEtU~XV@PGlVMt_10pb#de1`1lhH;Gk>K;IDF+&wY9>YVR zbQX}O0LI!tIvuDimmvvAmQ0>_N_=vgl)&UOY+T###WBueoW3KJQDnPW8e@zw0IiiC A;s5{u diff --git a/iOSClient/Supporting Files/hy.lproj/Localizable.strings b/iOSClient/Supporting Files/hy.lproj/Localizable.strings index 0bf8dfb850b259a768a1bb2341ca600a3eef6c50..e20d9ac33701ddf6f97f084ef4314f87dcf16f0b 100644 GIT binary patch delta 936 zcmah|&ubGw6n=vS>ax^?&^8v+Od=LU4J|#2w6zyKh=~e4NL@BRXqT+nvYQfm(mz3b z;NZohH$8~ZgT+5VJ$dNKqX>fD75v^L3+YynWo9yO-uu4qeKUFaG572!_vD@GH#cum zmqL6IX&TTDb}>d3yG`tT#5A?B_Oaf=kc^Oa@oOJ7dsv4Q*h;C7sDs>nTIEXNoO#t$ zVbMoQA4Nl~0~{vQJD9txUMSOQsM0B)0^Vv>`R6y+e7~dI#~K+s02*4#RFDQq0)%ZK z30iEWbwQ`81wu{hkXOfg8z((v2+p2uW+Y4_9s*V52~x+(Zz0yDt7uV}+SHo|NR6(M zgV_UBgjo*Zgn8Xmmw4$DN>kB`l zx~_3}F)FK^B8@AoDC(q82J*(qK-oaIu- zH6MI6rADkyP-1<>WxY6xl=D&O&yIlBO<@~}z^}g&Nm)b)Q zy~;bx#fyJ|IS2(=M7$`1`UmJCe}VskelzJ}+!SP)nSF2G`@Z+SH{ZUeKmJG`ycP5I z<{he%kFQCRcBuv{z_MU%!gAr0RKeN6xsIhHe0qw%HgZ}x_h`pt>ii~cA+}8`Y@MG` z_if>G?aIj6T{uIk^H^u{_@^xng&Hr3>0uX{4)R?J*m|7f&iasgbx&koNJQ5Fwr6PL zq9npY5N@M{hAC8+R|Tac52TV-QDOn-T|m}xqj6fMp6Zk@WuBkQV4j~&_ zMPh~itrQY2ZBNz1mbgaJB=+57c`DHZ4@4sgFz)Iybdos399k#lY zd>Bph^Un+XA(~g7CnmV_CdEh563c~AQQ=R~CGK8NVL)vVR(Wj0;_l*{RqolRwo`yQ z1vOj}xwH|5SHE25;{K3&P!i+kB(0q&r6aPsw*Z|4=;A~vkj!v#>CCk?_*>9k&rgf2 zpIWSDLPPcXhRE`*Uvp}rBrZ7psQT%;z;F_(hw$WqcoQp+q6WF*%mf!^@ef6Kl=wHL(*yh delta 71 zcmX?foAumz)`l&NazWD_bQszAOBfOviW#zjSb-svA#ZZx9r4L}f;`jvf*5mH^BM9O aQYZ6o5Z$g6%-F&>xni}*_KYaTC}993O%>Vz diff --git a/iOSClient/Supporting Files/id.lproj/Localizable.strings b/iOSClient/Supporting Files/id.lproj/Localizable.strings index 242314f4f0536bb879dce7ae828430b1865c39bd..414316c20ed52342626b1681731c120fc482d954 100644 GIT binary patch delta 967 zcmaJ=PiqrV5T8d)8$(brsZ9>S-B^(lHrSp-Ew$ugEtRFI_GZ|oOBxb4Z8o8#U|T4H zAc!!Bhc`cfN1>;HpFr^LLHq=M01y7=ZGvGfvb>$0c{9J?{M$F*62HGEPCu)ko7<1c zr#7ArS=6IFXk8o|+BP%~HjCJ;Pg!`QC=48ZJpy!2OY zJHQaU+PErhH&lk*^=S^irf&5{lyUcoCh*GHEZ-mI_~`k(em|wsd^*g|hE${nl)}4a zdUXgHu|HYAA=T*PO_fw)DzJNY2@@J19iIzN7x;^lwYMVu!t%;!a#<@^-AtG!@nY$= zzWY!m^U?wtlFU0WgRG3YDBc;_t1fEpBAZbcdAO117faKN6*G&rnP!0RXbYlF$NMWY zcG-4HI_;{gKM5;K>;a?}8%YRD5n;;&JqTcl4!D}g!lSlFQCJCgA$TB-KZ{GWTyqVue*70|1_dtCtmTyy) zTzpN^)TJ7B9gGZiW$YZpG*vKrnD1anhD$B1`l#8#+@rdwl=>!ZA-7LA*#2=L_)}Ca zFD}RCx(NEzIBb^GYsFqY68tKt_>4pKz*$f!-hVOA!}-Y5djRYLT>csrH30Su^+cEk znUAw=$ncCH_89Rj4W` z&2wfiVVNET?{jLE^-8p~0VHAZk>sJaj{i3H4#s1|e0+{kH(~SP$3azNoK*pzM^BL3 zkz7u{O7hXwSW4P1xpFG#_OwSXy-o7VY-IU9PP$_a;fkrQaOqo;YlHKA?bBr*=3=}* zi1W$aC_j2LYf0aFTpg_O?p|~bYbdMmyFrR?K8wussm1Lt8UC8OnAw2W7P9~4FOM$g zgP#SJwxnkce^|MMON<7yMRoR!rgaHY*JLEbYzNjlur+QSw?C(^tefV$=K4IWN7p`T zZK$E#exBgr$x?7!Q0MGvKzY0lAnec$_=seo{W?Y#dgQsDrkYfSsK`G>)|@5_KRg?r b!WZ{{(lCM!BaF!xgM+e)A1*ym4-L&ydHE Xx_xUP<4nfs&pa4KwhKfuMhOD|GoKb* diff --git a/iOSClient/Supporting Files/is.lproj/Localizable.strings b/iOSClient/Supporting Files/is.lproj/Localizable.strings index 783cfbf16a9791fcb1bca6146880fd1168ead846..c0bf9130443c571322c345f9aa2b74087cb8dccd 100644 GIT binary patch delta 999 zcmaJ{wB_F3DVxD2&p`MPE_yKLgcSsAd)l^F* zn?kscflaF5yn&#IBChO}_;5CNHbOv1{e$;SwWs2bwwgNQP>-LVo8`xo#_p1K_w{PR zaSI$fTFqFi3H}hkT~K+jK&$RRiI5GkHlVzQ{XYKmwV)7qlIKXXiFX&MqFj(Rb!HoQ z>*T14^o6QT{kZ-}UCQdJ#Eqt!<7EAOV--L`BAI#!?P2ZW?!cbnEyU+&abvO&<#Q0# zA9?Y1QdG70>s?hSi6;3y0Ehs#PO{?H?E+8F8@!xks?jlN+CN=|o*?n}nF7B^a{OxT z8vBAW4 z^+nwdS2xtdd8Bhx?R1T$&CA@aR^m^#y4X1lD>Lo@q?Kw*h?g?L?!d~>Bvbt-**G#y w7V4=wq1!!8mmbF!a!WV&|FmfY^DhLQt&AxWL)Cb@dL_=Rs0#;A9x5aM8$8O}4gdfE delta 106 zcmX^2g7w24)`l&NY>|@{q0 C?j8sL diff --git a/iOSClient/Supporting Files/it.lproj/Localizable.strings b/iOSClient/Supporting Files/it.lproj/Localizable.strings index 5ba204181f467d69df115a771f08a009bb574021..55395880377ffc906c6c7060b635bfadbdb2913a 100644 GIT binary patch delta 1209 zcmaJ=&ubGw6n;b7HHK=7Q8fNYCXv*NHboC!L}Tg2B1MsgAVtDv8%(=#H)K~sPxS|S z7UnSjMm*)>!E0Of*i(h}=FR>Ies4Aly0&GR*`1m1z3+S9ynXk<+WTbncdw|Axih-| zNLaIyR%n$ZmFNLxk9-VCO$x9ZP>8RaSa+z4uNUO233+a$P5%2zZcxdI<;B`V9JKNH zfQ8h;ilGIpYFOFWljPvdX%{dU(MCc^>xg-UcMnt?$?-nq*f^=Ex!+m6T_tm?2?6(K;zl!N8_- z?G5ZTr)UP+naV+EnTm9m(wOU@@-g!ns?tAQab5M@(^@_gqg5tlUp#Yv9;8Jzzr-e8I!as%Km-xs`|AvrS>x8N^Yf34p}Ksr@ow5yPLPw!FEcQU15#!^*QR&R$d)A zqbEX;bf~F5FBR0}%;myTv_7=tGAu0XLQCYwjB{##yrjR*i(AxQk+krh+S5y9ZzqP`;}+jE;T~4v(#u^snLE15fx?VK-y=0wRm}LeO=fkJN4a zP0GKI4T5?iU4We;k{m50j!X&(OgkPU#X3q$vdQZxAlVVeYf5JLzdsT-T|C?;{uKr# IPdq>S2h5%RsQ>@~ delta 210 zcmaDbfvu^PZNnW|))0mahSbRu9fT);kY#htXGmjEU{Gan0Fo&T`3$KH#XwdbkW~Vt zQyD54GJzr`3_3tw0YeT@BoW9@1@aPs>QjJpCPPWHwcK`VImT$;$qSbAi6#Rz=K>W2 zC4ejihC+r?hRn$a-$_nq%V9KA4PYn&>dysAgY@PA$pWB)9H6O*KzmETW@}H@eEAb!=lhZZD07nddmrVG_v3JwO;aJNy552H5$dwtqyA-(fae+9cknYNs5 z_si1DIw;ps_=%pJ8XL)EVB11>q{Gq%i466&RhGU#lTIRs_=xHdi+7I`X4ba{nOzAH>yA;Rb02G5!KclMB9yO+LfMwcSFLF^q9~pC+Tob}JdiOMCzk?HsrO diff --git a/iOSClient/Supporting Files/ka-GE.lproj/Localizable.strings b/iOSClient/Supporting Files/ka-GE.lproj/Localizable.strings index 5e19ef2c075b8a88d945b2a23f60ce1625ec6683..d1674981f2b2e11fb9a69419ad24d9789efa4878 100644 GIT binary patch delta 912 zcmZuvL2DC16n+mAO{7Sgv?eOzCZb>=X{#qKA|WUi5fWStcyPN-(;5?-K`VF` z$~(+87eVVqK`400e-J!)5)Yn~$|C*%zu9guZptz(uZwb$*AIfZd=g@_zT2 z`fYM6ZUfRr(z>!up4@5Q=9jD#SX+>_+_J< zW`L@JxlNCNb;uRYq}BFS9y_6p*HC?^$*q4K%hrpuJY7C0y-ShPU8Ly(!uTPFqBMbSM`hJHKaf3>Mks6WGojYjML6d=xf!G+A z)TeWtN)PM}nJRC+Y4xPSM=uO>==W>mF09ZExQOIX?q!S|1od;{Glw6Y aHDddyLN=TS=upautY0AT=g0Be&`pdFVSRRMYhZvbupWw)tS0hs}p NnqdJHw**}QS`$%e66F8@ diff --git a/iOSClient/Supporting Files/ka.lproj/Localizable.strings b/iOSClient/Supporting Files/ka.lproj/Localizable.strings index d88100a0effde92eafb9bdce64120cb8751d0630..8d030886c90074f2dc8bfb48de00e2c23e294a73 100644 GIT binary patch delta 957 zcmah|L2DC16rMp-(j`i0z|?5dO{^eFQ+pGt)X<9_3@M6%f~?zQX+zd**i9&fLYrIZ zK`jp){0H8uAoLJW@hE5!ya=8>rJ!dK{N5y~bt{x*W->GHec$`OH+l9kvh_8x@mhW_ zmTn10xcEF_il$h`&d11LSHjN5nJLOx>sW7NaD*!w__d0h7S)OMfD%jZ%>3v%>?Evl-0T8@z!H|X6=biIYB27sA30G)?S zCLfe$f-b;*37CxQ>mnTxn_>~vrpTkv9M(IysUd=KT3S}GL=)#OP`OY>s%RmLICI40 z1k^HB8L2su6*I!XY=O$d%y;la?M=!l6~88pyss$(WYhsv#k>Sa7X1ik0X|!oI-m|9 z&PFk{epH#>c9OK|#MJq`jGbfiIm81HgIa%e(&u)P-pm@ACem4FIQGC3dR0l%T4z*k zU6Lbo^F@lTc8BQkg(3RTiBal)Y`E7D=+W%o+G%>Z8KuiR2EFg3=;`#yND~wd+TP31 zLo1!R$2*ksKV>=ilvA=G$0OPvvUe`h4H6;{xp7N zPWQmoU1Y0!-S3$ivXXvxr<75UXC52|;}>ir-3s|0;3SK>Z(w9m3%}WaX$F~LnqS>% sO|yQOCj9EGBV9O-E{t%rkNkFv`Ze;u#i3LCW9myuj_h8)D;E-f0G%(_yZ`_I delta 66 zcmeCW$vW*UYr_`CX91HZ@U!ukFeEY*Gh_p?0z)Q4-ekku;*>XN8$>cj2?GH6^%g_` diff --git a/iOSClient/Supporting Files/kab.lproj/Localizable.strings b/iOSClient/Supporting Files/kab.lproj/Localizable.strings index eac58dd08bcf6ba55f42822b88f65de16be4dc43..fbe26df7e381c9cea6c739c97c8b041a2f1bfafd 100644 GIT binary patch delta 929 zcmah|F=$g!6nz(yHYpz|txZCu_)-)VX|$UNVoMe~lq9xN5H$RxpEe{-BtIntq;=87 zrFe&@i{LJF5ejCoI4kO)RFDkfSiwac_1u>R!#Ayj97cs4h*sIv*uq30RZsV*UsKD-ZK*4U za&(W!PdkGR1bkKAd61*WWIkpnal}<6rb3cu9y#V#hMY}}j8zmjElp2^2Cxr;6;M{K zFp=YdLaPEuw3>ya6!v*=l<`Gy)@=h_F&gm-P(_j;EnCS2#5^^HwuGsu%I4PsjfHKu zCYhtNycIo~UIdPC_{gcEvV`9ioL#In#C$xi)z;(kVT+5Z@NI>&{8sn#(IB&cj;^C=*32htb&+M~+-@9Xcr=8%h*H7|HXNY&(G4tXKIZ<(_ z3D7Rou%QZR%r>UhRqn2>8Yc`I)oe;*Jb)V9XXe_?pN&nRq-yP)pIxC zDgWF*Z}u`2yV;K@Q|h8TAGiP?kx77E#+rm6najUvdX+(%AN$jmX0x9rIB)qw7xF*4 eAj07*m8s3zIh22g$AjM{OlXcqb}lZ^jnUtV_}E_n delta 61 zcmV-D0K)%<*#(l#1%R{x;!u;&CVB5mzOLFr2k6f7EE(?=Q-UrL^}fp82j}lcK40C zTv)nDKGpFxNYgD^gVn^!!CHdl!KbN=J-~hmOCsvDj#CRc8`w9gYBMFjL92*u(F{L+ zKWgq5RGr=Nq$w_{^vlBtcrWC9lBe(EVSUfP^GOP=M4b{uS6B)=l_Ae4Z_?M;1aC4_UQtR2g&u-9~JaLY~fK z%+EQMJSqZ9s9Xwo>2HTT_&vklW)eI*Igr)1lm5s(T5xXJ;m_|zc&B`lUpPtD?KHQa zCCzhB#c^@w$wZv*yiV}(_7UE`HxO%q)90^KqkQ92irt~1ToDbz_s^#CP&RMQx+)v9 z(y)4Ug4dVh=7FnHhcwN1^^yojI&7k$CYtH$1)}V!c`JC`K3RaXOjA$Kh>Be4OqmO= z8ou5SCWGlAy%do=gl7z3=ds36i;Uu4nqFp<#_vp9nyr4C=!li}N&i2(D8l~7lR0zg diff --git a/iOSClient/Supporting Files/kn.lproj/Localizable.strings b/iOSClient/Supporting Files/kn.lproj/Localizable.strings index 8e0949d6abfa7014e111331883a9fafb0b94b9a3..8fecd742234cbf32b972a38149ddf916f037e221 100644 GIT binary patch delta 918 zcmaJK2&jp_9rSEt?NHTpN`HekP`g8`x>r!o zZ4cXZG<2}Gu@O@3sI#VCD*ee-)2BSD^2C(QA77v2=GBD0?W*Jx2US}D>KMx5Kn_Sk zplkw75L;%n3g{g2LFmv8jF-cD2Rk)X2+p<<(;v)1J^-qWCrBG+d>^?AWg$_R%2eZL zb9P)JNY`i9)MY-LNac$F5|R)_b+lIT-9#(`A0Zdw@yu{TvJf75=&B8>%KWOE*6$xE zi?3y^3u3aAGyo)^rN7Ep`Dy()t&$7v0c*5)#0Fm3o8)`lY2N*Lh26z5{<@IhH(iT= z9awtSR|#c^+45)k@I@ltKMTpt@czMAybW1Rs&M9RhLv-oekKE9IX@;Fqns2=oL(>?giQcvn)I`ti|8!9V PCyD@l*`u>X71)0OmJQKB delta 58 zcmV-A0LB0E+6Dg31%R{x&QX)V4+aNx0AT=g0Be)MogI^I-VT>OQUPoQZvbupWw*Uj Q0hs}lI!P3_7+L{Z6S!p+5dZ)H diff --git a/iOSClient/Supporting Files/ko.lproj/Localizable.strings b/iOSClient/Supporting Files/ko.lproj/Localizable.strings index f1fc28a950b58c6a1758222e9e85806799eca960..fb576a638a3b1db357a01317e6c2d25934231f5d 100644 GIT binary patch delta 1062 zcmaJ=&ubGw6n=v-q;bJRjOnIgr%@>djn-Q!wMZ{jD2fz{AjWMPYDl_Cb`wia_25P5 zA@YQU{t2E6iUm*2-GV1i9=r;IcR}j+Cc9XsMV8swnR)N~@!ss-_tehM)XNWQr?|36 zHEQB%k)|#^$11>>z-k364||%bn0?ImFeIW$ZG3GZr-yly>Lye2TeODQEt==fpL}#s zRP7NDAwH5eqc>wJ&rkZ<{rh9;m5TbV8a?Ato#)O^^0Q1j8g-SkttkXY3#h`F=+WQ^ zv0ev|fCWZb4Q!fL!Kdji)Ri$W;iQ2G!RZ+99Sbxms=>787e`j6zZ}arK3I6g}kK-GP zPV-+zIm^*&SB;-FRpd_p8sGYnErh0_T~PZV*R(LKu?fvdRRZV<;6JW0|C%naGnJWN zz+fBsuAWU46L^4^4+X6N7H>V36m}N15oYt1fIGCz4;l18l`=VThd)faWkt z(0P(R$+YQ1b0IAOaogm2czie*IbDo>&nrl?Z^r*c*$Dasf>oUq{F(Bo+5N*ys+#)? Di%{lZ delta 81 zcmeBq!?NK4OT!k%9;wL=KiDKo7!nzZ8M1*`fgzJ24@hP(q%ssSq)smUD>}JAfMxn0 iDaIVue4te7wZ{Nnns{+7mbwcZ;qVbsMrIN3W!Mh?e3nQ`2jr{@>>9X* z3H{@@%_L{m4Q^$P6VhLas322N3I~<&x|8JR=_qgC>pN5LGKJ%ND(B57L%h-)<||*$ zaXV-5`(~VfPsf7uwlY+Q_N8-*2R7pTt~tVOtM70f@~h0 zrMcts1fMN68j+SbyqP(Ri;D$2chtZUlcvN;bsdqAxv8P18tUoxhL7b_{AnV3c}n-! z(>GMccjsn1{WB>{YLd;bvKNDvr3T$zSQ%0WAcath4^J8uT*XR5m5k-zWc!(6GGC9$ s0o__JU3k6=?<4fQ2}h1G)4gslRB>o Tx7bkugaMPpNEEjoSpixT(^FW@qNjd*AoIZ)W?W^WuxMwXKHA zi&v>Z0lqeAifI+Q4n_gHMeKaUG}SSOnCCGhBcL^`Hc_*Vd4rm^QtI2ZjNDDS#P1GH zmJGWmV?7{9(bhzp7Jg`?5Vl6K#F*Ic( z`;S~c_5G;%Ra9fOK|pz^J|Jo--y=k(0eBH(8vV%A+)I^Irl@cmgJkVNvh-uCK~ws| X{!f}l&}oD|`4NuiM|Y|fRdxRW);-r^ delta 58 zcmV-A0LA~P+Xb}G1%R{x1X7dmf(8e40AT=g0Be((o*k3S5e}DhQUPq2WJduIxBgNA Qjsce>N&ytNPFew46OD@$VE_OC diff --git a/iOSClient/Supporting Files/lo.lproj/Localizable.strings b/iOSClient/Supporting Files/lo.lproj/Localizable.strings index a782e89a677918169060542f87147af767cd183a..6a743f03ee747c872776c9a4c6d93d86b9d7c0e3 100644 GIT binary patch delta 965 zcmaJ<&rcIk5T2(L*1CxZ*furbqbeMPdPXqDaKS^<)sV=+*eqoOO_vtCO9-cmcrad! z8Iz4Cjeme%j2wFD&4Yh{7Y?30syE_Ezjyfi(&1E-V*5O=X-loVTzf!ly0#?ILFz=MGg&rsUUY4Y9j)gZRr~ zCYo@R&)25yp%C7Hst3;;^-@JvL1j+5RN*tj)9eflHj4lX0V=yj;0Azp3}vq%4UzzN z>!7chBBeABbedK{sOcuk%i}D7s)`%I**0Q2g=zSGph|gyv}Vd*h0mL!S!fihGE(!D zqj^eVFM+C#UG6{^HE*k{-2PxKE(2FM1Ekb|RKdOu+r`?1FTm%TT6^39)Vav2cB9Jt zs+H!a&uz}m+UKR^5>W?^V3v+`@~^!#=Z^;Y$K1fhu*(~d@9PUc*gwbjS{Y7#oMK(E z_;o9FA|U?uHpOpRW1M_6!Op(L3-6M!v!_L62r*ma@!ouz50A3k9=9i#JHEe)%A-g5 zNEOtDglP?*y?>e8kFDrZQ4OEcwAk&JM2yNnHK42kqHe#0Xin30faee_W8|Bcly-L} zpwriQH|2dD{yuXhvI}a=>xY&Jb^%g~JwO0Y4r%`;>t)8t0y9p>bj^Oc_#Bp?FaC!A fr;Q`%KM;&snNJ8q=lJK$cr>u2Mh+J4sFl&*Xk*mA delta 73 zcmeCU&N}5ZYr_`CXa3W)7}=ys7!nzZ8M1*`fgzJ24@hP(q%sr%@#F`0B<2Fy^r4 Y1I1IfUkzZK$v9bRwa9jZ2*wCu05=#F+W-In diff --git a/iOSClient/Supporting Files/lt_LT.lproj/Localizable.strings b/iOSClient/Supporting Files/lt_LT.lproj/Localizable.strings index 36e66674392db16070f25f5a8412def93c112f2b..e7470b668e6f2170bb250ba206f38ddbf4a53cce 100644 GIT binary patch delta 962 zcmaJ<&ubG=5T2(=2xSpT`ok*5O|(TLrt~aDG$1Mnib+~Q*>022gse%LO({9G2fc^~ z=?L$n;(t(r(37Ni61<2#c<@r_KVbd<(Qlp!##Ll_@9mp6^L;bl%-)x9_b|NmR>ca7 zH>gY=z6ROUrV4H?tR!xWxVf;|RK!`wc@s-KJZj>vgP3)k8&oxs65pT_d^>bS&ll7a zPM^1;Z5Vv25g&BNcRv->W2HlRb>gT?RbDG4w+}7-ET^nZ8(tLvY#6FxA~wK%kgS5X zZjzIf%Am1n1w=MoMOqo0x4~1xi{PxAB7P-g!|nl9(h{VSNqYshGF?Jx!c?T1p1ZFm z!lq(;ZsLs75gggF4iO1e0;9SZOG+A8W&Nufme~=^`>;Rs4Shn zuB=lcuJ|;7B&g2s%J@fdieEJ%+r7})_JBC(J|YCa`8vT5&RP7SXYtdIv-<+Z;P?KNoC;1?e zNao~*EdNz;2C8TD_nbNt77<-uOV9A~LUdY05m0G-9lK3gd8f8~|xP=5<-16}#` z{xt7=j$F)|-gu_Fb^J^4Q2T)5obS)+NM21o90im)b`kCb+T+8L0@wmp3i-(7{z)^8 x4AR`|&l{S}QJUa1I;bYt*ZHSfdC((Q?wlH}lPY_>uVYJJC8+XRB*V zRH7OlpENb7j3~g$B3eV_;!IN!yN7)lOEPLy$JY)pP3+rLiI9T#X#=@C^qAFX+USC+ z*)CE%fVQzWa2ZndXs@8YC}ZW-_yw0LynE>m-(RtIn@FgGQ#3;;`2dM-uPP0t5b-AH zgffWgD?wA!I#e}1MU6S^&u~*khVV3_jylz8oY%lAH44*4)WbT?N^~!UMn$SfReq4M zFG>Uh%)6IrmRldKbOB7F5+cb1wu1L2Vwba@>{;nv?&<&qP!}!n&PUc35CQJQ&q{QX zGybBwmKL)GB1uB)^qMo-va(tKO7i5JBV=+!nf~NwO_X2k0$8UtQDq;50L{fU_n{m%(}X?hL$R zs-^*L#(6}Bn*`ESkY)DXJ9lQznKSp%XY5~36sS2w|6(;4XMg&uM_&pyqe_2$MN-zoSId9qZ-qDvua?MM-~05tQ-#Z?dQb{@wFBT z27nL>A%N?E5*l5SE`URvmqE#ohzs2^D7dr)5-wc^a?N;+hkq%Yp5!e zrBW$sX$ihEowU%mKozN~RbC~`NDlpBLXC4~PyA*Mg#;u-jE~d`{>wOfusiUD_`HZ* z7c4|O9hR`PjJG~oJe7_ebfZps zV|%pV8yfEBx*4S$==aKJ3!8F=3oBP8(CkDcH1; zqVr@?3dl_~7*JV1aAYi>V%ncmHouGwCv)waf3};3=4q|6>X0c4dYm0Q%9(}uh$tc> zlh@$m@6wFiQ!@&7M1}_XZUDI>Y8Tl)F%ylZ77f?OzWpfL%J=EbPait`^Tz>gWz}Gz zx2imI4NOR*fFmEf%aF}G4KxfD3`7c Kte?B3GR|M=O5i>K delta 72 zcmex%kG1PEYr_`CC&7~gCb99CFeEY*Gh_p?0z)Q4-sJrKqSODFFmg=~2w^N>&1cAC cNS$22QFQx@5XKh9>9Z0UMYgNOFa`(%0J8=de*gdg diff --git a/iOSClient/Supporting Files/mn.lproj/Localizable.strings b/iOSClient/Supporting Files/mn.lproj/Localizable.strings index c24d9b278b01dd84461e93b1517395b255558bc3..6ecb7788add60804340a098f68f24f1df13b06bf 100644 GIT binary patch delta 967 zcmah{O=}ZT6ul4gWehYJ`(ZRVO%Vy!AmUD>YB!1mO$>DBbdnBf$TSHvi6tw=?uB~6 zhoD0J18&5ETPkrz8ZWVOE9mCGdU<?ingYjeiVQXueKlJDH zubeu`#$uYkEhJ8eunq)+K<`x4H2-)P;*b3++_qv-8{NUE`JkWS8=pd$Sr1xl-oBUO zKZO}i9-NvlANq!M9IZBw*5&oxqBxwwq0E@mV1l_AI@9Q8ZUW&QoCN^yJttS%STj5!W0kJvw1H+kY6@yP~)Jk!qwG3K!5GvqO( VZubploXI%Z%2Q>4RMJMOFDX)6*QiGk5$Q#RxLXMnkrKA)(uS-_%qE7MT7Q6o z%;DidMDQR8q9F7T@SsO~^x#o%p0o#lfZuEqj9ZcAy_cCc^L^jUynNk_ef|>Ley83z zE7!;)A770$HE9!811ke-1(pk+rV`F7&g)nb;Zq&II>>3^Y*RVRl>8d4Bep|VSbdl= z-yP+1dOmTm31>i+o!iUmi83#2HGROPGTVpdd2c*!EL$bEG$J+u&^aN@6eV~dgL0S)GufgZhS(GMBC90$WlBG*D zj=cz~8g{t^o%!jg1+GsVwhO=&&Hy=8AeFIiz;?0j!WZCkL#+eu0IhP7Rq02S09>W} zh;2|{9w3_7e z57WFomrwzJyOA90mF7{Km%A6a{$SMHbW~iG*mP%k&mO^`I#A*9z5D{}BU70|&;DnL zIdm*%p4#eUENm3NT|9+xj+!5~Iy$Clsoy<`NJ_m8l-EFM{qFHZeum$li(goRzlC|o zOr`Pi*{CkpKVy delta 61 zcmV-D0K)&&*ahIs1%R{xuuzlGCq}x8xiv9M zvpZ2WxhP#4nF#e&5cEWWwtK@WZOJRo`UbZ47v&A9ex_vPm@V?^>41#U=4Ozrd1Ji_ zs5O8yxd+M;NLWBeo(W11=M{)4YRN3r0l6ubKyQlc5Sze$3(N(4VVtUNttHb$+yyF& zWu#@@%o1XbmQ?fHp#Xo|4nFJ7^@+0AdmkIYpG_@wI*C4y>-zoH+>$`iRQHWm6B&3bc6B0+9=QZ4EGQjgYK(XW6;7QdYGnD>8Cl~oY6Gv q-8B9I%c2wief^)#k6`|Tz+-CZ!Q(%PL$PK|eb2~KTQ_gZdqW4HP2FSw delta 69 zcmbPnlXb~i)`l&NUjimikYwX8VMt^sX2=F&1%^z9yvdHY#V7k5;+dWj$e1%d#Ens4 Z@)gWtR)TDO8M@4cPbneUtV-t7I2Q1@-<)+6<^ zurNn8^6<3CrVd?&)y7J}T7c!kXHx}d6X$6xiSTF{Z>z{z!FiqPCR6fTR7LD6&G5Ie zgg#kR9%l|&dSqTjA3rRrJ4%1etMP3v)%o?F7(3-rE?lM*}cj^)~h(MLuTICbnxKy72rOjaMZ%O!hHO{wtUNk|=_0V%_K>Eh9L{*;uyuTvfNVQgxmrmNELx;^Ay(*vGVb4wN zZ;AlqKIm9VDAWXbfb%M3d0IS6b|8a!26*3x>=O2;5Z1*F<80|9{|Yu?cY(^X8L6$) zYQpA-Vgk7{RZDbLwl1gRx+LmtRo+yMIhml{bZmhUnY|4y9+WVmDK=mW@Yq^g$Q2+P z8|>YYSBu^aFRJghOse$~(f)6f4o(?aZ5`z`nSBWzC@H%lvvIDcO}WN3bucdtRa%ybGdx{9XN!EN ztcuK~cno_G-UHvLy8rkxhsMmJ?vktf?BSck!!AT>uU_1dDSBAUD7PXnSmVe%_BJrA zNVfu5@<3d`%0m_p`XpBv8Q~JaB%43Z7M)S+SnK}B8bvtP{E+>!`m%(ds$P@z)E^1N B;Q0Um delta 120 zcmbRAkafdV)`l&NUqU7;2(V24Aj!d<&rkq_=?sYsWs?)yD6-uu ImN7yY056IlbN~PV diff --git a/iOSClient/Supporting Files/ne.lproj/Localizable.strings b/iOSClient/Supporting Files/ne.lproj/Localizable.strings index 68b899e474803626613171eaaa3fe9d9177c547e..a2c8dbd825d06e0907f416c0f03c60ee7ee15b02 100644 GIT binary patch delta 890 zcmZuvL2DC16n=w7ZCOHSj7<>fBvqk@rWP-15edEMA(&vLAeFFb7A=k0xSLXXvU)61 z3Qw4;Uc7k_gdTD!UIf9Lhaz6Q2em&y@q3d5!%&u)*?IHc_kHi1+4++Adzje%puSa> zZ&N@`d?9J-&|~Z(j68PB*!hTQTE*JJdIv)?n$*VM7HT%IZqk~ql=_ew$lan_e3Z(W ziK=Rj`ABJ@Xwy8esLcLuRqZNst*o*qd|ESa>MG5;p5)hWy=QlU*8#T7nqqK;Ko{nr zE)A|2`$qs2(8wwbz^bVZUQH#aEMhI=WE~lTvtfJc>(hugfhsBmsbRaQBNkkyS+put zt0*l}ffi^Sa}87>W;uf)CRbH6y!R?uS^}=jT(rJ#%upGoW?zM{MCAD;bWuSw6gf zkq`cO{BrsXA9S&P@)GtoIP6|G?TSj;wBE}2=?;2nQ^4=$a{T*iiuW#@%`f$1$M#f& zMMX1TR#OS<48MPP$)qYOJEm!IV3nMv<(eXN6``YnRUnyCizc5@>B*scj%R$-n=?1c z>fF`QdU>cmF0JMm93xVI%$pbm8XymoUF2WAd5gR-itIkA8uYsN;WFgiBJCETi&^ur Np(ggP+*1$I{{YUc&m#Z; delta 66 zcmex&gSG1{Yr_`CTLF_N@U!ukFeEY*Gh_p?0z)Q4-sJpx(aCjp`KEgXGUl-6GvqO( VZa*2wIFoVmsZ}D|RU#RqgaH%w7MuV8 diff --git a/iOSClient/Supporting Files/nl.lproj/Localizable.strings b/iOSClient/Supporting Files/nl.lproj/Localizable.strings index 8d5fc3b13fad430fafd7193add0d5eb0d3c36442..65b652295cf126a1802c9c6d431b75bb98c3fb5c 100644 GIT binary patch delta 950 zcmaJ=O=}ZT6unQ4O+%=$p|)0ylZaH2PKnD(kgyb>UA4bZh=We}UM&+kh+a+?NF7_(6E{=G{5>oO{o^dG|H4|1GlLQ>6kQO`m$; zl1p_ms7X6yQv)lF(<7WL#0;un_psl^5`;@H@zzGpF7_7Hbf)CjX&cx!-3~eh)#W?a z;!{lod~*1pH+PtNqF$>YmsblXEvoU`#1bFPM1wDnRjgwGwF82dmKqmgfZT^j6|$Z# zPD-*NV^A4F2Cbtk6ZmtNtvnevD6x zQ7tfQX@1j9aUv1rQZJewGKju#JCvbY6vML!Q`?Bzd~iR_rPRfAZg~D{t0oLLgU!6U z91;I4&X%w8RxKL5&#TxOnH8Cu4f#Y(jZ4Z{HPCbeQX?-Rbw0ZWUMSC delta 78 zcmZp8!#e2!Yr_`Cgs904IxNx(3?&SS48;uDK&-%!$&d#mGZ<1CiWpKS>k3IuzYxWk e!gG-C_nWGiox?MspvV}t>NlQ<$L#1FjYiL34svW$UikE^`cPTO|Sw?rTZMU6mXC(x|Zru|5 z4zEK+b?Ol0A?PqCPn|jhbtod}4-heZTkS?dQbv?}ChwuW-;d=p@J`hvt-(dq&dh}fhqz8sbm_MN6xb- zgD!c27f0B0aOf|izh1>3F?~(lvi}0pbJ6(# delta 70 zcmbPplXb;e)`l&NKLREn=wRb7VMt^sX2=F&1%^z9yvZLAh)$k&mv4GTAY%?|K0_Wu a>g2%nqTBxjGPW>IzTqLV-6@hWN*DmMxEZkk diff --git a/iOSClient/Supporting Files/oc.lproj/Localizable.strings b/iOSClient/Supporting Files/oc.lproj/Localizable.strings index 115a8e850664cb9842c9c0d94e18e23882f2ea17..aadbd4f192b60bc29f63fc223bcf4bddca84efdd 100644 GIT binary patch delta 868 zcmZ8f&ubGw6n=vyS#gojZP-RJnJsEnNL0LtQbg)O4`SW61_~0k>C%S8q$DXdC#|g@ z7!dn}K~MT0C0npD&O=O632P1OO-!9pr4{_OQL~D5jmkl#uCLP~a@%y3*Sl%; zZAMi2&4ig~A=sqK_T;R1CDc<_j2-Z(%r8m~cmJ3w=?e3qM8+}z)&i=&5DD-mNR~ib z3&d$jK4>H@fJo9c$jW2A1&#_%G|p;ZA`~PMuL4!e(nyPe>;=Sp%E6juDp5tfzbjI) zz_H5BiE~@ejPxvEG({6BH58WdUBceuw;jvYZuQv;pc|mpMCcXgc5ME=Wva8+g?UUj zQ>WBXs$q)%o$&JwoAdi|e&@uUkSLlx;G4yh6z_c+x~R|H;sf3%b}X%JbmXNuAmJWK3_PW=FUilrxQjti1c*d4}>=1LAN@uhgx?< zHl{n|@%J+)Sspgj=53KYBxy$ps-7J&~)e1GEor=1ecAiB)AF6x2Ju|{f-!9JziMY^$ISu@-VlNf0*kwRNesYuZ@U%vLO|*g)`PMXJ<;J*=j9C`ci0x}l~?v*~V1=}85V z9t{s1&>vA1pm9zA&Sn@J;KBg^dUd-LA+z3-d-^DXh~N8;%R@in(_ zmx@%u6Og1jJw_B_IfxbzdB{mx!d}HbhovhjRKwRMde*UTP|0-a{(yYcZqkf$vLeZ6 z27A=8c~R-}P*+9ghFZ%ZKfT?*vo$ZC3pL@2)NzkWs!Px9*FfCz2vC1;Ez4 z?WrV~BE-u;(rBTXuLw>_1+YrGjTtl8U7VCrp>fuYqSka0`3g|=j2g)|8Vbl2X$m?u z(-M_Ao3>BuNbAoVi~{fNPxAig$(hhFMX0Qzzl8TPkhH>8jMxBU^WHB)@&%Q(NC zviRktUfyk5y!ggS81H$td0joqi2+mg$?rc=2V{+k{LG!?&nv@e6a80A2DWCD4@{La9_+OOo^0u47i8pm!$dQs!_1+gl KJMw{eX#WLjcX}ds++nfR1^jjOeUF-iHS*=H#THdx9+TW zF@M5ED`LTg*i{A5f8fH6J3sz_;JI%ySYE9%%)GgG&OP_skNNa#>dn*QlUK@%{55J( z57&TPN+^OAxGT#Hz)ZP0SsShfBAF6XF0_yPg*%E($Imh*T z(_2C}G^8$|5eP>{XF zT%Js$oB-*&6I2uF`m_mbY_JJ9#8XYTeKlVeof0B0Nm9!zh~`&{6+2TnQ9t;wTha`3DnEoQ~~;40T&JC`?2Wwz!& z=mZjm)Z!m^D!g<0Lgj8||Ft7kjHR0Hhw9>#X)EtG7t)Qknw^nll(oD$>9$}>(x4c9 zh#_jyZlo#u*8ubYm0j&&1hQTO6P^#n9))bfhMBLdU$zHX?_^ZiKrm#&whkwiUnoF= znl)Jsj|a-*h2)X85N#IoA5x0PX|dP6c4X}T(_}LoT^y@T=TTz5V~6@AtHS&1XZBt^ HRNmZgLnZTM delta 85 zcmdnh$#(89Yr_^srHsjTGX=Oy7!nzZ7*ZM17!oJjFBhG>=Plp#Ng0fGg82*z3`q?6 p3?&SYfusV1_T;?_Bw0%tav2i03uZE&W}N;mo>64`i~>d_5dg-B8#w>~ diff --git a/iOSClient/Supporting Files/pt-PT.lproj/Localizable.strings b/iOSClient/Supporting Files/pt-PT.lproj/Localizable.strings index d95b685c99c6d6cc61bdcdc00e4b54ae414ca6eb..47e20bba715e8af74bae7cc408f5c80c00bb4009 100644 GIT binary patch delta 964 zcmah{L2DCH5PlE4F~$W$wkh>c-G&OKx|^ zsZ_oxT+zhm3sbbk16VDrX;@`g4t%Dl;`DId!s3Xg2=KRwoDR+nQI9e?-xoE+Zi;K_ z>m`|02PGM7&FZRFk{O!&klKD!md~V`D9XuWj;O2UaXCfrPNrz@Me=b6`2i4_F$9kf zWM&;`GQktVUV{QJ(#h&vFq&c&oTj)Ag*MJbXlUSuaXL{wJ#8lZO`x(uMyf>>titCC zYX)U9RTZhW$cZZ=g}nkQA3NWHPQAY<^RzUZRNWhLY>`PrqU_FE{#OFj} z2jn5jb0DYDkE#OM6YGd=aV-6zEOjmYG!KAd`IgZugOZhONP`N?3FVE;1nvD8qXR23 zWkw|qXACuoo<7gg+>EZVnl?GEML}Mt!sm>7X-i$>PHxc8oy(NIkTTXI`A3A&?tSBU z8yyRTORts;`ttkCmZeYM>AC({v5gMe>Qqsl)uP7HymOAaZ<1<%MW#=fVxiwnjxf0V z7K&}5(0(%`p0@@4`~v(P%!g;pgXq1DUc+N;rrpE5nk>qRd&7YIupOjVVlxTh$pQ2# tRt|bbX$F~bnw`V>h{hkLiNDFp$VmQ27e^QwD(bV;>)SH3oxUqqjK4;!+BEVOZ#KJ-Zdl&Fo!Oc1o9}z?2FgNwYzkWza1y_ zQc*RhY?QRn)YF5anmhbdRQpOFTWaBqO?6&NT<3#WRBu}<{>CI1_-!BxTQG|Wd>8R1 zKm^F~rmBI-qz!PHRKR38>`(BgfeOLd@pKGlGLdfrRVEXpsyF!tay80AoiJ6X!G)9& zlL)Hy*A}WGS|3&b}}j2q|~ZDpHaqq;AqH4{yE6& zKbE?1bF^PBs15teA=6!C7D0R$Ymt29pg4?tG`Qg*-yTH{U#s$%;o}VHaMRc+K}D9o MKb|?fUs5lO|MrH=kN^Mx delta 73 zcmbR6n03x|)`l&NZ$hU}2xnxIEMZ7wC}zk8Vg-gwhCCpd!H~*O#E?4q;v>oFF=323 b(_Oq71-3s6W9(y`zABkfWV=}$W0Wuec6t{l diff --git a/iOSClient/Supporting Files/ru.lproj/Localizable.strings b/iOSClient/Supporting Files/ru.lproj/Localizable.strings index d5ab7a15eb177dc924b35e34d4f4b415e92dee57..8d6dadfa181716b88f04642ea29158f6a0207acb 100644 GIT binary patch delta 1147 zcmaJ=&ubGw6n=xdZ7C4~q1F~DlSRE~QyUNzwNiRf@n@-6s5H(dp$Unbu)8+q)Oz+L zb6D}Fe?Sow>RGU7`yaIbLG>)+_hz#oqbbYI?99CPec$`u?8hVb?Jsxxv-qi2Z&97v zcse90rUq6a-Xc~{uu|BQRK*-%zKxe7+SJ8YA2}~D_o!(yIln_S#P;bXC3#=mzw?+P z#AnEoF5ufoR%~J!lXIAXS(%Na72W_}J#(&r(W2cCzW6GV^13*ELQzw{Y83UsPHyXg zLF<}PBC-Lzo>80CBB3t=(gqM2H5~BNu;)Xoq(xL#!h8=WEu$a;9xKZ9Db(DgQ<|;+d+7)Dn4gjm9xnK-Iy>XDBOqb6w17IXCun1-Q(~z65ZqiE#sKuK79k zB0S1y&A21Dt&r6kL{$MC&?aI-6B`4AcS@8TJQAm-SU2bJ9se1`JHincQ3E6T*|qoPuktPnrthPJGtOob|fu0^Uk8P zr1$faqcO_uQe7V|7xlB*Gu}$N{%^@86oE1>u8SGhIH!N)&L!WgVr-gy;XcOjBETqV zDYZ5YFR&XSY7SBFz*|sGt=>SShgiO=ZTQVMt7G!B4%oi)O?b$?1rx_`AJU$2UvT$Q zuUX{Q->m!(DKJ#5K1SH_fHGW3mT4=AV8*dpMu&>AFM95S@>Ws04{htZAY7i7g`5h({@<6)*Krs31rKX0yhiA?dE{CKV5*y|nby z9_FwfN};#jBv9~HDC8)D;-&VIQ|YmS2ldo9-vq<9lx6?l`G4lUH*famugKwPKd(n4W(@qgF~j*r->7fcY%?(#kqf0)7F5yc1}BeG+>G5yElLDk|LQdZ z>oUPfRwX0GF|Oae!w>s=5@LspuL@uXtERb@qmn!%9(Z8&&~3|6P8}yW_Pu*z4(Bz@ zNaifcq`2*R$ZM{jPX!lQHN4yjC_l8cW`zdh<0K8R`*_pP^EXYqGEB2`K5b~+PMYw4 ZX`!3&B>ztrM$lmd^f$TN%&Rv;e*o@d*<=6! delta 71 zcmccgh4tAB)`l&NbK)jX=wOp9VMt^sX2=F&1%^z9JRq6DkjhZRkUDvzpyYI(c*dN` ZlM;ls&xmL2VVvGk!6>r*UK(SGFaT=07gzuQ diff --git a/iOSClient/Supporting Files/si.lproj/Localizable.strings b/iOSClient/Supporting Files/si.lproj/Localizable.strings index 3429cd544f930ab2a83f8d40d5e3c83dcbc8ef25..707374b723d6cf351b8e4ff495f8cffc7dc85794 100644 GIT binary patch delta 959 zcmaJEkZd^z*8U^P5u%eWj>k>ox@-X#!R#`w7_~ z32okJB45}t}d4v-BWnSfZ?9<7@3~+@rKuiNjHS7zxORY~~3-Gxn*FJXu$y`L$ zdtMd(y_4c@%Hp-Gbyg%7k0x*gvwT=5XFDn0&qsOg+R(*zk2mZ;pbLL{l;rMCn%{lA z%v(8&kL||z)oY8>@2$<<1Yhl(*V836sw9JP-gq%|qK#S>$>XVCao%!9c`kWwV&=g1 z&zf?OD5sw}>O#aQ!|voI{iCST!!{LqQpsyv+SEc(Efmy~sz(yt+>1^XOb2|^LlIg= zt*>ZUN9pV|7qgRk%~1(&5Lsr|1xPt0eE>@aVK=ZcNKB^nC|N%;Oct0yI;2}4qzg}A k8Rdn$_kY?jg8dhQsn$%UD~j;L?6`jWSjD!-?yCp!KQ#&1Qvd(} delta 60 zcmV-C0K@;@+y&~;1%R{xx>A#1kp>5J0AT=g0Be(9o*kDQQvrGgZvbupWdM`V1{Alx SQvrklmzrMz6t@Ih0a_EaKoo8O diff --git a/iOSClient/Supporting Files/sk-SK.lproj/Localizable.strings b/iOSClient/Supporting Files/sk-SK.lproj/Localizable.strings index b7bfff22510b903743d335c89cc742ec8b32a7d8..bc3b85fb7b4d04f1478dbd7def595970f87ed2af 100644 GIT binary patch delta 1127 zcmaJ=O>0v@6upBcmJ+3yYNHi0jUNk3qG(r&l+aDP@S~s>q~vwdls2TkystKmo4WG{ z%wqnAg3_%TS6#Re1ebQ{%9TsObLZt@9ko0r^L6hz=bk(H^ey-PNACG+vENvlr#0Hd z*Cj~_ZD1E+OkuZ#ox+)r z@dAq~5xW7zo|T&wBOx#b);cg5HySdvaMpxaNp+M~!+aMv9jhS*9&5@JDiqzrZQmlb zZ0Sv$txkRB(hZk8M z^G9GCdaJ{m<;6LiZJ{^ZUmt5u%)ECU=dM4nedKqR_B-tscLYxj2&uZ=!*u+AQ6TV~ z2F5t40P+q-1#15ml4ZI=V*3D(kdptO7WY|uFvNW(M{9BsxUGL8I5wR}dD#;?JLy(dcS6n_DRK&n{5En2MF@R*t7%G9{Wk7KtfAYr*!rPCKQcI6LW%0*DS+=dTep*qPZJT_+cL6A5u@oEdKJq4r zJE1%g)c}!AHBi}f3Brn)Z-Uyxir@^wV?>f{)Z0K6VS-c-h1XDP&{-T#m^R4cv27ic z4EXv+RW0)8(P91bs+w6979YDh=xt$aB6kt@QS@?SFgkE^h_!}(<*8%*=$6T)#cbhrRE&<0 zIA{@e7X?5cDXZhD&@=w!6!gV2Dm^cT$d&bBLIM z3-4m6xX@1!gl>%Z1EOF-LBS&KTv+@9&wWWC89|0OZ{D4I&pG$J`{GmL@%O}o*J|(f z@)fF*kEc$WnzVsa11pQuGEOdHnkv`>>{qcQ!>4=rYNKWody8tOQtIophTJw4q5~!M zke|(3qfG=t^0v21YD+~wifU?~OEq4Ybl9$p^Qo*A-6<;TzDCvt09uBsFGK@81j#yR z10zl(RY9X^6-1gYK~^67b?|ujBRHF;iLM}xxDQm3B}i*V_9|jknn!EGR3VQSv@NxLucA;S?LZk#Jtl_;52p8)vVj&*aNb6IDaK;6>*UjP&olNv?MOhroTZhG1 zNvH!xAj^YYMYm3(XRuSGSTFwXHS>loYVRJ3zL+50su_Ux1lM+U2b-Wwq5849B5xkYmy zzPZr=-!gE0!(%pc_L0MXejkc{7S&{V5K!jV1w=WfFhnE=zzbM8=#hyXrs`M5s3;sF fs}GXJXR&N-9KPBAlg1JLx#%AG!R<3k%6I+(H_O-8 delta 61 zcmV-D0K)%@-vyx41%R{x@Kck3yaWj#0CWIh0CNCqlgFPOm%1_m4wqn50c@8Qn)a0hh*L0Tj0|Tmf1W(aRO9 diff --git a/iOSClient/Supporting Files/sr.lproj/Localizable.strings b/iOSClient/Supporting Files/sr.lproj/Localizable.strings index 4cd2c14ad500395141d039991e5a315f96701678..15c18e04902ab88acaf81495dc72a09d057b473f 100644 GIT binary patch delta 1194 zcmaJ=zfTlF6n+!u4su#txCV_e!$whKJTI6iYD5T)5{W0lXvDD0{SYn(cbA>T%N6D< zElsA_zr)0Yp4eGvXQwta6c!d%7JhH`R$vjbnVEfW-uvG7e$1cU%#YuhHy=dae|Deh zw2rS$ib7h06kv@(dJ0KHr>Kgvh4TRxTdY$De?9nY;_Onxd9r_-YOw9mJ+h^d(deNs zZp=L)1D^z4(G~nx;2Ao5#$`W_!jU;5TWhQq{<`j71EWb>AARvtL@$@b=m|{?`#OKo zmQH5-j~rUZ?FyD_!0S53Nj?f34UkrW$f&^}P7S&W_$rz~c14`?xM?~D26)UU;ir*z z7q>l!RCC!^psSD5RgmXURk#+(qdSzt?juwiJKrI#==*JP&F0S!_ZNZ7F}W@+RIGu0 z6*A|%0iD689oHnbLB%z^nggpUfLrtmwt=$^fx(*n!I^vLBKg=Ewc~8eDF}X2ib#Z_ zU{~H|?Q%OivGC7F9rS@dUA;J>{kHAdxzFR#yPO!hbObGPpeAUl-PyhseVG@-m#5@} zoRo^DWl7$Y*CoqSa$4>cF+{C8RJV`IW48R_thX4;|E*UM6(~})vLw!BT-oi6zQ8jQ z{dz7&a*Af-Uc@E^E?)r70qQYm2Ev@SrLWnAS?D*>ULFzdBR_y*rLvuW- bG{WH-N|8GQuJMTS@b6U$&EI;j#iznwj`!~6 delta 109 zcmaFxjrGqf)`l&N0`ZeKd}f)vX0ZUjIg0^{2@pF0u@#HKWPKsY=?(FWX1dBO_AGWl zVFwmd7DE;bAj_0Rfx(2unZ=mJ2F%uG0PzigqQ;Xa&K2F3!1#`Fa>quI?W@umw+RCP DNc$UB diff --git a/iOSClient/Supporting Files/sr@latin.lproj/Localizable.strings b/iOSClient/Supporting Files/sr@latin.lproj/Localizable.strings index 95ccbc80250b2ae7d626bf2e62be57e2ca51f6ff..e7110e701f0ec132302e56da816ca5fc07164af5 100644 GIT binary patch delta 982 zcmaJ=&ubG=5S~X#2tjB`s5R8$CRl^DP3pyqv?9IOgP69#6rrr!bg3b6w`4ahDb(so z5Lyo-ES~h>A7Dl3!5({*f+xX4@hk-F#gmHPyfidjMV7a3XWlp8H$UF)A4J}Li9CL< zUd)tkkxex`E*a$016XyeG^`RV3qFI&*d6TCSc0h03ci|%X<%QYijI_cmzIES(hcrC zOa&ijR4r)%;vi@ZyN^SUs=E`j>ZRg?N;KFms6>xN74GfJ^4mwT%?3a#;1yjDYAymr zyOWs#H4pYOD22AJbF`sr&?1x#x`{k<*l**c3WV@9bXDyP4ftzdl{|%MN!PImpG{+N z6f06?ss=kn)gRG42*&1Ap6`#uN<|QfhzE>=&i4@ z0I9&cUIyfke7qpxUzj;z5?|dF$Cm+qu!tWpF5XN^kg+G1o=N})=@uj;L zC2tzhyd+ zVVSMgRV}mGEfc+Q6Re0B1;-?c!k$5-K$t{GG+Wag zL_vX=;#&Ry2qC1<`3KNYP$ALM(9j?uD&DMpigWqUX>a#-XWqQ`cJAx<uv?ko1AI1x&$h4=dHNR|R2B%aB*Vd=DoL!@&oh zaEcf@5Z=XUV36uYeieJ0boCPGOQ{-C3zVmul*YS)QZC+d24Uf^>*^YB%%wv;trA5c zDrD-Q)&jAC=x5mT@i>NRRN99)2V@$O~o?ut@Q5 z$7dE(tS1u}2l4t){D?iQdRczjKhOWZTz>OsoZt7Sv;F~LayO7>h}`PUbLnG-KlEq$ zZ7($zkjD?dSzOA@TWg@^fy`0z3NWh>7R%~F(s;zna~3adq{CNbH4)x^q*A9f-5t6o zVN-H*Z3t|m_Rv0d?$6jo;O7x4E!l*n(mgJW**96!L2Ko%OXl{Zy9T!`HVV%z47rp6 zx82dA@~1c`P>I3$*vTXLcp_0|oQQAQ?gT2Soq?TG=Km9AM!sWhel`bcRfZG=`GN`VS;0>k0BqKN8MpHo2gV uWwJva7rQot0z)=K-sFYVvXi@R@i3<{Z@!s5I?jFhk=y&I9(x-;b@BDc zp@??Krx0Tir#em=cMdhN23YT7h((wB*bU+H9P5DE)>Hg@)Pn7hs_`>V)p%=u@}3Wq z1lXZ#*f-!5SyO=&7YQIiC74)ii~zfVeb;2Dvp=z=zN&b2Q(c(Q)aJF~6)wJ;+7+n2 z1$1Dx1F(T*n&#nvn*q!=-~=)p2WjE10iF)6AS)N^I$k<9yaArjNy%yCJHYGEBDHMR z4cz&3eF@AZR1>Z)Rp=HKFnb8q!z^#eDSqXttFIg9;eqN%$%5 z3_fkSrm+pO(_qsXTQvb3&r4FdV2Y1NbJIy~s0_a!E%Tj?0)HPBFA8~$=tInZG>CBS zlh4)KB7bt{7wqY;$-21=W1*X!RU5G5b)U)oX>{+p;{10tGrPq?eCn`xX&~89T`0btyD*uc|054NJ zgPRH<&nA+3W{DWviT|LIKZ{GYzr9lh{vW3-!s!;t$TWu(({4JUlXO6Qqpr^HA8EB$ F`VDT(>~jDB delta 91 zcmX@{jrGwh)`l&NA@P&t+IU#A84?+)Ci^cIpWOAFZ~CElMziS!28=wSNkH)uAkJgR tWKdwJ1hNzuv?u5HNwelMAevo)MrSZSOVadHq7tb)CPeI83^)Y&@THc_*Iz0InoQr9=x3UW8uRrR*W zpV8xsG0R85V>Lzxk;J}S;7_@FE_v#h!>Y<2=mJlcnO4M>_(^Yrtt6#$i7K z93HqdsuS3APMA!LFmb9QFd}YWSjYHjI{jUjp$uP<3}2X~lHEjF`)8DAY_9 zR#Q7AekyE)tHUDCQhe-Wz62c2;UTAi$|`U&9jk-yS^-}6`mXA~S!6bdT5Tm7D zjBeeu=+gVph&HMXfA6G1OAbEU5u)Gd3-(*baDQZDu3-klm=uzPeUHcfB%^Tyqzyuts|1rfww2u7uuMEzHB>4ZI{cIWx{{?|KvY2qhMm(z&= delta 66 zcmdmXi}lbs)`l&NlL9ACXlCOtVMt^sX2=F&1%^z9yvY~qMJHPc@=a$7V$5O9XUJnn V-QF0)IFoU**J_dNcOn_1gaP@&6}bQa diff --git a/iOSClient/Supporting Files/th_TH.lproj/Localizable.strings b/iOSClient/Supporting Files/th_TH.lproj/Localizable.strings index b6c1e547e5e6c2d060051b170de708927991ebcf..4eda54f09b765466f58d5d95f73c8cbe90f95fcb 100644 GIT binary patch delta 880 zcmZuv&ubGw6n=vw#%0m!(l#kVCJ|GDByII(En4fL9-rHz{=HJei6!5^SU z5Bk74h#>w0MG$&f!JAN|c<>+|M8RvJM-h>Jv*|*b$TBng-oE#J-}`pnY{y=Fi#>cN z?w99g$)y^e21#nsGNL9{2GJZM2RTU#IO{mCV(E$+t>UeXo;957R1G_Ie}fiL+op@^ zRardY@lo?|3kjb*{<$%@dEth5EL77L$wLlRd1_#kch4u(-|ND7AW^jpfc20n5+VWa zgJcP`^-!Fafhh>jqMd=02tmPT3(WzQq$QURlBrUmjw z0FtFiGO$-b)xfSlf=<0Ji#*%L(dk>j)to+h>OiVuUqb9)tsv*)aYC&Tw-2ivba|aB z&IBoS=e96a^0F{f+G<_V0FK72M02up#o|jroWE}yJYI;WWjIlH?Ez2t_?E@lAjy9| z=ANH1`17d*uLdUnerfW(4U==hS^jV=5p!XL)FOCf%fPQ`gWTn3r9AI`9ZHAMf5$1p zoucw=k&cC?@ZJ1r^`jsXDzL@C5lJ)qR_PK;-=QXkYGR;$t9YW6;(rryF{9q*gf$#F z*y9^N-5FCYTMV7+#?ZIZf!&oJlYL~e&^v{dr4I67RD1xLAAK6;*E=Ue?8$EI-iuY~ aGrFH6)7#YdcQ8kRf6boQ{Bl!Vv-SWY;Li#G delta 63 zcmV-F0Kos2+6AJ{1%R{xeo>RqkOl{I0AT=g0Be&ko*k2@-VT@UQ2}fPZvbupWs~8r V6}N&?0fYgUSWN*Gx5!xmP7@v-7o-3H diff --git a/iOSClient/Supporting Files/tk.lproj/Localizable.strings b/iOSClient/Supporting Files/tk.lproj/Localizable.strings index c2571af504faaf2424beed9c04d2ad4a36665542..2618fad11a2fc2ac08b43d52177f94bf23b1dd82 100644 GIT binary patch delta 932 zcmah|OKTHR6uuXlhcSvaG>L?mYe8K|68r&bOI;|EnyHUULEexHjFJqYuSKPx}fSPse8=}RProJm0$PL5=+C6$q z_m`wgPfjOeJp_Hx-Z^wlK9%~GC9?-@RMk*x3r~NF$pXEIMfKOJOl~Tqt^sL-IY){V z@_n#1!QWw(Mw_3 z;SD(xVe7h5mSs}8#JtB0J~BFBY~i;Fd>iX7Vm=<5m5rEubO_F%W0+N^FJXo*7PC3i zUGudI41-tOb3`w~RVPFJFiYF-PSKHalD>u0bpJ}6s{JV) zS~4z&bm7)LPDO@`zhQ;009pD_?aTLn5H}Jbb%xPv{XVj`^vy+i_<#}%CMtTIk}~7t!JG$UzuUz;Z!1w6e2AV|;HJBLZ5_Ou@fx*t zqM6hhl=X@w)3?V#&7|5WuT6B`M`RXY=dfm>$;{^8G^5H%n&#jv(`=5@OpaM?-*xcl^7xs5heN?2Pp_U$@4UG#S5m(Kzj4{7 delta 65 zcmeCU!#d>xYr_`Cl%UBEB-!{&7!nzZ8M1*`fgzJ2Z?b-a=wv%VzUfzj7;~l{ac30R U9v95m$2d8}Q)K(PD8?vZ0LqOO2><{9 diff --git a/iOSClient/Supporting Files/tr.lproj/Localizable.strings b/iOSClient/Supporting Files/tr.lproj/Localizable.strings index 6de73fc4a2592a2a235bf144bb4f04fb108c2417..61a8ba00d2da18bf9abc08da42130d893758002d 100644 GIT binary patch delta 1142 zcmaJ=ziU)M5S~Tu5~3W2^OAUqZXPPA(R5x zCtux~eMAvLQgBVzajqgTw(bHeVUk1=OG5EBSOHEQ`)+{I;@xavXK6|OR*7Cz6DJHc zd21}sADd^kE5nYT|bp zcPVrYwg{iGv%VZ5kmD zG-A}_{@>b5Xh4aQ;-b2c5j_p>XD_pUHCz;Zq=7Ls3Gl0FM&5C< z-g!|a1icU$2O9H}Zx`o;@&;NYZ4Pjj$g~Hp^3wdWZKn)ezwQ05NoO!`qIN&u=D~dO z?=i^WJDrSR@ql>;%LCuTOwve~Nn{7{7%oFcl5ywTha~P~98bzc(1#Elp^B(49jk*% QVFr@viYn|puBtcrzv=YvmjD0& delta 127 zcmX@{owe-^Yr_`CiulRzmI_JcG5leuVo+d6V#s7D0@CRW-+*)~L)m1-Wx|u^2=YyT z5zlBgIU$#eAEu;)A(0`EA$9uRU`APKZJ?eKhEj%j#!{eQBG{xvhGGUo#=OY~*+jS3 TBrtwsoXq7TvRx;GagHzm;WH$X diff --git a/iOSClient/Supporting Files/ug.lproj/Localizable.strings b/iOSClient/Supporting Files/ug.lproj/Localizable.strings index b1b6b24de1cbb62a5f6bc78ad733c4d2636a128f..933315638702c216d42836f4f30bbe03fd5a4c4f 100644 GIT binary patch delta 1111 zcmaJ=&ubG=5T2*z(FT#$Qf;X}Ho;15aiJCzky1i0S}4X+(F#r6G)o&2lbB5jfkJcB zo0U22)jz<42cb7F0l`bbgPsJv>D@!cli$3}((+nldB5h(eBaDBZ{Pk(eEXf~yjOo# zAKxXPD)?&TP?I)rYG9;rdW4e)n?q|@t61-0h)0F$*tHR}iM2%~6DjdED#Eu-bHqD? zBjG|$-CTG`0b*iwho-PEAhK!v1y|yPMVN(AygiI6b}e)70i#U2dpWhI!igtp z{_LkXyLEc|zCo*-*@5Q<@LDG0xE=>N20&T|qM$ZZ&e=YFIb+0-<~1Qe6|+P0 zk)EQn2^o@pHjs4#wf8gy^C<63>$!Q@H_;{Ow6sG2V%=@ErThtveu!?B5 z57WpG;{h~pH7tN7jlfxqG%`I(9!J{bfq8xweXnKm7(AZc?>{aT`%UK&WWR|2r(`2I WM-VJ!P92iwyRU|K-#t}ZL;nDhN$ZXP delta 118 zcmccfnRU$z)`l&NKVl~b$goW=Sj;zh$zvhz(`;ASjMzNc>e#MMz9=s|Jt~gTMOd3b zfx(&WIGY;~*ReSRrH!W>CNPRi*Rd%uXaKo}Kq*fk+X<+^kWHPv9bM2r_fCVO~-Kh~H+i&HOh9QYH(A6#LPY_c&I5_jY7>XPaX zJHc*>Dg6g56of;t6t1zfvJmaWLK_>=!tYJ8oNVr7*_oX;?|t8U-+TM*=hWMc>5bRo zYisoqwW*IMAW1Q;;S}O6sz zBLW`hm1gOp_yo5@gX9?rny|GO=p;&Dsun^WvT2cKu+~8pVC6d$r=Ks0Q|jfD+`T)% zWlmPdhiqM}9h|xH2e3tWT$5|c9YJ;%QN6KO3&1`-hHq$mV_>kw(dFBsc!<;H5H>MM zeYm1gqRk5~ZiBbK4+WK@Niu&1%kgE1DCyi}K6_klxo2ho_8~>eaC$x^_+E zN-Pz(HikStR!PevwMXs+7AS-cAv74vZD_AbIuB|`d{ jsjnmJ?120~qb!1)K*(rQO2tW2Ged6vjm_Y`_+9u5rzh`i delta 120 zcmdnf!1n43Yr_`C7s-?BmUB#&TF%2~&SJn~#9{&@ZCQL+raPoC`b};q5}STcgptF{ z3MgpDV$Wj5;>@DJpv|Depupe>6fp$y99f)z8tqsdSS)}n3!sQ4kY^0U+i#>W9$}om OtB_G-dsq(RGhqN$iyVgl diff --git a/iOSClient/Supporting Files/ur_PK.lproj/Localizable.strings b/iOSClient/Supporting Files/ur_PK.lproj/Localizable.strings index ce28e219704843f67bf4337e0c4bdabf664c32e5..eed072512ac4a3dffe842b98d849df7026bd2104 100644 GIT binary patch delta 946 zcmah{L2DC16rRCGw;_i#Dcu;M-NY6J$(G!NDmCaZQydsrOdiZ1@Pk<*9W5-pv{`JQMXc3Ujd+^5TG zwj$lMgOCoAwqSd>3`G0zbw$3F$|%ds1xK`KDtV1QE*UTS2O8fMrvpc>+m&2VID%6s*Y4! z&@k!#)damCCe-eAnW8(- zva~RiZlTr_U1=xMlm zp4{~C9fa+TG?jjxEUI`}&OAAd$j{h8dMz@60G>RGzm1hgLHuz4r5R^NX?~s*HH~+g eCi?zrXC`tUT@>NWOrfu!zE)-G@YVzQIQ<(~)6_Kp delta 60 zcmV-C0K@-^*#(x(1%R{x=1`ZwD**-vbO2!ha{z0TDV-gYe%%h2R#5?L1#bXu0A;t; SQ306&mj+1z6t^&00a_ECUK9iX diff --git a/iOSClient/Supporting Files/uz.lproj/Localizable.strings b/iOSClient/Supporting Files/uz.lproj/Localizable.strings index 7a535fe984140f11bc8b2676dec499f53d95253d..72f6cfa58b196bbefd29d0cc2a6c1dc02d57db95 100644 GIT binary patch delta 906 zcmah{&rcIU6rPE8U20>Lr3eRbK#c*?YA@c@C>J?2>>`0^qU{zo*sylnt%UTV7`*Ah zj3%DcqX~%+5B>rExNcwtqjV;)o+3R!7M ztkSPh=NLz*T)+3^1ihX)LFLa!vnj)=)9cXO zRt;-@Q=L|-ZeeQdU(^*>4&F_Y^7HhOUg`#eq$I{cb_HV`p7=p_%XBh(WupCpk%^Kr ey|1fu=q~;z*UNC|Hc?+r&j+%9>&q>9$N2|hm()Z6 delta 61 zcmV-D0K)%}+6A1>1%R{xY*CY-ya)wh0CNCqlbM|zlm6Wfm)ubSYz1!sZUAMI&juB@ Ta8dz;0hcOY0Tj2kSpixT{C5`1 diff --git a/iOSClient/Supporting Files/vi.lproj/Localizable.strings b/iOSClient/Supporting Files/vi.lproj/Localizable.strings index 06334a05ca965fa9287e4f67c336b8faf88a253a..16b939fd9925dcf71bbbe21ad59a3cf99d1a88f6 100644 GIT binary patch delta 955 zcmZWnL2DCH5T1u-jdc-CDybCPOP}@)r1UVoHfv^Q6 zL2DXmWzada4nl_tkmtfa!A%trg0p9e=?Qb-uK`u$3DSm~2v*X?&Cj_n58GE;!oBz1d(ZiPzvp{yzCP{y^15&JRQJvm zDabOvig?nHC3ba2irsB?K4+dRu$Eb`GAv?Qs(iJ`Sz&ET(PdhGMe@Y9BkGLh!noe$1jEBGgoJH^qv)*ILbCyG_CsUM`VZF{x$?*r^nVv|UPti?o zTMjAjO3!gt7?%m88`T1-8A;2e9AeHwRbjR}iqhKVX{|o*pPC2OD9w~IZWWmq*;~Q) zIScstPHIFQ;I>azDU8x5ozeXn(ThKhjG02at^i_y*?%>vKQpY4@`86CIR`aNqMgvMgb?yxmxKQ9b~P+)s2rTtTU2(qzm1)QV-xxh%Hc&Csk uGcF{6W=|=}-L%9Xbe00*4`OdkaRhG%LQLDyU3}t+Xzka~+V{<$1AhS<;_>wW delta 71 zcmX@p&vK!eWrNP=$qGwYIDY$u`v3M@7&Lhwzr^G@pUoy4OlA>NV9;hzVBkz%H$^(= bSKNlwmNJl%%`#sM7$=Lp5!w9U*Bc%HGb0|I diff --git a/iOSClient/Supporting Files/zh-Hant-TW.lproj/Localizable.strings b/iOSClient/Supporting Files/zh-Hant-TW.lproj/Localizable.strings index 6d9f2534675ddec34a4273dc59c2a9f162be48c7..0b35ba3a0b74dcbf54fff14d9822c7dbbb76e9f2 100644 GIT binary patch delta 959 zcmah{O=}ZT6ul2^TGH{Oomxy4^YU5zO5GOmBXpynpb+Rr7^ZF7q=_?WCLwmE-H9vV zE*@?~+pQ}Vy73PPF8l$m`~gxo>O!dJPBWNJ1R36Y^X}Yx&bjB#(;r8_ZjC&7uT~3- zx2Q@@JU(gaPz|>>RtC34++4&oE#vfX-o}!QCbjUkiJC6X9@WE2srP9Gxtnx@w?3xK z%Yth1tDH63K`@}k?unv$p-e5WP9Jfp&T7i$FUcu>KAJFlIb}W8$f^OP7t%#yGzbC^ zt%2GL1&XXHs5C8sOj8-sN;vO=r-3hm(+wLr5Ty}s0#&35(n=_O39%|&Mtj1vOpO_U zWa%m;u{)shvCBu$nf;=g$9kWSR5c|EUe-rGp2R=aaKK>}W7K?nzx>_~&+xf4wR4 z$qN>LL+!mCYa%$*Se1Ibzdg^bKkV&2?hZe}lWb7VxO$&V&CEb=^tPQ1YG~0KP-M8tB3y5_w6Pc@+FNKlI zF>m?)b({64vt~W7rkz1R`IB8hII&>_h-3kH4J!*h!&FgajEaIoWd0yo{Eu{oCiFkj VIKt43`{XmGa!F0@ewbBv>_5#@(DMKQ delta 80 zcmaFV$lCFZrC|$WnyS1XLq3B7LkUA7Loq`(5GycbGUNft42D#OB8Jq-jVC3iUsGkw hVao>!r!q|bFkNJOmKtLVF diff --git a/iOSClient/Supporting Files/zh_HK.lproj/Localizable.strings b/iOSClient/Supporting Files/zh_HK.lproj/Localizable.strings index f59392b323d870d8fcf7b41a58ca4827995d3dc1..393d734456ea413e15766e75555418e69fb40643 100644 GIT binary patch delta 1137 zcmaJ=O=}ZT6uqfw3_+px!!|W7lc9Daj^IL+A|+&_#oAIL3Tmg5CbS_n6EhP-R_&rt z=we*lh1>pwLjQm+1%H8{?V=mOO%^G*5zl>-LHlY;c<;@7@7{CHJ@@6yPW&s!_ouoq z9#NS#@N>x^pVqN!U}Uj-j-7)ugI2NDus+6+hz+XatBsr{))rM%rsTU+LTsC6iB}GX zd1WBZPrCQ!o{@*Nh`^v*_%0&ZS1AH5sX`>=LMqV?Mh#yrb?*S9O1!r{e6Zim#Q3-~ zcFCa%cRo+>$5e{9l8Ma)g;`g#f%tWxwv_a!Fau&e;H?2yup2$L63&VcZcq+oT3F|C zQ&l=V;EAqCr2_+6xNR$>lCq(Qv+^|EMhQZ-id2hCnxPbC8&oc4xkGX6e!IhUE14?* zRw%`s8l07L3 z5HB|q{ChHScL`U1k5^C8ZK)_QE(9jupC9L~8-uYt+rj8H?^J2%5Wfr_@W5Phbo<@a zS8sR!7Ve|KI+gkTOqTaXGG-y%|F=jBmZ6yMoymUX6lb$HrLCEqazYyCqxu?VgYu!5 zDtt)#*nrpus_vNy#xy^jPAuhd-b8n#!_o#xjttjrmcB1aCqW}IVtd#kX#-vx5JH8U z|5D1o#{q$@b31bzK`d-U= d08R@~uw{gHlc0nYvnci)0h8kY6tnL9>*Xs`S!Z zLEd5TPiPB;f&umBA@-mL(S!a15A`4Ty=`LSMv!G@cHX@Aec$_Lwhtnk-y+ZV)Q{P@ zdsHA7UxhSPX%#yUV-&kN>>R{2<*}Br-p7y(m)7uCM@N~B}mJG>?OnsbRDe;Q=Uo*fTZXq zS(tO6s$iBw(3y)_HO{xc4rIK5(MLrYI7Q4WKy)ykAm-z90;O$cA4WOQQfg)80b8bL z$o0rK?-o^@KTbx^i{p|ihXS5EZkJy;akD+7tjkr@NWbCa4sUpO^Bh+j!+iI{Ro-bN z`R9W^GvTNxf>V5G^|F2)*lsY&I{4_Jp`pn?yqoe&l8%o2Sv}xMvQY0{i zcPFlxwF}BJ$ys%}U(-y>DoKe+dmg&+&`HZIKARcgw>NsP%^+Tb`Er5cvb?Xhp&=Xj zQ!~b|CX?oitp?{iLFKtRfXIb>_Yp|}a2g{8P2E)O%D<@ipc(%|ROuuNKf7F){r@8j RBXnJ0tINcG>Y;LDzX8>x(&7LB delta 59 zcmV-B0L1@-*#(Zx1%R{x-cXYug9Zn50AT=g0Be&eogI_>-42&dQ2}fPZvbupWw*^y R0hs}pxJ&^Qw Date: Tue, 17 Feb 2026 09:43:37 +0100 Subject: [PATCH 14/22] svg-fix (#3990) * fix Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 +- iOSClient/Extensions/UIImage+Extension.swift | 19 +- iOSClient/Menu/NCContextMenuMain.swift | 16 +- iOSClient/Utility/NCSVGRenderer.swift | 324 +++++++++++++------ 4 files changed, 248 insertions(+), 115 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 696786d48e..f7ad38bf23 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5775,7 +5775,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5841,7 +5841,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; diff --git a/iOSClient/Extensions/UIImage+Extension.swift b/iOSClient/Extensions/UIImage+Extension.swift index 40b8be463a..797238c539 100644 --- a/iOSClient/Extensions/UIImage+Extension.swift +++ b/iOSClient/Extensions/UIImage+Extension.swift @@ -26,6 +26,24 @@ import UIKit import Accelerate extension UIImage { + /// Returns a raster-resized copy of the image at the specified size, + /// preserving the original scale and renderingMode. + /// + /// - Parameter size: Target size in points. + /// - Returns: A resized UIImage. + func rasterResized(to size: CGSize) -> UIImage { + let format = UIGraphicsImageRendererFormat.default() + format.scale = self.scale + format.opaque = false + + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: size)) + } + .withRenderingMode(self.renderingMode) + } + func resizeImage(size: CGSize, isAspectRation: Bool = true) -> UIImage? { let originRatio = self.size.width / self.size.height let newRatio = size.width / size.height @@ -51,7 +69,6 @@ extension UIImage { } func fixedOrientation() -> UIImage? { - guard imageOrientation != UIImage.Orientation.up else { // This is default orientation, don't need to do anything return self.copy() as? UIImage diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 820a4c9c61..9359a89ce8 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -499,16 +499,18 @@ class NCContextMenuMain: NSObject { let deferredElement = UIDeferredMenuElement { completion in Task { var iconImage = UIImage() + if let iconUrl = item.icon { - if let image = await NCUtility().convertSVGtoPNGWriteToUserData(serverUrl: metadata.urlBase + iconUrl, - rewrite: false, - account: metadata.account).image { - if let image = image.withTintColor( + if let image = await NCUtility().convertSVGtoPNGWriteToUserData( + serverUrl: metadata.urlBase + iconUrl, + rewrite: false, + account: metadata.account + ).image { + let image = image.rasterResized(to: CGSize(width: 20, height: 20)) + iconImage = image.withTintColor( NCBrandColor.shared.iconImageColor, renderingMode: .alwaysOriginal - ).resizeImage(size: CGSize(width: 20, height: 20)) { - iconImage = image - } + ) } } diff --git a/iOSClient/Utility/NCSVGRenderer.swift b/iOSClient/Utility/NCSVGRenderer.swift index 95b7d75c7a..0568ff892b 100644 --- a/iOSClient/Utility/NCSVGRenderer.swift +++ b/iOSClient/Utility/NCSVGRenderer.swift @@ -5,90 +5,254 @@ import UIKit import WebKit +/// SVG rasterizer based on WKWebView + takeSnapshot. +/// +/// Design goals: +/// - Render at the final pixel size (avoid "rasterize small then upscale"). +/// - Prefer inline SVG in the DOM (avoid rasterization path). +/// - Keep alpha edges intact (avoid trimming that kills antialiasing). @MainActor final class NCSVGRenderer: NSObject, WKNavigationDelegate { + + // MARK: - State + private var navigationContinuation: CheckedContinuation? private var webView: WKWebView? - private let utilityFileSystem = NCUtilityFileSystem() - func renderSVGToUIImage(svgData: Data?, - size: CGSize = CGSize(width: 256, height: 256), - backgroundColor: UIColor = .clear, - trimTransparentPixels: Bool = true, - alphaThreshold: UInt8 = 8) async throws -> UIImage? { + // MARK: - Public API + + /// Renders an SVG into a UIImage using WKWebView snapshotting. + /// + /// - Parameters: + /// - svgData: Raw SVG data (UTF-8 expected). + /// - size: Target output size in *pixels* (e.g. 256x256). + /// - backgroundColor: Background fill behind the SVG (use .clear for transparency). + /// - trimTransparentPixels: If true, crops transparent borders. + /// - alphaThreshold: Pixels with alpha <= threshold are considered transparent during trimming. + /// - Returns: A UIImage with sharp edges at the requested pixel size, or nil if input is nil. + func renderSVGToUIImage( + svgData: Data?, + size: CGSize = CGSize(width: 256, height: 256), + backgroundColor: UIColor = .clear, + trimTransparentPixels: Bool = true, + alphaThreshold: UInt8 = 0 + ) async throws -> UIImage? { guard let svgData else { return nil } - let targetSize = size - let logicalSize = CGSize(width: max(1, targetSize.width / max(UIScreen.main.scale, 1)), - height: max(1, targetSize.height / max(UIScreen.main.scale, 1))) + // Treat `size` as pixels. Convert to points for WKWebView/snapshot. + let scale = max(UIScreen.main.scale, 1) + let targetPixelSize = CGSize(width: max(1, size.width), height: max(1, size.height)) + let targetPointSize = CGSize( + width: max(1, targetPixelSize.width / scale), + height: max(1, targetPixelSize.height / scale) + ) - let webView = WKWebView(frame: CGRect(origin: .zero, size: logicalSize)) + // Build a dedicated WKWebView sized in points. + let webView = makeWebView(sizeInPoints: targetPointSize, backgroundColor: backgroundColor) self.webView = webView + // Inline the SVG into the DOM to avoid rasterization path. + let html = makeHTML(svgData: svgData, canvasPointSize: targetPointSize, backgroundColor: backgroundColor) + + try await loadHTMLAsync(webView: webView, html: html) + try await waitForInlineSVGReady(webView: webView) + + // Snapshot exactly the webView bounds; WebKit will render at device scale. + let config = WKSnapshotConfiguration() + config.rect = CGRect(origin: .zero, size: targetPointSize) + config.afterScreenUpdates = true + config.snapshotWidth = NSNumber(value: Double(targetPointSize.width)) + + let snapshot = try await takeSnapshotAsync(webView: webView, configuration: config) + + // Ensure the returned image is exactly the requested pixel dimensions. + // This is a defensive step; it should usually already match. + let finalImage = Self.normalize(snapshot, toPixelSize: targetPixelSize, scale: scale) + + if trimTransparentPixels, + let trimmed = Self.trimTransparentPixels(in: finalImage, alphaThreshold: alphaThreshold) { + return trimmed + } + + return finalImage + } + + // MARK: - WebView / HTML + + private func makeWebView(sizeInPoints: CGSize, backgroundColor: UIColor) -> WKWebView { + let config = WKWebViewConfiguration() + config.suppressesIncrementalRendering = false + + let webView = WKWebView(frame: CGRect(origin: .zero, size: sizeInPoints), configuration: config) webView.navigationDelegate = self webView.isOpaque = false webView.backgroundColor = backgroundColor webView.scrollView.backgroundColor = backgroundColor webView.layer.backgroundColor = backgroundColor.cgColor + webView.scrollView.isScrollEnabled = false + return webView + } - let cssBackground = backgroundColor == .clear ? "transparent" : backgroundColor.toCSSColor() + private func makeHTML(svgData: Data, canvasPointSize: CGSize, backgroundColor: UIColor) -> String { + let w = Int(canvasPointSize.width.rounded(.down)) + let h = Int(canvasPointSize.height.rounded(.down)) - let html = """ + // Base64 payload is decoded in JS and inserted as inline SVG markup. + let base64 = svgData.base64EncodedString() + let cssBackground = (backgroundColor == .clear) ? "transparent" : backgroundColor.toCSSColor() + + return """ + - - + + + - +
+ """ + } - try await loadHTMLAsync(webView: webView, html: html) - try await waitForImageReady(webView: webView) + private func loadHTMLAsync(webView: WKWebView, html: String) async throws { + webView.stopLoading() - let config = WKSnapshotConfiguration() - config.rect = CGRect(origin: .zero, size: logicalSize) - config.afterScreenUpdates = true + if let pending = navigationContinuation { + pending.resume(throwing: NSError( + domain: "NCSVGRenderer", + code: -22, + userInfo: [NSLocalizedDescriptionKey: "Cancelled previous load."] + )) + navigationContinuation = nil + } - let image = try await takeSnapshotAsync(webView: webView, configuration: config) - // Upscale to requested target size using Core Graphics - let rendererFormat = UIGraphicsImageRendererFormat.default() - rendererFormat.scale = 1.0 - let renderer = UIGraphicsImageRenderer(size: targetSize, format: rendererFormat) - let scaled = renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: targetSize)) + try await withCheckedThrowingContinuation { cont in + navigationContinuation = cont + webView.loadHTMLString(html, baseURL: nil) } + } - if trimTransparentPixels, - let trimmed = Self.trimTransparentPixels(in: scaled, alphaThreshold: alphaThreshold) { - return trimmed + private func waitForInlineSVGReady(webView: WKWebView) async throws { + // Wait until the inline SVG exists and has a non-zero bounding box. + let js = """ + (function() { + const svg = document.querySelector('#container svg'); + if (!svg) return false; + const box = svg.getBoundingClientRect(); + return box.width > 0 && box.height > 0; + })(); + """ + + // ~3 seconds max (100 * 30ms) + for _ in 0..<100 { + let ready = try await webView.evaluateJavaScript(js) as? Bool + if ready == true { return } + try await Task.sleep(nanoseconds: 30_000_000) } - return scaled + throw NSError( + domain: "NCSVGRenderer", + code: -24, + userInfo: [NSLocalizedDescriptionKey: "Inline SVG not ready within timeout."] + ) } + private func takeSnapshotAsync(webView: WKWebView, configuration: WKSnapshotConfiguration) async throws -> UIImage { + try await withCheckedThrowingContinuation { cont in + webView.takeSnapshot(with: configuration) { image, error in + if let image { + cont.resume(returning: image) + } else { + cont.resume(throwing: error ?? NSError(domain: "NCSVGRenderer", code: -21)) + } + } + } + } + + // MARK: - WKNavigationDelegate + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + navigationContinuation?.resume() + navigationContinuation = nil + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + navigationContinuation?.resume(throwing: error) + navigationContinuation = nil + } + + // MARK: - Image helpers + + /// Ensures an image matches a requested pixel size without "double scaling" artifacts. + /// If the snapshot already matches, it is returned unchanged. + private static func normalize(_ image: UIImage, toPixelSize pixelSize: CGSize, scale: CGFloat) -> UIImage { + let currentPixelSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + + // Close enough: avoid any resample. + if abs(currentPixelSize.width - pixelSize.width) < 0.5, + abs(currentPixelSize.height - pixelSize.height) < 0.5 { + return image + } + + // Render in points with the intended scale, producing exactly `pixelSize` pixels. + let targetPointSize = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) + + let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + format.opaque = false + + let renderer = UIGraphicsImageRenderer(size: targetPointSize, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: targetPointSize)) + } + } + + /// Crops transparent borders while preserving antialiased edges. + /// To avoid clipping feathered pixels, default alphaThreshold should be 0. private static func trimTransparentPixels(in image: UIImage, alphaThreshold: UInt8) -> UIImage? { guard let cgImage = image.cgImage else { return nil } @@ -133,6 +297,12 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { guard found else { return nil } + // Expand by 1 pixel to preserve edge AA when threshold > 0. + minX = max(minX - 1, 0) + minY = max(minY - 1, 0) + maxX = min(maxX + 1, width - 1) + maxY = min(maxY + 1, height - 1) + let cropRect = CGRect( x: minX, y: minY, @@ -143,60 +313,4 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { guard let cropped = cgImage.cropping(to: cropRect) else { return nil } return UIImage(cgImage: cropped, scale: image.scale, orientation: .up) } - - private func loadHTMLAsync(webView: WKWebView, html: String) async throws { - // Cancel any in-flight load to avoid overlapping delegates/continuations - webView.stopLoading() - if let pending = navigationContinuation { - pending.resume(throwing: NSError(domain: "NCSVGRenderer", code: -22, userInfo: [NSLocalizedDescriptionKey: "Cancelled previous load"])) - navigationContinuation = nil - } - - try await withCheckedThrowingContinuation { cont in - navigationContinuation = cont - webView.loadHTMLString(html, baseURL: nil) - } - } - - private func waitForImageReady(webView: WKWebView) async throws { - let js = """ - (function() { - const img = document.getElementById('svgImage'); - if (!img) return false; - return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0; - })(); - """ - - // wait max ~3 sec - for _ in 0..<100 { - let ready = try await webView.evaluateJavaScript(js) as? Bool - if ready == true { return } - try await Task.sleep(nanoseconds: 30_000_000) - } - throw NSError(domain: "NCSVGRenderer", code: -24, userInfo: [NSLocalizedDescriptionKey: "Image not ready within timeout"]) - } - - private func takeSnapshotAsync(webView: WKWebView, configuration: WKSnapshotConfiguration) async throws -> UIImage { - try await withCheckedThrowingContinuation { cont in - webView.takeSnapshot(with: configuration) { image, error in - if let image { - cont.resume(returning: image) - } else { - cont.resume(throwing: error ?? NSError(domain: "NCSVGRenderer", code: -21)) - } - } - } - } - - // MARK: - WKNavigationDelegate - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - navigationContinuation?.resume() - navigationContinuation = nil - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - navigationContinuation?.resume(throwing: error) - navigationContinuation = nil - } } From d1728480efad2e3525665e3cd8b38395b8a671a9 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:37:29 +0100 Subject: [PATCH 15/22] Fix gui svg (#3989) * code Signed-off-by: Marino Faggiana * test Signed-off-by: Marino Faggiana * preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana Co-authored-by: Milen Pivchev From 6816bc345f74819970848c55853d45e85138aca8 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:37:29 +0100 Subject: [PATCH 16/22] Fix gui svg (#3989) * code Signed-off-by: Marino Faggiana * test Signed-off-by: Marino Faggiana * preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana Co-authored-by: Milen Pivchev --- iOSClient/Menu/NCContextMenuMain.swift | 12 ++++ iOSClient/Utility/NCSVGRenderer.swift | 89 ++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 9359a89ce8..34aa5b66a2 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -501,6 +501,7 @@ class NCContextMenuMain: NSObject { var iconImage = UIImage() if let iconUrl = item.icon { +<<<<<<< HEAD if let image = await NCUtility().convertSVGtoPNGWriteToUserData( serverUrl: metadata.urlBase + iconUrl, rewrite: false, @@ -511,6 +512,17 @@ class NCContextMenuMain: NSObject { NCBrandColor.shared.iconImageColor, renderingMode: .alwaysOriginal ) +======= + if let image = await NCUtility().convertSVGtoPNGWriteToUserData(serverUrl: metadata.urlBase + iconUrl, + rewrite: false, + account: metadata.account).image { + if let image = image.withTintColor( + NCBrandColor.shared.iconImageColor, + renderingMode: .alwaysOriginal + ).resizeImage(size: CGSize(width: 20, height: 20)) { + iconImage = image + } +>>>>>>> fd0de89732 (Fix gui svg (#3989)) } } diff --git a/iOSClient/Utility/NCSVGRenderer.swift b/iOSClient/Utility/NCSVGRenderer.swift index 0568ff892b..ce52c30a52 100644 --- a/iOSClient/Utility/NCSVGRenderer.swift +++ b/iOSClient/Utility/NCSVGRenderer.swift @@ -19,6 +19,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { private var navigationContinuation: CheckedContinuation? private var webView: WKWebView? +<<<<<<< HEAD // MARK: - Public API /// Renders an SVG into a UIImage using WKWebView snapshotting. @@ -37,6 +38,13 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { trimTransparentPixels: Bool = true, alphaThreshold: UInt8 = 0 ) async throws -> UIImage? { +======= + func renderSVGToUIImage(svgData: Data?, + size: CGSize = CGSize(width: 256, height: 256), + backgroundColor: UIColor = .clear, + trimTransparentPixels: Bool = true, + alphaThreshold: UInt8 = 8) async throws -> UIImage? { +>>>>>>> fd0de89732 (Fix gui svg (#3989)) guard let svgData else { return nil } @@ -156,6 +164,87 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { """ +<<<<<<< HEAD +======= + + try await loadHTMLAsync(webView: webView, html: html) + try await waitForImageReady(webView: webView) + + let config = WKSnapshotConfiguration() + config.rect = CGRect(origin: .zero, size: logicalSize) + config.afterScreenUpdates = true + + let image = try await takeSnapshotAsync(webView: webView, configuration: config) + // Upscale to requested target size using Core Graphics + let rendererFormat = UIGraphicsImageRendererFormat.default() + rendererFormat.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: targetSize, format: rendererFormat) + let scaled = renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: targetSize)) + } + + if trimTransparentPixels, + let trimmed = Self.trimTransparentPixels(in: scaled, alphaThreshold: alphaThreshold) { + return trimmed + } + + return scaled +>>>>>>> fd0de89732 (Fix gui svg (#3989)) + } + + private static func trimTransparentPixels(in image: UIImage, alphaThreshold: UInt8) -> UIImage? { + guard let cgImage = image.cgImage else { return nil } + + let width = cgImage.width + let height = cgImage.height + let bytesPerRow = width * 4 + let colorSpace = CGColorSpaceCreateDeviceRGB() + + guard let context = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ), let data = context.data else { + return nil + } + + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + let buffer = data.bindMemory(to: UInt8.self, capacity: width * height * 4) + var minX = width + var minY = height + var maxX = 0 + var maxY = 0 + var found = false + + for y in 0.. alphaThreshold { + found = true + if x < minX { minX = x } + if y < minY { minY = y } + if x > maxX { maxX = x } + if y > maxY { maxY = y } + } + } + } + + guard found else { return nil } + + let cropRect = CGRect( + x: minX, + y: minY, + width: maxX - minX + 1, + height: maxY - minY + 1 + ) + + guard let cropped = cgImage.cropping(to: cropRect) else { return nil } + return UIImage(cgImage: cropped, scale: image.scale, orientation: .up) } private func loadHTMLAsync(webView: WKWebView, html: String) async throws { From 707d09cd991bdb3f8b182665ff52390af16283bc Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 17 Feb 2026 09:43:37 +0100 Subject: [PATCH 17/22] svg-fix (#3990) * fix Signed-off-by: Marino Faggiana --- iOSClient/Menu/NCContextMenuMain.swift | 9 ++ iOSClient/Utility/NCSVGRenderer.swift | 113 +++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 34aa5b66a2..7100f1842d 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -502,6 +502,9 @@ class NCContextMenuMain: NSObject { if let iconUrl = item.icon { <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> d99135d60a (svg-fix (#3990)) if let image = await NCUtility().convertSVGtoPNGWriteToUserData( serverUrl: metadata.urlBase + iconUrl, rewrite: false, @@ -509,6 +512,7 @@ class NCContextMenuMain: NSObject { ).image { let image = image.rasterResized(to: CGSize(width: 20, height: 20)) iconImage = image.withTintColor( +<<<<<<< HEAD NCBrandColor.shared.iconImageColor, renderingMode: .alwaysOriginal ) @@ -523,6 +527,11 @@ class NCContextMenuMain: NSObject { iconImage = image } >>>>>>> fd0de89732 (Fix gui svg (#3989)) +======= + NCBrandColor.shared.iconImageColor, + renderingMode: .alwaysOriginal + ) +>>>>>>> d99135d60a (svg-fix (#3990)) } } diff --git a/iOSClient/Utility/NCSVGRenderer.swift b/iOSClient/Utility/NCSVGRenderer.swift index ce52c30a52..e14fa8e846 100644 --- a/iOSClient/Utility/NCSVGRenderer.swift +++ b/iOSClient/Utility/NCSVGRenderer.swift @@ -20,6 +20,9 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { private var webView: WKWebView? <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> d99135d60a (svg-fix (#3990)) // MARK: - Public API /// Renders an SVG into a UIImage using WKWebView snapshotting. @@ -38,6 +41,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { trimTransparentPixels: Bool = true, alphaThreshold: UInt8 = 0 ) async throws -> UIImage? { +<<<<<<< HEAD ======= func renderSVGToUIImage(svgData: Data?, size: CGSize = CGSize(width: 256, height: 256), @@ -45,6 +49,8 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { trimTransparentPixels: Bool = true, alphaThreshold: UInt8 = 8) async throws -> UIImage? { >>>>>>> fd0de89732 (Fix gui svg (#3989)) +======= +>>>>>>> d99135d60a (svg-fix (#3990)) guard let svgData else { return nil } @@ -165,6 +171,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { """ <<<<<<< HEAD +<<<<<<< HEAD ======= try await loadHTMLAsync(webView: webView, html: html) @@ -190,8 +197,105 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { return scaled >>>>>>> fd0de89732 (Fix gui svg (#3989)) +======= +>>>>>>> d99135d60a (svg-fix (#3990)) + } + + private func loadHTMLAsync(webView: WKWebView, html: String) async throws { + webView.stopLoading() + + if let pending = navigationContinuation { + pending.resume(throwing: NSError( + domain: "NCSVGRenderer", + code: -22, + userInfo: [NSLocalizedDescriptionKey: "Cancelled previous load."] + )) + navigationContinuation = nil + } + + try await withCheckedThrowingContinuation { cont in + navigationContinuation = cont + webView.loadHTMLString(html, baseURL: nil) + } } + private func waitForInlineSVGReady(webView: WKWebView) async throws { + // Wait until the inline SVG exists and has a non-zero bounding box. + let js = """ + (function() { + const svg = document.querySelector('#container svg'); + if (!svg) return false; + const box = svg.getBoundingClientRect(); + return box.width > 0 && box.height > 0; + })(); + """ + + // ~3 seconds max (100 * 30ms) + for _ in 0..<100 { + let ready = try await webView.evaluateJavaScript(js) as? Bool + if ready == true { return } + try await Task.sleep(nanoseconds: 30_000_000) + } + + throw NSError( + domain: "NCSVGRenderer", + code: -24, + userInfo: [NSLocalizedDescriptionKey: "Inline SVG not ready within timeout."] + ) + } + + private func takeSnapshotAsync(webView: WKWebView, configuration: WKSnapshotConfiguration) async throws -> UIImage { + try await withCheckedThrowingContinuation { cont in + webView.takeSnapshot(with: configuration) { image, error in + if let image { + cont.resume(returning: image) + } else { + cont.resume(throwing: error ?? NSError(domain: "NCSVGRenderer", code: -21)) + } + } + } + } + + // MARK: - WKNavigationDelegate + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + navigationContinuation?.resume() + navigationContinuation = nil + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + navigationContinuation?.resume(throwing: error) + navigationContinuation = nil + } + + // MARK: - Image helpers + + /// Ensures an image matches a requested pixel size without "double scaling" artifacts. + /// If the snapshot already matches, it is returned unchanged. + private static func normalize(_ image: UIImage, toPixelSize pixelSize: CGSize, scale: CGFloat) -> UIImage { + let currentPixelSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + + // Close enough: avoid any resample. + if abs(currentPixelSize.width - pixelSize.width) < 0.5, + abs(currentPixelSize.height - pixelSize.height) < 0.5 { + return image + } + + // Render in points with the intended scale, producing exactly `pixelSize` pixels. + let targetPointSize = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) + + let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + format.opaque = false + + let renderer = UIGraphicsImageRenderer(size: targetPointSize, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: targetPointSize)) + } + } + + /// Crops transparent borders while preserving antialiased edges. + /// To avoid clipping feathered pixels, default alphaThreshold should be 0. private static func trimTransparentPixels(in image: UIImage, alphaThreshold: UInt8) -> UIImage? { guard let cgImage = image.cgImage else { return nil } @@ -236,6 +340,12 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { guard found else { return nil } + // Expand by 1 pixel to preserve edge AA when threshold > 0. + minX = max(minX - 1, 0) + minY = max(minY - 1, 0) + maxX = min(maxX + 1, width - 1) + maxY = min(maxY + 1, height - 1) + let cropRect = CGRect( x: minX, y: minY, @@ -246,6 +356,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { guard let cropped = cgImage.cropping(to: cropRect) else { return nil } return UIImage(cgImage: cropped, scale: image.scale, orientation: .up) } +<<<<<<< HEAD private func loadHTMLAsync(webView: WKWebView, html: String) async throws { webView.stopLoading() @@ -402,4 +513,6 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { guard let cropped = cgImage.cropping(to: cropRect) else { return nil } return UIImage(cgImage: cropped, scale: image.scale, orientation: .up) } +======= +>>>>>>> d99135d60a (svg-fix (#3990)) } From 4b408cdddd8ecd1feab8799ffb50497a67a9d1f0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 17 Feb 2026 11:24:58 +0100 Subject: [PATCH 18/22] added tintcolor (#3992) Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 +- iOSClient/Menu/NCContextMenuMain.swift | 14 ++- iOSClient/Utility/NCSVGRenderer.swift | 117 ++++++++++++++++--------- 3 files changed, 93 insertions(+), 42 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f7ad38bf23..f996034b5e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5775,7 +5775,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5841,7 +5841,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; diff --git a/iOSClient/Menu/NCContextMenuMain.swift b/iOSClient/Menu/NCContextMenuMain.swift index 7100f1842d..0da40bb617 100644 --- a/iOSClient/Menu/NCContextMenuMain.swift +++ b/iOSClient/Menu/NCContextMenuMain.swift @@ -498,11 +498,12 @@ class NCContextMenuMain: NSObject { if shouldShowMenu { let deferredElement = UIDeferredMenuElement { completion in Task { - var iconImage = UIImage() + var iconImage = UIImage(systemName: "exclamationmark.triangle.fill") if let iconUrl = item.icon { <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= >>>>>>> d99135d60a (svg-fix (#3990)) if let image = await NCUtility().convertSVGtoPNGWriteToUserData( @@ -532,6 +533,17 @@ class NCContextMenuMain: NSObject { renderingMode: .alwaysOriginal ) >>>>>>> d99135d60a (svg-fix (#3990)) +======= + let results = await NextcloudKit.shared.downloadContentAsync(serverUrl: metadata.urlBase + iconUrl, account: metadata.account) + if results.error == .success, let data = results.responseData?.data, + let image = try? await NCSVGRenderer().renderSVGToUIImage( + svgData: data, + size: CGSize(width: UIScreen.main.scale * 20, + height: UIScreen.main.scale * 20), + tintColor: NCBrandColor.shared.iconImageColor, + trimTransparentPixels: false) { + iconImage = image +>>>>>>> 688c5b5c5a (added tintcolor (#3992)) } } diff --git a/iOSClient/Utility/NCSVGRenderer.swift b/iOSClient/Utility/NCSVGRenderer.swift index e14fa8e846..a3e56505d8 100644 --- a/iOSClient/Utility/NCSVGRenderer.swift +++ b/iOSClient/Utility/NCSVGRenderer.swift @@ -38,6 +38,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { svgData: Data?, size: CGSize = CGSize(width: 256, height: 256), backgroundColor: UIColor = .clear, + tintColor: UIColor? = nil, trimTransparentPixels: Bool = true, alphaThreshold: UInt8 = 0 ) async throws -> UIImage? { @@ -68,7 +69,7 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { self.webView = webView // Inline the SVG into the DOM to avoid rasterization path. - let html = makeHTML(svgData: svgData, canvasPointSize: targetPointSize, backgroundColor: backgroundColor) + let html = makeHTML(svgData: svgData, canvasPointSize: targetPointSize, backgroundColor: backgroundColor, tintColor: tintColor) try await loadHTMLAsync(webView: webView, html: html) try await waitForInlineSVGReady(webView: webView) @@ -109,51 +110,48 @@ final class NCSVGRenderer: NSObject, WKNavigationDelegate { return webView } - private func makeHTML(svgData: Data, canvasPointSize: CGSize, backgroundColor: UIColor) -> String { + private func makeHTML( + svgData: Data, + canvasPointSize: CGSize, + backgroundColor: UIColor, + tintColor: UIColor? + ) -> String { + let w = Int(canvasPointSize.width.rounded(.down)) let h = Int(canvasPointSize.height.rounded(.down)) - // Base64 payload is decoded in JS and inserted as inline SVG markup. let base64 = svgData.base64EncodedString() - let cssBackground = (backgroundColor == .clear) ? "transparent" : backgroundColor.toCSSColor() + + let cssBackground: String = (backgroundColor == .clear) + ? "transparent" + : backgroundColor.toCSSColor() + + // Non-optional CSS tint (inherit if nil) + let cssTint: String = tintColor.map { $0.toCSSColor() } ?? "inherit" + + // Apply fill only when tintColor is provided + let svgFillRule: String = tintColor != nil + ? "#container svg { fill: currentColor; }" + : "" return """ - - - - - - - - -
- + + + """ +>>>>>>> 688c5b5c5a (added tintcolor (#3992)) } private func loadHTMLAsync(webView: WKWebView, html: String) async throws { From 545ff54397c6d5928d4c40c97fd4060fed4f4cff Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:37:29 +0100 Subject: [PATCH 19/22] Fix gui svg (#3989) * code Signed-off-by: Marino Faggiana * test Signed-off-by: Marino Faggiana * preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana Co-authored-by: Milen Pivchev From aa09af7392f1a8ed2f22eccf67710d17e0eed66a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 17 Feb 2026 09:43:37 +0100 Subject: [PATCH 20/22] svg-fix (#3990) * fix Signed-off-by: Marino Faggiana From bac26fe4599d8fc6e8883ec3a7628fddc8ee07b5 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Mon, 16 Feb 2026 18:37:29 +0100 Subject: [PATCH 21/22] Fix gui svg (#3989) * code Signed-off-by: Marino Faggiana * test Signed-off-by: Marino Faggiana * preferredSearchBarPlacement = .inline Signed-off-by: Marino Faggiana --------- Signed-off-by: Marino Faggiana Co-authored-by: Milen Pivchev From 9b2f2376085cf66baa0e859ee8cd69024f8be2ca Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 17 Feb 2026 16:38:39 +0100 Subject: [PATCH 22/22] cleaning Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 ++++ iOSClient/Extensions/UIAlertController+Extension.swift | 3 --- .../Main/Collection Common/NCCollectionViewCommon.swift | 3 +-- iOSClient/Networking/NCNetworking+WebDAV.swift | 6 ------ iOSClient/Supporting Files/en.lproj/Localizable.strings | 2 +- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f996034b5e..d33e4f91d1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5625,10 +5625,12 @@ ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/iOSClient.entitlements"; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = NKUJUXUJ3B; GCC_SYMBOLS_PRIVATE_EXTERN = YES; INFOPLIST_FILE = "$(SRCROOT)/Brand/iOSClient.plist"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 33.0.0; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5648,10 +5650,12 @@ ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/iOSClient.entitlements"; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = NKUJUXUJ3B; GCC_SYMBOLS_PRIVATE_EXTERN = YES; INFOPLIST_FILE = "$(SRCROOT)/Brand/iOSClient.plist"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 33.0.0; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index d981404b8a..e775ce1ee5 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -99,9 +99,6 @@ extension UIAlertController { metadata.sessionDate = Date() NCManageDatabase.shared.addMetadata(metadata) - - // START Network process - NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) #endif } }) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 8076130d80..8d0c486b37 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -880,8 +880,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { } if status == self.global.networkingStatusCreateFolder { - if error == .success, - serverUrl == self.serverUrl, + if serverUrl == self.serverUrl, selector != self.global.selectorUploadAutoUpload, let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { self.pushMetadata(metadata) diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 27fc1d093c..9e761710d7 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -464,9 +464,6 @@ extension NCNetworking { serverUrlss.forEach { serverUrl in delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitDelete) } - - // START Network process - NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) } } } @@ -548,9 +545,6 @@ extension NCNetworking { await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: ocId, status: self.global.metadataStatusWaitRename) delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitRename) } - - // START Network process - NotificationCenter.default.postOnGlobal(name: NCGlobal.shared.notificationCenterNetworkProcess, second: 0.1) } } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 26990a62c5..d83b987a48 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -646,7 +646,7 @@ "_create_folder_error_" = "An error has occurred while creating the folder:\n%@.\n\nPlease resolve the issue as soon as possible.\n\nAll uploads are suspended until the problem is resolved.\n"; "_creating_dir_progress_" = "Creating directories in progress … keep the application active."; "_creating_db_photo_progress_" = "Creating photo archive in progress … keep the application active."; -"_account_unauthorized_" = "There was an issue authorizing the account. Please log in again."; +"_account_unauthorized_" = "There was an issue authorizing the account %@. Please log in again."; "_folder_offline_desc_" = "Even without an internet connection, you can organize your folders, create files. Once you're back online, your pending actions will automatically sync."; "_offline_not_allowed_" = "This operation is not allowed in offline mode"; "_Upload_native_format_yes_"= "Upload in native format: yes";