From 24fa25bad555836b02df062b9a5dd9e8fc9d19fb Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 11:31:18 -0400 Subject: [PATCH 01/11] Add initial PostStatusPickerView --- .../Settings/PostSettingsCheckmark.swift | 18 ++ .../Views/Settings/SecureTextField.swift | 71 ++++++ .../Views/Settings/SettingsRow.swift | 23 ++ .../Extensions/PostStatus+Extensions.swift | 42 ++++ .../Post/PostSettings/PostSettingsView.swift | 23 -- .../Views/PostSettingsPasswordEntryView.swift | 67 +++++ .../Views/PostStatusPickerView.swift | 230 ++++++++++++++++++ .../post-status/Contents.json | 6 + .../post-status-draft.imageset/Contents.json | 16 ++ .../post-status-draft.svg | 3 + .../Contents.json | 16 ++ .../post-status-pending.svg | 3 + .../Contents.json | 16 ++ .../post-status-private.svg | 3 + .../Contents.json | 16 ++ .../post-status-published.svg | 3 + .../Contents.json | 16 ++ .../post-status-scheduled.svg | 3 + 18 files changed, 552 insertions(+), 23 deletions(-) create mode 100644 Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift create mode 100644 Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift create mode 100644 Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/Extensions/PostStatus+Extensions.swift create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPasswordEntryView.swift create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/post-status-draft.svg create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/post-status-pending.svg create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/post-status-private.svg create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/post-status-published.svg create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/Contents.json create mode 100644 WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/post-status-scheduled.svg diff --git a/Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift b/Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift new file mode 100644 index 000000000000..ec7fc8292302 --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift @@ -0,0 +1,18 @@ +import SwiftUI +import DesignSystem + +public struct SettingsCheckmark: View { + let isSelected: Bool + + public init(isSelected: Bool) { + self.isSelected = isSelected + } + + public var body: some View { + Image(systemName: "checkmark") + .font(.subheadline.weight(.medium)) + .opacity(isSelected ? 1 : 0) + .foregroundStyle(AppColor.primary) + .symbolEffect(.bounce.up, value: isSelected) + } +} diff --git a/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift b/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift new file mode 100644 index 000000000000..6d97d271c8ac --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift @@ -0,0 +1,71 @@ +import SwiftUI +import UIKit + +public struct SecureTextField: UIViewRepresentable { + @Binding var text: String + var isSecure: Bool + let placeholder: String + + public init(text: Binding, isSecure: Bool, placeholder: String) { + self._text = text + self.isSecure = isSecure + self.placeholder = placeholder + } + + public func makeUIView(context: Context) -> UITextField { + let textField = UITextField() + textField.placeholder = placeholder + textField.delegate = context.coordinator + textField.isSecureTextEntry = isSecure + textField.borderStyle = .none + textField.isSecureTextEntry = true + textField.textContentType = .password + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.spellCheckingType = .no + textField.adjustsFontForContentSizeCategory = true + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { + textField.becomeFirstResponder() + } + return textField + } + + public func updateUIView(_ textView: UITextField, context: Context) { + textView.text = text + textView.isSecureTextEntry = isSecure + textView.font = { + if isSecure { + return UIFont.preferredFont(forTextStyle: .body) + } + guard let font = UIFont(name: "Menlo", size: 17) else { + return UIFont.preferredFont(forTextStyle: .body) + } + return UIFontMetrics(forTextStyle: .body).scaledFont(for: font) + }() + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public class Coordinator: NSObject, UITextFieldDelegate { + let parent: SecureTextField + + init(_ parent: SecureTextField) { + self.parent = parent + } + + public func textFieldDidChangeSelection(_ textField: UITextField) { + parent.text = textField.text ?? "" + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let currentText = textField.text, + let textRange = Range(range, in: currentText) { + let updatedText = currentText.replacingCharacters(in: textRange, with: string) + parent.text = updatedText + } + return true + } + } +} diff --git a/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift new file mode 100644 index 000000000000..ef0a5cad71ef --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift @@ -0,0 +1,23 @@ +import SwiftUI + +public struct SettingsRow: View { + let title: String + let value: String + + public init(_ title: String, value: String) { + self.title = title + self.value = value + } + + public var body: some View { + HStack { + Text(title) + .layoutPriority(1) + Spacer() + Text(value) + .foregroundColor(.secondary) + .textSelection(.enabled) + } + .lineLimit(1) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Extensions/PostStatus+Extensions.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Extensions/PostStatus+Extensions.swift new file mode 100644 index 000000000000..9f401149a3f7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Extensions/PostStatus+Extensions.swift @@ -0,0 +1,42 @@ +import SwiftUI +import WordPressData +import WordPressShared + +extension BasePost.Status: @retroactive Identifiable { + public var id: Self { self } + + var title: String { + switch self { + case .draft: NSLocalizedString("postStatus.draft.title", value: "Draft", comment: "Post status title") + case .pending: NSLocalizedString("postStatus.pending.title", value: "Pending", comment: "Post status title") + case .publishPrivate: NSLocalizedString("postStatus.private.title", value: "Private", comment: "Post status title") + case .scheduled: NSLocalizedString("postStatus.scheduled.title", value: "Scheduled", comment: "Post status title") + case .publish: NSLocalizedString("postStatus.published.title", value: "Published", comment: "Post status title") + case .trash: NSLocalizedString("postStatus.trash.title", value: "Trashed", comment: "Post status title") + case .deleted: NSLocalizedString("postStatus.deleted.title", value: "Deleted", comment: "Post status title") + } + } + + var details: String { + switch self { + case .draft: NSLocalizedString("postStatus.draft.details", value: "Not ready to publish", comment: "Post status details") + case .pending: NSLocalizedString("postStatus.pending.details", value: "Waiting for review before publishing", comment: "Post status details") + case .publishPrivate: NSLocalizedString("postStatus.private.details", value: "Only visible to site admins and editors", comment: "Post status details") + case .scheduled: NSLocalizedString("postStatus.scheduled.details", value: "Publish automatically on a chosen date", comment: "Post status details") + case .publish: NSLocalizedString("postStatus.published.details", value: "Visible to everyone", comment: "Post status details") + case .trash: NSLocalizedString("postStatus.trash.details", value: "Trashed but not deleted yet", comment: "Post status title") + case .deleted: NSLocalizedString("postStatus.deleted.details", value: "Permanently deleted", comment: "Post status title") + } + } + + var image: String { + switch self { + case .draft: "post-status-draft" + case .pending: "post-status-pending" + case .publishPrivate: "post-status-private" + case .scheduled: "post-status-scheduled" + case .publish: "post-status-published" + case .trash, .deleted: "" // We don't show these anywhere in the UI + } + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 5b2600bb0c0b..8b97473bda9b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -410,29 +410,6 @@ private struct PostSettingsAuthorRow: View { } } -@MainActor -private struct SettingsRow: View { - let title: String - let value: String - - init(_ title: String, value: String) { - self.title = title - self.value = value - } - - var body: some View { - HStack { - Text(title) - .layoutPriority(1) - Spacer() - Text(value) - .foregroundColor(.secondary) - .textSelection(.enabled) - } - .lineLimit(1) - } -} - @MainActor private struct SettingsTextFieldView: View { let title: String diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPasswordEntryView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPasswordEntryView.swift new file mode 100644 index 000000000000..bf7513b9d123 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPasswordEntryView.swift @@ -0,0 +1,67 @@ +import SwiftUI +import WordPressUI + +struct PostSettingsPasswordEntryView: View { + let password: String + + @State private var input = "" + @State private var isSecure = true + + @Environment(\.dismiss) private var dismiss + + var onSave: (String) -> Void + var onCancel: () -> Void + + var body: some View { + NavigationStack { + Form { + Section { + HStack { + SecureTextField(text: $input, isSecure: isSecure, placeholder: Strings.placeholder) + .fixedSize(horizontal: false, vertical: true) + + Button { + isSecure.toggle() + } label: { + Image(systemName: isSecure ? "eye" : "eye.slash") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .accessibilityLabel(isSecure ? Strings.showPassword : Strings.hidePassword) + } + } footer: { + Text(Strings.instructions) + } + } + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button.make(role: .cancel) { + onCancel() + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button.make(role: .confirm) { + onSave(input) + dismiss() + } + .disabled(input.isEmpty) + } + } + } + .onAppear { + input = password + } + } +} + +private enum Strings { + static let title = NSLocalizedString("postSettings.passwordEntry.navigationTitle", value: "Enter Password", comment: "Navigation title for password entry screen") + static let placeholder = NSLocalizedString("postSettings.passwordEntry.placeholder", value: "Enter password", comment: "Placeholder for password field") + static let instructions = NSLocalizedString("postSettings.passwordEntry.instructions", value: "Enter a password to protect this post. Only users with the password will be able to view it.", comment: "Instructions for password entry") + static let showPassword = NSLocalizedString("postSettings.passwordEntry.showPassword", value: "Show password", comment: "Accessibility label for show password button") + static let hidePassword = NSLocalizedString("postSettings.passwordEntry.hidePassword", value: "Hide password", comment: "Accessibility label for hide password button") +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift new file mode 100644 index 000000000000..2bb7684cd871 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -0,0 +1,230 @@ +import SwiftUI +import WordPressUI +import WordPressData + +struct PostStatusPickerView: View { + @Binding var settings: PostSettings + let timeZone: TimeZone + + @State private var isShowingPublishDatePicker = false + @State private var isShowingPasswordEntry = false + @State private var publishDate = Date() + @State private var password = "" + + @ScaledMetric + private var statusRowLeadingInset: CGFloat = PostStatusRow.leadingInset + + private let statuses = [BasePost.Status.draft, .pending, .publishPrivate, .scheduled, .publish] + + var body: some View { + Form { + statusSection + if settings.status != .publishPrivate { + passwordSection + } + stickySection + } + .dynamicTypeSize(...DynamicTypeSize.accessibility1) + .animation(.default, value: settings) + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $isShowingPublishDatePicker) { + NavigationStack { + PostStatusPublishDatePicker(selection: settings.publishDate, timeZone: timeZone) { + settings.status = .scheduled + settings.publishDate = $0 + } + } + .presentationDetents([.large]) + } + .sheet(isPresented: $isShowingPasswordEntry) { + passwordEntryView + } + } + + @ViewBuilder + private var statusSection: some View { + Section { + ForEach(statuses) { status in + Button { + switch status { + case .draft, .pending, .publishPrivate, .publish: + settings.publishDate = nil + settings.status = status + case .scheduled: + isShowingPublishDatePicker = true + default: + wpAssertionFailure("unsupported case") + } + } label: { + PostStatusRow(status: status, isSelected: settings.status == status) + } + .buttonStyle(.plain) + .accessibilityLabel(status.title) + +// if status == .scheduled && settings.status == .scheduled { +// NavigationLink { +// EmptyView() // TODO: shwo actual picker +// // PostSettingsPublishDatePicker(viewModel: viewModel) +// } label: { +// SettingsRow(Strings.scheduleDate, value: settings.publishDate?.formatted() ?? "–") +// .padding(.leading, statusRowLeadingInset) +// } +// } + } + } + } + + @ViewBuilder + private var passwordSection: some View { + Section { + VStack(alignment: .leading, spacing: 0) { + Toggle(isOn: Binding( + get: { !(settings.password ?? "").isEmpty }, + set: { newValue in + if newValue { + isShowingPasswordEntry = true + } else { + settings.password = nil + } + } + )) { + VStack(alignment: .leading, spacing: 2) { + Text(Strings.passwordProtected) + Text(Strings.passwordProtectedSubtitle) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } + if !(settings.password ?? "").isEmpty { + Button { + isShowingPasswordEntry = true + } label: { + HStack { + Text(Strings.passwordLabel) + Spacer() + Text("••••••••••••") + .font(.footnote.monospaced()) + .foregroundStyle(.secondary) + Image(systemName: "chevron.forward") + .font(.subheadline.weight(.medium)) + .foregroundStyle(.secondary.opacity(0.5)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + } + + @ViewBuilder + private var stickySection: some View { + Section { + Toggle(isOn: $settings.isStickyPost) { + VStack(alignment: .leading, spacing: 2) { + Text(Strings.sticky) + Text(Strings.stickySubtitle) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } + } + + private var passwordEntryView: some View { + PostSettingsPasswordEntryView( + password: password, + onSave: { + settings.password = $0 + }, + onCancel: { + // Do nothing + } + ) + .presentationDetents([.large]) + } +} + +private struct PostStatusPublishDatePicker: View { + let selection: Date? + let timeZone: TimeZone + let onSubmit: (Date) -> Void + + @State private var newDate: Date? + + @Environment(\.dismiss) var dismiss + + var body: some View { + PublishDatePickerView(configuration: PublishDatePickerConfiguration( + date: selection, + isRequired: true, + timeZone: timeZone, + updated: { date in + newDate = date + } + )) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button.make(role: .cancel) { + dismiss() + } + } + ToolbarItem(placement: .topBarLeading) { + Button.make(role: .confirm) { + if let newDate { + onSubmit(newDate) + } + dismiss() + } + .disabled(newDate == nil) + } + } + } +} + +private struct PostStatusRow: View { + let status: BasePost.Status + let isSelected: Bool + + @ScaledMetric + private var leadingInset: CGFloat = Self.leadingInset + + static let leadingInset: CGFloat = 28 + + var body: some View { + HStack(alignment: .center, spacing: 0) { + HStack(alignment: .top, spacing: 0) { + ScaledImage(status.image, height: 23) + .foregroundStyle(isSelected ? Color.primary : .secondary.opacity(0.66)) + .frame(width: leadingInset, alignment: .leading) + .offset(y: -1) + VStack(alignment: .leading, spacing: 2) { + Text(status.title) + .font(isSelected ? .body.weight(.medium) : .body) + .foregroundStyle(.primary) + Text(status.details) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + Spacer(minLength: 0) + SettingsCheckmark(isSelected: isSelected) + } + .contentShape(Rectangle()) + } +} + +private enum Strings { + static let title = NSLocalizedString("postSettings.statusPicker.navigationTitle", value: "Status & Visibility", comment: "Navigation title for status picker") + + static let passwordProtected = NSLocalizedString("postSettings.statusPicker.passwordProtected", value: "Password Protected", comment: "Toggle label for password protection") + static let passwordProtectedSubtitle = NSLocalizedString("postSettings.statusPicker.passwordProtectedSubtitle", value: "Only visible to those who know the password", comment: "Subtitle for password protected toggle") + + static let sticky = NSLocalizedString("postSettings.statusPicker.sticky", value: "Sticky", comment: "Toggle label for sticky posts") + static let stickySubtitle = NSLocalizedString("postSettings.statusPicker.stickySubtitle", value: "Pin this post to the top of the blog", comment: "Subtitle for sticky toggle") + + static let passwordLabel = NSLocalizedString("postSettings.statusPicker.passwordLabel", value: "Password", comment: "Label showing the current password") + + static let scheduleDate = NSLocalizedString("postSettings.statusPicker.scheduleDate", value: "Date", comment: "Label for schedule date row") +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/Contents.json new file mode 100644 index 000000000000..73c00596a7fc --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/Contents.json new file mode 100644 index 000000000000..cf2c7a3491d8 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "post-status-draft.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/post-status-draft.svg b/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/post-status-draft.svg new file mode 100644 index 000000000000..e5533413fda3 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-draft.imageset/post-status-draft.svg @@ -0,0 +1,3 @@ + + + diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/Contents.json new file mode 100644 index 000000000000..dc306c93b44c --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "post-status-pending.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/post-status-pending.svg b/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/post-status-pending.svg new file mode 100644 index 000000000000..fb66b339cac9 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-pending.imageset/post-status-pending.svg @@ -0,0 +1,3 @@ + + + diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/Contents.json new file mode 100644 index 000000000000..1077c0c91524 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "post-status-private.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/post-status-private.svg b/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/post-status-private.svg new file mode 100644 index 000000000000..f4c213af7820 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-private.imageset/post-status-private.svg @@ -0,0 +1,3 @@ + + + diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/Contents.json new file mode 100644 index 000000000000..cef277eb056d --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "post-status-published.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/post-status-published.svg b/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/post-status-published.svg new file mode 100644 index 000000000000..3dc6a10efe54 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-published.imageset/post-status-published.svg @@ -0,0 +1,3 @@ + + + diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/Contents.json new file mode 100644 index 000000000000..97e5cba87b31 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "post-status-scheduled.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/post-status-scheduled.svg b/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/post-status-scheduled.svg new file mode 100644 index 000000000000..fd5b47ec7a89 --- /dev/null +++ b/WordPress/Resources/AppImages.xcassets/post-status/post-status-scheduled.imageset/post-status-scheduled.svg @@ -0,0 +1,3 @@ + + + From c911623885fbf6f613e7f4da41b99e9e30a536e5 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 13:02:09 -0400 Subject: [PATCH 02/11] Integrate PostStatusPickerView --- .../Views/Settings/SettingsRow.swift | 18 +++++++++---- .../Post/PostSettings/PostSettingsView.swift | 24 +++++++++++++++++ .../Views/PostStatusPickerView.swift | 26 ++++++++++++------- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift index ef0a5cad71ef..ec5fb6221e8d 100644 --- a/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift +++ b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift @@ -1,12 +1,12 @@ import SwiftUI -public struct SettingsRow: View { +public struct SettingsRow: View { let title: String - let value: String + let content: Content - public init(_ title: String, value: String) { + public init(_ title: String, @ViewBuilder content: () -> Content) { self.title = title - self.value = value + self.content = content() } public var body: some View { @@ -14,10 +14,18 @@ public struct SettingsRow: View { Text(title) .layoutPriority(1) Spacer() - Text(value) + content .foregroundColor(.secondary) .textSelection(.enabled) } .lineLimit(1) } } + +public extension SettingsRow where Content == Text { + init(_ title: String, value: String) { + self.init(title) { + Text(value) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 8b97473bda9b..26db9bc3ace0 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -240,6 +240,9 @@ struct PostSettingsFormContentView: View { @ViewBuilder private var generalSection: some View { Section { + if viewModel.context != .publishing { + statusRow + } authorRow if !viewModel.isDraftOrPending || viewModel.context == .publishing { publishDateRow @@ -264,6 +267,21 @@ struct PostSettingsFormContentView: View { } } + private var statusRow: some View { + NavigationLink { + PostStatusPickerView(settings: $viewModel.settings, timeZone: viewModel.timeZone) + } label: { + SettingsRow(Strings.status) { + HStack(alignment: .center, spacing: 2) { + ScaledImage(viewModel.settings.status.image, height: 23) + VStack(alignment: .leading, spacing: 2) { + Text(viewModel.settings.status.title) + } + } + } + } + } + private var pendingReviewRow: some View { Toggle(isOn: $viewModel.settings.isPendingReview) { Text(Strings.pendingReviewLabel) @@ -615,4 +633,10 @@ private enum Strings { value: "Ready to Publish?", comment: "The title of the top section that shows the site your are publishing to. Default is 'Ready to Publish?'" ) + + static let status = NSLocalizedString( + "postSettings.status.label", + value: "Status", + comment: "Label for the status field in Post Settings" + ) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift index 2bb7684cd871..0e1654c3d724 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -62,15 +62,21 @@ struct PostStatusPickerView: View { .buttonStyle(.plain) .accessibilityLabel(status.title) -// if status == .scheduled && settings.status == .scheduled { -// NavigationLink { -// EmptyView() // TODO: shwo actual picker -// // PostSettingsPublishDatePicker(viewModel: viewModel) -// } label: { -// SettingsRow(Strings.scheduleDate, value: settings.publishDate?.formatted() ?? "–") -// .padding(.leading, statusRowLeadingInset) -// } -// } + if status == .scheduled && settings.status == .scheduled { + NavigationLink { + PublishDatePickerView(configuration: PublishDatePickerConfiguration( + date: settings.publishDate, + isRequired: true, + timeZone: timeZone, + updated: { date in + settings.publishDate = date + } + )) + } label: { + SettingsRow(Strings.scheduleDate, value: settings.publishDate?.formatted() ?? "–") + .padding(.leading, statusRowLeadingInset) + } + } } } } @@ -170,7 +176,7 @@ private struct PostStatusPublishDatePicker: View { dismiss() } } - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .topBarTrailing) { Button.make(role: .confirm) { if let newDate { onSubmit(newDate) From 54a3b1fe8f8d9f42815016821cdac407eee25b4c Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 13:05:05 -0400 Subject: [PATCH 03/11] Update how timezone is formatted --- .../ViewRelated/Post/PostSettings/PostSettingsViewModel.swift | 4 ++++ .../Post/PostSettings/Views/PostStatusPickerView.swift | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index d6c7faaaf75d..d7ca01e51ec3 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -53,6 +53,10 @@ final class PostSettingsViewModel: NSObject, ObservableObject { guard let date = settings.publishDate else { return nil } + return Self.formattedDate(date, in: timeZone) + } + + static func formattedDate(_ date: Date, in timeZone: TimeZone) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift index 0e1654c3d724..d29be55394a6 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -62,7 +62,7 @@ struct PostStatusPickerView: View { .buttonStyle(.plain) .accessibilityLabel(status.title) - if status == .scheduled && settings.status == .scheduled { + if status == .scheduled && settings.status == .scheduled, let date = settings.publishDate { NavigationLink { PublishDatePickerView(configuration: PublishDatePickerConfiguration( date: settings.publishDate, @@ -73,7 +73,7 @@ struct PostStatusPickerView: View { } )) } label: { - SettingsRow(Strings.scheduleDate, value: settings.publishDate?.formatted() ?? "–") + SettingsRow(Strings.scheduleDate, value: PostSettingsViewModel.formattedDate(date, in: timeZone)) .padding(.leading, statusRowLeadingInset) } } From 5440858e906df5a8b3a7122e807db2903eb634fe Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 13:40:09 -0400 Subject: [PATCH 04/11] Restrict date for scheduling to the future dates --- .../Post/PostSettings/Views/PostStatusPickerView.swift | 2 ++ .../Post/Scheduling/PublishDatePickerViewController.swift | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift index d29be55394a6..e6ba8fd816f1 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -68,6 +68,7 @@ struct PostStatusPickerView: View { date: settings.publishDate, isRequired: true, timeZone: timeZone, + range: Date.now...Date.distantFuture, updated: { date in settings.publishDate = date } @@ -166,6 +167,7 @@ private struct PostStatusPublishDatePicker: View { date: selection, isRequired: true, timeZone: timeZone, + range: Date.now...Date.distantFuture, updated: { date in newDate = date } diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index 133e82f18507..b45fb150ff2d 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -11,6 +11,7 @@ struct PublishDatePickerConfiguration { /// If set to `true`, the user will no longer be able to remove the selection. var isRequired = false var timeZone: TimeZone + var range = Date.distantFuture...Date.distantFuture var updated: (Date?) -> Void } @@ -90,7 +91,7 @@ struct PublishDatePickerView: View { configuration.date ?? Date() }, set: { configuration.date = $0 - }), displayedComponents: [.date, .hourAndMinute]) + }), in: configuration.range, displayedComponents: [.date, .hourAndMinute]) .environment(\.timeZone, configuration.timeZone) .datePickerStyle(.graphical) .labelsHidden() From a0eecdfb5f173a417232c4959ffa1d283ab7959a Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 13:51:37 -0400 Subject: [PATCH 05/11] Simplify how publishing works --- .../Classes/Services/PostCoordinator.swift | 27 ++++++++----------- .../PostSettings/PostSettingsViewModel.swift | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index a3e0bacf6b9c..b00d1b0d17a0 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -82,12 +82,6 @@ class PostCoordinator: NSObject { /// with the publishing options. @MainActor func publish(_ post: AbstractPost, parameters: RemotePostUpdateParameters = .init()) async throws { - wpAssert(post.isOriginal()) - wpAssert(post.isStatus(in: [.draft, .pending])) - - await pauseSyncing(for: post) - defer { resumeSyncing(for: post) } - var parameters = parameters if parameters.status == nil { parameters.status = Post.Status.publish.rawValue @@ -98,16 +92,7 @@ class PostCoordinator: NSObject { parameters.date = post.shouldPublishImmediately() ? nil : Date() } - do { - let repository = PostRepository(coreDataStack: coreDataStack) - try await repository.save(post, changes: parameters) - didPublish(post) - show(PostCoordinator.makeUploadSuccessNotice(for: post)) - } catch { - trackError(error, operation: "post-publish", post: post) - handleError(error, for: post) - throw error - } + try await save(post, changes: parameters) } @MainActor @@ -132,6 +117,16 @@ class PostCoordinator: NSObject { do { let previousStatus = post.status try await PostRepository().save(post, changes: changes) + + if previousStatus != post.status && post.isStatus(in: [.scheduled, .publish]) { + if post.status == .scheduled { + notifyNewPostScheduled() + } else if post.status == .publish { + notifyNewPostPublished() + } + SearchManager.shared.indexItem(post) + AppRatingUtility.shared.incrementSignificantEvent() + } show(PostCoordinator.makeUploadSuccessNotice(for: post, previousStatus: previousStatus)) return post } catch { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index d7ca01e51ec3..849d82ca6485 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -268,7 +268,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject { do { let settings = getSettingsToSave(for: self.settings) let coordinator = PostCoordinator.shared - if coordinator.isSyncAllowed(for: post) { + if coordinator.isSyncAllowed(for: post) && post.status == settings.status { let revision = post.createRevision() settings.apply(to: revision) coordinator.setNeedsSync(for: revision) From 299af97bc35f1fdc58c5a54d02b3e28bc36a7d87 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 14:09:44 -0400 Subject: [PATCH 06/11] Show publish date for drafts --- .../WordPressUI/Views/Settings/SettingsRow.swift | 1 + .../Post/PostSettings/PostSettingsView.swift | 5 +---- .../PostSettings/Views/PostStatusPickerView.swift | 13 ++++++++----- .../PublishDatePickerViewController.swift | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift index ec5fb6221e8d..f1904965af25 100644 --- a/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift +++ b/Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift @@ -15,6 +15,7 @@ public struct SettingsRow: View { .layoutPriority(1) Spacer() content + .font(.callout) .foregroundColor(.secondary) .textSelection(.enabled) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 26db9bc3ace0..b37c81648074 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -244,10 +244,7 @@ struct PostSettingsFormContentView: View { statusRow } authorRow - if !viewModel.isDraftOrPending || viewModel.context == .publishing { - publishDateRow - visibilityRow - } + publishDateRow slugRow } header: { SectionHeader(Strings.generalHeader) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift index e6ba8fd816f1..f1291891c70a 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -158,7 +158,7 @@ private struct PostStatusPublishDatePicker: View { let timeZone: TimeZone let onSubmit: (Date) -> Void - @State private var newDate: Date? + @State private var newSelection: Date? @Environment(\.dismiss) var dismiss @@ -169,9 +169,12 @@ private struct PostStatusPublishDatePicker: View { timeZone: timeZone, range: Date.now...Date.distantFuture, updated: { date in - newDate = date + newSelection = date } )) + .onAppear { + newSelection = selection + } .toolbar { ToolbarItem(placement: .topBarLeading) { Button.make(role: .cancel) { @@ -180,12 +183,12 @@ private struct PostStatusPublishDatePicker: View { } ToolbarItem(placement: .topBarTrailing) { Button.make(role: .confirm) { - if let newDate { - onSubmit(newDate) + if let newSelection { + onSubmit(newSelection) } dismiss() } - .disabled(newDate == nil) + .disabled(newSelection == nil) } } } diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index b45fb150ff2d..f9ea122d78a9 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -11,7 +11,7 @@ struct PublishDatePickerConfiguration { /// If set to `true`, the user will no longer be able to remove the selection. var isRequired = false var timeZone: TimeZone - var range = Date.distantFuture...Date.distantFuture + var range = Date.distantPast...Date.distantFuture var updated: (Date?) -> Void } From dc7a49da79970ea3e973c9d061a3d517d6a50353 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 15:36:20 -0400 Subject: [PATCH 07/11] Larger checkmark --- .../{PostSettingsCheckmark.swift => SettingsCheckmark.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Modules/Sources/WordPressUI/Views/Settings/{PostSettingsCheckmark.swift => SettingsCheckmark.swift} (89%) diff --git a/Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift b/Modules/Sources/WordPressUI/Views/Settings/SettingsCheckmark.swift similarity index 89% rename from Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift rename to Modules/Sources/WordPressUI/Views/Settings/SettingsCheckmark.swift index ec7fc8292302..99d383895c9c 100644 --- a/Modules/Sources/WordPressUI/Views/Settings/PostSettingsCheckmark.swift +++ b/Modules/Sources/WordPressUI/Views/Settings/SettingsCheckmark.swift @@ -10,7 +10,7 @@ public struct SettingsCheckmark: View { public var body: some View { Image(systemName: "checkmark") - .font(.subheadline.weight(.medium)) + .font(.headline) .opacity(isSelected ? 1 : 0) .foregroundStyle(AppColor.primary) .symbolEffect(.bounce.up, value: isSelected) From b11dcf3fdfe27231d15f4e2290c7865c6323c856 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 15:47:10 -0400 Subject: [PATCH 08/11] Update release notes --- RELEASE-NOTES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5b3672489631..20d9d2fca552 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,9 @@ 26.5 ----- +* [**] Add "Status" field to the "Post Settings" screen to make it easier to move posts from one state to another [#24939] +* [*] Add "Publish Date" back to "Post Settings" for draft and pending posts [#24939] +* [*] Use "Menlo" font for the password field to make it easier to distinguish between characters like `0` and `0` [#24939] 26.4 ----- From 190027b1b40a2bc4cb542015c8ad503a92c6c2e1 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 16:48:16 -0400 Subject: [PATCH 09/11] Set date to now when publishing a scheduled post --- .../Classes/Services/PostCoordinator.swift | 21 ++++++++++++------- .../Views/PostStatusPickerView.swift | 4 +++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index b00d1b0d17a0..5afd0b748efd 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -86,12 +86,6 @@ class PostCoordinator: NSObject { if parameters.status == nil { parameters.status = Post.Status.publish.rawValue } - if parameters.date == nil { - // If the post was previously scheduled for a different date, - // the app has to send a new value to override it. - parameters.date = post.shouldPublishImmediately() ? nil : Date() - } - try await save(post, changes: parameters) } @@ -114,8 +108,21 @@ class PostCoordinator: NSObject { await pauseSyncing(for: post) defer { resumeSyncing(for: post) } + let previousStatus = post.status + + var changes = changes ?? .init() + + // If the post was previously scheduled and the user wants to publish + // it without specifying a new publish date, we have to send `.now` + // to ensure it gets published immediatelly. + if (changes.status == Post.Status.publish.rawValue || + changes.status == Post.Status.publishPrivate.rawValue) && + previousStatus == .scheduled && + changes.date == nil { + changes.date = .now + } + do { - let previousStatus = post.status try await PostRepository().save(post, changes: changes) if previousStatus != post.status && post.isStatus(in: [.scheduled, .publish]) { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift index f1291891c70a..860e2a250d8c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift @@ -49,7 +49,9 @@ struct PostStatusPickerView: View { Button { switch status { case .draft, .pending, .publishPrivate, .publish: - settings.publishDate = nil + if settings.status == .scheduled { + settings.publishDate = nil + } settings.status = status case .scheduled: isShowingPublishDatePicker = true From c1e4511e51eb4307baa900eedc444494d2251acb Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 17:03:19 -0400 Subject: [PATCH 10/11] Simplify SecureTextField --- .../Views/Settings/SecureTextField.swift | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift b/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift index 6d97d271c8ac..3317cab43e61 100644 --- a/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift +++ b/Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift @@ -1,21 +1,23 @@ import SwiftUI import UIKit +/// - note: The SwiftUI version of the secure text field does not allow you to +/// change to the "view password" mode while preserving the focus – you have +/// to create a separate (regular) text field for that, and it loses focus. public struct SecureTextField: UIViewRepresentable { @Binding var text: String var isSecure: Bool let placeholder: String - + public init(text: Binding, isSecure: Bool, placeholder: String) { self._text = text self.isSecure = isSecure self.placeholder = placeholder } - + public func makeUIView(context: Context) -> UITextField { let textField = UITextField() textField.placeholder = placeholder - textField.delegate = context.coordinator textField.isSecureTextEntry = isSecure textField.borderStyle = .none textField.isSecureTextEntry = true @@ -24,12 +26,13 @@ public struct SecureTextField: UIViewRepresentable { textField.autocapitalizationType = .none textField.spellCheckingType = .no textField.adjustsFontForContentSizeCategory = true + textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange), for: .editingChanged) DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { textField.becomeFirstResponder() } return textField } - + public func updateUIView(_ textView: UITextField, context: Context) { textView.text = text textView.isSecureTextEntry = isSecure @@ -43,29 +46,20 @@ public struct SecureTextField: UIViewRepresentable { return UIFontMetrics(forTextStyle: .body).scaledFont(for: font) }() } - + public func makeCoordinator() -> Coordinator { Coordinator(self) } - - public class Coordinator: NSObject, UITextFieldDelegate { + + public class Coordinator: NSObject { let parent: SecureTextField - + init(_ parent: SecureTextField) { self.parent = parent } - - public func textFieldDidChangeSelection(_ textField: UITextField) { + + @objc func textFieldDidChange(_ textField: UITextField) { parent.text = textField.text ?? "" } - - public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if let currentText = textField.text, - let textRange = Range(range, in: currentText) { - let updatedText = currentText.replacingCharacters(in: textRange, with: string) - parent.text = updatedText - } - return true - } } } From ba92c56c23c1a85fd8725864fb6d085445b48cb6 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Oct 2025 19:35:18 -0400 Subject: [PATCH 11/11] Show schedule date in the same row --- .../Post/PostSettings/PostSettingsView.swift | 2 +- ...sPickerView.swift => PostStatusView.swift} | 31 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) rename WordPress/Classes/ViewRelated/Post/PostSettings/Views/{PostStatusPickerView.swift => PostStatusView.swift} (90%) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index b37c81648074..b08be4faad2f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -266,7 +266,7 @@ struct PostSettingsFormContentView: View { private var statusRow: some View { NavigationLink { - PostStatusPickerView(settings: $viewModel.settings, timeZone: viewModel.timeZone) + PostStatusView(settings: $viewModel.settings, timeZone: viewModel.timeZone) } label: { SettingsRow(Strings.status) { HStack(alignment: .center, spacing: 2) { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusView.swift similarity index 90% rename from WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift rename to WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusView.swift index 860e2a250d8c..00f319fd03d9 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusPickerView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostStatusView.swift @@ -2,7 +2,7 @@ import SwiftUI import WordPressUI import WordPressData -struct PostStatusPickerView: View { +struct PostStatusView: View { @Binding var settings: PostSettings let timeZone: TimeZone @@ -59,27 +59,22 @@ struct PostStatusPickerView: View { wpAssertionFailure("unsupported case") } } label: { - PostStatusRow(status: status, isSelected: settings.status == status) - } - .buttonStyle(.plain) - .accessibilityLabel(status.title) - - if status == .scheduled && settings.status == .scheduled, let date = settings.publishDate { - NavigationLink { - PublishDatePickerView(configuration: PublishDatePickerConfiguration( - date: settings.publishDate, - isRequired: true, - timeZone: timeZone, - range: Date.now...Date.distantFuture, - updated: { date in - settings.publishDate = date + VStack { + PostStatusRow(status: status, isSelected: settings.status == status) + if status == .scheduled && settings.status == .scheduled, let date = settings.publishDate { + HStack { + SettingsRow(Strings.scheduleDate, value: PostSettingsViewModel.formattedDate(date, in: timeZone)) + Image(systemName: "chevron.forward") + .font(.footnote.weight(.semibold)) + .foregroundColor(Color(.tertiaryLabel)) } - )) - } label: { - SettingsRow(Strings.scheduleDate, value: PostSettingsViewModel.formattedDate(date, in: timeZone)) .padding(.leading, statusRowLeadingInset) + .padding(.top, 8) + } } } + .buttonStyle(.plain) + .accessibilityLabel(status.title) } } }