From cebfd401f5b88762706ba24dbc425b44f5f83fc2 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 22 Apr 2026 16:52:05 +0200 Subject: [PATCH 01/10] Add on this day stream --- .../FormattableContent/FormattableContentRange.swift | 1 + Modules/Sources/FormattableContentKit/Notifiable.swift | 1 + .../WordPressKit/ReaderPostServiceRemote+Cards.swift | 1 + WordPress/Classes/System/Root View/ReaderPresenter.swift | 9 ++++++++- .../Controllers/ReaderDiscoverViewController.swift | 9 ++++++--- .../Reader/Headers/ReaderDiscoverHeaderView.swift | 6 ++++++ .../Reader/Navigation/ReaderNavigationPath.swift | 1 + 7 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift index 71ea93b69c5b..e237475971b6 100644 --- a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift +++ b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift @@ -76,6 +76,7 @@ extension FormattableRangeKind { public static let user = FormattableRangeKind("user") public static let post = FormattableRangeKind("post") public static let comment = FormattableRangeKind("comment") + public static let onThisDay = FormattableRangeKind("on_this_day") public static let stats = FormattableRangeKind("stat") public static let follow = FormattableRangeKind("follow") public static let blockquote = FormattableRangeKind("blockquote") diff --git a/Modules/Sources/FormattableContentKit/Notifiable.swift b/Modules/Sources/FormattableContentKit/Notifiable.swift index 68653fcb7eec..d54617787e5d 100644 --- a/Modules/Sources/FormattableContentKit/Notifiable.swift +++ b/Modules/Sources/FormattableContentKit/Notifiable.swift @@ -15,6 +15,7 @@ public enum NotificationKind: String, Sendable { case user = "user" case login = "push_auth" case viewMilestone = "view_milestone" + case onThisDay = "on_this_day" case unknown = "unknown" } diff --git a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift index f89a47939e4c..af76c58179da 100644 --- a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift +++ b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift @@ -16,6 +16,7 @@ public enum ReaderStream: String { case discover = "discover" case freshlyPressed = "freshly-pressed" case firstPosts = "first-posts" + case onThisDay = "on-this-day" } extension ReaderPostServiceRemote { diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index f9d8ec43a9aa..dcb837c1ebac 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -22,6 +22,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } private var selectionObserver: AnyCancellable? + private var pendingDiscoverChannel: ReaderDiscoverChannel? public convenience override init() { self.init(viewModel: ReaderSidebarViewModel()) @@ -141,7 +142,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .recent, .discover, .likes: if let topic = screen.topicType.flatMap(sidebarViewModel.getTopic) { if screen == .discover { - return ReaderDiscoverViewController(topic: topic) + let initialChannel = pendingDiscoverChannel ?? .freshlyPressed + pendingDiscoverChannel = nil + return ReaderDiscoverViewController(topic: topic, initialChannel: initialChannel) } else { return ReaderStreamViewController.controllerWithTopic(topic) } @@ -290,6 +293,10 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .recent: viewModel.selection = .main(.recent) case .discover: + pendingDiscoverChannel = nil + viewModel.selection = .main(.discover) + case .discoverOnThisDay: + pendingDiscoverChannel = .onThisDay viewModel.selection = .main(.discover) case .likes: viewModel.selection = .main(.likes) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift index 3a0c21a6048e..6664e1053a08 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift @@ -7,7 +7,7 @@ import WordPressShared class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDelegate { private let headerView = ReaderDiscoverHeaderView() - private var selectedChannel: ReaderDiscoverChannel = .freshlyPressed + private var selectedChannel: ReaderDiscoverChannel private let topic: ReaderAbstractTopic private var streamVC: ReaderStreamViewController? private weak var selectInterestsVC: ReaderSelectInterestsViewController? @@ -18,10 +18,11 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe private let notificationsButtonViewModel = NotificationsButtonViewModel() private var notificationsButtonCancellable: AnyCancellable? - init(topic: ReaderAbstractTopic) { + init(topic: ReaderAbstractTopic, initialChannel: ReaderDiscoverChannel = .freshlyPressed) { wpAssert(ReaderHelpers.topicIsDiscover(topic)) self.viewContext = ContextManager.shared.mainContext self.topic = topic + self.selectedChannel = initialChannel self.tags = ManagedObjectsObserver( predicate: ReaderSidebarTagsSection.predicate, sortDescriptors: [SortDescriptor(\.title, order: .forward)], @@ -90,7 +91,7 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe .filter { $0.slug != ReaderTagTopic.dailyPromptTag } .map(ReaderDiscoverChannel.tag) - headerView.configure(channels: [.freshlyPressed, .recommended, .latest, .firstPosts, .dailyPrompts] + channels) + headerView.configure(channels: [.freshlyPressed, .recommended, .latest, .firstPosts, .dailyPrompts, .onThisDay] + channels) headerView.setSelectedChannel(selectedChannel) } @@ -112,6 +113,8 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe ReaderDiscoverStreamViewController(topic: topic, sorting: .date) case .dailyPrompts: ReaderStreamViewController.controllerWithTagSlug(ReaderTagTopic.dailyPromptTag) + case .onThisDay: + ReaderDiscoverStreamViewController(topic: topic, stream: .onThisDay) case .tag(let tag): ReaderStreamViewController.controllerWithTopic(tag) } diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index da0bc38a870e..279d89c519b1 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -180,6 +180,9 @@ enum ReaderDiscoverChannel: Hashable { case dailyPrompts + /// Posts you published on this day in previous years. + case onThisDay + /// A quick access for your tags. case tag(ReaderTagTopic) @@ -197,6 +200,8 @@ enum ReaderDiscoverChannel: Hashable { NSLocalizedString("reader.discover.channel.dailyPrompts", value: "Daily Prompts", comment: "Header view channel (filter)") case .tag(let tag): tag.formattedTitle + case .onThisDay: + NSLocalizedString("reader.discover.channel.onThisDay", value: "On This Day", comment: "Header view channel (filter) showing posts you published on this day in previous years") } } @@ -215,6 +220,7 @@ enum ReaderDiscoverChannel: Hashable { case .firstPosts: "first_posts" case .latest: "latest" case .dailyPrompts: "daily_prompts" + case .onThisDay: "on_this_day" case .tag: "tag" } } diff --git a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift index 4a5097a05701..390d1c98d259 100644 --- a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift +++ b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift @@ -6,6 +6,7 @@ import WordPressKit enum ReaderNavigationPath: Hashable { case recent case discover + case discoverOnThisDay case likes case search case subscriptions From 7fefb70a139e1706a2584208846396d5f3d14158 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 15:14:44 +0200 Subject: [PATCH 02/10] Cleaner approach --- .../FormattableContentRange.swift | 1 + .../Ranges/NotificationContentRange.swift | 3 ++ .../NotificationContentRangeFactory.swift | 1 + .../ReaderPostServiceRemote+Cards.swift | 44 +++++++++---------- .../notifications-readerstream-range.json | 8 ++++ .../Activity/MockContentCoordinator.swift | 15 ++++++- ...NotificationContentRangeFactoryTests.swift | 11 +++++ .../Classes/Utility/ContentCoordinator.swift | 29 +++++++++--- .../NotificationContentRouter.swift | 24 ++++++---- 9 files changed, 96 insertions(+), 40 deletions(-) create mode 100644 Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json diff --git a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift index e237475971b6..ed98a375384a 100644 --- a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift +++ b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift @@ -76,6 +76,7 @@ extension FormattableRangeKind { public static let user = FormattableRangeKind("user") public static let post = FormattableRangeKind("post") public static let comment = FormattableRangeKind("comment") + public static let readerStream = FormattableRangeKind("readerstream") public static let onThisDay = FormattableRangeKind("on_this_day") public static let stats = FormattableRangeKind("stat") public static let follow = FormattableRangeKind("follow") diff --git a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift index cf84913e4aa4..f468bf4c5028 100644 --- a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift +++ b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift @@ -7,6 +7,7 @@ public class NotificationContentRange: FormattableContentRange, LinkContentRange public let userID: NSNumber? public let siteID: NSNumber? public let postID: NSNumber? + public let streamKey: String? public let url: URL? public init(kind: FormattableRangeKind, properties: Properties) { @@ -16,6 +17,7 @@ public class NotificationContentRange: FormattableContentRange, LinkContentRange siteID = properties.siteID userID = properties.userID postID = properties.postID + streamKey = properties.streamKey } } @@ -26,6 +28,7 @@ extension NotificationContentRange { public var siteID: NSNumber? public var userID: NSNumber? public var postID: NSNumber? + public var streamKey: String? public init(range: NSRange) { self.range = range diff --git a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift index ed266fb22454..cafe8540ccf4 100644 --- a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift +++ b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift @@ -19,6 +19,7 @@ struct NotificationContentRangeFactory: FormattableRangesFactory { properties.siteID = dictionary[RangeKeys.siteId] as? NSNumber properties.postID = dictionary[RangeKeys.postId] as? NSNumber + properties.streamKey = dictionary[RangeKeys.id] as? String if let url = dictionary[RangeKeys.url] as? String { properties.url = URL(string: url) diff --git a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift index af76c58179da..9993e9ab952a 100644 --- a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift +++ b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift @@ -32,16 +32,16 @@ extension ReaderPostServiceRemote { /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid public func fetchCards(for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { let path = "read/tags/cards" guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, + topics: topics, + page: page, + sortingOption: sortingOption, refreshCount: refreshCount) else { return } @@ -62,19 +62,19 @@ extension ReaderPostServiceRemote { /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid public func fetchStreamCards(stream: ReaderStream = .discover, - for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - count: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, + for topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + count: Int? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { let path = "read/streams/\(stream.rawValue)" guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, - count: count, + topics: topics, + page: page, + sortingOption: sortingOption, + count: count, refreshCount: refreshCount) else { return } @@ -82,7 +82,7 @@ extension ReaderPostServiceRemote { } private func fetch(_ endpoint: String, - success: @escaping ([RemoteReaderCard], String?) -> Void, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { Task { @MainActor [wordPressComRestApi] in await wordPressComRestApi.perform(.get, URLString: endpoint, type: ReaderCardEnvelope.self) @@ -93,10 +93,10 @@ extension ReaderPostServiceRemote { } private func cardsEndpoint(with path: String, - topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - count: Int? = nil, + topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + count: Int? = nil, refreshCount: Int? = nil) -> String? { var path = URLComponents(string: path) diff --git a/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json b/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json new file mode 100644 index 000000000000..760f85871b66 --- /dev/null +++ b/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json @@ -0,0 +1,8 @@ +{ + "url" : "https://wordpress.com/read/tags/mental-health", + "type" : "readerstream", + "indices" : [ + 0, + 12 + ] +} diff --git a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift index f6e0866368d1..c0cb4cc2d0be 100644 --- a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift +++ b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift @@ -1,4 +1,3 @@ - @testable import WordPress class MockContentCoordinator: ContentCoordinator { @@ -15,7 +14,12 @@ class MockContentCoordinator: ContentCoordinator { var commentsWasDisplayed = false var commentPostID: NSNumber? var commentSiteID: NSNumber? - func displayCommentsWithPostId(_ postID: NSNumber?, siteID: NSNumber?, commentID: NSNumber?, source: ReaderCommentsSource) throws { + func displayCommentsWithPostId( + _ postID: NSNumber?, + siteID: NSNumber?, + commentID: NSNumber?, + source: ReaderCommentsSource + ) throws { commentsWasDisplayed = true commentPostID = postID commentSiteID = siteID @@ -44,6 +48,13 @@ class MockContentCoordinator: ContentCoordinator { streamSiteID = siteID } + var streamWasDisplayedByStreamKey = false + var streamKey: String? + func displayStreamWithStreamKey(_ streamKey: String?) throws { + streamWasDisplayedByStreamKey = true + self.streamKey = streamKey + } + func displayWebViewWithURL(_ url: URL, source: String) { } diff --git a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift index e43061bcea85..4cfe07035b52 100644 --- a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift @@ -40,6 +40,13 @@ final class NotificationContentRangeFactoryTests: XCTestCase { XCTAssertNotNil(subject) } + func testReaderStreamRangeReturnsExpectedKind() throws { + let subject = + NotificationContentRangeFactory.contentRange(from: try mockReaderStreamRange()) as? NotificationContentRange + + XCTAssertEqual(subject?.kind, .readerStream) + } + private func mockCommentRange() throws -> JSONObject { return try JSONObject(fromFileNamed: "notifications-comment-range.json") } @@ -64,4 +71,8 @@ final class NotificationContentRangeFactoryTests: XCTestCase { return try JSONObject(fromFileNamed: "notifications-blockquote-range.json") } + private func mockReaderStreamRange() throws -> JSONObject { + try JSONObject(fromFileNamed: "notifications-readerstream-range.json") + } + } diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index bfdeb94b5d85..cd8da242eb83 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -8,6 +8,7 @@ protocol ContentCoordinator { func displayStatsWithSiteID(_ siteID: NSNumber?, url: URL?) throws func displayFollowersWithSiteID(_ siteID: NSNumber?, expirationTime: TimeInterval) throws func displayStreamWithSiteID(_ siteID: NSNumber?) throws + func displayStreamWithStreamKey(_ streamKey: String?) throws func displayWebViewWithURL(_ url: URL, source: String) func displayFullscreenImage(_ image: UIImage) func displayPlugin(withSlug pluginSlug: String, on siteSlug: String) throws @@ -54,8 +55,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayStatsWithSiteID(_ siteID: NSNumber?, url: URL? = nil) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.supports(.stats) + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.supports(.stats) else { throw DisplayError.missingParameter } @@ -77,7 +78,7 @@ struct DefaultContentCoordinator: ContentCoordinator { let matcher = RouteMatcher(routes: UniversalLinkRouter.statsRoutes) let matches = matcher.routesMatching(url) if let match = matches.first, - let action = match.action as? StatsRoute, + let action = match.action as? StatsRoute, let tab = action.tab { SiteStatsDashboardPreferences.setSelected(tabType: tab, siteID: siteID) } @@ -85,7 +86,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayBackupWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } @@ -99,8 +100,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayScanWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.isScanAllowed() + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.isScanAllowed() else { throw DisplayError.missingParameter } @@ -111,7 +112,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayFollowersWithSiteID(_ siteID: NSNumber?, expirationTime: TimeInterval) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } @@ -134,6 +135,20 @@ struct DefaultContentCoordinator: ContentCoordinator { controller?.navigationController?.pushViewController(browseViewController, animated: true) } + func displayStreamWithStreamKey(_ streamKey: String?) throws { + guard let streamKey, !streamKey.isEmpty else { + throw DisplayError.missingParameter + } + + if streamKey == "on-this-day" { + RootViewCoordinator.sharedPresenter.showReader(path: .discoverOnThisDay) + return + } + + let browseViewController = ReaderStreamViewController.controllerWithTagSlug(streamKey) + controller?.navigationController?.pushViewController(browseViewController, animated: true) + } + func displayWebViewWithURL(_ url: URL, source: String) { if UniversalLinkRouter(routes: UniversalLinkRouter.readerRoutes).canHandle(url: url) { UniversalLinkRouter(routes: UniversalLinkRouter.readerRoutes).handle(url: url, source: .inApp(presenter: controller)) diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift index ad17aa74d345..4b139c9bf9b1 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift @@ -57,16 +57,18 @@ struct NotificationContentRouter { case .comment: // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID - try coordinator.displayCommentsWithPostId(notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, - source: .commentNotification) + try coordinator.displayCommentsWithPostId( + notification.metaPostID, + siteID: notification.metaSiteID, + commentID: commentID, + source: .commentNotification + ) case .commentLike: // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID try coordinator.displayCommentsWithPostId(notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, + siteID: notification.metaSiteID, + commentID: commentID, source: .commentLikeNotification) default: throw DefaultContentCoordinator.DisplayError.unsupportedType @@ -87,8 +89,8 @@ struct NotificationContentRouter { // Focus on the comment reply if it's set over the primary comment ID let commentID = notification.metaReplyID ?? notification.metaCommentID try coordinator.displayCommentsWithPostId(range.postID, - siteID: range.siteID, - commentID: commentID, + siteID: range.siteID, + commentID: commentID, source: .commentNotification) case .stats: /// Backup notifications are configured as "stat" notifications @@ -105,7 +107,11 @@ struct NotificationContentRouter { try coordinator.displayStreamWithSiteID(range.siteID) case .scan: try coordinator.displayScanWithSiteID(range.siteID) - + case .readerStream: + guard let streamKey = range.streamKey else { + throw DefaultContentCoordinator.DisplayError.missingParameter + } + try coordinator.displayStreamWithStreamKey(streamKey) default: throw DefaultContentCoordinator.DisplayError.unsupportedType } From 4addcf466928bffa84fb3755fa9fd130b5c73b06 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 17:58:54 +0200 Subject: [PATCH 03/10] Move to `on-this-day` --- .../FormattableContent/FormattableContentRange.swift | 2 +- Modules/Sources/FormattableContentKit/Notifiable.swift | 2 +- .../ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift index ed98a375384a..8a8c09bd95d3 100644 --- a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift +++ b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift @@ -77,7 +77,7 @@ extension FormattableRangeKind { public static let post = FormattableRangeKind("post") public static let comment = FormattableRangeKind("comment") public static let readerStream = FormattableRangeKind("readerstream") - public static let onThisDay = FormattableRangeKind("on_this_day") + public static let onThisDay = FormattableRangeKind("on-this-day") public static let stats = FormattableRangeKind("stat") public static let follow = FormattableRangeKind("follow") public static let blockquote = FormattableRangeKind("blockquote") diff --git a/Modules/Sources/FormattableContentKit/Notifiable.swift b/Modules/Sources/FormattableContentKit/Notifiable.swift index d54617787e5d..78049d0115b1 100644 --- a/Modules/Sources/FormattableContentKit/Notifiable.swift +++ b/Modules/Sources/FormattableContentKit/Notifiable.swift @@ -15,7 +15,7 @@ public enum NotificationKind: String, Sendable { case user = "user" case login = "push_auth" case viewMilestone = "view_milestone" - case onThisDay = "on_this_day" + case onThisDay = "on-this-day" case unknown = "unknown" } diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index 279d89c519b1..8f21a9429b03 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -220,7 +220,7 @@ enum ReaderDiscoverChannel: Hashable { case .firstPosts: "first_posts" case .latest: "latest" case .dailyPrompts: "daily_prompts" - case .onThisDay: "on_this_day" + case .onThisDay: "on-this-day" case .tag: "tag" } } From 8e59588e0b3b2ef27f4c907a8832102bb822154f Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 18:44:05 +0200 Subject: [PATCH 04/10] More streamlined approach --- .../FormattableContentRange.swift | 1 - .../FormattableContentKit/Notifiable.swift | 11 +- .../System/Root View/ReaderPresenter.swift | 100 +++++++++++------- .../Classes/Utility/ContentCoordinator.swift | 9 +- .../Universal Links/Routes+Reader.swift | 11 +- .../Universal Links/UniversalLinkRouter.swift | 5 +- .../Headers/ReaderDiscoverHeaderView.swift | 23 +++- .../Navigation/ReaderNavigationPath.swift | 4 +- .../Sidebar/ReaderSidebarViewModel.swift | 21 ++-- 9 files changed, 121 insertions(+), 64 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift index 8a8c09bd95d3..9a0f11992d97 100644 --- a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift +++ b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContentRange.swift @@ -77,7 +77,6 @@ extension FormattableRangeKind { public static let post = FormattableRangeKind("post") public static let comment = FormattableRangeKind("comment") public static let readerStream = FormattableRangeKind("readerstream") - public static let onThisDay = FormattableRangeKind("on-this-day") public static let stats = FormattableRangeKind("stat") public static let follow = FormattableRangeKind("follow") public static let blockquote = FormattableRangeKind("blockquote") diff --git a/Modules/Sources/FormattableContentKit/Notifiable.swift b/Modules/Sources/FormattableContentKit/Notifiable.swift index 78049d0115b1..acfbb684d797 100644 --- a/Modules/Sources/FormattableContentKit/Notifiable.swift +++ b/Modules/Sources/FormattableContentKit/Notifiable.swift @@ -15,7 +15,6 @@ public enum NotificationKind: String, Sendable { case user = "user" case login = "push_auth" case viewMilestone = "view_milestone" - case onThisDay = "on-this-day" case unknown = "unknown" } @@ -26,14 +25,14 @@ extension NotificationKind { .commentLike, .like, .matcher, - .login, + .login ] /// Enumerates the Kinds of rich notifications that include body text private static let kindsWithoutRichNotificationBodyText: Set = [ .commentLike, .like, - .login, + .login ] private static let kindsWithNotificationIconSupport: Set = [ @@ -49,7 +48,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the kind of rich notification includes a body; `false` otherwise public static func omitsRichNotificationBody(_ kind: NotificationKind) -> Bool { - return kindsWithoutRichNotificationBodyText.contains(kind) + kindsWithoutRichNotificationBodyText.contains(kind) } /// Indicates whether or not a given kind has rich notification support. @@ -57,7 +56,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the kind supports rich notifications; `false` otherwise public static func isSupportedByRichNotifications(_ kind: NotificationKind) -> Bool { - return kindsWithRichNotificationSupport.contains(kind) + kindsWithRichNotificationSupport.contains(kind) } /// Indicates whether or not to download and attach the notification icon @@ -70,7 +69,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the notification kind is `viewMilestone`, `false` otherwise public static func isViewMilestone(_ kind: NotificationKind) -> Bool { - return kind == .viewMilestone + kind == .viewMilestone } /// Returns a client-side notification category. The category provides a match to ensure that the Long Look diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index dcb837c1ebac..d5fdccf11bc8 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -22,7 +22,6 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } private var selectionObserver: AnyCancellable? - private var pendingDiscoverChannel: ReaderDiscoverChannel? public convenience override init() { self.init(viewModel: ReaderSidebarViewModel()) @@ -92,20 +91,23 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { private func configure(for selection: ReaderSidebarItem) { let source = ScreenTrackingSource(ScreenID.Reader.sidebar) - let vc: UIViewController = switch selection { - case .main(let screen): - makeViewController(for: screen) - case .allSubscriptions: - makeAllSubscriptionsViewController(source: source) - case .subscription(let objectID): - makeViewController(withTopicID: objectID) - case .list(let objectID): - makeViewController(withTopicID: objectID) - case .tag(let objectID): - makeViewController(withTopicID: objectID) - case .organization(let objectID): - makeViewController(withTopicID: objectID) - } + let vc: UIViewController = + switch selection { + case .main(let screen): + makeViewController(for: screen) + case .discover(let channel): + makeDiscoverViewController(channel: channel) + case .allSubscriptions: + makeAllSubscriptionsViewController(source: source) + case .subscription(let objectID): + makeViewController(withTopicID: objectID) + case .list(let objectID): + makeViewController(withTopicID: objectID) + case .tag(let objectID): + makeViewController(withTopicID: objectID) + case .organization(let objectID): + makeViewController(withTopicID: objectID) + } vc.trackingContext.source = source @@ -127,7 +129,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } } - private func makeViewController(withTopicID objectID: TaggedManagedObjectID) -> UIViewController { + private func makeViewController( + withTopicID objectID: TaggedManagedObjectID + ) -> UIViewController { do { let topic = try viewContext.existingObject(with: objectID) return ReaderStreamViewController.controllerWithTopic(topic) @@ -139,18 +143,14 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { private func makeViewController(for screen: ReaderStaticScreen) -> UIViewController { switch screen { - case .recent, .discover, .likes: + case .recent, .likes: if let topic = screen.topicType.flatMap(sidebarViewModel.getTopic) { - if screen == .discover { - let initialChannel = pendingDiscoverChannel ?? .freshlyPressed - pendingDiscoverChannel = nil - return ReaderDiscoverViewController(topic: topic, initialChannel: initialChannel) - } else { - return ReaderStreamViewController.controllerWithTopic(topic) - } + return ReaderStreamViewController.controllerWithTopic(topic) } else { return makeErrorViewController() // This should never happen } + case .discover: + return makeDiscoverViewController(channel: .freshlyPressed) case .saved: return ReaderStreamViewController.controllerForContentType(.saved) case .search: @@ -164,15 +164,27 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } } + private func makeDiscoverViewController(channel: ReaderDiscoverChannel) -> UIViewController { + guard let topic = sidebarViewModel.getTopic(for: .discover) else { + return makeErrorViewController() // This should never happen + } + return ReaderDiscoverViewController(topic: topic, initialChannel: channel) + } + private func makeAllSubscriptionsViewController(source: ScreenTrackingSource? = nil) -> UIViewController { let view = ReaderSubscriptionsView { [weak self] selection in let streamVC = ReaderStreamViewController.controllerWithTopic(selection) - streamVC.trackingContext.source = ScreenTrackingSource(ScreenID.Reader.subscriptions, component: ElementID.Reader.subscriptionCell) + streamVC.trackingContext.source = ScreenTrackingSource( + ScreenID.Reader.subscriptions, + component: ElementID.Reader.subscriptionCell + ) self?.push(streamVC) } - let hostVC = UIHostingController(rootView: view - .environment(\.managedObjectContext, viewContext) - .environment(\.trackingContext, ScreenTrackingContext(source: source)) + let hostVC = UIHostingController( + rootView: + view + .environment(\.managedObjectContext, viewContext) + .environment(\.trackingContext, ScreenTrackingContext(source: source)) ) hostVC.title = SharedStrings.Reader.subscriptions if sidebarViewModel.isCompact { @@ -194,7 +206,8 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { let view = ReaderListsView() { [weak self] selection in let streamVC = ReaderStreamViewController.controllerWithTopic(selection) self?.push(streamVC) - }.environment(\.managedObjectContext, viewContext) + } + .environment(\.managedObjectContext, viewContext) let hostVC = UIHostingController(rootView: view) hostVC.title = SharedStrings.Reader.lists if sidebarViewModel.isCompact { @@ -204,7 +217,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } private func makeErrorViewController() -> UIViewController { - UIHostingController(rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle")) + UIHostingController( + rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle") + ) } /// Shows the given view controller by either displaying it in the `.secondary` @@ -293,11 +308,15 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .recent: viewModel.selection = .main(.recent) case .discover: - pendingDiscoverChannel = nil - viewModel.selection = .main(.discover) - case .discoverOnThisDay: - pendingDiscoverChannel = .onThisDay viewModel.selection = .main(.discover) + case let .discoverStream(key): + if let channel = ReaderDiscoverChannel(streamKey: key) { + viewModel.selection = .discover(channel: channel) + } else { + // Unknown stream key: fall back to rendering it as a tag stream. + viewModel.selection = nil + show(ReaderStreamViewController.controllerWithTagSlug(key)) + } case .likes: viewModel.selection = .main(.likes) case .search: @@ -305,7 +324,13 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .subscriptions: viewModel.selection = .allSubscriptions case let .post(postID, siteID, isFeed): - push(ReaderDetailViewController.controllerWithPostID(NSNumber(value: postID), siteID: NSNumber(value: siteID), isFeed: isFeed)) + push( + ReaderDetailViewController.controllerWithPostID( + NSNumber(value: postID), + siteID: NSNumber(value: siteID), + isFeed: isFeed + ) + ) case let .postURL(url): push(ReaderDetailViewController.controllerWithPostURL(url)) case let .topic(topic): @@ -334,7 +359,10 @@ private extension UINavigationController { // A workaround for https://a8c.sentry.io/issues/3140539221. func safePushViewController(_ viewController: UIViewController, animated: Bool) { guard !children.contains(viewController) else { - return wpAssertionFailure("pushing the same view controller more than once", userInfo: ["viewController": "\(viewController)"]) + return wpAssertionFailure( + "pushing the same view controller more than once", + userInfo: ["viewController": "\(viewController)"] + ) } pushViewController(viewController, animated: animated) } diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index cd8da242eb83..b38f79e40a4e 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -139,14 +139,7 @@ struct DefaultContentCoordinator: ContentCoordinator { guard let streamKey, !streamKey.isEmpty else { throw DisplayError.missingParameter } - - if streamKey == "on-this-day" { - RootViewCoordinator.sharedPresenter.showReader(path: .discoverOnThisDay) - return - } - - let browseViewController = ReaderStreamViewController.controllerWithTagSlug(streamKey) - controller?.navigationController?.pushViewController(browseViewController, animated: true) + RootViewCoordinator.sharedPresenter.showReader(path: .discoverStream(key: streamKey)) } func displayWebViewWithURL(_ url: URL, source: String) { diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift index 92f8dc8da2a6..fdb6b0c6bf40 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift @@ -15,6 +15,7 @@ enum ReaderRoute { case blog case feedsPost case blogsPost + case stream case wpcomPost } @@ -53,6 +54,8 @@ extension ReaderRoute: Route { return ["/read/feeds/:feed_id/posts/:post_id", "/reader/feeds/:feed_id/posts/:post_id"] case .blogsPost: return ["/read/blogs/:blog_id/posts/:post_id", "/reader/blogs/:blog_id/posts/:post_id"] + case .stream: + return ["/read/streams/:stream_key", "/reader/streams/:stream_key"] case .wpcomPost: return ["/:post_year/:post_month/:post_day/:post_name"] } @@ -118,6 +121,10 @@ extension ReaderRoute: NavigationAction { if let (blogID, postID) = blogAndPostID(from: values) { presenter.showReader(path: .post(postID: postID, siteID: blogID)) } + case .stream: + if let streamKey = values["stream_key"] { + presenter.showReader(path: .discoverStream(key: streamKey)) + } case .wpcomPost: if let urlString = values[MatchedRouteURLComponentKey.url.rawValue], let url = URL(string: urlString) { presenter.showReader(path: .postURL(url)) @@ -130,7 +137,7 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let feedID = Int(feedIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (feedID, postID) @@ -141,7 +148,7 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let blogID = Int(blogIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (blogID, postID) diff --git a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift index 589a4afa2404..d0a9d756bd75 100644 --- a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift @@ -82,6 +82,7 @@ struct UniversalLinkRouter: LinkRouter { ReaderRoute.blog, ReaderRoute.feedsPost, ReaderRoute.blogsPost, + ReaderRoute.stream, ReaderRoute.wpcomPost ] @@ -123,7 +124,7 @@ struct UniversalLinkRouter: LinkRouter { // If there's a hostname, check if it's WordPress.com or jetpack.com/app. return (scheme == "https" || scheme == "http") && (host == "wordpress.com" || host == "jetpack.com" || host.hasSuffix(".wordpress.com") || host.hasSuffix(".jetpack.com")) - && matcherCanHandle + && matcherCanHandle } /// Attempts to find a route that matches the url's path, and perform its @@ -150,7 +151,7 @@ struct UniversalLinkRouter: LinkRouter { } UIApplication.shared.open(url, - options: [:], + options: [:], completionHandler: nil) } diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index 8f21a9429b03..5fb6021318e1 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -87,7 +87,7 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView { private func makeChannelView(_ channel: ReaderDiscoverChannel) -> ReaderDiscoverChannelView { let view = ReaderDiscoverChannelView(channel: channel) view.button.addAction(UIAction { [weak self] _ in - self?.didSelectChannel(channel) + self?.didSelectChannel(channel) }, for: .primaryActionTriggered) return view } @@ -225,6 +225,27 @@ enum ReaderDiscoverChannel: Hashable { } } + /// The stream key a channel responds to when resolving deep links of the + /// form `/read/streams/:stream_key`. Returns `nil` for channels that are + /// not backed by a stream key. + var streamKey: String? { + switch self { + case .onThisDay: "on-this-day" + case .freshlyPressed, .recommended, .firstPosts, .latest, .dailyPrompts, .tag: + nil + } + } + + /// Returns the channel (if any) that responds to the given stream key. + init?(streamKey: String) { + let candidates: [ReaderDiscoverChannel] = [ + .freshlyPressed, .recommended, .firstPosts, .latest, .dailyPrompts, .onThisDay + ] + guard let match = candidates.first(where: { $0.streamKey == streamKey }) else { + return nil + } + self = match + } } #Preview { diff --git a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift index 390d1c98d259..121dcf47d75f 100644 --- a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift +++ b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift @@ -6,7 +6,9 @@ import WordPressKit enum ReaderNavigationPath: Hashable { case recent case discover - case discoverOnThisDay + /// A Reader stream identified by key, e.g. the key used by `/read/streams/:stream_key`. + /// Resolves to a matching Discover channel when available, otherwise falls back to a tag. + case discoverStream(key: String) case likes case search case subscriptions diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift index 50dac586ef76..10d47dae198e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift @@ -18,9 +18,11 @@ final class ReaderSidebarViewModel: ObservableObject { let menu: [ReaderStaticScreen] - init(menuStore: ReaderMenuStoreProtocol = ReaderMenuStore(), - contextManager: CoreDataStackSwift = ContextManager.shared, - isReaderAppModeEnabled: Bool = false) { + init( + menuStore: ReaderMenuStoreProtocol = ReaderMenuStore(), + contextManager: CoreDataStackSwift = ContextManager.shared, + isReaderAppModeEnabled: Bool = false + ) { self.tabItemsStore = menuStore self.contextManager = contextManager @@ -46,9 +48,10 @@ final class ReaderSidebarViewModel: ObservableObject { } func getTopic(for topicType: ReaderTopicType) -> ReaderAbstractTopic? { - return try? ReaderAbstractTopic.lookupAllMenus(in: contextManager.mainContext).first { - ReaderHelpers.topicType($0) == topicType - } + try? ReaderAbstractTopic.lookupAllMenus(in: contextManager.mainContext) + .first { + ReaderHelpers.topicType($0) == topicType + } } func onAppear() { @@ -64,7 +67,8 @@ final class ReaderSidebarViewModel: ObservableObject { private func persistenSelection() { if !isRestoringSelection, case .main(let screen)? = selection, - screen == .recent || screen == .discover { + screen == .recent || screen == .discover + { UserDefaults.standard.readerSidebarSelection = screen } } @@ -73,6 +77,9 @@ final class ReaderSidebarViewModel: ObservableObject { enum ReaderSidebarItem: Identifiable, Hashable { /// One of the main navigation areas. case main(ReaderStaticScreen) + /// The Discover screen opened on a specific channel. Used for deep links + /// like `/read/streams/:stream_key`; does not highlight a sidebar row. + case discover(channel: ReaderDiscoverChannel) case allSubscriptions case subscription(TaggedManagedObjectID) case list(TaggedManagedObjectID) From e43030f471f9abdf288f6dda679d712f5b6d5410 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 18:57:22 +0200 Subject: [PATCH 05/10] Remove excess changes --- .../FormattableContentKit/Notifiable.swift | 8 ++-- .../ReaderPostServiceRemote+Cards.swift | 46 +++++++++---------- .../Activity/MockContentCoordinator.swift | 7 +-- .../System/Root View/ReaderPresenter.swift | 4 +- .../Classes/Utility/ContentCoordinator.swift | 14 +++--- .../Universal Links/Routes+Reader.swift | 4 +- .../Universal Links/UniversalLinkRouter.swift | 6 +-- .../NotificationContentRouter.swift | 18 ++++---- .../Headers/ReaderDiscoverHeaderView.swift | 2 +- .../Sidebar/ReaderSidebarViewModel.swift | 18 +++----- 10 files changed, 57 insertions(+), 70 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/Notifiable.swift b/Modules/Sources/FormattableContentKit/Notifiable.swift index acfbb684d797..495541a1cdcd 100644 --- a/Modules/Sources/FormattableContentKit/Notifiable.swift +++ b/Modules/Sources/FormattableContentKit/Notifiable.swift @@ -25,7 +25,7 @@ extension NotificationKind { .commentLike, .like, .matcher, - .login + .login, ] /// Enumerates the Kinds of rich notifications that include body text @@ -48,7 +48,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the kind of rich notification includes a body; `false` otherwise public static func omitsRichNotificationBody(_ kind: NotificationKind) -> Bool { - kindsWithoutRichNotificationBodyText.contains(kind) + return kindsWithoutRichNotificationBodyText.contains(kind) } /// Indicates whether or not a given kind has rich notification support. @@ -56,7 +56,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the kind supports rich notifications; `false` otherwise public static func isSupportedByRichNotifications(_ kind: NotificationKind) -> Bool { - kindsWithRichNotificationSupport.contains(kind) + return kindsWithRichNotificationSupport.contains(kind) } /// Indicates whether or not to download and attach the notification icon @@ -69,7 +69,7 @@ extension NotificationKind { /// - Parameter kind: the notification type to evaluate /// - Returns: `true` if the notification kind is `viewMilestone`, `false` otherwise public static func isViewMilestone(_ kind: NotificationKind) -> Bool { - kind == .viewMilestone + return kind == .viewMilestone } /// Returns a client-side notification category. The category provides a match to ensure that the Long Look diff --git a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift index 9993e9ab952a..d5295951e6ed 100644 --- a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift +++ b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift @@ -32,16 +32,16 @@ extension ReaderPostServiceRemote { /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid public func fetchCards(for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { let path = "read/tags/cards" guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, + topics: topics, + page: page, + sortingOption: sortingOption, refreshCount: refreshCount) else { return } @@ -62,19 +62,19 @@ extension ReaderPostServiceRemote { /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid public func fetchStreamCards(stream: ReaderStream = .discover, - for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - count: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, + for topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + count: Int? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { let path = "read/streams/\(stream.rawValue)" guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, - count: count, + topics: topics, + page: page, + sortingOption: sortingOption, + count: count, refreshCount: refreshCount) else { return } @@ -82,7 +82,7 @@ extension ReaderPostServiceRemote { } private func fetch(_ endpoint: String, - success: @escaping ([RemoteReaderCard], String?) -> Void, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { Task { @MainActor [wordPressComRestApi] in await wordPressComRestApi.perform(.get, URLString: endpoint, type: ReaderCardEnvelope.self) @@ -93,10 +93,10 @@ extension ReaderPostServiceRemote { } private func cardsEndpoint(with path: String, - topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - count: Int? = nil, + topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + count: Int? = nil, refreshCount: Int? = nil) -> String? { var path = URLComponents(string: path) @@ -124,4 +124,4 @@ extension ReaderPostServiceRemote { return self.path(forEndpoint: endpoint, withVersion: ._2_0) } -} +} \ No newline at end of file diff --git a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift index c0cb4cc2d0be..386b7413f209 100644 --- a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift +++ b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift @@ -14,12 +14,7 @@ class MockContentCoordinator: ContentCoordinator { var commentsWasDisplayed = false var commentPostID: NSNumber? var commentSiteID: NSNumber? - func displayCommentsWithPostId( - _ postID: NSNumber?, - siteID: NSNumber?, - commentID: NSNumber?, - source: ReaderCommentsSource - ) throws { + func displayCommentsWithPostId(_ postID: NSNumber?, siteID: NSNumber?, commentID: NSNumber?, source: ReaderCommentsSource) throws { commentsWasDisplayed = true commentPostID = postID commentSiteID = siteID diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index d5fdccf11bc8..df2ec7d50529 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -129,9 +129,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } } - private func makeViewController( - withTopicID objectID: TaggedManagedObjectID - ) -> UIViewController { + private func makeViewController(withTopicID objectID: TaggedManagedObjectID) -> UIViewController { do { let topic = try viewContext.existingObject(with: objectID) return ReaderStreamViewController.controllerWithTopic(topic) diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index b38f79e40a4e..c8c28761d6a1 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -55,8 +55,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayStatsWithSiteID(_ siteID: NSNumber?, url: URL? = nil) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.supports(.stats) + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.supports(.stats) else { throw DisplayError.missingParameter } @@ -78,7 +78,7 @@ struct DefaultContentCoordinator: ContentCoordinator { let matcher = RouteMatcher(routes: UniversalLinkRouter.statsRoutes) let matches = matcher.routesMatching(url) if let match = matches.first, - let action = match.action as? StatsRoute, + let action = match.action as? StatsRoute, let tab = action.tab { SiteStatsDashboardPreferences.setSelected(tabType: tab, siteID: siteID) } @@ -86,7 +86,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayBackupWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } @@ -100,8 +100,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayScanWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.isScanAllowed() + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.isScanAllowed() else { throw DisplayError.missingParameter } @@ -112,7 +112,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayFollowersWithSiteID(_ siteID: NSNumber?, expirationTime: TimeInterval) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift index fdb6b0c6bf40..a17b393f04d7 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift @@ -137,7 +137,7 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let feedID = Int(feedIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (feedID, postID) @@ -148,7 +148,7 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let blogID = Int(blogIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (blogID, postID) diff --git a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift index d0a9d756bd75..f338f4d5ee2b 100644 --- a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift @@ -124,7 +124,7 @@ struct UniversalLinkRouter: LinkRouter { // If there's a hostname, check if it's WordPress.com or jetpack.com/app. return (scheme == "https" || scheme == "http") && (host == "wordpress.com" || host == "jetpack.com" || host.hasSuffix(".wordpress.com") || host.hasSuffix(".jetpack.com")) - && matcherCanHandle + && matcherCanHandle } /// Attempts to find a route that matches the url's path, and perform its @@ -150,8 +150,8 @@ struct UniversalLinkRouter: LinkRouter { return } - UIApplication.shared.open(url, - options: [:], + UIApplication.shared.open(url, + options: [:], completionHandler: nil) } diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift index 4b139c9bf9b1..62cc3f8a33e8 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift @@ -57,18 +57,16 @@ struct NotificationContentRouter { case .comment: // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID - try coordinator.displayCommentsWithPostId( - notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, - source: .commentNotification - ) + try coordinator.displayCommentsWithPostId(notification.metaPostID, + siteID: notification.metaSiteID, + commentID: commentID, + source: .commentNotification) case .commentLike: // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID try coordinator.displayCommentsWithPostId(notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, + siteID: notification.metaSiteID, + commentID: commentID, source: .commentLikeNotification) default: throw DefaultContentCoordinator.DisplayError.unsupportedType @@ -89,8 +87,8 @@ struct NotificationContentRouter { // Focus on the comment reply if it's set over the primary comment ID let commentID = notification.metaReplyID ?? notification.metaCommentID try coordinator.displayCommentsWithPostId(range.postID, - siteID: range.siteID, - commentID: commentID, + siteID: range.siteID, + commentID: commentID, source: .commentNotification) case .stats: /// Backup notifications are configured as "stat" notifications diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index 5fb6021318e1..5c03462fe23e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -87,7 +87,7 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView { private func makeChannelView(_ channel: ReaderDiscoverChannel) -> ReaderDiscoverChannelView { let view = ReaderDiscoverChannelView(channel: channel) view.button.addAction(UIAction { [weak self] _ in - self?.didSelectChannel(channel) + self?.didSelectChannel(channel) }, for: .primaryActionTriggered) return view } diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift index 10d47dae198e..2ea07e7e84af 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewModel.swift @@ -18,11 +18,9 @@ final class ReaderSidebarViewModel: ObservableObject { let menu: [ReaderStaticScreen] - init( - menuStore: ReaderMenuStoreProtocol = ReaderMenuStore(), - contextManager: CoreDataStackSwift = ContextManager.shared, - isReaderAppModeEnabled: Bool = false - ) { + init(menuStore: ReaderMenuStoreProtocol = ReaderMenuStore(), + contextManager: CoreDataStackSwift = ContextManager.shared, + isReaderAppModeEnabled: Bool = false) { self.tabItemsStore = menuStore self.contextManager = contextManager @@ -48,10 +46,9 @@ final class ReaderSidebarViewModel: ObservableObject { } func getTopic(for topicType: ReaderTopicType) -> ReaderAbstractTopic? { - try? ReaderAbstractTopic.lookupAllMenus(in: contextManager.mainContext) - .first { - ReaderHelpers.topicType($0) == topicType - } + return try? ReaderAbstractTopic.lookupAllMenus(in: contextManager.mainContext).first { + ReaderHelpers.topicType($0) == topicType + } } func onAppear() { @@ -67,8 +64,7 @@ final class ReaderSidebarViewModel: ObservableObject { private func persistenSelection() { if !isRestoringSelection, case .main(let screen)? = selection, - screen == .recent || screen == .discover - { + screen == .recent || screen == .discover { UserDefaults.standard.readerSidebarSelection = screen } } From 627ed2bdd0bffca416138ec4c1afa3ba8658e222 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 19:12:36 +0200 Subject: [PATCH 06/10] Remove unneeded changes --- .../FormattableContentKit/Notifiable.swift | 2 +- .../ReaderPostServiceRemote+Cards.swift | 2 +- .../notifications-readerstream-range.json | 3 +- ...NotificationContentRangeFactoryTests.swift | 2 +- .../System/Root View/ReaderPresenter.swift | 28 +++++-------------- .../Universal Links/UniversalLinkRouter.swift | 2 +- .../Headers/ReaderDiscoverHeaderView.swift | 1 + 7 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/Notifiable.swift b/Modules/Sources/FormattableContentKit/Notifiable.swift index 495541a1cdcd..68653fcb7eec 100644 --- a/Modules/Sources/FormattableContentKit/Notifiable.swift +++ b/Modules/Sources/FormattableContentKit/Notifiable.swift @@ -32,7 +32,7 @@ extension NotificationKind { private static let kindsWithoutRichNotificationBodyText: Set = [ .commentLike, .like, - .login + .login, ] private static let kindsWithNotificationIconSupport: Set = [ diff --git a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift index d5295951e6ed..af76c58179da 100644 --- a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift +++ b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift @@ -124,4 +124,4 @@ extension ReaderPostServiceRemote { return self.path(forEndpoint: endpoint, withVersion: ._2_0) } -} \ No newline at end of file +} diff --git a/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json b/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json index 760f85871b66..490973296652 100644 --- a/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json +++ b/Tests/KeystoneTests/Resources/Mocks/Notifications/notifications-readerstream-range.json @@ -1,6 +1,7 @@ { - "url" : "https://wordpress.com/read/tags/mental-health", + "url" : "https://wordpress.com/reader/on-this-day", "type" : "readerstream", + "id" : "on-this-day", "indices" : [ 0, 12 diff --git a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift index 4cfe07035b52..bad67a14c105 100644 --- a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationContentRangeFactoryTests.swift @@ -72,7 +72,7 @@ final class NotificationContentRangeFactoryTests: XCTestCase { } private func mockReaderStreamRange() throws -> JSONObject { - try JSONObject(fromFileNamed: "notifications-readerstream-range.json") + return try JSONObject(fromFileNamed: "notifications-readerstream-range.json") } } diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index df2ec7d50529..6bafdf5321a9 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -178,11 +178,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { ) self?.push(streamVC) } - let hostVC = UIHostingController( - rootView: - view - .environment(\.managedObjectContext, viewContext) - .environment(\.trackingContext, ScreenTrackingContext(source: source)) + let hostVC = UIHostingController(rootView: view + .environment(\.managedObjectContext, viewContext) + .environment(\.trackingContext, ScreenTrackingContext(source: source)) ) hostVC.title = SharedStrings.Reader.subscriptions if sidebarViewModel.isCompact { @@ -204,8 +202,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { let view = ReaderListsView() { [weak self] selection in let streamVC = ReaderStreamViewController.controllerWithTopic(selection) self?.push(streamVC) - } - .environment(\.managedObjectContext, viewContext) + }.environment(\.managedObjectContext, viewContext) let hostVC = UIHostingController(rootView: view) hostVC.title = SharedStrings.Reader.lists if sidebarViewModel.isCompact { @@ -215,9 +212,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } private func makeErrorViewController() -> UIViewController { - UIHostingController( - rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle") - ) + UIHostingController(rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle")) } /// Shows the given view controller by either displaying it in the `.secondary` @@ -322,13 +317,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .subscriptions: viewModel.selection = .allSubscriptions case let .post(postID, siteID, isFeed): - push( - ReaderDetailViewController.controllerWithPostID( - NSNumber(value: postID), - siteID: NSNumber(value: siteID), - isFeed: isFeed - ) - ) + push(ReaderDetailViewController.controllerWithPostID(NSNumber(value: postID), siteID: NSNumber(value: siteID), isFeed: isFeed)) case let .postURL(url): push(ReaderDetailViewController.controllerWithPostURL(url)) case let .topic(topic): @@ -357,10 +346,7 @@ private extension UINavigationController { // A workaround for https://a8c.sentry.io/issues/3140539221. func safePushViewController(_ viewController: UIViewController, animated: Bool) { guard !children.contains(viewController) else { - return wpAssertionFailure( - "pushing the same view controller more than once", - userInfo: ["viewController": "\(viewController)"] - ) + return wpAssertionFailure("pushing the same view controller more than once", userInfo: ["viewController": "\(viewController)"]) } pushViewController(viewController, animated: animated) } diff --git a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift index f338f4d5ee2b..6967052d20f5 100644 --- a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift @@ -150,7 +150,7 @@ struct UniversalLinkRouter: LinkRouter { return } - UIApplication.shared.open(url, + UIApplication.shared.open(url, options: [:], completionHandler: nil) } diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index 5c03462fe23e..c1ed86c9f402 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -85,6 +85,7 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView { // MARK: Channels private func makeChannelView(_ channel: ReaderDiscoverChannel) -> ReaderDiscoverChannelView { + let view = ReaderDiscoverChannelView { let view = ReaderDiscoverChannelView(channel: channel) view.button.addAction(UIAction { [weak self] _ in self?.didSelectChannel(channel) From 1104e0bd6c09d1b722c04e2b246e22c8cfc36e52 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 19:15:36 +0200 Subject: [PATCH 07/10] Remove formatting changes --- WordPress/Classes/System/Root View/ReaderPresenter.swift | 2 +- .../ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index 6bafdf5321a9..ea15c0b3c5e6 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -202,7 +202,7 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { let view = ReaderListsView() { [weak self] selection in let streamVC = ReaderStreamViewController.controllerWithTopic(selection) self?.push(streamVC) - }.environment(\.managedObjectContext, viewContext) + }.environment(\.managedObjectContext, viewContext) let hostVC = UIHostingController(rootView: view) hostVC.title = SharedStrings.Reader.lists if sidebarViewModel.isCompact { diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index c1ed86c9f402..cbdcbf4ceb02 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -85,10 +85,9 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView { // MARK: Channels private func makeChannelView(_ channel: ReaderDiscoverChannel) -> ReaderDiscoverChannelView { - let view = ReaderDiscoverChannelView { let view = ReaderDiscoverChannelView(channel: channel) view.button.addAction(UIAction { [weak self] _ in - self?.didSelectChannel(channel) + self?.didSelectChannel(channel) }, for: .primaryActionTriggered) return view } From d675d406f143fd5c6dca29020a444cfda8bdc744 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Thu, 23 Apr 2026 22:23:24 +0200 Subject: [PATCH 08/10] Improve empty state --- .../ReaderDiscoverViewController.swift | 5 ++++- .../ReaderStreamViewController+Helper.swift | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift index 6664e1053a08..c9e0b526381c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift @@ -212,7 +212,9 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe } } -private class ReaderDiscoverStreamViewController: ReaderStreamViewController { +private class ReaderDiscoverStreamViewController: ReaderStreamViewController, ReaderStreamProviding { + let readerStream: ReaderStream? + private let readerCardTopicsIdentifier = "ReaderTopicsCell" private let readerCardSitesIdentifier = "ReaderSitesCell" @@ -235,6 +237,7 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController { init(topic: ReaderAbstractTopic, stream: ReaderStream = .discover, sorting: ReaderSortingOption = .noSorting) { self.cardsService = ReaderCardService(stream: stream, sorting: sorting) + self.readerStream = stream super.init(nibName: nil, bundle: nil) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController+Helper.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController+Helper.swift index 40c90db83388..c8db59cae060 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController+Helper.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController+Helper.swift @@ -1,8 +1,15 @@ import Foundation import SwiftUI import WordPressData +import WordPressKit import WordPressUI +/// Implemented by stream view controllers that can report which `ReaderStream` they are showing, +/// so the base class can tailor things like empty-state messaging. +protocol ReaderStreamProviding: AnyObject { + var readerStream: ReaderStream? { get } +} + // MARK: - ReaderHeader extension ReaderStreamViewController { @@ -44,7 +51,8 @@ extension ReaderStreamViewController { extension ReaderStreamViewController { func makeEmptyStateView(for topic: ReaderAbstractTopic) -> UIView { - let response = ReaderStreamViewController.responseForNoResults(topic) + let stream = (self as? ReaderStreamProviding)?.readerStream + let response = ReaderStreamViewController.responseForNoResults(topic, stream: stream) return UIHostingView(view: EmptyStateView(response.title, scaledImage: response.scaledImageName, description: response.message)) } @@ -54,7 +62,13 @@ extension ReaderStreamViewController { var scaledImageName = "wpl-glasses" } - private class func responseForNoResults(_ topic: ReaderAbstractTopic) -> NoResultsResponse { + private class func responseForNoResults(_ topic: ReaderAbstractTopic, stream: ReaderStream? = nil) -> NoResultsResponse { + if stream == .onThisDay { + return NoResultsResponse( + title: NSLocalizedString("reader.no.results.onThisDay.title", value: "No posts from this day", comment: "Empty-state title shown in the Reader's On This Day stream when the user has no posts published on this date in previous years."), + message: NSLocalizedString("reader.no.results.onThisDay.message", value: "You haven’t published any posts on this day in previous years. Check back as your blog grows.", comment: "Empty-state message shown in the Reader's On This Day stream when the user has no posts published on this date in previous years.") + ) + } if ReaderHelpers.topicIsFollowing(topic) { return NoResultsResponse( title: NSLocalizedString("Welcome to the Reader", comment: "A message title"), From fb272d524a71fca3f3f790f9859993e4a2107f04 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Mon, 27 Apr 2026 21:32:06 +0200 Subject: [PATCH 09/10] Add query params support --- .../Ranges/NotificationContentRange.swift | 3 + .../NotificationContentRangeFactory.swift | 16 ++- .../ReaderPostServiceRemote+Cards.swift | 84 ++++++++----- .../Activity/MockContentCoordinator.swift | 4 +- .../Services/ReaderCardServiceTests.swift | 41 ++++--- .../Classes/Services/ReaderCardService.swift | 110 ++++++++++-------- .../System/Root View/ReaderPresenter.swift | 46 ++++++-- .../Classes/Utility/ContentCoordinator.swift | 22 ++-- .../Universal Links/Routes+Reader.swift | 34 ++++-- .../NotificationContentRouter.swift | 15 +-- .../ReaderDiscoverViewController.swift | 106 +++++++++++------ .../Navigation/ReaderNavigationPath.swift | 2 +- WordPress/WordPress.xcodeproj/project.pbxproj | 16 ++- 13 files changed, 323 insertions(+), 176 deletions(-) diff --git a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift index f468bf4c5028..6dc710e0141c 100644 --- a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift +++ b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRange.swift @@ -8,6 +8,7 @@ public class NotificationContentRange: FormattableContentRange, LinkContentRange public let siteID: NSNumber? public let postID: NSNumber? public let streamKey: String? + public let streamQueryParameters: [String: String] public let url: URL? public init(kind: FormattableRangeKind, properties: Properties) { @@ -18,6 +19,7 @@ public class NotificationContentRange: FormattableContentRange, LinkContentRange userID = properties.userID postID = properties.postID streamKey = properties.streamKey + streamQueryParameters = properties.streamQueryParameters } } @@ -29,6 +31,7 @@ extension NotificationContentRange { public var userID: NSNumber? public var postID: NSNumber? public var streamKey: String? + public var streamQueryParameters: [String: String] = [:] public init(range: NSRange) { self.range = range diff --git a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift index cafe8540ccf4..715ae5c002c3 100644 --- a/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift +++ b/Modules/Sources/FormattableContentKit/Ranges/NotificationContentRangeFactory.swift @@ -14,15 +14,25 @@ struct NotificationContentRangeFactory: FormattableRangesFactory { return contentRangeWithoutKindSpecified(with: properties, from: dictionary) } - private static func propertiesFrom(_ dictionary: [String: AnyObject], with range: NSRange) -> NotificationContentRange.Properties { + private static func propertiesFrom( + _ dictionary: [String: AnyObject], + with range: NSRange + ) -> NotificationContentRange.Properties { var properties = NotificationContentRange.Properties(range: range) properties.siteID = dictionary[RangeKeys.siteId] as? NSNumber properties.postID = dictionary[RangeKeys.postId] as? NSNumber properties.streamKey = dictionary[RangeKeys.id] as? String - if let url = dictionary[RangeKeys.url] as? String { - properties.url = URL(string: url) + if let urlString = dictionary[RangeKeys.url] as? String, let url = URL(string: urlString) { + properties.url = url + if let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems { + for item in items { + if let value = item.value, !value.isEmpty { + properties.streamQueryParameters[item.name] = value + } + } + } } return properties } diff --git a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift index af76c58179da..83f30630807c 100644 --- a/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift +++ b/Modules/Sources/WordPressKit/ReaderPostServiceRemote+Cards.swift @@ -31,18 +31,24 @@ extension ReaderPostServiceRemote { /// - Parameter sortingOption: a ReaderSortingOption that represents a sorting option /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid - public func fetchCards(for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, - failure: @escaping (Error) -> Void) { + public func fetchCards( + for topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, + failure: @escaping (Error) -> Void + ) { let path = "read/tags/cards" - guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, - refreshCount: refreshCount) else { + guard + let requestUrl = cardsEndpoint( + with: path, + topics: topics, + page: page, + sortingOption: sortingOption, + refreshCount: refreshCount + ) + else { return } fetch(requestUrl, success: success, failure: failure) @@ -61,28 +67,36 @@ extension ReaderPostServiceRemote { /// - Parameter count: the number of cards to fetch. Warning: This also changes the number of objects returned for recommended sites/tags. /// - Parameter success: Called when the request succeeds and the data returned is valid /// - Parameter failure: Called if the request fails for any reason, or the response data is invalid - public func fetchStreamCards(stream: ReaderStream = .discover, - for topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - refreshCount: Int? = nil, - count: Int? = nil, - success: @escaping ([RemoteReaderCard], String?) -> Void, - failure: @escaping (Error) -> Void) { + public func fetchStreamCards( + stream: ReaderStream = .discover, + for topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + refreshCount: Int? = nil, + count: Int? = nil, + forwardedQueryParameters: [String: String]? = nil, + success: @escaping ([RemoteReaderCard], String?) -> Void, + failure: @escaping (Error) -> Void + ) { let path = "read/streams/\(stream.rawValue)" - guard let requestUrl = cardsEndpoint(with: path, - topics: topics, - page: page, - sortingOption: sortingOption, - count: count, - refreshCount: refreshCount) else { + guard + let requestUrl = cardsEndpoint( + with: path, + topics: topics, + page: page, + sortingOption: sortingOption, + count: count, + refreshCount: refreshCount, + forwardedQueryParameters: forwardedQueryParameters + ) + else { return } fetch(requestUrl, success: success, failure: failure) } private func fetch(_ endpoint: String, - success: @escaping ([RemoteReaderCard], String?) -> Void, + success: @escaping ([RemoteReaderCard], String?) -> Void, failure: @escaping (Error) -> Void) { Task { @MainActor [wordPressComRestApi] in await wordPressComRestApi.perform(.get, URLString: endpoint, type: ReaderCardEnvelope.self) @@ -93,11 +107,13 @@ extension ReaderPostServiceRemote { } private func cardsEndpoint(with path: String, - topics: [String], - page: String? = nil, - sortingOption: ReaderSortingOption = .noSorting, - count: Int? = nil, - refreshCount: Int? = nil) -> String? { + topics: [String], + page: String? = nil, + sortingOption: ReaderSortingOption = .noSorting, + count: Int? = nil, + refreshCount: Int? = nil, + forwardedQueryParameters: [String: String]? = nil + ) -> String? { var path = URLComponents(string: path) path?.queryItems = topics.map { URLQueryItem(name: "tags[]", value: $0) } @@ -118,6 +134,12 @@ extension ReaderPostServiceRemote { path?.queryItems?.append(URLQueryItem(name: "refresh", value: String(refreshCount))) } + if let forwardedQueryParameters { + for (name, value) in forwardedQueryParameters { + path?.queryItems?.append(URLQueryItem(name: name, value: value)) + } + } + guard let endpoint = path?.string else { return nil } diff --git a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift index 386b7413f209..cecdc743c341 100644 --- a/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift +++ b/Tests/KeystoneTests/Tests/Features/Activity/MockContentCoordinator.swift @@ -45,9 +45,11 @@ class MockContentCoordinator: ContentCoordinator { var streamWasDisplayedByStreamKey = false var streamKey: String? - func displayStreamWithStreamKey(_ streamKey: String?) throws { + var streamQueryParameters: [String: String]? + func displayStreamWithStreamKey(_ streamKey: String?, queryParameters: [String: String]?) throws { streamWasDisplayedByStreamKey = true self.streamKey = streamKey + streamQueryParameters = queryParameters } func displayWebViewWithURL(_ url: URL, source: String) { diff --git a/Tests/KeystoneTests/Tests/Services/ReaderCardServiceTests.swift b/Tests/KeystoneTests/Tests/Services/ReaderCardServiceTests.swift index 53d8ff002f5a..1fdb2145844f 100644 --- a/Tests/KeystoneTests/Tests/Services/ReaderCardServiceTests.swift +++ b/Tests/KeystoneTests/Tests/Services/ReaderCardServiceTests.swift @@ -25,8 +25,8 @@ class ReaderCardServiceTests: CoreDataTestCase { service.fetch(isFirstPage: true, success: { _, _ in let cards = try? self.mainContext.fetch(NSFetchRequest(entityName: ReaderCard.classNameWithoutNamespaces())) - XCTAssertEqual(cards?.count, 9) - expectation.fulfill() + XCTAssertEqual(cards?.count, 9) + expectation.fulfill() }, failure: { _ in }) waitForExpectations(timeout: 5, handler: nil) @@ -41,8 +41,8 @@ class ReaderCardServiceTests: CoreDataTestCase { service.fetch(isFirstPage: true, success: { _, _ in let cards = try? self.mainContext.fetch(NSFetchRequest(entityName: ReaderCard.classNameWithoutNamespaces())) as? [ReaderCard] - XCTAssertEqual(cards?.filter { $0.post != nil }.count, 8) - expectation.fulfill() + XCTAssertEqual(cards?.filter { $0.post != nil }.count, 8) + expectation.fulfill() }, failure: { _ in }) waitForExpectations(timeout: 5, handler: nil) @@ -56,8 +56,8 @@ class ReaderCardServiceTests: CoreDataTestCase { remoteService.shouldCallFailure = true service.fetch(isFirstPage: true, success: { _, _ in }, failure: { error in - XCTAssertNotNil(error) - expectation.fulfill() + XCTAssertNotNil(error) + expectation.fulfill() }) waitForExpectations(timeout: 5, handler: nil) @@ -70,11 +70,11 @@ class ReaderCardServiceTests: CoreDataTestCase { let service = ReaderCardService(service: remoteService, coreDataStack: contextManager, followedInterestsService: followedInterestsService) service.fetch(isFirstPage: false, success: { _, _ in - // Fetch again, this time the 1st page + // Fetch again, this time the 1st page service.fetch(isFirstPage: true, success: { _, _ in let cards = try? self.mainContext.fetch(NSFetchRequest(entityName: ReaderCard.classNameWithoutNamespaces())) as? [ReaderCard] - XCTAssertEqual(cards?.count, 9) - expectation.fulfill() + XCTAssertEqual(cards?.count, 9) + expectation.fulfill() }, failure: { _ in }) }, failure: {_ in }) @@ -89,14 +89,17 @@ class ReaderCardServiceTests: CoreDataTestCase { final class ReaderPostServiceRemoteMock: ReaderCardServiceRemote { var shouldCallFailure = false - func fetchStreamCards(stream: WordPressKit.ReaderStream, - for topics: [String], - page: String?, - sortingOption: WordPressKit.ReaderSortingOption, - refreshCount: Int?, - count: Int?, - success: @escaping ([WordPressKit.RemoteReaderCard], String?) -> Void, - failure: @escaping (any Error) -> Void) { + func fetchStreamCards( + stream: WordPressKit.ReaderStream, + for topics: [String], + page: String?, + sortingOption: WordPressKit.ReaderSortingOption, + refreshCount: Int?, + count: Int?, + forwardedQueryParameters: [String: String]?, + success: @escaping ([WordPressKit.RemoteReaderCard], String?) -> Void, + failure: @escaping (any Error) -> Void + ) { mockFetch(success: success, failure: failure) } @@ -108,7 +111,7 @@ final class ReaderPostServiceRemoteMock: ReaderCardServiceRemote { } guard let fileUrl = Bundle(for: ReaderPostServiceRemoteMock.self).url(forResource: "reader-cards.json", withExtension: nil), - let data = try? Data(contentsOf: fileUrl), + let data = try? Data(contentsOf: fileUrl), let cards = try? JSONDecoder().decode([RemoteReaderCard].self, from: data) else { XCTFail("Error setting up mock data") return @@ -166,6 +169,6 @@ private class ReaderFollowedInterestsServiceMock: ReaderFollowedInterestsService } func path(slug: String) -> String { - return "" + "" } } diff --git a/WordPress/Classes/Services/ReaderCardService.swift b/WordPress/Classes/Services/ReaderCardService.swift index 4e66545a35df..0ac9feeef9c9 100644 --- a/WordPress/Classes/Services/ReaderCardService.swift +++ b/WordPress/Classes/Services/ReaderCardService.swift @@ -4,18 +4,21 @@ import WordPressKit import WordPressShared protocol ReaderCardServiceRemote { - func fetchStreamCards(stream: ReaderStream, - for topics: [String], - page: String?, - sortingOption: ReaderSortingOption, - refreshCount: Int?, - count: Int?, - success: @escaping ([RemoteReaderCard], String?) -> Void, - failure: @escaping (Error) -> Void) + func fetchStreamCards( + stream: ReaderStream, + for topics: [String], + page: String?, + sortingOption: ReaderSortingOption, + refreshCount: Int?, + count: Int?, + forwardedQueryParameters: [String: String]?, + success: @escaping ([RemoteReaderCard], String?) -> Void, + failure: @escaping (Error) -> Void + ) } -extension ReaderPostServiceRemote: ReaderCardServiceRemote { } +extension ReaderPostServiceRemote: ReaderCardServiceRemote {} class ReaderCardService { private let stream: ReaderStream @@ -33,14 +36,20 @@ class ReaderCardService { /// Used only internally to order the cards private var pageNumber = 1 - init(stream: ReaderStream = .discover, - sorting: ReaderSortingOption = .noSorting, - service: ReaderCardServiceRemote = ReaderPostServiceRemote.withDefaultApi(), - coreDataStack: CoreDataStack = ContextManager.shared, - followedInterestsService: ReaderFollowedInterestsService? = nil, - siteInfoService: ReaderSiteInfoService? = nil) { + private let streamQueryParameters: [String: String]? + + init( + stream: ReaderStream = .discover, + sorting: ReaderSortingOption = .noSorting, + streamQueryParameters: [String: String]? = nil, + service: ReaderCardServiceRemote = ReaderPostServiceRemote.withDefaultApi(), + coreDataStack: CoreDataStack = ContextManager.shared, + followedInterestsService: ReaderFollowedInterestsService? = nil, + siteInfoService: ReaderSiteInfoService? = nil + ) { self.stream = stream self.sorting = sorting + self.streamQueryParameters = streamQueryParameters self.service = service self.coreDataStack = coreDataStack self.followedInterestsService = followedInterestsService ?? ReaderTopicService(coreDataStack: coreDataStack) @@ -73,44 +82,44 @@ class ReaderCardService { self.pageHandle = pageHandle self.coreDataStack.performAndSave({ context in - if isFirstPage { - self.pageNumber = 1 - ReaderCardService.removeAllCards(in: context) - } else { - self.pageNumber += 1 - } + if isFirstPage { + self.pageNumber = 1 + ReaderCardService.removeAllCards(in: context) + } else { + self.pageNumber += 1 + } updatedCards.enumerated().forEach { index, remoteCard in - let card = ReaderCard.createOrReuse(context: context, from: remoteCard) - - // Assign each interest an endpoint - card? - .topics? - .array - .compactMap { $0 as? ReaderTagTopic } - .forEach { $0.path = self.followedInterestsService.path(slug: $0.slug) } - - // Assign each site an endpoint URL if needed - card? - .sites? - .array - .compactMap { $0 as? ReaderSiteTopic } - .forEach { - let path = $0.path - // Sites coming from the cards API only have a path and not a full url - // Once we save the model locally it will be a full URL, so we don't - // want to reapply this logic - if !path.hasPrefix("http") { - $0.path = self.siteInfoService.endpointURLString(path: path) - } + let card = ReaderCard.createOrReuse(context: context, from: remoteCard) + + // Assign each interest an endpoint + card? + .topics? + .array + .compactMap { $0 as? ReaderTagTopic } + .forEach { $0.path = self.followedInterestsService.path(slug: $0.slug) } + + // Assign each site an endpoint URL if needed + card? + .sites? + .array + .compactMap { $0 as? ReaderSiteTopic } + .forEach { + let path = $0.path + // Sites coming from the cards API only have a path and not a full url + // Once we save the model locally it will be a full URL, so we don't + // want to reapply this logic + if !path.hasPrefix("http") { + $0.path = self.siteInfoService.endpointURLString(path: path) + } + } + + // To keep the API order + card?.sortRank = Double((self.pageNumber * Constants.paginationMultiplier) + index) } - - // To keep the API order - card?.sortRank = Double((self.pageNumber * Constants.paginationMultiplier) + index) - } }, completion: { - let hasMore = pageHandle != nil - success(cards.count, hasMore) + let hasMore = pageHandle != nil + success(cards.count, hasMore) }, on: .main) } let failure: (Error?) -> Void = { error in @@ -124,6 +133,7 @@ class ReaderCardService { sortingOption: self.sorting, refreshCount: refreshCount, count: nil, + forwardedQueryParameters: self.streamQueryParameters, success: success, failure: failure ) @@ -174,7 +184,7 @@ extension ReaderPostServiceRemote { let token: String? = defaultAccount?.authToken let api = WordPressComRestApi.defaultApi(oAuthToken: token, - userAgent: WPUserAgent.wordPress(), + userAgent: WPUserAgent.wordPress(), localeKey: WordPressComRestApi.LocaleKeyV2) return ReaderPostServiceRemote(wordPressComRestApi: api) } diff --git a/WordPress/Classes/System/Root View/ReaderPresenter.swift b/WordPress/Classes/System/Root View/ReaderPresenter.swift index ea15c0b3c5e6..3969942dd248 100644 --- a/WordPress/Classes/System/Root View/ReaderPresenter.swift +++ b/WordPress/Classes/System/Root View/ReaderPresenter.swift @@ -9,6 +9,8 @@ import WordPressUI public final class ReaderPresenter: NSObject, SplitViewDisplayable { private let sidebarViewModel: ReaderSidebarViewModel + private var discoverStreamQueryParameters: [String: String]? + // The view controllers used during split view presentation. let sidebar: ReaderSidebarViewController let supplementary: UINavigationController @@ -129,7 +131,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } } - private func makeViewController(withTopicID objectID: TaggedManagedObjectID) -> UIViewController { + private func makeViewController( + withTopicID objectID: TaggedManagedObjectID + ) -> UIViewController { do { let topic = try viewContext.existingObject(with: objectID) return ReaderStreamViewController.controllerWithTopic(topic) @@ -166,7 +170,13 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { guard let topic = sidebarViewModel.getTopic(for: .discover) else { return makeErrorViewController() // This should never happen } - return ReaderDiscoverViewController(topic: topic, initialChannel: channel) + let query = discoverStreamQueryParameters + discoverStreamQueryParameters = nil + return ReaderDiscoverViewController( + topic: topic, + initialChannel: channel, + streamQueryParameters: query + ) } private func makeAllSubscriptionsViewController(source: ScreenTrackingSource? = nil) -> UIViewController { @@ -178,9 +188,11 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { ) self?.push(streamVC) } - let hostVC = UIHostingController(rootView: view - .environment(\.managedObjectContext, viewContext) - .environment(\.trackingContext, ScreenTrackingContext(source: source)) + let hostVC = UIHostingController( + rootView: + view + .environment(\.managedObjectContext, viewContext) + .environment(\.trackingContext, ScreenTrackingContext(source: source)) ) hostVC.title = SharedStrings.Reader.subscriptions if sidebarViewModel.isCompact { @@ -202,7 +214,8 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { let view = ReaderListsView() { [weak self] selection in let streamVC = ReaderStreamViewController.controllerWithTopic(selection) self?.push(streamVC) - }.environment(\.managedObjectContext, viewContext) + } + .environment(\.managedObjectContext, viewContext) let hostVC = UIHostingController(rootView: view) hostVC.title = SharedStrings.Reader.lists if sidebarViewModel.isCompact { @@ -212,7 +225,9 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { } private func makeErrorViewController() -> UIViewController { - UIHostingController(rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle")) + UIHostingController( + rootView: EmptyStateView(SharedStrings.Error.generic, systemImage: "exclamationmark.circle") + ) } /// Shows the given view controller by either displaying it in the `.secondary` @@ -302,10 +317,12 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { viewModel.selection = .main(.recent) case .discover: viewModel.selection = .main(.discover) - case let .discoverStream(key): + case let .discoverStream(key, queryParameters): if let channel = ReaderDiscoverChannel(streamKey: key) { + discoverStreamQueryParameters = queryParameters viewModel.selection = .discover(channel: channel) } else { + discoverStreamQueryParameters = nil // Unknown stream key: fall back to rendering it as a tag stream. viewModel.selection = nil show(ReaderStreamViewController.controllerWithTagSlug(key)) @@ -317,7 +334,13 @@ public final class ReaderPresenter: NSObject, SplitViewDisplayable { case .subscriptions: viewModel.selection = .allSubscriptions case let .post(postID, siteID, isFeed): - push(ReaderDetailViewController.controllerWithPostID(NSNumber(value: postID), siteID: NSNumber(value: siteID), isFeed: isFeed)) + push( + ReaderDetailViewController.controllerWithPostID( + NSNumber(value: postID), + siteID: NSNumber(value: siteID), + isFeed: isFeed + ) + ) case let .postURL(url): push(ReaderDetailViewController.controllerWithPostURL(url)) case let .topic(topic): @@ -346,7 +369,10 @@ private extension UINavigationController { // A workaround for https://a8c.sentry.io/issues/3140539221. func safePushViewController(_ viewController: UIViewController, animated: Bool) { guard !children.contains(viewController) else { - return wpAssertionFailure("pushing the same view controller more than once", userInfo: ["viewController": "\(viewController)"]) + return wpAssertionFailure( + "pushing the same view controller more than once", + userInfo: ["viewController": "\(viewController)"] + ) } pushViewController(viewController, animated: animated) } diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index c8c28761d6a1..617b7e0fc633 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -8,7 +8,7 @@ protocol ContentCoordinator { func displayStatsWithSiteID(_ siteID: NSNumber?, url: URL?) throws func displayFollowersWithSiteID(_ siteID: NSNumber?, expirationTime: TimeInterval) throws func displayStreamWithSiteID(_ siteID: NSNumber?) throws - func displayStreamWithStreamKey(_ streamKey: String?) throws + func displayStreamWithStreamKey(_ streamKey: String?, queryParameters: [String: String]?) throws func displayWebViewWithURL(_ url: URL, source: String) func displayFullscreenImage(_ image: UIImage) func displayPlugin(withSlug pluginSlug: String, on siteSlug: String) throws @@ -55,8 +55,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayStatsWithSiteID(_ siteID: NSNumber?, url: URL? = nil) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.supports(.stats) + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.supports(.stats) else { throw DisplayError.missingParameter } @@ -78,7 +78,7 @@ struct DefaultContentCoordinator: ContentCoordinator { let matcher = RouteMatcher(routes: UniversalLinkRouter.statsRoutes) let matches = matcher.routesMatching(url) if let match = matches.first, - let action = match.action as? StatsRoute, + let action = match.action as? StatsRoute, let tab = action.tab { SiteStatsDashboardPreferences.setSelected(tabType: tab, siteID: siteID) } @@ -86,7 +86,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayBackupWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } @@ -100,8 +100,8 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayScanWithSiteID(_ siteID: NSNumber?) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext), - blog.isScanAllowed() + let blog = Blog.lookup(withID: siteID, in: mainContext), + blog.isScanAllowed() else { throw DisplayError.missingParameter } @@ -112,7 +112,7 @@ struct DefaultContentCoordinator: ContentCoordinator { func displayFollowersWithSiteID(_ siteID: NSNumber?, expirationTime: TimeInterval) throws { guard let siteID, - let blog = Blog.lookup(withID: siteID, in: mainContext) + let blog = Blog.lookup(withID: siteID, in: mainContext) else { throw DisplayError.missingParameter } @@ -135,11 +135,13 @@ struct DefaultContentCoordinator: ContentCoordinator { controller?.navigationController?.pushViewController(browseViewController, animated: true) } - func displayStreamWithStreamKey(_ streamKey: String?) throws { + func displayStreamWithStreamKey(_ streamKey: String?, queryParameters: [String: String]?) throws { guard let streamKey, !streamKey.isEmpty else { throw DisplayError.missingParameter } - RootViewCoordinator.sharedPresenter.showReader(path: .discoverStream(key: streamKey)) + RootViewCoordinator.sharedPresenter.showReader( + path: .discoverStream(key: streamKey, queryParameters: queryParameters) + ) } func displayWebViewWithURL(_ url: URL, source: String) { diff --git a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift index a17b393f04d7..3fd61109efae 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+Reader.swift @@ -55,22 +55,24 @@ extension ReaderRoute: Route { case .blogsPost: return ["/read/blogs/:blog_id/posts/:post_id", "/reader/blogs/:blog_id/posts/:post_id"] case .stream: - return ["/read/streams/:stream_key", "/reader/streams/:stream_key"] + return [ + "/read/streams/:stream_key", "/reader/streams/:stream_key", "/read/:stream_key", "/reader/:stream_key" + ] case .wpcomPost: return ["/:post_year/:post_month/:post_day/:post_name"] } } var section: DeepLinkSection? { - return .reader + .reader } var action: NavigationAction { - return self + self } var jetpackPowered: Bool { - return true + true } } @@ -122,8 +124,10 @@ extension ReaderRoute: NavigationAction { presenter.showReader(path: .post(postID: postID, siteID: blogID)) } case .stream: + let query = queryParametersFromReaderStreamURL(in: values) + let queryParameters: [String: String]? = query.isEmpty ? nil : query if let streamKey = values["stream_key"] { - presenter.showReader(path: .discoverStream(key: streamKey)) + presenter.showReader(path: .discoverStream(key: streamKey, queryParameters: queryParameters)) } case .wpcomPost: if let urlString = values[MatchedRouteURLComponentKey.url.rawValue], let url = URL(string: urlString) { @@ -137,7 +141,7 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let feedID = Int(feedIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (feedID, postID) @@ -148,11 +152,27 @@ extension ReaderRoute: NavigationAction { let postIDValue = values?["post_id"], let blogID = Int(blogIDValue), let postID = Int(postIDValue) else { - return nil + return nil } return (blogID, postID) } + + private func queryParametersFromReaderStreamURL(in values: [String: String]) -> [String: String] { + guard let urlString = values[MatchedRouteURLComponentKey.url.rawValue], + let url = URL(string: urlString), + let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + else { + return [:] + } + var result: [String: String] = [:] + for item in items { + if let value = item.value, !value.isEmpty { + result[item.name] = value + } + } + return result + } } // MARK: - RootViewPresenter (Extensions) diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift index 62cc3f8a33e8..b52a62a83e30 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationContentRouter.swift @@ -58,15 +58,15 @@ struct NotificationContentRouter { // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID try coordinator.displayCommentsWithPostId(notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, + siteID: notification.metaSiteID, + commentID: commentID, source: .commentNotification) case .commentLike: // Focus on the primary comment, and default to the reply ID if its set let commentID = notification.metaCommentID ?? notification.metaReplyID try coordinator.displayCommentsWithPostId(notification.metaPostID, - siteID: notification.metaSiteID, - commentID: commentID, + siteID: notification.metaSiteID, + commentID: commentID, source: .commentLikeNotification) default: throw DefaultContentCoordinator.DisplayError.unsupportedType @@ -87,8 +87,8 @@ struct NotificationContentRouter { // Focus on the comment reply if it's set over the primary comment ID let commentID = notification.metaReplyID ?? notification.metaCommentID try coordinator.displayCommentsWithPostId(range.postID, - siteID: range.siteID, - commentID: commentID, + siteID: range.siteID, + commentID: commentID, source: .commentNotification) case .stats: /// Backup notifications are configured as "stat" notifications @@ -109,7 +109,8 @@ struct NotificationContentRouter { guard let streamKey = range.streamKey else { throw DefaultContentCoordinator.DisplayError.missingParameter } - try coordinator.displayStreamWithStreamKey(streamKey) + let query: [String: String]? = range.streamQueryParameters.isEmpty ? nil : range.streamQueryParameters + try coordinator.displayStreamWithStreamKey(streamKey, queryParameters: query) default: throw DefaultContentCoordinator.DisplayError.unsupportedType } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift index c9e0b526381c..4f524175cd4e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift @@ -9,6 +9,7 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe private let headerView = ReaderDiscoverHeaderView() private var selectedChannel: ReaderDiscoverChannel private let topic: ReaderAbstractTopic + private let streamQueryParameters: [String: String]? private var streamVC: ReaderStreamViewController? private weak var selectInterestsVC: ReaderSelectInterestsViewController? private let selectInterestsCoordinator = ReaderSelectInterestsCoordinator() @@ -18,10 +19,11 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe private let notificationsButtonViewModel = NotificationsButtonViewModel() private var notificationsButtonCancellable: AnyCancellable? - init(topic: ReaderAbstractTopic, initialChannel: ReaderDiscoverChannel = .freshlyPressed) { + init(topic: ReaderAbstractTopic, initialChannel: ReaderDiscoverChannel = .freshlyPressed, streamQueryParameters: [String: String]? = nil) { wpAssert(ReaderHelpers.topicIsDiscover(topic)) self.viewContext = ContextManager.shared.mainContext self.topic = topic + self.streamQueryParameters = streamQueryParameters self.selectedChannel = initialChannel self.tags = ManagedObjectsObserver( predicate: ReaderSidebarTagsSection.predicate, @@ -80,7 +82,7 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe private func setupHeaderView() { tags.$objects.sink { [weak self] tags in - self?.configureHeader(tags: tags) + self?.configureHeader(tags: tags) }.store(in: &cancellables) headerView.delegate = self @@ -106,15 +108,28 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe case .freshlyPressed: ReaderStreamViewController.controllerWithTopic(ReaderHelpers.getFreshlyPressedTopic()) case .recommended: - ReaderDiscoverStreamViewController(topic: topic) + ReaderDiscoverStreamViewController(topic: topic, streamQueryParameters: streamQueryParameters) case .firstPosts: - ReaderDiscoverStreamViewController(topic: topic, stream: .firstPosts, sorting: .date) + ReaderDiscoverStreamViewController( + topic: topic, + stream: .firstPosts, + sorting: .date, + streamQueryParameters: streamQueryParameters + ) case .latest: - ReaderDiscoverStreamViewController(topic: topic, sorting: .date) + ReaderDiscoverStreamViewController( + topic: topic, + sorting: .date, + streamQueryParameters: streamQueryParameters + ) case .dailyPrompts: ReaderStreamViewController.controllerWithTagSlug(ReaderTagTopic.dailyPromptTag) case .onThisDay: - ReaderDiscoverStreamViewController(topic: topic, stream: .onThisDay) + ReaderDiscoverStreamViewController( + topic: topic, + stream: .onThisDay, + streamQueryParameters: streamQueryParameters + ) case .tag(let tag): ReaderStreamViewController.controllerWithTopic(tag) } @@ -232,7 +247,7 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re /// Whether the current view controller is visible private var isVisible: Bool { - return isViewLoaded && view.window != nil + isViewLoaded && view.window != nil } init(topic: ReaderAbstractTopic, stream: ReaderStream = .discover, sorting: ReaderSortingOption = .noSorting) { @@ -298,7 +313,8 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re } } - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) + { super.tableView(tableView, willDisplay: cell, forRowAt: indexPath) if let posts = content.content as? [ReaderCard], let post = posts[indexPath.row].post { @@ -307,7 +323,8 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re } private func makeRecommendedTagsCell(for interests: [ReaderTagTopic]) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: readerCardTopicsIdentifier) as! ReaderRecommendedTagsCell + let cell = + tableView.dequeueReusableCell(withIdentifier: readerCardTopicsIdentifier) as! ReaderRecommendedTagsCell cell.configure(with: interests, delegate: self) cell.accessibilityIdentifier = "topics-card-cell" hideSeparator(for: cell) @@ -315,7 +332,8 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re } private func makeRecommendedSitesCell(for sites: [ReaderSiteTopic]) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: readerCardSitesIdentifier) as! ReaderRecommendedSitesCell + let cell = + tableView.dequeueReusableCell(withIdentifier: readerCardSitesIdentifier) as! ReaderRecommendedSitesCell cell.configure(with: sites, delegate: self) hideSeparator(for: cell) return cell @@ -327,17 +345,26 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re // MARK: - Sync - override func fetch(for topic: ReaderAbstractTopic, success: @escaping ((Int, Bool) -> Void), failure: @escaping ((Error?) -> Void)) { + override func fetch( + for topic: ReaderAbstractTopic, + success: @escaping ((Int, Bool) -> Void), + failure: @escaping ((Error?) -> Void) + ) { page = 1 refreshCount += 1 - cardsService.fetch(isFirstPage: true, refreshCount: refreshCount, success: { [weak self] cardsCount, hasMore in - self?.trackContentPresented() - success(cardsCount, hasMore) - }, failure: { [weak self] error in - self?.trackContentPresented() - failure(error) - }) + cardsService.fetch( + isFirstPage: true, + refreshCount: refreshCount, + success: { [weak self] cardsCount, hasMore in + self?.trackContentPresented() + success(cardsCount, hasMore) + }, + failure: { [weak self] error in + self?.trackContentPresented() + failure(error) + } + ) } override func loadMoreItems(_ success: ((Bool) -> Void)?, failure: ((NSError) -> Void)?) { @@ -346,19 +373,23 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re page += 1 WPAnalytics.trackReader(.readerDiscoverPaginated, properties: ["page": page]) - cardsService.fetch(isFirstPage: false, success: { _, hasMore in - success?(hasMore) - }, failure: { error in - guard let error else { - return - } + cardsService.fetch( + isFirstPage: false, + success: { _, hasMore in + success?(hasMore) + }, + failure: { error in + guard let error else { + return + } - failure?(error as NSError) - }) + failure?(error as NSError) + } + ) } override var topicPostsCount: Int { - return cards?.count ?? 0 + cards?.count ?? 0 } override func syncIfAppropriate(forceSync: Bool = false) { @@ -390,25 +421,28 @@ private class ReaderDiscoverStreamViewController: ReaderStreamViewController, Re } override func predicateForFetchRequest() -> NSPredicate { - return NSPredicate(format: "post != NULL OR topics.@count != 0 OR sites.@count != 0") + NSPredicate(format: "post != NULL OR topics.@count != 0 OR sites.@count != 0") } private func addObservers() { // Listens for when a site is blocked - NotificationCenter.default.addObserver(self, - selector: #selector(siteBlocked(_:)), - name: .ReaderSiteBlocked, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(siteBlocked(_:)), + name: .ReaderSiteBlocked, + object: nil + ) } /// Update the post card when a site is blocked from post details. /// @objc private func siteBlocked(_ notification: Foundation.Notification) { guard let userInfo = notification.userInfo, - let post = userInfo[ReaderNotificationKeys.post] as? ReaderPost, - let posts = content.content as? [ReaderCard], // let posts = cards - let contentPost = posts.first(where: { $0.post?.postID == post.postID }), - let indexPath = content.indexPath(forObject: contentPost) else { + let post = userInfo[ReaderNotificationKeys.post] as? ReaderPost, + let posts = content.content as? [ReaderCard], // let posts = cards + let contentPost = posts.first(where: { $0.post?.postID == post.postID }), + let indexPath = content.indexPath(forObject: contentPost) + else { return } diff --git a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift index 121dcf47d75f..351a83d28101 100644 --- a/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift +++ b/WordPress/Classes/ViewRelated/Reader/Navigation/ReaderNavigationPath.swift @@ -8,7 +8,7 @@ enum ReaderNavigationPath: Hashable { case discover /// A Reader stream identified by key, e.g. the key used by `/read/streams/:stream_key`. /// Resolves to a matching Discover channel when available, otherwise falls back to a tag. - case discoverStream(key: String) + case discoverStream(key: String, queryParameters: [String: String]?) case likes case search case subscriptions diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3aa12d0b4bae..9073f7c109f2 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3817,6 +3817,7 @@ CODE_SIGN_ENTITLEMENTS = ../Sources/JetpackStatsWidgets/JetpackStatsWidgets.entitlements; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3836,6 +3837,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack.JetpackStatsWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development Stats Widget"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development Stats Widget"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; @@ -3942,6 +3944,7 @@ CODE_SIGN_ENTITLEMENTS = "JetpackIntents/Supporting Files/JetpackIntents.entitlements"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3961,6 +3964,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack.JetpackIntents; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development Intents"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development Intents Extension"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; @@ -6720,8 +6724,13 @@ CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = NO; CODE_SIGN_ENTITLEMENTS = Jetpack/JetpackDebug.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREFIX_HEADER = WordPress_Prefix.pch; @@ -6761,7 +6770,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack; PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0; @@ -6780,6 +6790,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Automattic, Inc. (PZYM8XX95Q)"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_PREFIX_HEADER = WordPress_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -6817,6 +6828,7 @@ PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.jetpack"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0; @@ -6835,6 +6847,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution: Automattic, Inc."; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_PREFIX_HEADER = WordPress_Prefix.pch; GCC_SYMBOLS_PRIVATE_EXTERN = NO; @@ -6868,6 +6881,7 @@ PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match InHouse com.jetpack.alpha"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "About Automattic Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0; From 620321f34a9a0ebfb363f1f15d2b7d5578390743 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Mon, 27 Apr 2026 21:33:17 +0200 Subject: [PATCH 10/10] Remove unneeded changes --- WordPress/WordPress.xcodeproj/project.pbxproj | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 9073f7c109f2..3aa12d0b4bae 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3817,7 +3817,6 @@ CODE_SIGN_ENTITLEMENTS = ../Sources/JetpackStatsWidgets/JetpackStatsWidgets.entitlements; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3837,7 +3836,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack.JetpackStatsWidgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development Stats Widget"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development Stats Widget"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; @@ -3944,7 +3942,6 @@ CODE_SIGN_ENTITLEMENTS = "JetpackIntents/Supporting Files/JetpackIntents.entitlements"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3964,7 +3961,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack.JetpackIntents; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development Intents"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development Intents Extension"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; @@ -6724,13 +6720,8 @@ CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = NO; CODE_SIGN_ENTITLEMENTS = Jetpack/JetpackDebug.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREFIX_HEADER = WordPress_Prefix.pch; @@ -6770,8 +6761,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.automattic.jetpack; PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development"; + PROVISIONING_PROFILE_SPECIFIER = "Jetpack iOS Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0; @@ -6790,7 +6780,6 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Automattic, Inc. (PZYM8XX95Q)"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_PREFIX_HEADER = WordPress_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -6828,7 +6817,6 @@ PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.automattic.jetpack"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Jetpack iOS Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0; @@ -6847,7 +6835,6 @@ CODE_SIGN_IDENTITY = "iPhone Distribution: Automattic, Inc."; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "${VERSION_LONG}"; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_BITCODE = NO; GCC_PREFIX_HEADER = WordPress_Prefix.pch; GCC_SYMBOLS_PRIVATE_EXTERN = NO; @@ -6881,7 +6868,6 @@ PRODUCT_MODULE_NAME = WordPress; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match InHouse com.jetpack.alpha"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "About Automattic Development"; SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WordPress-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; SWIFT_VERSION = 5.0;