Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Modules/Sources/WordPressUI/Views/Settings/SecureTextField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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<String>, 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.isSecureTextEntry = isSecure
textField.borderStyle = .none
textField.isSecureTextEntry = true
textField.textContentType = .password
textField.autocorrectionType = .no
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
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 {
let parent: SecureTextField

init(_ parent: SecureTextField) {
self.parent = parent
}

@objc func textFieldDidChange(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
18 changes: 18 additions & 0 deletions Modules/Sources/WordPressUI/Views/Settings/SettingsCheckmark.swift
Original file line number Diff line number Diff line change
@@ -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(.headline)
.opacity(isSelected ? 1 : 0)
.foregroundStyle(AppColor.primary)
.symbolEffect(.bounce.up, value: isSelected)
}
}
32 changes: 32 additions & 0 deletions Modules/Sources/WordPressUI/Views/Settings/SettingsRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

public struct SettingsRow<Content: View>: View {
let title: String
let content: Content

public init(_ title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}

public var body: some View {
HStack {
Text(title)
.layoutPriority(1)
Spacer()
content
.font(.callout)
.foregroundColor(.secondary)
.textSelection(.enabled)
}
.lineLimit(1)
}
}

public extension SettingsRow where Content == Text {
init(_ title: String, value: String) {
self.init(title) {
Text(value)
}
}
}
3 changes: 3 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -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
-----
Expand Down
48 changes: 25 additions & 23 deletions WordPress/Classes/Services/PostCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,32 +82,11 @@ 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
}
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()
}

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
Expand All @@ -129,9 +108,32 @@ 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]) {
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,11 @@ struct PostSettingsFormContentView: View {
@ViewBuilder
private var generalSection: some View {
Section {
authorRow
if !viewModel.isDraftOrPending || viewModel.context == .publishing {
publishDateRow
visibilityRow
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visilbilityRow is displayed at the top of the screen in the "Publishing" sheet (matches the web).

if viewModel.context != .publishing {
statusRow
}
authorRow
publishDateRow
slugRow
} header: {
SectionHeader(Strings.generalHeader)
Expand All @@ -264,6 +264,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)
Expand Down Expand Up @@ -410,29 +425,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
Expand Down Expand Up @@ -638,4 +630,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"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -264,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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you try to change the status of a post, it will be performed as a synchronous operation (showing a spinner until succeeds).

let revision = post.createRevision()
settings.apply(to: revision)
coordinator.setNeedsSync(for: revision)
Expand Down
Loading