diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f52c19caf0..0379aee988 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -118,6 +118,9 @@ F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */; }; F343A4B62A1E084200DDA874 /* PHAsset+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */; }; F343A4BB2A1E734600DDA874 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; + F34E1AD72ECB937D00FA10C3 /* NCStatusMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E1AD62ECB937D00FA10C3 /* NCStatusMessageView.swift */; }; + F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E1AD82ECC839100FA10C3 /* EmojiTextField.swift */; }; + F34E1ADB2ECC842B00FA10C3 /* NCStatusMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34E1ADA2ECC842200FA10C3 /* NCStatusMessageModel.swift */; }; F359D8672A7D03420023F405 /* NCUtility+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F359D8662A7D03420023F405 /* NCUtility+Exif.swift */; }; F36C514F2E89393C0097E5F7 /* UIView+BlurVibrancy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36C514D2E89393C0097E5F7 /* UIView+BlurVibrancy.swift */; }; F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift */; }; @@ -158,6 +161,8 @@ F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047952BD2668800658E7B /* NCAssistantTaskDetail.swift */; }; F3A0479B2BD2668800658E7B /* NCAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047962BD2668800658E7B /* NCAssistant.swift */; }; F3A0479E2BD268B500658E7B /* PopupView in Frameworks */ = {isa = PBXBuildFile; productRef = F3A0479D2BD268B500658E7B /* PopupView */; }; + F3A0B1F12EC76FE800F10B82 /* NCUserStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A0B1EF2EC76FE800F10B82 /* NCUserStatusModel.swift */; }; + F3A0B1F22EC76FE800F10B82 /* NCUserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A0B1F02EC76FE800F10B82 /* NCUserStatusView.swift */; }; F3BB464D2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */; }; F3BB46522A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */; }; F3BB46542A3A1E9D00461F6E /* CCCellMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */; }; @@ -836,7 +841,6 @@ F7D7A7722DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; }; F7D890752BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */; }; F7E0710128B13BB00001B882 /* DashboardData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E0710028B13BB00001B882 /* DashboardData.swift */; }; - F7E0CDCF265CE8610044854E /* NCUserStatus.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7E0CDCE265CE8610044854E /* NCUserStatus.storyboard */; }; F7E2B64F2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E2B64E2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift */; }; F7E402292BA85D1D007E5609 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */; }; F7E4022A2BA85D1D007E5609 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */; }; @@ -876,7 +880,6 @@ F7EF2AEB2E43157B0081B2C9 /* NCNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EF2AE92E43157B0081B2C9 /* NCNotification.swift */; }; F7EF2AEC2E43157B0081B2C9 /* NCNotification.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7EF2AE82E43157B0081B2C9 /* NCNotification.storyboard */; }; F7EFA47825ADBA500083159A /* NCViewerProviderContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EFA47725ADBA500083159A /* NCViewerProviderContextMenu.swift */; }; - F7EFC0CD256BF8DD00461AAD /* NCUserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */; }; F7F1FB9D2E27CE7200C79E20 /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F7F1FB9E2E27CE7200C79E20 /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F7F1FBA82E27D13700C79E20 /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F7F1FBA72E27D13700C79E20 /* Queuer */; }; @@ -1241,6 +1244,9 @@ F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; + F34E1AD62ECB937D00FA10C3 /* NCStatusMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCStatusMessageView.swift; sourceTree = ""; }; + F34E1AD82ECC839100FA10C3 /* EmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiTextField.swift; sourceTree = ""; }; + F34E1ADA2ECC842200FA10C3 /* NCStatusMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCStatusMessageModel.swift; sourceTree = ""; }; F351D1A52D0AF24A00930F94 /* PHAssetCollection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAssetCollection+Extension.swift"; sourceTree = ""; }; F359D8662A7D03420023F405 /* NCUtility+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCUtility+Exif.swift"; sourceTree = ""; }; F36C514D2E89393C0097E5F7 /* UIView+BlurVibrancy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+BlurVibrancy.swift"; sourceTree = ""; }; @@ -1258,6 +1264,8 @@ F3A047932BD2668800658E7B /* NCAssistantModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantModel.swift; sourceTree = ""; }; F3A047952BD2668800658E7B /* NCAssistantTaskDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantTaskDetail.swift; sourceTree = ""; }; F3A047962BD2668800658E7B /* NCAssistant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistant.swift; sourceTree = ""; }; + F3A0B1EF2EC76FE800F10B82 /* NCUserStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatusModel.swift; sourceTree = ""; }; + F3A0B1F02EC76FE800F10B82 /* NCUserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatusView.swift; sourceTree = ""; }; F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMoreAppSuggestionsCell.xib; sourceTree = ""; }; F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreAppSuggestionsCell.swift; sourceTree = ""; }; F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CCCellMore.swift; sourceTree = ""; }; @@ -1747,7 +1755,6 @@ F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+DragDrop.swift"; sourceTree = ""; }; F7DE9AB01F482FA5008DFE10 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; F7E0710028B13BB00001B882 /* DashboardData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardData.swift; sourceTree = ""; }; - F7E0CDCE265CE8610044854E /* NCUserStatus.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCUserStatus.storyboard; sourceTree = ""; }; F7E2B64E2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+TransferDelegate.swift"; sourceTree = ""; }; F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; F7E402302BA891EB007E5609 /* NCTrash+SelectTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCTrash+SelectTabBarDelegate.swift"; sourceTree = ""; }; @@ -1767,7 +1774,6 @@ F7EF2AE82E43157B0081B2C9 /* NCNotification.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCNotification.storyboard; sourceTree = ""; }; F7EF2AE92E43157B0081B2C9 /* NCNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNotification.swift; sourceTree = ""; }; F7EFA47725ADBA500083159A /* NCViewerProviderContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewerProviderContextMenu.swift; sourceTree = ""; }; - F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatus.swift; sourceTree = ""; }; F7F3E58A2D3BB65000A32B14 /* NCNetworking+Recommendations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCNetworking+Recommendations.swift"; sourceTree = ""; }; F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-SemiBold.ttf"; sourceTree = ""; }; F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Medium.ttf"; sourceTree = ""; }; @@ -2112,6 +2118,16 @@ path = Components; sourceTree = ""; }; + F34E1AD52ECB935E00FA10C3 /* StatusMessage */ = { + isa = PBXGroup; + children = ( + F34E1AD82ECC839100FA10C3 /* EmojiTextField.swift */, + F34E1ADA2ECC842200FA10C3 /* NCStatusMessageModel.swift */, + F34E1AD62ECB937D00FA10C3 /* NCStatusMessageView.swift */, + ); + path = StatusMessage; + sourceTree = ""; + }; F389C9F32CEE381E00049762 /* SelectAlbum */ = { isa = PBXGroup; children = ( @@ -3153,8 +3169,8 @@ F7EFC0CB256BF89300461AAD /* UserStatus */ = { isa = PBXGroup; children = ( - F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */, - F7E0CDCE265CE8610044854E /* NCUserStatus.storyboard */, + F3A0B1EF2EC76FE800F10B82 /* NCUserStatusModel.swift */, + F3A0B1F02EC76FE800F10B82 /* NCUserStatusView.swift */, ); path = UserStatus; sourceTree = ""; @@ -3265,6 +3281,7 @@ F7132C6D2D085AD200B42D6A /* Terms of service */, F7E9C41320F4CA870040CF18 /* Transfers */, F78F74322163753B00C2ADAD /* Trash */, + F34E1AD52ECB935E00FA10C3 /* StatusMessage */, F7EFC0CB256BF89300461AAD /* UserStatus */, F7BFFA991A24D7BB0044ED85 /* Utility */, F79630EC215526B60015EEA5 /* Viewer */, @@ -3949,7 +3966,6 @@ F7B8B83025681C3400967775 /* GoogleService-Info.plist in Resources */, F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */, F768822D2C0DD1E7001CF441 /* Acknowledgements.rtf in Resources */, - F7E0CDCF265CE8610044854E /* NCUserStatus.storyboard in Resources */, F76D3CF32428B94E005DFA87 /* NCViewerPDFSearchCell.xib in Resources */, F717402D24F699A5000C87D5 /* NCFavorite.storyboard in Resources */, F723B3DD22FC6D1D00301EFE /* NCShareCommentsCell.xib in Resources */, @@ -4456,6 +4472,7 @@ F76B649C2ADFFAED00014640 /* NCImageCache.swift in Sources */, F76341182EBE0BC60056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */, + F34E1AD92ECC839100FA10C3 /* EmojiTextField.swift in Sources */, F768822E2C0DD1E7001CF441 /* NCSettingsBundleHelper.swift in Sources */, F72408332B8A27C900F128E2 /* NCMedia+Command.swift in Sources */, F755CB402B8CB13C00CE27E9 /* NCMediaLayout.swift in Sources */, @@ -4514,6 +4531,8 @@ F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, + F3A0B1F12EC76FE800F10B82 /* NCUserStatusModel.swift in Sources */, + F3A0B1F22EC76FE800F10B82 /* NCUserStatusView.swift in Sources */, F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, @@ -4583,6 +4602,7 @@ F718C24E254D507B00C5C256 /* NCViewerMediaDetailView.swift in Sources */, F33EE6F22BF4C9B200CA1A51 /* PKCS12.swift in Sources */, F7145610296433C80038D028 /* NCDocumentCamera.swift in Sources */, + F34E1AD72ECB937D00FA10C3 /* NCStatusMessageView.swift in Sources */, F76882312C0DD1E7001CF441 /* NCFileNameView.swift in Sources */, F7381EE1218218C9000B1560 /* NCOffline.swift in Sources */, F751247C2C42919C00E63DB8 /* NCPhotoCell.swift in Sources */, @@ -4619,7 +4639,6 @@ F7E8A391295DC5E0006CB2D0 /* View+Extension.swift in Sources */, F79B869B265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift in Sources */, F7B7504B2397D38F004E13EC /* UIImage+Extension.swift in Sources */, - F7EFC0CD256BF8DD00461AAD /* NCUserStatus.swift in Sources */, AF3FDCC22796ECC300710F60 /* NCTrash+CollectionView.swift in Sources */, F70D7C3725FFBF82002B9E34 /* NCCollectionViewCommon.swift in Sources */, F76D364628A4F8BF00214537 /* NCActivityIndicator.swift in Sources */, @@ -4628,6 +4647,7 @@ F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */, F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */, F75D19E325EFE09000D74598 /* NCTrash+Menu.swift in Sources */, + F34E1ADB2ECC842B00FA10C3 /* NCStatusMessageModel.swift in Sources */, F70CAE3A1F8CF31A008125FD /* NCEndToEndEncryption.m in Sources */, AA8D316E2D4123B200FE2775 /* NCShareDownloadLimitTableViewControllerDelegate.swift in Sources */, F36C514F2E89393C0097E5F7 /* UIView+BlurVibrancy.swift in Sources */, diff --git a/iOSClient/Account/Account Settings/NCAccountSettingsModel.swift b/iOSClient/Account/Account Settings/NCAccountSettingsModel.swift index 0267991c2b..d7742e9f4c 100644 --- a/iOSClient/Account/Account Settings/NCAccountSettingsModel.swift +++ b/iOSClient/Account/Account Settings/NCAccountSettingsModel.swift @@ -39,9 +39,9 @@ class NCAccountSettingsModel: ObservableObject, ViewOnAppearHandling { init(controller: NCMainTabBarController?, delegate: NCAccountSettingsModelDelegate?) { self.controller = controller self.delegate = delegate - if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { + if isXcodeRunningForPreviews { Task { - await self.database.previewCreateDB() + await self.database.createDBForPreview() } } onViewAppear() diff --git a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift index 12fb4dc4f2..faa766acb3 100644 --- a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift +++ b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift @@ -9,7 +9,6 @@ struct NCAccountSettingsView: View { @ObservedObject var model: NCAccountSettingsModel @State private var isExpanded: Bool = false - @State private var showUserStatus = false @State private var showServerCertificate = false @State private var showPushCertificate = false @State private var showDeleteAccountAlert: Bool = false @@ -141,31 +140,45 @@ struct NCAccountSettingsView: View { // // User Status if capabilities.userStatusEnabled { - Button(action: { - showUserStatus = true - }, label: { - HStack { - Image(systemName: "moon.fill") - .resizable() - .scaledToFit() - .font(Font.system(.body).weight(.light)) - .frame(width: 20, height: 20) - .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) - Text(NSLocalizedString("_set_user_status_", comment: "")) - .lineLimit(1) - .truncationMode(.middle) - .foregroundStyle(Color(NCBrandColor.shared.textColor)) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) + if let account = model.tblAccount?.account { + NavigationLink(destination: NCUserStatusView(account: account)) { + HStack { + Image(systemName: "moon.fill") + .resizable() + .scaledToFit() + .font(Font.system(.body).weight(.light)) + .frame(width: 20, height: 20) + .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) + Text(NSLocalizedString("_set_user_status_", comment: "")) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(Color(NCBrandColor.shared.textColor)) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) + } + .font(.subheadline) } - .font(.subheadline) - }) - .sheet(isPresented: $showUserStatus) { - if let account = model.tblAccount?.account { - UserStatusView(showUserStatus: $showUserStatus, account: account) + } + + if let account = model.tblAccount?.account { + NavigationLink(destination: NCStatusMessageView(account: account)) { + HStack { + Image(systemName: "message.fill") + .resizable() + .scaledToFit() + .font(Font.system(.body).weight(.light)) + .frame(width: 20, height: 20) + .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) + Text(NSLocalizedString("_set_user_status_message_", comment: "")) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(Color(NCBrandColor.shared.textColor)) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) + } + .font(.subheadline) } } - .onChange(of: showUserStatus) { } } + // // Certificate server if model.isAdminGroup() { diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index c010620e01..9ac618ca1a 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -444,7 +444,7 @@ final class NCManageDatabase: @unchecked Sendable { // MARK: - // MARK: SWIFTUI PREVIEW - func previewCreateDB() async { + func createDBForPreview() async { // Account let account = "marinofaggiana https://cloudtest.nextcloud.com" let account2 = "mariorossi https://cloudtest.nextcloud.com" diff --git a/iOSClient/Extensions/View+Extension.swift b/iOSClient/Extensions/View+Extension.swift index 25ed0dfed1..77aab3bea7 100644 --- a/iOSClient/Extensions/View+Extension.swift +++ b/iOSClient/Extensions/View+Extension.swift @@ -72,7 +72,3 @@ struct ViewFirstAppearModifier: ViewModifier { } } } - -var isRunningForPreviews: Bool { - return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" -} diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 290f941517..51df24b68c 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -403,3 +403,10 @@ final class NCGlobal: Sendable { let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" } + +/** + Indicates whether Xcode is running SwiftUI previews. + */ +var isXcodeRunningForPreviews: Bool { + return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" +} diff --git a/iOSClient/StatusMessage/EmojiTextField.swift b/iOSClient/StatusMessage/EmojiTextField.swift new file mode 100644 index 0000000000..c7fb7525d7 --- /dev/null +++ b/iOSClient/StatusMessage/EmojiTextField.swift @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +// UIKit-backed emoji-only text field that forces the Emoji keyboard +final class EmojiTextField: UITextField { + override var textInputContextIdentifier: String? { "" } // return non-nil to show the Emoji keyboard + + override var textInputMode: UITextInputMode? { + for mode in UITextInputMode.activeInputModes { + if mode.primaryLanguage == "emoji" { + return mode + } + } + return nil + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil) + addTarget(self, action: #selector(textChanged), for: .editingChanged) + } + + @objc func inputModeDidChange(_ notification: Notification) { + guard isFirstResponder else { + return + } + + DispatchQueue.main.async { [weak self] in + self?.reloadInputViews() + } + } + + // Keep only a single emoji character + @objc private func textChanged() { + guard let t = text, !t.isEmpty else { return } + // Trim to first extended grapheme cluster (so flags/skin tones stay intact) + let first = String(t.prefix(1)) + if first != t { text = first } + } +} + +struct EmojiField: UIViewRepresentable { + @Binding var text: String + + func makeUIView(context: Context) -> EmojiTextField { + let tf = EmojiTextField(frame: .zero) + tf.delegate = context.coordinator + tf.text = text + tf.setContentHuggingPriority(.required, for: .horizontal) + tf.setContentCompressionResistancePriority(.required, for: .horizontal) + return tf + } + + func updateUIView(_ uiView: EmojiTextField, context: Context) { + if uiView.text != text { + uiView.text = text + } + } + + func makeCoordinator() -> Coordinator { Coordinator(self) } + + final class Coordinator: NSObject, UITextFieldDelegate { + var parent: EmojiField + init(_ parent: EmojiField) { self.parent = parent } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if textField is EmojiTextField { + if string.isEmpty { + textField.text = "πŸ˜€" + return false + } + textField.text = string + textField.endEditing(true) + } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return false + } + } +} diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift new file mode 100644 index 0000000000..63d52662b9 --- /dev/null +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import NextcloudKit + +@Observable class NCStatusMessageModel { + enum ClearAfter: String, CaseIterable, Identifiable { + case dontClear = "_dont_clear_" + case thirtyMinutes = "_30_minutes_" + case fifteenMinutes = "_15_minutes_" + case oneHour = "_an_hour_" + case fourHours = "_4_hours_" + case today = "_day_" + case thisWeek = "_this_week_" + + var id: String { rawValue } + } + + var predefinedStatuses: [NKUserStatus] = [] + + var emojiText: String = "" + var statusText: String = "" + var clearAfterString = "_dont_clear_" + + func chooseStatusPreset(preset: NKUserStatus, clearAtText: String) { + emojiText = preset.icon ?? "" + statusText = preset.message ?? "" + clearAfterString = clearAtText + } + + func getStatus(account: String) { + Task { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, name: "getUserStatus") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error == .success { + emojiText = result.icon ?? "πŸ˜€" + statusText = result.message ?? "" + clearAfterString = getPredefinedClearStatusString(clearAt: result.clearAt, clearAtTime: "", clearAtType: "") + } + } + } + + func clearStatus(account: String) { + Task { + let result = await NextcloudKit.shared.clearMessageAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, name: "clearMessage") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error != .success { + NCContentPresenter().showError(error: result.error) + } + } + } + + func getPredefinedStatusTexts(account: String) { + Task { + let result = await NextcloudKit.shared.getUserStatusPredefinedStatusesAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, name: "getUserStatusPredefinedStatuses") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error == .success { + predefinedStatuses = isXcodeRunningForPreviews ? createStatusesForPreview() : result.userStatuses ?? [] + } else { + NCContentPresenter().showError(error: result.error) + } + } + } + + func submitStatus(account: String) { + Task { + let result = await NextcloudKit.shared.setCustomMessageUserDefinedAsync(statusIcon: emojiText, message: statusText, clearAt: getClearAt(clearAfterString), account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, name: "setCustomMessageUserDefined") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error != .success { + NCContentPresenter().showError(error: result.error) + } + } + } + + func setAccountUserStatus(account: String) { + Task { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + name: "getUserStatus") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error == .success { + await NCManageDatabase.shared.setAccountUserStatusAsync(userStatusClearAt: result.clearAt, + userStatusIcon: result.icon, + userStatusMessage: result.message, + userStatusMessageId: result.messageId, + userStatusMessageIsPredefined: result.messageIsPredefined, + userStatusStatus: result.status, + userStatusStatusIsUserDefined: result.statusIsUserDefined, + account: result.account) + } else { + NCContentPresenter().showError(error: result.error) + } + } + } + + func getPredefinedClearStatusString(clearAt: Date?, clearAtTime: String?, clearAtType: String?) -> String { + // Date + if let clearAt { + let from = Date() + let to = clearAt + let day = Calendar.current.dateComponents([.day], from: from, to: to).day ?? 0 + let hour = Calendar.current.dateComponents([.hour], from: from, to: to).hour ?? 0 + let minute = Calendar.current.dateComponents([.minute], from: from, to: to).minute ?? 0 + + if day > 0 { + if day == 1 { return NSLocalizedString("_day_", comment: "") } + return "\(day) " + NSLocalizedString("_days_", comment: "") + } + + if hour > 0 { + if hour == 1 { return NSLocalizedString("_an_hour_", comment: "") } + if hour == 4 { return NSLocalizedString("_4_hour_", comment: "") } + return "\(hour) " + NSLocalizedString("_hours_", comment: "") + } + + if minute > 0 { + if minute >= 25 && minute <= 30 { return NSLocalizedString("_30_minutes_", comment: "") } + if minute > 30 { return NSLocalizedString("_an_hour_", comment: "") } + return "\(minute) " + NSLocalizedString("_minutes_", comment: "") + } + } + // Period + if let clearAtTime, clearAtType == "period" { + switch clearAtTime { + case "3600": + return NSLocalizedString("_an_hour_", comment: "") + case "1800": + return NSLocalizedString("_30_minutes_", comment: "") + case "900": + return NSLocalizedString("_15_minutes_", comment: "") + default: + return NSLocalizedString("_dont_clear_", comment: "") + } + } + // End of + if let clearAtTime, clearAtType == "end-of" { + if clearAtTime == "day" { + return NSLocalizedString("_day_", comment: "") + } + } + + return NSLocalizedString("_dont_clear_", comment: "") + } + + private func getClearAt(_ clearAtString: String) -> Double { + let now = Date() + let calendar = Calendar.current + let gregorian = Calendar(identifier: .gregorian) + let midnight = calendar.startOfDay(for: now) + guard let tomorrow = calendar.date(byAdding: .day, value: 1, to: midnight) else { return 0 } + guard let startweek = gregorian.date(from: gregorian.dateComponents([.yearForWeekOfYear, .weekOfYear], from: now)) else { return 0 } + guard let endweek = gregorian.date(byAdding: .day, value: 6, to: startweek) else { return 0 } + + switch clearAtString { + case NSLocalizedString("_dont_clear_", comment: ""): + return 0 + case NSLocalizedString("_15_minutes_", comment: ""): + let date = now.addingTimeInterval(900) + return date.timeIntervalSince1970 + case NSLocalizedString("_30_minutes_", comment: ""): + let date = now.addingTimeInterval(1800) + return date.timeIntervalSince1970 + case NSLocalizedString("_1_hour_", comment: ""), NSLocalizedString("_an_hour_", comment: ""): + let date = now.addingTimeInterval(3600) + return date.timeIntervalSince1970 + case NSLocalizedString("_4_hours_", comment: ""): + let date = now.addingTimeInterval(14400) + return date.timeIntervalSince1970 + case NSLocalizedString("_day_", comment: ""): + return tomorrow.timeIntervalSince1970 + case NSLocalizedString("_this_week_", comment: ""): + return endweek.timeIntervalSince1970 + default: + return 0 + } + } + + private func createStatusesForPreview() -> [NKUserStatus] { + let meeting = NKUserStatus() + meeting.clearAt = nil + meeting.clearAtTime = "3600" + meeting.clearAtType = "period" + meeting.icon = "πŸ“…" + meeting.id = "meeting" + meeting.message = "In a meeting" + meeting.predefined = true + meeting.status = "busy" + meeting.userId = "preview_user" + + let commuting = NKUserStatus() + commuting.clearAt = nil + commuting.clearAtTime = "1800" + commuting.clearAtType = "period" + commuting.icon = "🚌" + commuting.id = "commuting" + commuting.message = "Commuting" + commuting.predefined = true + commuting.status = "away" + commuting.userId = "preview_user" + + return [meeting, commuting] + } +} + + diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift new file mode 100644 index 0000000000..de183c300a --- /dev/null +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import UIKit +import NextcloudKit + +struct NCStatusMessageView: View { + let account: String + + @State private var model = NCStatusMessageModel() + @Environment(\.dismiss) private var dismiss + @FocusState private var isTextFieldFocused: Bool + + var body: some View { + ScrollView { + VStack(spacing: 24) { + HStack(spacing: 12) { + EmojiField(text: $model.emojiText) + + TextField("_status_message_placehorder_", text: $model.statusText) + .focused($isTextFieldFocused) + .textFieldStyle(.roundedBorder) + } + .frame(height: 20) + + VStack(spacing: 18) { + ForEach(model.predefinedStatuses) { preset in + StatusPresetRow(model: $model, preset: preset) + } + } + .padding(.top, 8) + + HStack { + Text("_clear_status_message_after_") + Menu { + ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in + Button { + model.clearAfterString = option.rawValue + } label: { + Text(NSLocalizedString(option.rawValue, comment: "")) + } + } + } label: { + Text(model.clearAfterString) + .foregroundStyle(.blue) + Image(systemName: "chevron.up.chevron.down") + .imageScale(.small) + } + Spacer() + } + + HStack { + Button("_clear_") { + model.clearStatus(account: account) + dismiss() + } + .buttonStyle(.bordered) + .controlSize(.large) + + Spacer() + + Button("_set_status_message_") { + model.submitStatus(account: account) + dismiss() + } + .fontWeight(.semibold) + .buttonStyle(.borderedProminent) + .controlSize(.large) + .disabled(model.emojiText.isEmpty && model.statusText.isEmpty) + } + .padding(8) + } + .padding(24) + } + .scrollDismissesKeyboard(.interactively) + .onTapGesture { + isTextFieldFocused = false + } + .navigationTitle(NSLocalizedString("_select_status_message_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + model.getStatus(account: account) + model.getPredefinedStatusTexts(account: account) + } + .onDisappear { + model.setAccountUserStatus(account: account) + } + } +} + +private struct StatusPresetRow: View { + @Binding var model: NCStatusMessageModel + let preset: NKUserStatus + + var body: some View { + let cleatAtText = model.getPredefinedClearStatusString(clearAt: preset.clearAt, clearAtTime: preset.clearAtTime, clearAtType: preset.clearAtType) + + Button(action: { + model.chooseStatusPreset(preset: preset, clearAtText: cleatAtText) + }) { + HStack(spacing: 16) { + Text(preset.icon ?? "") + .font(.title3) + .frame(width: 32) + Text(preset.message ?? "") + .font(.headline) + .foregroundStyle(.primary) + Text("β€”") + .foregroundStyle(.secondary) + Text(cleatAtText) + .foregroundStyle(.secondary) + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} + +#Preview() { + NCStatusMessageView(account: "") +} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index d823b6802b..1aed65ceb6 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -137,12 +137,13 @@ "_delete_file_" = "Delete file"; "_delete_folder_" = "Delete folder"; "_size_" = "Size"; -"_set_user_status_" = "Set user status"; +"_set_user_status_" = "Online status"; +"_set_user_status_message_" = "Status message"; "_open_settings_" = "Open settings"; "_settings_account_request_" = "Request account at startup"; "_alias_" = "Alias"; -"_alias_placeholder_" = "Write the alias"; -"_alias_footer_" = "Give your account names a descriptive name such as Home, Office, School …"; +"_alias_placeholder_" = "Write alias"; +"_alias_footer_" = "Give your accounts a descriptive name such as Home, Office, School…"; "_privacy_legal_" = "Privacy and Legal Policy"; "_source_code_" = "Get source code"; "_account_select_" = "Select the account"; @@ -191,21 +192,23 @@ "_busy_" = "Busy"; /* User status */ +"_select_user_status_" = "Set online status"; +"_select_status_message_" = "Set status message"; "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; -"_clear_status_message_" = "Clear status message"; "_set_status_message_" = "Set status message"; -"_clear_status_message_after_" = "Clear status message after"; +"_clear_status_message_after_" = "Clear status after"; /* User status */ "_select_option_" = "Select option"; "_dont_clear_" = "Don't clear"; +"_15_minutes_" = "15 minutes"; "_30_minutes_" = "30 minutes"; "_an_hour_" = "an hour"; "_1_hour_" = "1 hour"; "_4_hours_" = "4 hours"; -"day" = "Today"; +"_day_" = "Today"; "_this_week_" = "This week"; "_days_" = "Days"; "_hours_" = "Hours"; diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 3532d96f68..74501b87a7 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -46,7 +46,7 @@ final class TransfersViewModel: ObservableObject, NCMetadataTransfersSuccessDele @MainActor func pollTransfers() async { while !Task.isCancelled { - if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" { + if isXcodeRunningForPreviews { isLoading = true // Items diff --git a/iOSClient/UserStatus/NCUserStatus.storyboard b/iOSClient/UserStatus/NCUserStatus.storyboard deleted file mode 100644 index e989ba3897..0000000000 --- a/iOSClient/UserStatus/NCUserStatus.storyboard +++ /dev/null @@ -1,521 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOSClient/UserStatus/NCUserStatus.swift b/iOSClient/UserStatus/NCUserStatus.swift deleted file mode 100644 index d7ade1e36e..0000000000 --- a/iOSClient/UserStatus/NCUserStatus.swift +++ /dev/null @@ -1,722 +0,0 @@ -// -// NCUserStatus.swift -// Nextcloud -// -// Created by Marino Faggiana on 25/05/21. -// Copyright Β© 2021 Marino Faggiana. All rights reserved. -// -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation -import UIKit -import SwiftUI -import NextcloudKit -import DropDown - -class NCUserStatus: UIViewController { - - @IBOutlet weak var buttonCancel: UIBarButtonItem! - - @IBOutlet weak var onlineButton: UIButton! - @IBOutlet weak var onlineImage: UIImageView! - @IBOutlet weak var onlineLabel: UILabel! - - @IBOutlet weak var awayButton: UIButton! - @IBOutlet weak var awayImage: UIImageView! - @IBOutlet weak var awayLabel: UILabel! - - @IBOutlet weak var dndButton: UIButton! - @IBOutlet weak var dndImage: UIImageView! - @IBOutlet weak var dndLabel: UILabel! - @IBOutlet weak var dndDescrLabel: UILabel! - - @IBOutlet weak var invisibleButton: UIButton! - @IBOutlet weak var invisibleImage: UIImageView! - @IBOutlet weak var invisibleLabel: UILabel! - @IBOutlet weak var invisibleDescrLabel: UILabel! - - @IBOutlet weak var busyButton: UIButton! - @IBOutlet weak var busyImage: UIImageView! - @IBOutlet weak var busyLabel: UILabel! - - @IBOutlet weak var statusMessageLabel: UILabel! - - @IBOutlet weak var statusMessageEmojiTextField: emojiTextField! - @IBOutlet weak var statusMessageTextField: UITextField! - - @IBOutlet weak var tableView: UITableView! - - @IBOutlet weak var clearStatusMessageAfterLabel: UILabel! - @IBOutlet weak var clearStatusMessageAfterText: UILabel! - - @IBOutlet weak var clearStatusMessageButton: UIButton! - @IBOutlet weak var setStatusMessageButton: UIButton! - - @IBOutlet weak var statusDescriptionTopConstraint: NSLayoutConstraint! - - private var statusPredefinedStatuses: [NKUserStatus] = [] - private let utility = NCUtility() - private var clearAtTimestamp: Double = 0 // Unix Timestamp representing the time to clear the status - private let borderWidthButton: CGFloat = 1.5 - private var borderColorButton: CGColor = NCBrandColor.shared.customer.cgColor - - public var account: String = "" - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = NSLocalizedString("_online_status_", comment: "") - - view.backgroundColor = .systemBackground - tableView.backgroundColor = .systemBackground - - borderColorButton = NCBrandColor.shared.getElement(account: account).cgColor - buttonCancel.image = utility.loadImage(named: "xmark", colors: [NCBrandColor.shared.iconImageColor]) - - onlineButton.layer.cornerRadius = 10 - onlineButton.layer.masksToBounds = true - onlineButton.backgroundColor = .systemGray5 - let onLine = utility.getUserStatus(userIcon: nil, userStatus: "online", userMessage: nil) - onlineImage.image = onLine.statusImage - onlineLabel.text = onLine.statusMessage - onlineLabel.textColor = NCBrandColor.shared.textColor - - awayButton.layer.cornerRadius = 10 - awayButton.layer.masksToBounds = true - awayButton.backgroundColor = .systemGray5 - let away = utility.getUserStatus(userIcon: nil, userStatus: "away", userMessage: nil) - awayImage.image = away.statusImage - awayLabel.text = away.statusMessage - awayLabel.textColor = NCBrandColor.shared.textColor - - dndButton.layer.cornerRadius = 10 - dndButton.layer.masksToBounds = true - dndButton.backgroundColor = .systemGray5 - let dnd = utility.getUserStatus(userIcon: nil, userStatus: "dnd", userMessage: nil) - dndImage.image = dnd.statusImage - dndLabel.text = dnd.statusMessage - dndLabel.textColor = NCBrandColor.shared.textColor - dndDescrLabel.text = dnd.descriptionMessage - dndDescrLabel.textColor = .darkGray - - invisibleButton.layer.cornerRadius = 10 - invisibleButton.layer.masksToBounds = true - invisibleButton.backgroundColor = .systemGray5 - let invisible = utility.getUserStatus(userIcon: nil, userStatus: "invisible", userMessage: nil) - invisibleImage.image = invisible.statusImage - invisibleLabel.text = invisible.statusMessage - invisibleLabel.textColor = NCBrandColor.shared.textColor - invisibleDescrLabel.text = invisible.descriptionMessage - invisibleDescrLabel.textColor = .darkGray - - busyButton.layer.cornerRadius = 10 - busyButton.layer.masksToBounds = true - busyButton.backgroundColor = .systemGray5 - let busy = utility.getUserStatus(userIcon: nil, userStatus: "busy", userMessage: nil) - busyImage.image = busy.statusImage - busyLabel.text = busy.statusMessage - busyLabel.textColor = NCBrandColor.shared.textColor - - statusMessageLabel.text = NSLocalizedString("_status_message_", comment: "") - statusMessageLabel.textColor = NCBrandColor.shared.textColor - - statusMessageEmojiTextField.delegate = self - statusMessageEmojiTextField.backgroundColor = .systemGray5 - - statusMessageTextField.delegate = self - statusMessageTextField.placeholder = NSLocalizedString("_status_message_placehorder_", comment: "") - statusMessageTextField.textColor = NCBrandColor.shared.textColor - - tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: 1)) - tableView.separatorStyle = UITableViewCell.SeparatorStyle.none - - clearStatusMessageAfterLabel.text = NSLocalizedString("_clear_status_message_after_", comment: "") - clearStatusMessageAfterLabel.textColor = NCBrandColor.shared.textColor - - clearStatusMessageAfterText.layer.cornerRadius = 5 - clearStatusMessageAfterText.layer.masksToBounds = true - clearStatusMessageAfterText.layer.borderWidth = 0.2 - clearStatusMessageAfterText.layer.borderColor = UIColor.lightGray.cgColor - clearStatusMessageAfterText.text = NSLocalizedString("_dont_clear_", comment: "") - if traitCollection.userInterfaceStyle == .dark { - clearStatusMessageAfterText.backgroundColor = .black - clearStatusMessageAfterText.textColor = .white - } else { - clearStatusMessageAfterText.backgroundColor = .white - clearStatusMessageAfterText.textColor = .black - } - let tap = UITapGestureRecognizer(target: self, action: #selector(self.actionClearStatusMessageAfterText(sender:))) - clearStatusMessageAfterText.isUserInteractionEnabled = true - clearStatusMessageAfterText.addGestureRecognizer(tap) - clearStatusMessageAfterText.text = " " + NSLocalizedString("_dont_clear_", comment: "") - - clearStatusMessageButton.layer.cornerRadius = 20 - clearStatusMessageButton.layer.masksToBounds = true - clearStatusMessageButton.layer.borderWidth = 0.5 - clearStatusMessageButton.layer.borderColor = UIColor.darkGray.cgColor - clearStatusMessageButton.backgroundColor = .systemGray5 - clearStatusMessageButton.setTitle(NSLocalizedString("_clear_status_message_", comment: ""), for: .normal) - clearStatusMessageButton.setTitleColor(NCBrandColor.shared.textColor, for: .normal) - - setStatusMessageButton.layer.cornerRadius = 20 - setStatusMessageButton.layer.masksToBounds = true - setStatusMessageButton.backgroundColor = NCBrandColor.shared.getElement(account: account) - setStatusMessageButton.setTitle(NSLocalizedString("_set_status_message_", comment: ""), for: .normal) - setStatusMessageButton.setTitleColor(NCBrandColor.shared.getText(account: account), for: .normal) - - if let capabilities = NCNetworking.shared.capabilities[account], !capabilities.userStatusSupportsBusy { - busyButton.isHidden = true - busyImage.isHidden = true - busyLabel.isHidden = true - - statusDescriptionTopConstraint.constant -= 80 - } - - getStatus() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - NextcloudKit.shared.getUserStatus(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "getUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, _, _, error in - if error == .success { - Task { - await NCManageDatabase.shared.setAccountUserStatusAsync(userStatusClearAt: clearAt, - userStatusIcon: icon, - userStatusMessage: message, - userStatusMessageId: messageId, - userStatusMessageIsPredefined: messageIsPredefined, - userStatusStatus: status, - userStatusStatusIsUserDefined: statusIsUserDefined, - account: account) - } - } - } - } - - func dismissIfError(_ error: NKError) { - if error != .success && error.errorCode != NCGlobal.shared.errorResourceNotFound { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.dismiss(animated: true) { - NCContentPresenter().showError(error: error) - } - } - } - } - - // MARK: ACTION - - @IBAction func actionCancel(_ sender: UIBarButtonItem) { - self.dismiss(animated: true, completion: nil) - } - - @IBAction func actionOnline(_ sender: UIButton) { - self.onlineButton.layer.borderWidth = self.borderWidthButton - self.onlineButton.layer.borderColor = self.borderColorButton - self.awayButton.layer.borderWidth = 0 - self.awayButton.layer.borderColor = nil - self.dndButton.layer.borderWidth = 0 - self.dndButton.layer.borderColor = nil - self.invisibleButton.layer.borderWidth = 0 - self.invisibleButton.layer.borderColor = nil - self.busyButton.layer.borderWidth = 0 - self.busyButton.layer.borderColor = nil - - let status = "online" - NextcloudKit.shared.setUserStatus(status: status, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - self.dismissIfError(error) - } - } - - @IBAction func actionAway(_ sender: UIButton) { - self.onlineButton.layer.borderWidth = 0 - self.onlineButton.layer.borderColor = nil - self.awayButton.layer.borderWidth = self.borderWidthButton - self.awayButton.layer.borderColor = self.borderColorButton - self.dndButton.layer.borderWidth = 0 - self.dndButton.layer.borderColor = nil - self.invisibleButton.layer.borderWidth = 0 - self.invisibleButton.layer.borderColor = nil - self.busyButton.layer.borderWidth = 0 - self.busyButton.layer.borderColor = nil - - let status = "away" - NextcloudKit.shared.setUserStatus(status: status, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - self.dismissIfError(error) - } - } - - @IBAction func actionDnd(_ sender: UIButton) { - self.onlineButton.layer.borderWidth = 0 - self.onlineButton.layer.borderColor = nil - self.awayButton.layer.borderWidth = 0 - self.awayButton.layer.borderColor = nil - self.dndButton.layer.borderWidth = self.borderWidthButton - self.dndButton.layer.borderColor = self.borderColorButton - self.invisibleButton.layer.borderWidth = 0 - self.invisibleButton.layer.borderColor = nil - self.busyButton.layer.borderWidth = 0 - self.busyButton.layer.borderColor = nil - - let status = "dnd" - NextcloudKit.shared.setUserStatus(status: status, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - self.dismissIfError(error) - } - } - - @IBAction func actionInvisible(_ sender: UIButton) { - self.onlineButton.layer.borderWidth = 0 - self.onlineButton.layer.borderColor = nil - self.awayButton.layer.borderWidth = 0 - self.awayButton.layer.borderColor = nil - self.dndButton.layer.borderWidth = 0 - self.dndButton.layer.borderColor = nil - self.invisibleButton.layer.borderWidth = self.borderWidthButton - self.invisibleButton.layer.borderColor = self.borderColorButton - self.busyButton.layer.borderWidth = 0 - self.busyButton.layer.borderColor = nil - - let status = "invisible" - NextcloudKit.shared.setUserStatus(status: status, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - self.dismissIfError(error) - } - } - - @IBAction func actionBusy(_ sender: UIButton) { - self.onlineButton.layer.borderWidth = 0 - self.onlineButton.layer.borderColor = nil - self.awayButton.layer.borderWidth = 0 - self.awayButton.layer.borderColor = nil - self.dndButton.layer.borderWidth = 0 - self.dndButton.layer.borderColor = nil - self.invisibleButton.layer.borderWidth = 0 - self.invisibleButton.layer.borderColor = nil - self.busyButton.layer.borderWidth = self.borderWidthButton - self.busyButton.layer.borderColor = self.borderColorButton - - let status = "busy" - NextcloudKit.shared.setUserStatus(status: status, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - self.dismissIfError(error) - } - } - - @objc func actionClearStatusMessageAfterText(sender: UITapGestureRecognizer) { - let dropDown = DropDown() - let appearance = DropDown.appearance() - let clearStatusMessageAfterTextBackup = clearStatusMessageAfterText.text - - if traitCollection.userInterfaceStyle == .dark { - appearance.backgroundColor = .black - appearance.textColor = .white - } else { - appearance.backgroundColor = .white - appearance.textColor = .black - } - appearance.cornerRadius = 5 - appearance.shadowRadius = 0 - appearance.animationEntranceOptions = .transitionCurlUp - appearance.animationduration = 0.25 - appearance.setupMaskedCorners([.layerMaxXMaxYCorner, .layerMinXMaxYCorner]) - - dropDown.dataSource.append(NSLocalizedString("_dont_clear_", comment: "")) - dropDown.dataSource.append(NSLocalizedString("_30_minutes_", comment: "")) - dropDown.dataSource.append(NSLocalizedString("_1_hour_", comment: "")) - dropDown.dataSource.append(NSLocalizedString("_4_hours_", comment: "")) - dropDown.dataSource.append(NSLocalizedString("day", comment: "")) - dropDown.dataSource.append(NSLocalizedString("_this_week_", comment: "")) - - dropDown.anchorView = clearStatusMessageAfterText - dropDown.topOffset = CGPoint(x: 0, y: -clearStatusMessageAfterText.bounds.height) - dropDown.width = clearStatusMessageAfterText.bounds.width - dropDown.direction = .top - - dropDown.selectionAction = { _, item in - - self.clearAtTimestamp = self.getClearAt(item) - self.clearStatusMessageAfterText.text = " " + item - } - - dropDown.cancelAction = { [unowned self] in - clearStatusMessageAfterText.text = clearStatusMessageAfterTextBackup - } - - clearStatusMessageAfterText.text = " " + NSLocalizedString("_select_option_", comment: "") - - dropDown.show() - } - - @IBAction func actionClearStatusMessage(_ sender: UIButton) { - NextcloudKit.shared.clearMessage(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "clearMessage") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - if error != .success { - NCContentPresenter().showError(error: error) - } - - self.dismiss(animated: true) - } - } - - @IBAction func actionSetStatusMessage(_ sender: UIButton) { - guard let message = statusMessageTextField.text else { return } - - NextcloudKit.shared.setCustomMessageUserDefined(statusIcon: statusMessageEmojiTextField.text, message: message, clearAt: clearAtTimestamp, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "setCustomMessageUserDefined") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - if error != .success { - NCContentPresenter().showError(error: error) - } - - self.dismiss(animated: true) - } - } - - // MARK: - Networking - - func getStatus() { - NextcloudKit.shared.getUserStatus(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "getUserStatus") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, clearAt, icon, message, _, _, status, _, _, _, error in - if error == .success || error.errorCode == NCGlobal.shared.errorResourceNotFound { - - if icon != nil { - self.statusMessageEmojiTextField.text = icon - } - if message != nil { - self.statusMessageTextField.text = message - } - if clearAt != nil { - self.clearStatusMessageAfterText.text = " " + self.getPredefinedClearStatusText(clearAt: clearAt, clearAtTime: nil, clearAtType: nil) - } - - switch status { - case "online": - self.onlineButton.layer.borderWidth = self.borderWidthButton - self.onlineButton.layer.borderColor = self.borderColorButton - case "away": - self.awayButton.layer.borderWidth = self.borderWidthButton - self.awayButton.layer.borderColor = self.borderColorButton - case "dnd": - self.dndButton.layer.borderWidth = self.borderWidthButton - self.dndButton.layer.borderColor = self.borderColorButton - case "invisible", "offline": - self.invisibleButton.layer.borderWidth = self.borderWidthButton - self.invisibleButton.layer.borderColor = self.borderColorButton - case "busy": - self.busyButton.layer.borderWidth = self.borderWidthButton - self.busyButton.layer.borderColor = self.borderColorButton - default: - print("No status") - } - - NextcloudKit.shared.getUserStatusPredefinedStatuses(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "getUserStatusPredefinedStatuses") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, userStatuses, _, error in - if error == .success { - if let userStatuses = userStatuses { - self.statusPredefinedStatuses = userStatuses - } - - self.tableView.reloadData() - } - - self.dismissIfError(error) - } - - } - - self.dismissIfError(error) - } - } - - // MARK: - Algorithms - - func getClearAt(_ clearAtString: String) -> Double { - let now = Date() - let calendar = Calendar.current - let gregorian = Calendar(identifier: .gregorian) - let midnight = calendar.startOfDay(for: now) - guard let tomorrow = calendar.date(byAdding: .day, value: 1, to: midnight) else { return 0 } - guard let startweek = gregorian.date(from: gregorian.dateComponents([.yearForWeekOfYear, .weekOfYear], from: now)) else { return 0 } - guard let endweek = gregorian.date(byAdding: .day, value: 6, to: startweek) else { return 0 } - - switch clearAtString { - case NSLocalizedString("_dont_clear_", comment: ""): - return 0 - case NSLocalizedString("_30_minutes_", comment: ""): - let date = now.addingTimeInterval(1800) - return date.timeIntervalSince1970 - case NSLocalizedString("_1_hour_", comment: ""), NSLocalizedString("_an_hour_", comment: ""): - let date = now.addingTimeInterval(3600) - return date.timeIntervalSince1970 - case NSLocalizedString("_4_hours_", comment: ""): - let date = now.addingTimeInterval(14400) - return date.timeIntervalSince1970 - case NSLocalizedString("day", comment: ""): - return tomorrow.timeIntervalSince1970 - case NSLocalizedString("_this_week_", comment: ""): - return endweek.timeIntervalSince1970 - default: - return 0 - } - } - - func getPredefinedClearStatusText(clearAt: Date?, clearAtTime: String?, clearAtType: String?) -> String { - // Date - if let clearAt { - let from = Date() - let to = clearAt - let day = Calendar.current.dateComponents([.day], from: from, to: to).day ?? 0 - let hour = Calendar.current.dateComponents([.hour], from: from, to: to).hour ?? 0 - let minute = Calendar.current.dateComponents([.minute], from: from, to: to).minute ?? 0 - - if day > 0 { - if day == 1 { return NSLocalizedString("day", comment: "") } - return "\(day) " + NSLocalizedString("_days_", comment: "") - } - - if hour > 0 { - if hour == 1 { return NSLocalizedString("_an_hour_", comment: "") } - if hour == 4 { return NSLocalizedString("_4_hour_", comment: "") } - return "\(hour) " + NSLocalizedString("_hours_", comment: "") - } - - if minute > 0 { - if minute >= 25 && minute <= 30 { return NSLocalizedString("_30_minutes_", comment: "") } - if minute > 30 { return NSLocalizedString("_an_hour_", comment: "") } - return "\(minute) " + NSLocalizedString("_minutes_", comment: "") - } - } - // Period - if let clearAtTime, clearAtType == "period" { - switch clearAtTime { - case "3600": - return NSLocalizedString("_an_hour_", comment: "") - case "1800": - return NSLocalizedString("_30_minutes_", comment: "") - default: - return NSLocalizedString("_dont_clear_", comment: "") - } - } - // End of - if let clearAtTime, clearAtType == "end-of" { - return NSLocalizedString(clearAtTime, comment: "") - } - - return NSLocalizedString("_dont_clear_", comment: "") - } -} - -extension NCUserStatus: UITextFieldDelegate { - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if textField is emojiTextField { - if string.isEmpty { - textField.text = "πŸ˜€" - return false - } - textField.text = string - textField.endEditing(true) - } - return true - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return false - } -} - -class emojiTextField: UITextField { - override var textInputContextIdentifier: String? { "" } // return non-nil to show the Emoji keyboard Β―\_(ツ)_/Β― - - override var textInputMode: UITextInputMode? { - for mode in UITextInputMode.activeInputModes { - if mode.primaryLanguage == "emoji" { - return mode - } - } - return nil - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - func commonInit() { - NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil) - } - - @objc func inputModeDidChange(_ notification: Notification) { - guard isFirstResponder else { - return - } - - DispatchQueue.main.async { [weak self] in - self?.reloadInputViews() - } - } -} - -extension NCUserStatus: UITableViewDelegate { - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 45 - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let cell = tableView.cellForRow(at: indexPath) else { return } - let status = statusPredefinedStatuses[indexPath.row] - - if let messageId = status.id { - NextcloudKit.shared.setCustomMessagePredefined(messageId: messageId, clearAt: 0, account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - path: messageId, - name: "setCustomMessagePredefined") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, error in - cell.isSelected = false - - if error == .success { - let clearAtTimestampString = self.getPredefinedClearStatusText(clearAt: status.clearAt, clearAtTime: status.clearAtTime, clearAtType: status.clearAtType) - - self.statusMessageEmojiTextField.text = status.icon - self.statusMessageTextField.text = status.message - self.clearStatusMessageAfterText.text = " " + clearAtTimestampString - self.clearAtTimestamp = self.getClearAt(clearAtTimestampString) - } - - self.dismissIfError(error) - } - } - } -} - -extension NCUserStatus: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return statusPredefinedStatuses.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - let status = statusPredefinedStatuses[indexPath.row] - let icon = cell.viewWithTag(10) as? UILabel - let message = cell.viewWithTag(20) as? UILabel - var timeString = getPredefinedClearStatusText(clearAt: status.clearAt, clearAtTime: status.clearAtTime, clearAtType: status.clearAtType) - - cell.backgroundColor = tableView.backgroundColor - icon?.text = status.icon - - if let messageText = status.message { - message?.text = messageText - timeString = " - " + timeString - let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: messageText + timeString) - attributedString.setColor(color: .lightGray, font: UIFont.systemFont(ofSize: 15), forText: timeString) - message?.attributedText = attributedString - } - - return cell - } -} - -struct UserStatusView: UIViewControllerRepresentable { - @Binding var showUserStatus: Bool - var account: String - - class Coordinator: NSObject { - var parent: UserStatusView - - init(_ parent: UserStatusView) { - self.parent = parent - } - } - - func makeUIViewController(context: Context) -> UINavigationController { - let storyboard = UIStoryboard(name: "NCUserStatus", bundle: nil) - let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController - let viewController = navigationController!.topViewController as? NCUserStatus - viewController?.account = account - return navigationController! - } - - func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } -} diff --git a/iOSClient/UserStatus/NCUserStatusModel.swift b/iOSClient/UserStatus/NCUserStatusModel.swift new file mode 100644 index 0000000000..8df08fd87e --- /dev/null +++ b/iOSClient/UserStatus/NCUserStatusModel.swift @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import NextcloudKit + +@Observable class NCUserStatusModel { + struct UserStatus: Hashable { + let name: String + let titleKey: String + var descriptionKey: String = "" + } + + @ObservationIgnored var userStatuses: [UserStatus] = [ + .init(name: "online", titleKey: "_online_"), + .init(name: "away", titleKey: "_away_"), + .init(name: "dnd", titleKey: "_dnd_", descriptionKey: "_dnd_description_"), + .init(name: "invisible", titleKey: "_invisible_", descriptionKey: "_invisible_description_") + ] + + var selectedStatus: String? + var canDismiss = false + + @ObservationIgnored let account: String + + init(account: String) { + self.account = account + + if let capabilities = NCNetworking.shared.capabilities[account], capabilities.userStatusSupportsBusy { + userStatuses.insert(.init(name: "busy", titleKey: "_busy_"), at: 2) + } + } + + func getStatusDetails(name: String) -> (statusImage: UIImage?, statusImageColor: UIColor, statusMessage: String, descriptionMessage: String) { + return NCUtility().getUserStatus(userIcon: nil, userStatus: name, userMessage: nil) + } + + func getStatus(account: String) { + Task { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, name: "getUserStatus") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error == .success { + selectedStatus = result.status + } else { + NCContentPresenter().showError(error: result.error) + } + } + } + + func setStatus(account: String) { + Task { + let result = await NextcloudKit.shared.setUserStatusAsync(status: selectedStatus ?? "", account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, name: "setUserStatus") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + self.canDismiss = true + } + } + + if result.error != .success { + NCContentPresenter().showError(error: result.error) + } + } + } + + func setAccountUserStatus(account: String) { + Task { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, name: "getUserStatus") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if result.error != .success { + NCContentPresenter().showError(error: result.error) + } + + await NCManageDatabase.shared.setAccountUserStatusAsync(userStatusClearAt: result.clearAt, + userStatusIcon: result.icon, + userStatusMessage: result.message, + userStatusMessageId: result.messageId, + userStatusMessageIsPredefined: result.messageIsPredefined, + userStatusStatus: result.status, + userStatusStatusIsUserDefined: result.statusIsUserDefined, + account: result.account) + } + } +} diff --git a/iOSClient/UserStatus/NCUserStatusView.swift b/iOSClient/UserStatus/NCUserStatusView.swift new file mode 100644 index 0000000000..8b234ec472 --- /dev/null +++ b/iOSClient/UserStatus/NCUserStatusView.swift @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct NCUserStatusView: View { + let account: String + + @State private var model: NCUserStatusModel + @Environment(\.dismiss) private var dismiss + + init(account: String) { + self.account = account + model = NCUserStatusModel(account: account) + } + + var body: some View { + List { + ForEach(model.userStatuses, id: \.self) { item in + HStack { + let status = model.getStatusDetails(name: item.name) + + Image(uiImage: status.statusImage ?? UIImage()) + .renderingMode(.template) + .resizable() + .foregroundStyle(Color(status.statusImageColor)) + .frame(width: 20, height: 20) + VStack(alignment: .leading) { + Text(NSLocalizedString(item.titleKey, comment: "")) + + if !item.descriptionKey.isEmpty { + Text(NSLocalizedString(item.descriptionKey, comment: "")).font(.subheadline).foregroundStyle(.secondary) + } + } + Spacer() + if model.selectedStatus == item.name { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .contentShape(Rectangle()) // make the whole row tappable + .onTapGesture { + model.selectedStatus = (model.selectedStatus == item.name) ? nil : item.name + model.setStatus(account: account) + } + .onChange(of: model.canDismiss) { _, newValue in + if newValue { dismiss() } + } + } + } + .navigationTitle(NSLocalizedString("_select_user_status_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + model.getStatus(account: account) + } + .onDisappear { + model.setAccountUserStatus(account: account) + } + } +} + +#Preview { + NavigationStack { + NCUserStatusView(account: "demo@example.com") + } +} diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index cda3acf1a1..1e8b7c30c0 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -376,7 +376,9 @@ extension NCUtility { if let userMessage = userMessage { statusMessage += userMessage } + statusMessage = statusMessage.trimmingCharacters(in: .whitespaces) + if statusMessage.isEmpty { statusMessage = messageUserDefined }