diff --git a/Tests/KeystoneTests/Tests/Features/CustomPostTypes/BlogCustomPostTypesTests.swift b/Tests/KeystoneTests/Tests/Features/CustomPostTypes/BlogCustomPostTypesTests.swift
new file mode 100644
index 000000000000..f578fa236405
--- /dev/null
+++ b/Tests/KeystoneTests/Tests/Features/CustomPostTypes/BlogCustomPostTypesTests.swift
@@ -0,0 +1,40 @@
+import Testing
+
+@testable import WordPress
+@testable import WordPressData
+
+@MainActor
+struct BlogCustomPostTypesTests {
+ @Test("uses Custom Post Types views when XML-RPC is disabled on self-hosted")
+ func usesCustomPostTypeViews() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext)
+ .isNotHostedAtWPcom()
+ .build()
+ blog.account = nil
+ blog.isXMLRPCDisabled = true
+
+ #expect(blog.usesCustomPostTypeViewsForPostsAndPages)
+ }
+
+ @Test("does not use Custom Post Types views when XML-RPC is enabled")
+ func doesNotUseCustomPostTypeViewsWithXMLRPC() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ blog.isXMLRPCDisabled = false
+
+ #expect(!blog.usesCustomPostTypeViewsForPostsAndPages)
+ }
+
+ @Test("does not use Custom Post Types views for WordPress.com")
+ func doesNotUseCustomPostTypeViewsForWordPressCom() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext)
+ .isHostedAtWPcom()
+ .withAnAccount()
+ .build()
+ blog.isXMLRPCDisabled = true
+
+ #expect(!blog.usesCustomPostTypeViewsForPostsAndPages)
+ }
+}
diff --git a/Tests/KeystoneTests/Tests/Features/CustomPostTypes/MySiteCreateEditorRoutingTests.swift b/Tests/KeystoneTests/Tests/Features/CustomPostTypes/MySiteCreateEditorRoutingTests.swift
deleted file mode 100644
index ab2d718209ae..000000000000
--- a/Tests/KeystoneTests/Tests/Features/CustomPostTypes/MySiteCreateEditorRoutingTests.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-import Testing
-
-@testable import WordPress
-@testable import WordPressData
-
-@MainActor
-struct MySiteCreateEditorRoutingTests {
-
- @Test("uses Core REST when XML-RPC is disabled on a self-hosted site")
- func usesCoreRESTForXMLRPCDisabledSelfHostedSite() {
- let context = ContextManager.forTesting().mainContext
- let blog = BlogBuilder(context).build()
- blog.isXMLRPCDisabled = true
-
- #expect(MySiteViewController.shouldUseCoreRESTEditor(for: blog))
- }
-
- @Test("uses the existing editor when XML-RPC is enabled on a self-hosted site")
- func usesExistingEditorForXMLRPCEnabledSelfHostedSite() {
- let context = ContextManager.forTesting().mainContext
- let blog = BlogBuilder(context).build()
- blog.isXMLRPCDisabled = false
-
- #expect(!MySiteViewController.shouldUseCoreRESTEditor(for: blog))
- }
-
- @Test("uses the existing editor for a WordPress.com site")
- func usesExistingEditorForWordPressComSite() {
- let context = ContextManager.forTesting().mainContext
- let blog = BlogBuilder(context).isHostedAtWPcom().withAnAccount().build()
- blog.isXMLRPCDisabled = true
-
- #expect(!MySiteViewController.shouldUseCoreRESTEditor(for: blog))
- }
-
- @Test("uses the existing editor when there is no selected site")
- func usesExistingEditorWithoutSelectedSite() {
- #expect(!MySiteViewController.shouldUseCoreRESTEditor(for: nil))
- }
-}
diff --git a/Tests/KeystoneTests/Tests/Features/CustomPostTypes/PostEditorRouterTests.swift b/Tests/KeystoneTests/Tests/Features/CustomPostTypes/PostEditorRouterTests.swift
new file mode 100644
index 000000000000..74b7f48c2f59
--- /dev/null
+++ b/Tests/KeystoneTests/Tests/Features/CustomPostTypes/PostEditorRouterTests.swift
@@ -0,0 +1,245 @@
+import Foundation
+import Testing
+
+@testable import WordPress
+@testable import WordPressData
+
+@MainActor
+struct PostEditorRouterTests {
+ @Test("selects Core REST for sites using Custom Post Types views")
+ func selectsCoreREST() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ blog.isXMLRPCDisabled = true
+ #expect(PostEditorRouter.destination(for: blog) == .coreREST)
+ }
+
+ @Test("selects legacy for other sites")
+ func selectsLegacy() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ blog.isXMLRPCDisabled = false
+ #expect(PostEditorRouter.destination(for: blog) == .legacy)
+ }
+
+ @Test("converts title content and tags for Core REST")
+ func convertsCoreRESTValues() throws {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ let context = NewPostEditorContext(
+ title: "Title",
+ content: "
Body
",
+ tags: "swift, ios"
+ )
+
+ #expect(try context.makeEditorContent().title == "Title")
+ #expect(try context.makeEditorContent().content.contains("Body"))
+ #expect(
+ context.makePostSettings(for: blog).tags == [
+ PostSettings.Term(id: 0, name: "swift"),
+ PostSettings.Term(id: 0, name: "ios")
+ ]
+ )
+ }
+
+ @Test("uses voice output instead of seeded content")
+ func usesVoiceContent() throws {
+ let context = NewPostEditorContext(content: "old", voiceContent: "voice blocks")
+
+ #expect(try context.makeEditorContent().content == "voice blocks")
+ }
+
+ @Test("preserves initial media order as Core REST blocks")
+ func preservesMediaOrder() throws {
+ let contextManager = ContextManager.forTesting()
+ let first = MediaBuilder(contextManager.mainContext).build()
+ first.mediaID = 1
+ first.mediaType = .image
+ first.remoteURL = "https://example.com/one.jpg"
+ let second = MediaBuilder(contextManager.mainContext).build()
+ second.mediaID = 2
+ second.mediaType = .video
+ second.remoteURL = "https://example.com/two.mp4"
+
+ let html = try NewPostEditorContext(initialMedia: [first, second])
+ .makeEditorContent()
+ .content
+ let firstRange = try #require(html.range(of: "one.jpg"))
+ let secondRange = try #require(html.range(of: "two.mp4"))
+
+ #expect(firstRange.lowerBound < secondRange.lowerBound)
+ #expect(html.contains(""#
+ )
+ )
+ #expect(
+ html.contains(
+ #"July Summary.pdf"#
+ )
+ )
+ }
+
+ @Test("preserves blogging prompts through both editor adapters")
+ func preservesBloggingPrompt() throws {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ let prompt = try #require(BloggingPrompt.newObject(in: contextManager.mainContext))
+ prompt.promptID = 42
+ prompt.text = "What did you learn today?"
+ prompt.additionalPostTags = ["learning", "daily"]
+ let context = NewPostEditorContext(prompt: prompt)
+
+ let settings = context.makePostSettings(for: blog)
+ #expect(settings.bloggingPromptID == "42")
+ #expect(
+ settings.makeCreateParameters().meta?
+ .valueForKey(
+ key: "_jetpack_blogging_prompt_key"
+ ) == .string("42")
+ )
+ #expect(try context.makeEditorContent().content.contains(prompt.text))
+ #expect(
+ settings.tags.map(\.name) == [
+ "dailyprompt", "dailyprompt-42", "learning", "daily"
+ ]
+ )
+
+ let post = blog.createDraftPost()
+ context.applyLegacyValues(to: post)
+ #expect(post.bloggingPromptID == "42")
+ #expect(post.content?.contains(prompt.text) == true)
+ #expect(post.tags == "dailyprompt, dailyprompt-42, learning, daily")
+ }
+
+ @Test("builds Core REST editor session attribution without a post")
+ func buildsCoreRESTEditorSessionAttribution() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ blog.dotComID = 42
+ let entryPoint = PostEditorEntryPoint.bloggingPromptsDashboardCard
+
+ let session = PostEditorAnalyticsSession(
+ editor: .gutenbergKit,
+ blog: blog,
+ postType: "post",
+ contentType: .new,
+ entryPoint: entryPoint
+ )
+
+ #expect(session.blogID == 42)
+ #expect(session.postType == "post")
+ #expect(session.contentType == PostEditorAnalyticsSession.ContentType.new.rawValue)
+ #expect(session.entryPoint == entryPoint)
+ #expect(session.started == false)
+ }
+
+ @Test("runs the Core REST dismissal callback once after view disappearance")
+ func callsCoreRESTDismissalOnceAfterViewDisappears() {
+ var callbackCount = 0
+ let controller = CoreRESTPostEditorHostingController {
+ callbackCount += 1
+ }
+
+ #expect(callbackCount == 0)
+ controller.viewDidDisappear(false)
+ controller.viewDidDisappear(false)
+ #expect(callbackCount == 1)
+ }
+
+ @Test("applies values through the legacy adapter")
+ func appliesLegacyPostValues() {
+ let contextManager = ContextManager.forTesting()
+ let blog = BlogBuilder(contextManager.mainContext).build()
+ let context = NewPostEditorContext(title: "Title", content: "Body", tags: "swift")
+ let post = blog.createDraftPost()
+ context.applyLegacyValues(to: post)
+
+ #expect(post.postTitle == "Title")
+ #expect(post.content == "Body")
+ #expect(post.tags == "swift")
+ }
+}
diff --git a/WordPress/Classes/Apps/Reader/Home/ReaderHomeViewController.swift b/WordPress/Classes/Apps/Reader/Home/ReaderHomeViewController.swift
index 2aa81b509acb..8e017fb49964 100644
--- a/WordPress/Classes/Apps/Reader/Home/ReaderHomeViewController.swift
+++ b/WordPress/Classes/Apps/Reader/Home/ReaderHomeViewController.swift
@@ -16,7 +16,12 @@ final class ReaderHomeViewController: ReaderStreamViewController {
titleView.textLabel.text = SharedStrings.Reader.home
navigationItem.titleView = titleView
- navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "wpl-add-card")?.resized(to: CGSize(width: 28, height: 28)), style: .plain, target: self, action: #selector(buttonCreatePostTapped))
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ image: UIImage(named: "wpl-add-card")?.resized(to: CGSize(width: 28, height: 28)),
+ style: .plain,
+ target: self,
+ action: #selector(buttonCreatePostTapped)
+ )
}
override func headerForStream(_ topic: ReaderAbstractTopic?, container: UITableViewController) -> UIView? {
@@ -44,12 +49,18 @@ final class ReaderHomeViewController: ReaderStreamViewController {
}
private func showCreatePostScreen(blog: Blog) {
- let editorVC = EditPostViewController(blog: blog)
- editorVC.entryPoint = .dashboard
- present(editorVC, animated: true)
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: self,
+ context: NewPostEditorContext(entryPoint: .dashboard)
+ )
}
}
private enum Strings {
- static let homeDetails = NSLocalizedString("reader.home.header.details", value: "Stay current with the blogs you've subscribed to.", comment: "Screen header details")
+ static let homeDetails = NSLocalizedString(
+ "reader.home.header.details",
+ value: "Stay current with the blogs you've subscribed to.",
+ comment: "Screen header details"
+ )
}
diff --git a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift
index b2e8ef7963c7..bd562f5b0513 100644
--- a/WordPress/Classes/Extensions/Post+BloggingPrompts.swift
+++ b/WordPress/Classes/Extensions/Post+BloggingPrompts.swift
@@ -1,50 +1,33 @@
import Foundation
import WordPressData
-extension Post {
-
- func prepareForPrompt(_ prompt: BloggingPrompt?) {
- guard let prompt else {
- return
- }
-
- content = promptContent(withPromptText: prompt.text)
- bloggingPromptID = String(prompt.promptID)
-
- if let currentTags = tags {
- tags = "\(currentTags), \(Strings.promptTag)"
- } else {
- tags = Strings.promptTag
- }
-
- tags?.append(", \(Strings.promptTag)-\(prompt.promptID)")
-
- // add any additional tags for the prompt.
- if let additionalPostTags = prompt.additionalPostTags, !additionalPostTags.isEmpty {
- additionalPostTags
- .map { ", \($0)" }
- .forEach { self.tags?.append($0) }
- }
+extension BloggingPrompt {
+ var editorContent: String {
+ """
+
+ \(text)
+
+
+
+
+ """
}
- private func promptContent(withPromptText promptText: String) -> String {
- return pullquoteBlock(promptText: promptText) + Strings.emptyParagraphBlock
+ var editorTags: [String] {
+ [Post.Strings.promptTag, "\(Post.Strings.promptTag)-\(promptID)"] + (additionalPostTags ?? [])
}
+}
- private func pullquoteBlock(promptText: String) -> String {
- return """
-
- \(promptText)
-
- """
+extension Post {
+ func prepareForPrompt(_ prompt: BloggingPrompt?) {
+ guard let prompt else { return }
+ content = prompt.editorContent
+ bloggingPromptID = String(prompt.promptID)
+ tags = (AbstractPost.makeTags(from: tags ?? "") + prompt.editorTags)
+ .joined(separator: ", ")
}
enum Strings {
static let promptTag = "dailyprompt"
- fileprivate static let emptyParagraphBlock = """
-
-
-
- """
}
}
diff --git a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutHandler.swift b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutHandler.swift
index 586e22c197a2..7a9ada09dedf 100644
--- a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutHandler.swift
+++ b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutHandler.swift
@@ -10,7 +10,7 @@ open class WP3DTouchShortcutHandler: NSObject {
case Notifications
var type: String {
- return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
+ Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
}
}
@@ -20,27 +20,32 @@ open class WP3DTouchShortcutHandler: NSObject {
let rootViewPresenter = RootViewCoordinator.sharedPresenter
switch shortcutItem.type {
- case ShortcutIdentifier.LogIn.type:
- WPAnalytics.track(.shortcutLogIn)
- return true
- case ShortcutIdentifier.NewPost.type:
- WPAnalytics.track(.shortcutNewPost)
- rootViewPresenter.showPostEditor(animated: false)
- return true
- case ShortcutIdentifier.Stats.type:
- WPAnalytics.track(.shortcutStats)
- clearCurrentViewController(rootViewPresenter.rootViewController)
- if let mainBlog = Blog.lastUsedOrFirst(in: ContextManager.shared.mainContext) {
- rootViewPresenter.showStats(for: mainBlog, source: .shortcut)
- }
- return true
- case ShortcutIdentifier.Notifications.type:
- WPAnalytics.track(.shortcutNotifications)
- clearCurrentViewController(rootViewPresenter.rootViewController)
- rootViewPresenter.showNotificationsTab()
- return true
- default:
- return false
+ case ShortcutIdentifier.LogIn.type:
+ WPAnalytics.track(.shortcutLogIn)
+ return true
+ case ShortcutIdentifier.NewPost.type:
+ WPAnalytics.track(.shortcutNewPost)
+ rootViewPresenter.showNewPostEditor(
+ context: NewPostEditorContext(
+ analytics: .editorCreatedPost(source: "create_button", postType: "post"),
+ animated: false
+ )
+ )
+ return true
+ case ShortcutIdentifier.Stats.type:
+ WPAnalytics.track(.shortcutStats)
+ clearCurrentViewController(rootViewPresenter.rootViewController)
+ if let mainBlog = Blog.lastUsedOrFirst(in: ContextManager.shared.mainContext) {
+ rootViewPresenter.showStats(for: mainBlog, source: .shortcut)
+ }
+ return true
+ case ShortcutIdentifier.Notifications.type:
+ WPAnalytics.track(.shortcutNotifications)
+ clearCurrentViewController(rootViewPresenter.rootViewController)
+ rootViewPresenter.showNotificationsTab()
+ return true
+ default:
+ return false
}
}
diff --git a/WordPress/Classes/System/Root View/RootViewPresenter+EditorNavigation.swift b/WordPress/Classes/System/Root View/RootViewPresenter+EditorNavigation.swift
index 722d42884ba0..feee1fdf3f96 100644
--- a/WordPress/Classes/System/Root View/RootViewPresenter+EditorNavigation.swift
+++ b/WordPress/Classes/System/Root View/RootViewPresenter+EditorNavigation.swift
@@ -1,7 +1,5 @@
import Foundation
-import SwiftUI
import WordPressData
-import WordPressShared
extension RootViewPresenter {
func currentOrLastBlog() -> Blog? {
@@ -12,120 +10,69 @@ extension RootViewPresenter {
return Blog.lastUsedOrFirst(in: context)
}
- func showPostEditor(
- animated: Bool = true,
- post: Post? = nil,
+ func showNewPostEditor(
blog: Blog? = nil,
- completion afterDismiss: (() -> Void)? = nil
+ context: NewPostEditorContext = .init(
+ analytics: .editorCreatedPost(source: "create_button", postType: "post")
+ )
) {
if rootViewController.presentedViewController != nil {
rootViewController.dismiss(animated: false)
}
-
- guard let blog = blog ?? currentOrLastBlog() else {
- return
+ guard let blog = blog ?? currentOrLastBlog() else { return }
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPost(for: blog, from: rootViewController, context: context)
}
-
- let editor: EditPostViewController
- if let post {
- editor = EditPostViewController(post: post)
- } else {
- editor = EditPostViewController(blog: blog)
- }
- editor.modalPresentationStyle = .fullScreen
- editor.showImmediately = !animated
- editor.afterDismiss = afterDismiss
-
- let properties = [WPAppAnalyticsKeyTapSource: "create_button", WPAppAnalyticsKeyPostType: "post"]
- WPAppAnalytics.track(.editorCreatedPost, properties: properties, blog: blog)
- rootViewController.present(editor, animated: false)
}
- func showCoreRESTPostEditor(blog: Blog) {
+ func showPostEditor(
+ post: Post,
+ animated: Bool = true,
+ completion afterDismiss: (() -> Void)? = nil
+ ) {
if rootViewController.presentedViewController != nil {
rootViewController.dismiss(animated: false)
}
-
- let properties = [WPAppAnalyticsKeyTapSource: "create_button", WPAppAnalyticsKeyPostType: "post"]
- WPAppAnalytics.track(.editorCreatedPost, properties: properties, blog: blog)
- presentCoreRESTEditor(blog: blog, postType: .posts)
+ let editor = EditPostViewController(post: post)
+ editor.showImmediately = !animated
+ editor.afterDismiss = afterDismiss
+ rootViewController.present(editor, animated: false)
}
- /// - parameter blog: Blog to a add a page to. Uses the current or last blog if not provided
- func showPageEditor(
+ func showNewPageEditor(
blog: Blog? = nil,
- title: String? = nil,
- content: String? = nil,
- source: String = "create_button"
+ context: NewPostEditorContext = .init(
+ analytics: .editorCreatedPage(source: "create_button")
+ )
) {
-
- // If we are already showing a view controller, dismiss and show the editor afterward
guard rootViewController.presentedViewController == nil else {
rootViewController.dismiss(animated: true) { [weak self] in
- self?.showPageEditor(blog: blog, title: title, content: content, source: source)
+ self?.showNewPageEditor(blog: blog, context: context)
}
return
}
- guard let blog = blog ?? self.currentOrLastBlog() else {
- return
- }
- guard content == nil else {
- showEditor(blog: blog, title: title, content: content)
+ guard let blog = blog ?? currentOrLastBlog() else { return }
+ guard context.content == nil else {
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPage(for: blog, from: rootViewController, context: context)
+ }
return
}
- WPAnalytics.track(
- WPAnalyticsEvent.editorCreatedPage,
- properties: [WPAppAnalyticsKeyTapSource: source],
- blog: blog
- )
- PageCoordinator.showLayoutPickerIfNeeded(from: rootViewController, forBlog: blog) {
- [weak self] selectedLayout in
- self?.showEditor(blog: blog, title: selectedLayout?.title, content: selectedLayout?.content)
+ var context = context
+ MainActor.assumeIsolated {
+ context.trackAnalytics(for: blog)
}
- }
-
- func showCoreRESTPageEditor(blog: Blog, source: String = "create_button") {
- guard rootViewController.presentedViewController == nil else {
- rootViewController.dismiss(animated: true) { [weak self] in
- self?.showCoreRESTPageEditor(blog: blog, source: source)
- }
- return
- }
- WPAnalytics.track(
- WPAnalyticsEvent.editorCreatedPage,
- properties: [WPAppAnalyticsKeyTapSource: source],
- blog: blog
- )
+ context.analytics = .none
PageCoordinator.showLayoutPickerIfNeeded(from: rootViewController, forBlog: blog) {
[weak self] selectedLayout in
- let initialContent = selectedLayout.map {
- EditorContent(title: $0.title ?? "", content: $0.content)
+ guard let self else { return }
+ var context = context
+ context.title = selectedLayout?.title
+ context.content = selectedLayout?.content
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPage(for: blog, from: self.rootViewController, context: context)
}
- self?.presentCoreRESTEditor(blog: blog, postType: .pages, initialContent: initialContent)
}
}
-
- private func presentCoreRESTEditor(
- blog: Blog,
- postType: PinnedPostType,
- initialContent: EditorContent? = nil
- ) {
- let controller = UIHostingController(rootView: AnyView(EmptyView()))
- controller.rootView = AnyView(
- CoreRESTPostEditorRoute(
- blog: blog,
- postType: postType,
- initialContent: initialContent,
- presentingViewController: controller
- )
- )
- controller.modalPresentationStyle = .fullScreen
- rootViewController.present(controller, animated: true)
- }
-
- private func showEditor(blog: Blog, title: String?, content: String?) {
- let editorViewController = EditPageViewController(blog: blog, postTitle: title, content: content)
- rootViewController.present(editorViewController, animated: false)
- }
}
diff --git a/WordPress/Classes/System/WordPressAppDelegate+openURL.swift b/WordPress/Classes/System/WordPressAppDelegate+openURL.swift
index d5e7f389c5dc..112437b4e347 100644
--- a/WordPress/Classes/System/WordPressAppDelegate+openURL.swift
+++ b/WordPress/Classes/System/WordPressAppDelegate+openURL.swift
@@ -155,19 +155,14 @@ import WordPressShared
// Should more formats be accepted in the future, this line would have to be expanded to accomodate it.
let contentEscaped = contentRaw.escapeHtmlNamedEntities()
- let post = blog.createDraftPost()
- post.postTitle = title
- post.content = contentEscaped
- post.tags = tags
-
- let postVC = EditPostViewController(post: post)
- postVC.modalPresentationStyle = .fullScreen
-
- RootViewCoordinator.sharedPresenter.rootViewController.present(postVC, animated: true, completion: nil)
-
- WPAppAnalytics.track(
- .editorCreatedPost,
- withProperties: [WPAppAnalyticsKeyTapSource: "url_scheme", WPAppAnalyticsKeyPostType: "post"]
+ RootViewCoordinator.sharedPresenter.showNewPostEditor(
+ blog: blog,
+ context: NewPostEditorContext(
+ title: title,
+ content: contentEscaped,
+ tags: tags,
+ analytics: .editorCreatedPost(source: "url_scheme", postType: "post")
+ )
)
return true
@@ -196,11 +191,13 @@ import WordPressShared
// Should more formats be accepted be accepted in the future, this line would have to be expanded to accomodate it.
let contentEscaped = contentRaw.escapeHtmlNamedEntities()
- RootViewCoordinator.sharedPresenter.showPageEditor(
+ RootViewCoordinator.sharedPresenter.showNewPageEditor(
blog: blog,
- title: title,
- content: contentEscaped,
- source: "url_scheme"
+ context: NewPostEditorContext(
+ title: title,
+ content: contentEscaped,
+ analytics: .editorCreatedPage(source: "url_scheme")
+ )
)
return true
diff --git a/WordPress/Classes/Utility/Universal Links/Route+Page.swift b/WordPress/Classes/Utility/Universal Links/Route+Page.swift
index be9a6de556a0..f89407b7cd10 100644
--- a/WordPress/Classes/Utility/Universal Links/Route+Page.swift
+++ b/WordPress/Classes/Utility/Universal Links/Route+Page.swift
@@ -16,6 +16,6 @@ struct NewPageForSiteRoute: Route {
struct NewPageNavigationAction: NavigationAction {
func perform(_ values: [String: String], source: UIViewController? = nil, router: LinkRouter) {
- RootViewCoordinator.sharedPresenter.showPageEditor(blog: blog(from: values))
+ RootViewCoordinator.sharedPresenter.showNewPageEditor(blog: blog(from: values))
}
}
diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Post.swift b/WordPress/Classes/Utility/Universal Links/Routes+Post.swift
index cd009ca55e99..fa6612a1ac2f 100644
--- a/WordPress/Classes/Utility/Universal Links/Routes+Post.swift
+++ b/WordPress/Classes/Utility/Universal Links/Routes+Post.swift
@@ -16,6 +16,6 @@ struct NewPostForSiteRoute: Route {
struct NewPostNavigationAction: NavigationAction {
func perform(_ values: [String: String], source: UIViewController? = nil, router: LinkRouter) {
- RootViewCoordinator.sharedPresenter.showPostEditor(blog: blog(from: values))
+ RootViewCoordinator.sharedPresenter.showNewPostEditor(blog: blog(from: values))
}
}
diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift
index b2a663808365..d63657fa4e20 100644
--- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift
+++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Pages/PagesCardViewModel.swift
@@ -57,7 +57,8 @@ class PagesCardViewModel: NSObject {
typealias Snapshot = NSDiffableDataSourceSnapshot
typealias PagesSnapshot = NSDiffableDataSourceSnapshot
- lazy var diffableDataSource = DataSource(tableView: view!.tableView) { [weak self] tableView, indexPath, item -> UITableViewCell? in
+ lazy var diffableDataSource = DataSource(tableView: view!.tableView) {
+ [weak self] tableView, indexPath, item -> UITableViewCell? in
guard let self else {
return nil
}
@@ -67,11 +68,20 @@ class PagesCardViewModel: NSObject {
case .ghost:
return self.configureGhostCell(tableView: tableView, indexPath: indexPath)
case .createPage(let compact, let hasPages):
- return self.configureCreationCell(compact: compact, hasPages: hasPages, tableView: tableView, indexPath: indexPath)
+ return self.configureCreationCell(
+ compact: compact,
+ hasPages: hasPages,
+ tableView: tableView,
+ indexPath: indexPath
+ )
}
}
- init(blog: Blog, view: PagesCardView, managedObjectContext: NSManagedObjectContext = ContextManager.shared.mainContext) {
+ init(
+ blog: Blog,
+ view: PagesCardView,
+ managedObjectContext: NSManagedObjectContext = ContextManager.shared.mainContext
+ ) {
self.blog = blog
self.view = view
self.managedObjectContext = managedObjectContext
@@ -105,14 +115,28 @@ class PagesCardViewModel: NSObject {
guard let viewController = view?.parentViewController else {
return
}
+ trackCreateSectionTapped()
+ var context = NewPostEditorContext(
+ analytics: .editorCreatedPage(source: Constants.analyticsPageCreationSource),
+ animated: false
+ )
+ context.trackAnalytics(for: blog)
+ context.analytics = .none
PageCoordinator.showLayoutPickerIfNeeded(from: viewController, forBlog: blog) { [weak self] selectedLayout in
guard let blog = self?.blog else {
return
}
- let editorViewController = EditPageViewController(blog: blog, postTitle: selectedLayout?.title, content: selectedLayout?.content)
- viewController.present(editorViewController, animated: false)
+ var context = context
+ context.title = selectedLayout?.title
+ context.content = selectedLayout?.content
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPage(
+ for: blog,
+ from: viewController,
+ context: context
+ )
+ }
}
- trackCreateSectionTapped()
}
func trackPageTapped() {
@@ -121,10 +145,6 @@ class PagesCardViewModel: NSObject {
private func trackCreateSectionTapped() {
trackCardItemTapped(itemType: Constants.analyticsCreationItemType)
-
- WPAnalytics.track(WPAnalyticsEvent.editorCreatedPage,
- properties: [WPAppAnalyticsKeyTapSource: Constants.analyticsPageCreationSource],
- blog: blog)
}
private func trackCardItemTapped(itemType: String) {
@@ -132,9 +152,11 @@ class PagesCardViewModel: NSObject {
Constants.analyticsTypeKey: DashboardCard.pages.rawValue,
Constants.analyticsItemTypeKey: itemType
]
- WPAnalytics.track(.dashboardCardItemTapped,
- properties: properties,
- blog: blog)
+ WPAnalytics.track(
+ .dashboardCardItemTapped,
+ properties: properties,
+ blog: blog
+ )
}
func tearDown() {
@@ -146,12 +168,18 @@ class PagesCardViewModel: NSObject {
// MARK: Cells Configuration
private extension PagesCardViewModel {
- private func configurePageCell(objectID: NSManagedObjectID, tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
+ private func configurePageCell(
+ objectID: NSManagedObjectID,
+ tableView: UITableView,
+ indexPath: IndexPath
+ ) -> UITableViewCell {
guard let page = try? self.managedObjectContext.existingObject(with: objectID) as? Page else {
return UITableViewCell()
}
- let cell = tableView.dequeueReusableCell(withIdentifier: DashboardPageCell.defaultReuseID, for: indexPath) as? DashboardPageCell
+ let cell =
+ tableView.dequeueReusableCell(withIdentifier: DashboardPageCell.defaultReuseID, for: indexPath)
+ as? DashboardPageCell
cell?.accessoryType = .none
cell?.configure(using: page, rowIndex: indexPath.row)
@@ -160,23 +188,38 @@ private extension PagesCardViewModel {
}
private func configureGhostCell(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: BlogDashboardPostCardGhostCell.defaultReuseID, for: indexPath) as? BlogDashboardPostCardGhostCell
- let style = GhostStyle(beatDuration: GhostStyle.Defaults.beatDuration,
- beatStartColor: UIAppColor.placeholderElement,
- beatEndColor: UIAppColor.placeholderElementFaded)
+ let cell =
+ tableView.dequeueReusableCell(withIdentifier: BlogDashboardPostCardGhostCell.defaultReuseID, for: indexPath)
+ as? BlogDashboardPostCardGhostCell
+ let style = GhostStyle(
+ beatDuration: GhostStyle.Defaults.beatDuration,
+ beatStartColor: UIAppColor.placeholderElement,
+ beatEndColor: UIAppColor.placeholderElementFaded
+ )
cell?.contentView.stopGhostAnimation()
cell?.contentView.startGhostAnimation(style: style)
return cell ?? UITableViewCell()
}
- private func configureCreationCell(compact: Bool, hasPages: Bool, tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
+ private func configureCreationCell(
+ compact: Bool,
+ hasPages: Bool,
+ tableView: UITableView,
+ indexPath: IndexPath
+ ) -> UITableViewCell {
var cell: DashboardPageCreationCell?
if compact {
- cell = tableView.dequeueReusableCell(withIdentifier: DashboardPageCreationCompactCell.defaultReuseID,
- for: indexPath) as? DashboardPageCreationCell
+ cell =
+ tableView.dequeueReusableCell(
+ withIdentifier: DashboardPageCreationCompactCell.defaultReuseID,
+ for: indexPath
+ ) as? DashboardPageCreationCell
} else {
- cell = tableView.dequeueReusableCell(withIdentifier: DashboardPageCreationExpandedCell.defaultReuseID,
- for: indexPath) as? DashboardPageCreationCell
+ cell =
+ tableView.dequeueReusableCell(
+ withIdentifier: DashboardPageCreationExpandedCell.defaultReuseID,
+ for: indexPath
+ ) as? DashboardPageCreationCell
}
cell?.viewModel = self
cell?.configure(hasPages: hasPages)
@@ -202,7 +245,12 @@ private extension PagesCardViewModel {
fetchedResultsController?.delegate = nil
fetchedResultsController = nil
- fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
+ fetchedResultsController = NSFetchedResultsController(
+ fetchRequest: fetchRequest(),
+ managedObjectContext: managedObjectContext,
+ sectionNameKeyPath: nil,
+ cacheName: nil
+ )
fetchedResultsController?.delegate = self
}
@@ -221,7 +269,7 @@ private extension PagesCardViewModel {
}
func sortDescriptorsForFetchRequest() -> [NSSortDescriptor] {
- return filter.sortDescriptors
+ filter.sortDescriptors
}
func sync() {
@@ -238,15 +286,18 @@ private extension PagesCardViewModel {
// Only show loading state if there are no pages at all
if numberOfPages == 0 && isSyncing {
currentState = .loading
- }
- else {
+ } else {
currentState = .loaded
}
}
func trackCardDisplayedIfNeeded() {
if currentState == .loaded {
- BlogDashboardAnalytics.shared.track(.dashboardCardShown, properties: ["type": DashboardCard.pages.rawValue], blog: blog)
+ BlogDashboardAnalytics.shared.track(
+ .dashboardCardShown,
+ properties: ["type": DashboardCard.pages.rawValue],
+ blog: blog
+ )
}
}
@@ -263,12 +314,15 @@ private extension PagesCardViewModel {
// MARK: DashboardPostsSyncManagerListener
extension PagesCardViewModel: DashboardPostsSyncManagerListener {
- func postsSynced(success: Bool,
- blog: Blog,
- postType: DashboardPostsSyncManager.PostType,
- for statuses: [BasePost.Status]) {
+ func postsSynced(
+ success: Bool,
+ blog: Blog,
+ postType: DashboardPostsSyncManager.PostType,
+ for statuses: [BasePost.Status]
+ ) {
guard postType == .page,
- self.blog == blog else {
+ self.blog == blog
+ else {
return
}
@@ -282,7 +336,10 @@ extension PagesCardViewModel: DashboardPostsSyncManagerListener {
// MARK: - NSFetchedResultsControllerDelegate
extension PagesCardViewModel: NSFetchedResultsControllerDelegate {
- func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+ func controller(
+ _ controller: NSFetchedResultsController,
+ didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
+ ) {
guard let dataSource = view?.tableView.dataSource as? DataSource else {
return
}
@@ -313,7 +370,8 @@ extension PagesCardViewModel: NSFetchedResultsControllerDelegate {
var adjustedPagesSnapshot = pagesSnapshot
// Delete extra pages
- let sequenceToDelete = adjustedPagesSnapshot.itemIdentifiers.enumerated().filter { $0.offset > Constants.numberOfPages - 1 }
+ let sequenceToDelete = adjustedPagesSnapshot.itemIdentifiers.enumerated()
+ .filter { $0.offset > Constants.numberOfPages - 1 }
let managedObjectIDsToDelete = sequenceToDelete.map { $0.element }
adjustedPagesSnapshot.deleteItems(managedObjectIDsToDelete)
@@ -322,15 +380,19 @@ extension PagesCardViewModel: NSFetchedResultsControllerDelegate {
snapshot.appendItems(pageItems, toSection: .pages)
// Reload items if needed
- let reloadIdentifiers = identifiersToBeReloaded(newSnapshot: snapshot,
- currentSnapshot: currentSnapshot,
- pagesSnapshot: pagesSnapshot)
+ let reloadIdentifiers = identifiersToBeReloaded(
+ newSnapshot: snapshot,
+ currentSnapshot: currentSnapshot,
+ pagesSnapshot: pagesSnapshot
+ )
snapshot.reloadItems(reloadIdentifiers)
// Add Create Page Item
// Section should be compact if there are more than one pages. Should be expanded otherwise.
- let createPageItem: PagesListItem = .createPage(compact: pageItems.hasMultiplePages,
- hasPages: pageItems.hasPages)
+ let createPageItem: PagesListItem = .createPage(
+ compact: pageItems.hasMultiplePages,
+ hasPages: pageItems.hasPages
+ )
snapshot.appendItems([createPageItem], toSection: .create)
case .loading:
@@ -347,16 +409,23 @@ extension PagesCardViewModel: NSFetchedResultsControllerDelegate {
/// - currentSnapshot: The old snapshot. Used to check if the item changed position
/// - pagesSnapshot: Snapshot retrieved from CoreDate
/// - Returns: Array of items that should be reloaded
- private func identifiersToBeReloaded(newSnapshot: Snapshot, currentSnapshot: Snapshot, pagesSnapshot: PagesSnapshot) -> [PagesListItem] {
- return newSnapshot.itemIdentifiers.compactMap { item in
+ private func identifiersToBeReloaded(
+ newSnapshot: Snapshot,
+ currentSnapshot: Snapshot,
+ pagesSnapshot: PagesSnapshot
+ ) -> [PagesListItem] {
+ newSnapshot.itemIdentifiers.compactMap { item in
guard case PagesListItem.page(let objectID) = item,
- let currentIndex = currentSnapshot.indexOfItem(item),
- let index = pagesSnapshot.indexOfItem(objectID),
- index == currentIndex else {
+ let currentIndex = currentSnapshot.indexOfItem(item),
+ let index = pagesSnapshot.indexOfItem(objectID),
+ index == currentIndex
+ else {
return nil // No need to reload if the index changed
}
- guard let existingObject = try? fetchedResultsController?.managedObjectContext.existingObject(with: objectID),
- existingObject.isUpdated else {
+ guard
+ let existingObject = try? fetchedResultsController?.managedObjectContext.existingObject(with: objectID),
+ existingObject.isUpdated
+ else {
return nil // No need to reload if the object wasn't updated
}
return item
@@ -372,10 +441,10 @@ extension PagesCardViewModel: NSFetchedResultsControllerDelegate {
private extension Array where Element == PagesListItem {
var hasPages: Bool {
- return !isEmpty
+ !isEmpty
}
var hasMultiplePages: Bool {
- return count > 1
+ count > 1
}
}
diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift
index 3db7569b82d8..151add09f90b 100644
--- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift
+++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift
@@ -41,7 +41,7 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
}
private lazy var bloggingPromptsService: BloggingPromptsService? = {
- return BloggingPromptsService(blog: blog)
+ BloggingPromptsService(blog: blog)
}()
/// When set to true, a "default" version of the card is displayed. That is:
@@ -103,7 +103,10 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(promptLabel)
- view.pinSubviewToAllEdges(promptLabel, insets: UIEdgeInsets(top: Constants.spacing, left: 0, bottom: 0, right: 0))
+ view.pinSubviewToAllEdges(
+ promptLabel,
+ insets: UIEdgeInsets(top: Constants.spacing, left: 0, bottom: 0, right: 0)
+ )
return view
}()
@@ -164,8 +167,8 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
button.titleLabel?.font = WPStyleGuide.BloggingPrompts.answerInfoButtonFont
button.setTitleColor(
RemoteFeatureFlag.bloggingPromptsSocial.enabled()
- ? WPStyleGuide.BloggingPrompts.buttonTitleColor
- : WPStyleGuide.BloggingPrompts.answerInfoButtonColor,
+ ? WPStyleGuide.BloggingPrompts.buttonTitleColor
+ : WPStyleGuide.BloggingPrompts.answerInfoButtonColor,
for: .normal
)
button.titleLabel?.numberOfLines = 0
@@ -177,7 +180,8 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
@objc
private func didTapAnswerInfoButton() {
guard RemoteFeatureFlag.bloggingPromptsSocial.enabled(),
- let promptID = prompt?.promptID else {
+ let promptID = prompt?.promptID
+ else {
return
}
let tagName = "\(ReaderTagTopic.dailyPromptTag)-\(promptID)"
@@ -229,7 +233,9 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
private lazy var attributionTrailingImage = UIImageView()
private lazy var attributionStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [attributionIcon, attributionSourceLabel, attributionTrailingImage])
+ let stackView = UIStackView(arrangedSubviews: [
+ attributionIcon, attributionSourceLabel, attributionTrailingImage
+ ])
stackView.setCustomSpacing(Constants.attributionTrailingImageSpacing, after: attributionSourceLabel)
stackView.alignment = .center
@@ -338,15 +344,23 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
}
private var contextMenu: UIMenu {
- return .init(title: String(), options: .displayInline, children: contextMenuItems.map { menuSection in
- UIMenu(title: String(), options: .displayInline, children: [
- // Use an uncached deferred element so we can track each time the menu is shown
- UIDeferredMenuElement.uncached { completion in
- WPAnalytics.track(.promptsDashboardCardMenu)
- completion(menuSection.map { $0.toAction })
- }
- ])
- })
+ .init(
+ title: String(),
+ options: .displayInline,
+ children: contextMenuItems.map { menuSection in
+ UIMenu(
+ title: String(),
+ options: .displayInline,
+ children: [
+ // Use an uncached deferred element so we can track each time the menu is shown
+ UIDeferredMenuElement.uncached { completion in
+ WPAnalytics.track(.promptsDashboardCardMenu)
+ completion(menuSection.map { $0.toAction })
+ }
+ ]
+ )
+ }
+ )
}
// MARK: Initializers
@@ -364,7 +378,8 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// refresh when the appearance style changed so the placeholder images are correctly recolored.
if let previousTraitCollection,
- previousTraitCollection.userInterfaceStyle != traitCollection.userInterfaceStyle {
+ previousTraitCollection.userInterfaceStyle != traitCollection.userInterfaceStyle
+ {
refreshStackView()
}
}
@@ -380,8 +395,9 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable {
// and therefore should not be shown.
static func shouldShowCard(for blog: Blog) -> Bool {
guard FeatureFlag.bloggingPrompts.enabled,
- blog.isAccessibleThroughWPCom,
- let promptsService = BloggingPromptsService(blog: blog) else {
+ blog.isAccessibleThroughWPCom,
+ let promptsService = BloggingPromptsService(blog: blog)
+ else {
return false
}
@@ -449,10 +465,10 @@ private extension DashboardPromptsCardCell {
func observeManagedObjectsChange() {
NotificationCenter.default.addObserver(
- self,
- selector: #selector(handleObjectsChange),
- name: .NSManagedObjectContextObjectsDidChange,
- object: ContextManager.shared.mainContext
+ self,
+ selector: #selector(handleObjectsChange),
+ name: .NSManagedObjectContextObjectsDidChange,
+ object: ContextManager.shared.mainContext
)
}
@@ -477,36 +493,46 @@ private extension DashboardPromptsCardCell {
return
}
- bloggingPromptsService.todaysPrompt(success: { [weak self] prompt in
- self?.prompt = prompt
- self?.didFailLoadingPrompt = false
- }, failure: { [weak self] error in
- self?.prompt = nil
- self?.didFailLoadingPrompt = true
- DDLogError("Failed fetching blogging prompt: \(String(describing: error))")
- })
+ bloggingPromptsService.todaysPrompt(
+ success: { [weak self] prompt in
+ self?.prompt = prompt
+ self?.didFailLoadingPrompt = false
+ },
+ failure: { [weak self] error in
+ self?.prompt = nil
+ self?.didFailLoadingPrompt = true
+ DDLogError("Failed fetching blogging prompt: \(String(describing: error))")
+ }
+ )
}
// MARK: Button actions
@objc func answerButtonTapped() {
guard let blog,
- let prompt else {
+ let prompt
+ else {
return
}
WPAnalytics.track(.promptsDashboardCardAnswerPrompt)
- let editor = EditPostViewController(blog: blog, prompt: prompt)
- editor.modalPresentationStyle = .fullScreen
- editor.entryPoint = .bloggingPromptsDashboardCard
- presenterViewController?.present(editor, animated: true)
+ guard let presenterViewController else { return }
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: presenterViewController,
+ context: NewPostEditorContext(
+ prompt: prompt,
+ entryPoint: .bloggingPromptsDashboardCard
+ )
+ )
}
// MARK: Context menu actions
func viewMoreMenuTapped() {
guard let blog,
- let presenterViewController else {
+ let presenterViewController
+ else {
DDLogError("Failed showing Blogging Prompts from Dashboard card. Missing blog or controller.")
return
}
@@ -519,7 +545,11 @@ private extension DashboardPromptsCardCell {
WPAnalytics.track(.promptsDashboardCardMenuSkip)
saveSkippedPromptForSite()
presenterViewController?.reloadCardsLocally()
- let notice = Notice(title: Strings.promptSkippedTitle, feedbackType: .success, actionTitle: Strings.undoSkipTitle) { [weak self] _ in
+ let notice = Notice(
+ title: Strings.promptSkippedTitle,
+ feedbackType: .success,
+ actionTitle: Strings.undoSkipTitle
+ ) { [weak self] _ in
self?.clearSkippedPromptForSite()
self?.presenterViewController?.reloadCardsLocally()
}
@@ -542,32 +572,62 @@ private extension DashboardPromptsCardCell {
wpAssertionFailure("invalid_state")
return
}
- BloggingPromptsIntroductionPresenter(interactionType: .actionable(blog: blog)).present(from: presenterViewController)
+ BloggingPromptsIntroductionPresenter(interactionType: .actionable(blog: blog))
+ .present(from: presenterViewController)
}
// MARK: Constants
struct Strings {
- static let examplePrompt = NSLocalizedString("Cast the movie of your life.", comment: "Example prompt for the Prompts card in Feature Introduction.")
- static let cardFrameTitle = NSLocalizedString("Prompts", comment: "Title label for the Prompts card in My Sites tab.")
- static let answerButtonTitle = NSLocalizedString("Answer Prompt", comment: "Title for a call-to-action button on the prompts card.")
- static let answeredLabelTitle = NSLocalizedString("✓ Answered", comment: "Title label that indicates the prompt has been answered.")
- static let answerInfoSingularFormat = NSLocalizedString("%1$d answer", comment: "Singular format string for displaying the number of users "
- + "that answered the blogging prompt.")
- static let answerInfoPluralFormat = NSLocalizedString("%1$d answers", comment: "Plural format string for displaying the number of users "
- + "that answered the blogging prompt.")
- static let viewAllResponses = NSLocalizedString("prompts.card.viewprompts.title",
- value: "View all responses",
- comment: "Title for a tappable string that opens the reader with a prompts tag")
- static let errorTitle = NSLocalizedString("Error loading prompt", comment: "Text displayed when there is a failure loading a blogging prompt.")
- static let promptSkippedTitle = NSLocalizedString("Prompt skipped", comment: "Title of the notification presented when a prompt is skipped")
- static let undoSkipTitle = NSLocalizedString("Undo", comment: "Button in the notification presented when a prompt is skipped")
+ static let examplePrompt = NSLocalizedString(
+ "Cast the movie of your life.",
+ comment: "Example prompt for the Prompts card in Feature Introduction."
+ )
+ static let cardFrameTitle = NSLocalizedString(
+ "Prompts",
+ comment: "Title label for the Prompts card in My Sites tab."
+ )
+ static let answerButtonTitle = NSLocalizedString(
+ "Answer Prompt",
+ comment: "Title for a call-to-action button on the prompts card."
+ )
+ static let answeredLabelTitle = NSLocalizedString(
+ "✓ Answered",
+ comment: "Title label that indicates the prompt has been answered."
+ )
+ static let answerInfoSingularFormat = NSLocalizedString(
+ "%1$d answer",
+ comment: "Singular format string for displaying the number of users "
+ + "that answered the blogging prompt."
+ )
+ static let answerInfoPluralFormat = NSLocalizedString(
+ "%1$d answers",
+ comment: "Plural format string for displaying the number of users "
+ + "that answered the blogging prompt."
+ )
+ static let viewAllResponses = NSLocalizedString(
+ "prompts.card.viewprompts.title",
+ value: "View all responses",
+ comment: "Title for a tappable string that opens the reader with a prompts tag"
+ )
+ static let errorTitle = NSLocalizedString(
+ "Error loading prompt",
+ comment: "Text displayed when there is a failure loading a blogging prompt."
+ )
+ static let promptSkippedTitle = NSLocalizedString(
+ "Prompt skipped",
+ comment: "Title of the notification presented when a prompt is skipped"
+ )
+ static let undoSkipTitle = NSLocalizedString(
+ "Undo",
+ comment: "Button in the notification presented when a prompt is skipped"
+ )
}
struct Style {
static var avatarPlaceholderImage: UIImage {
// this needs to be computed so the color is correct depending on the user interface style.
- return UIImage(color: UIColor(light: .quaternarySystemFill, dark: .systemGray4))
+ UIImage(color: UIColor(light: .quaternarySystemFill, dark: .systemGray4))
}
}
@@ -598,9 +658,15 @@ private extension DashboardPromptsCardCell {
case .skip:
return NSLocalizedString("Skip for today", comment: "Menu title to skip today's prompt.")
case .remove:
- return NSLocalizedString("Turn off prompts", comment: "Destructive menu title to remove the prompt card from the dashboard.")
+ return NSLocalizedString(
+ "Turn off prompts",
+ comment: "Destructive menu title to remove the prompt card from the dashboard."
+ )
case .learnMore:
- return NSLocalizedString("Learn more", comment: "Menu title to show the prompts feature introduction modal.")
+ return NSLocalizedString(
+ "Learn more",
+ comment: "Menu title to show the prompts feature introduction modal."
+ )
}
}
@@ -629,9 +695,9 @@ private extension DashboardPromptsCardCell {
var toAction: UIAction {
switch self {
case .viewMore(let handler),
- .skip(let handler),
- .remove(let handler),
- .learnMore(let handler):
+ .skip(let handler),
+ .remove(let handler),
+ .learnMore(let handler):
return UIAction(title: title, image: image, attributes: menuAttributes) { _ in
handler()
}
@@ -645,12 +711,13 @@ private extension DashboardPromptsCardCell {
private extension DashboardPromptsCardCell {
static var allSkippedPrompts: [[String: Int32]] {
- return UserPersistentStoreFactory.instance().array(forKey: Constants.skippedPromptsUDKey) as? [[String: Int32]] ?? []
+ UserPersistentStoreFactory.instance().array(forKey: Constants.skippedPromptsUDKey) as? [[String: Int32]] ?? []
}
func saveSkippedPromptForSite() {
guard let prompt,
- let siteID = blog?.dotComID?.stringValue else {
+ let siteID = blog?.dotComID?.stringValue
+ else {
return
}
@@ -674,7 +741,8 @@ private extension DashboardPromptsCardCell {
static func userSkippedPrompt(_ prompt: BloggingPrompt, for blog: Blog) -> Bool {
guard FeatureFlag.bloggingPrompts.enabled,
- let siteID = blog.dotComID?.stringValue else {
+ let siteID = blog.dotComID?.stringValue
+ else {
return false
}
diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift
index 391e97e0b5b5..ae2211b800e4 100644
--- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift
+++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift
@@ -76,7 +76,7 @@ extension BlogDetailsViewController {
public func showPostList(from source: BlogDetailsNavigationSource) {
trackEvent(.openedPosts, from: source)
- if blog.isSelfHosted, blog.isXMLRPCDisabled {
+ if blog.usesCustomPostTypeViewsForPostsAndPages {
showPinnedPostType(.posts)
return
}
@@ -89,7 +89,7 @@ extension BlogDetailsViewController {
public func showPageList(from source: BlogDetailsNavigationSource) {
trackEvent(.openedPages, from: source)
- if blog.isSelfHosted, blog.isXMLRPCDisabled {
+ if blog.usesCustomPostTypeViewsForPostsAndPages {
showPinnedPostType(.pages)
return
}
diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift
index 6bff92074e42..2461eed59f27 100644
--- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift
+++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptCoordinator.swift
@@ -49,22 +49,29 @@ import WordPressData
/// - promptID: The ID of the blogging prompt. When nil, the method will use today's prompt.
/// - blog: The blog associated with the blogging prompt.
/// - completion: Closure invoked after the post creation flow is presented.
- func showPromptAnsweringFlow(from viewController: UIViewController,
- promptID: Int? = nil,
- blog: Blog,
- source: Source,
- completion: (() -> Void)? = nil) {
+ func showPromptAnsweringFlow(
+ from viewController: UIViewController,
+ promptID: Int? = nil,
+ blog: Blog,
+ source: Source,
+ completion: (() -> Void)? = nil
+ ) {
fetchPrompt(with: promptID, blog: blog) { result in
guard case .success(let prompt) = result else {
completion?()
return
}
- // Present the post creation flow.
- let editor = EditPostViewController(blog: blog, prompt: prompt)
- editor.modalPresentationStyle = .fullScreen
- editor.entryPoint = source.editorEntryPoint
- viewController.present(editor, animated: true)
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: viewController,
+ context: NewPostEditorContext(
+ prompt: prompt,
+ entryPoint: source.editorEntryPoint
+ )
+ )
+ }
completion?()
}
}
@@ -76,16 +83,18 @@ import WordPressData
/// - completion: Closure invoked after the scheduling process completes.
func updatePromptsIfNeeded(for blog: Blog, completion: ((Result) -> Void)? = nil) {
guard FeatureFlag.bloggingPrompts.enabled,
- let service = self.promptsServiceFactory.makeService(for: blog) else {
+ let service = self.promptsServiceFactory.makeService(for: blog)
+ else {
return
}
// fetch and update local prompts.
service.fetchPrompts { [weak self] _ in
// try to reschedule prompts if the user has any active reminders.
- self?.reschedulePromptRemindersIfNeeded(for: blog) {
- completion?(.success(()))
- }
+ self?
+ .reschedulePromptRemindersIfNeeded(for: blog) {
+ completion?(.success(()))
+ }
} failure: { error in
completion?(.failure(error ?? Errors.unknown))
}
@@ -114,37 +123,47 @@ private extension BloggingPromptCoordinator {
}
}
- private func reschedulePromptRemindersIfNeeded(for blog: Blog, in context: NSManagedObjectContext, completion: @escaping () -> Void) {
+ private func reschedulePromptRemindersIfNeeded(
+ for blog: Blog,
+ in context: NSManagedObjectContext,
+ completion: @escaping () -> Void
+ ) {
assert(blog.managedObjectContext == context)
guard let settings = try? BloggingPromptSettings.of(blog),
- let activeWeekdays = settings.reminderDays?.getActiveWeekdays(),
- let reminderTimeDate = settings.reminderTimeDate(),
- settings.promptRemindersEnabled
+ let activeWeekdays = settings.reminderDays?.getActiveWeekdays(),
+ let reminderTimeDate = settings.reminderTimeDate(),
+ settings.promptRemindersEnabled
else {
completion()
return
}
// IMPORTANT: Ensure that push authorization is already granted before rescheduling.
- UNUserNotificationCenter.current().getNotificationSettings { [weak self] notificationSettings in
- guard let self,
- notificationSettings.authorizationStatus == .authorized else {
- completion()
- return
- }
-
- context.perform {
- // Reschedule the prompt reminders.
- let schedule = BloggingRemindersScheduler.Schedule.weekdays(activeWeekdays)
- self.scheduler.schedule(schedule, for: blog, time: reminderTimeDate) { _ in
+ UNUserNotificationCenter.current()
+ .getNotificationSettings { [weak self] notificationSettings in
+ guard let self,
+ notificationSettings.authorizationStatus == .authorized
+ else {
completion()
+ return
+ }
+
+ context.perform {
+ // Reschedule the prompt reminders.
+ let schedule = BloggingRemindersScheduler.Schedule.weekdays(activeWeekdays)
+ self.scheduler.schedule(schedule, for: blog, time: reminderTimeDate) { _ in
+ completion()
+ }
}
}
- }
}
- func fetchPrompt(with localPromptID: Int? = nil, blog: Blog, completion: @escaping (Result) -> Void) {
+ func fetchPrompt(
+ with localPromptID: Int? = nil,
+ blog: Blog,
+ completion: @escaping (Result) -> Void
+ ) {
guard let service = promptsServiceFactory.makeService(for: blog) else {
completion(.failure(Errors.invalidSite))
return
@@ -152,7 +171,8 @@ private extension BloggingPromptCoordinator {
// When the promptID is specified, there may be a cached prompt available.
if let promptID = localPromptID,
- let prompt = service.loadPrompt(with: promptID, in: blog) {
+ let prompt = service.loadPrompt(with: promptID, in: blog)
+ {
completion(.success(prompt))
return
}
diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift
index 9bd7786af27e..17a1c6534010 100644
--- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift
+++ b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/BloggingPromptsViewController.swift
@@ -18,7 +18,7 @@ class BloggingPromptsViewController: UIViewController, NoResultsViewHost {
}
private lazy var bloggingPromptsService: BloggingPromptsService? = {
- return BloggingPromptsService(blog: blog)
+ BloggingPromptsService(blog: blog)
}()
private var isLoading: Bool = false {
@@ -63,8 +63,10 @@ class BloggingPromptsViewController: UIViewController, NoResultsViewHost {
private extension BloggingPromptsViewController {
func configureTableView() {
- tableView.register(BloggingPromptTableViewCell.defaultNib,
- forCellReuseIdentifier: BloggingPromptTableViewCell.defaultReuseID)
+ tableView.register(
+ BloggingPromptTableViewCell.defaultNib,
+ forCellReuseIdentifier: BloggingPromptTableViewCell.defaultReuseID
+ )
tableView.accessibilityIdentifier = "Blogging Prompts List"
tableView.allowsSelection = FeatureFlag.bloggingPrompts.enabled
@@ -88,24 +90,30 @@ private extension BloggingPromptsViewController {
func showNoResultsView() {
hideNoResults()
- configureAndDisplayNoResults(on: view,
- title: NoResults.emptyTitle,
- image: NoResults.imageName)
+ configureAndDisplayNoResults(
+ on: view,
+ title: NoResults.emptyTitle,
+ image: NoResults.imageName
+ )
}
func showLoadingView() {
hideNoResults()
- configureAndDisplayNoResults(on: view,
- title: NoResults.loadingTitle,
- accessoryView: NoResultsViewController.loadingAccessoryView())
+ configureAndDisplayNoResults(
+ on: view,
+ title: NoResults.loadingTitle,
+ accessoryView: NoResultsViewController.loadingAccessoryView()
+ )
}
func showErrorView() {
hideNoResults()
- configureAndDisplayNoResults(on: view,
- title: NoResults.errorTitle,
- subtitle: NoResults.errorSubtitle,
- image: NoResults.imageName)
+ configureAndDisplayNoResults(
+ on: view,
+ title: NoResults.errorTitle,
+ subtitle: NoResults.errorSubtitle,
+ image: NoResults.imageName
+ )
}
func fetchPrompts() {
@@ -117,14 +125,17 @@ private extension BloggingPromptsViewController {
isLoading = true
- bloggingPromptsService.fetchListPrompts(success: { [weak self] prompts in
- self?.isLoading = false
- self?.prompts = prompts.sorted(by: { $0.date.compare($1.date) == .orderedDescending })
- }, failure: { [weak self] error in
- DDLogError("Failed fetching blogging prompts: \(String(describing: error))")
- self?.isLoading = false
- self?.showErrorView()
- })
+ bloggingPromptsService.fetchListPrompts(
+ success: { [weak self] prompts in
+ self?.isLoading = false
+ self?.prompts = prompts.sorted(by: { $0.date.compare($1.date) == .orderedDescending })
+ },
+ failure: { [weak self] error in
+ DDLogError("Failed fetching blogging prompts: \(String(describing: error))")
+ self?.isLoading = false
+ self?.showErrorView()
+ }
+ )
}
enum Strings {
@@ -132,10 +143,22 @@ private extension BloggingPromptsViewController {
}
enum NoResults {
- static let loadingTitle = NSLocalizedString("Loading prompts...", comment: "Displayed while blogging prompts are being loaded.")
- static let errorTitle = NSLocalizedString("Oops", comment: "Title for the view when there's an error loading blogging prompts.")
- static let errorSubtitle = NSLocalizedString("There was an error loading prompts.", comment: "Text displayed when there is a failure loading blogging prompts.")
- static let emptyTitle = NSLocalizedString("No prompts yet", comment: "Title displayed when there are no blogging prompts to display.")
+ static let loadingTitle = NSLocalizedString(
+ "Loading prompts...",
+ comment: "Displayed while blogging prompts are being loaded."
+ )
+ static let errorTitle = NSLocalizedString(
+ "Oops",
+ comment: "Title for the view when there's an error loading blogging prompts."
+ )
+ static let errorSubtitle = NSLocalizedString(
+ "There was an error loading prompts.",
+ comment: "Text displayed when there is a failure loading blogging prompts."
+ )
+ static let emptyTitle = NSLocalizedString(
+ "No prompts yet",
+ comment: "Title displayed when there are no blogging prompts to display."
+ )
static let imageName = "wp-illustration-empty-results"
}
}
@@ -145,12 +168,15 @@ private extension BloggingPromptsViewController {
extension BloggingPromptsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return prompts.count
+ prompts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- guard let cell = tableView.dequeueReusableCell(withIdentifier: BloggingPromptTableViewCell.defaultReuseID) as? BloggingPromptTableViewCell,
- let prompt = prompts[safe: indexPath.row] else {
+ guard
+ let cell = tableView.dequeueReusableCell(withIdentifier: BloggingPromptTableViewCell.defaultReuseID)
+ as? BloggingPromptTableViewCell,
+ let prompt = prompts[safe: indexPath.row]
+ else {
return UITableViewCell()
}
@@ -161,16 +187,21 @@ extension BloggingPromptsViewController: UITableViewDataSource, UITableViewDeleg
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard FeatureFlag.bloggingPrompts.enabled,
- let blog,
- let cell = tableView.cellForRow(at: indexPath) as? BloggingPromptTableViewCell,
- let prompt = cell.prompt else {
+ let blog,
+ let cell = tableView.cellForRow(at: indexPath) as? BloggingPromptTableViewCell,
+ let prompt = cell.prompt
+ else {
return
}
- let editor = EditPostViewController(blog: blog, prompt: prompt)
- editor.modalPresentationStyle = .fullScreen
- editor.entryPoint = .bloggingPromptsListView
- present(editor, animated: true)
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: self,
+ context: NewPostEditorContext(
+ prompt: prompt,
+ entryPoint: .bloggingPromptsListView
+ )
+ )
}
}
@@ -194,7 +225,8 @@ private extension BloggingPromptsViewController {
switch self {
case .all: return NSLocalizedString("All", comment: "Title of all Blogging Prompts filter.")
case .answered: return NSLocalizedString("Answered", comment: "Title of answered Blogging Prompts filter.")
- case .notAnswered: return NSLocalizedString("Not Answered", comment: "Title of unanswered Blogging Prompts filter.")
+ case .notAnswered:
+ return NSLocalizedString("Not Answered", comment: "Title of unanswered Blogging Prompts filter.")
}
}
}
@@ -216,6 +248,6 @@ private extension BloggingPromptsViewController {
extension BloggingPromptsViewController: StoryboardLoadable {
static var defaultStoryboardName: String {
- return "BloggingPromptsViewController"
+ "BloggingPromptsViewController"
}
}
diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+FAB.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+FAB.swift
index 2030ed95fd6e..d88832172035 100644
--- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+FAB.swift
+++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController+FAB.swift
@@ -4,32 +4,20 @@ import WordPressData
import WordPressShared
extension MySiteViewController {
-
- static func shouldUseCoreRESTEditor(for blog: Blog?) -> Bool {
- guard let blog else { return false }
- return blog.isSelfHosted && blog.isXMLRPCDisabled
- }
-
/// Make a create button coordinator with
/// - Returns: CreateButtonCoordinator with new post, page, and story actions.
@objc func makeCreateButtonCoordinator() -> CreateButtonCoordinator {
- let newPage = { [weak self] in
- let presenter = RootViewCoordinator.sharedPresenter
- if let blog = self?.blog, Self.shouldUseCoreRESTEditor(for: blog) {
- presenter.showCoreRESTPageEditor(blog: blog)
- } else {
- presenter.showPageEditor()
- }
+ let newPage = {
+ RootViewCoordinator.sharedPresenter.showNewPageEditor()
}
- let newPost = { [weak self] in
- let presenter = RootViewCoordinator.sharedPresenter
- if let blog = self?.blog, Self.shouldUseCoreRESTEditor(for: blog) {
- presenter.showCoreRESTPostEditor(blog: blog)
- } else {
- presenter.showPostEditor()
- }
+ let newPost = {
+ RootViewCoordinator.sharedPresenter.showNewPostEditor(
+ context: NewPostEditorContext(
+ analytics: .editorCreatedPost(source: "create_button", postType: "post")
+ )
+ )
}
let source = "my_site"
@@ -67,10 +55,14 @@ extension MySiteViewController {
let viewModel = VoiceToContentViewModel(blog: blog) { [weak self] transcription in
guard let self else { return }
self.dismiss(animated: true) {
- let presenter = RootViewCoordinator.sharedPresenter
- let post = blog.createDraftPost()
- post.voiceContent = transcription
- presenter.showPostEditor(post: post)
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: self,
+ context: NewPostEditorContext(
+ voiceContent: transcription,
+ analytics: .editorCreatedPost(source: "create_button", postType: "post")
+ )
+ )
}
}
let view = VoiceToContentView(viewModel: viewModel)
diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/Blog+CustomPostTypes.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/Blog+CustomPostTypes.swift
new file mode 100644
index 000000000000..4aa3e0d4a71b
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/CustomPostTypes/Blog+CustomPostTypes.swift
@@ -0,0 +1,7 @@
+import WordPressData
+
+extension Blog {
+ var usesCustomPostTypeViewsForPostsAndPages: Bool {
+ isSelfHosted && isXMLRPCDisabled
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CoreRESTPostEditorRoute.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CoreRESTPostEditorRoute.swift
index 4bd7b6e11d1a..9eb3de5016c5 100644
--- a/WordPress/Classes/ViewRelated/CustomPostTypes/CoreRESTPostEditorRoute.swift
+++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CoreRESTPostEditorRoute.swift
@@ -5,7 +5,9 @@ import WordPressData
struct CoreRESTPostEditorRoute: View {
let blog: Blog
let postType: PinnedPostType
+ let initialSettings: PostSettings?
let initialContent: EditorContent?
+ let entryPoint: PostEditorEntryPoint
weak var presentingViewController: UIViewController?
@Environment(\.dismiss) private var dismiss
@@ -40,8 +42,9 @@ struct CoreRESTPostEditorRoute: View {
post: nil,
details: resolved.details,
blog: blog,
- initialSettings: nil,
- initialContent: initialContent
+ initialSettings: initialSettings,
+ initialContent: initialContent,
+ entryPoint: entryPoint
)
.toolbar(.hidden, for: .navigationBar)
)
diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditor.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditor.swift
index 1f52e9d2fc26..9e68a72a347f 100644
--- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditor.swift
+++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditor.swift
@@ -13,6 +13,7 @@ struct CustomPostEditor: View {
let blog: Blog
let initialSettings: PostSettings?
let initialContent: EditorContent?
+ var entryPoint: PostEditorEntryPoint? = nil
var body: some View {
ViewControllerWrapper(
@@ -22,7 +23,8 @@ struct CustomPostEditor: View {
details: details,
blog: blog,
initialSettings: initialSettings,
- initialContent: initialContent
+ initialContent: initialContent,
+ entryPoint: entryPoint
)
.ignoresSafeArea()
}
@@ -36,6 +38,7 @@ private struct ViewControllerWrapper: UIViewControllerRepresentable {
let blog: Blog
let initialSettings: PostSettings?
let initialContent: EditorContent?
+ let entryPoint: PostEditorEntryPoint?
@Environment(\.dismiss)
var dismiss: DismissAction
@@ -48,7 +51,8 @@ private struct ViewControllerWrapper: UIViewControllerRepresentable {
post: post,
details: details,
initialSettings: initialSettings,
- initialContent: initialContent
+ initialContent: initialContent,
+ entryPoint: entryPoint
) {
dismiss()
}
diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/PostEditorRouter.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/PostEditorRouter.swift
new file mode 100644
index 000000000000..cef6e11f25fe
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/CustomPostTypes/PostEditorRouter.swift
@@ -0,0 +1,274 @@
+import SwiftUI
+import UIKit
+import WordPressData
+import WordPressShared
+
+struct NewPostEditorContext {
+ enum Analytics {
+ case editorCreatedPost(source: String, postType: String)
+ case editorCreatedPage(source: String)
+ case none
+ }
+
+ var title: String?
+ var content: String?
+ var tags: String?
+ var prompt: BloggingPrompt?
+ var voiceContent: String?
+ var initialMedia: [Media]
+ var entryPoint: PostEditorEntryPoint
+ var analytics: Analytics
+ var animated: Bool
+ var afterDismiss: (() -> Void)?
+
+ init(
+ title: String? = nil,
+ content: String? = nil,
+ tags: String? = nil,
+ prompt: BloggingPrompt? = nil,
+ voiceContent: String? = nil,
+ initialMedia: [Media] = [],
+ entryPoint: PostEditorEntryPoint = .unknown,
+ analytics: Analytics = .none,
+ animated: Bool = true,
+ afterDismiss: (() -> Void)? = nil
+ ) {
+ self.title = title
+ self.content = content
+ self.tags = tags
+ self.prompt = prompt
+ self.voiceContent = voiceContent
+ self.initialMedia = initialMedia
+ self.entryPoint = entryPoint
+ self.analytics = analytics
+ self.animated = animated
+ self.afterDismiss = afterDismiss
+ }
+
+ func makeEditorContent() throws -> EditorContent {
+ let body = voiceContent ?? prompt?.editorContent ?? content ?? ""
+ let mediaBlocks = try initialMedia.map(Self.blockHTML(for:))
+ let combined = ([body] + mediaBlocks)
+ .filter { !$0.isEmpty }
+ .joined(separator: "\n\n")
+ return EditorContent(title: title ?? "", content: combined)
+ }
+
+ func makePostSettings(for blog: Blog) -> PostSettings {
+ var settings = PostSettings.defaults(from: blog)
+ let names = AbstractPost.makeTags(from: tags ?? "") + (prompt?.editorTags ?? [])
+ settings.tags = names.map { PostSettings.Term(id: 0, name: $0) }
+ settings.bloggingPromptID = prompt.map { String($0.promptID) }
+ return settings
+ }
+
+ func trackAnalytics(for blog: Blog) {
+ analytics.track(blog: blog)
+ }
+
+ func applyLegacyValues(to post: Post) {
+ post.postTitle = title
+ post.content = content
+ post.tags = tags
+ post.prepareForPrompt(prompt)
+ post.voiceContent = voiceContent
+ }
+
+ private static func blockHTML(for media: Media) throws -> String {
+ guard
+ let id = media.mediaID?.intValue,
+ id > 0,
+ let remoteURL = media.remoteURL,
+ let validatedURL = URL(string: remoteURL),
+ let scheme = validatedURL.scheme?.lowercased(),
+ ["http", "https"].contains(scheme),
+ validatedURL.host?.isEmpty == false
+ else {
+ throw InitialContextError.invalidMedia
+ }
+
+ let absoluteURL = validatedURL.absoluteString
+ let url = absoluteURL.escapeHtmlNamedEntities()
+ switch media.mediaType {
+ case .image:
+ let alt = (media.alt ?? "").escapeHtmlNamedEntities()
+ return """
+
+
+
+ """
+ case .video:
+ return """
+
+
+
+ """
+ case .audio:
+ return """
+
+
+
+ """
+ case .document, .powerpoint:
+ let name = (media.filename ?? absoluteURL).escapeHtmlNamedEntities()
+ return """
+
+
+
+ """
+ }
+ }
+
+ private enum InitialContextError: LocalizedError {
+ case invalidMedia
+
+ var errorDescription: String? {
+ NSLocalizedString(
+ "postEditorRouter.invalidMedia",
+ value: "The selected media could not be added.",
+ comment: "Error shown when creating a post from media without a usable remote URL."
+ )
+ }
+ }
+}
+
+@MainActor
+enum PostEditorRouter {
+ enum Destination: Equatable {
+ case legacy
+ case coreREST
+ }
+
+ static func destination(for blog: Blog) -> Destination {
+ blog.usesCustomPostTypeViewsForPostsAndPages ? .coreREST : .legacy
+ }
+
+ static func showNewPost(
+ for blog: Blog,
+ from presenter: UIViewController,
+ context: NewPostEditorContext = .init()
+ ) {
+ context.trackAnalytics(for: blog)
+ switch destination(for: blog) {
+ case .coreREST:
+ presentCoreRESTEditor(blog: blog, postType: .posts, context: context, from: presenter)
+ case .legacy:
+ let post = blog.createDraftPost()
+ context.applyLegacyValues(to: post)
+ let editor = EditPostViewController(post: post)
+ editor.insertedMedia = context.initialMedia
+ editor.entryPoint = context.entryPoint
+ editor.showImmediately = !context.animated
+ editor.afterDismiss = context.afterDismiss
+ presenter.present(editor, animated: false)
+ }
+ }
+
+ static func showNewPage(
+ for blog: Blog,
+ from presenter: UIViewController,
+ context: NewPostEditorContext = .init()
+ ) {
+ context.trackAnalytics(for: blog)
+ switch destination(for: blog) {
+ case .coreREST:
+ presentCoreRESTEditor(blog: blog, postType: .pages, context: context, from: presenter)
+ case .legacy:
+ let editor = EditPageViewController(
+ blog: blog,
+ postTitle: context.title,
+ content: context.content
+ )
+ editor.entryPoint = context.entryPoint
+ editor.onClose = context.afterDismiss
+ presenter.present(editor, animated: false)
+ }
+ }
+
+ private static func presentCoreRESTEditor(
+ blog: Blog,
+ postType: PinnedPostType,
+ context: NewPostEditorContext,
+ from presenter: UIViewController
+ ) {
+ let controller = CoreRESTPostEditorHostingController(onDismiss: context.afterDismiss)
+ let route: CoreRESTPostEditorRoute
+ do {
+ route = try makeCoreRESTRoute(
+ blog: blog,
+ postType: postType,
+ context: context,
+ presentingViewController: controller
+ )
+ } catch {
+ Notice(error: error).post()
+ return
+ }
+
+ controller.rootView = AnyView(route)
+ controller.modalPresentationStyle = .fullScreen
+ presenter.present(controller, animated: context.animated)
+ }
+
+ static func makeCoreRESTRoute(
+ blog: Blog,
+ postType: PinnedPostType,
+ context: NewPostEditorContext,
+ presentingViewController: UIViewController
+ ) throws -> CoreRESTPostEditorRoute {
+ CoreRESTPostEditorRoute(
+ blog: blog,
+ postType: postType,
+ initialSettings: context.makePostSettings(for: blog),
+ initialContent: try context.makeEditorContent(),
+ entryPoint: context.entryPoint,
+ presentingViewController: presentingViewController
+ )
+ }
+}
+
+@MainActor
+final class CoreRESTPostEditorHostingController: UIHostingController {
+ private var onDismiss: (() -> Void)?
+
+ init(onDismiss: (() -> Void)?) {
+ self.onDismiss = onDismiss
+ super.init(rootView: AnyView(EmptyView()))
+ }
+
+ @preconcurrency required dynamic init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ guard isBeingDismissed || presentingViewController == nil else { return }
+ let onDismiss = onDismiss
+ self.onDismiss = nil
+ onDismiss?()
+ }
+}
+
+private extension NewPostEditorContext.Analytics {
+ func track(blog: Blog) {
+ switch self {
+ case let .editorCreatedPost(source, postType):
+ WPAppAnalytics.track(
+ .editorCreatedPost,
+ properties: [
+ WPAppAnalyticsKeyTapSource: source,
+ WPAppAnalyticsKeyPostType: postType
+ ],
+ blog: blog
+ )
+ case let .editorCreatedPage(source):
+ WPAnalytics.track(
+ .editorCreatedPage,
+ properties: [WPAppAnalyticsKeyTapSource: source],
+ blog: blog
+ )
+ case .none:
+ break
+ }
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift
index 03b6d4718673..14123e8b16f3 100644
--- a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift
+++ b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift
@@ -38,7 +38,7 @@ class BloggingPromptsIntroductionPresenter: NSObject {
}()
private lazy var bloggingPromptsService: BloggingPromptsService? = {
- return BloggingPromptsService(blog: blogToUse())
+ BloggingPromptsService(blog: blogToUse())
}()
// MARK: - Init
@@ -81,7 +81,8 @@ private extension BloggingPromptsIntroductionPresenter {
func showPostCreation() {
guard let blog = blogToUse(),
- let presentingViewController else {
+ let presentingViewController
+ else {
wpAssertionFailure("invalid_state")
navigationController.dismiss(animated: true)
return
@@ -94,38 +95,57 @@ private extension BloggingPromptsIntroductionPresenter {
return
}
- let editor = EditPostViewController(blog: blog, prompt: prompt)
- editor.modalPresentationStyle = .fullScreen
- editor.entryPoint = .bloggingPromptsFeatureIntroduction
-
- self?.navigationController.dismiss(animated: true, completion: { [weak self] in
- presentingViewController.present(editor, animated: false)
- self?.trackPostEditorShown(blog)
- })
+ self?.navigationController
+ .dismiss(
+ animated: true,
+ completion: { [weak self] in
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: presentingViewController,
+ context: NewPostEditorContext(
+ prompt: prompt,
+ entryPoint: .bloggingPromptsFeatureIntroduction
+ )
+ )
+ self?.trackPostEditorShown(blog)
+ }
+ )
})
}
func showRemindersScheduling() {
guard let blog = blogToUse(),
- let presentingViewController else {
+ let presentingViewController
+ else {
wpAssertionFailure("invalid_state")
navigationController.dismiss(animated: true)
return
}
- navigationController.dismiss(animated: true, completion: {
- BloggingRemindersFlow.present(from: presentingViewController,
- for: blog,
- source: .bloggingPromptsFeatureIntroduction)
- })
+ navigationController.dismiss(
+ animated: true,
+ completion: {
+ BloggingRemindersFlow.present(
+ from: presentingViewController,
+ for: blog,
+ source: .bloggingPromptsFeatureIntroduction
+ )
+ }
+ )
}
func blogToUse() -> Blog? {
- return accountHasMultipleSites ? selectedBlog : accountSites?.first
+ accountHasMultipleSites ? selectedBlog : accountSites?.first
}
func trackPostEditorShown(_ blog: Blog) {
- WPAppAnalytics.track(.editorCreatedPost, properties: [WPAppAnalyticsKeyTapSource: "blogging_prompts_feature_introduction", WPAppAnalyticsKeyPostType: "post"], blog: blog)
+ WPAppAnalytics.track(
+ .editorCreatedPost,
+ properties: [
+ WPAppAnalyticsKeyTapSource: "blogging_prompts_feature_introduction", WPAppAnalyticsKeyPostType: "post"
+ ],
+ blog: blog
+ )
}
// MARK: Prompt Fetching
@@ -138,16 +158,22 @@ private extension BloggingPromptsIntroductionPresenter {
return
}
- bloggingPromptsService.fetchTodaysPrompt(success: { prompt in
- completion(prompt)
- }, failure: { error in
- completion(nil)
- DDLogError("Feature Introduction: failed fetching blogging prompt: \(String(describing: error))")
- })
+ bloggingPromptsService.fetchTodaysPrompt(
+ success: { prompt in
+ completion(prompt)
+ },
+ failure: { error in
+ completion(nil)
+ DDLogError("Feature Introduction: failed fetching blogging prompt: \(String(describing: error))")
+ }
+ )
}
func dispatchErrorNotice() {
- let message = NSLocalizedString("Error loading prompt", comment: "Text displayed when there is a failure loading a blogging prompt.")
+ let message = NSLocalizedString(
+ "Error loading prompt",
+ comment: "Text displayed when there is a failure loading a blogging prompt."
+ )
presentingViewController?.displayNotice(title: message)
}
}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaNoticeNavigationCoordinator.swift b/WordPress/Classes/ViewRelated/Media/MediaNoticeNavigationCoordinator.swift
index c8687888d8c6..00680d2f5107 100644
--- a/WordPress/Classes/ViewRelated/Media/MediaNoticeNavigationCoordinator.swift
+++ b/WordPress/Classes/ViewRelated/Media/MediaNoticeNavigationCoordinator.swift
@@ -14,11 +14,16 @@ class MediaNoticeNavigationCoordinator {
static func presentEditor(for blog: Blog, source: String, media: [Media]) {
WPAppAnalytics.track(.notificationsUploadMediaSuccessWritePost, blog: blog)
- let editor = EditPostViewController(blog: blog)
- editor.modalPresentationStyle = .fullScreen
- editor.insertedMedia = media
- RootViewCoordinator.sharedPresenter.rootViewController.present(editor, animated: false)
- WPAppAnalytics.track(.editorCreatedPost, properties: [WPAppAnalyticsKeyTapSource: source, WPAppAnalyticsKeyPostType: "post"], blog: blog)
+ MainActor.assumeIsolated {
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: RootViewCoordinator.sharedPresenter.rootViewController,
+ context: NewPostEditorContext(
+ initialMedia: media,
+ analytics: .editorCreatedPost(source: source, postType: "post")
+ )
+ )
+ }
}
static func navigateToMediaLibrary(with userInfo: NSDictionary) {
@@ -39,19 +44,20 @@ class MediaNoticeNavigationCoordinator {
let URIRepresentation = URL(string: blogID),
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: URIRepresentation),
let managedObject = try? context.existingObject(with: objectID),
- let blog = managedObject as? Blog else {
- return nil
+ let blog = managedObject as? Blog
+ else {
+ return nil
}
return blog
}
private static func successfulMedia(from userInfo: NSDictionary) -> [Media] {
- return media(from: userInfo, withKey: MediaNoticeUserInfoKey.mediaIDs)
+ media(from: userInfo, withKey: MediaNoticeUserInfoKey.mediaIDs)
}
private static func failedMedia(from userInfo: NSDictionary) -> [Media] {
- return media(from: userInfo, withKey: MediaNoticeUserInfoKey.failedMediaIDs)
+ media(from: userInfo, withKey: MediaNoticeUserInfoKey.failedMediaIDs)
}
private static func media(from userInfo: NSDictionary, withKey key: String) -> [Media] {
@@ -60,9 +66,11 @@ class MediaNoticeNavigationCoordinator {
if let mediaIDs = userInfo[key] as? [String] {
let media = mediaIDs.compactMap({ mediaID -> Media? in
if let URIRepresentation = URL(string: mediaID),
- let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: URIRepresentation),
+ let objectID = context.persistentStoreCoordinator?
+ .managedObjectID(forURIRepresentation: URIRepresentation),
let managedObject = try? context.existingObject(with: objectID),
- let media = managedObject as? Media {
+ let media = managedObject as? Media
+ {
return media
}
diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift
index 0e5b1d209c9c..824eb8d0ca74 100644
--- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift
+++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift
@@ -16,6 +16,7 @@ class CustomPostEditorViewController: PostGBKEditorViewController {
let details: PostTypeDetailsWithEditContext
let completion: () -> Void
let editorService: CustomPostEditorService
+ private var editorSession: PostEditorAnalyticsSession?
private lazy var primarySaveButton = UIBarButtonItem(primaryAction: savePostAction())
private lazy var redoButton = UIBarButtonItem(
@@ -39,11 +40,21 @@ class CustomPostEditorViewController: PostGBKEditorViewController {
details: PostTypeDetailsWithEditContext,
initialSettings: PostSettings? = nil,
initialContent: EditorContent? = nil,
+ entryPoint: PostEditorEntryPoint? = nil,
completion: @escaping () -> Void
) {
self.client = client
self.details = details
self.completion = completion
+ self.editorSession = entryPoint.map {
+ PostEditorAnalyticsSession(
+ editor: .gutenbergKit,
+ blog: blog,
+ postType: details.slug,
+ contentType: .init(content: initialContent?.content),
+ entryPoint: $0
+ )
+ }
self.editorService = CustomPostEditorService(
blog: blog,
@@ -88,6 +99,12 @@ class CustomPostEditorViewController: PostGBKEditorViewController {
undoButton.isEnabled = false
}
+ override func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) {
+ if editorSession?.started == false {
+ editorSession?.start()
+ }
+ }
+
override func editor(
_ viewController: GutenbergKit.EditorViewController,
didUpdateHistoryState state: EditorState
@@ -134,7 +151,9 @@ private extension CustomPostEditorViewController {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.addCancelActionWithTitle(PostEditorStrings.keepEditing)
alert.addDestructiveActionWithTitle(PostEditorStrings.discardChanges) { [weak self] _ in
- self?.navigationController?.dismiss(animated: true)
+ guard let self else { return }
+ self.endEditorSession(outcome: .discard)
+ self.navigationController?.dismiss(animated: true)
}
if post?.status ?? .draft == .draft {
alert.addAction(
@@ -153,7 +172,8 @@ private extension CustomPostEditorViewController {
alert.popoverPresentationController?.barButtonItem = self.navigationItem.leftBarButtonItem
self.present(alert, animated: true)
} else {
- self.navigationController?.dismiss(animated: true)
+ endEditorSession(outcome: .cancel)
+ navigationController?.dismiss(animated: true)
}
}
@@ -229,6 +249,7 @@ private extension CustomPostEditorViewController {
from: self,
completion: { [weak self] result in
if case .published = result {
+ self?.endEditorSession(outcome: .publish)
self?.completion()
}
}
@@ -238,6 +259,12 @@ private extension CustomPostEditorViewController {
private func separator() -> UIBarButtonItem {
UIBarButtonItem(systemItem: .fixedSpace)
}
+
+ func endEditorSession(outcome: PostEditorAnalyticsSession.Outcome) {
+ guard let editorSession, editorSession.started else { return }
+ editorSession.end(outcome: outcome)
+ self.editorSession = nil
+ }
}
// MARK: - Update post
diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift
index 913324145bf6..4f2e75ad4b5f 100644
--- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift
+++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift
@@ -94,7 +94,7 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
private var shouldCancelNextUpdateAnimation = false
private lazy var notificationCommentDetailCoordinator: NotificationCommentDetailCoordinator = {
- return NotificationCommentDetailCoordinator(notificationsNavigationDataSource: self)
+ NotificationCommentDetailCoordinator(notificationsNavigationDataSource: self)
}()
/// Activity Indicator to be shown when refreshing a Jetpack site status.
@@ -121,7 +121,10 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
// MARK: - View Lifecycle
- static func showInPopover(from presentingVC: UIViewController, sourceItem: UIPopoverPresentationControllerSourceItem) {
+ static func showInPopover(
+ from presentingVC: UIViewController,
+ sourceItem: UIPopoverPresentationControllerSourceItem
+ ) {
let notificationsVC = Notifications.instantiateInitialViewController()
notificationsVC.isSidebarModeEnabled = true
@@ -285,7 +288,9 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.reuseIdentifier) as? TableViewCell,
- let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification else {
+ let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification
+ else {
return UITableViewCell()
}
cell.selectionStyle = splitViewControllerIsHorizontallyCompact ? .none : .default
@@ -302,18 +307,23 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- return true
+ true
}
- func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
- return .none
+ func tableView(
+ _ tableView: UITableView,
+ editingStyleForRowAt indexPath: IndexPath
+ ) -> UITableViewCell.EditingStyle {
+ .none
}
// MARK: - UITableViewDelegate Methods
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let sectionInfo = tableViewHandler?.resultsController?.sections?[section],
- let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: NotificationsTableHeaderView.reuseIdentifier) as? NotificationsTableHeaderView
+ let view = tableView.dequeueReusableHeaderFooterView(
+ withIdentifier: NotificationsTableHeaderView.reuseIdentifier
+ ) as? NotificationsTableHeaderView
else {
return nil
}
@@ -323,12 +333,12 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
// Make sure no SectionFooter is rendered
- return CGFloat.leastNormalMagnitude
+ CGFloat.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
// Make sure no SectionFooter is rendered
- return nil
+ nil
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
@@ -343,12 +353,15 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return UITableView.automaticDimension
+ UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Failsafe: Make sure that the Notification (still) exists
- guard let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification else {
+ guard
+ let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification
+ else {
tableView.deselectSelectedRowWithAnimation(true)
return
}
@@ -369,56 +382,83 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
}
}
- func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
// skip when the notification is marked for deletion.
- guard let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification,
- deletionRequestForNoteWithID(note.objectID) == nil else {
+ guard
+ let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification,
+ deletionRequestForNoteWithID(note.objectID) == nil
+ else {
return nil
}
let isRead = note.read
- let title = isRead ? NSLocalizedString("Mark Unread", comment: "Marks a notification as unread") :
- NSLocalizedString("Mark Read", comment: "Marks a notification as unread")
-
- let action = UIContextualAction(style: .normal, title: title, handler: { _, _, completionHandler in
- if isRead {
- WPAnalytics.track(.notificationMarkAsUnreadTapped)
- self.markAsUnread(note: note)
- } else {
- WPAnalytics.track(.notificationMarkAsReadTapped)
- self.markAsRead(note: note)
+ let title =
+ isRead
+ ? NSLocalizedString("Mark Unread", comment: "Marks a notification as unread")
+ : NSLocalizedString("Mark Read", comment: "Marks a notification as unread")
+
+ let action = UIContextualAction(
+ style: .normal,
+ title: title,
+ handler: { _, _, completionHandler in
+ if isRead {
+ WPAnalytics.track(.notificationMarkAsUnreadTapped)
+ self.markAsUnread(note: note)
+ } else {
+ WPAnalytics.track(.notificationMarkAsReadTapped)
+ self.markAsRead(note: note)
+ }
+ completionHandler(true)
}
- completionHandler(true)
- })
+ )
action.backgroundColor = UIAppColor.neutral(.shade50)
return UISwipeActionsConfiguration(actions: [action])
}
- func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
// skip when the notification is marked for deletion.
- guard let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification,
+ guard
+ let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification,
let block: FormattableCommentContent = note.contentGroup(ofKind: .comment)?.blockOfKind(.comment),
- deletionRequestForNoteWithID(note.objectID) == nil else {
+ deletionRequestForNoteWithID(note.objectID) == nil
+ else {
return nil
}
// Approve comment
guard let approveEnabled = block.action(id: ApproveCommentAction.actionIdentifier())?.enabled,
- approveEnabled == true,
- let approveAction = block.action(id: ApproveCommentAction.actionIdentifier()),
- let actionTitle = approveAction.command?.actionTitle else {
+ approveEnabled == true,
+ let approveAction = block.action(id: ApproveCommentAction.actionIdentifier()),
+ let actionTitle = approveAction.command?.actionTitle
+ else {
return nil
}
- let action = UIContextualAction(style: .normal, title: actionTitle, handler: { _, _, completionHandler in
- WPAppAnalytics.track(approveAction.on ? .notificationsCommentUnapproved : .notificationsCommentApproved, properties: [Stats.sourceKey: Stats.sourceValue], blogID: block.metaSiteID)
+ let action = UIContextualAction(
+ style: .normal,
+ title: actionTitle,
+ handler: { _, _, completionHandler in
+ WPAppAnalytics.track(
+ approveAction.on ? .notificationsCommentUnapproved : .notificationsCommentApproved,
+ properties: [Stats.sourceKey: Stats.sourceValue],
+ blogID: block.metaSiteID
+ )
- let actionContext = ActionContext(block: block)
- approveAction.execute(context: actionContext)
- completionHandler(true)
- })
+ let actionContext = ActionContext(block: block)
+ approveAction.execute(context: actionContext)
+ completionHandler(true)
+ }
+ )
action.backgroundColor = approveAction.command?.actionColor
let configuration = UISwipeActionsConfiguration(actions: [action])
@@ -426,7 +466,10 @@ class NotificationsViewController: UIViewController, UITableViewDataSource, UITa
return configuration
}
- fileprivate func configureDetailsViewController(_ detailsViewController: NotificationDetailsViewController, withNote note: WordPressData.Notification) {
+ fileprivate func configureDetailsViewController(
+ _ detailsViewController: NotificationDetailsViewController,
+ withNote note: WordPressData.Notification
+ ) {
detailsViewController.navigationItem.largeTitleDisplayMode = .never
detailsViewController.hidesBottomBarWhenPushed = true
detailsViewController.dataSource = self
@@ -483,7 +526,8 @@ private extension NotificationsViewController {
func makeMoreMenuElements() -> [UIAction] {
// Mark All As Read
let markAllAsRead: UIAction? = { () -> UIAction? in
- guard let notes = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification] else {
+ guard let notes = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification]
+ else {
return nil
}
let isEnabled = notes.contains { !$0.read }
@@ -538,7 +582,10 @@ private extension NotificationsViewController {
func setupTableView() {
// Register the cells
- tableView.register(NotificationsTableHeaderView.self, forHeaderFooterViewReuseIdentifier: NotificationsTableHeaderView.reuseIdentifier)
+ tableView.register(
+ NotificationsTableHeaderView.self,
+ forHeaderFooterViewReuseIdentifier: NotificationsTableHeaderView.reuseIdentifier
+ )
tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.reuseIdentifier)
// UITableView
@@ -612,39 +659,67 @@ extension NotificationsViewController {
private extension NotificationsViewController {
func startListeningToNotifications() {
let nc = NotificationCenter.default
- nc.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
- nc.addObserver(self, selector: #selector(notificationsWereUpdated), name: NSNotification.Name(rawValue: NotificationSyncMediatorDidUpdateNotifications), object: nil)
- nc.addObserver(self, selector: #selector(dynamicTypeDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
+ nc.addObserver(
+ self,
+ selector: #selector(applicationDidBecomeActive),
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
+ nc.addObserver(
+ self,
+ selector: #selector(notificationsWereUpdated),
+ name: NSNotification.Name(rawValue: NotificationSyncMediatorDidUpdateNotifications),
+ object: nil
+ )
+ nc.addObserver(
+ self,
+ selector: #selector(dynamicTypeDidChange),
+ name: UIContentSizeCategory.didChangeNotification,
+ object: nil
+ )
}
func startListeningToAccountNotifications() {
let nc = NotificationCenter.default
- nc.addObserver(self, selector: #selector(defaultAccountDidChange), name: .wpAccountDefaultWordPressComAccountChanged, object: nil)
+ nc.addObserver(
+ self,
+ selector: #selector(defaultAccountDidChange),
+ name: .wpAccountDefaultWordPressComAccountChanged,
+ object: nil
+ )
}
func startListeningToTimeChangeNotifications() {
let nc = NotificationCenter.default
- nc.addObserver(self,
- selector: #selector(significantTimeChange),
- name: UIApplication.significantTimeChangeNotification,
- object: nil)
+ nc.addObserver(
+ self,
+ selector: #selector(significantTimeChange),
+ name: UIApplication.significantTimeChangeNotification,
+ object: nil
+ )
}
func startListeningToCommentDeletedNotifications() {
- NotificationCenter.default.addObserver(self,
- selector: #selector(removeDeletedNotification),
- name: .NotificationCommentDeletedNotification,
- object: nil)
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(removeDeletedNotification),
+ name: .NotificationCommentDeletedNotification,
+ object: nil
+ )
}
func stopListeningToNotifications() {
let nc = NotificationCenter.default
- nc.removeObserver(self,
- name: UIApplication.didBecomeActiveNotification,
- object: nil)
- nc.removeObserver(self,
- name: NSNotification.Name(rawValue: NotificationSyncMediatorDidUpdateNotifications),
- object: nil)
+ nc.removeObserver(
+ self,
+ name: UIApplication.didBecomeActiveNotification,
+ object: nil
+ )
+ nc.removeObserver(
+ self,
+ name: NSNotification.Name(rawValue: NotificationSyncMediatorDidUpdateNotifications),
+ object: nil
+ )
}
@objc func applicationDidBecomeActive(_ note: Foundation.Notification) {
@@ -684,15 +759,17 @@ private extension NotificationsViewController {
needsReloadResults = true
if UIApplication.shared.applicationState == .active
&& isViewLoaded == true
- && view.window != nil {
+ && view.window != nil
+ {
reloadResultsControllerIfNeeded()
}
}
@objc func dynamicTypeDidChange() {
- tableViewHandler?.resultsController?.fetchedObjects?.forEach {
- ($0 as? WordPressData.Notification)?.resetCachedAttributes()
- }
+ tableViewHandler?.resultsController?.fetchedObjects?
+ .forEach {
+ ($0 as? WordPressData.Notification)?.resetCachedAttributes()
+ }
}
}
@@ -770,11 +847,14 @@ extension NotificationsViewController {
//
if let postID = note.metaPostID,
let siteID = note.metaSiteID,
- note.kind == .matcher || note.kind == .newPost {
+ note.kind == .matcher || note.kind == .newPost
+ {
if isSidebarModeEnabled && splitViewController == nil {
presentingViewController?.dismiss(animated: true)
- RootViewCoordinator.sharedPresenter.showReader(path: .post(postID: postID.intValue, siteID: siteID.intValue))
+ RootViewCoordinator.sharedPresenter.showReader(
+ path: .post(postID: postID.intValue, siteID: siteID.intValue)
+ )
} else {
let readerViewController = ReaderDetailViewController.controllerWithPostID(postID, siteID: siteID)
readerViewController.navigationItem.largeTitleDisplayMode = .never
@@ -815,8 +895,12 @@ extension NotificationsViewController {
}
}
- private func getNotificationCommentDetailViewController(for note: WordPressData.Notification) -> NotificationCommentDetailViewController? {
- guard let commentDetailViewController = self.notificationCommentDetailCoordinator.createViewController(with: note) else {
+ private func getNotificationCommentDetailViewController(
+ for note: WordPressData.Notification
+ ) -> NotificationCommentDetailViewController? {
+ guard
+ let commentDetailViewController = self.notificationCommentDetailCoordinator.createViewController(with: note)
+ else {
DDLogError("Notifications: failed creating Comment Detail view.")
return nil
}
@@ -829,7 +913,9 @@ extension NotificationsViewController {
return commentDetailViewController
}
- private func getNotificationDetailsViewController(for note: WordPressData.Notification) -> NotificationDetailsViewController? {
+ private func getNotificationDetailsViewController(
+ for note: WordPressData.Notification
+ ) -> NotificationDetailsViewController? {
let viewControllerID = NotificationDetailsViewController.classNameWithoutNamespaces()
let detailsViewController = storyboard?.instantiateViewController(withIdentifier: viewControllerID)
guard let detailsViewController = detailsViewController as? NotificationDetailsViewController else {
@@ -880,9 +966,11 @@ extension NotificationsViewController {
return
}
- let noteIndexPath = tableView.indexPathsForVisibleRows?.first { indexPath in
- return note == tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification
- }
+ let noteIndexPath = tableView.indexPathsForVisibleRows?
+ .first { indexPath in
+ note == tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification
+ }
guard noteIndexPath == nil else {
return
@@ -912,7 +1000,11 @@ extension NotificationsViewController {
///
@objc func showNotificationSettings() {
let controller = NotificationSettingsViewController()
- controller.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(closeNotificationSettings))
+ controller.navigationItem.rightBarButtonItem = UIBarButtonItem(
+ barButtonSystemItem: .done,
+ target: self,
+ action: #selector(closeNotificationSettings)
+ )
let navigationController = UINavigationController(rootViewController: controller)
navigationController.modalPresentationStyle = .formSheet
@@ -957,11 +1049,15 @@ private extension NotificationsViewController {
notificationDeletionRequests.removeValue(forKey: noteObjectID)
reloadRowForNotificationWithID(noteObjectID)
- NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(deleteNoteWithID), object: noteObjectID)
+ NSObject.cancelPreviousPerformRequests(
+ withTarget: self,
+ selector: #selector(deleteNoteWithID),
+ object: noteObjectID
+ )
}
func deletionRequestForNoteWithID(_ noteObjectID: NSManagedObjectID) -> NotificationDeletionRequest? {
- return notificationDeletionRequests[noteObjectID]
+ notificationDeletionRequests[noteObjectID]
}
// MARK: - Notifications Deletion from CommentDetailViewController
@@ -974,7 +1070,7 @@ private extension NotificationsViewController {
selectNextAvailableNotification(ignoring: notificationCommentDetailCoordinator.notificationsCommentModerated)
notificationCommentDetailCoordinator.notificationsCommentModerated.forEach {
- syncNotification(with: $0.notificationId, timeout: Syncing.pushMaxWait, success: {_ in })
+ syncNotification(with: $0.notificationId, timeout: Syncing.pushMaxWait, success: { _ in })
}
notificationCommentDetailCoordinator.notificationsCommentModerated = []
@@ -982,10 +1078,11 @@ private extension NotificationsViewController {
@objc func removeDeletedNotification(notification: NSNotification) {
guard let userInfo = notification.userInfo,
- let deletedCommentID = userInfo[userInfoCommentIdKey] as? Int32,
- let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification] else {
- return
- }
+ let deletedCommentID = userInfo[userInfoCommentIdKey] as? Int32,
+ let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification]
+ else {
+ return
+ }
let notification = notifications.first(where: { notification -> Bool in
guard let commentID = notification.metaCommentID else {
@@ -1005,24 +1102,33 @@ private extension NotificationsViewController {
selectNextAvailableNotification(ignoring: [notification])
- syncNotification(with: notification.notificationId, timeout: Syncing.pushMaxWait, success: { [weak self] notification in
- self?.notificationCommentDetailCoordinator.notificationsCommentModerated.removeAll(where: { $0.notificationId == notification.notificationId })
- })
+ syncNotification(
+ with: notification.notificationId,
+ timeout: Syncing.pushMaxWait,
+ success: { [weak self] notification in
+ self?.notificationCommentDetailCoordinator.notificationsCommentModerated
+ .removeAll(where: { $0.notificationId == notification.notificationId })
+ }
+ )
}
func selectNextAvailableNotification(ignoring: [WordPressData.Notification]) {
// If the currently selected notification is about to be removed, find the next available and select it.
// This is only necessary for split view to prevent the details from showing for removed notifications.
if !splitViewControllerIsHorizontallyCompact,
- let selectedNotification,
- ignoring.contains(selectedNotification) {
-
- guard let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification],
- let nextAvailable = notifications.first(where: { !ignoring.contains($0) }),
- let indexPath = tableViewHandler?.resultsController?.indexPath(forObject: nextAvailable) else {
- self.selectedNotification = nil
- return
- }
+ let selectedNotification,
+ ignoring.contains(selectedNotification)
+ {
+
+ guard
+ let notifications = tableViewHandler?.resultsController?.fetchedObjects
+ as? [WordPressData.Notification],
+ let nextAvailable = notifications.first(where: { !ignoring.contains($0) }),
+ let indexPath = tableViewHandler?.resultsController?.indexPath(forObject: nextAvailable)
+ else {
+ self.selectedNotification = nil
+ return
+ }
self.selectedNotification = nextAvailable
tableView(tableView, didSelectRowAt: indexPath)
@@ -1074,12 +1180,17 @@ private extension NotificationsViewController {
!$0.read
}
- NotificationSyncMediator()?.markAsRead(unreadNotifications, completion: { error in
- let notice = Notice(
- title: error != nil ? Localization.markAllAsReadNoticeFailure : Localization.markAllAsReadNoticeSuccess
+ NotificationSyncMediator()?
+ .markAsRead(
+ unreadNotifications,
+ completion: { error in
+ let notice = Notice(
+ title: error != nil
+ ? Localization.markAllAsReadNoticeFailure : Localization.markAllAsReadNoticeSuccess
+ )
+ ActionDispatcherFacade().dispatch(NoticeAction.post(notice))
+ }
)
- ActionDispatcherFacade().dispatch(NoticeAction.post(notice))
- })
}
/// Presents a confirmation action sheet for mark all as read action.
@@ -1096,7 +1207,8 @@ private extension NotificationsViewController {
default:
title = NSLocalizedString(
"Mark all %1$@ notifications as read?",
- comment: "Confirmation title for marking all notifications under a filter as read. %1$@ is replaced by the filter name."
+ comment:
+ "Confirmation title for marking all notifications under a filter as read. %1$@ is replaced by the filter name."
)
}
@@ -1148,9 +1260,10 @@ private extension NotificationsViewController {
// This is additive because we don't want to remove anything
// from the list unless we explicitly call
// clearUnreadNotifications()
- notes.lazy.filter({ !$0.read }).forEach { note in
- unreadNotificationIds.insert(note.objectID)
- }
+ notes.lazy.filter({ !$0.read })
+ .forEach { note in
+ unreadNotificationIds.insert(note.objectID)
+ }
if previous != unreadNotificationIds && reloadingResultsController {
reloadResultsController()
}
@@ -1213,20 +1326,26 @@ private extension NotificationsViewController {
}
}
- func selectRow(for notification: WordPressData.Notification, animated: Bool = true,
- scrollPosition: UITableView.ScrollPosition = .none) {
+ func selectRow(
+ for notification: WordPressData.Notification,
+ animated: Bool = true,
+ scrollPosition: UITableView.ScrollPosition = .none
+ ) {
selectedNotification = notification
// also ensure that the index path returned from results controller does not have negative row index.
// ref: https://github.com/wordpress-mobile/WordPress-iOS/issues/15370
guard let indexPath = tableViewHandler?.resultsController?.indexPath(forObject: notification),
- indexPath != tableView.indexPathForSelectedRow,
- 0.. Bool {
- return tableViewHandler?.resultsController?.isEmpty() ?? true
+ tableViewHandler?.resultsController?.isEmpty() ?? true
}
func noConnectionMessage() -> String {
- return NSLocalizedString("No internet connection. Some content may be unavailable while offline.",
- comment: "Error message shown when the user is browsing Notifications without an internet connection.")
+ NSLocalizedString(
+ "No internet connection. Some content may be unavailable while offline.",
+ comment: "Error message shown when the user is browsing Notifications without an internet connection."
+ )
}
}
@@ -1308,8 +1429,10 @@ extension NotificationsViewController {
}
// If we don't currently have a selected notification and there is a notification in the list, then select it.
- if let firstNotification = tableViewHandler?.resultsController?.fetchedObjects?.first as? WordPressData.Notification,
- let indexPath = tableViewHandler?.resultsController?.indexPath(forObject: firstNotification) {
+ if let firstNotification = tableViewHandler?.resultsController?.fetchedObjects?.first
+ as? WordPressData.Notification,
+ let indexPath = tableViewHandler?.resultsController?.indexPath(forObject: firstNotification)
+ {
selectRow(for: firstNotification, animated: false, scrollPosition: .none)
self.tableView(tableView, didSelectRowAt: indexPath)
return
@@ -1329,7 +1452,7 @@ extension NotificationsViewController {
//
extension NotificationsViewController: WPTableViewHandlerDelegate {
func managedObjectContext() -> NSManagedObjectContext {
- return ContextManager.shared.mainContext
+ ContextManager.shared.mainContext
}
func fetchRequest() -> NSFetchRequest? {
@@ -1360,8 +1483,11 @@ extension NotificationsViewController: WPTableViewHandlerDelegate {
}
func configureCell(_ cell: UITableViewCell, at indexPath: IndexPath) {
- guard let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath) as? WordPressData.Notification,
- let cell = cell as? ListTableViewCell else {
+ guard
+ let note = tableViewHandler?.resultsController?.managedObject(atUnsafe: indexPath)
+ as? WordPressData.Notification,
+ let cell = cell as? ListTableViewCell
+ else {
return
}
@@ -1378,24 +1504,26 @@ extension NotificationsViewController: WPTableViewHandlerDelegate {
}
func sectionNameKeyPath() -> String {
- return "sectionIdentifier"
+ "sectionIdentifier"
}
@objc func entityName() -> String {
- return Notification.classNameWithoutNamespaces()
+ Notification.classNameWithoutNamespaces()
}
private var shouldCountNotificationsForSecondAlert: Bool {
- userDefaults.notificationPrimerInlineWasAcknowledged &&
- userDefaults.secondNotificationsAlertCount != Constants.secondNotificationsAlertDisabled
+ userDefaults.notificationPrimerInlineWasAcknowledged
+ && userDefaults.secondNotificationsAlertCount != Constants.secondNotificationsAlertDisabled
}
func tableViewWillChangeContent(_ tableView: UITableView) {
guard shouldCountNotificationsForSecondAlert,
- let notification = tableViewHandler?.resultsController?.fetchedObjects?.first as? WordPressData.Notification,
- let timestamp = notification.timestamp else {
- timestampBeforeUpdatesForSecondAlert = nil
- return
+ let notification = tableViewHandler?.resultsController?.fetchedObjects?.first
+ as? WordPressData.Notification,
+ let timestamp = notification.timestamp
+ else {
+ timestampBeforeUpdatesForSecondAlert = nil
+ return
}
timestampBeforeUpdatesForSecondAlert = timestamp
@@ -1429,14 +1557,15 @@ extension NotificationsViewController: WPTableViewHandlerDelegate {
}
func shouldCancelUpdateAnimation() -> Bool {
- return shouldCancelNextUpdateAnimation
+ shouldCancelNextUpdateAnimation
}
// counts the new notifications for the second alert
private var newNotificationsForSecondAlert: Int {
guard let previousTimestamp = timestampBeforeUpdatesForSecondAlert,
- let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification] else {
+ let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification]
+ else {
return 0
}
@@ -1451,17 +1580,25 @@ extension NotificationsViewController: WPTableViewHandlerDelegate {
private static func accessibilityHint(for note: WordPressData.Notification) -> String? {
switch note.kind {
case .comment:
- return NSLocalizedString("Shows details and moderation actions.",
- comment: "Accessibility hint for a comment notification.")
+ return NSLocalizedString(
+ "Shows details and moderation actions.",
+ comment: "Accessibility hint for a comment notification."
+ )
case .commentLike, .like:
- return NSLocalizedString("Shows all likes.",
- comment: "Accessibility hint for a post or comment “like” notification.")
+ return NSLocalizedString(
+ "Shows all likes.",
+ comment: "Accessibility hint for a post or comment “like” notification."
+ )
case .follow:
- return NSLocalizedString("Shows all followers",
- comment: "Accessibility hint for a follow notification.")
+ return NSLocalizedString(
+ "Shows all followers",
+ comment: "Accessibility hint for a follow notification."
+ )
case .matcher, .newPost:
- return NSLocalizedString("Shows the post",
- comment: "Accessibility hint for a match/mention on a post notification.")
+ return NSLocalizedString(
+ "Shows the post",
+ comment: "Accessibility hint for a match/mention on a post notification."
+ )
default:
return nil
}
@@ -1476,9 +1613,12 @@ private extension NotificationsViewController {
return
}
- UIView.animate(withDuration: 0.33, animations: {
- self.filterTabBar.isHidden = false
- })
+ UIView.animate(
+ withDuration: 0.33,
+ animations: {
+ self.filterTabBar.isHidden = false
+ }
+ )
}
func hideFiltersSegmentedControlIfApplicable() {
@@ -1491,7 +1631,7 @@ private extension NotificationsViewController {
// Filters should only be hidden whenever there are no Notifications in the bucket (contrary to the FRC's
// results, which are filtered by the active predicate!).
//
- return mainContext.countObjects(ofType: Notification.self) > 0 && !shouldDisplayJetpackPrompt
+ mainContext.countObjects(ofType: Notification.self) > 0 && !shouldDisplayJetpackPrompt
}
}
@@ -1531,13 +1671,19 @@ private extension NotificationsViewController {
switch filter {
case .none, .comment, .follow, .like:
Button(Strings.openReader) {
- WPAnalytics.track(.notificationsTappedViewReader, withProperties: [Stats.sourceKey: Stats.sourceValue])
+ WPAnalytics.track(
+ .notificationsTappedViewReader,
+ withProperties: [Stats.sourceKey: Stats.sourceValue]
+ )
RootViewCoordinator.sharedPresenter.showReader()
}
case .unread:
Button(Strings.createPost) {
- WPAnalytics.track(.notificationsTappedNewPost, withProperties: [Stats.sourceKey: Stats.sourceValue])
- RootViewCoordinator.sharedPresenter.showPostEditor()
+ WPAnalytics.track(
+ .notificationsTappedNewPost,
+ withProperties: [Stats.sourceKey: Stats.sourceValue]
+ )
+ RootViewCoordinator.sharedPresenter.showNewPostEditor()
}
}
}
@@ -1548,7 +1694,9 @@ private extension NotificationsViewController {
}
func showNoConnectionView() {
- noResultsViewController.rootView = AnyView(EmptyStateView(noConnectionTitleText, systemImage: "network.slash", description: noConnectionMessage()))
+ noResultsViewController.rootView = AnyView(
+ EmptyStateView(noConnectionTitleText, systemImage: "network.slash", description: noConnectionMessage())
+ )
addNoResultsToView()
}
@@ -1570,27 +1718,30 @@ private extension NotificationsViewController {
}
var noConnectionTitleText: String {
- return NSLocalizedString("Unable to Sync", comment: "Title of error prompt shown when a sync the user initiated fails.")
+ NSLocalizedString(
+ "Unable to Sync",
+ comment: "Title of error prompt shown when a sync the user initiated fails."
+ )
}
var noResultsButtonText: String? {
- return filter.noResultsButtonTitle
+ filter.noResultsButtonTitle
}
var shouldDisplayJetpackPrompt: Bool {
- return AccountHelper.isDotcomAvailable() == false && blogForJetpackPrompt != nil
+ AccountHelper.isDotcomAvailable() == false && blogForJetpackPrompt != nil
}
var shouldDisplaySettingsButton: Bool {
- return AccountHelper.isDotcomAvailable()
+ AccountHelper.isDotcomAvailable()
}
var shouldDisplayNoResultsView: Bool {
- return tableViewHandler?.resultsController?.fetchedObjects?.isEmpty == true && !shouldDisplayJetpackPrompt
+ tableViewHandler?.resultsController?.fetchedObjects?.isEmpty == true && !shouldDisplayJetpackPrompt
}
var shouldDisplayFullscreenNoResultsView: Bool {
- return shouldDisplayNoResultsView && filter == .none
+ shouldDisplayNoResultsView && filter == .none
}
}
@@ -1600,27 +1751,46 @@ internal extension NotificationsViewController {
func showInlinePrompt() {
guard inlinePromptView.alpha != 1,
userDefaults.notificationPrimerAlertWasDisplayed,
- userDefaults.notificationsTabAccessCount >= Constants.inlineTabAccessCount else {
+ userDefaults.notificationsTabAccessCount >= Constants.inlineTabAccessCount
+ else {
return
}
- UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseIn, animations: {
- self.inlinePromptView.isHidden = false
- })
+ UIView.animate(
+ withDuration: 0.33,
+ delay: 0,
+ options: .curveEaseIn,
+ animations: {
+ self.inlinePromptView.isHidden = false
+ }
+ )
- UIView.animate(withDuration: 0.33 * 0.5, delay: 0.33 * 0.75, options: .curveEaseIn, animations: {
- self.inlinePromptView.alpha = 1
- })
+ UIView.animate(
+ withDuration: 0.33 * 0.5,
+ delay: 0.33 * 0.75,
+ options: .curveEaseIn,
+ animations: {
+ self.inlinePromptView.alpha = 1
+ }
+ )
}
func hideInlinePrompt(delay: TimeInterval) {
- UIView.animate(withDuration: 0.33 * 0.75, delay: delay, animations: {
- self.inlinePromptView.alpha = 0
- })
+ UIView.animate(
+ withDuration: 0.33 * 0.75,
+ delay: delay,
+ animations: {
+ self.inlinePromptView.alpha = 0
+ }
+ )
- UIView.animate(withDuration: 0.33, delay: delay + 0.33 * 0.5, animations: {
- self.inlinePromptView.isHidden = true
- })
+ UIView.animate(
+ withDuration: 0.33,
+ delay: delay + 0.33 * 0.5,
+ animations: {
+ self.inlinePromptView.isHidden = true
+ }
+ )
}
}
@@ -1636,30 +1806,36 @@ private extension NotificationsViewController {
mediator?.sync()
}
- func syncNotification(with noteId: String, timeout: TimeInterval, success: @escaping (_ note: WordPressData.Notification) -> Void) {
+ func syncNotification(
+ with noteId: String,
+ timeout: TimeInterval,
+ success: @escaping (_ note: WordPressData.Notification) -> Void
+ ) {
let mediator = NotificationSyncMediator()
let startDate = Date()
DDLogInfo("Sync'ing Notification [\(noteId)]")
- mediator?.syncNote(with: noteId) { _, note in
- guard abs(startDate.timeIntervalSinceNow) <= timeout else {
- DDLogError("Error: Timeout while trying to load Notification [\(noteId)]")
- return
- }
+ mediator?
+ .syncNote(with: noteId) { _, note in
+ guard abs(startDate.timeIntervalSinceNow) <= timeout else {
+ DDLogError("Error: Timeout while trying to load Notification [\(noteId)]")
+ return
+ }
- guard let note else {
- DDLogError("Error: Couldn't load Notification [\(noteId)]")
- return
- }
+ guard let note else {
+ DDLogError("Error: Couldn't load Notification [\(noteId)]")
+ return
+ }
- DDLogInfo("Notification Sync'ed in \(startDate.timeIntervalSinceNow) seconds")
- success(note)
- }
+ DDLogInfo("Notification Sync'ed in \(startDate.timeIntervalSinceNow) seconds")
+ success(note)
+ }
}
func updateLastSeenTime() {
- guard let note = tableViewHandler?.resultsController?.fetchedObjects?.first as? WordPressData.Notification else {
+ guard let note = tableViewHandler?.resultsController?.fetchedObjects?.first as? WordPressData.Notification
+ else {
return
}
@@ -1672,8 +1848,12 @@ private extension NotificationsViewController {
return mainContext.firstObject(ofType: Notification.self, matching: predicate)
}
- func loadNotification(near note: WordPressData.Notification, withIndexDelta delta: Int) -> WordPressData.Notification? {
- guard let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification] else {
+ func loadNotification(
+ near note: WordPressData.Notification,
+ withIndexDelta delta: Int
+ ) -> WordPressData.Notification? {
+ guard let notifications = tableViewHandler?.resultsController?.fetchedObjects as? [WordPressData.Notification]
+ else {
return nil
}
@@ -1705,11 +1885,11 @@ private extension NotificationsViewController {
//
extension NotificationsViewController: NotificationsNavigationDataSource {
@objc func notification(succeeding note: WordPressData.Notification) -> WordPressData.Notification? {
- return loadNotification(near: note, withIndexDelta: -1)
+ loadNotification(near: note, withIndexDelta: -1)
}
@objc func notification(preceding note: WordPressData.Notification) -> WordPressData.Notification? {
- return loadNotification(near: note, withIndexDelta: +1)
+ loadNotification(near: note, withIndexDelta: +1)
}
}
@@ -1717,16 +1897,22 @@ extension NotificationsViewController: NotificationsNavigationDataSource {
//
extension NotificationsViewController: SearchableActivityConvertable {
var activityType: String {
- return WPActivityType.notifications.rawValue
+ WPActivityType.notifications.rawValue
}
var activityTitle: String {
- return NSLocalizedString("Notifications", comment: "Title of the 'Notifications' tab - used for spotlight indexing on iOS.")
+ NSLocalizedString(
+ "Notifications",
+ comment: "Title of the 'Notifications' tab - used for spotlight indexing on iOS."
+ )
}
var activityKeywords: Set? {
- let keyWordString = NSLocalizedString("wordpress, notifications, alerts, updates",
- comment: "This is a comma separated list of keywords used for spotlight indexing of the 'Notifications' tab.")
+ let keyWordString = NSLocalizedString(
+ "wordpress, notifications, alerts, updates",
+ comment:
+ "This is a comma separated list of keywords used for spotlight indexing of the 'Notifications' tab."
+ )
let keywordArray = keyWordString.arrayOfTags()
guard !keywordArray.isEmpty else {
@@ -1741,11 +1927,11 @@ extension NotificationsViewController: SearchableActivityConvertable {
//
private extension NotificationsViewController {
var mainContext: NSManagedObjectContext {
- return ContextManager.shared.mainContext
+ ContextManager.shared.mainContext
}
var userDefaults: UserPersistentRepository {
- return UserPersistentStoreFactory.instance()
+ UserPersistentStoreFactory.instance()
}
var filter: Filter {
@@ -1768,82 +1954,147 @@ private extension NotificationsViewController {
var condition: String? {
switch self {
- case .none: return nil
- case .unread: return "read = NO"
- case .comment: return "type = '\(NotificationKind.comment.rawValue)'"
- case .follow: return "type = '\(NotificationKind.follow.rawValue)'"
- case .like: return "type = '\(NotificationKind.like.rawValue)' OR type = '\(NotificationKind.commentLike.rawValue)'"
+ case .none: return nil
+ case .unread: return "read = NO"
+ case .comment: return "type = '\(NotificationKind.comment.rawValue)'"
+ case .follow: return "type = '\(NotificationKind.follow.rawValue)'"
+ case .like:
+ return "type = '\(NotificationKind.like.rawValue)' OR type = '\(NotificationKind.commentLike.rawValue)'"
}
}
var title: String {
switch self {
- case .none: return NSLocalizedString("All", comment: "Displays all of the Notifications, unfiltered")
- case .unread: return NSLocalizedString("Unread", comment: "Filters Unread Notifications")
- case .comment: return NSLocalizedString("Comments", comment: "Filters Comments Notifications")
- case .follow: return NSLocalizedString("notifications.filter.subscribers.title", value: "Subscribers", comment: "Filters Subscribers Notifications")
- case .like: return NSLocalizedString("Likes", comment: "Filters Likes Notifications")
+ case .none: return NSLocalizedString("All", comment: "Displays all of the Notifications, unfiltered")
+ case .unread: return NSLocalizedString("Unread", comment: "Filters Unread Notifications")
+ case .comment: return NSLocalizedString("Comments", comment: "Filters Comments Notifications")
+ case .follow:
+ return NSLocalizedString(
+ "notifications.filter.subscribers.title",
+ value: "Subscribers",
+ comment: "Filters Subscribers Notifications"
+ )
+ case .like: return NSLocalizedString("Likes", comment: "Filters Likes Notifications")
}
}
var analyticsTitle: String {
switch self {
- case .none: return "All"
- case .unread: return "Unread"
- case .comment: return "Comments"
- case .follow: return "Follows"
- case .like: return "Likes"
+ case .none: return "All"
+ case .unread: return "Unread"
+ case .comment: return "Comments"
+ case .follow: return "Follows"
+ case .like: return "Likes"
}
}
var confirmationMessageTitle: String {
switch self {
- case .none: return ""
- case .unread: return NSLocalizedString("unread", comment: "Displayed in the confirmation alert when marking unread notifications as read.")
- case .comment: return NSLocalizedString("comment", comment: "Displayed in the confirmation alert when marking comment notifications as read.")
- case .follow: return NSLocalizedString("notifications.filter.subscriptions.confirmationMessageTitle", value: "subscribe", comment: "Displayed in the confirmation alert when marking follow notifications as read.")
- case .like: return NSLocalizedString("like", comment: "Displayed in the confirmation alert when marking like notifications as read.")
+ case .none: return ""
+ case .unread:
+ return NSLocalizedString(
+ "unread",
+ comment: "Displayed in the confirmation alert when marking unread notifications as read."
+ )
+ case .comment:
+ return NSLocalizedString(
+ "comment",
+ comment: "Displayed in the confirmation alert when marking comment notifications as read."
+ )
+ case .follow:
+ return NSLocalizedString(
+ "notifications.filter.subscriptions.confirmationMessageTitle",
+ value: "subscribe",
+ comment: "Displayed in the confirmation alert when marking follow notifications as read."
+ )
+ case .like:
+ return NSLocalizedString(
+ "like",
+ comment: "Displayed in the confirmation alert when marking like notifications as read."
+ )
}
}
var noResultsTitle: String {
switch self {
- case .none: return NSLocalizedString("No notifications yet",
- comment: "Displayed in the Notifications Tab as a title, when there are no notifications")
- case .unread: return NSLocalizedString("You're all up to date!",
- comment: "Displayed in the Notifications Tab as a title, when the Unread Filter shows no unread notifications as a title")
- case .comment: return NSLocalizedString("No comments yet",
- comment: "Displayed in the Notifications Tab as a title, when the Comments Filter shows no notifications")
- case .follow: return NSLocalizedString("notifications.noresults.subscribers", value: "No subscribers yet",
- comment: "Displayed in the Notifications Tab as a title, when the Subscriber Filter shows no notifications")
- case .like: return NSLocalizedString("No likes yet",
- comment: "Displayed in the Notifications Tab as a title, when the Likes Filter shows no notifications")
+ case .none:
+ return NSLocalizedString(
+ "No notifications yet",
+ comment: "Displayed in the Notifications Tab as a title, when there are no notifications"
+ )
+ case .unread:
+ return NSLocalizedString(
+ "You're all up to date!",
+ comment:
+ "Displayed in the Notifications Tab as a title, when the Unread Filter shows no unread notifications as a title"
+ )
+ case .comment:
+ return NSLocalizedString(
+ "No comments yet",
+ comment:
+ "Displayed in the Notifications Tab as a title, when the Comments Filter shows no notifications"
+ )
+ case .follow:
+ return NSLocalizedString(
+ "notifications.noresults.subscribers",
+ value: "No subscribers yet",
+ comment:
+ "Displayed in the Notifications Tab as a title, when the Subscriber Filter shows no notifications"
+ )
+ case .like:
+ return NSLocalizedString(
+ "No likes yet",
+ comment:
+ "Displayed in the Notifications Tab as a title, when the Likes Filter shows no notifications"
+ )
}
}
var noResultsMessage: String {
switch self {
- case .none: return NSLocalizedString("Get active! Comment on posts from blogs you follow.",
- comment: "Displayed in the Notifications Tab as a message, when there are no notifications")
- case .unread: return NSLocalizedString("Reignite the conversation: write a new post.",
- comment: "Displayed in the Notifications Tab as a message, when the Unread Filter shows no notifications")
- case .comment: return NSLocalizedString("Join a conversation: comment on posts from blogs you follow.",
- comment: "Displayed in the Notifications Tab as a message, when the Comments Filter shows no notifications")
+ case .none:
+ return NSLocalizedString(
+ "Get active! Comment on posts from blogs you follow.",
+ comment: "Displayed in the Notifications Tab as a message, when there are no notifications"
+ )
+ case .unread:
+ return NSLocalizedString(
+ "Reignite the conversation: write a new post.",
+ comment:
+ "Displayed in the Notifications Tab as a message, when the Unread Filter shows no notifications"
+ )
+ case .comment:
+ return NSLocalizedString(
+ "Join a conversation: comment on posts from blogs you follow.",
+ comment:
+ "Displayed in the Notifications Tab as a message, when the Comments Filter shows no notifications"
+ )
case .follow,
- .like: return NSLocalizedString("Get noticed: comment on posts you've read.",
- comment: "Displayed in the Notifications Tab as a message, when the Follow Filter shows no notifications")
+ .like:
+ return NSLocalizedString(
+ "Get noticed: comment on posts you've read.",
+ comment:
+ "Displayed in the Notifications Tab as a message, when the Follow Filter shows no notifications"
+ )
}
}
var noResultsButtonTitle: String {
switch self {
case .none,
- .comment,
- .follow,
- .like: return NSLocalizedString("Go to Reader",
- comment: "Displayed in the Notifications Tab as a button title, when there are no notifications")
- case .unread: return NSLocalizedString("Create a Post",
- comment: "Displayed in the Notifications Tab as a button title, when the Unread Filter shows no notifications")
+ .comment,
+ .follow,
+ .like:
+ return NSLocalizedString(
+ "Go to Reader",
+ comment: "Displayed in the Notifications Tab as a button title, when there are no notifications"
+ )
+ case .unread:
+ return NSLocalizedString(
+ "Create a Post",
+ comment:
+ "Displayed in the Notifications Tab as a button title, when the Unread Filter shows no notifications"
+ )
}
}
@@ -1964,13 +2215,21 @@ extension NotificationsViewController: JPScrollViewDelegate {
extension NotificationsViewController: StoryboardLoadable {
static var defaultStoryboardName: String {
- return "Notifications"
+ "Notifications"
}
}
private enum Strings {
- static let openReader = NSLocalizedString("notifications.emptyState.buttonOpenReader", value: "Open Reader", comment: "Displayed in the Notifications Tab as a button title, when there are no notifications")
- static let createPost = NSLocalizedString("notifications.emptyState.buttonCreatePost", value: "Create Post", comment: "Displayed in the Notifications Tab as a button title, when the Unread Filter shows no notifications")
+ static let openReader = NSLocalizedString(
+ "notifications.emptyState.buttonOpenReader",
+ value: "Open Reader",
+ comment: "Displayed in the Notifications Tab as a button title, when there are no notifications"
+ )
+ static let createPost = NSLocalizedString(
+ "notifications.emptyState.buttonCreatePost",
+ value: "Create Post",
+ comment: "Displayed in the Notifications Tab as a button title, when the Unread Filter shows no notifications"
+ )
enum NavigationBar {
static let notificationSettingsActionTitle = NSLocalizedString(
diff --git a/WordPress/Classes/ViewRelated/Pages/Controllers/PageListViewController.swift b/WordPress/Classes/ViewRelated/Pages/Controllers/PageListViewController.swift
index bc76180b152f..e18415e63e02 100644
--- a/WordPress/Classes/ViewRelated/Pages/Controllers/PageListViewController.swift
+++ b/WordPress/Classes/ViewRelated/Pages/Controllers/PageListViewController.swift
@@ -32,9 +32,12 @@ final class PageListViewController: AbstractPostListViewController {
private lazy var homepageSettingsService = HomepageSettingsService(blog: blog, coreDataStack: ContextManager.shared)
private lazy var createButtonCoordinator: CreateButtonCoordinator = {
- let action = PageAction(handler: { [weak self] in
- self?.createPost()
- }, source: Constant.Events.source)
+ let action = PageAction(
+ handler: { [weak self] in
+ self?.createPost()
+ },
+ source: Constant.Events.source
+ )
return CreateButtonCoordinator(self, actions: [action], source: Constant.Events.source)
}()
@@ -46,7 +49,10 @@ final class PageListViewController: AbstractPostListViewController {
return isFSETheme && filterSettings.currentPostListFilter().filterType == .published
}
- private lazy var editorSettingsService = BlockEditorSettingsService(blog: blog, coreDataStack: ContextManager.shared)
+ private lazy var editorSettingsService = BlockEditorSettingsService(
+ blog: blog,
+ coreDataStack: ContextManager.shared
+ )
private var pages: [Page] = []
@@ -75,7 +81,11 @@ final class PageListViewController: AbstractPostListViewController {
title = NSLocalizedString("Pages", comment: "Title of the screen showing the list of pages for a blog.")
- createButtonCoordinator.add(to: view, trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor, bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor)
+ createButtonCoordinator.add(
+ to: view,
+ trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor,
+ bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor
+ )
refreshNoResultsViewController = { [weak self] in
self?.handleRefreshNoResultsViewController($0)
@@ -112,18 +122,24 @@ final class PageListViewController: AbstractPostListViewController {
tableView.estimatedRowHeight = Constant.Size.pageCellEstimatedRowHeight
tableView.register(PageListCell.self, forCellReuseIdentifier: Constant.Identifiers.pageCellIdentifier)
- tableView.register(TemplatePageTableViewCell.self, forCellReuseIdentifier: Constant.Identifiers.templatePageCellIdentifier)
+ tableView.register(
+ TemplatePageTableViewCell.self,
+ forCellReuseIdentifier: Constant.Identifiers.templatePageCellIdentifier
+ )
}
private func beginRefreshingManually() {
refreshControl.beginRefreshing()
- tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - refreshControl.frame.size.height), animated: true)
+ tableView.setContentOffset(
+ CGPoint(x: 0, y: tableView.contentOffset.y - refreshControl.frame.size.height),
+ animated: true
+ )
}
// MARK: - Sync Methods
override internal func postTypeToSync() -> PostServiceType {
- return .page
+ .page
}
@MainActor
@@ -142,19 +158,34 @@ final class PageListViewController: AbstractPostListViewController {
return (posts, false)
}
- override func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((Bool) -> ())?, failure: ((NSError) -> ())?) {
+ override func syncHelper(
+ _ syncHelper: WPContentSyncHelper,
+ syncContentWithUserInteraction userInteraction: Bool,
+ success: ((Bool) -> ())?,
+ failure: ((NSError) -> ())?
+ ) {
// The success and failure blocks are called in the parent class `AbstractPostListViewController` by the `syncPosts` method. Since that class is
// used by both this one and the "Posts" screen, making changes to the sync helper is tough. To get around that, we make the fetch settings call
// async and then just await it before calling either the final success or failure block. This ensures that both the `syncPosts` call in the parent
// and the `fetchSettings` call here finish before calling the final success or failure block.
let (wrappedSuccess, wrappedFailure) = fetchEditorSettings(success: success, failure: failure)
- super.syncHelper(syncHelper, syncContentWithUserInteraction: userInteraction, success: wrappedSuccess, failure: wrappedFailure)
- }
-
- private func fetchEditorSettings(success: ((Bool) -> ())?, failure: ((NSError) -> ())?) -> (success: (_ hasMore: Bool) -> (), failure: (NSError) -> ()) {
+ super
+ .syncHelper(
+ syncHelper,
+ syncContentWithUserInteraction: userInteraction,
+ success: wrappedSuccess,
+ failure: wrappedFailure
+ )
+ }
+
+ private func fetchEditorSettings(
+ success: ((Bool) -> ())?,
+ failure: ((NSError) -> ())?
+ ) -> (success: (_ hasMore: Bool) -> (), failure: (NSError) -> ()) {
let fetchTask = Task { @MainActor [weak self] in
guard RemoteFeatureFlag.siteEditorMVP.enabled(),
- let result = await self?.editorSettingsService?.fetchSettings() else {
+ let result = await self?.editorSettingsService?.fetchSettings()
+ else {
return
}
switch result {
@@ -232,7 +263,13 @@ final class PageListViewController: AbstractPostListViewController {
// Do nothing
}
- override func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
+ override func controller(
+ _ controller: NSFetchedResultsController,
+ didChange anObject: Any,
+ at indexPath: IndexPath?,
+ for type: NSFetchedResultsChangeType,
+ newIndexPath: IndexPath?
+ ) {
// Do nothing, refresh all
}
@@ -245,7 +282,7 @@ final class PageListViewController: AbstractPostListViewController {
// MARK: - Core Data
override func entityName() -> String {
- return String(describing: Page.self)
+ String(describing: Page.self)
}
override func predicateForFetchRequest() -> NSPredicate {
@@ -276,10 +313,11 @@ final class PageListViewController: AbstractPostListViewController {
}
if RemoteFeatureFlag.siteEditorMVP.enabled(),
- blog.blockEditorSettings?.isFSETheme ?? false,
- let homepageID = blog.homepagePageID,
- let homepageType = blog.homepageType,
- homepageType == .page {
+ blog.blockEditorSettings?.isFSETheme ?? false,
+ let homepageID = blog.homepagePageID,
+ let homepageType = blog.homepageType,
+ homepageType == .page
+ {
predicates.append(NSPredicate(format: "postID != %i", homepageID))
}
@@ -298,9 +336,11 @@ final class PageListViewController: AbstractPostListViewController {
return
}
- let webViewController = WebViewControllerFactory.controller(url: editorUrl,
- blog: blog,
- source: Constant.Events.editHomepageSource)
+ let webViewController = WebViewControllerFactory.controller(
+ url: editorUrl,
+ blog: blog,
+ source: Constant.Events.editHomepageSource
+ )
let navigationController = UINavigationController(rootViewController: webViewController)
present(navigationController, animated: true)
case .pages:
@@ -310,7 +350,11 @@ final class PageListViewController: AbstractPostListViewController {
}
}
- func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ contextMenuConfigurationForRowAt indexPath: IndexPath,
+ point: CGPoint
+ ) -> UIContextMenuConfiguration? {
guard indexPath.section == Section.pages.rawValue else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
guard let self else { return nil }
@@ -320,13 +364,19 @@ final class PageListViewController: AbstractPostListViewController {
}
}
- func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
guard indexPath.section == Section.pages.rawValue else { return nil }
let actions = AbstractPostHelper.makeLeadingContextualActions(for: pages[indexPath.row], delegate: self)
return UISwipeActionsConfiguration(actions: actions)
}
- func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
guard indexPath.section == Section.pages.rawValue else { return nil }
let actions = AbstractPostHelper.makeTrailingContextualActions(for: pages[indexPath.row], delegate: self)
return UISwipeActionsConfiguration(actions: actions)
@@ -354,19 +404,29 @@ final class PageListViewController: AbstractPostListViewController {
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
return cell
case .pages:
- let cell = tableView.dequeueReusableCell(withIdentifier: Constant.Identifiers.pageCellIdentifier, for: indexPath) as! PageListCell
+ let cell =
+ tableView.dequeueReusableCell(withIdentifier: Constant.Identifiers.pageCellIdentifier, for: indexPath)
+ as! PageListCell
let page = pages[indexPath.row]
let indentation = getIndentationLevel(at: indexPath)
- let isFirstSubdirectory = getIndentationLevel(at: IndexPath(row: indexPath.row - 1, section: indexPath.section)) == (indentation - 1)
+ let isFirstSubdirectory =
+ getIndentationLevel(at: IndexPath(row: indexPath.row - 1, section: indexPath.section))
+ == (indentation - 1)
let viewModel = PageListItemViewModel(page: page)
- cell.configure(with: viewModel, indentation: indentation, isFirstSubdirectory: isFirstSubdirectory, delegate: self)
+ cell.configure(
+ with: viewModel,
+ indentation: indentation,
+ isFirstSubdirectory: isFirstSubdirectory,
+ delegate: self
+ )
return cell
}
}
private func getIndentationLevel(at indexPath: IndexPath) -> Int {
guard filterSettings.currentPostListFilter().filterType == .published,
- indexPath.row > 0 else {
+ indexPath.row > 0
+ else {
return 0
}
return pages[indexPath.row].hierarchyIndex
@@ -375,16 +435,31 @@ final class PageListViewController: AbstractPostListViewController {
// MARK: - Post Actions
override func createPost() {
- WPAppAnalytics.track(.editorCreatedPost, properties: [WPAppAnalyticsKeyTapSource: Constant.Events.source, WPAppAnalyticsKeyPostType: Constant.Events.pagePostType], blog: blog)
+ var context = NewPostEditorContext(
+ entryPoint: .pagesList,
+ analytics: .editorCreatedPost(
+ source: Constant.Events.source,
+ postType: Constant.Events.pagePostType
+ ),
+ animated: false
+ )
+ context.trackAnalytics(for: blog)
+ context.analytics = .none
PageCoordinator.showLayoutPickerIfNeeded(from: self, forBlog: blog) { [weak self] selectedLayout in
- self?.createPage(selectedLayout)
+ self?.createPage(selectedLayout, context: context)
}
}
- private func createPage(_ starterLayout: PageTemplateLayout?) {
- let editorViewController = EditPageViewController(blog: blog, postTitle: starterLayout?.title, content: starterLayout?.content)
- present(editorViewController, animated: false)
+ private func createPage(_ starterLayout: PageTemplateLayout?, context: NewPostEditorContext) {
+ var context = context
+ context.title = starterLayout?.title
+ context.content = starterLayout?.content
+ PostEditorRouter.showNewPage(
+ for: blog,
+ from: self,
+ context: context
+ )
}
// MARK: - Cell Action Handling
@@ -392,26 +467,38 @@ final class PageListViewController: AbstractPostListViewController {
func setPageAsHomepage(_ page: Page) {
guard let homePageID = page.postID?.intValue else { return }
beginRefreshingManually()
- homepageSettingsService?.setHomepageType(.page, homePageID: homePageID, success: { [weak self] in
- self?.refreshAndReload()
- self?.handleHomepageSettingsSuccess()
- }, failure: { [weak self] _ in
- self?.refreshControl.endRefreshing()
- self?.handleHomepageSettingsFailure()
- })
+ homepageSettingsService?
+ .setHomepageType(
+ .page,
+ homePageID: homePageID,
+ success: { [weak self] in
+ self?.refreshAndReload()
+ self?.handleHomepageSettingsSuccess()
+ },
+ failure: { [weak self] _ in
+ self?.refreshControl.endRefreshing()
+ self?.handleHomepageSettingsFailure()
+ }
+ )
}
func togglePageAsPostsPage(_ page: Page) {
let newValue = !page.isSitePostsPage
let postsPageID = page.isSitePostsPage ? 0 : (page.postID?.intValue ?? 0)
beginRefreshingManually()
- homepageSettingsService?.setHomepageType(.page, withPostsPageID: postsPageID, success: { [weak self] in
- self?.refreshAndReload()
- self?.handleHomepagePostsPageSettingsSuccess(isPostsPage: newValue)
- }, failure: { [weak self] _ in
- self?.refreshControl.endRefreshing()
- self?.handleHomepageSettingsFailure()
- })
+ homepageSettingsService?
+ .setHomepageType(
+ .page,
+ withPostsPageID: postsPageID,
+ success: { [weak self] in
+ self?.refreshAndReload()
+ self?.handleHomepagePostsPageSettingsSuccess(isPostsPage: newValue)
+ },
+ failure: { [weak self] _ in
+ self?.refreshControl.endRefreshing()
+ self?.handleHomepageSettingsFailure()
+ }
+ )
}
private func handleHomepageSettingsSuccess() {
@@ -420,29 +507,52 @@ final class PageListViewController: AbstractPostListViewController {
}
private func handleHomepagePostsPageSettingsSuccess(isPostsPage: Bool) {
- let title = isPostsPage ? HomepageSettingsText.updatePostsPageSuccessTitle : HomepageSettingsText.updatePageSuccessTitle
+ let title =
+ isPostsPage ? HomepageSettingsText.updatePostsPageSuccessTitle : HomepageSettingsText.updatePageSuccessTitle
let notice = Notice(title: title, feedbackType: .success)
ActionDispatcher.global.dispatch(NoticeAction.post(notice))
}
private func handleHomepageSettingsFailure() {
- let notice = Notice(title: HomepageSettingsText.updateErrorTitle, message: HomepageSettingsText.updateErrorMessage, feedbackType: .error)
+ let notice = Notice(
+ title: HomepageSettingsText.updateErrorTitle,
+ message: HomepageSettingsText.updateErrorMessage,
+ feedbackType: .error
+ )
ActionDispatcher.global.dispatch(NoticeAction.post(notice))
}
// MARK: - NetworkAwareUI
override func noConnectionMessage() -> String {
- return NSLocalizedString("No internet connection. Some pages may be unavailable while offline.",
- comment: "Error message shown when the user is browsing Site Pages without an internet connection.")
+ NSLocalizedString(
+ "No internet connection. Some pages may be unavailable while offline.",
+ comment: "Error message shown when the user is browsing Site Pages without an internet connection."
+ )
}
struct HomepageSettingsText {
- static let updateErrorTitle = NSLocalizedString("Unable to update homepage settings", comment: "Error informing the user that their homepage settings could not be updated")
- static let updateErrorMessage = NSLocalizedString("Please try again later.", comment: "Prompt for the user to retry a failed action again later")
- static let updatePageSuccessTitle = NSLocalizedString("pages.updatePage.successTitle", value: "Page successfully updated", comment: "Message informing the user that their static homepage page was set successfully")
- static let updateHomepageSuccessTitle = NSLocalizedString("Homepage successfully updated", comment: "Message informing the user that their static homepage page was set successfully")
- static let updatePostsPageSuccessTitle = NSLocalizedString("Posts page successfully updated", comment: "Message informing the user that their static homepage for posts was set successfully")
+ static let updateErrorTitle = NSLocalizedString(
+ "Unable to update homepage settings",
+ comment: "Error informing the user that their homepage settings could not be updated"
+ )
+ static let updateErrorMessage = NSLocalizedString(
+ "Please try again later.",
+ comment: "Prompt for the user to retry a failed action again later"
+ )
+ static let updatePageSuccessTitle = NSLocalizedString(
+ "pages.updatePage.successTitle",
+ value: "Page successfully updated",
+ comment: "Message informing the user that their static homepage page was set successfully"
+ )
+ static let updateHomepageSuccessTitle = NSLocalizedString(
+ "Homepage successfully updated",
+ comment: "Message informing the user that their static homepage page was set successfully"
+ )
+ static let updatePostsPageSuccessTitle = NSLocalizedString(
+ "Posts page successfully updated",
+ comment: "Message informing the user that their static homepage for posts was set successfully"
+ )
}
}
@@ -453,20 +563,33 @@ private extension PageListViewController {
func handleRefreshNoResultsViewController(_ noResultsViewController: NoResultsViewController) {
guard connectionAvailable() else {
- noResultsViewController.configure(title: "", noConnectionTitle: NoResultsText.noConnectionTitle, buttonTitle: nil, subtitle: nil, noConnectionSubtitle: NoResultsText.noConnectionSubtitle, attributedSubtitle: nil, attributedSubtitleConfiguration: nil, image: nil, subtitleImage: nil, accessoryView: nil)
+ noResultsViewController.configure(
+ title: "",
+ noConnectionTitle: NoResultsText.noConnectionTitle,
+ buttonTitle: nil,
+ subtitle: nil,
+ noConnectionSubtitle: NoResultsText.noConnectionSubtitle,
+ attributedSubtitle: nil,
+ attributedSubtitleConfiguration: nil,
+ image: nil,
+ subtitleImage: nil,
+ accessoryView: nil
+ )
return
}
let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil
- noResultsViewController.configure(title: noResultsTitle(),
- buttonTitle: nil,
- image: noResultsImageName,
- accessoryView: accessoryView)
+ noResultsViewController.configure(
+ title: noResultsTitle(),
+ buttonTitle: nil,
+ image: noResultsImageName,
+ accessoryView: accessoryView
+ )
}
var noResultsImageName: String {
- return "pages-no-results"
+ "pages-no-results"
}
func noResultsTitle() -> String {
@@ -493,11 +616,29 @@ private extension PageListViewController {
}
struct NoResultsText {
- static let noDraftsTitle = NSLocalizedString("You don't have any draft pages", comment: "Displayed when the user views drafts in the pages list and there are no pages")
- static let noScheduledTitle = NSLocalizedString("You don't have any scheduled pages", comment: "Displayed when the user views scheduled pages in the pages list and there are no pages")
- static let noTrashedTitle = NSLocalizedString("You don't have any trashed pages", comment: "Displayed when the user views trashed in the pages list and there are no pages")
- static let noPublishedTitle = NSLocalizedString("You haven't published any pages yet", comment: "Displayed when the user views published pages in the pages list and there are no pages")
- static let noConnectionTitle: String = NSLocalizedString("Unable to load pages right now.", comment: "Title for No results full page screen displayedfrom pages list when there is no connection")
- static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a page.", comment: "Subtitle for No results full page screen displayed from pages list when there is no connection")
+ static let noDraftsTitle = NSLocalizedString(
+ "You don't have any draft pages",
+ comment: "Displayed when the user views drafts in the pages list and there are no pages"
+ )
+ static let noScheduledTitle = NSLocalizedString(
+ "You don't have any scheduled pages",
+ comment: "Displayed when the user views scheduled pages in the pages list and there are no pages"
+ )
+ static let noTrashedTitle = NSLocalizedString(
+ "You don't have any trashed pages",
+ comment: "Displayed when the user views trashed in the pages list and there are no pages"
+ )
+ static let noPublishedTitle = NSLocalizedString(
+ "You haven't published any pages yet",
+ comment: "Displayed when the user views published pages in the pages list and there are no pages"
+ )
+ static let noConnectionTitle: String = NSLocalizedString(
+ "Unable to load pages right now.",
+ comment: "Title for No results full page screen displayedfrom pages list when there is no connection"
+ )
+ static let noConnectionSubtitle: String = NSLocalizedString(
+ "Check your network connection and try again. Or draft a page.",
+ comment: "Subtitle for No results full page screen displayed from pages list when there is no connection"
+ )
}
}
diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift
index 310082c72f7d..d67d655be498 100644
--- a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift
+++ b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift
@@ -17,7 +17,11 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
return vc
}
- static func showForBlog(_ blog: Blog, from sourceController: UIViewController, withPostStatus postStatus: BasePost.Status? = nil) {
+ static func showForBlog(
+ _ blog: Blog,
+ from sourceController: UIViewController,
+ withPostStatus postStatus: BasePost.Status? = nil
+ ) {
let controller = PostListViewController.controllerWithBlog(blog)
controller.navigationItem.largeTitleDisplayMode = .never
controller.initialFilterWithPostStatus = postStatus
@@ -34,7 +38,11 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
configureInitialFilterIfNeeded()
listenForAppComingToForeground()
- createButtonCoordinator.add(to: view, trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor, bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor)
+ createButtonCoordinator.add(
+ to: view,
+ trailingAnchor: view.safeAreaLayoutGuide.trailingAnchor,
+ bottomAnchor: view.safeAreaLayoutGuide.bottomAnchor
+ )
refreshNoResultsViewController = { [weak self] in
self?.handleRefreshNoResultsViewController($0)
@@ -47,10 +55,13 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
private lazy var createButtonCoordinator: CreateButtonCoordinator = {
var actions: [ActionSheetItem] = [
- PostAction(handler: { [weak self] in
- self?.dismiss(animated: false, completion: nil)
- self?.createPost()
- }, source: Constants.source)
+ PostAction(
+ handler: { [weak self] in
+ self?.dismiss(animated: false, completion: nil)
+ self?.createPost()
+ },
+ source: Constants.source
+ )
]
return CreateButtonCoordinator(self, actions: actions, source: Constants.source, blog: blog)
}()
@@ -90,16 +101,18 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
/// Listens for the app coming to foreground in order to properly set the create button
private func listenForAppComingToForeground() {
- NotificationCenter.default.addObserver(self,
- selector: #selector(toggleCreateButton),
- name: UIApplication.willEnterForegroundNotification,
- object: nil)
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(toggleCreateButton),
+ name: UIApplication.willEnterForegroundNotification,
+ object: nil
+ )
}
// MARK: - Sync Methods
override func postTypeToSync() -> PostServiceType {
- return .post
+ .post
}
// MARK: - Data Model Interaction
@@ -114,7 +127,7 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
// MARK: - TableViewHandler
override func entityName() -> String {
- return String(describing: Post.self)
+ String(describing: Post.self)
}
override func predicateForFetchRequest() -> NSPredicate {
@@ -144,7 +157,8 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: PostListCell.defaultReuseID, for: indexPath) as! PostListCell
+ let cell =
+ tableView.dequeueReusableCell(withIdentifier: PostListCell.defaultReuseID, for: indexPath) as! PostListCell
let post = postAtIndexPath(indexPath)
cell.accessoryType = .none
cell.configure(with: PostListItemViewModel(post: post, shouldHideAuthor: shouldHideAuthor), delegate: self)
@@ -162,7 +176,11 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
editPost(post)
}
- func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ contextMenuConfigurationForRowAt indexPath: IndexPath,
+ point: CGPoint
+ ) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
guard let self else { return nil }
let post = self.postAtIndexPath(indexPath)
@@ -171,12 +189,18 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
}
}
- func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
let actions = AbstractPostHelper.makeLeadingContextualActions(for: postAtIndexPath(indexPath), delegate: self)
return UISwipeActionsConfiguration(actions: actions)
}
- func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ func tableView(
+ _ tableView: UITableView,
+ trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
+ ) -> UISwipeActionsConfiguration? {
let actions = AbstractPostHelper.makeTrailingContextualActions(for: postAtIndexPath(indexPath), delegate: self)
return UISwipeActionsConfiguration(actions: actions)
}
@@ -184,11 +208,14 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
// MARK: - Post Actions
override func createPost() {
- let editor = EditPostViewController(blog: blog)
- editor.modalPresentationStyle = .fullScreen
- editor.entryPoint = .postsList
- present(editor, animated: false, completion: nil)
- WPAppAnalytics.track(.editorCreatedPost, properties: [WPAppAnalyticsKeyTapSource: "posts_view", WPAppAnalyticsKeyPostType: "post"], blog: blog)
+ PostEditorRouter.showNewPost(
+ for: blog,
+ from: self,
+ context: NewPostEditorContext(
+ entryPoint: .postsList,
+ analytics: .editorCreatedPost(source: "posts_view", postType: "post")
+ )
+ )
}
private func editPost(_ post: AbstractPost) {
@@ -243,7 +270,12 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
func comments(_ post: AbstractPost) {
WPAnalytics.track(.postListCommentsAction, properties: propertiesForAnalytics())
let contentCoordinator = DefaultContentCoordinator(controller: self, context: ContextManager.shared.mainContext)
- try? contentCoordinator.displayCommentsWithPostId(post.postID, siteID: blog.dotComID, commentID: nil, source: .postsList)
+ try? contentCoordinator.displayCommentsWithPostId(
+ post.postID,
+ siteID: blog.dotComID,
+ commentID: nil,
+ source: .postsList
+ )
}
func showSettings(for post: AbstractPost) {
@@ -254,8 +286,10 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
// MARK: - NetworkAwareUI
override func noConnectionMessage() -> String {
- return NSLocalizedString("No internet connection. Some posts may be unavailable while offline.",
- comment: "Error message shown when the user is browsing Site Posts without an internet connection.")
+ NSLocalizedString(
+ "No internet connection. Some posts may be unavailable while offline.",
+ comment: "Error message shown when the user is browsing Site Posts without an internet connection."
+ )
}
private enum Constants {
@@ -270,20 +304,33 @@ private extension PostListViewController {
func handleRefreshNoResultsViewController(_ noResultsViewController: NoResultsViewController) {
guard connectionAvailable() else {
- noResultsViewController.configure(title: "", noConnectionTitle: NoResultsText.noConnectionTitle, buttonTitle: nil, subtitle: nil, noConnectionSubtitle: NoResultsText.noConnectionSubtitle, attributedSubtitle: nil, attributedSubtitleConfiguration: nil, image: nil, subtitleImage: nil, accessoryView: nil)
+ noResultsViewController.configure(
+ title: "",
+ noConnectionTitle: NoResultsText.noConnectionTitle,
+ buttonTitle: nil,
+ subtitle: nil,
+ noConnectionSubtitle: NoResultsText.noConnectionSubtitle,
+ attributedSubtitle: nil,
+ attributedSubtitleConfiguration: nil,
+ image: nil,
+ subtitleImage: nil,
+ accessoryView: nil
+ )
return
}
let accessoryView = syncHelper.isSyncing ? NoResultsViewController.loadingAccessoryView() : nil
- noResultsViewController.configure(title: noResultsTitle(),
- buttonTitle: nil,
- image: noResultsImageName,
- accessoryView: accessoryView)
+ noResultsViewController.configure(
+ title: noResultsTitle(),
+ buttonTitle: nil,
+ image: noResultsImageName,
+ accessoryView: accessoryView
+ )
}
var noResultsImageName: String {
- return "posts-no-results"
+ "posts-no-results"
}
func noResultsTitle() -> String {
@@ -310,11 +357,29 @@ private extension PostListViewController {
}
struct NoResultsText {
- static let noDraftsTitle = NSLocalizedString("You don't have any draft posts", comment: "Displayed when the user views drafts in the posts list and there are no posts")
- static let noScheduledTitle = NSLocalizedString("You don't have any scheduled posts", comment: "Displayed when the user views scheduled posts in the posts list and there are no posts")
- static let noTrashedTitle = NSLocalizedString("You don't have any trashed posts", comment: "Displayed when the user views trashed in the posts list and there are no posts")
- static let noPublishedTitle = NSLocalizedString("You haven't published any posts yet", comment: "Displayed when the user views published posts in the posts list and there are no posts")
- static let noConnectionTitle: String = NSLocalizedString("Unable to load posts right now.", comment: "Title for No results full page screen displayedfrom post list when there is no connection")
- static let noConnectionSubtitle: String = NSLocalizedString("Check your network connection and try again. Or draft a post.", comment: "Subtitle for No results full page screen displayed from post list when there is no connection")
+ static let noDraftsTitle = NSLocalizedString(
+ "You don't have any draft posts",
+ comment: "Displayed when the user views drafts in the posts list and there are no posts"
+ )
+ static let noScheduledTitle = NSLocalizedString(
+ "You don't have any scheduled posts",
+ comment: "Displayed when the user views scheduled posts in the posts list and there are no posts"
+ )
+ static let noTrashedTitle = NSLocalizedString(
+ "You don't have any trashed posts",
+ comment: "Displayed when the user views trashed in the posts list and there are no posts"
+ )
+ static let noPublishedTitle = NSLocalizedString(
+ "You haven't published any posts yet",
+ comment: "Displayed when the user views published posts in the posts list and there are no posts"
+ )
+ static let noConnectionTitle: String = NSLocalizedString(
+ "Unable to load posts right now.",
+ comment: "Title for No results full page screen displayedfrom post list when there is no connection"
+ )
+ static let noConnectionSubtitle: String = NSLocalizedString(
+ "Check your network connection and try again. Or draft a post.",
+ comment: "Subtitle for No results full page screen displayed from post list when there is no connection"
+ )
}
}
diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorAnalyticsSession.swift b/WordPress/Classes/ViewRelated/Post/PostEditorAnalyticsSession.swift
index a632655da8fa..b054935068ad 100644
--- a/WordPress/Classes/ViewRelated/Post/PostEditorAnalyticsSession.swift
+++ b/WordPress/Classes/ViewRelated/Post/PostEditorAnalyticsSession.swift
@@ -15,11 +15,27 @@ struct PostEditorAnalyticsSession {
private let startTime = DispatchTime.now().uptimeNanoseconds
init(editor: Editor, post: AbstractPost) {
- currentEditor = editor
- postType = post.analyticsPostType ?? "unsupported"
- blogID = post.blog.dotComID
- blogType = post.blog.analyticsType.rawValue
- contentType = ContentType(post: post).rawValue
+ self.init(
+ editor: editor,
+ blog: post.blog,
+ postType: post.analyticsPostType ?? "unsupported",
+ contentType: ContentType(post: post)
+ )
+ }
+
+ init(
+ editor: Editor,
+ blog: Blog,
+ postType: String,
+ contentType: ContentType,
+ entryPoint: PostEditorEntryPoint? = nil
+ ) {
+ self.currentEditor = editor
+ self.postType = postType
+ self.blogID = blog.dotComID
+ self.blogType = blog.analyticsType.rawValue
+ self.contentType = contentType.rawValue
+ self.entryPoint = entryPoint
}
mutating func start(unsupportedBlocks: [String] = []) {
@@ -37,7 +53,7 @@ struct PostEditorAnalyticsSession {
// Let's make sure to round the value and send an integer for consistency
let startupTimeNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime
let startupTimeMilliseconds = Int(Double(startupTimeNanoseconds) / 1_000_000)
- var properties: [String: Any] = [ Property.startupTime: startupTimeMilliseconds ]
+ var properties: [String: Any] = [Property.startupTime: startupTimeMilliseconds]
// Tracks custom event types can't be arrays so we need to convert this to JSON
if let data = try? JSONSerialization.data(withJSONObject: unsupportedBlocks, options: .fragmentsAllowed) {
@@ -60,7 +76,8 @@ struct PostEditorAnalyticsSession {
let properties: [String: Any] = [
Property.outcome: outcome.rawValue,
Property.entryPoint: (entryPoint ?? .unknown).rawValue
- ].merging(commonProperties, uniquingKeysWith: { $1 })
+ ]
+ .merging(commonProperties, uniquingKeysWith: { $1 })
WPAppAnalytics.track(.editorSessionEnd, withProperties: properties)
}
@@ -83,15 +100,16 @@ private extension PostEditorAnalyticsSession {
}
var commonProperties: [String: String] {
- return [
+ [
Property.editor: currentEditor.rawValue,
Property.contentType: contentType,
Property.postType: postType,
Property.blogID: blogID?.stringValue,
Property.blogType: blogType,
Property.sessionId: sessionId,
- Property.hasUnsupportedBlocks: hasUnsupportedBlocks ? "1" : "0",
- ].compactMapValues { $0 }
+ Property.hasUnsupportedBlocks: hasUnsupportedBlocks ? "1" : "0"
+ ]
+ .compactMapValues { $0 }
}
}
@@ -109,9 +127,15 @@ extension PostEditorAnalyticsSession {
case classic
init(post: AbstractPost) {
- if post.isContentEmpty() {
+ self.init(content: post.content)
+ }
+
+ init(content: String?) {
+ // A duplication of `BasePost.isContentEmpty`
+ let emptyGutenbergParagraph = "\n\n"
+ if content?.isEmpty != false || content == emptyGutenbergParagraph {
self = .new
- } else if post.containsGutenbergBlocks() {
+ } else if content?.contains("