Skip to content

Commit f322613

Browse files
authored
Add "Status" row to "Post Settings" (#24939)
* Add initial PostStatusPickerView * Integrate PostStatusPickerView * Update how timezone is formatted * Restrict date for scheduling to the future dates * Simplify how publishing works * Show publish date for drafts * Larger checkmark * Update release notes * Set date to now when publishing a scheduled post * Simplify SecureTextField * Show schedule date in the same row * Enable status field only for the standalone settings * Fix password not shown * Fix password font * Update tests
1 parent 51c1fa4 commit f322613

File tree

22 files changed

+615
-53
lines changed

22 files changed

+615
-53
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
/// - note: The SwiftUI version of the secure text field does not allow you to
5+
/// change to the "view password" mode while preserving the focus – you have
6+
/// to create a separate (regular) text field for that, and it loses focus.
7+
public struct SecureTextField: UIViewRepresentable {
8+
@Binding var text: String
9+
var isSecure: Bool
10+
let placeholder: String
11+
12+
public init(text: Binding<String>, isSecure: Bool, placeholder: String) {
13+
self._text = text
14+
self.isSecure = isSecure
15+
self.placeholder = placeholder
16+
}
17+
18+
public func makeUIView(context: Context) -> UITextField {
19+
let textField = UITextField()
20+
textField.placeholder = placeholder
21+
textField.isSecureTextEntry = isSecure
22+
textField.borderStyle = .none
23+
textField.isSecureTextEntry = true
24+
textField.textContentType = .password
25+
textField.autocorrectionType = .no
26+
textField.autocapitalizationType = .none
27+
textField.spellCheckingType = .no
28+
textField.adjustsFontForContentSizeCategory = true
29+
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange), for: .editingChanged)
30+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
31+
textField.becomeFirstResponder()
32+
}
33+
return textField
34+
}
35+
36+
public func updateUIView(_ textView: UITextField, context: Context) {
37+
textView.text = text
38+
textView.isSecureTextEntry = isSecure
39+
textView.font = {
40+
if isSecure || text.isEmpty {
41+
return UIFont.preferredFont(forTextStyle: .body)
42+
}
43+
guard let font = UIFont(name: "Menlo", size: 17) else {
44+
return UIFont.preferredFont(forTextStyle: .body)
45+
}
46+
return UIFontMetrics(forTextStyle: .body).scaledFont(for: font)
47+
}()
48+
}
49+
50+
public func makeCoordinator() -> Coordinator {
51+
Coordinator(self)
52+
}
53+
54+
public class Coordinator: NSObject {
55+
let parent: SecureTextField
56+
57+
init(_ parent: SecureTextField) {
58+
self.parent = parent
59+
}
60+
61+
@objc func textFieldDidChange(_ textField: UITextField) {
62+
parent.text = textField.text ?? ""
63+
}
64+
}
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import SwiftUI
2+
import DesignSystem
3+
4+
public struct SettingsCheckmark: View {
5+
let isSelected: Bool
6+
7+
public init(isSelected: Bool) {
8+
self.isSelected = isSelected
9+
}
10+
11+
public var body: some View {
12+
Image(systemName: "checkmark")
13+
.font(.headline)
14+
.opacity(isSelected ? 1 : 0)
15+
.foregroundStyle(AppColor.primary)
16+
.symbolEffect(.bounce.up, value: isSelected)
17+
}
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import SwiftUI
2+
3+
public struct SettingsRow<Content: View>: View {
4+
let title: String
5+
let content: Content
6+
7+
public init(_ title: String, @ViewBuilder content: () -> Content) {
8+
self.title = title
9+
self.content = content()
10+
}
11+
12+
public var body: some View {
13+
HStack {
14+
Text(title)
15+
.layoutPriority(1)
16+
Spacer()
17+
content
18+
.font(.callout)
19+
.foregroundColor(.secondary)
20+
.textSelection(.enabled)
21+
}
22+
.lineLimit(1)
23+
}
24+
}
25+
26+
public extension SettingsRow where Content == Text {
27+
init(_ title: String, value: String) {
28+
self.init(title) {
29+
Text(value)
30+
}
31+
}
32+
}

RELEASE-NOTES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
26.5
22
-----
3+
* [**] Add "Status" field to the "Post Settings" screen to make it easier to move posts from one state to another [#24939]
4+
* [*] Add "Publish Date" back to "Post Settings" for draft and pending posts [#24939]
5+
* [*] Use "Menlo" font for the password field to make it easier to distinguish between characters like `0` and `0` [#24939]
36
* [*] Add "Access" section to "Post Settings" [#24942]
47
* [*] Add "Email to Subscribers" row to "Publishing" sheet [#24946]
58

WordPress/Classes/Services/PostCoordinator.swift

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -82,32 +82,11 @@ class PostCoordinator: NSObject {
8282
/// with the publishing options.
8383
@MainActor
8484
func publish(_ post: AbstractPost, parameters: RemotePostUpdateParameters = .init()) async throws {
85-
wpAssert(post.isOriginal())
86-
wpAssert(post.isStatus(in: [.draft, .pending]))
87-
88-
await pauseSyncing(for: post)
89-
defer { resumeSyncing(for: post) }
90-
9185
var parameters = parameters
9286
if parameters.status == nil {
9387
parameters.status = Post.Status.publish.rawValue
9488
}
95-
if parameters.date == nil {
96-
// If the post was previously scheduled for a different date,
97-
// the app has to send a new value to override it.
98-
parameters.date = post.shouldPublishImmediately() ? nil : Date()
99-
}
100-
101-
do {
102-
let repository = PostRepository(coreDataStack: coreDataStack)
103-
try await repository.save(post, changes: parameters)
104-
didPublish(post)
105-
show(PostCoordinator.makeUploadSuccessNotice(for: post))
106-
} catch {
107-
trackError(error, operation: "post-publish", post: post)
108-
handleError(error, for: post)
109-
throw error
110-
}
89+
try await save(post, changes: parameters)
11190
}
11291

11392
@MainActor
@@ -129,9 +108,33 @@ class PostCoordinator: NSObject {
129108
await pauseSyncing(for: post)
130109
defer { resumeSyncing(for: post) }
131110

111+
let previousStatus = post.status
112+
113+
var changes = changes ?? .init()
114+
115+
// If the post was previously scheduled and the user wants to publish
116+
// it without specifying a new publish date, we have to send `.now`
117+
// to ensure it gets published immediatelly.
118+
if (changes.status == Post.Status.publish.rawValue ||
119+
changes.status == Post.Status.publishPrivate.rawValue) &&
120+
previousStatus == .scheduled &&
121+
changes.date == nil {
122+
changes.date = .now
123+
}
124+
132125
do {
133-
let previousStatus = post.status
134-
try await PostRepository().save(post, changes: changes)
126+
let repository = PostRepository(coreDataStack: coreDataStack)
127+
try await repository.save(post, changes: changes)
128+
129+
if previousStatus != post.status && post.isStatus(in: [.scheduled, .publish]) {
130+
if post.status == .scheduled {
131+
notifyNewPostScheduled()
132+
} else if post.status == .publish {
133+
notifyNewPostPublished()
134+
}
135+
SearchManager.shared.indexItem(post)
136+
AppRatingUtility.shared.incrementSignificantEvent()
137+
}
135138
show(PostCoordinator.makeUploadSuccessNotice(for: post, previousStatus: previousStatus))
136139
return post
137140
} catch {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import SwiftUI
2+
import WordPressData
3+
import WordPressShared
4+
5+
extension BasePost.Status: @retroactive Identifiable {
6+
public var id: Self { self }
7+
8+
var title: String {
9+
switch self {
10+
case .draft: NSLocalizedString("postStatus.draft.title", value: "Draft", comment: "Post status title")
11+
case .pending: NSLocalizedString("postStatus.pending.title", value: "Pending", comment: "Post status title")
12+
case .publishPrivate: NSLocalizedString("postStatus.private.title", value: "Private", comment: "Post status title")
13+
case .scheduled: NSLocalizedString("postStatus.scheduled.title", value: "Scheduled", comment: "Post status title")
14+
case .publish: NSLocalizedString("postStatus.published.title", value: "Published", comment: "Post status title")
15+
case .trash: NSLocalizedString("postStatus.trash.title", value: "Trashed", comment: "Post status title")
16+
case .deleted: NSLocalizedString("postStatus.deleted.title", value: "Deleted", comment: "Post status title")
17+
}
18+
}
19+
20+
var details: String {
21+
switch self {
22+
case .draft: NSLocalizedString("postStatus.draft.details", value: "Not ready to publish", comment: "Post status details")
23+
case .pending: NSLocalizedString("postStatus.pending.details", value: "Waiting for review before publishing", comment: "Post status details")
24+
case .publishPrivate: NSLocalizedString("postStatus.private.details", value: "Only visible to site admins and editors", comment: "Post status details")
25+
case .scheduled: NSLocalizedString("postStatus.scheduled.details", value: "Publish automatically on a chosen date", comment: "Post status details")
26+
case .publish: NSLocalizedString("postStatus.published.details", value: "Visible to everyone", comment: "Post status details")
27+
case .trash: NSLocalizedString("postStatus.trash.details", value: "Trashed but not deleted yet", comment: "Post status title")
28+
case .deleted: NSLocalizedString("postStatus.deleted.details", value: "Permanently deleted", comment: "Post status title")
29+
}
30+
}
31+
32+
var image: String {
33+
switch self {
34+
case .draft: "post-status-draft"
35+
case .pending: "post-status-pending"
36+
case .publishPrivate: "post-status-private"
37+
case .scheduled: "post-status-scheduled"
38+
case .publish: "post-status-published"
39+
case .trash, .deleted: "" // We don't show these anywhere in the UI
40+
}
41+
}
42+
}

WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,11 @@ struct PostSettingsFormContentView: View {
241241
@ViewBuilder
242242
private var generalSection: some View {
243243
Section {
244-
authorRow
245-
if !viewModel.isDraftOrPending || viewModel.context == .publishing {
246-
publishDateRow
247-
visibilityRow
244+
if viewModel.context == .settings && viewModel.isStandalone {
245+
statusRow
248246
}
247+
authorRow
248+
publishDateRow
249249
slugRow
250250
} header: {
251251
SectionHeader(Strings.generalHeader)
@@ -265,6 +265,21 @@ struct PostSettingsFormContentView: View {
265265
}
266266
}
267267

268+
private var statusRow: some View {
269+
NavigationLink {
270+
PostStatusView(settings: $viewModel.settings, timeZone: viewModel.timeZone)
271+
} label: {
272+
SettingsRow(Strings.status) {
273+
HStack(alignment: .center, spacing: 2) {
274+
ScaledImage(viewModel.settings.status.image, height: 23)
275+
VStack(alignment: .leading, spacing: 2) {
276+
Text(viewModel.settings.status.title)
277+
}
278+
}
279+
}
280+
}
281+
}
282+
268283
private var pendingReviewRow: some View {
269284
Toggle(isOn: $viewModel.settings.isPendingReview) {
270285
Text(Strings.pendingReviewLabel)
@@ -439,29 +454,6 @@ private struct PostSettingsAuthorRow: View {
439454
}
440455
}
441456

442-
@MainActor
443-
private struct SettingsRow: View {
444-
let title: String
445-
let value: String
446-
447-
init(_ title: String, value: String) {
448-
self.title = title
449-
self.value = value
450-
}
451-
452-
var body: some View {
453-
HStack {
454-
Text(title)
455-
.layoutPriority(1)
456-
Spacer()
457-
Text(value)
458-
.foregroundColor(.secondary)
459-
.textSelection(.enabled)
460-
}
461-
.lineLimit(1)
462-
}
463-
}
464-
465457
@MainActor
466458
private struct SettingsTextFieldView: View {
467459
let title: String
@@ -679,4 +671,10 @@ private enum Strings {
679671
value: "Ready to Publish?",
680672
comment: "The title of the top section that shows the site your are publishing to. Default is 'Ready to Publish?'"
681673
)
674+
675+
static let status = NSLocalizedString(
676+
"postSettings.status.label",
677+
value: "Status",
678+
comment: "Label for the status field in Post Settings"
679+
)
682680
}

WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ final class PostSettingsViewModel: NSObject, ObservableObject {
5858
guard let date = settings.publishDate else {
5959
return nil
6060
}
61+
return Self.formattedDate(date, in: timeZone)
62+
}
63+
64+
static func formattedDate(_ date: Date, in timeZone: TimeZone) -> String {
6165
let formatter = DateFormatter()
6266
formatter.dateStyle = .medium
6367
formatter.timeStyle = .short
@@ -283,7 +287,7 @@ final class PostSettingsViewModel: NSObject, ObservableObject {
283287
do {
284288
let settings = getSettingsToSave(for: self.settings)
285289
let coordinator = PostCoordinator.shared
286-
if coordinator.isSyncAllowed(for: post) {
290+
if coordinator.isSyncAllowed(for: post) && post.status == settings.status {
287291
let revision = post.createRevision()
288292
settings.apply(to: revision)
289293
coordinator.setNeedsSync(for: revision)

0 commit comments

Comments
 (0)