From 6a35a45562b561817f4ffb8d3fbcbd5ca03cc3b2 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 10:01:03 -0400 Subject: [PATCH 01/41] Add empty PublishPostViewController --- .../BuildInformation/FeatureFlag.swift | 4 +++ .../PrepublishingViewController+Helpers.swift | 11 ++++++-- .../PublishPostViewController.swift | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index acc58d8b27d5..140fbc9255b4 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -26,6 +26,7 @@ public enum FeatureFlag: Int, CaseIterable { case pluginManagementOverhaul case newsletterSubscribers case newStats + case newPublishingSheet /// Returns a boolean indicating if the feature is enabled. /// @@ -82,6 +83,8 @@ public enum FeatureFlag: Int, CaseIterable { return true case .newStats: return false + case .newPublishingSheet: + return true } } @@ -125,6 +128,7 @@ extension FeatureFlag { case .readerGutenbergCommentComposer: "Gutenberg Comment Composer" case .newsletterSubscribers: "Newsletter Subscribers" case .newStats: "New Stats" + case .newPublishingSheet: "New Publishing Sheet" } } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index b1b6dc56fdc7..a40eca513244 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -6,7 +6,14 @@ extension PrepublishingViewController { // End editing to avoid issues with accessibility presentingViewController.view.endEditing(true) - let viewController = PrepublishingViewController(post: revision, isStandalone: isStandalone, completion: completion) - viewController.presentAsSheet(from: presentingViewController) + guard FeatureFlag.newPublishingSheet.enabled else { + let viewController = PrepublishingViewController(post: revision, isStandalone: isStandalone, completion: completion) + viewController.presentAsSheet(from: presentingViewController) + return + } + + let publishVC = PublishPostViewController(post: revision) + publishVC.sheetPresentationController?.detents = [.medium(), .large()] + presentingViewController.present(publishVC, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift new file mode 100644 index 000000000000..d0dcc52a2ef3 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -0,0 +1,26 @@ +import UIKit +import Combine +import SwiftUI +import WordPressData +import WordPressShared +import WordPressUI + +/// A screen shown just before publishing the post and allows you to change +/// the post settings along with some publishing options like the publish date. +final class PublishPostViewController: UIHostingController> { + init(post: AbstractPost) { + super.init(rootView: NavigationView { PublishPostView() }) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// TODO: add isStandalone support +// TODO: add `PrepublishingSheetResult` +struct PublishPostView: View { + var body: some View { + Text("Here") + } +} From 60fe6f6df5ddbf0ce281e21884e2bdf3f7513235 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 10:03:50 -0400 Subject: [PATCH 02/41] Extract PostSettingsFormContentView --- .../Post/PostSettings/PostSettingsView.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index a093645d6884..ef5caad326be 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -50,14 +50,7 @@ private struct PostSettingsView: View { var body: some View { Form { - featuredImageSection - generalSection - if viewModel.isPost { - organizationSection - } - excerptSection - moreOptionsSection - infoSection + PostSettingsFormContentView(viewModel: viewModel) } .accessibilityIdentifier("post_settings_form") .disabled(viewModel.isSaving) @@ -125,6 +118,21 @@ private struct PostSettingsView: View { .tint(AppColor.tint) } } +} + +struct PostSettingsFormContentView: View { + @ObservedObject var viewModel: PostSettingsViewModel + + var body: some View { + featuredImageSection + generalSection + if viewModel.isPost { + organizationSection + } + excerptSection + moreOptionsSection + infoSection + } // MARK: - "Featured Image" Section From 3e6694764f5acecc39b565fca3e9f4f6e17b6ee8 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 10:08:01 -0400 Subject: [PATCH 03/41] Show PostSettingsFormContentView --- .../Publishing/PublishPostViewController.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index d0dcc52a2ef3..02e98074ff94 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -8,8 +8,13 @@ import WordPressUI /// A screen shown just before publishing the post and allows you to change /// the post settings along with some publishing options like the publish date. final class PublishPostViewController: UIHostingController> { + private let settingsViewModel: PostSettingsViewModel + init(post: AbstractPost) { - super.init(rootView: NavigationView { PublishPostView() }) + // TODO: add isStandalone support + let settingsViewModel = PostSettingsViewModel(post: post, isStandalone: true) + self.settingsViewModel = settingsViewModel + super.init(rootView: NavigationView { PublishPostView(settingsViewModel: settingsViewModel) }) } required dynamic init?(coder aDecoder: NSCoder) { @@ -17,10 +22,13 @@ final class PublishPostViewController: UIHostingController Date: Wed, 17 Sep 2025 11:33:35 -0400 Subject: [PATCH 04/41] Add initial header view --- .../PrepublishingViewController.swift | 19 +------ .../PublishPostViewController.swift | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index aab75bde3050..c5879eb07674 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -567,21 +567,4 @@ private struct PrepublishingStackView: UIViewRepresentable { } } -private enum Strings { - static let publish = NSLocalizedString("prepublishing.publish", value: "Publish", comment: "Primary button label in the pre-publishing sheet") - static let schedule = NSLocalizedString("prepublishing.schedule", value: "Schedule", comment: "Primary button label in the pre-publishing shee") - static let publishDate = NSLocalizedString("prepublishing.publishDate", value: "Publish Date", comment: "Label for a cell in the pre-publishing sheet") - static let visibility = NSLocalizedString("prepublishing.visibility", value: "Visibility", comment: "Label for a cell in the pre-publishing sheet") - static let categories = NSLocalizedString("prepublishing.categories", value: "Categories", comment: "Label for a cell in the pre-publishing sheet") - static let tags = NSLocalizedString("prepublishing.tags", value: "Tags", comment: "Label for a cell in the pre-publishing sheet") - static let jetpackSocial = NSLocalizedString("prepublishing.jetpackSocial", value: "Jetpack Social", comment: "Label for a cell in the pre-publishing sheet") - static let immediately = NSLocalizedString("prepublishing.publishDateImmediately", value: "Immediately", comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected") - static let uploadingMedia = NSLocalizedString("prepublishing.uploadingMedia", value: "Uploading media", comment: "Title for a publish button state in the pre-publishing sheet") - private static let uploadMediaOneItemRemaining = NSLocalizedString("prepublishing.uploadMediaOneItemRemaining", value: "%@ item remaining", comment: "Details label for a publish button state in the pre-publishing sheet") - private static let uploadMediaManyItemsRemaining = NSLocalizedString("prepublishing.uploadMediaManyItemsRemaining", value: "%@ items remaining", comment: "Details label for a publish button state in the pre-publishing sheet") - static func uploadMediaRemaining(count: Int) -> String { - String(format: count == 1 ? Strings.uploadMediaOneItemRemaining : Strings.uploadMediaManyItemsRemaining, count.description) - } - static let mediaUploadFailedTitle = NSLocalizedString("prepublishing.mediaUploadFailedTitle", value: "Failed to upload media", comment: "Title for a publish button state in the pre-publishing sheet") - static let mediaUploadFailedDetailsMultipleFailures = NSLocalizedString("prepublishing.mediaUploadFailedDetails", value: "%@ items failed to upload", comment: "Details for a publish button state in the pre-publishing sheet; count as a parameter") -} +private typealias Strings = PrepublishingSheetStrings diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 02e98074ff94..72d37c033006 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -22,13 +22,66 @@ final class PublishPostViewController: UIHostingController String { + String(format: count == 1 ? Strings.uploadMediaOneItemRemaining : Strings.uploadMediaManyItemsRemaining, count.description) + } + static let mediaUploadFailedTitle = NSLocalizedString("prepublishing.mediaUploadFailedTitle", value: "Failed to upload media", comment: "Title for a publish button state in the pre-publishing sheet") + static let mediaUploadFailedDetailsMultipleFailures = NSLocalizedString("prepublishing.mediaUploadFailedDetails", value: "%@ items failed to upload", comment: "Details for a publish button state in the pre-publishing sheet; count as a parameter") +} + From 82566d0dc69ae09b3c9f1b32b6b0630596029a2b Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 11:47:11 -0400 Subject: [PATCH 05/41] Add temporary cancel and publish options --- .../PublishPostViewController.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 72d37c033006..8d95d68ae45e 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -34,6 +34,43 @@ struct PublishPostView: View { } .navigationTitle(Strings.title) .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + buttonCancel + } + ToolbarItem(placement: .navigationBarTrailing) { + buttonPublish + } + } + } + + private var buttonCancel: some View { + // TODO: connect to actual hasChanges + Button(SharedStrings.Button.cancel) { + if settingsViewModel.hasChanges { + // TODO: implement isShowingDiscardChangesAlert +// isShowingDiscardChangesAlert = true + } else { + settingsViewModel.buttonCancelTapped() + } + } + .tint(AppColor.tint) + } + + @ViewBuilder + private var buttonPublish: some View { + // TODO: connect to actual isSaving and save + if settingsViewModel.isSaving { + ProgressView() + } else { + // TODO: change dyncam to "Schedule" + Button(Strings.publish) { + settingsViewModel.buttonSaveTapped() + } + .buttonStyle(.borderedProminent) + .buttonBorderShape(.capsule) + .tint(AppColor.tint) + } } } From b99cca21699f41238c1ecdfea25ae1f89fd90c57 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 11:53:58 -0400 Subject: [PATCH 06/41] Move general section below --- .../ViewRelated/Post/PostSettings/PostSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index ef5caad326be..068605337a6e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -125,11 +125,11 @@ struct PostSettingsFormContentView: View { var body: some View { featuredImageSection - generalSection if viewModel.isPost { organizationSection } excerptSection + generalSection moreOptionsSection infoSection } From d11b2f7be33f60ed4a132de8ce5c34e2360773a7 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 12:07:07 -0400 Subject: [PATCH 07/41] Update PostSettigns to support .publishing context --- .../Post/PostSettings/PostSettingsView.swift | 10 ++++++++-- .../Post/PostSettings/PostSettingsViewModel.swift | 13 ++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 068605337a6e..46114ec9e784 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -209,7 +209,7 @@ struct PostSettingsFormContentView: View { private var generalSection: some View { Section { authorRow - if !viewModel.isDraftOrPending { + if !viewModel.isDraftOrPending || viewModel.context == .publishing { publishDateRow visibilityRow } @@ -249,7 +249,7 @@ struct PostSettingsFormContentView: View { } )) } label: { - SettingsRow(Strings.publishDateLabel, value: viewModel.publishDateText ?? "–") + SettingsRow(Strings.publishDateLabel, value: viewModel.publishDateText ?? Strings.immediately) } } @@ -574,4 +574,10 @@ private enum Strings { value: "ID", comment: "Label for the post ID field in Post Settings" ) + + static let immediately = NSLocalizedString( + "postSettings.publishDateImmediately", + value: "Immediately", + comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" + ) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 5b5144da7127..65c5eff0875a 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -8,6 +8,7 @@ import Combine final class PostSettingsViewModel: ObservableObject { let post: AbstractPost let isStandalone: Bool + let context: Context let featuredImageViewModel: PostSettingsFeaturedImageViewModel @Published var settings: PostSettings { @@ -111,9 +112,19 @@ final class PostSettingsViewModel: ObservableObject { /// This is temporary until we can fully migrate to SwiftUI navigation. weak var viewController: UIViewController? - init(post: AbstractPost, isStandalone: Bool = false) { + enum Context { + case settings + case publishing + } + + init( + post: AbstractPost, + isStandalone: Bool = false, + context: Context = .settings + ) { self.post = post self.isStandalone = isStandalone + self.context = context // Initialize settings from the post let initialSettings = PostSettings(from: post) From a00f718f52f274803c3c8f0b8d4518ee9210094f Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 13:22:52 -0400 Subject: [PATCH 08/41] Use primary style --- .../PublishPostViewController.swift | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 8d95d68ae45e..1391f63b249d 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -12,7 +12,11 @@ final class PublishPostViewController: UIHostingController Date: Wed, 17 Sep 2025 13:50:05 -0400 Subject: [PATCH 09/41] Add publishing section directly to PostSettingsView --- .../Post/PostSettings/PostSettingsView.swift | 31 ++++++++++++++++++- .../PublishPostViewController.swift | 30 ------------------ 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 46114ec9e784..879bd0f9962d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -124,6 +124,9 @@ struct PostSettingsFormContentView: View { @ObservedObject var viewModel: PostSettingsViewModel var body: some View { + if viewModel.context == .publishing { + publishingOptionsSection + } featuredImageSection if viewModel.isPost { organizationSection @@ -134,6 +137,17 @@ struct PostSettingsFormContentView: View { infoSection } + // MARK: - "Publishing Options" Section + @ViewBuilder + private var publishingOptionsSection: some View { + Section { + BlogListSiteView(site: .init(blog: viewModel.post.blog)) + publishDateRow + visibilityRow + } header: { + SectionHeader(Strings.publishingOptionsHeader) + } + } // MARK: - "Featured Image" Section @ViewBuilder @@ -209,7 +223,7 @@ struct PostSettingsFormContentView: View { private var generalSection: some View { Section { authorRow - if !viewModel.isDraftOrPending || viewModel.context == .publishing { + if !viewModel.isDraftOrPending { publishDateRow visibilityRow } @@ -580,4 +594,19 @@ private enum Strings { value: "Immediately", comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" ) + static let publishingOptionsHeader = NSLocalizedString( + "postSettings.publishing.header", + value: "Publishing", + comment: "Section header for Publishing Options in Post Settings" + ) + static let publishingTo = NSLocalizedString( + "postSettings.publishingTo", + value: "Publishing to", + comment: "Label indicating which site you are publishing to" + ) + static let previewLabel = NSLocalizedString( + "postSettings.preview.label", + value: "Preview", + comment: "Label for the preview button in Post Settings" + ) } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 1391f63b249d..d9c08b726359 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -36,12 +36,8 @@ struct PublishPostView: View { Form { PostSettingsFormContentView(viewModel: settingsViewModel) } - .navigationTitle(Strings.title) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .title) { - PublishingHeaderView(blog: post.blog) - } ToolbarItem(placement: .navigationBarLeading) { buttonCancel } @@ -89,32 +85,6 @@ struct PublishPostView: View { } } -private struct PublishingHeaderView: View { - let blog: Blog - - var body: some View { -// HStack(alignment: .center, spacing: 12) { - VStack(alignment: .center, spacing: 3) { - Text(Strings.title) - .font(.headline) - - HStack(alignment: .center, spacing: 4) { - SiteIconView(viewModel: SiteIconViewModel(blog: blog, size: .regular)) - .frame(width: 20, height: 20) - Text(blog.title ?? "") - .font(.subheadline) - .foregroundStyle(.secondary) - .lineLimit(1) - } - } - -// Spacer() -// } -// .listRowBackground(Color.clear) -// .listRowInsets(EdgeInsets.zero) - } -} - private typealias Strings = PrepublishingSheetStrings enum PrepublishingSheetStrings { From 91ee556aee6beefc4652ac86ac4f7bbdd303a40e Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 14:04:59 -0400 Subject: [PATCH 10/41] Improve the publishing sheet design --- Modules/Sources/WordPressUI/Views/SiteIconView.swift | 6 +++++- .../ViewRelated/Post/PostSettings/PostSettingsView.swift | 5 +++-- .../PostSettings/Views/PostSettingsFeaturedImageRow.swift | 2 +- .../Post/Publishing/PublishPostViewController.swift | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Modules/Sources/WordPressUI/Views/SiteIconView.swift b/Modules/Sources/WordPressUI/Views/SiteIconView.swift index 9816b8a89703..9a7b7359cb02 100644 --- a/Modules/Sources/WordPressUI/Views/SiteIconView.swift +++ b/Modules/Sources/WordPressUI/Views/SiteIconView.swift @@ -13,7 +13,11 @@ public struct SiteIconView: View { public var body: some View { contents - .clipShape(RoundedRectangle(cornerRadius: 6)) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + } + + private var cornerRadius: CGFloat { + if #available(iOS 26, *) { 12 } else { 6 } } @ViewBuilder diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 879bd0f9962d..67323dab3455 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -138,16 +138,17 @@ struct PostSettingsFormContentView: View { } // MARK: - "Publishing Options" Section + @ViewBuilder private var publishingOptionsSection: some View { Section { BlogListSiteView(site: .init(blog: viewModel.post.blog)) + .listRowSeparator(.hidden, edges: .bottom) publishDateRow visibilityRow - } header: { - SectionHeader(Strings.publishingOptionsHeader) } } + // MARK: - "Featured Image" Section @ViewBuilder diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift index feb55d997441..1670f484ed0b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift @@ -8,7 +8,7 @@ struct PostSettingsFeaturedImageRow: View { @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel @State private var presentedMedia: Media? - @ScaledMetric(relativeTo: .body) var height = 120 + @ScaledMetric(relativeTo: .body) var height = 110 var body: some View { Group { diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index d9c08b726359..ab2fc9262c83 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -36,6 +36,7 @@ struct PublishPostView: View { Form { PostSettingsFormContentView(viewModel: settingsViewModel) } + .environment(\.defaultMinListHeaderHeight, 0) // Reduces top inset a bit .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { From 63fd4c7de900f1b14a297676ba332197763f94c6 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 14:07:03 -0400 Subject: [PATCH 11/41] Increase SiteIcon corner radius --- Modules/Sources/WordPressUI/Views/SiteIconView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Sources/WordPressUI/Views/SiteIconView.swift b/Modules/Sources/WordPressUI/Views/SiteIconView.swift index 9a7b7359cb02..3d7cec0444bf 100644 --- a/Modules/Sources/WordPressUI/Views/SiteIconView.swift +++ b/Modules/Sources/WordPressUI/Views/SiteIconView.swift @@ -17,7 +17,7 @@ public struct SiteIconView: View { } private var cornerRadius: CGFloat { - if #available(iOS 26, *) { 12 } else { 6 } + if #available(iOS 26, *) { 10 } else { 6 } } @ViewBuilder From bd140e87216d4653569bdb1f0ceed5f6b00b3caf Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 14:15:11 -0400 Subject: [PATCH 12/41] Add preview button (empty) --- .../Post/Publishing/PublishPostViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index ab2fc9262c83..c896dd8bb2f6 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -42,6 +42,13 @@ struct PublishPostView: View { ToolbarItem(placement: .navigationBarLeading) { buttonCancel } + ToolbarItem(placement: .navigationBarTrailing) { + Button { + // TODO: implement preview + } label: { + Image(systemName: "safari") + } + } ToolbarItem(placement: .navigationBarTrailing) { buttonPublish } From 3aad4544652166090f0489987c18992f30ae58b7 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 14:21:22 -0400 Subject: [PATCH 13/41] Add top icons --- .../Utility/BuildInformation/FeatureFlag.swift | 2 +- .../Post/Publishing/PublishPostViewController.swift | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 140fbc9255b4..6b5a5011170e 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -84,7 +84,7 @@ public enum FeatureFlag: Int, CaseIterable { case .newStats: return false case .newPublishingSheet: - return true + return BuildConfiguration.current == .debug } } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index c896dd8bb2f6..ac89e705caeb 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -31,7 +31,7 @@ struct PublishPostView: View { var post: AbstractPost { settingsViewModel.post } - // TODO: figure out the media upload situation + // TODO: (publish) figure out the media upload situation var body: some View { Form { PostSettingsFormContentView(viewModel: settingsViewModel) @@ -39,17 +39,15 @@ struct PublishPostView: View { .environment(\.defaultMinListHeaderHeight, 0) // Reduces top inset a bit .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { + ToolbarItem(placement: .topBarLeading) { buttonCancel } - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItemGroup(placement: .topBarTrailing) { Button { - // TODO: implement preview + // TODO: (publish) show preview } label: { Image(systemName: "safari") } - } - ToolbarItem(placement: .navigationBarTrailing) { buttonPublish } } @@ -57,7 +55,7 @@ struct PublishPostView: View { @ViewBuilder private var buttonCancel: some View { - // TODO: connect to actual hasChanges + // TODO: (publish) connect to actual hasChanges if #available(iOS 26, *) { Button(role: .cancel, action: buttonCancelTapped) } else { From 5158a32e6a3e7bbbe6c6e44253ed14ca961ed710 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 14:35:11 -0400 Subject: [PATCH 14/41] Implement publishing --- .../Classes/Services/PostCoordinator.swift | 40 ++++++++++++++++ .../PostSettings/PostSettingsViewModel.swift | 21 +++++++++ .../PublishPostViewController.swift | 47 ++++++++++++++----- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 4db753cd5955..b3874c21e0c6 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -89,6 +89,9 @@ class PostCoordinator: NSObject { /// /// - warning: Before publishing, ensure that the media for the post got /// uploaded. Managing media is not the responsibility of `PostRepository.` + /// + /// - parameter changes: The set of changes apply to the post together + /// with the publishing options. @MainActor func publish(_ post: AbstractPost, options: PublishingOptions) async throws { wpAssert(post.isOriginal()) @@ -128,6 +131,43 @@ class PostCoordinator: NSObject { } } + /// Publishes the post according to the current settings and user capabilities. + /// + /// - warning: Before publishing, ensure that the media for the post got + /// uploaded. Managing media is not the responsibility of `PostRepository.` + /// + /// - parameter changes: The set of changes apply to the post together + /// with the publishing options. + @MainActor + func publish_v2(_ post: AbstractPost, parameters: RemotePostUpdateParameters) 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 + } + } + @MainActor private func didPublish(_ post: AbstractPost) { if post.status == .scheduled { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 65c5eff0875a..dca4ffb03d78 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -226,6 +226,27 @@ final class PostSettingsViewModel: ObservableObject { } } + func buttonPublishTapped() { + // Check if the post still exists + guard let context = post.managedObjectContext, + let _ = try? context.existingObject(with: post.objectID) else { + isShowingDeletedAlert = true + return + } + + isSaving = true + Task { + do { + let coordinator = PostCoordinator.shared + let changes = settings.makeUpdateParameters(from: post) + try await coordinator.publish_v2(post.original(), parameters: changes) + } catch { + isSaving = false + // `PostCoordinator` handles errors by showing an alert when needed + } + } + } + private func didSaveChanges() { trackChanges(from: originalSettings, to: settings) } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index ac89e705caeb..0529a502bf0e 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -8,33 +8,46 @@ import WordPressUI /// A screen shown just before publishing the post and allows you to change /// the post settings along with some publishing options like the publish date. final class PublishPostViewController: UIHostingController> { - private let settingsViewModel: PostSettingsViewModel + private let viewModel: PostSettingsViewModel + // TODO: add isShowingDeletedAlert init(post: AbstractPost) { // TODO: add isStandalone support - let settingsViewModel = PostSettingsViewModel( + let viewModel = PostSettingsViewModel( post: post, isStandalone: true, context: .publishing ) - self.settingsViewModel = settingsViewModel - super.init(rootView: NavigationView { PublishPostView(settingsViewModel: settingsViewModel) }) + self.viewModel = viewModel + super.init(rootView: NavigationView { PublishPostView(viewModel: viewModel) }) } required dynamic init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + + viewModel.onDismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: true) + } + + // Set the view controller reference for navigation + // This is temporary until we can fully migrate to SwiftUI navigation + viewModel.viewController = self + } } struct PublishPostView: View { - @ObservedObject var settingsViewModel: PostSettingsViewModel + @ObservedObject var viewModel: PostSettingsViewModel - var post: AbstractPost { settingsViewModel.post } + var post: AbstractPost { viewModel.post } // TODO: (publish) figure out the media upload situation var body: some View { Form { - PostSettingsFormContentView(viewModel: settingsViewModel) + PostSettingsFormContentView(viewModel: viewModel) } .environment(\.defaultMinListHeaderHeight, 0) // Reduces top inset a bit .navigationBarTitleDisplayMode(.inline) @@ -51,8 +64,19 @@ struct PublishPostView: View { buttonPublish } } + .interactiveDismissDisabled(viewModel.isSaving || viewModel.hasChanges) + .alert(viewModel.deletedAlertTitle, isPresented: $viewModel.isShowingDeletedAlert) { + Button(SharedStrings.Button.ok) { + viewModel.onDismiss?() + } + } message: { + Text(viewModel.deletedAlertMessage) + } + .disabled(viewModel.isSaving) } + // MARK: – Actions + @ViewBuilder private var buttonCancel: some View { // TODO: (publish) connect to actual hasChanges @@ -65,23 +89,22 @@ struct PublishPostView: View { } private func buttonCancelTapped() { - if settingsViewModel.hasChanges { + if viewModel.hasChanges { // TODO: implement isShowingDiscardChangesAlert // isShowingDiscardChangesAlert = true } else { - settingsViewModel.buttonCancelTapped() + viewModel.buttonCancelTapped() } } @ViewBuilder private var buttonPublish: some View { - // TODO: connect to actual isSaving and save - if settingsViewModel.isSaving { + if viewModel.isSaving { ProgressView() } else { // TODO: change dyncam to "Schedule" Button(Strings.publish) { - settingsViewModel.buttonSaveTapped() + viewModel.buttonPublishTapped() } .fontWeight(.medium) .buttonStyle(.borderedProminent) From c33eea5018770dd905fa31f94c5770ad8c030f37 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:09:11 -0400 Subject: [PATCH 15/41] Add PrepublishingSheetResult support --- .../Post/PostSettings/PostSettingsViewModel.swift | 2 ++ .../Prepublishing/PrepublishingViewController+Helpers.swift | 1 + .../Post/Publishing/PublishPostViewController.swift | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index dca4ffb03d78..5d6892308383 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -107,6 +107,7 @@ final class PostSettingsViewModel: ObservableObject { var onDismiss: (() -> Void)? var onEditorPostSaved: (() -> Void)? + var onPostPublished: (() -> Void)? /// Weak reference to the view controller for navigation. /// This is temporary until we can fully migrate to SwiftUI navigation. @@ -240,6 +241,7 @@ final class PostSettingsViewModel: ObservableObject { let coordinator = PostCoordinator.shared let changes = settings.makeUpdateParameters(from: post) try await coordinator.publish_v2(post.original(), parameters: changes) + onPostPublished?() } catch { isSaving = false // `PostCoordinator` handles errors by showing an alert when needed diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index a40eca513244..c3524401b3dc 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -13,6 +13,7 @@ extension PrepublishingViewController { } let publishVC = PublishPostViewController(post: revision) + publishVC.onCompletion = completion publishVC.sheetPresentationController?.detents = [.medium(), .large()] presentingViewController.present(publishVC, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 0529a502bf0e..a5b19d50ed43 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -10,6 +10,8 @@ import WordPressUI final class PublishPostViewController: UIHostingController> { private let viewModel: PostSettingsViewModel + var onCompletion: ((PrepublishingSheetResult) -> Void)? + // TODO: add isShowingDeletedAlert init(post: AbstractPost) { // TODO: add isStandalone support @@ -29,6 +31,9 @@ final class PublishPostViewController: UIHostingController Date: Wed, 17 Sep 2025 15:14:00 -0400 Subject: [PATCH 16/41] Make it possible to keep changes to the post --- .../PrepublishingViewController+Helpers.swift | 2 +- .../PublishPostViewController.swift | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index c3524401b3dc..2b9050c8d334 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -12,7 +12,7 @@ extension PrepublishingViewController { return } - let publishVC = PublishPostViewController(post: revision) + let publishVC = PublishPostViewController(post: revision, isStandalone: isStandalone) publishVC.onCompletion = completion publishVC.sheetPresentationController?.detents = [.medium(), .large()] presentingViewController.present(publishVC, animated: true) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index a5b19d50ed43..4301282b999e 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -12,12 +12,10 @@ final class PublishPostViewController: UIHostingController Void)? - // TODO: add isShowingDeletedAlert - init(post: AbstractPost) { - // TODO: add isStandalone support + init(post: AbstractPost, isStandalone: Bool) { let viewModel = PostSettingsViewModel( post: post, - isStandalone: true, + isStandalone: isStandalone, context: .publishing ) self.viewModel = viewModel @@ -47,6 +45,8 @@ final class PublishPostViewController: UIHostingController Date: Wed, 17 Sep 2025 15:40:18 -0400 Subject: [PATCH 17/41] Fix confirmation dialog savings --- .../Post/PostSettings/PostSettingsView.swift | 26 ++++++++++--------- .../PostSettings/PostSettingsViewModel.swift | 16 +++++++++++- .../PublishPostViewController.swift | 20 +++++++------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 67323dab3455..90989b27e045 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -55,10 +55,21 @@ private struct PostSettingsView: View { .accessibilityIdentifier("post_settings_form") .disabled(viewModel.isSaving) .toolbar { - ToolbarItem(placement: .navigationBarLeading) { + ToolbarItem(placement: .topBarLeading) { buttonCancel + .confirmationDialog(Strings.discardChangesTitle, isPresented: $isShowingDiscardChangesAlert) { + Button(Strings.discardChangesButton, role: .destructive) { + viewModel.buttonCancelTapped() + } + Button(SharedStrings.Button.cancel, role: .cancel) { + // Do nothing - continue editing + } + } message: { + Text(Strings.discardChangesMessage) + } } - ToolbarItem(placement: .navigationBarTrailing) { + + ToolbarItem(placement: .topBarTrailing) { buttonSave } } @@ -70,16 +81,6 @@ private struct PostSettingsView: View { } message: { Text(viewModel.deletedAlertMessage) } - .confirmationDialog(Strings.discardChangesTitle, isPresented: $isShowingDiscardChangesAlert) { - Button(Strings.discardChangesButton, role: .destructive) { - viewModel.buttonCancelTapped() - } - Button(SharedStrings.Button.cancel, role: .cancel) { - // Do nothing - continue editing - } - } message: { - Text(Strings.discardChangesMessage) - } } private var buttonCancel: some View { @@ -92,6 +93,7 @@ private struct PostSettingsView: View { } .tint(AppColor.tint) .accessibilityIdentifier("post_settings_cancel_button") + } @ViewBuilder diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 5d6892308383..b9fe26af7f33 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -149,7 +149,7 @@ final class PostSettingsViewModel: ObservableObject { } private func refresh(from old: PostSettings, to new: PostSettings) { - hasChanges = new != originalSettings + hasChanges = getSettingsToSave(for: new) != originalSettings if old.categoryIDs != new.categoryIDs { refreshDisplayedCategories() @@ -209,6 +209,7 @@ final class PostSettingsViewModel: ObservableObject { private func actuallySave() async { do { + let settings = getSettingsToSave(for: self.settings) let coordinator = PostCoordinator.shared if coordinator.isSyncAllowed(for: post) { let revision = post.createRevision() @@ -227,6 +228,19 @@ final class PostSettingsViewModel: ObservableObject { } } + func getSettingsToSave(for settings: PostSettings) -> PostSettings { + var settings = settings + if context == .publishing { + // We don't support saving these changes on the "Publishing" sheet + // as it would trigger the change in status and publishing. We'll + // only save what we can without publishing: tags, categories, etc. + settings.status = originalSettings.status + settings.password = originalSettings.password + settings.publishDate = originalSettings.publishDate + } + return settings + } + func buttonPublishTapped() { // Check if the post still exists guard let context = post.managedObjectContext, diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 4301282b999e..00e86fbfd829 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -59,6 +59,16 @@ struct PublishPostView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { buttonCancel + .confirmationDialog(Strings.discardChangesTitle, isPresented: $isShowingDiscardChangesAlert) { + Button(Strings.discardChangesButton, role: .destructive) { + viewModel.buttonCancelTapped() + } + Button(SharedStrings.Button.save) { + viewModel.buttonSaveTapped() + } + } message: { + Text(Strings.discardChangesMessage) + } } ToolbarItemGroup(placement: .topBarTrailing) { Button { @@ -77,16 +87,6 @@ struct PublishPostView: View { } message: { Text(viewModel.deletedAlertMessage) } - .confirmationDialog(Strings.discardChangesTitle, isPresented: $isShowingDiscardChangesAlert) { - Button(Strings.discardChangesButton, role: .destructive) { - viewModel.buttonCancelTapped() - } - Button(SharedStrings.Button.save, role: .cancel) { - viewModel.buttonSaveTapped() - } - } message: { - Text(Strings.discardChangesMessage) - } .disabled(viewModel.isSaving) } From 8957da298d6f557bce7a9710ef1688651c0fd05f Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:44:46 -0400 Subject: [PATCH 18/41] Fix save not showing --- .../Publishing/PublishPostViewController.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 00e86fbfd829..3e3d2cd9a317 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -60,12 +60,14 @@ struct PublishPostView: View { ToolbarItem(placement: .topBarLeading) { buttonCancel .confirmationDialog(Strings.discardChangesTitle, isPresented: $isShowingDiscardChangesAlert) { + Button(Strings.saveChangesButton) { + viewModel.buttonSaveTapped() + } + // - warning: It's important for the destructive button to + // be at the bottom or "Save" will not be shown Button(Strings.discardChangesButton, role: .destructive) { viewModel.buttonCancelTapped() } - Button(SharedStrings.Button.save) { - viewModel.buttonSaveTapped() - } } message: { Text(Strings.discardChangesMessage) } @@ -166,4 +168,10 @@ enum PrepublishingSheetStrings { value: "Discard Changes", comment: "Button to confirm discarding changes" ) + + static let saveChangesButton = NSLocalizedString( + "prepublishing.saveChanges.button", + value: "Save Changes", + comment: "Button to confirm discarding changes" + ) } From 1a0e244a4d528fbf59f38ac3b64fbc0840477165 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:49:53 -0400 Subject: [PATCH 19/41] Show spinner loading separately --- .../Post/Publishing/PublishPostViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 3e3d2cd9a317..0e99895d9288 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -49,7 +49,6 @@ struct PublishPostView: View { var post: AbstractPost { viewModel.post } - // TODO: (publish) figure out the media upload situation var body: some View { Form { PostSettingsFormContentView(viewModel: viewModel) @@ -72,12 +71,14 @@ struct PublishPostView: View { Text(Strings.discardChangesMessage) } } - ToolbarItemGroup(placement: .topBarTrailing) { + ToolbarItem(placement: .topBarTrailing) { Button { // TODO: (publish) show preview } label: { Image(systemName: "safari") } + } + ToolbarItem(placement: .topBarTrailing) { buttonPublish } } From fea38bfbbbccc3f5173e0f1a6d4179fa47644fc5 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:50:38 -0400 Subject: [PATCH 20/41] Fix navigation for tags and categories --- .../Prepublishing/PrepublishingViewController+Helpers.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index 2b9050c8d334..ab14224479b6 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -14,7 +14,9 @@ extension PrepublishingViewController { let publishVC = PublishPostViewController(post: revision, isStandalone: isStandalone) publishVC.onCompletion = completion - publishVC.sheetPresentationController?.detents = [.medium(), .large()] - presentingViewController.present(publishVC, animated: true) + // - warning: Has to be UIKit because some of the `PostSettingsView` rows rely on it. + let navigationVC = UINavigationController(rootViewController: publishVC) + navigationVC.sheetPresentationController?.detents = [.medium(), .large()] + presentingViewController.present(navigationVC, animated: true) } } From e26d8c3ad495ec164e59d1c4786c1bc1d18c2d9b Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:53:29 -0400 Subject: [PATCH 21/41] Update publish button title dynamically --- .../Post/Publishing/PublishPostViewController.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 0e99895d9288..9e20aedf3e5c 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -118,8 +118,7 @@ struct PublishPostView: View { if viewModel.isSaving { ProgressView() } else { - // TODO: change dyncam to "Schedule" - Button(Strings.publish) { + Button(viewModel.publishButtonTitle) { viewModel.buttonPublishTapped() } .fontWeight(.medium) @@ -130,6 +129,13 @@ struct PublishPostView: View { } } +private extension PostSettingsViewModel { + var publishButtonTitle: String { + let isScheduled = settings.publishDate.map { $0 > .now } ?? false + return isScheduled ? Strings.schedule : Strings.publish + } +} + private typealias Strings = PrepublishingSheetStrings enum PrepublishingSheetStrings { From b48cd9652bbe09ea9a46e7b9d84b331a9f3daa22 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 17 Sep 2025 15:55:18 -0400 Subject: [PATCH 22/41] Remove extra navigation view --- .../Post/Publishing/PublishPostViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 9e20aedf3e5c..0d913acdb953 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -7,7 +7,7 @@ import WordPressUI /// A screen shown just before publishing the post and allows you to change /// the post settings along with some publishing options like the publish date. -final class PublishPostViewController: UIHostingController> { +final class PublishPostViewController: UIHostingController { private let viewModel: PostSettingsViewModel var onCompletion: ((PrepublishingSheetResult) -> Void)? @@ -19,7 +19,7 @@ final class PublishPostViewController: UIHostingController Date: Sun, 21 Sep 2025 08:15:05 -0400 Subject: [PATCH 23/41] Add initial social sharing --- .../PostSettings/PostSettingsViewModel.swift | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index b9fe26af7f33..6085fe642f60 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import BuildSettingsKit import WordPressData import WordPressKit import WordPressShared @@ -22,6 +23,7 @@ final class PostSettingsViewModel: ObservableObject { @Published private(set) var displayedCategories: [String] = [] @Published private(set) var displayedTags: [String] = [] @Published private(set) var parentPageText: String? + @Published private(set) var socialSharingState: SocialSharingRowState = .hidden @Published var isShowingDeletedAlert = false @@ -102,7 +104,16 @@ final class PostSettingsViewModel: ObservableObject { return postID } + enum SocialSharingRowState { + /// The initial prompt to set up connections. + case setup + /// The site has existing connections. + case connected + case hidden + } + private let originalSettings: PostSettings + private let preferences: UserPersistentRepository private var cancellables = Set() var onDismiss: (() -> Void)? @@ -121,11 +132,13 @@ final class PostSettingsViewModel: ObservableObject { init( post: AbstractPost, isStandalone: Bool = false, - context: Context = .settings + context: Context = .settings, + preferences: UserPersistentRepository = UserDefaults.standard ) { self.post = post self.isStandalone = isStandalone self.context = context + self.preferences = preferences // Initialize settings from the post let initialSettings = PostSettings(from: post) @@ -144,10 +157,13 @@ final class PostSettingsViewModel: ObservableObject { refreshDisplayedCategories() refreshDisplayedTags() refreshParentPageText() + refreshSocialSharingState() WPAnalytics.track(.postSettingsShown) } + // MARK: - Refresh + private func refresh(from old: PostSettings, to new: PostSettings) { hasChanges = getSettingsToSave(for: new) != originalSettings @@ -180,6 +196,29 @@ final class PostSettingsViewModel: ObservableObject { } } + private func refreshSocialSharingState() { + guard BuildSettings.current.brand == .jetpack && + RemoteFeatureFlag.jetpackSocialImprovements.enabled() && + post.status != .publishPrivate && + !getPublicizeServices().isEmpty && + post.blog.supportsPublicize() else { + socialSharingState = .hidden + return + } + + if (post.blog.connections ?? []).isEmpty && { + socialSharingState = isSocialConnectionSetupDismissed ? .hidden : .setup + } else { + socialSharingState = .connected + } + } + + private func getPublicizeServices() -> [PublicizeService] { + try? PublicizeService.allPublicizeServices(in: ContextManager.shared.mainContext) ?? [] + } + + // MARK: - Actions + func buttonCancelTapped() { onDismiss?() } @@ -343,6 +382,30 @@ final class PostSettingsViewModel: ObservableObject { private func track(_ event: WPAnalyticsEvent) { WPAnalytics.track(event, properties: ["via": "settings"]) } + + // MARK: - Helpers + + /// Convenience variable representing whether the No Connection view has been dismissed. + /// Note: the value is stored per site. + private var isSocialConnectionSetupDismissed: Bool { + get { + guard let blogID = post.blog.dotComID?.intValue, + let dictionary = preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool], + let value = dictionary["\(blogID)"] else { + return false + } + return value + } + + set { + guard let blogID = post.blog.dotComID?.intValue else { + return wpAssertionFailure("blogID missing") + } + var dictionary = (persistentStore.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool]) ?? .init() + dictionary["\(postBlogID)"] = newValue + preferences.set(dictionary, forKey: Constants.noConnectionKey) + } + } } // MARK: - Localized Strings @@ -384,3 +447,7 @@ private enum Strings { comment: "Message when trying to save a deleted page" ) } + +private enum Constants { + static let noConnectionKey = "prepublishing-social-no-connection-view-hidden" +} From 957d1bc7939bb250fd997b4b4761900bee668f21 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 13:03:38 -0400 Subject: [PATCH 24/41] Show social section separately --- .../JetpackSocialNoConnectionView.swift | 2 +- .../Post/PostSettings/PostSettingsView.swift | 40 ++++- .../PostSettings/PostSettingsViewModel.swift | 158 ++++++++++++------ .../Views/PostSettingsFeaturedImageRow.swift | 4 - 4 files changed, 147 insertions(+), 57 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift index 62b63f36aec4..4cecd30b94c0 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift @@ -5,7 +5,7 @@ import WordPressUI struct JetpackSocialNoConnectionView: View { - private let viewModel: JetpackSocialNoConnectionViewModel + let viewModel: JetpackSocialNoConnectionViewModel var body: some View { VStack(alignment: .leading, spacing: 12.0) { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 90989b27e045..2cf335177cd5 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -134,9 +134,14 @@ struct PostSettingsFormContentView: View { organizationSection } excerptSection + if viewModel.context == .publishing { + socialSharingSection + } generalSection moreOptionsSection - infoSection + if viewModel.context != .publishing { + infoSection + } } // MARK: - "Publishing Options" Section @@ -144,10 +149,12 @@ struct PostSettingsFormContentView: View { @ViewBuilder private var publishingOptionsSection: some View { Section { - BlogListSiteView(site: .init(blog: viewModel.post.blog)) - .listRowSeparator(.hidden, edges: .bottom) publishDateRow visibilityRow + } header: { + BlogListSiteView(site: .init(blog: viewModel.post.blog)) + .padding(.bottom, 8) + .foregroundStyle(.primary) } } @@ -284,6 +291,24 @@ struct PostSettingsFormContentView: View { } } + // MARK: - "Social Sharing" Section + + @ViewBuilder + private var socialSharingSection: some View { + if let state = viewModel.socialSharingState { + Section { + switch state { + case .setup(let viewModel): + JetpackSocialNoConnectionView(viewModel: viewModel) + case .connected: + Text("Connected (not implemented)") + } + } header: { + SectionHeader(Strings.socialSharing) + } + } + } + // MARK: - "More Options" Section /// The least-used options. @@ -597,19 +622,28 @@ private enum Strings { value: "Immediately", comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" ) + static let publishingOptionsHeader = NSLocalizedString( "postSettings.publishing.header", value: "Publishing", comment: "Section header for Publishing Options in Post Settings" ) + static let publishingTo = NSLocalizedString( "postSettings.publishingTo", value: "Publishing to", comment: "Label indicating which site you are publishing to" ) + static let previewLabel = NSLocalizedString( "postSettings.preview.label", value: "Preview", comment: "Label for the preview button in Post Settings" ) + + static let socialSharing = NSLocalizedString( + "postSettings.socialSharing.header", + value: "Social Sharing", + comment: "Label for the preview button in Post Settings" + ) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 6085fe642f60..bf802e5956c1 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -23,7 +23,7 @@ final class PostSettingsViewModel: ObservableObject { @Published private(set) var displayedCategories: [String] = [] @Published private(set) var displayedTags: [String] = [] @Published private(set) var parentPageText: String? - @Published private(set) var socialSharingState: SocialSharingRowState = .hidden + @Published private(set) var socialSharingState: SocialSharingSectionState? @Published var isShowingDeletedAlert = false @@ -104,12 +104,11 @@ final class PostSettingsViewModel: ObservableObject { return postID } - enum SocialSharingRowState { + enum SocialSharingSectionState { /// The initial prompt to set up connections. - case setup + case setup(JetpackSocialNoConnectionViewModel) /// The site has existing connections. case connected - case hidden } private let originalSettings: PostSettings @@ -196,27 +195,6 @@ final class PostSettingsViewModel: ObservableObject { } } - private func refreshSocialSharingState() { - guard BuildSettings.current.brand == .jetpack && - RemoteFeatureFlag.jetpackSocialImprovements.enabled() && - post.status != .publishPrivate && - !getPublicizeServices().isEmpty && - post.blog.supportsPublicize() else { - socialSharingState = .hidden - return - } - - if (post.blog.connections ?? []).isEmpty && { - socialSharingState = isSocialConnectionSetupDismissed ? .hidden : .setup - } else { - socialSharingState = .connected - } - } - - private func getPublicizeServices() -> [PublicizeService] { - try? PublicizeService.allPublicizeServices(in: ContextManager.shared.mainContext) ?? [] - } - // MARK: - Actions func buttonCancelTapped() { @@ -322,6 +300,112 @@ final class PostSettingsViewModel: ObservableObject { settings.password = selection.password.isEmpty ? nil : selection.password } + // MARK: - Social Sharing + + private func refreshSocialSharingState() { + guard BuildSettings.current.brand == .jetpack && + RemoteFeatureFlag.jetpackSocialImprovements.enabled() && + post.status != .publishPrivate && + !getPublicizeServices().isEmpty && + post.blog.supportsPublicize() else { + socialSharingState = nil + return + } + + if (post.blog.connections ?? []).isEmpty { + if isSocialConnectionSetupDismissed { + socialSharingState = nil + } else { + socialSharingState = .setup(makeSocialSharingSetupViewModel()) + } + } else { + socialSharingState = .connected + } + + // TEMP: + socialSharingState = .setup(makeSocialSharingSetupViewModel()) + } + + private func getPublicizeServices() -> [PublicizeService] { + let context = ContextManager.shared.mainContext + return (try? PublicizeService.allPublicizeServices(in: context)) ?? [] + } + + /// Convenience variable representing whether the No Connection view has been dismissed. + /// Note: the value is stored per site. + private var isSocialConnectionSetupDismissed: Bool { + get { + guard let blogID = post.blog.dotComID?.intValue, + let dictionary = preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool], + let value = dictionary["\(blogID)"] else { + return false + } + return value + } + + set { + guard let blogID = post.blog.dotComID?.intValue else { + return wpAssertionFailure("blogID missing") + } + var dictionary = (preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool]) ?? .init() + dictionary["\(blogID)"] = newValue + preferences.set(dictionary, forKey: Constants.noConnectionKey) + } + } + + private func makeSocialSharingSetupViewModel() -> JetpackSocialNoConnectionViewModel { + JetpackSocialNoConnectionViewModel( + services: getPublicizeServices(), + padding: .zero, + onConnectTap: { [weak self] in + // TODO: + }, + onNotNowTap: { [weak self] in + // TODO: + } + ) + } + +// /// A closure to be executed when the Connect button is tapped in the No Connection view. +// func noConnectionConnectTapped() -> () -> Void { +// return { [weak self] in +// guard let self, +// let controller = SharingViewController(blog: self.post.blog, delegate: self), +// self.presentedViewController == nil else { +// return +// } +// +// WPAnalytics.track(.jetpackSocialNoConnectionCTATapped, properties: ["source": Constants.trackingSource]) +// +// let navigationController = UINavigationController(rootViewController: controller) +// self.show(navigationController, sender: nil) +// } +// } +// +// /// A closure to be executed when the "Not now" button is tapped in the No Connection view. +// func noConnectionDismissTapped() -> () -> Void { +// return { [weak self] in +// guard let self, +// let autoSharingRowIndex = options.firstIndex(where: { $0.id == .autoSharing }) else { +// return +// } +// +// WPAnalytics.track(.jetpackSocialNoConnectionCardDismissed, properties: ["source": Constants.trackingSource]) +// +// self.isNoConnectionDismissed = true +// self.refreshOptions() +// +// // ensure that the `.autoSharing` identifier is truly removed to prevent table updates from crashing. +// guard options.firstIndex(where: { $0.id == .autoSharing }) == nil else { +// return +// } +// +// self.tableView.performBatchUpdates { +// self.tableView.deleteRows(at: [.init(row: autoSharingRowIndex, section: .zero)], with: .fade) +// } completion: { _ in } +// } +// } + // MARK: - Navigation func showCategoriesPicker() { @@ -382,30 +466,6 @@ final class PostSettingsViewModel: ObservableObject { private func track(_ event: WPAnalyticsEvent) { WPAnalytics.track(event, properties: ["via": "settings"]) } - - // MARK: - Helpers - - /// Convenience variable representing whether the No Connection view has been dismissed. - /// Note: the value is stored per site. - private var isSocialConnectionSetupDismissed: Bool { - get { - guard let blogID = post.blog.dotComID?.intValue, - let dictionary = preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool], - let value = dictionary["\(blogID)"] else { - return false - } - return value - } - - set { - guard let blogID = post.blog.dotComID?.intValue else { - return wpAssertionFailure("blogID missing") - } - var dictionary = (persistentStore.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool]) ?? .init() - dictionary["\(postBlogID)"] = newValue - preferences.set(dictionary, forKey: Constants.noConnectionKey) - } - } } // MARK: - Localized Strings diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift index 1670f484ed0b..17f0ad44e8c3 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsFeaturedImageRow.swift @@ -142,10 +142,6 @@ struct PostSettingsFeaturedImageRow: View { RoundedRectangle(cornerRadius: cornerRadius) .fill(Color(UIColor.secondarySystemGroupedBackground)) - // Very subtle accent tint - RoundedRectangle(cornerRadius: cornerRadius) - .fill(Color.accentColor.opacity(0.02)) - content() // Prominent border From 02a9b8ff5fdfe811718407d9a544608609f8cf0d Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 13:26:33 -0400 Subject: [PATCH 25/41] Implement setup view --- .../Post/PostSettings/PostSettingsView.swift | 2 +- .../PostSettings/PostSettingsViewModel.swift | 85 ++++++++----------- 2 files changed, 36 insertions(+), 51 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 2cf335177cd5..d9672245da3a 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -134,7 +134,7 @@ struct PostSettingsFormContentView: View { organizationSection } excerptSection - if viewModel.context == .publishing { + if viewModel.isPost, viewModel.context == .publishing { socialSharingSection } generalSection diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index bf802e5956c1..789ea74b61eb 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -1,5 +1,6 @@ import Foundation import BuildSettingsKit +import SwiftUI import WordPressData import WordPressKit import WordPressShared @@ -321,9 +322,6 @@ final class PostSettingsViewModel: ObservableObject { } else { socialSharingState = .connected } - - // TEMP: - socialSharingState = .setup(makeSocialSharingSetupViewModel()) } private func getPublicizeServices() -> [PublicizeService] { @@ -357,54 +355,28 @@ final class PostSettingsViewModel: ObservableObject { JetpackSocialNoConnectionViewModel( services: getPublicizeServices(), padding: .zero, - onConnectTap: { [weak self] in - // TODO: - }, - onNotNowTap: { [weak self] in - // TODO: - } + onConnectTap: { [weak self] in self?.showSocialSharingSetupScreen() }, + onNotNowTap: { [weak self] in self?.didDismissSocialSharingSetupPrompt() } ) } -// /// A closure to be executed when the Connect button is tapped in the No Connection view. -// func noConnectionConnectTapped() -> () -> Void { -// return { [weak self] in -// guard let self, -// let controller = SharingViewController(blog: self.post.blog, delegate: self), -// self.presentedViewController == nil else { -// return -// } -// -// WPAnalytics.track(.jetpackSocialNoConnectionCTATapped, properties: ["source": Constants.trackingSource]) -// -// let navigationController = UINavigationController(rootViewController: controller) -// self.show(navigationController, sender: nil) -// } -// } -// -// /// A closure to be executed when the "Not now" button is tapped in the No Connection view. -// func noConnectionDismissTapped() -> () -> Void { -// return { [weak self] in -// guard let self, -// let autoSharingRowIndex = options.firstIndex(where: { $0.id == .autoSharing }) else { -// return -// } -// -// WPAnalytics.track(.jetpackSocialNoConnectionCardDismissed, properties: ["source": Constants.trackingSource]) -// -// self.isNoConnectionDismissed = true -// self.refreshOptions() -// -// // ensure that the `.autoSharing` identifier is truly removed to prevent table updates from crashing. -// guard options.firstIndex(where: { $0.id == .autoSharing }) == nil else { -// return -// } -// -// self.tableView.performBatchUpdates { -// self.tableView.deleteRows(at: [.init(row: autoSharingRowIndex, section: .zero)], with: .fade) -// } completion: { _ in } -// } -// } + private func showSocialSharingSetupScreen() { + guard let sharingVC = SharingViewController(blog: post.blog, delegate: self) else { + return wpAssertionFailure("failed to instantiate SharingVC") + } + track(.jetpackSocialNoConnectionCTATapped) + let navigationVC = UINavigationController(rootViewController: sharingVC) + viewController?.present(navigationVC, animated: true) + } + + private func didDismissSocialSharingSetupPrompt() { + track(.jetpackSocialNoConnectionCardDismissed) + isSocialConnectionSetupDismissed = true + + withAnimation { + socialSharingState = nil + } + } // MARK: - Navigation @@ -445,7 +417,7 @@ final class PostSettingsViewModel: ObservableObject { } if old.featuredImageID != new.featuredImageID { let action = new.featuredImageID == nil ? "removed" : "changed" - WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": action]) + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": source, "action": action]) } if old.excerpt != new.excerpt { track(.editorPostExcerptChanged) @@ -464,7 +436,20 @@ final class PostSettingsViewModel: ObservableObject { } private func track(_ event: WPAnalyticsEvent) { - WPAnalytics.track(event, properties: ["via": "settings"]) + WPAnalytics.track(event, properties: ["via": source]) + } + + private var source: String { + switch context { + case .settings: "post_settings" + case .publishing: "pre_publishing" + } + } +} + +extension PostSettingsViewModel: @MainActor SharingViewControllerDelegate { + func didChangePublicizeServices() { + refreshSocialSharingState() } } From 21a503f35f7d201ac8f0b694e6cd11b4bc0aeff7 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 14:20:37 -0400 Subject: [PATCH 26/41] Add initil support for auto sharing view --- .../Post/PostSettings/PostSettingsView.swift | 43 ++++++---- .../PostSettings/PostSettingsViewModel.swift | 86 ++++++++++++++++++- 2 files changed, 108 insertions(+), 21 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index d9672245da3a..6afa46b7bba7 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -183,28 +183,16 @@ struct PostSettingsFormContentView: View { } private var categoriesRow: some View { - Button(action: viewModel.showCategoriesPicker) { - HStack { - PostSettingsCategoriesRow(categories: viewModel.displayedCategories) - Image(systemName: "chevron.forward") - .font(.footnote.weight(.semibold)) - .foregroundColor(Color(.tertiaryLabel)) - } + LegacyNavigationLinkRow(action: viewModel.showCategoriesPicker) { + PostSettingsCategoriesRow(categories: viewModel.displayedCategories) } - .tint(.primary) .accessibilityIdentifier("post_settings_categories") } private var tagsRow: some View { - Button(action: viewModel.showTagsPicker) { - HStack { - PostSettingsTagsRow(tags: viewModel.displayedTags) - Image(systemName: "chevron.forward") - .font(.footnote.weight(.semibold)) - .foregroundColor(Color(.tertiaryLabel)) - } + LegacyNavigationLinkRow(action: viewModel.showTagsPicker) { + PostSettingsTagsRow(tags: viewModel.displayedTags) } - .tint(.primary) .accessibilityIdentifier("post_settings_tags") } @@ -300,8 +288,10 @@ struct PostSettingsFormContentView: View { switch state { case .setup(let viewModel): JetpackSocialNoConnectionView(viewModel: viewModel) - case .connected: - Text("Connected (not implemented)") + case .connected(let viewModel): + LegacyNavigationLinkRow(action: self.viewModel.showSocialSharingOptions) { + PrepublishingAutoSharingView(model: viewModel) + } } } header: { SectionHeader(Strings.socialSharing) @@ -472,6 +462,23 @@ private struct SettingsTextFieldView: View { } } +private struct LegacyNavigationLinkRow: View { + let action: () -> Void + @ViewBuilder let label: () -> Content + + var body: some View { + Button(action: action) { + HStack { + label() + Image(systemName: "chevron.forward") + .font(.footnote.weight(.semibold)) + .foregroundColor(Color(.tertiaryLabel)) + } + } + .tint(.primary) + } +} + private enum Strings { static let generalHeader = NSLocalizedString( "postSettings.section.general", diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 789ea74b61eb..718d20b04637 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -109,7 +109,7 @@ final class PostSettingsViewModel: ObservableObject { /// The initial prompt to set up connections. case setup(JetpackSocialNoConnectionViewModel) /// The site has existing connections. - case connected + case connected(PrepublishingAutoSharingModel) } private let originalSettings: PostSettings @@ -308,7 +308,8 @@ final class PostSettingsViewModel: ObservableObject { RemoteFeatureFlag.jetpackSocialImprovements.enabled() && post.status != .publishPrivate && !getPublicizeServices().isEmpty && - post.blog.supportsPublicize() else { + post.blog.supportsPublicize(), + let post = post as? Post else { socialSharingState = nil return } @@ -320,7 +321,7 @@ final class PostSettingsViewModel: ObservableObject { socialSharingState = .setup(makeSocialSharingSetupViewModel()) } } else { - socialSharingState = .connected + socialSharingState = .connected(makeAutoSharingModel(for: post)) } } @@ -378,6 +379,54 @@ final class PostSettingsViewModel: ObservableObject { } } + private func makeAutoSharingModel(for post: Post) -> PrepublishingAutoSharingModel { + let connections = post.blog.sortedConnections + + // first, build a dictionary to categorize the connections. + var connectionsMap = [PublicizeService.ServiceName: [PublicizeConnection]]() + connections.filter { !$0.requiresUserAction() }.forEach { connection in + let serviceName = PublicizeService.ServiceName(rawValue: connection.service) ?? .unknown + var serviceConnections = connectionsMap[serviceName] ?? [] + serviceConnections.append(connection) + connectionsMap[serviceName] = serviceConnections + } + + let services = getPublicizeServices().compactMap { service -> PrepublishingAutoSharingModel.Service? in + // skip services without connections. + guard let serviceConnections = connectionsMap[service.name], + !serviceConnections.isEmpty else { + return nil + } + + return PrepublishingAutoSharingModel.Service( + name: service.name, + connections: serviceConnections.map { + .init(account: $0.externalDisplay, + keyringID: $0.keyringConnectionID.intValue, + enabled: !post.publicizeConnectionDisabledForKeyringID($0.keyringConnectionID)) + } + ) + } + + return .init(services: services, message: post.publicizeMessage ?? post.titleForDisplay(), sharingLimit: post.blog.sharingLimit) + } + + func showSocialSharingOptions() { + guard let blogID = post.blog.dotComID?.intValue, + let post = post as? Post else { + return wpAssertionFailure("invalid context") + } + let delegate = PrepublishingSocialAccountsDelegateAdapter() + cancellables.insert(AnyCancellable { _ = delegate }) // Retain it + let optionsVC = PrepublishingSocialAccountsViewController( + blogID: blogID, + model: makeAutoSharingModel(for: post), + delegate: delegate, + coreDataStack: ContextManager.shared + ) + viewController?.navigationController?.pushViewController(optionsVC, animated: true) + } + // MARK: - Navigation func showCategoriesPicker() { @@ -453,6 +502,37 @@ extension PostSettingsViewModel: @MainActor SharingViewControllerDelegate { } } +private final class PrepublishingSocialAccountsDelegateAdapter: NSObject, @MainActor PrepublishingSocialAccountsDelegate { + func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) { +// reloadData() + } + + func didFinish(with connectionChanges: [Int: Bool], message: String?) { +// DispatchQueue.main.async { +// self._didFinish(with: connectionChanges, message: message) +// } + } + + private func _didFinish(with connectionChanges: [Int: Bool], message: String?) { +// guard let post = post as? Post else { +// wpAssertionFailure("invalid post type") +// return +// } +// connectionChanges.forEach { (keyringID, enabled) in +// if enabled { +// post.enablePublicizeConnectionWithKeyringID(NSNumber(value: keyringID)) +// } else { +// post.disablePublicizeConnectionWithKeyringID(NSNumber(value: keyringID)) +// } +// } +// +// let isMessageEmpty = message?.isEmpty ?? true +// post.publicizeMessage = isMessageEmpty ? nil : message +// +// reloadData() + } +} + // MARK: - Localized Strings private enum Strings { From bed24442da65d6e7865681d99e326229b528c297 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 14:49:34 -0400 Subject: [PATCH 27/41] Rename to PostSocialSharingSettings --- .../Post/PostSettings/PostSettingsViewModel.swift | 8 ++++---- .../Post/Prepublishing/PrepublishingAutoSharingView.swift | 8 ++++---- .../PrepublishingSocialAccountsViewController.swift | 4 ++-- .../PrepublishingViewController+JetpackSocial.swift | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 718d20b04637..11cc41d9b11e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -109,7 +109,7 @@ final class PostSettingsViewModel: ObservableObject { /// The initial prompt to set up connections. case setup(JetpackSocialNoConnectionViewModel) /// The site has existing connections. - case connected(PrepublishingAutoSharingModel) + case connected(PostSocialSharingSettings) } private let originalSettings: PostSettings @@ -379,7 +379,7 @@ final class PostSettingsViewModel: ObservableObject { } } - private func makeAutoSharingModel(for post: Post) -> PrepublishingAutoSharingModel { + private func makeAutoSharingModel(for post: Post) -> PostSocialSharingSettings { let connections = post.blog.sortedConnections // first, build a dictionary to categorize the connections. @@ -391,14 +391,14 @@ final class PostSettingsViewModel: ObservableObject { connectionsMap[serviceName] = serviceConnections } - let services = getPublicizeServices().compactMap { service -> PrepublishingAutoSharingModel.Service? in + let services = getPublicizeServices().compactMap { service -> PostSocialSharingSettings.Service? in // skip services without connections. guard let serviceConnections = connectionsMap[service.name], !serviceConnections.isEmpty else { return nil } - return PrepublishingAutoSharingModel.Service( + return PostSocialSharingSettings.Service( name: service.name, connections: serviceConnections.map { .init(account: $0.externalDisplay, diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingAutoSharingView.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingAutoSharingView.swift index 9aed6f1e1f77..380255248981 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingAutoSharingView.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingAutoSharingView.swift @@ -3,7 +3,7 @@ import WordPressUI struct PrepublishingAutoSharingView: View { - let model: PrepublishingAutoSharingModel + let model: PostSocialSharingSettings @Environment(\.sizeCategory) private var sizeCategory @@ -107,7 +107,7 @@ private extension PrepublishingAutoSharingView { // MARK: - PrepublishingAutoSharingModel Private Extensions -private extension PrepublishingAutoSharingModel.Service { +private extension PostSocialSharingSettings.Service { /// Whether the icon for this service should be opaque or transparent. /// If at least one account is enabled, an opaque version should be shown. var usesOpaqueIcon: Bool { @@ -116,12 +116,12 @@ private extension PrepublishingAutoSharingModel.Service { } } - var enabledConnections: [PrepublishingAutoSharingModel.Connection] { + var enabledConnections: [PostSocialSharingSettings.Connection] { connections.filter { $0.enabled } } } -private extension PrepublishingAutoSharingModel { +private extension PostSocialSharingSettings { var enabledConnectionsCount: Int { services.reduce(0) { $0 + $1.enabledConnections.count } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift index edcf60adc43c..ab0dc884af0f 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift @@ -84,7 +84,7 @@ class PrepublishingSocialAccountsViewController: UITableViewController { } init(blogID: Int, - model: PrepublishingAutoSharingModel, + model: PostSocialSharingSettings, delegate: PrepublishingSocialAccountsDelegate?, coreDataStack: CoreDataStackSwift = ContextManager.shared, blogService: BlogService? = nil) { @@ -374,7 +374,7 @@ private extension PrepublishingSocialAccountsViewController { } -private extension PrepublishingAutoSharingModel { +private extension PostSocialSharingSettings { var enabledConnectionsCount: Int { services.flatMap { $0.connections }.filter { $0.enabled }.count } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift index dc515c2c01d8..a01cadb0d908 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift @@ -195,7 +195,7 @@ private extension PrepublishingViewController { // MARK: - Model Creation - func makeAutoSharingModel() -> PrepublishingAutoSharingModel { + func makeAutoSharingModel() -> PostSocialSharingSettings { return coreDataStack.performQuery { [postObjectID = post.objectID] context in guard let post = (try? context.existingObject(with: postObjectID)) as? Post, let supportedServices = try? PublicizeService.allSupportedServices(in: context) else { @@ -213,14 +213,14 @@ private extension PrepublishingViewController { } // then, transform [PublicizeService] to [PrepublishingAutoSharingModel.Service]. - let modelServices = supportedServices.compactMap { service -> PrepublishingAutoSharingModel.Service? in + let modelServices = supportedServices.compactMap { service -> PostSocialSharingSettings.Service? in // skip services without connections. guard let serviceConnections = connectionsMap[service.name], !serviceConnections.isEmpty else { return nil } - return PrepublishingAutoSharingModel.Service( + return PostSocialSharingSettings.Service( name: service.name, connections: serviceConnections.map { .init(account: $0.externalDisplay, @@ -247,7 +247,7 @@ private extension PrepublishingViewController { // MARK: - Auto Sharing Model /// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. -struct PrepublishingAutoSharingModel { +struct PostSocialSharingSettings { let services: [Service] let message: String let sharingLimit: PublicizeInfo.SharingLimit? From 35bd98b93c6d564bdfdd2792085fae3b79f9b13f Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 15:07:52 -0400 Subject: [PATCH 28/41] Move social sharing settings to PostSettings (struct --- .../Swift/PublicizeInfo+CoreDataClass.swift | 2 +- .../Post/PostSettings/PostSettings.swift | 69 +++++++++++++++++++ .../Post/PostSettings/PostSettingsView.swift | 6 +- .../PostSettings/PostSettingsViewModel.swift | 40 ++--------- ...blishingViewController+JetpackSocial.swift | 20 ------ 5 files changed, 78 insertions(+), 59 deletions(-) diff --git a/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift b/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift index 5162f92756f9..4c4c0c557edd 100644 --- a/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift +++ b/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift @@ -31,7 +31,7 @@ public class PublicizeInfo: NSManagedObject { } /// A value-type representation for Publicize auto-sharing usage. - public struct SharingLimit { + public struct SharingLimit: Hashable { /// The remaining shares available for use. public let remaining: Int diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index c4afe4b29d8b..70540ae9858f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -25,6 +25,7 @@ struct PostSettings: Hashable { // MARK: - Post-specific var postFormat: String? var isStickyPost = false + var sharing: PostSocialSharingSettings? // MARK: - Page-specific var parentPageID: Int? @@ -57,6 +58,7 @@ struct PostSettings: Hashable { categoryIDs = Set((post.categories ?? []).compactMap { $0.categoryID?.intValue }) + sharing = PostSocialSharingSettings.make(for: post) case let page as Page: parentPageID = page.parentID?.intValue default: @@ -186,3 +188,70 @@ extension PostSettings { .map { $0.stringByDecodingXMLCharacters() } } } + +/// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. +struct PostSocialSharingSettings: Hashable { + let services: [Service] + let message: String + let sharingLimit: PublicizeInfo.SharingLimit? + + struct Service: Hashable { + let name: PublicizeService.ServiceName + let connections: [Connection] + } + + struct Connection: Hashable { + let account: String + let keyringID: Int + var enabled: Bool + } + + static func make(for post: Post) -> PostSocialSharingSettings? { + guard let context = post.managedObjectContext else { + wpAssertionFailure("missing moc") + return nil + } + + let connections = post.blog.sortedConnections + + // first, build a dictionary to categorize the connections. + var connectionsMap = [PublicizeService.ServiceName: [PublicizeConnection]]() + connections.filter { !$0.requiresUserAction() }.forEach { connection in + let name = PublicizeService.ServiceName(rawValue: connection.service) ?? .unknown + var serviceConnections = connectionsMap[name] ?? [] + serviceConnections.append(connection) + connectionsMap[name] = serviceConnections + } + + let publicizeServices: [PublicizeService] + do { + publicizeServices = try PublicizeService.allPublicizeServices(in: context) + } catch { + wpAssertionFailure("failed to fetch services", userInfo: ["error": error.localizedDescription]) + return nil + } + + let services = publicizeServices.compactMap { service -> PostSocialSharingSettings.Service? in + // skip services without connections. + guard let serviceConnections = connectionsMap[service.name], + !serviceConnections.isEmpty else { + return nil + } + + return PostSocialSharingSettings.Service( + name: service.name, + connections: serviceConnections.map { + .init(account: $0.externalDisplay, + keyringID: $0.keyringConnectionID.intValue, + enabled: !post.publicizeConnectionDisabledForKeyringID($0.keyringConnectionID)) + } + ) + } + + return PostSocialSharingSettings( + services: services, + message: post.publicizeMessage ?? post.titleForDisplay(), + sharingLimit: post.blog.sharingLimit + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 6afa46b7bba7..8b5ff463f833 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -288,9 +288,9 @@ struct PostSettingsFormContentView: View { switch state { case .setup(let viewModel): JetpackSocialNoConnectionView(viewModel: viewModel) - case .connected(let viewModel): - LegacyNavigationLinkRow(action: self.viewModel.showSocialSharingOptions) { - PrepublishingAutoSharingView(model: viewModel) + case .connected(let settings): + LegacyNavigationLinkRow(action: viewModel.showSocialSharingOptions) { + PrepublishingAutoSharingView(model: settings) } } } header: { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 11cc41d9b11e..4fcca7a2a67f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -320,8 +320,10 @@ final class PostSettingsViewModel: ObservableObject { } else { socialSharingState = .setup(makeSocialSharingSetupViewModel()) } + } else if let settings = settings.sharing { + socialSharingState = .connected(settings) } else { - socialSharingState = .connected(makeAutoSharingModel(for: post)) + socialSharingState = nil } } @@ -379,48 +381,16 @@ final class PostSettingsViewModel: ObservableObject { } } - private func makeAutoSharingModel(for post: Post) -> PostSocialSharingSettings { - let connections = post.blog.sortedConnections - - // first, build a dictionary to categorize the connections. - var connectionsMap = [PublicizeService.ServiceName: [PublicizeConnection]]() - connections.filter { !$0.requiresUserAction() }.forEach { connection in - let serviceName = PublicizeService.ServiceName(rawValue: connection.service) ?? .unknown - var serviceConnections = connectionsMap[serviceName] ?? [] - serviceConnections.append(connection) - connectionsMap[serviceName] = serviceConnections - } - - let services = getPublicizeServices().compactMap { service -> PostSocialSharingSettings.Service? in - // skip services without connections. - guard let serviceConnections = connectionsMap[service.name], - !serviceConnections.isEmpty else { - return nil - } - - return PostSocialSharingSettings.Service( - name: service.name, - connections: serviceConnections.map { - .init(account: $0.externalDisplay, - keyringID: $0.keyringConnectionID.intValue, - enabled: !post.publicizeConnectionDisabledForKeyringID($0.keyringConnectionID)) - } - ) - } - - return .init(services: services, message: post.publicizeMessage ?? post.titleForDisplay(), sharingLimit: post.blog.sharingLimit) - } - func showSocialSharingOptions() { guard let blogID = post.blog.dotComID?.intValue, - let post = post as? Post else { + let settigns = settings.sharing else { return wpAssertionFailure("invalid context") } let delegate = PrepublishingSocialAccountsDelegateAdapter() cancellables.insert(AnyCancellable { _ = delegate }) // Retain it let optionsVC = PrepublishingSocialAccountsViewController( blogID: blogID, - model: makeAutoSharingModel(for: post), + model: settigns, delegate: delegate, coreDataStack: ContextManager.shared ) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift index a01cadb0d908..9e3d468c47f9 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift @@ -244,26 +244,6 @@ private extension PrepublishingViewController { } } -// MARK: - Auto Sharing Model - -/// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. -struct PostSocialSharingSettings { - let services: [Service] - let message: String - let sharingLimit: PublicizeInfo.SharingLimit? - - struct Service: Hashable { - let name: PublicizeService.ServiceName - let connections: [Connection] - } - - struct Connection: Hashable { - let account: String - let keyringID: Int - var enabled: Bool - } -} - // MARK: - Sharing View Controller Delegate extension PrepublishingViewController: SharingViewControllerDelegate { From ce85c78bd3e93b060048634621649449890dcbcc Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 17:46:10 -0400 Subject: [PATCH 29/41] Initial integration with sharing settings --- .../Post/PostSettings/PostSettings.swift | 8 +-- .../Post/PostSettings/PostSettingsView.swift | 8 ++- .../PostSettings/PostSettingsViewModel.swift | 56 ++++++++----------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index 70540ae9858f..17a1b3f47464 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -191,13 +191,13 @@ extension PostSettings { /// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. struct PostSocialSharingSettings: Hashable { - let services: [Service] - let message: String - let sharingLimit: PublicizeInfo.SharingLimit? + var services: [Service] + var message: String + var sharingLimit: PublicizeInfo.SharingLimit? struct Service: Hashable { let name: PublicizeService.ServiceName - let connections: [Connection] + var connections: [Connection] } struct Connection: Hashable { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 8b5ff463f833..bf1b91b12ff2 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -288,9 +288,11 @@ struct PostSettingsFormContentView: View { switch state { case .setup(let viewModel): JetpackSocialNoConnectionView(viewModel: viewModel) - case .connected(let settings): - LegacyNavigationLinkRow(action: viewModel.showSocialSharingOptions) { - PrepublishingAutoSharingView(model: settings) + case .connected: + if let settings = viewModel.settings.sharing { + LegacyNavigationLinkRow(action: viewModel.showSocialSharingOptions) { + PrepublishingAutoSharingView(model: settings) + } } } } header: { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 4fcca7a2a67f..818577aad55f 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -7,7 +7,7 @@ import WordPressShared import Combine @MainActor -final class PostSettingsViewModel: ObservableObject { +final class PostSettingsViewModel: NSObject, ObservableObject { let post: AbstractPost let isStandalone: Bool let context: Context @@ -109,7 +109,7 @@ final class PostSettingsViewModel: ObservableObject { /// The initial prompt to set up connections. case setup(JetpackSocialNoConnectionViewModel) /// The site has existing connections. - case connected(PostSocialSharingSettings) + case connected } private let originalSettings: PostSettings @@ -148,6 +148,8 @@ final class PostSettingsViewModel: ObservableObject { // Initialize featured image view model self.featuredImageViewModel = PostSettingsFeaturedImageViewModel(post: post) + super.init() + // Observe selection changes from featured image view model featuredImageViewModel.$selection.dropFirst().sink { [weak self] media in self?.settings.featuredImageID = media?.mediaID?.intValue @@ -320,10 +322,8 @@ final class PostSettingsViewModel: ObservableObject { } else { socialSharingState = .setup(makeSocialSharingSetupViewModel()) } - } else if let settings = settings.sharing { - socialSharingState = .connected(settings) } else { - socialSharingState = nil + socialSharingState = .connected } } @@ -386,12 +386,10 @@ final class PostSettingsViewModel: ObservableObject { let settigns = settings.sharing else { return wpAssertionFailure("invalid context") } - let delegate = PrepublishingSocialAccountsDelegateAdapter() - cancellables.insert(AnyCancellable { _ = delegate }) // Retain it let optionsVC = PrepublishingSocialAccountsViewController( blogID: blogID, model: settigns, - delegate: delegate, + delegate: self, coreDataStack: ContextManager.shared ) viewController?.navigationController?.pushViewController(optionsVC, animated: true) @@ -472,34 +470,28 @@ extension PostSettingsViewModel: @MainActor SharingViewControllerDelegate { } } -private final class PrepublishingSocialAccountsDelegateAdapter: NSObject, @MainActor PrepublishingSocialAccountsDelegate { +extension PostSettingsViewModel: @MainActor PrepublishingSocialAccountsDelegate { func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) { -// reloadData() + settings.sharing?.sharingLimit = newValue } func didFinish(with connectionChanges: [Int: Bool], message: String?) { -// DispatchQueue.main.async { -// self._didFinish(with: connectionChanges, message: message) -// } - } - - private func _didFinish(with connectionChanges: [Int: Bool], message: String?) { -// guard let post = post as? Post else { -// wpAssertionFailure("invalid post type") -// return -// } -// connectionChanges.forEach { (keyringID, enabled) in -// if enabled { -// post.enablePublicizeConnectionWithKeyringID(NSNumber(value: keyringID)) -// } else { -// post.disablePublicizeConnectionWithKeyringID(NSNumber(value: keyringID)) -// } -// } -// -// let isMessageEmpty = message?.isEmpty ?? true -// post.publicizeMessage = isMessageEmpty ? nil : message -// -// reloadData() + guard var settings = settings.sharing else { + return wpAssertionFailure("social sharing settings missing") + } + settings.services = settings.services.map { + var service = $0 + service.connections = service.connections.map { + var connection = $0 + if let isEnabled = connectionChanges[connection.keyringID] { + connection.enabled = isEnabled + } + return connection + } + return service + } + settings.message = message ?? "" + self.settings.sharing = settings } } From df872d46041d95359cf6b0355cbcb0453c58bed3 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 17:50:08 -0400 Subject: [PATCH 30/41] Implement apply of sharing settings --- .../Post/PostSettings/PostSettings.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index 17a1b3f47464..1afb635d1bee 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -131,6 +131,22 @@ struct PostSettings: Hashable { if post.isStickyPost != isStickyPost { post.isStickyPost = isStickyPost } + + if let sharing { + for connection in sharing.services.flatMap(\.connections) { + let keyringID = NSNumber(value: connection.keyringID) + if !post.publicizeConnectionDisabledForKeyringID(keyringID) != connection.enabled { + if connection.enabled { + post.enablePublicizeConnectionWithKeyringID(keyringID) + } else { + post.disablePublicizeConnectionWithKeyringID(keyringID) + } + } + } + if post.publicizeMessage != sharing.message { + post.publicizeMessage = sharing.message + } + } case let page as Page: if page.parentID?.intValue != parentPageID { page.parentID = parentPageID.map { NSNumber(value: $0) } From 5f5a0d666330da01de2871915b3fcd884bab9387 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 18:12:17 -0400 Subject: [PATCH 31/41] Remove preview post --- .../Post/Publishing/PublishPostViewController.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 0d913acdb953..d6784615d7c4 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -71,13 +71,6 @@ struct PublishPostView: View { Text(Strings.discardChangesMessage) } } - ToolbarItem(placement: .topBarTrailing) { - Button { - // TODO: (publish) show preview - } label: { - Image(systemName: "safari") - } - } ToolbarItem(placement: .topBarTrailing) { buttonPublish } From bca323b91a70b148be264df8afad37bb3334040c Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 22 Sep 2025 18:24:06 -0400 Subject: [PATCH 32/41] Update based on comments --- .../Post/PostSettings/PostSettingsView.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index bf1b91b12ff2..b2cd92366448 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -632,24 +632,12 @@ private enum Strings { comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" ) - static let publishingOptionsHeader = NSLocalizedString( - "postSettings.publishing.header", - value: "Publishing", - comment: "Section header for Publishing Options in Post Settings" - ) - static let publishingTo = NSLocalizedString( "postSettings.publishingTo", value: "Publishing to", comment: "Label indicating which site you are publishing to" ) - static let previewLabel = NSLocalizedString( - "postSettings.preview.label", - value: "Preview", - comment: "Label for the preview button in Post Settings" - ) - static let socialSharing = NSLocalizedString( "postSettings.socialSharing.header", value: "Social Sharing", From 1bcae8253c0ae28a3237056fb79204751d57cfc1 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 07:25:22 -0400 Subject: [PATCH 33/41] Extract isPostEligibleForSocialSharing --- .../PostSettings/PostSettingsViewModel.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 818577aad55f..cac7919b9516 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -306,16 +306,10 @@ final class PostSettingsViewModel: NSObject, ObservableObject { // MARK: - Social Sharing private func refreshSocialSharingState() { - guard BuildSettings.current.brand == .jetpack && - RemoteFeatureFlag.jetpackSocialImprovements.enabled() && - post.status != .publishPrivate && - !getPublicizeServices().isEmpty && - post.blog.supportsPublicize(), - let post = post as? Post else { + guard let post = post as? Post, isPostEligibleForSocialSharing(post) else { socialSharingState = nil return } - if (post.blog.connections ?? []).isEmpty { if isSocialConnectionSetupDismissed { socialSharingState = nil @@ -327,6 +321,14 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } } + private func isPostEligibleForSocialSharing(_ post: Post) -> Bool { + BuildSettings.current.brand == .jetpack && + RemoteFeatureFlag.jetpackSocialImprovements.enabled() && + post.status != .publishPrivate && + !getPublicizeServices().isEmpty && + post.blog.supportsPublicize() + } + private func getPublicizeServices() -> [PublicizeService] { let context = ContextManager.shared.mainContext return (try? PublicizeService.allPublicizeServices(in: context)) ?? [] @@ -343,7 +345,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject { } return value } - set { guard let blogID = post.blog.dotComID?.intValue else { return wpAssertionFailure("blogID missing") @@ -375,7 +376,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject { private func didDismissSocialSharingSetupPrompt() { track(.jetpackSocialNoConnectionCardDismissed) isSocialConnectionSetupDismissed = true - withAnimation { socialSharingState = nil } From 258f4b8fb33b75a42ea546ae5f9d202682387562 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 08:00:38 -0400 Subject: [PATCH 34/41] Fix publish date being non-optional --- .../ViewRelated/Post/PostSettings/PostSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index b2cd92366448..f95145db7ceb 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -254,7 +254,7 @@ struct PostSettingsFormContentView: View { NavigationLink { PublishDatePickerView(configuration: PublishDatePickerConfiguration( date: viewModel.settings.publishDate, - isRequired: true, + isRequired: viewModel.isDraftOrPending, timeZone: viewModel.timeZone, updated: { date in viewModel.settings.publishDate = date From 5eee22528ae3019c6e4b7e68aaab2c2b87f89368 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 08:24:37 -0400 Subject: [PATCH 35/41] Further simplify publishing --- .../Post/PostSettings/PostSettingsView.swift | 23 +++++++++++----- .../PublishPostViewController.swift | 26 ++++++++++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index f95145db7ceb..2ddb029ceb04 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -126,18 +126,21 @@ struct PostSettingsFormContentView: View { @ObservedObject var viewModel: PostSettingsViewModel var body: some View { - if viewModel.context == .publishing { - publishingOptionsSection + Section { + BlogListSiteView(site: .init(blog: viewModel.post.blog)) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + } header: { + SectionHeader(Strings.readyToPublish) } + featuredImageSection if viewModel.isPost { organizationSection } excerptSection - if viewModel.isPost, viewModel.context == .publishing { - socialSharingSection - } generalSection + socialSharingSection moreOptionsSection if viewModel.context != .publishing { infoSection @@ -221,7 +224,7 @@ struct PostSettingsFormContentView: View { private var generalSection: some View { Section { authorRow - if !viewModel.isDraftOrPending { + if !viewModel.isDraftOrPending || viewModel.context == .publishing { publishDateRow visibilityRow } @@ -254,7 +257,7 @@ struct PostSettingsFormContentView: View { NavigationLink { PublishDatePickerView(configuration: PublishDatePickerConfiguration( date: viewModel.settings.publishDate, - isRequired: viewModel.isDraftOrPending, + isRequired: !viewModel.isDraftOrPending, timeZone: viewModel.timeZone, updated: { date in viewModel.settings.publishDate = date @@ -632,6 +635,12 @@ private enum Strings { comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" ) + static let readyToPublish = NSLocalizedString( + "postSettings.publishingSectionTitle", + 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 publishingTo = NSLocalizedString( "postSettings.publishingTo", value: "Publishing to", diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index d6784615d7c4..f3185679f314 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -46,6 +46,7 @@ struct PublishPostView: View { @ObservedObject var viewModel: PostSettingsViewModel @State private var isShowingDiscardChangesAlert = false + @State private var isShowingDatePicker = false var post: AbstractPost { viewModel.post } @@ -71,7 +72,8 @@ struct PublishPostView: View { Text(Strings.discardChangesMessage) } } - ToolbarItem(placement: .topBarTrailing) { + ToolbarItemGroup(placement: .topBarTrailing) { + buttonSchedule buttonPublish } } @@ -106,6 +108,28 @@ struct PublishPostView: View { } } + @ViewBuilder + private var buttonSchedule: some View { + Button { + isShowingDatePicker = true + } label: { + Image(systemName: "calendar") + } + .popover(isPresented: $isShowingDatePicker) { + NavigationView { + PublishDatePickerView(configuration: PublishDatePickerConfiguration( + date: viewModel.settings.publishDate, + isRequired: !viewModel.isDraftOrPending, + timeZone: viewModel.timeZone, + updated: { date in + viewModel.settings.publishDate = date + } + )) + } + .presentationDetents([.height(430), .large]) + } + } + @ViewBuilder private var buttonPublish: some View { if viewModel.isSaving { From accc2e59dbd5abee9f07ecc9ebb035dee730df74 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 08:34:26 -0400 Subject: [PATCH 36/41] Add PostSettingsPublishDatePicker --- .../Post/PostSettings/PostSettingsView.swift | 9 +-------- .../Views/PostSettingsPublishDatePicker.swift | 17 +++++++++++++++++ .../Publishing/PublishPostViewController.swift | 18 ++---------------- 3 files changed, 20 insertions(+), 24 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPublishDatePicker.swift diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 2ddb029ceb04..781f4de726b6 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -255,14 +255,7 @@ struct PostSettingsFormContentView: View { private var publishDateRow: some View { NavigationLink { - PublishDatePickerView(configuration: PublishDatePickerConfiguration( - date: viewModel.settings.publishDate, - isRequired: !viewModel.isDraftOrPending, - timeZone: viewModel.timeZone, - updated: { date in - viewModel.settings.publishDate = date - } - )) + PostSettingsPublishDatePicker(viewModel: viewModel) } label: { SettingsRow(Strings.publishDateLabel, value: viewModel.publishDateText ?? Strings.immediately) } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPublishDatePicker.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPublishDatePicker.swift new file mode 100644 index 000000000000..bc0974a33e4b --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/Views/PostSettingsPublishDatePicker.swift @@ -0,0 +1,17 @@ +import SwiftUI +import WordPressUI + +struct PostSettingsPublishDatePicker: View { + @ObservedObject var viewModel: PostSettingsViewModel + + var body: some View { + PublishDatePickerView(configuration: PublishDatePickerConfiguration( + date: viewModel.settings.publishDate, + isRequired: !viewModel.isDraftOrPending, + timeZone: viewModel.timeZone, + updated: { date in + viewModel.settings.publishDate = date + } + )) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index f3185679f314..91b8931cc269 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -46,7 +46,6 @@ struct PublishPostView: View { @ObservedObject var viewModel: PostSettingsViewModel @State private var isShowingDiscardChangesAlert = false - @State private var isShowingDatePicker = false var post: AbstractPost { viewModel.post } @@ -110,24 +109,11 @@ struct PublishPostView: View { @ViewBuilder private var buttonSchedule: some View { - Button { - isShowingDatePicker = true + NavigationLink { + PostSettingsPublishDatePicker(viewModel: viewModel) } label: { Image(systemName: "calendar") } - .popover(isPresented: $isShowingDatePicker) { - NavigationView { - PublishDatePickerView(configuration: PublishDatePickerConfiguration( - date: viewModel.settings.publishDate, - isRequired: !viewModel.isDraftOrPending, - timeZone: viewModel.timeZone, - updated: { date in - viewModel.settings.publishDate = date - } - )) - } - .presentationDetents([.height(430), .large]) - } } @ViewBuilder From bd02f286e36969352b33419bfafdb16ae24fff38 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 11:03:09 -0400 Subject: [PATCH 37/41] Improve header design --- .../Post/PostSettings/PostSettingsView.swift | 20 ------------------- .../PrepublishingViewController+Helpers.swift | 5 ++++- .../PublishPostViewController.swift | 11 ++++++++++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 781f4de726b6..8e89aa67445d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -126,14 +126,6 @@ struct PostSettingsFormContentView: View { @ObservedObject var viewModel: PostSettingsViewModel var body: some View { - Section { - BlogListSiteView(site: .init(blog: viewModel.post.blog)) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - } header: { - SectionHeader(Strings.readyToPublish) - } - featuredImageSection if viewModel.isPost { organizationSection @@ -628,18 +620,6 @@ private enum Strings { comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected" ) - static let readyToPublish = NSLocalizedString( - "postSettings.publishingSectionTitle", - 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 publishingTo = NSLocalizedString( - "postSettings.publishingTo", - value: "Publishing to", - comment: "Label indicating which site you are publishing to" - ) - static let socialSharing = NSLocalizedString( "postSettings.socialSharing.header", value: "Social Sharing", diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index ab14224479b6..c460ecd4e8f7 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -16,7 +16,10 @@ extension PrepublishingViewController { publishVC.onCompletion = completion // - warning: Has to be UIKit because some of the `PostSettingsView` rows rely on it. let navigationVC = UINavigationController(rootViewController: publishVC) - navigationVC.sheetPresentationController?.detents = [.medium(), .large()] + navigationVC.sheetPresentationController?.detents = [ + .custom(identifier: .medium, resolver: { context in 456 }), + .large() + ] presentingViewController.present(navigationVC, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 91b8931cc269..bea9f17b4aac 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -51,6 +51,11 @@ struct PublishPostView: View { var body: some View { Form { + Section { + BlogListSiteView(site: .init(blog: viewModel.post.blog)) + } header: { + SectionHeader(Strings.readyToPublish) + } PostSettingsFormContentView(viewModel: viewModel) } .environment(\.defaultMinListHeaderHeight, 0) // Reduces top inset a bit @@ -184,4 +189,10 @@ enum PrepublishingSheetStrings { value: "Save Changes", comment: "Button to confirm discarding changes" ) + + static let readyToPublish = NSLocalizedString( + "prepublishing.publishingSectionTitle", + value: "Ready to Publish?", + comment: "The title of the top section that shows the site your are publishing to. Default is 'Ready to Publish?'" + ) } From c855e063b115375947e6a978caa212872eb64229 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 11:05:14 -0400 Subject: [PATCH 38/41] Add shouldPublishImmediatelly support --- .../Classes/ViewRelated/Post/PostSettings/PostSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index 1afb635d1bee..b95c303a6777 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -37,7 +37,7 @@ struct PostSettings: Hashable { excerpt = post.mt_excerpt ?? "" slug = post.wp_slug ?? "" status = post.status ?? .draft - publishDate = post.dateCreated + publishDate = post.shouldPublishImmediately() ? nil : post.dateCreated password = post.password if let authorID = post.authorID?.intValue, authorID > 0 { From ead649d4309c9de2e06fec8dd38d9dd267eec308 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 11:07:18 -0400 Subject: [PATCH 39/41] Move info section --- .../Post/PostSettings/PostSettingsView.swift | 38 +++++++++---------- .../PrepublishingViewController+Helpers.swift | 2 +- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 8e89aa67445d..1f6b41ee42de 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -51,6 +51,7 @@ private struct PostSettingsView: View { var body: some View { Form { PostSettingsFormContentView(viewModel: viewModel) + infoSection } .accessibilityIdentifier("post_settings_form") .disabled(viewModel.isSaving) @@ -120,6 +121,22 @@ private struct PostSettingsView: View { .tint(AppColor.tint) } } + + @ViewBuilder + private var infoSection: some View { + if viewModel.lastEditedText != nil || viewModel.postID != nil { + Section { + if let postID = viewModel.postID { + SettingsRow(Strings.postIDLabel, value: String(postID)) + } + if let lastEditedText = viewModel.lastEditedText { + SettingsRow(Strings.lastEditedLabel, value: lastEditedText) + } + } header: { + SectionHeader(Strings.infoLabel) + } + } + } } struct PostSettingsFormContentView: View { @@ -134,9 +151,6 @@ struct PostSettingsFormContentView: View { generalSection socialSharingSection moreOptionsSection - if viewModel.context != .publishing { - infoSection - } } // MARK: - "Publishing Options" Section @@ -360,24 +374,6 @@ struct PostSettingsFormContentView: View { Text(Strings.stickyPostLabel) } } - - // MARK: - "Info" Section - - @ViewBuilder - private var infoSection: some View { - if viewModel.lastEditedText != nil || viewModel.postID != nil { - Section { - if let postID = viewModel.postID { - SettingsRow(Strings.postIDLabel, value: String(postID)) - } - if let lastEditedText = viewModel.lastEditedText { - SettingsRow(Strings.lastEditedLabel, value: lastEditedText) - } - } header: { - SectionHeader(Strings.infoLabel) - } - } - } } @MainActor diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift index c460ecd4e8f7..8c3904508865 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+Helpers.swift @@ -17,7 +17,7 @@ extension PrepublishingViewController { // - warning: Has to be UIKit because some of the `PostSettingsView` rows rely on it. let navigationVC = UINavigationController(rootViewController: publishVC) navigationVC.sheetPresentationController?.detents = [ - .custom(identifier: .medium, resolver: { context in 456 }), + .custom(identifier: .medium, resolver: { context in 460 }), .large() ] presentingViewController.present(navigationVC, animated: true) From 1e79d33d717055876d39b024ab02abd7edc3fce1 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 11:57:20 -0400 Subject: [PATCH 40/41] Integrate upload view snackbar --- .../Classes/Services/MediaCoordinator.swift | 8 +-- .../BuildInformation/FeatureFlag.swift | 2 +- .../Views/PostMediaUploadsSnackbarView.swift | 58 +++++++++++++++++++ .../PublishPostViewController.swift | 21 ++++++- .../Views/PostMediaUploadsViewModel.swift | 18 ++++++ 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Post/Prepublishing/Views/PostMediaUploadsSnackbarView.swift diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index 443c69294e6c..2ee2644e383f 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -398,10 +398,10 @@ class MediaCoordinator: NSObject { // https://github.com/wordpress-mobile/WordPress-iOS/issues/20298#issuecomment-1465319707 let service = self.mediaServiceFactory.create(coreDataStack.mainContext) var progress: Progress? = nil - service.uploadMedia(media, automatedRetry: automatedRetry, progress: &progress, success: success, failure: failure) - if let progress { - resultProgress.addChild(progress, withPendingUnitCount: resultProgress.totalUnitCount) - } +// service.uploadMedia(media, automatedRetry: automatedRetry, progress: &progress, success: success, failure: failure) +// if let progress { +// resultProgress.addChild(progress, withPendingUnitCount: resultProgress.totalUnitCount) +// } uploading(media, progress: resultProgress) diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 6b5a5011170e..e55951e6aea0 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -84,7 +84,7 @@ public enum FeatureFlag: Int, CaseIterable { case .newStats: return false case .newPublishingSheet: - return BuildConfiguration.current == .debug + return false } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/Views/PostMediaUploadsSnackbarView.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/Views/PostMediaUploadsSnackbarView.swift new file mode 100644 index 000000000000..f5d9e39415b3 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/Views/PostMediaUploadsSnackbarView.swift @@ -0,0 +1,58 @@ +import SwiftUI + +struct PostMediaUploadsSnackbarView: View { + let state: PostMediaUploadsSnackbarState + + private let accessoryViewWidth: CGFloat = 20 + + var body: some View { + HStack(spacing: 10) { + switch state { + case let .uploading(title, details, progress): + if let progress { + MediaUploadProgressView(progress: progress) + .frame(width: accessoryViewWidth) + } else { + ProgressView() + .foregroundStyle(.secondary) + .frame(width: accessoryViewWidth) + } + makeDetailsView(title: title, details: details) + case let .failed(title, details): + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(Color.red) + .frame(width: accessoryViewWidth) + makeDetailsView(title: title, details: details) + } + } + } + + private func makeDetailsView(title: String, details: String?) -> some View { + VStack(alignment: .leading) { + Text(title) + .font(.subheadline.weight(.medium)) + .foregroundStyle(.primary) + if let details { + Text(details) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .tint(.primary) + .lineLimit(1) + } +} + +enum PostMediaUploadsSnackbarState { + case uploading(title: String, details: String, progress: Double?) + case failed(title: String, details: String? = nil) +} + +#Preview { + VStack(spacing: 16) { + PostMediaUploadsSnackbarView(state: .uploading(title: "Uploading media...", details: "2 items remaining", progress: 0.2)) + PostMediaUploadsSnackbarView(state: .failed(title: "Failed to upload media")) + PostMediaUploadsSnackbarView(state: .failed(title: "Failed to upload media", details: "Not connected to Internet")) + } + .padding() +} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index bea9f17b4aac..875a73e869ce 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -9,6 +9,7 @@ import WordPressUI /// the post settings along with some publishing options like the publish date. final class PublishPostViewController: UIHostingController { private let viewModel: PostSettingsViewModel + private let uploadsViewModel: PostMediaUploadsViewModel var onCompletion: ((PrepublishingSheetResult) -> Void)? @@ -19,7 +20,12 @@ final class PublishPostViewController: UIHostingController { context: .publishing ) self.viewModel = viewModel - super.init(rootView: PublishPostView(viewModel: viewModel)) + + let uploadsViewModel = PostMediaUploadsViewModel(post: post) + self.uploadsViewModel = uploadsViewModel + + let view = PublishPostView(viewModel: viewModel, uploadsViewModel: uploadsViewModel) + super.init(rootView: view) } required dynamic init?(coder aDecoder: NSCoder) { @@ -44,6 +50,7 @@ final class PublishPostViewController: UIHostingController { struct PublishPostView: View { @ObservedObject var viewModel: PostSettingsViewModel + @ObservedObject var uploadsViewModel: PostMediaUploadsViewModel @State private var isShowingDiscardChangesAlert = false @@ -52,6 +59,13 @@ struct PublishPostView: View { var body: some View { Form { Section { + if let state = uploadsViewModel.uploadingSnackbarState { + NavigationLink { + PostMediaUploadsView(viewModel: uploadsViewModel) + } label: { + PostMediaUploadsSnackbarView(state: state) + } + } BlogListSiteView(site: .init(blog: viewModel.post.blog)) } header: { SectionHeader(Strings.readyToPublish) @@ -126,13 +140,16 @@ struct PublishPostView: View { if viewModel.isSaving { ProgressView() } else { + let isDisabled = !uploadsViewModel.isCompleted + Button(viewModel.publishButtonTitle) { viewModel.buttonPublishTapped() } .fontWeight(.medium) .buttonStyle(.borderedProminent) .buttonBorderShape(.capsule) - .tint(AppColor.primary) + .tint(isDisabled ? Color(.opaqueSeparator) : AppColor.primary) + .disabled(isDisabled) } } } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift index c4c545b4ee75..a02c5d2faa8c 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift @@ -42,6 +42,24 @@ final class PostMediaUploadsViewModel: ObservableObject { }.store(in: &cancellables) } + /// - returns `nil` if upload is completed. + var uploadingSnackbarState: PostMediaUploadsSnackbarState? { + guard !isCompleted else { + return nil + } + typealias Strings = PrepublishingSheetStrings + let errors = uploads.compactMap(\.error) + if !errors.isEmpty { + let details = errors.count == 1 ? errors[0].localizedDescription : String(format: Strings.mediaUploadFailedDetailsMultipleFailures, errors.count.description) + return .failed(title: Strings.mediaUploadFailedTitle, details: details) + } + return .uploading( + title: Strings.uploadingMedia, + details: Strings.uploadMediaRemaining(count: uploads.count - completedUploadsCount), + progress: fractionCompleted + ) + } + private func didUpdateMedia(_ media: Set) { let remainingObjectIDs = Set(media.map(\.objectID)) withAnimation { From 9b1648dfd8f9130794f13c63fc88253c746e0ccd Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Sep 2025 15:27:57 -0400 Subject: [PATCH 41/41] Revert unwanted changes --- WordPress/Classes/Services/MediaCoordinator.swift | 8 ++++---- WordPress/Classes/Services/PostCoordinator.swift | 3 --- .../ViewRelated/Post/PostSettings/PostSettingsView.swift | 1 - 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index 2ee2644e383f..443c69294e6c 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -398,10 +398,10 @@ class MediaCoordinator: NSObject { // https://github.com/wordpress-mobile/WordPress-iOS/issues/20298#issuecomment-1465319707 let service = self.mediaServiceFactory.create(coreDataStack.mainContext) var progress: Progress? = nil -// service.uploadMedia(media, automatedRetry: automatedRetry, progress: &progress, success: success, failure: failure) -// if let progress { -// resultProgress.addChild(progress, withPendingUnitCount: resultProgress.totalUnitCount) -// } + service.uploadMedia(media, automatedRetry: automatedRetry, progress: &progress, success: success, failure: failure) + if let progress { + resultProgress.addChild(progress, withPendingUnitCount: resultProgress.totalUnitCount) + } uploading(media, progress: resultProgress) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index b3874c21e0c6..b629749a1180 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -89,9 +89,6 @@ class PostCoordinator: NSObject { /// /// - warning: Before publishing, ensure that the media for the post got /// uploaded. Managing media is not the responsibility of `PostRepository.` - /// - /// - parameter changes: The set of changes apply to the post together - /// with the publishing options. @MainActor func publish(_ post: AbstractPost, options: PublishingOptions) async throws { wpAssert(post.isOriginal()) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 1f6b41ee42de..67adebf65953 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -94,7 +94,6 @@ private struct PostSettingsView: View { } .tint(AppColor.tint) .accessibilityIdentifier("post_settings_cancel_button") - } @ViewBuilder