From 5ea5172bc2031f3dfbe2c1952ec7290ca1fa3347 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 19 Nov 2025 14:00:51 +0100 Subject: [PATCH 01/13] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 28 +++ .../NCAccountSettingsView.swift | 55 +++--- iOSClient/StatusMessage/EmojiTextField.swift | 90 ++++++++++ .../StatusMessage/NCStatusMessageModel.swift | 20 +++ .../StatusMessage/NCStatusMessageView.swift | 164 ++++++++++++++++++ .../en.lproj/Localizable.strings | 4 +- iOSClient/UserStatus/NCUserStatusModel.swift | 62 +++++++ iOSClient/UserStatus/NCUserStatusView.swift | 57 ++++++ iOSClient/Utility/NCUtility+Image.swift | 2 + 9 files changed, 460 insertions(+), 22 deletions(-) create mode 100644 iOSClient/StatusMessage/EmojiTextField.swift create mode 100644 iOSClient/StatusMessage/NCStatusMessageModel.swift create mode 100644 iOSClient/StatusMessage/NCStatusMessageView.swift create mode 100644 iOSClient/UserStatus/NCUserStatusModel.swift create mode 100644 iOSClient/UserStatus/NCUserStatusView.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 01a08bd22b..d183d2d67e 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 */; }; @@ -1237,6 +1242,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 = ""; }; @@ -1254,6 +1262,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 = ""; }; @@ -2106,6 +2116,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 = ( @@ -3139,6 +3159,8 @@ F7EFC0CB256BF89300461AAD /* UserStatus */ = { isa = PBXGroup; children = ( + F3A0B1EF2EC76FE800F10B82 /* NCUserStatusModel.swift */, + F3A0B1F02EC76FE800F10B82 /* NCUserStatusView.swift */, F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */, F7E0CDCE265CE8610044854E /* NCUserStatus.storyboard */, ); @@ -3251,6 +3273,7 @@ F7132C6D2D085AD200B42D6A /* Terms of service */, F7E9C41320F4CA870040CF18 /* Transfers */, F78F74322163753B00C2ADAD /* Trash */, + F34E1AD52ECB935E00FA10C3 /* StatusMessage */, F7EFC0CB256BF89300461AAD /* UserStatus */, F7BFFA991A24D7BB0044ED85 /* Utility */, F79630EC215526B60015EEA5 /* Viewer */, @@ -4437,6 +4460,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 */, @@ -4497,6 +4521,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 */, @@ -4564,6 +4590,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 */, @@ -4609,6 +4636,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/NCAccountSettingsView.swift b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift index 12fb4dc4f2..8a4b6f2e6b 100644 --- a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift +++ b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift @@ -141,30 +141,43 @@ 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 diff --git a/iOSClient/StatusMessage/EmojiTextField.swift b/iOSClient/StatusMessage/EmojiTextField.swift new file mode 100644 index 0000000000..67c4dd7694 --- /dev/null +++ b/iOSClient/StatusMessage/EmojiTextField.swift @@ -0,0 +1,90 @@ +// 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() { + borderStyle = .none + textAlignment = .center + font = .systemFont(ofSize: 28) + setContentHuggingPriority(.required, for: .horizontal) + setContentCompressionResistancePriority(.required, for: .horizontal) + NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil) + addTarget(self, action: #selector(textChanged), for: .editingChanged) + } + + @objc private 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 } + } +} + +// SwiftUI wrapper +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 textFieldDidChangeSelection(_ textField: UITextField) { + parent.text = textField.text ?? "" + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow change then enforce single grapheme cluster in textFieldDidChangeSelection + return true + } + } +} diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift new file mode 100644 index 0000000000..26205f7ab4 --- /dev/null +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +@Observable class NCStatusMessageModel { + @ObservationIgnored let statusPresets: [Status] = [ + .init(emoji: "📅", title: "In a meeting", detail: "In 1 hour"), + .init(emoji: "🚌", title: "Commuting", detail: "In 30 minutes"), + .init(emoji: "🏡", title: "Working remotely", detail: "Today"), + .init(emoji: "🤒", title: "Out sick", detail: "Today"), + .init(emoji: "🌴", title: "Vacationing", detail: "Don't clear") + ] + + var statusText: String = "" + var selectedStatus: Status? +// e var clearAfter: ClearAfter = .dontClear + var emojiText: String = "😀" +} diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift new file mode 100644 index 0000000000..78916e95bb --- /dev/null +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import UIKit + +struct Status: Identifiable, Equatable { + let id = UUID() + let emoji: String + let title: String + let detail: String +} + +struct NCStatusMessageView: View { + let account: String + + // MARK: - State + @State private var model = NCStatusMessageModel() +// @State private var statusText: String = "" +// @State private var selectedStatus: Status? + @State private var clearAfter: ClearAfter = .dontClear +// @State private var emojiText: String = "😀" + + enum ClearAfter: String, CaseIterable, Identifiable { + case dontClear = "Don't clear" + case thirtyMinutes = "In 30 minutes" + case oneHour = "In 1 hour" + case today = "Today" + case thisWeek = "This week" + + var id: String { rawValue } + } + + var body: some View { + VStack(spacing: 24) { + // Input field with emoji + HStack(alignment: .top, spacing: 12) { + EmojiField(text: $model.emojiText) + .frame(width: 40, height: 40, alignment: .center) + .background(Color.clear) + + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 12) + .stroke(.secondary.opacity(0.6), lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.secondarySystemBackground).opacity(0.2)) + ) + + TextField("What is your status?", text: $model.statusText) + .textFieldStyle(.plain) + .padding(.horizontal, 14) + .padding(.vertical, 14) + } +// .frame(minHeight: 72) + } + + // Presets list + VStack(spacing: 18) { + ForEach(model.statusPresets) { preset in + StatusRow(preset: preset) { + model.selectedStatus = preset + model.emojiText = preset.emoji + model.statusText = preset.title + // Update clearAfter suggestion if present + switch preset.title { + case "In a meeting": clearAfter = .oneHour + case "Commuting": clearAfter = .thirtyMinutes + case "Working remotely": clearAfter = .today + case "Out sick": clearAfter = .today + case "Vacationing": clearAfter = .dontClear + default: break + } + } + } + } + .padding(.top, 8) + + // Clear after section + VStack(alignment: .leading, spacing: 8) { + Text("Clear status after") + .font(.headline) + HStack(spacing: 12) { + Text("🌴") + .font(.title3) + Picker("Clear after", selection: $clearAfter) { + ForEach(ClearAfter.allCases) { option in + Text(option.rawValue).tag(option) + } + } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + // Bottom actions + HStack { + Button("Clear") { + model.statusText = "" + model.selectedStatus = nil + clearAfter = .dontClear + model.emojiText = "😀" + } + .foregroundStyle(.blue) + + Spacer() + + Button(action: setMessage) { + Text("Set message") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + .tint(Color(.systemGray3)) + .foregroundStyle(Color(.systemBackground)) + .clipShape(Capsule()) + .frame(width: 220) + } + .padding(.top, 8) + } + .padding(24) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color(.systemBackground)) + } + + private func setMessage() { + // Hook up to your model or networking here. + // For now we just print to console. + let preset = model.selectedStatus?.title ?? "Custom" + print("Set status: \(model.emojiText) \(model.statusText.isEmpty ? preset : model.statusText) — clear: \(clearAfter.rawValue)") + } +} + +private struct StatusRow: View { + let preset: Status + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 16) { + Text(preset.emoji) + .font(.title2) + .frame(width: 32) + Text(preset.title) + .font(.title3.weight(.semibold)) + .foregroundStyle(.primary) + Text("—") + .foregroundStyle(.secondary) + Text(preset.detail) + .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 28992e6a09..a810efa5f7 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -137,7 +137,8 @@ "_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"; @@ -191,6 +192,7 @@ "_busy_" = "Busy"; /* User status */ +"_select_user_status_" = "Select online status"; "_status_message_" = "Status message"; "_status_message_placehorder_" = "What is your status?"; "_online_status_" = "Online status"; diff --git a/iOSClient/UserStatus/NCUserStatusModel.swift b/iOSClient/UserStatus/NCUserStatusModel.swift new file mode 100644 index 0000000000..bdbd613cfe --- /dev/null +++ b/iOSClient/UserStatus/NCUserStatusModel.swift @@ -0,0 +1,62 @@ +// 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 names: [String] // should be array + let titleKey: String + } + + @ObservationIgnored let userStatuses: [UserStatus] = [ + .init(names: ["online"], titleKey: "_online_"), + .init(names: ["away"], titleKey: "_away_"), + .init(names: ["busy"], titleKey: "_busy_"), + .init(names: ["dnd"], titleKey: "_dnd_"), + .init(names: ["invisible", "offline"], titleKey: "_invisible_") + ] + + var selectedStatus: String? + + @ObservationIgnored let account: String + + init(account: String) { + self.account = account + } + + func getStatusDetails(name: String) -> (statusImage: UIImage?, statusImageColor: UIColor, statusMessage: String, descriptionMessage: String) { + return NCUtility().getUserStatus(userIcon: nil, userStatus: name, userMessage: nil) + } + + func getStatuses() { + // NCUtility().getUserStatus(userIcon: <#T##String?#>, userStatus: <#T##String?#>, userMessage: <#T##String?#>) + } + + func getStatus(account: String) { + Task { + let status = 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) + } + } + + selectedStatus = status.status + } + } + + 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) + } + } + } + } +} diff --git a/iOSClient/UserStatus/NCUserStatusView.swift b/iOSClient/UserStatus/NCUserStatusView.swift new file mode 100644 index 0000000000..545611fff7 --- /dev/null +++ b/iOSClient/UserStatus/NCUserStatusView.swift @@ -0,0 +1,57 @@ +// 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 selectedItem: String? + @State private var model: NCUserStatusModel + + 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.names.first!) + + Image(uiImage: status.statusImage ?? UIImage()) + .renderingMode(.template) + .resizable() + .foregroundStyle(Color(status.statusImageColor)) + .frame(width: 20, height: 20) + .padding(.trailing, 8) + Text(NSLocalizedString(item.titleKey, comment: "")) + Spacer() + if model.selectedStatus == item.names.first { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .contentShape(Rectangle()) // make the whole row tappable + .onTapGesture { + let firstStatus = item.names.first! + model.selectedStatus = (model.selectedStatus == firstStatus) ? nil : firstStatus + model.setStatus(account: account) + } + } + } + .navigationTitle(NSLocalizedString("_select_user_status_", comment: "")) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + model.getStatus(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 } From 3892467e7be4244772ce1d12493beda666805105 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 19 Nov 2025 14:41:59 +0100 Subject: [PATCH 02/13] WIP Signed-off-by: Milen Pivchev --- .../NCAccountSettingsView.swift | 25 +++++++++++ iOSClient/UserStatus/NCUserStatusModel.swift | 42 ++++++++++++++++--- iOSClient/UserStatus/NCUserStatusView.swift | 17 +++++++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift index 8a4b6f2e6b..49ca18be60 100644 --- a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift +++ b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift @@ -178,6 +178,31 @@ struct NCAccountSettingsView: View { .font(.subheadline) } } + + 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)) + } + .font(.subheadline) + }) + .sheet(isPresented: $showUserStatus) { + if let account = model.tblAccount?.account { + UserStatusView(showUserStatus: $showUserStatus, account: account) + } + } + .onChange(of: showUserStatus) { } } // // Certificate server diff --git a/iOSClient/UserStatus/NCUserStatusModel.swift b/iOSClient/UserStatus/NCUserStatusModel.swift index bdbd613cfe..c7ff1760c4 100644 --- a/iOSClient/UserStatus/NCUserStatusModel.swift +++ b/iOSClient/UserStatus/NCUserStatusModel.swift @@ -6,24 +6,31 @@ import NextcloudKit @Observable class NCUserStatusModel { struct UserStatus: Hashable { - let names: [String] // should be array + let names: [String] let titleKey: String + var descriptionKey: String = "" } - @ObservationIgnored let userStatuses: [UserStatus] = [ + @ObservationIgnored var userStatuses: [UserStatus] = [ .init(names: ["online"], titleKey: "_online_"), .init(names: ["away"], titleKey: "_away_"), - .init(names: ["busy"], titleKey: "_busy_"), - .init(names: ["dnd"], titleKey: "_dnd_"), - .init(names: ["invisible", "offline"], titleKey: "_invisible_") + .init(names: ["dnd"], titleKey: "_dnd_", descriptionKey: "_dnd_description_"), + .init(names: ["invisible", "offline"], titleKey: "_invisible_", descriptionKey: "_invisible_description_") ] var selectedStatus: String? + var userStatusSupportsBusy = false + var canDismiss = false @ObservationIgnored let account: String init(account: String) { self.account = account + + if let capabilities = NCNetworking.shared.capabilities[account], capabilities.userStatusSupportsBusy { +// userStatusSupportsBusy = true + userStatuses.insert(.init(names: ["busy"], titleKey: "_busy_"), at: 2) + } } func getStatusDetails(name: String) -> (statusImage: UIImage?, statusImageColor: UIColor, statusMessage: String, descriptionMessage: String) { @@ -46,6 +53,8 @@ import NextcloudKit selectedStatus = status.status } + + } func setStatus(account: String) { @@ -59,4 +68,27 @@ import NextcloudKit } } } + + func setAccountUserStatus(account: String) { + 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) + } + } + } + } } diff --git a/iOSClient/UserStatus/NCUserStatusView.swift b/iOSClient/UserStatus/NCUserStatusView.swift index 545611fff7..4fbf4997c6 100644 --- a/iOSClient/UserStatus/NCUserStatusView.swift +++ b/iOSClient/UserStatus/NCUserStatusView.swift @@ -9,6 +9,7 @@ struct NCUserStatusView: View { // @State private var selectedItem: String? @State private var model: NCUserStatusModel + @Environment(\.dismiss) private var dismiss init(account: String) { self.account = account @@ -26,8 +27,14 @@ struct NCUserStatusView: View { .resizable() .foregroundStyle(Color(status.statusImageColor)) .frame(width: 20, height: 20) - .padding(.trailing, 8) - Text(NSLocalizedString(item.titleKey, comment: "")) +// .padding(.trailing, 8) + 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.names.first { Image(systemName: "checkmark") @@ -40,6 +47,9 @@ struct NCUserStatusView: View { model.selectedStatus = (model.selectedStatus == firstStatus) ? nil : firstStatus model.setStatus(account: account) } + .onChange(of: model.canDismiss) { _, newValue in + if newValue { dismiss() } + } } } .navigationTitle(NSLocalizedString("_select_user_status_", comment: "")) @@ -47,6 +57,9 @@ struct NCUserStatusView: View { .onAppear { model.getStatus(account: account) } + .onDisappear { + model.setAccountUserStatus(account: account) + } } } From 18cd6048edb985d9ea71bc7e01f00b589ebb0154 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 20 Nov 2025 16:43:22 +0100 Subject: [PATCH 03/13] WIP Signed-off-by: Milen Pivchev --- .../NCAccountSettingsView.swift | 18 ++ iOSClient/StatusMessage/EmojiTextField.swift | 1 - .../StatusMessage/NCStatusMessageModel.swift | 52 +++++- .../StatusMessage/NCStatusMessageView.swift | 167 +++++++----------- .../en.lproj/Localizable.strings | 3 +- iOSClient/UserStatus/NCUserStatusModel.swift | 23 +-- iOSClient/UserStatus/NCUserStatusView.swift | 7 +- 7 files changed, 142 insertions(+), 129 deletions(-) diff --git a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift index 49ca18be60..9cc4e8a381 100644 --- a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift +++ b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift @@ -204,6 +204,24 @@ struct NCAccountSettingsView: View { } .onChange(of: showUserStatus) { } } + + 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) + } + // // Certificate server if model.isAdminGroup() { diff --git a/iOSClient/StatusMessage/EmojiTextField.swift b/iOSClient/StatusMessage/EmojiTextField.swift index 67c4dd7694..edac9cecab 100644 --- a/iOSClient/StatusMessage/EmojiTextField.swift +++ b/iOSClient/StatusMessage/EmojiTextField.swift @@ -83,7 +83,6 @@ struct EmojiField: UIViewRepresentable { } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - // Allow change then enforce single grapheme cluster in textFieldDidChangeSelection return true } } diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift index 26205f7ab4..2af59ab678 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -5,16 +5,50 @@ import SwiftUI @Observable class NCStatusMessageModel { - @ObservationIgnored let statusPresets: [Status] = [ - .init(emoji: "📅", title: "In a meeting", detail: "In 1 hour"), - .init(emoji: "🚌", title: "Commuting", detail: "In 30 minutes"), - .init(emoji: "🏡", title: "Working remotely", detail: "Today"), - .init(emoji: "🤒", title: "Out sick", detail: "Today"), - .init(emoji: "🌴", title: "Vacationing", detail: "Don't clear") + struct StatusPreset: Identifiable, Equatable { + let id = UUID() + let emoji: String + let title: String + let clearAfter: ClearAfter + } + + enum ClearAfter: String, CaseIterable, Identifiable { + case dontClear = "Don't clear" + case thirtyMinutes = "30 minutes" + case oneHour = "1 hour" + case fourHours = "4 hours" + case today = "Today" + case thisWeek = "This week" + + var id: String { rawValue } + } + + @ObservationIgnored let statusPresets: [StatusPreset] = [ + .init(emoji: "📅", title: "In a meeting", clearAfter: .oneHour), + .init(emoji: "🚌", title: "Commuting", clearAfter: .thirtyMinutes), + .init(emoji: "⏳", title: "Be right back", clearAfter: .thirtyMinutes), + .init(emoji: "🏡", title: "Working remotely", clearAfter: .thisWeek), + .init(emoji: "🤒", title: "Out sick", clearAfter: .today), + .init(emoji: "🌴", title: "Vacationing", clearAfter: .dontClear) ] - var statusText: String = "" - var selectedStatus: Status? -// e var clearAfter: ClearAfter = .dontClear var emojiText: String = "😀" + var statusText: String = "" + var clearAfter: ClearAfter = .dontClear + + func chooseStatusPreset(preset: StatusPreset) { + emojiText = preset.emoji + statusText = preset.title + clearAfter = preset.clearAfter + } + + func clearStatus() { + emojiText = "😀" + statusText = "" + clearAfter = .dontClear + } + + func submitStatus() { + + } } diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 78916e95bb..deebd88321 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -19,127 +19,96 @@ struct NCStatusMessageView: View { @State private var model = NCStatusMessageModel() // @State private var statusText: String = "" // @State private var selectedStatus: Status? - @State private var clearAfter: ClearAfter = .dontClear // @State private var emojiText: String = "😀" - - enum ClearAfter: String, CaseIterable, Identifiable { - case dontClear = "Don't clear" - case thirtyMinutes = "In 30 minutes" - case oneHour = "In 1 hour" - case today = "Today" - case thisWeek = "This week" - - var id: String { rawValue } - } + @FocusState private var isTextFieldFocused: Bool var body: some View { - VStack(spacing: 24) { - // Input field with emoji - HStack(alignment: .top, spacing: 12) { - EmojiField(text: $model.emojiText) - .frame(width: 40, height: 40, alignment: .center) - .background(Color.clear) - - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 12) - .stroke(.secondary.opacity(0.6), lineWidth: 1) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(Color(.secondarySystemBackground).opacity(0.2)) - ) + ScrollView { + VStack(spacing: 24) { + // Input field with emoji + HStack(spacing: 12) { + EmojiField(text: $model.emojiText) TextField("What is your status?", text: $model.statusText) - .textFieldStyle(.plain) - .padding(.horizontal, 14) - .padding(.vertical, 14) + .focused($isTextFieldFocused) + .textFieldStyle(.roundedBorder) } -// .frame(minHeight: 72) - } + .frame(height: 20) - // Presets list - VStack(spacing: 18) { - ForEach(model.statusPresets) { preset in - StatusRow(preset: preset) { - model.selectedStatus = preset - model.emojiText = preset.emoji - model.statusText = preset.title - // Update clearAfter suggestion if present - switch preset.title { - case "In a meeting": clearAfter = .oneHour - case "Commuting": clearAfter = .thirtyMinutes - case "Working remotely": clearAfter = .today - case "Out sick": clearAfter = .today - case "Vacationing": clearAfter = .dontClear - default: break - } + // Presets list + VStack(spacing: 18) { + ForEach(model.statusPresets) { preset in + StatusPresetRow(model: $model, preset: preset) } } - } - .padding(.top, 8) - - // Clear after section - VStack(alignment: .leading, spacing: 8) { - Text("Clear status after") - .font(.headline) - HStack(spacing: 12) { - Text("🌴") - .font(.title3) - Picker("Clear after", selection: $clearAfter) { - ForEach(ClearAfter.allCases) { option in - Text(option.rawValue).tag(option) + .padding(.top, 8) + + // Clear after section + VStack(alignment: .leading, spacing: 8) { + Text("Clear status after") + .font(.headline) + HStack(spacing: 12) { + Text("🌴") + .font(.title3) + Picker("Clear after", selection: $model.clearAfter) { + ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in + Text(option.rawValue).tag(option) + } } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) } - .pickerStyle(.menu) - .frame(maxWidth: .infinity, alignment: .leading) } - } - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) - // Bottom actions - HStack { - Button("Clear") { - model.statusText = "" - model.selectedStatus = nil - clearAfter = .dontClear - model.emojiText = "😀" - } - .foregroundStyle(.blue) + // Bottom actions + HStack { + Button("Clear") { + model.clearStatus() + } + .frame(maxWidth: .infinity) + .buttonStyle(.bordered) +// .foregroundStyle(.blue) - Spacer() + Spacer() + + Button(action: {}) { + Text("Set message") + .fontWeight(.semibold) +// .padding(.vertical, 14) + .frame(maxWidth: .infinity) - Button(action: setMessage) { - Text("Set message") - .fontWeight(.semibold) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) +// .tint(Color(.systemGray3)) +// .foregroundStyle(Color(.systemBackground)) +// .clipShape(Capsule()) +// .frame(width: 220) } - .buttonStyle(.borderedProminent) - .tint(Color(.systemGray3)) - .foregroundStyle(Color(.systemBackground)) - .clipShape(Capsule()) - .frame(width: 220) + .frame(minWidth: 0, maxWidth: .infinity) + .padding(.top, 8) } - .padding(.top, 8) + .padding(24) } - .padding(24) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color(.systemBackground)) - } - - private func setMessage() { - // Hook up to your model or networking here. - // For now we just print to console. - let preset = model.selectedStatus?.title ?? "Custom" - print("Set status: \(model.emojiText) \(model.statusText.isEmpty ? preset : model.statusText) — clear: \(clearAfter.rawValue)") + .scrollDismissesKeyboard(.interactively) + .onTapGesture { + isTextFieldFocused = false + } +// .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) +// .background(Color(.systemBackground)) + .navigationTitle(NSLocalizedString("_select_status_message_", comment: "")) + .navigationBarTitleDisplayMode(.inline) } } -private struct StatusRow: View { - let preset: Status - let onTap: () -> Void +private struct StatusPresetRow: View { + @Binding var model: NCStatusMessageModel + let preset: NCStatusMessageModel.StatusPreset var body: some View { - Button(action: onTap) { + Button(action: { + model.chooseStatusPreset(preset: preset) + }) { HStack(spacing: 16) { Text(preset.emoji) .font(.title2) @@ -149,7 +118,7 @@ private struct StatusRow: View { .foregroundStyle(.primary) Text("—") .foregroundStyle(.secondary) - Text(preset.detail) + Text(preset.clearAfter.rawValue) .foregroundStyle(.secondary) Spacer() } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index a810efa5f7..3aca25265a 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -192,7 +192,8 @@ "_busy_" = "Busy"; /* User status */ -"_select_user_status_" = "Select online 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"; diff --git a/iOSClient/UserStatus/NCUserStatusModel.swift b/iOSClient/UserStatus/NCUserStatusModel.swift index c7ff1760c4..31b27b7a0b 100644 --- a/iOSClient/UserStatus/NCUserStatusModel.swift +++ b/iOSClient/UserStatus/NCUserStatusModel.swift @@ -6,20 +6,19 @@ import NextcloudKit @Observable class NCUserStatusModel { struct UserStatus: Hashable { - let names: [String] + let name: String let titleKey: String var descriptionKey: String = "" } @ObservationIgnored var userStatuses: [UserStatus] = [ - .init(names: ["online"], titleKey: "_online_"), - .init(names: ["away"], titleKey: "_away_"), - .init(names: ["dnd"], titleKey: "_dnd_", descriptionKey: "_dnd_description_"), - .init(names: ["invisible", "offline"], titleKey: "_invisible_", descriptionKey: "_invisible_description_") + .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 userStatusSupportsBusy = false var canDismiss = false @ObservationIgnored let account: String @@ -28,8 +27,7 @@ import NextcloudKit self.account = account if let capabilities = NCNetworking.shared.capabilities[account], capabilities.userStatusSupportsBusy { -// userStatusSupportsBusy = true - userStatuses.insert(.init(names: ["busy"], titleKey: "_busy_"), at: 2) + userStatuses.insert(.init(name: "busy", titleKey: "_busy_"), at: 2) } } @@ -37,10 +35,6 @@ import NextcloudKit return NCUtility().getUserStatus(userIcon: nil, userStatus: name, userMessage: nil) } - func getStatuses() { - // NCUtility().getUserStatus(userIcon: <#T##String?#>, userStatus: <#T##String?#>, userMessage: <#T##String?#>) - } - func getStatus(account: String) { Task { let status = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in @@ -53,17 +47,16 @@ import NextcloudKit selectedStatus = status.status } - - } func setStatus(account: String) { Task { - let result = await NextcloudKit.shared.setUserStatusAsync(status: selectedStatus ?? "", account: account) { task in + 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 } } } diff --git a/iOSClient/UserStatus/NCUserStatusView.swift b/iOSClient/UserStatus/NCUserStatusView.swift index 4fbf4997c6..f4ddb42cf3 100644 --- a/iOSClient/UserStatus/NCUserStatusView.swift +++ b/iOSClient/UserStatus/NCUserStatusView.swift @@ -20,7 +20,7 @@ struct NCUserStatusView: View { List { ForEach(model.userStatuses, id: \.self) { item in HStack { - let status = model.getStatusDetails(name: item.names.first!) + let status = model.getStatusDetails(name: item.name) Image(uiImage: status.statusImage ?? UIImage()) .renderingMode(.template) @@ -36,15 +36,14 @@ struct NCUserStatusView: View { } } Spacer() - if model.selectedStatus == item.names.first { + if model.selectedStatus == item.name { Image(systemName: "checkmark") .foregroundColor(.blue) } } .contentShape(Rectangle()) // make the whole row tappable .onTapGesture { - let firstStatus = item.names.first! - model.selectedStatus = (model.selectedStatus == firstStatus) ? nil : firstStatus + model.selectedStatus = (model.selectedStatus == item.name) ? nil : item.name model.setStatus(account: account) } .onChange(of: model.canDismiss) { _, newValue in From 9b8b98979089f6beae79bb1b4d8e0c9980faf3fe Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 20 Nov 2025 18:19:42 +0100 Subject: [PATCH 04/13] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 + .../StatusMessage/NCStatusMessageModel.swift | 173 +++++++++++++++--- .../StatusMessage/NCStatusMessageView.swift | 53 +++--- .../en.lproj/Localizable.strings | 2 +- iOSClient/UserStatus/NCUserStatus.swift | 6 +- 5 files changed, 178 insertions(+), 58 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index d183d2d67e..7ce15dda37 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1268,6 +1268,7 @@ 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 = ""; }; + F3CA2A342ECF7188008E150E /* NextcloudKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NextcloudKit; path = ../NextcloudKit; sourceTree = SOURCE_ROOT; }; F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumModel.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 = ""; }; @@ -3200,6 +3201,7 @@ F7F67B9F1A24D27800EE80DA = { isa = PBXGroup; children = ( + F3CA2A342ECF7188008E150E /* NextcloudKit */, AA8E041E2D3114E200E7E89C /* README.md */, F7B8B82F25681C3400967775 /* GoogleService-Info.plist */, F7C1CDD91E6DFC6F005D92BE /* Brand */, diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift index 2af59ab678..7c9ede90e6 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -3,43 +3,46 @@ // SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI +import NextcloudKit @Observable class NCStatusMessageModel { - struct StatusPreset: Identifiable, Equatable { - let id = UUID() - let emoji: String - let title: String - let clearAfter: ClearAfter - } + // struct StatusPreset: Identifiable, Equatable { + // let id = UUID() + // let emoji: String + // let title: String + // let clearAfter: ClearAfter + // } enum ClearAfter: String, CaseIterable, Identifiable { - case dontClear = "Don't clear" - case thirtyMinutes = "30 minutes" - case oneHour = "1 hour" - case fourHours = "4 hours" - case today = "Today" - case thisWeek = "This week" + case dontClear = "_dont_clear_" + case thirtyMinutes = "_30_minutes_" + case oneHour = "_an_hour_" + case fourHours = "_4_hours_" + case today = "_day_" + case thisWeek = "_this_week_" var id: String { rawValue } } - @ObservationIgnored let statusPresets: [StatusPreset] = [ - .init(emoji: "📅", title: "In a meeting", clearAfter: .oneHour), - .init(emoji: "🚌", title: "Commuting", clearAfter: .thirtyMinutes), - .init(emoji: "⏳", title: "Be right back", clearAfter: .thirtyMinutes), - .init(emoji: "🏡", title: "Working remotely", clearAfter: .thisWeek), - .init(emoji: "🤒", title: "Out sick", clearAfter: .today), - .init(emoji: "🌴", title: "Vacationing", clearAfter: .dontClear) - ] + // @ObservationIgnored let statusPresets: [StatusPreset] = [ + // .init(emoji: "📅", title: "In a meeting", clearAfter: .oneHour), + // .init(emoji: "🚌", title: "Commuting", clearAfter: .thirtyMinutes), + // .init(emoji: "⏳", title: "Be right back", clearAfter: .thirtyMinutes), + // .init(emoji: "🏡", title: "Working remotely", clearAfter: .thisWeek), + // .init(emoji: "🤒", title: "Out sick", clearAfter: .today), + // .init(emoji: "🌴", title: "Vacationing", clearAfter: .dontClear) + // ] + + var statusPresets: [NKUserStatus] = [] var emojiText: String = "😀" var statusText: String = "" var clearAfter: ClearAfter = .dontClear - func chooseStatusPreset(preset: StatusPreset) { - emojiText = preset.emoji - statusText = preset.title - clearAfter = preset.clearAfter + func chooseStatusPreset(preset: NKUserStatus, clearAtText: String) { + emojiText = preset.icon ?? "" + statusText = preset.message ?? "" + clearAfter = stringToClearAfter(clearAtText) } func clearStatus() { @@ -48,7 +51,127 @@ import SwiftUI clearAfter = .dontClear } + func getPredefinedStatusTexts(account: String) { + Task { + let statuses = 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) + } + } + statusPresets = statuses.userStatuses ?? [] + } + } + func submitStatus() { - + // NextcloudKit.shared.setCustomMessagePredefinedAsync(messageId: emojiText, clearAt: <#T##Double#>, account: <#T##String#>) + // 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) + } + + 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" { + if clearAtTime == "day" { + return NSLocalizedString("_day_", comment: "") + } + } + + return NSLocalizedString("_dont_clear_", comment: "") + } + + func stringToClearAfter(_ clearAtString: String) -> ClearAfter { + switch clearAtString { + case NSLocalizedString("_30_minutes_", comment: ""): + return .thirtyMinutes + case NSLocalizedString("_an_hour_", comment: ""): + return .oneHour + case NSLocalizedString("_4_hours_", comment: ""): + return .fourHours + case NSLocalizedString("_day_", comment: ""): + return .today + case NSLocalizedString("_this_week_", comment: ""): + return .thisWeek + default: + return .dontClear + } + } + + 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 + } } } + + diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index deebd88321..16067111d2 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -4,6 +4,7 @@ import SwiftUI import UIKit +import NextcloudKit struct Status: Identifiable, Equatable { let id = UUID() @@ -25,7 +26,6 @@ struct NCStatusMessageView: View { var body: some View { ScrollView { VStack(spacing: 24) { - // Input field with emoji HStack(spacing: 12) { EmojiField(text: $model.emojiText) @@ -35,7 +35,6 @@ struct NCStatusMessageView: View { } .frame(height: 20) - // Presets list VStack(spacing: 18) { ForEach(model.statusPresets) { preset in StatusPresetRow(model: $model, preset: preset) @@ -43,49 +42,40 @@ struct NCStatusMessageView: View { } .padding(.top, 8) - // Clear after section VStack(alignment: .leading, spacing: 8) { Text("Clear status after") .font(.headline) - HStack(spacing: 12) { - Text("🌴") - .font(.title3) - Picker("Clear after", selection: $model.clearAfter) { - ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in - Text(option.rawValue).tag(option) - } + Picker("Clear after", selection: $model.clearAfter) { + ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in + Text(NSLocalizedString(option.rawValue, comment: "")).tag(option) } - .pickerStyle(.menu) - .frame(maxWidth: .infinity, alignment: .leading) } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, alignment: .leading) // Bottom actions - HStack { + HStack(spacing: 80) { Button("Clear") { model.clearStatus() } - .frame(maxWidth: .infinity) .buttonStyle(.bordered) + .controlSize(.large) // .foregroundStyle(.blue) - Spacer() - - Button(action: {}) { - Text("Set message") - .fontWeight(.semibold) -// .padding(.vertical, 14) - .frame(maxWidth: .infinity) - + Button("Set message") { + // Set message action } + .fontWeight(.semibold) .buttonStyle(.borderedProminent) + .controlSize(.large) // .tint(Color(.systemGray3)) // .foregroundStyle(Color(.systemBackground)) // .clipShape(Capsule()) // .frame(width: 220) } - .frame(minWidth: 0, maxWidth: .infinity) +// .frame(minWidth: 0, maxWidth: .infinity) .padding(.top, 8) } .padding(24) @@ -98,27 +88,32 @@ struct NCStatusMessageView: View { // .background(Color(.systemBackground)) .navigationTitle(NSLocalizedString("_select_status_message_", comment: "")) .navigationBarTitleDisplayMode(.inline) + .onAppear { + model.getPredefinedStatusTexts(account: account) + } } } private struct StatusPresetRow: View { @Binding var model: NCStatusMessageModel - let preset: NCStatusMessageModel.StatusPreset + let preset: NKUserStatus var body: some View { + let cleatAtText = model.getPredefinedClearStatusText(clearAt: preset.clearAt, clearAtTime: preset.clearAtTime, clearAtType: preset.clearAtType) + Button(action: { - model.chooseStatusPreset(preset: preset) + model.chooseStatusPreset(preset: preset, clearAtText: cleatAtText) }) { HStack(spacing: 16) { - Text(preset.emoji) - .font(.title2) + Text(preset.icon ?? "") + .font(.title3) .frame(width: 32) - Text(preset.title) + Text(preset.message ?? "") .font(.title3.weight(.semibold)) .foregroundStyle(.primary) Text("—") .foregroundStyle(.secondary) - Text(preset.clearAfter.rawValue) + Text(cleatAtText) .foregroundStyle(.secondary) Spacer() } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 3aca25265a..9cc2a145e4 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -208,7 +208,7 @@ "_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/UserStatus/NCUserStatus.swift b/iOSClient/UserStatus/NCUserStatus.swift index d7ade1e36e..312ffc4391 100644 --- a/iOSClient/UserStatus/NCUserStatus.swift +++ b/iOSClient/UserStatus/NCUserStatus.swift @@ -375,7 +375,7 @@ class NCUserStatus: UIViewController { 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("_day_", comment: "")) dropDown.dataSource.append(NSLocalizedString("_this_week_", comment: "")) dropDown.anchorView = clearStatusMessageAfterText @@ -521,7 +521,7 @@ class NCUserStatus: UIViewController { case NSLocalizedString("_4_hours_", comment: ""): let date = now.addingTimeInterval(14400) return date.timeIntervalSince1970 - case NSLocalizedString("day", comment: ""): + case NSLocalizedString("_day_", comment: ""): return tomorrow.timeIntervalSince1970 case NSLocalizedString("_this_week_", comment: ""): return endweek.timeIntervalSince1970 @@ -540,7 +540,7 @@ class NCUserStatus: UIViewController { let minute = Calendar.current.dateComponents([.minute], from: from, to: to).minute ?? 0 if day > 0 { - if day == 1 { return NSLocalizedString("day", comment: "") } + if day == 1 { return NSLocalizedString("_day_", comment: "") } return "\(day) " + NSLocalizedString("_days_", comment: "") } From 556936369d5080846325ffc729d23d30b9cb4b34 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 11:34:55 +0100 Subject: [PATCH 05/13] WIP Signed-off-by: Milen Pivchev --- .../NCAccountSettingsModel.swift | 4 +- iOSClient/Data/NCManageDatabase.swift | 2 +- iOSClient/Extensions/View+Extension.swift | 4 - iOSClient/NCGlobal.swift | 7 ++ .../StatusMessage/NCStatusMessageModel.swift | 101 +++++++++++++----- .../StatusMessage/NCStatusMessageView.swift | 47 ++++---- .../en.lproj/Localizable.strings | 4 +- iOSClient/Transfers/NCTransfersModel.swift | 2 +- 8 files changed, 110 insertions(+), 61 deletions(-) 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/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 0123d1ba43..3f3666e534 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -404,3 +404,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/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift index 7c9ede90e6..41248e14a9 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -16,6 +16,7 @@ import NextcloudKit 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_" @@ -33,7 +34,7 @@ import NextcloudKit // .init(emoji: "🌴", title: "Vacationing", clearAfter: .dontClear) // ] - var statusPresets: [NKUserStatus] = [] + var predefinedStatuses: [NKUserStatus] = [] var emojiText: String = "😀" var statusText: String = "" @@ -59,24 +60,43 @@ import NextcloudKit await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } } - statusPresets = statuses.userStatuses ?? [] + + predefinedStatuses = isXcodeRunningForPreviews ? createStatusesForPreview() : statuses.userStatuses ?? [] } } - func submitStatus() { - // NextcloudKit.shared.setCustomMessagePredefinedAsync(messageId: emojiText, clearAt: <#T##Double#>, account: <#T##String#>) - // 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) + func submitStatus(account: String) { + Task { + await NextcloudKit.shared.setCustomMessageUserDefinedAsync(statusIcon: emojiText, message: statusText, clearAt: getClearAt(clearAfter.rawValue), 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) + } + } + } + } + + func setAccountUserStatus(account: String) { + NextcloudKit.shared.getUserStatus(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) + } + } 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 getPredefinedClearStatusText(clearAt: Date?, clearAtTime: String?, clearAtType: String?) -> String { @@ -112,6 +132,8 @@ import NextcloudKit 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: "") } @@ -126,8 +148,10 @@ import NextcloudKit return NSLocalizedString("_dont_clear_", comment: "") } - func stringToClearAfter(_ clearAtString: String) -> ClearAfter { + private func stringToClearAfter(_ clearAtString: String) -> ClearAfter { switch clearAtString { + case NSLocalizedString("_15_minutes_", comment: ""): + return .fifteenMinutes case NSLocalizedString("_30_minutes_", comment: ""): return .thirtyMinutes case NSLocalizedString("_an_hour_", comment: ""): @@ -143,7 +167,7 @@ import NextcloudKit } } - func getClearAt(_ clearAtString: String) -> Double { + private func getClearAt(_ clearAtString: String) -> Double { let now = Date() let calendar = Calendar.current let gregorian = Calendar(identifier: .gregorian) @@ -153,25 +177,54 @@ import NextcloudKit guard let endweek = gregorian.date(byAdding: .day, value: 6, to: startweek) else { return 0 } switch clearAtString { - case NSLocalizedString("_dont_clear_", comment: ""): + case "_dont_clear_": return 0 - case NSLocalizedString("_30_minutes_", comment: ""): + case "_15_minutes_": + let date = now.addingTimeInterval(900) + return date.timeIntervalSince1970 + case "_30_minutes_": let date = now.addingTimeInterval(1800) return date.timeIntervalSince1970 - case NSLocalizedString("_1_hour_", comment: ""), NSLocalizedString("_an_hour_", comment: ""): + case "_1_hour_", "_an_hour_": let date = now.addingTimeInterval(3600) return date.timeIntervalSince1970 - case NSLocalizedString("_4_hours_", comment: ""): + case "_4_hours_": let date = now.addingTimeInterval(14400) return date.timeIntervalSince1970 - case NSLocalizedString("_day_", comment: ""): + case "_day_": return tomorrow.timeIntervalSince1970 - case NSLocalizedString("_this_week_", comment: ""): + case "_this_week_": 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 index 16067111d2..76113f2669 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -16,11 +16,8 @@ struct Status: Identifiable, Equatable { struct NCStatusMessageView: View { let account: String - // MARK: - State @State private var model = NCStatusMessageModel() -// @State private var statusText: String = "" -// @State private var selectedStatus: Status? -// @State private var emojiText: String = "😀" + @Environment(\.dismiss) private var dismiss @FocusState private var isTextFieldFocused: Bool var body: some View { @@ -36,47 +33,42 @@ struct NCStatusMessageView: View { .frame(height: 20) VStack(spacing: 18) { - ForEach(model.statusPresets) { preset in + ForEach(model.predefinedStatuses) { preset in StatusPresetRow(model: $model, preset: preset) } } .padding(.top, 8) - - VStack(alignment: .leading, spacing: 8) { - Text("Clear status after") - .font(.headline) - Picker("Clear after", selection: $model.clearAfter) { + + HStack(spacing: 0) { + Text("_clear_status_message_after_") + Picker("", selection: $model.clearAfter) { ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in Text(NSLocalizedString(option.rawValue, comment: "")).tag(option) } } .pickerStyle(.menu) - .frame(maxWidth: .infinity, alignment: .leading) + Spacer() } - .frame(maxWidth: .infinity, alignment: .leading) - // Bottom actions - HStack(spacing: 80) { - Button("Clear") { + HStack { + Button("_clear_") { model.clearStatus() } .buttonStyle(.bordered) .controlSize(.large) -// .foregroundStyle(.blue) - Button("Set message") { - // Set message action + Spacer() + + Button("_set_status_message_") { + model.submitStatus(account: account) + dismiss() } .fontWeight(.semibold) .buttonStyle(.borderedProminent) .controlSize(.large) -// .tint(Color(.systemGray3)) -// .foregroundStyle(Color(.systemBackground)) -// .clipShape(Capsule()) -// .frame(width: 220) + .disabled(model.emojiText.isEmpty && model.statusText.isEmpty) } -// .frame(minWidth: 0, maxWidth: .infinity) - .padding(.top, 8) + .padding(8) } .padding(24) } @@ -84,13 +76,14 @@ struct NCStatusMessageView: View { .onTapGesture { isTextFieldFocused = false } -// .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) -// .background(Color(.systemBackground)) .navigationTitle(NSLocalizedString("_select_status_message_", comment: "")) .navigationBarTitleDisplayMode(.inline) .onAppear { model.getPredefinedStatusTexts(account: account) } + .onDisappear { + model.setAccountUserStatus(account: account) + } } } @@ -109,7 +102,7 @@ private struct StatusPresetRow: View { .font(.title3) .frame(width: 32) Text(preset.message ?? "") - .font(.title3.weight(.semibold)) + .font(.headline) .foregroundStyle(.primary) Text("—") .foregroundStyle(.secondary) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 9cc2a145e4..e903661740 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -197,13 +197,13 @@ "_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"; diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 943c920add..ee20d47222 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -57,7 +57,7 @@ final class TransfersViewModel: ObservableObject { @MainActor func pollTransfers() async { while !Task.isCancelled { - if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" { + if isXcodeRunningForPreviews { isLoading = true // Items From 67500a266f848c29e0e306bab929b7fce9b78f72 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 15:43:01 +0100 Subject: [PATCH 06/13] WIP Signed-off-by: Milen Pivchev --- iOSClient/StatusMessage/EmojiTextField.swift | 34 +++++--- .../StatusMessage/NCStatusMessageModel.swift | 83 ++++++++++--------- .../StatusMessage/NCStatusMessageView.swift | 6 +- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/iOSClient/StatusMessage/EmojiTextField.swift b/iOSClient/StatusMessage/EmojiTextField.swift index edac9cecab..af48f7f855 100644 --- a/iOSClient/StatusMessage/EmojiTextField.swift +++ b/iOSClient/StatusMessage/EmojiTextField.swift @@ -28,17 +28,20 @@ final class EmojiTextField: UITextField { } private func commonInit() { - borderStyle = .none - textAlignment = .center - font = .systemFont(ofSize: 28) - setContentHuggingPriority(.required, for: .horizontal) - setContentCompressionResistancePriority(.required, for: .horizontal) +// borderStyle = .none +// textAlignment = .center +// font = .systemFont(ofSize: 28) +// setContentHuggingPriority(.required, for: .horizontal) +// setContentCompressionResistancePriority(.required, for: .horizontal) NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil) addTarget(self, action: #selector(textChanged), for: .editingChanged) } - @objc private func inputModeDidChange(_ notification: Notification) { - guard isFirstResponder else { return } + @objc func inputModeDidChange(_ notification: Notification) { + guard isFirstResponder else { + return + } + DispatchQueue.main.async { [weak self] in self?.reloadInputViews() } @@ -78,12 +81,21 @@ struct EmojiField: UIViewRepresentable { var parent: EmojiField init(_ parent: EmojiField) { self.parent = parent } - func textFieldDidChangeSelection(_ textField: UITextField) { - parent.text = textField.text ?? "" - } - 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 index 41248e14a9..bd4d53b19c 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -6,13 +6,6 @@ import SwiftUI import NextcloudKit @Observable class NCStatusMessageModel { - // struct StatusPreset: Identifiable, Equatable { - // let id = UUID() - // let emoji: String - // let title: String - // let clearAfter: ClearAfter - // } - enum ClearAfter: String, CaseIterable, Identifiable { case dontClear = "_dont_clear_" case thirtyMinutes = "_30_minutes_" @@ -25,15 +18,6 @@ import NextcloudKit var id: String { rawValue } } - // @ObservationIgnored let statusPresets: [StatusPreset] = [ - // .init(emoji: "📅", title: "In a meeting", clearAfter: .oneHour), - // .init(emoji: "🚌", title: "Commuting", clearAfter: .thirtyMinutes), - // .init(emoji: "⏳", title: "Be right back", clearAfter: .thirtyMinutes), - // .init(emoji: "🏡", title: "Working remotely", clearAfter: .thisWeek), - // .init(emoji: "🤒", title: "Out sick", clearAfter: .today), - // .init(emoji: "🌴", title: "Vacationing", clearAfter: .dontClear) - // ] - var predefinedStatuses: [NKUserStatus] = [] var emojiText: String = "😀" @@ -46,10 +30,35 @@ import NextcloudKit clearAfter = stringToClearAfter(clearAtText) } - func clearStatus() { - emojiText = "😀" - statusText = "" - clearAfter = .dontClear + 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 ?? "" + clearAfter = stringToClearAfter(getPredefinedClearStatusString(clearAt: result.clearAt, clearAtTime: "", clearAtType: "")) + } + } + } + + func clearStatus(account: String) { + Task { + 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) + } + } + + NCContentPresenter().showCustomMessage(message: "adasd", type: .error) + } } func getPredefinedStatusTexts(account: String) { @@ -77,29 +86,29 @@ import NextcloudKit } func setAccountUserStatus(account: String) { - NextcloudKit.shared.getUserStatus(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) - } - } completion: { account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, _, _, error in - if error == .success { + Task { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in Task { - await NCManageDatabase.shared.setAccountUserStatusAsync(userStatusClearAt: clearAt, - userStatusIcon: icon, - userStatusMessage: message, - userStatusMessageId: messageId, - userStatusMessageIsPredefined: messageIsPredefined, - userStatusStatus: status, - userStatusStatusIsUserDefined: statusIsUserDefined, - account: account) + 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) + } } } - func getPredefinedClearStatusText(clearAt: Date?, clearAtTime: String?, clearAtType: String?) -> String { + func getPredefinedClearStatusString(clearAt: Date?, clearAtTime: String?, clearAtType: String?) -> String { // Date if let clearAt { let from = Date() diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 76113f2669..1f76d1673e 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -52,7 +52,8 @@ struct NCStatusMessageView: View { HStack { Button("_clear_") { - model.clearStatus() + model.clearStatus(account: account) + dismiss() } .buttonStyle(.bordered) .controlSize(.large) @@ -79,6 +80,7 @@ struct NCStatusMessageView: View { .navigationTitle(NSLocalizedString("_select_status_message_", comment: "")) .navigationBarTitleDisplayMode(.inline) .onAppear { + model.getStatus(account: account) model.getPredefinedStatusTexts(account: account) } .onDisappear { @@ -92,7 +94,7 @@ private struct StatusPresetRow: View { let preset: NKUserStatus var body: some View { - let cleatAtText = model.getPredefinedClearStatusText(clearAt: preset.clearAt, clearAtTime: preset.clearAtTime, clearAtType: preset.clearAtType) + let cleatAtText = model.getPredefinedClearStatusString(clearAt: preset.clearAt, clearAtTime: preset.clearAtTime, clearAtType: preset.clearAtType) Button(action: { model.chooseStatusPreset(preset: preset, clearAtText: cleatAtText) From 41a4c1e746c15229ffcc9a3ae1f5468f3703a90b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 16:27:11 +0100 Subject: [PATCH 07/13] WIP Signed-off-by: Milen Pivchev --- .../StatusMessage/NCStatusMessageModel.swift | 61 ++++++++----------- .../StatusMessage/NCStatusMessageView.swift | 16 +++-- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift index bd4d53b19c..f5f5d47853 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -22,12 +22,12 @@ import NextcloudKit var emojiText: String = "😀" var statusText: String = "" - var clearAfter: ClearAfter = .dontClear + var clearAfterString = "_dont_clear_" func chooseStatusPreset(preset: NKUserStatus, clearAtText: String) { emojiText = preset.icon ?? "" statusText = preset.message ?? "" - clearAfter = stringToClearAfter(clearAtText) + clearAfterString = clearAtText } func getStatus(account: String) { @@ -43,45 +43,55 @@ import NextcloudKit if result.error == .success { emojiText = result.icon ?? "" statusText = result.message ?? "" - clearAfter = stringToClearAfter(getPredefinedClearStatusString(clearAt: result.clearAt, clearAtTime: "", clearAtType: "")) + clearAfterString = getPredefinedClearStatusString(clearAt: result.clearAt, clearAtTime: "", clearAtType: "") } } } func clearStatus(account: String) { Task { - await NextcloudKit.shared.clearMessageAsync(account: account) { task in + 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) } } - NCContentPresenter().showCustomMessage(message: "adasd", type: .error) + if result.error != .success { + NCContentPresenter().showError(error: result.error) + } } } func getPredefinedStatusTexts(account: String) { Task { - let statuses = await NextcloudKit.shared.getUserStatusPredefinedStatusesAsync(account: account) { task in + 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) } } - predefinedStatuses = isXcodeRunningForPreviews ? createStatusesForPreview() : statuses.userStatuses ?? [] + if result.error == .success { + predefinedStatuses = isXcodeRunningForPreviews ? createStatusesForPreview() : result.userStatuses ?? [] + } else { + NCContentPresenter().showError(error: result.error) + } } } func submitStatus(account: String) { Task { - await NextcloudKit.shared.setCustomMessageUserDefinedAsync(statusIcon: emojiText, message: statusText, clearAt: getClearAt(clearAfter.rawValue), account: account) { task in + 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) + } } } @@ -104,6 +114,8 @@ import NextcloudKit userStatusStatus: result.status, userStatusStatusIsUserDefined: result.statusIsUserDefined, account: result.account) + } else { + NCContentPresenter().showError(error: result.error) } } } @@ -157,25 +169,6 @@ import NextcloudKit return NSLocalizedString("_dont_clear_", comment: "") } - private func stringToClearAfter(_ clearAtString: String) -> ClearAfter { - switch clearAtString { - case NSLocalizedString("_15_minutes_", comment: ""): - return .fifteenMinutes - case NSLocalizedString("_30_minutes_", comment: ""): - return .thirtyMinutes - case NSLocalizedString("_an_hour_", comment: ""): - return .oneHour - case NSLocalizedString("_4_hours_", comment: ""): - return .fourHours - case NSLocalizedString("_day_", comment: ""): - return .today - case NSLocalizedString("_this_week_", comment: ""): - return .thisWeek - default: - return .dontClear - } - } - private func getClearAt(_ clearAtString: String) -> Double { let now = Date() let calendar = Calendar.current @@ -186,23 +179,23 @@ import NextcloudKit guard let endweek = gregorian.date(byAdding: .day, value: 6, to: startweek) else { return 0 } switch clearAtString { - case "_dont_clear_": + case NSLocalizedString("_dont_clear_", comment: ""): return 0 - case "_15_minutes_": + case NSLocalizedString("_15_minutes_", comment: ""): let date = now.addingTimeInterval(900) return date.timeIntervalSince1970 - case "_30_minutes_": + case NSLocalizedString("_30_minutes_", comment: ""): let date = now.addingTimeInterval(1800) return date.timeIntervalSince1970 - case "_1_hour_", "_an_hour_": + case NSLocalizedString("_1_hour_", comment: ""), NSLocalizedString("_an_hour_", comment: ""): let date = now.addingTimeInterval(3600) return date.timeIntervalSince1970 - case "_4_hours_": + case NSLocalizedString("_4_hours_", comment: ""): let date = now.addingTimeInterval(14400) return date.timeIntervalSince1970 - case "_day_": + case NSLocalizedString("_day_", comment: ""): return tomorrow.timeIntervalSince1970 - case "_this_week_": + case NSLocalizedString("_this_week_", comment: ""): return endweek.timeIntervalSince1970 default: return 0 diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 1f76d1673e..50147f77b2 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -39,14 +39,22 @@ struct NCStatusMessageView: View { } .padding(.top, 8) - HStack(spacing: 0) { + HStack { Text("_clear_status_message_after_") - Picker("", selection: $model.clearAfter) { + Menu { ForEach(NCStatusMessageModel.ClearAfter.allCases) { option in - Text(NSLocalizedString(option.rawValue, comment: "")).tag(option) + 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) } - .pickerStyle(.menu) Spacer() } From f4b3bc709d6614aa447676e1e240731f93c207f1 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 16:32:20 +0100 Subject: [PATCH 08/13] WIP Signed-off-by: Milen Pivchev --- .../NCAccountSettingsView.swift | 43 ------------------- .../StatusMessage/NCStatusMessageView.swift | 2 +- .../en.lproj/Localizable.strings | 4 +- 3 files changed, 3 insertions(+), 46 deletions(-) diff --git a/iOSClient/Account/Account Settings/NCAccountSettingsView.swift b/iOSClient/Account/Account Settings/NCAccountSettingsView.swift index 9cc4e8a381..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 @@ -178,50 +177,8 @@ struct NCAccountSettingsView: View { .font(.subheadline) } } - - 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)) - } - .font(.subheadline) - }) - .sheet(isPresented: $showUserStatus) { - if let account = model.tblAccount?.account { - UserStatusView(showUserStatus: $showUserStatus, account: account) - } - } - .onChange(of: showUserStatus) { } } - 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) - } - // // Certificate server if model.isAdminGroup() { diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 50147f77b2..2516124bae 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -51,7 +51,7 @@ struct NCStatusMessageView: View { } } label: { Text(model.clearAfterString) - .foregroundStyle(.blue) + .foregroundStyle(.blue) Image(systemName: "chevron.up.chevron.down") .imageScale(.small) } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index e903661740..9e88cafb1a 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -142,8 +142,8 @@ "_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"; From 93e745e912320d25a7db7546e3d0c9fc79a6df71 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 16:36:36 +0100 Subject: [PATCH 09/13] WIP Signed-off-by: Milen Pivchev --- iOSClient/StatusMessage/NCStatusMessageModel.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/iOSClient/StatusMessage/NCStatusMessageModel.swift b/iOSClient/StatusMessage/NCStatusMessageModel.swift index f5f5d47853..63d52662b9 100644 --- a/iOSClient/StatusMessage/NCStatusMessageModel.swift +++ b/iOSClient/StatusMessage/NCStatusMessageModel.swift @@ -20,7 +20,7 @@ import NextcloudKit var predefinedStatuses: [NKUserStatus] = [] - var emojiText: String = "😀" + var emojiText: String = "" var statusText: String = "" var clearAfterString = "_dont_clear_" @@ -34,14 +34,13 @@ import NextcloudKit Task { let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - name: "getUserStatus") + 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 ?? "" + emojiText = result.icon ?? "😀" statusText = result.message ?? "" clearAfterString = getPredefinedClearStatusString(clearAt: result.clearAt, clearAtTime: "", clearAtType: "") } From a08ab7123b106ef9b274e19de2b6e0af8d34283a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 16:41:12 +0100 Subject: [PATCH 10/13] Remove old code Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 8 - iOSClient/UserStatus/NCUserStatus.storyboard | 521 ------------- iOSClient/UserStatus/NCUserStatus.swift | 722 ------------------- 3 files changed, 1251 deletions(-) delete mode 100644 iOSClient/UserStatus/NCUserStatus.storyboard delete mode 100644 iOSClient/UserStatus/NCUserStatus.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 7ce15dda37..58d9869be6 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -836,7 +836,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 +875,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 */; }; F7F0691D2EAB9DA300B3B13B /* NotificationPresenterUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0691C2EAB9DA200B3B13B /* NotificationPresenterUI.swift */; }; F7F1FB9D2E27CE7200C79E20 /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F7F1FB9E2E27CE7200C79E20 /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; @@ -1753,7 +1751,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 = ""; }; @@ -1773,7 +1770,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 = ""; }; F7F0691C2EAB9DA200B3B13B /* NotificationPresenterUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPresenterUI.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 = ""; }; @@ -3162,8 +3158,6 @@ children = ( F3A0B1EF2EC76FE800F10B82 /* NCUserStatusModel.swift */, F3A0B1F02EC76FE800F10B82 /* NCUserStatusView.swift */, - F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */, - F7E0CDCE265CE8610044854E /* NCUserStatus.storyboard */, ); path = UserStatus; sourceTree = ""; @@ -3957,7 +3951,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 */, @@ -4629,7 +4622,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 */, 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 312ffc4391..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) - } -} From ade9bfb16c298a69769a70f47e47814696b670a9 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 26 Nov 2025 16:43:22 +0100 Subject: [PATCH 11/13] Remove comments Signed-off-by: Milen Pivchev --- iOSClient/StatusMessage/EmojiTextField.swift | 6 ------ iOSClient/UserStatus/NCUserStatusView.swift | 2 -- 2 files changed, 8 deletions(-) diff --git a/iOSClient/StatusMessage/EmojiTextField.swift b/iOSClient/StatusMessage/EmojiTextField.swift index af48f7f855..c7fb7525d7 100644 --- a/iOSClient/StatusMessage/EmojiTextField.swift +++ b/iOSClient/StatusMessage/EmojiTextField.swift @@ -28,11 +28,6 @@ final class EmojiTextField: UITextField { } private func commonInit() { -// borderStyle = .none -// textAlignment = .center -// font = .systemFont(ofSize: 28) -// setContentHuggingPriority(.required, for: .horizontal) -// setContentCompressionResistancePriority(.required, for: .horizontal) NotificationCenter.default.addObserver(self, selector: #selector(inputModeDidChange), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil) addTarget(self, action: #selector(textChanged), for: .editingChanged) } @@ -56,7 +51,6 @@ final class EmojiTextField: UITextField { } } -// SwiftUI wrapper struct EmojiField: UIViewRepresentable { @Binding var text: String diff --git a/iOSClient/UserStatus/NCUserStatusView.swift b/iOSClient/UserStatus/NCUserStatusView.swift index f4ddb42cf3..8b234ec472 100644 --- a/iOSClient/UserStatus/NCUserStatusView.swift +++ b/iOSClient/UserStatus/NCUserStatusView.swift @@ -7,7 +7,6 @@ import SwiftUI struct NCUserStatusView: View { let account: String -// @State private var selectedItem: String? @State private var model: NCUserStatusModel @Environment(\.dismiss) private var dismiss @@ -27,7 +26,6 @@ struct NCUserStatusView: View { .resizable() .foregroundStyle(Color(status.statusImageColor)) .frame(width: 20, height: 20) -// .padding(.trailing, 8) VStack(alignment: .leading) { Text(NSLocalizedString(item.titleKey, comment: "")) From c576fd9f6f1f4c56dfbadbe4cb19d188bad05749 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 27 Nov 2025 14:17:37 +0100 Subject: [PATCH 12/13] PR fixes Signed-off-by: Milen Pivchev --- .../StatusMessage/NCStatusMessageView.swift | 9 +--- iOSClient/UserStatus/NCUserStatusModel.swift | 53 +++++++++++-------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 2516124bae..de183c300a 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -6,13 +6,6 @@ import SwiftUI import UIKit import NextcloudKit -struct Status: Identifiable, Equatable { - let id = UUID() - let emoji: String - let title: String - let detail: String -} - struct NCStatusMessageView: View { let account: String @@ -26,7 +19,7 @@ struct NCStatusMessageView: View { HStack(spacing: 12) { EmojiField(text: $model.emojiText) - TextField("What is your status?", text: $model.statusText) + TextField("_status_message_placehorder_", text: $model.statusText) .focused($isTextFieldFocused) .textFieldStyle(.roundedBorder) } diff --git a/iOSClient/UserStatus/NCUserStatusModel.swift b/iOSClient/UserStatus/NCUserStatusModel.swift index 31b27b7a0b..8df08fd87e 100644 --- a/iOSClient/UserStatus/NCUserStatusModel.swift +++ b/iOSClient/UserStatus/NCUserStatusModel.swift @@ -37,51 +37,58 @@ import NextcloudKit func getStatus(account: String) { Task { - let status = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, - name: "getUserStatus") + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, name: "getUserStatus") await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } } - selectedStatus = status.status + if result.error == .success { + selectedStatus = result.status + } else { + NCContentPresenter().showError(error: result.error) + } } } func setStatus(account: String) { Task { - await NextcloudKit.shared.setUserStatusAsync(status: selectedStatus ?? "", account: account) { task in + 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") + 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) { - 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 { + let result = await NextcloudKit.shared.getUserStatusAsync(account: account) { task in Task { - await NCManageDatabase.shared.setAccountUserStatusAsync(userStatusClearAt: clearAt, - userStatusIcon: icon, - userStatusMessage: message, - userStatusMessageId: messageId, - userStatusMessageIsPredefined: messageIsPredefined, - userStatusStatus: status, - userStatusStatusIsUserDefined: statusIsUserDefined, - account: account) + 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) } } } From 12605002df053c42188aabec6ecdab554603b3bb Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 27 Nov 2025 14:27:12 +0100 Subject: [PATCH 13/13] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e2f44b0a9c..0379aee988 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1270,7 +1270,6 @@ 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 = ""; }; - F3CA2A342ECF7188008E150E /* NextcloudKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NextcloudKit; path = ../NextcloudKit; sourceTree = SOURCE_ROOT; }; F3CA337C2D0B2B6A00672333 /* AlbumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumModel.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 = ""; }; @@ -3209,7 +3208,6 @@ F7F67B9F1A24D27800EE80DA = { isa = PBXGroup; children = ( - F3CA2A342ECF7188008E150E /* NextcloudKit */, AA8E041E2D3114E200E7E89C /* README.md */, F7B8B82F25681C3400967775 /* GoogleService-Info.plist */, F7C1CDD91E6DFC6F005D92BE /* Brand */,