From 8c00705e18419acf753da588b3ff6f88ecde2886 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 14 Mar 2024 21:03:54 +0100 Subject: [PATCH 001/116] Add `BlogLisView` --- .../Site Picker/BlogList/BlogListView.swift | 168 ++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 4 + 2 files changed, 172 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift new file mode 100644 index 000000000000..191939e900e4 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -0,0 +1,168 @@ +import SwiftUI +import DesignSystem + +struct BlogListView: View { + private enum Constants { + static let imageDiameter: CGFloat = 40 + } + + struct Site { + let title: String + let domain: String + let imageURL: URL? + } + + @Binding private var isEditing: Bool + @Binding private var pinnedDomains: Set + private let sites: [Site] + + init(sites: [Site], pinnedDomains: Binding>, isEditing: Binding) { + self.sites = sites + self._pinnedDomains = pinnedDomains + self._isEditing = isEditing + } + + var body: some View { + List { + pinnedSection + unPinnedSection + } +// .scrollIndicators(.hidden) + .listStyle(.grouped) + .background(Color.DS.Background.primary) +// .scrollContentBackground(.hidden) + } + + private func sectionHeader(title: String) -> some View { + Text(title) + .style(.bodyLarge(.emphasized)) + .foregroundStyle(Color.DS.Foreground.primary) + .listRowSeparator(.hidden) + } + + @ViewBuilder + private var pinnedSection: some View { + let pinnedSites = BlogListReducer.pinnedSites( + allSites: sites, + pinnedDomains: pinnedDomains + ) + if !pinnedSites.isEmpty { + Section { + ForEach( + pinnedSites, + id: \.domain) { site in + siteHStack( + site: site + ) + } + } header: { + sectionHeader( + title: "Pinned sites" + ) + } + } + } + + @ViewBuilder + private var unPinnedSection: some View { + let unPinnedSites = BlogListReducer.unPinnedSites( + allSites: sites, + pinnedDomains: pinnedDomains + ) + if !unPinnedSites.isEmpty { + Section { + ForEach( + unPinnedSites, + id: \.domain) { site in + siteHStack( + site: site + ) + } + } header: { + sectionHeader( + title: "All sites" + ) + } + } + } + + private func siteHStack(site: Site) -> some View { + HStack(spacing: 0) { + AvatarsView(style: .single(site.imageURL)) + .padding(.leading, Length.Padding.double) + .padding(.trailing, Length.Padding.split) + + textsVStack(title: site.title, domain: site.domain) + + Spacer() + + if isEditing { + pinIcon( + domain: site.domain + ) + .padding(.trailing, Length.Padding.double) + } + } + .listRowSeparator(.hidden) + .listRowInsets( + .init( + top: Length.Padding.single, + leading: 0, + bottom: Length.Padding.single, + trailing: 0 + ) + ) + } + + private func textsVStack(title: String, domain: String) -> some View { + VStack(alignment: .leading, spacing: 0) { + Text(title) + .style(.bodySmall(.regular)) + .foregroundStyle(Color.DS.Foreground.primary) + .layoutPriority(1) + .lineLimit(2) + + Text(domain) + .style(.bodySmall(.regular)) + .foregroundStyle(Color.DS.Foreground.secondary) + .layoutPriority(2) + .lineLimit(1) + .padding(.top, Length.Padding.half) + } + } + + private func pinIcon(domain: String) -> some View { + Button(action: { + withAnimation(.interactiveSpring) { + pinnedDomains = pinnedDomains.symmetricDifference([domain]) + } + }, label: { + if pinnedDomains.contains(domain) { + Image(systemName: "pin.fill") + .imageScale(.small) + .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) + } else { + Image(systemName: "pin") + .imageScale(.small) + .foregroundStyle(Color.DS.Foreground.secondary) + } + }) + } +} + +#Preview { + BlogListView( + sites: [ + .init(title: "Clay Chronicles", + domain: "claychronicles.com", + imageURL: URL(string: "https://picsum.photos/40/40")! + ), + .init(title: "Culinary Wanderlust", + domain: "culinarywanderlust.wordpress.com", + imageURL: URL(string: "https://picsum.photos/40/40")! + ) + ], + pinnedDomains: .constant([]), + isEditing: .constant(true) + ) +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index d209168709d2..02478d358e0a 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -282,6 +282,8 @@ 08216FD31CDBF96000304BA7 /* MenuItemTagsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC31CDBF96000304BA7 /* MenuItemTagsViewController.m */; }; 08216FD41CDBF96000304BA7 /* MenuItemTypeSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC51CDBF96000304BA7 /* MenuItemTypeSelectionView.m */; }; 08216FD51CDBF96000304BA7 /* MenuItemTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */; }; + 0822C3F52BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; + 0822C3F62BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; 082635BB1CEA69280088030C /* MenuItemsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 082635BA1CEA69280088030C /* MenuItemsViewController.m */; }; 0828D7FA1E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828D7F91E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift */; }; @@ -6037,6 +6039,7 @@ 08216FC51CDBF96000304BA7 /* MenuItemTypeSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeSelectionView.m; sourceTree = ""; }; 08216FC61CDBF96000304BA7 /* MenuItemTypeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemTypeViewController.h; sourceTree = ""; }; 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeViewController.m; sourceTree = ""; }; + 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListView.swift; sourceTree = ""; }; 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListCardView.swift; sourceTree = ""; }; 082635B91CEA69280088030C /* MenuItemsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemsViewController.h; sourceTree = ""; }; 082635BA1CEA69280088030C /* MenuItemsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemsViewController.m; sourceTree = ""; }; @@ -25507,6 +25510,7 @@ FABB24A12602FC2C00C8785C /* UITableViewCell+enableDisable.swift in Sources */, FABB24A22602FC2C00C8785C /* WPAnalyticsTrackerAutomatticTracks.m in Sources */, FABB24A32602FC2C00C8785C /* CollapsableHeaderCollectionViewCell.swift in Sources */, + 0822C3F62BA1EA2100C53B50 /* BlogListView.swift in Sources */, C3643AD028AC049D00FC5FD3 /* SharingViewController.swift in Sources */, FABB24A42602FC2C00C8785C /* NotificationContentFactory.swift in Sources */, FABB24A52602FC2C00C8785C /* SiteIconView.swift in Sources */, From 9a319ca01535882a628fe849c46ad3b3149d3a36 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 14 Mar 2024 21:05:21 +0100 Subject: [PATCH 002/116] Add `SiteSwitcherReducer` --- .../Blog/Blog List/BlogListDataSource.swift | 4 ++-- .../Blog/Site Picker/SiteSwitcherReducer.swift | 10 ++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift index c0dbbac84aa1..6520eb004bd2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift @@ -293,8 +293,8 @@ private extension BlogListDataSource { // MARK: - Data -private extension BlogListDataSource { - var sections: [[Blog]] { +extension BlogListDataSource { + private var sections: [[Blog]] { if let sections = cachedSections { return sections } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift new file mode 100644 index 000000000000..1e6f6000c315 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift @@ -0,0 +1,10 @@ +enum SiteSwitcherReducer { + static func allBlogs() -> [Blog] { + let config = BlogListConfiguration.defaultConfig + let dataSource = BlogListDataSource() + dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites + dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains + + return dataSource.filteredBlogs + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 02478d358e0a..53e1d1e75956 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -284,6 +284,8 @@ 08216FD51CDBF96000304BA7 /* MenuItemTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */; }; 0822C3F52BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; 0822C3F62BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; + 0822C3FD2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; + 0822C3FE2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; 082635BB1CEA69280088030C /* MenuItemsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 082635BA1CEA69280088030C /* MenuItemsViewController.m */; }; 0828D7FA1E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828D7F91E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift */; }; @@ -6040,6 +6042,7 @@ 08216FC61CDBF96000304BA7 /* MenuItemTypeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemTypeViewController.h; sourceTree = ""; }; 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeViewController.m; sourceTree = ""; }; 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListView.swift; sourceTree = ""; }; + 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSwitcherReducer.swift; sourceTree = ""; }; 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListCardView.swift; sourceTree = ""; }; 082635B91CEA69280088030C /* MenuItemsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemsViewController.h; sourceTree = ""; }; 082635BA1CEA69280088030C /* MenuItemsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemsViewController.m; sourceTree = ""; }; @@ -18355,6 +18358,7 @@ FA73D7E72798766300DF24B3 /* Site Picker */ = { isa = PBXGroup; children = ( + 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */, FA73D7E42798765B00DF24B3 /* SitePickerViewController.swift */, FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */, FA73D7E827987BA500DF24B3 /* SitePickerViewController+SiteIcon.swift */, @@ -24161,6 +24165,7 @@ FE4DC5A8293A84E6008F322F /* MigrationDeepLinkRouter.swift in Sources */, 77DFF0892B68386800FA561D /* BooleanUserDefaultsDebugViewModel.swift in Sources */, 80DB57992AF99E0900C728FF /* BlogListConfiguration.swift in Sources */, + 0822C3FE2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */, FA4B203629A786460089FE68 /* BlazeEventsTracker.swift in Sources */, FABB20EA2602FC2C00C8785C /* ActivityTypeSelectorViewController.swift in Sources */, FABB20EB2602FC2C00C8785C /* ActivityActionsParser.swift in Sources */, From b4f0fd798faea15bdccecd32783866cf97d1e33a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 14 Mar 2024 21:05:39 +0100 Subject: [PATCH 003/116] Add `SiteSwitchView` --- .../BlogList/BlogListReducer.swift | 23 ++++++++ .../SitePickerViewController.swift | 53 ++++++++++++++----- .../Blog/Site Picker/SiteSwitcherView.swift | 52 ++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 23 ++++++++ 4 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift create mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift new file mode 100644 index 000000000000..e2108afab0d4 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -0,0 +1,23 @@ +enum BlogListReducer { + static func pinnedSites( + allSites: [BlogListView.Site], + pinnedDomains: Set + ) -> [BlogListView.Site] { + allSites.filter { + pinnedDomains.contains( + $0.domain + ) + } + } + + static func unPinnedSites( + allSites: [BlogListView.Site], + pinnedDomains: Set + ) -> [BlogListView.Site] { + allSites.filter { + !pinnedDomains.contains( + $0.domain + ) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 8b915c5acbb9..142c4a2af6be 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -3,6 +3,7 @@ import WordPressFlux import WordPressShared import SwiftUI import SVProgressHUD +import DesignSystem final class SitePickerViewController: UIViewController { @@ -92,20 +93,46 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { showSiteTitleSettings() } - func siteSwitcherTapped() { - let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) - - blogListController.blogSelected = { [weak self] controller, selectedBlog in - guard let self else { return } - self.switchToBlog(selectedBlog) - controller.dismiss(animated: true) { - self.onBlogListDismiss?() - } - } +// - (void)configureDataSource +// { +// self.dataSource = [BlogListDataSource new]; +// self.dataSource.shouldShowDisclosureIndicator = NO; +// self.dataSource.shouldHideSelfHostedSites = self.configuration.shouldHideSelfHostedSites; +// self.dataSource.shouldHideBlogsNotSupportingDomains = self.configuration.shouldHideBlogsNotSupportingDomains; +// +// __weak __typeof(self) weakSelf = self; +// self.dataSource.visibilityChanged = ^(Blog *blog, BOOL visible) { +// [weakSelf setVisible:visible forBlog:blog]; +// }; +// self.dataSource.dataChanged = ^{ +// if (weakSelf.visible) { +// [weakSelf dataChanged]; +// } +// }; +// } - let navigationController = UINavigationController(rootViewController: blogListController) - navigationController.modalPresentationStyle = .formSheet - present(navigationController, animated: true) + func siteSwitcherTapped() { + // Utilize existing DataSource class to fetch blogs. + let config = BlogListConfiguration.defaultConfig + let dataSource = BlogListDataSource() + dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites + dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains + + let hostingController = UIHostingController(rootView: SiteSwitcherView(pinnedDomains: ["https://alpavanoglu.wordpress.com"])) + present(hostingController, animated: true) +// let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) +// +// blogListController.blogSelected = { [weak self] controller, selectedBlog in +// guard let self else { return } +// self.switchToBlog(selectedBlog) +// controller.dismiss(animated: true) { +// self.onBlogListDismiss?() +// } +// } +// +// let navigationController = UINavigationController(rootViewController: blogListController) +// navigationController.modalPresentationStyle = .formSheet +// present(navigationController, animated: true) WPAnalytics.track(.mySiteSiteSwitcherTapped) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift new file mode 100644 index 000000000000..df1f72fae84a --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -0,0 +1,52 @@ +import SwiftUI + +struct SiteSwitcherView: View { + @State private var isEditing: Bool = false + @State private var pinnedDomains: Set + + init(pinnedDomains: Set) { + self.pinnedDomains = pinnedDomains + } + + var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + BlogListView( + sites: SiteSwitcherReducer.allBlogs().compactMap { + .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) + }, + pinnedDomains: $pinnedDomains, + isEditing: $isEditing + ) + .toolbar { + ellipsisButton + } + .navigationTitle("Switch Site") + .navigationBarTitleDisplayMode(.inline) + } + } else { + // Fallback on earlier versions + } + } + + private var ellipsisButton: some View { + Button(action: { + isEditing.toggle() + }, label: { + Text(isEditing ? "Done": "Edit") + .style(.bodyLarge(.regular)) + .foregroundStyle( + Color.DS.Foreground.primary + ) + }) + } +} + +#Preview { + SiteSwitcherView( + pinnedDomains: [ + "claychronicles.com", + "historyunearthed.wordpress.com" + ] + ) +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 53e1d1e75956..9a4731a62606 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -284,6 +284,10 @@ 08216FD51CDBF96000304BA7 /* MenuItemTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */; }; 0822C3F52BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; 0822C3F62BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; + 0822C3F72BA1EA2100C53B50 /* BlogListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */; }; + 0822C3F82BA1EA2100C53B50 /* BlogListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */; }; + 0822C3F92BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */; }; + 0822C3FA2BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */; }; 0822C3FD2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; 0822C3FE2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; @@ -6042,6 +6046,8 @@ 08216FC61CDBF96000304BA7 /* MenuItemTypeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemTypeViewController.h; sourceTree = ""; }; 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeViewController.m; sourceTree = ""; }; 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListView.swift; sourceTree = ""; }; + 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListReducer.swift; sourceTree = ""; }; + 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteSwitcherView.swift; sourceTree = ""; }; 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSwitcherReducer.swift; sourceTree = ""; }; 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListCardView.swift; sourceTree = ""; }; 082635B91CEA69280088030C /* MenuItemsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemsViewController.h; sourceTree = ""; }; @@ -10144,6 +10150,15 @@ path = "Feature Highlight"; sourceTree = ""; }; + 0822C3FB2BA1EA2400C53B50 /* BlogList */ = { + isa = PBXGroup; + children = ( + 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */, + 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */, + ); + path = BlogList; + sourceTree = ""; + }; 082EA3D82B4DDF6800E7F361 /* NotificationsViewController */ = { isa = PBXGroup; children = ( @@ -18358,6 +18373,8 @@ FA73D7E72798766300DF24B3 /* Site Picker */ = { isa = PBXGroup; children = ( + 0822C3FB2BA1EA2400C53B50 /* BlogList */, + 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */, 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */, FA73D7E42798765B00DF24B3 /* SitePickerViewController.swift */, FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */, @@ -22214,6 +22231,7 @@ 93C1147F18EC5DD500DAC95C /* AccountService.m in Sources */, 986C90882231AD6200FC31E1 /* PostStatsViewModel.swift in Sources */, FAFC064E27D2360B002F0483 /* QuickStartCell.swift in Sources */, + 0822C3F92BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */, 9A4A8F4B235758EF00088CE4 /* StatsStore+Cache.swift in Sources */, 083ED8CC2A4322CB007F89B3 /* ComplianceLocationService.swift in Sources */, BE87E1A21BD405790075D45B /* WP3DTouchShortcutHandler.swift in Sources */, @@ -22811,6 +22829,7 @@ B54C02241F38F50100574572 /* String+RegEx.swift in Sources */, FFB1FA9E1BF0EB840090C761 /* UIImage+Exporters.swift in Sources */, F4F7B2552AFA60DA00207282 /* DomainDetailsWebViewController.swift in Sources */, + 0822C3F52BA1EA2100C53B50 /* BlogListView.swift in Sources */, F48D44BD2989AA8C0051EAA6 /* ReaderSiteService.m in Sources */, 98FCFC232231DF43006ECDD4 /* PostStatsTitleCell.swift in Sources */, E1556CF2193F6FE900FC52EA /* CommentService.m in Sources */, @@ -23031,6 +23050,7 @@ 329F8E5824DDBD11002A5311 /* ReaderTopicCollectionViewCoordinator.swift in Sources */, 5948AD0E1AB734F2006E8882 /* WPAppAnalytics.m in Sources */, B53AD9BF1BE9584B009AB87E /* SettingsSelectionViewController.m in Sources */, + 0822C3F72BA1EA2100C53B50 /* BlogListReducer.swift in Sources */, 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */, FAD7626429F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift in Sources */, 24351254264DCA08009BB2B6 /* Secrets.swift in Sources */, @@ -23140,6 +23160,7 @@ B532D4EA199D4357006E4DF6 /* NoteBlockHeaderTableViewCell.swift in Sources */, 24ADA24C24F9A4CB001B5DAE /* RemoteFeatureFlagStore.swift in Sources */, 836498CE281735CC00A2C170 /* BloggingPromptsHeaderView.swift in Sources */, + 0822C3FD2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */, 3F43703F2893201400475B6E /* JetpackOverlayViewController.swift in Sources */, B09879762B572E1F0048256D /* StatsTrafficDatePickerViewModel.swift in Sources */, 319D6E8519E44F7F0013871C /* SuggestionsTableViewCell.m in Sources */, @@ -24825,6 +24846,7 @@ 4A1E77CD2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift in Sources */, FA98B61D29A3DB840071AAE8 /* BlazeHelper.swift in Sources */, 0830538D2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */, + 0822C3F82BA1EA2100C53B50 /* BlogListReducer.swift in Sources */, FABB22C32602FC2C00C8785C /* ReaderTagsFooter.swift in Sources */, 4A2C73F52A95856000ACE79E /* PostRepository.swift in Sources */, 98DCF4A6275945E00008630F /* ReaderDetailNoCommentCell.swift in Sources */, @@ -25065,6 +25087,7 @@ FABB23612602FC2C00C8785C /* ReaderTabItemsStore.swift in Sources */, FABB23622602FC2C00C8785C /* NoResultsViewController+Model.swift in Sources */, 3F435220289B2B2B00CE19ED /* JetpackBrandingCoordinator.swift in Sources */, + 0822C3FA2BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */, FABB23642602FC2C00C8785C /* UIAlertController+Helpers.swift in Sources */, 3F4D035128A56F9B00F0A4FD /* CircularImageButton.swift in Sources */, FABB23652602FC2C00C8785C /* RevisionDiff+CoreData.swift in Sources */, From 4c273e91e8b1ce27d9e243ce8184b0346edc2b28 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 21 Mar 2024 16:53:58 +0100 Subject: [PATCH 004/116] Add selection functionality to site picker --- .../Site Picker/BlogList/BlogListView.swift | 85 +++++++++---------- .../SitePickerViewController.swift | 20 ++++- .../Blog/Site Picker/SiteSwitcherView.swift | 20 ++--- 3 files changed, 66 insertions(+), 59 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 191939e900e4..02f49503582f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -15,13 +15,20 @@ struct BlogListView: View { @Binding private var isEditing: Bool @Binding private var pinnedDomains: Set private let sites: [Site] + private let selectionCallback: ((String) -> Void) - init(sites: [Site], pinnedDomains: Binding>, isEditing: Binding) { + init( + sites: [Site], + pinnedDomains: Binding>, + isEditing: Binding, + selectionCallback: @escaping ((String) -> Void) + ) { self.sites = sites self._pinnedDomains = pinnedDomains self._isEditing = isEditing + self.selectionCallback = selectionCallback } - + var body: some View { List { pinnedSection @@ -87,20 +94,30 @@ struct BlogListView: View { } private func siteHStack(site: Site) -> some View { - HStack(spacing: 0) { - AvatarsView(style: .single(site.imageURL)) - .padding(.leading, Length.Padding.double) - .padding(.trailing, Length.Padding.split) + Button { + if isEditing { + withAnimation { + pinnedDomains = pinnedDomains.symmetricDifference([site.domain]) + } + } else { + selectionCallback(site.domain) + } + } label: { + HStack(spacing: 0) { + AvatarsView(style: .single(site.imageURL)) + .padding(.leading, Length.Padding.double) + .padding(.trailing, Length.Padding.split) - textsVStack(title: site.title, domain: site.domain) + textsVStack(title: site.title, domain: site.domain) - Spacer() + Spacer() - if isEditing { - pinIcon( - domain: site.domain - ) - .padding(.trailing, Length.Padding.double) + if isEditing { + pinIcon( + domain: site.domain + ) + .padding(.trailing, Length.Padding.double) + } } } .listRowSeparator(.hidden) @@ -112,6 +129,7 @@ struct BlogListView: View { trailing: 0 ) ) + .listRowBackground(Color.DS.Background.primary) } private func textsVStack(title: String, domain: String) -> some View { @@ -132,37 +150,14 @@ struct BlogListView: View { } private func pinIcon(domain: String) -> some View { - Button(action: { - withAnimation(.interactiveSpring) { - pinnedDomains = pinnedDomains.symmetricDifference([domain]) - } - }, label: { - if pinnedDomains.contains(domain) { - Image(systemName: "pin.fill") - .imageScale(.small) - .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) - } else { - Image(systemName: "pin") - .imageScale(.small) - .foregroundStyle(Color.DS.Foreground.secondary) - } - }) + if pinnedDomains.contains(domain) { + Image(systemName: "pin.fill") + .imageScale(.small) + .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) + } else { + Image(systemName: "pin") + .imageScale(.small) + .foregroundStyle(Color.DS.Foreground.secondary) + } } } - -#Preview { - BlogListView( - sites: [ - .init(title: "Clay Chronicles", - domain: "claychronicles.com", - imageURL: URL(string: "https://picsum.photos/40/40")! - ), - .init(title: "Culinary Wanderlust", - domain: "culinarywanderlust.wordpress.com", - imageURL: URL(string: "https://picsum.photos/40/40")! - ) - ], - pinnedDomains: .constant([]), - isEditing: .constant(true) - ) -} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 142c4a2af6be..7c0bd62a4717 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -118,7 +118,25 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains - let hostingController = UIHostingController(rootView: SiteSwitcherView(pinnedDomains: ["https://alpavanoglu.wordpress.com"])) + var dismissAction: (() -> Void)? = nil + let hostingController = UIHostingController( + rootView: SiteSwitcherView( + pinnedDomains: ["https://alpavanoglu.wordpress.com"], + selectionCallback: { [weak self] selectedDomain in + guard let selectedBlog = dataSource.filteredBlogs.first(where: { $0.url == selectedDomain }) else { + return + } + self?.switchToBlog(selectedBlog) + // Dismiss hosting controller with completion block + dismissAction?() + } + ) + ) + dismissAction = { + hostingController.dismiss(animated: true) { [weak self] in + self?.onBlogListDismiss?() + } + } present(hostingController, animated: true) // let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) // diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index df1f72fae84a..377338f90c4b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -3,9 +3,11 @@ import SwiftUI struct SiteSwitcherView: View { @State private var isEditing: Bool = false @State private var pinnedDomains: Set + private let selectionCallback: ((String) -> Void) - init(pinnedDomains: Set) { + init(pinnedDomains: Set, selectionCallback: @escaping ((String) -> Void)) { self.pinnedDomains = pinnedDomains + self.selectionCallback = selectionCallback } var body: some View { @@ -16,10 +18,11 @@ struct SiteSwitcherView: View { .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) }, pinnedDomains: $pinnedDomains, - isEditing: $isEditing + isEditing: $isEditing, + selectionCallback: selectionCallback ) .toolbar { - ellipsisButton + editButton } .navigationTitle("Switch Site") .navigationBarTitleDisplayMode(.inline) @@ -29,7 +32,7 @@ struct SiteSwitcherView: View { } } - private var ellipsisButton: some View { + private var editButton: some View { Button(action: { isEditing.toggle() }, label: { @@ -41,12 +44,3 @@ struct SiteSwitcherView: View { }) } } - -#Preview { - SiteSwitcherView( - pinnedDomains: [ - "claychronicles.com", - "historyunearthed.wordpress.com" - ] - ) -} From 1b43b605219dd3ad0b308c37442d8f7450180cb5 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 21 Mar 2024 18:42:55 +0100 Subject: [PATCH 005/116] Add selection animation --- .../Blog/Site Picker/BlogList/BlogListView.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 02f49503582f..56b5610545da 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -34,10 +34,8 @@ struct BlogListView: View { pinnedSection unPinnedSection } -// .scrollIndicators(.hidden) .listStyle(.grouped) .background(Color.DS.Background.primary) -// .scrollContentBackground(.hidden) } private func sectionHeader(title: String) -> some View { @@ -119,7 +117,9 @@ struct BlogListView: View { .padding(.trailing, Length.Padding.double) } } + .padding(.vertical, Length.Padding.half) } + .buttonStyle(BlogListButtonStyle()) .listRowSeparator(.hidden) .listRowInsets( .init( @@ -161,3 +161,10 @@ struct BlogListView: View { } } } + +private struct BlogListButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background(configuration.isPressed ? Color.DS.Background.secondary : Color.DS.Background.primary) + } +} From 26b09da6f3a527dacdda3a2b3d3d43d996a4bcf2 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 21 Mar 2024 19:24:52 +0100 Subject: [PATCH 006/116] Add add site button --- .../Site Picker/BlogList/BlogListView.swift | 86 +++++++++++++------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 56b5610545da..f21050842381 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -28,14 +28,25 @@ struct BlogListView: View { self._isEditing = isEditing self.selectionCallback = selectionCallback } - + var body: some View { - List { - pinnedSection - unPinnedSection + if #available(iOS 16.0, *) { + contentVStack + .scrollContentBackground(.hidden) + } else { + contentVStack + } + } + + private var contentVStack: some View { + VStack { + List { + pinnedSection + unPinnedSection + } + .listStyle(.grouped) + addSiteButtonVStack } - .listStyle(.grouped) - .background(Color.DS.Background.primary) } private func sectionHeader(title: String) -> some View { @@ -56,7 +67,7 @@ struct BlogListView: View { ForEach( pinnedSites, id: \.domain) { site in - siteHStack( + siteButton( site: site ) } @@ -64,6 +75,13 @@ struct BlogListView: View { sectionHeader( title: "Pinned sites" ) + .listRowInsets(EdgeInsets( + top: Length.Padding.medium, + leading: Length.Padding.double, + bottom: 0, + trailing: Length.Padding.double) + ) + } } } @@ -79,7 +97,7 @@ struct BlogListView: View { ForEach( unPinnedSites, id: \.domain) { site in - siteHStack( + siteButton( site: site ) } @@ -91,7 +109,7 @@ struct BlogListView: View { } } - private func siteHStack(site: Site) -> some View { + private func siteButton(site: Site) -> some View { Button { if isEditing { withAnimation { @@ -101,23 +119,7 @@ struct BlogListView: View { selectionCallback(site.domain) } } label: { - HStack(spacing: 0) { - AvatarsView(style: .single(site.imageURL)) - .padding(.leading, Length.Padding.double) - .padding(.trailing, Length.Padding.split) - - textsVStack(title: site.title, domain: site.domain) - - Spacer() - - if isEditing { - pinIcon( - domain: site.domain - ) - .padding(.trailing, Length.Padding.double) - } - } - .padding(.vertical, Length.Padding.half) + siteHStack(site: site) } .buttonStyle(BlogListButtonStyle()) .listRowSeparator(.hidden) @@ -132,6 +134,26 @@ struct BlogListView: View { .listRowBackground(Color.DS.Background.primary) } + private func siteHStack(site: Site) -> some View { + HStack(spacing: 0) { + AvatarsView(style: .single(site.imageURL)) + .padding(.leading, Length.Padding.double) + .padding(.trailing, Length.Padding.split) + + textsVStack(title: site.title, domain: site.domain) + + Spacer() + + if isEditing { + pinIcon( + domain: site.domain + ) + .padding(.trailing, Length.Padding.double) + } + } + .padding(.vertical, Length.Padding.half) + } + private func textsVStack(title: String, domain: String) -> some View { VStack(alignment: .leading, spacing: 0) { Text(title) @@ -160,6 +182,18 @@ struct BlogListView: View { .foregroundStyle(Color.DS.Foreground.secondary) } } + + private var addSiteButtonVStack: some View { + VStack(spacing: Length.Padding.medium) { + Divider() + .background(Color.DS.Foreground.secondary) + DSButton(title: "Add a site", style: .init(emphasis: .primary, size: .large)) { + // Add a site + } + .padding(.horizontal, Length.Padding.medium) + } + .background(Color.DS.Background.primary) + } } private struct BlogListButtonStyle: ButtonStyle { From b872b82a1ac0d10b3ebcb6ffea248ccf5fa76e6a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 22 Mar 2024 17:37:06 +0100 Subject: [PATCH 007/116] Update design system dark theme background colors --- .../Background/backgroundSecondary.colorset/Contents.json | 6 +++--- .../Background/backgroundTertiary.colorset/Contents.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json index 3baf5fafee50..8cea2a23846b 100644 --- a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundSecondary.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x3C", - "green" : "0x3A", - "red" : "0x3A" + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" } }, "idiom" : "universal" diff --git a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json index a09a6f5394e2..ef59d63de8aa 100644 --- a/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json +++ b/Modules/Sources/DesignSystem/Foundation/Colors.xcassets/Foundation/Background/backgroundTertiary.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x2C", - "red" : "0x2C" + "blue" : "0x3C", + "green" : "0x3A", + "red" : "0x3A" } }, "idiom" : "universal" From f7fbc9c9250feb20647cb9c63cf591205eb1e28b Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 22 Mar 2024 17:37:25 +0100 Subject: [PATCH 008/116] Implement persistence for pinned domains --- .../BlogList/BlogListReducer.swift | 32 ++++++++++++ .../Site Picker/BlogList/BlogListView.swift | 37 ++++---------- ...SitePickerViewController+SiteActions.swift | 4 +- .../SitePickerViewController.swift | 42 ++-------------- .../Site Picker/SiteSwitcherReducer.swift | 1 + .../Blog/Site Picker/SiteSwitcherView.swift | 50 +++++++++++++------ 6 files changed, 84 insertions(+), 82 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index e2108afab0d4..435cab4f8b62 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -1,4 +1,10 @@ enum BlogListReducer { + private enum Constants { + static let pinnedDomainsKey = "site_switcher_pinned_domains_key" + static let jsonEncoder = JSONEncoder() + static let jsonDecoder = JSONDecoder() + } + static func pinnedSites( allSites: [BlogListView.Site], pinnedDomains: Set @@ -20,4 +26,30 @@ enum BlogListReducer { ) } } + + static func pinnedDomains( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance() + ) -> Set? { + if let data = repository.object(forKey: Constants.pinnedDomainsKey) as? Data, + let decodedDomains = try? Constants.jsonDecoder.decode(Set.self, from: data) { + return decodedDomains + } + + return nil + } + + static func togglePinnedDomain( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + domain: String + ) { + if var decodedPinnedDomains = pinnedDomains() { + decodedPinnedDomains = decodedPinnedDomains.symmetricDifference([domain]) + let encodedDomain = try? Constants.jsonEncoder.encode(decodedPinnedDomains) + repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) + } else { + var freshPinnedDomains: Set = [domain] + let encodedDomain = try? Constants.jsonEncoder.encode(freshPinnedDomains) + repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) + } + } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index f21050842381..3cfe143425b9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -13,18 +13,17 @@ struct BlogListView: View { } @Binding private var isEditing: Bool - @Binding private var pinnedDomains: Set + @State private var pinnedDomains: Set? private let sites: [Site] private let selectionCallback: ((String) -> Void) init( sites: [Site], - pinnedDomains: Binding>, isEditing: Binding, selectionCallback: @escaping ((String) -> Void) ) { self.sites = sites - self._pinnedDomains = pinnedDomains + self.pinnedDomains = BlogListReducer.pinnedDomains() self._isEditing = isEditing self.selectionCallback = selectionCallback } @@ -39,14 +38,11 @@ struct BlogListView: View { } private var contentVStack: some View { - VStack { - List { - pinnedSection - unPinnedSection - } - .listStyle(.grouped) - addSiteButtonVStack + List { + pinnedSection + unPinnedSection } + .listStyle(.grouped) } private func sectionHeader(title: String) -> some View { @@ -60,7 +56,7 @@ struct BlogListView: View { private var pinnedSection: some View { let pinnedSites = BlogListReducer.pinnedSites( allSites: sites, - pinnedDomains: pinnedDomains + pinnedDomains: pinnedDomains ?? [] ) if !pinnedSites.isEmpty { Section { @@ -81,7 +77,6 @@ struct BlogListView: View { bottom: 0, trailing: Length.Padding.double) ) - } } } @@ -90,7 +85,7 @@ struct BlogListView: View { private var unPinnedSection: some View { let unPinnedSites = BlogListReducer.unPinnedSites( allSites: sites, - pinnedDomains: pinnedDomains + pinnedDomains: pinnedDomains ?? [] ) if !unPinnedSites.isEmpty { Section { @@ -113,7 +108,8 @@ struct BlogListView: View { Button { if isEditing { withAnimation { - pinnedDomains = pinnedDomains.symmetricDifference([site.domain]) + BlogListReducer.togglePinnedDomain(domain: site.domain) + pinnedDomains = BlogListReducer.pinnedDomains() } } else { selectionCallback(site.domain) @@ -172,7 +168,7 @@ struct BlogListView: View { } private func pinIcon(domain: String) -> some View { - if pinnedDomains.contains(domain) { + if pinnedDomains?.contains(domain) == true { Image(systemName: "pin.fill") .imageScale(.small) .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) @@ -183,17 +179,6 @@ struct BlogListView: View { } } - private var addSiteButtonVStack: some View { - VStack(spacing: Length.Padding.medium) { - Divider() - .background(Color.DS.Foreground.secondary) - DSButton(title: "Add a site", style: .init(emphasis: .primary, size: .large)) { - // Add a site - } - .padding(.horizontal, Length.Padding.medium) - } - .background(Color.DS.Background.primary) - } } private struct BlogListButtonStyle: ButtonStyle { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift index b40c0dee66f5..42314560afb0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift @@ -73,7 +73,7 @@ extension SitePickerViewController { // MARK: - Add site - private func addSiteTapped() { + func addSiteTapped() { let canCreateWPComSite = defaultAccount() != nil let canAddSelfHostedSite = AppConfiguration.showAddSelfHostedSiteButton @@ -103,7 +103,7 @@ extension SitePickerViewController { actionSheet.popoverPresentationController?.sourceRect = sourceView.bounds actionSheet.popoverPresentationController?.permittedArrowDirections = .up - parent?.present(actionSheet, animated: true) + present(actionSheet, animated: true) } private func launchSiteCreation() { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 7c0bd62a4717..b524f6cabe86 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -93,42 +93,19 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { showSiteTitleSettings() } -// - (void)configureDataSource -// { -// self.dataSource = [BlogListDataSource new]; -// self.dataSource.shouldShowDisclosureIndicator = NO; -// self.dataSource.shouldHideSelfHostedSites = self.configuration.shouldHideSelfHostedSites; -// self.dataSource.shouldHideBlogsNotSupportingDomains = self.configuration.shouldHideBlogsNotSupportingDomains; -// -// __weak __typeof(self) weakSelf = self; -// self.dataSource.visibilityChanged = ^(Blog *blog, BOOL visible) { -// [weakSelf setVisible:visible forBlog:blog]; -// }; -// self.dataSource.dataChanged = ^{ -// if (weakSelf.visible) { -// [weakSelf dataChanged]; -// } -// }; -// } - func siteSwitcherTapped() { - // Utilize existing DataSource class to fetch blogs. - let config = BlogListConfiguration.defaultConfig - let dataSource = BlogListDataSource() - dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites - dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains - var dismissAction: (() -> Void)? = nil let hostingController = UIHostingController( rootView: SiteSwitcherView( - pinnedDomains: ["https://alpavanoglu.wordpress.com"], selectionCallback: { [weak self] selectedDomain in - guard let selectedBlog = dataSource.filteredBlogs.first(where: { $0.url == selectedDomain }) else { + guard let selectedBlog = SiteSwitcherReducer.allBlogs().first(where: { $0.url == selectedDomain }) else { return } self?.switchToBlog(selectedBlog) // Dismiss hosting controller with completion block dismissAction?() + }, addSiteCallback: { [weak self] in + self?.addSiteTapped() } ) ) @@ -138,19 +115,6 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { } } present(hostingController, animated: true) -// let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) -// -// blogListController.blogSelected = { [weak self] controller, selectedBlog in -// guard let self else { return } -// self.switchToBlog(selectedBlog) -// controller.dismiss(animated: true) { -// self.onBlogListDismiss?() -// } -// } -// -// let navigationController = UINavigationController(rootViewController: blogListController) -// navigationController.modalPresentationStyle = .formSheet -// present(navigationController, animated: true) WPAnalytics.track(.mySiteSiteSwitcherTapped) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift index 1e6f6000c315..1a3a9f46c50d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift @@ -1,5 +1,6 @@ enum SiteSwitcherReducer { static func allBlogs() -> [Blog] { + // Utilize existing DataSource class to fetch blogs. let config = BlogListConfiguration.defaultConfig let dataSource = BlogListDataSource() dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 377338f90c4b..3b8fd1bc4802 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -1,37 +1,45 @@ import SwiftUI +import DesignSystem struct SiteSwitcherView: View { @State private var isEditing: Bool = false - @State private var pinnedDomains: Set private let selectionCallback: ((String) -> Void) + private let addSiteCallback: (() -> Void) - init(pinnedDomains: Set, selectionCallback: @escaping ((String) -> Void)) { - self.pinnedDomains = pinnedDomains + init(selectionCallback: @escaping ((String) -> Void), + addSiteCallback: @escaping (() -> Void)) { self.selectionCallback = selectionCallback + self.addSiteCallback = addSiteCallback } var body: some View { if #available(iOS 16.0, *) { NavigationStack { - BlogListView( - sites: SiteSwitcherReducer.allBlogs().compactMap { - .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) - }, - pinnedDomains: $pinnedDomains, - isEditing: $isEditing, - selectionCallback: selectionCallback - ) - .toolbar { - editButton + VStack { + blogListView + addSiteButtonVStack } - .navigationTitle("Switch Site") - .navigationBarTitleDisplayMode(.inline) } } else { // Fallback on earlier versions } } + private var blogListView: some View { + BlogListView( + sites: SiteSwitcherReducer.allBlogs().compactMap { + .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) + }, + isEditing: $isEditing, + selectionCallback: selectionCallback + ) + .toolbar { + editButton + } + .navigationTitle("Switch Site") + .navigationBarTitleDisplayMode(.inline) + } + private var editButton: some View { Button(action: { isEditing.toggle() @@ -43,4 +51,16 @@ struct SiteSwitcherView: View { ) }) } + + private var addSiteButtonVStack: some View { + VStack(spacing: Length.Padding.medium) { + Divider() + .background(Color.DS.Foreground.secondary) + DSButton(title: "Add a site", style: .init(emphasis: .primary, size: .large)) { + addSiteCallback() + } + .padding(.horizontal, Length.Padding.medium) + } + .background(Color.DS.Background.primary) + } } From 06c63d34c5a72067b6ae949dc5ff06e2c853be23 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 22 Mar 2024 19:24:50 +0100 Subject: [PATCH 009/116] Fix presenting action sheet and flows --- ...SitePickerViewController+SiteActions.swift | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift index 42314560afb0..5e18593e169b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteActions.swift @@ -103,18 +103,30 @@ extension SitePickerViewController { actionSheet.popoverPresentationController?.sourceRect = sourceView.bounds actionSheet.popoverPresentationController?.permittedArrowDirections = .up - present(actionSheet, animated: true) + presentedViewController?.present(actionSheet, animated: true) } private func launchSiteCreation() { - guard let parent = parent as? MySiteViewController else { - return - } - parent.launchSiteCreation(source: "my_site") + let viewController = presentedViewController ?? self + let source = "my_site" + JetpackFeaturesRemovalCoordinator.presentSiteCreationOverlayIfNeeded(in: viewController, source: source, onDidDismiss: { + guard JetpackFeaturesRemovalCoordinator.siteCreationPhase() != .two else { + return + } + + // Display site creation flow if not in phase two + let wizardLauncher = SiteCreationWizardLauncher() + guard let wizard = wizardLauncher.ui else { + return + } + RootViewCoordinator.shared.isSiteCreationActive = true + viewController.present(wizard, animated: true) + SiteCreationAnalyticsHelper.trackSiteCreationAccessed(source: source) + }) } private func launchLoginForSelfHostedSite() { - WordPressAuthenticator.showLoginForSelfHostedSite(self) + WordPressAuthenticator.showLoginForSelfHostedSite(presentedViewController ?? self) } // MARK: - Personalize home From 27dd4e692aa64a47a501dd56f68a8eee4453b641 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Tue, 26 Mar 2024 17:25:54 +0100 Subject: [PATCH 010/116] Add search for older versions --- .../Site Picker/BlogList/BlogListView.swift | 33 +++++++-------- .../Blog/Site Picker/SiteSwitcherView.swift | 40 ++++++++++++++++--- .../Coordinators/MySitesCoordinator.swift | 5 --- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 3cfe143425b9..7640fc2b588a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -13,6 +13,7 @@ struct BlogListView: View { } @Binding private var isEditing: Bool + @Binding private var isSearching: Bool @State private var pinnedDomains: Set? private let sites: [Site] private let selectionCallback: ((String) -> Void) @@ -20,11 +21,13 @@ struct BlogListView: View { init( sites: [Site], isEditing: Binding, + isSearching: Binding, selectionCallback: @escaping ((String) -> Void) ) { self.sites = sites self.pinnedDomains = BlogListReducer.pinnedDomains() self._isEditing = isEditing + self._isSearching = isSearching self.selectionCallback = selectionCallback } @@ -39,8 +42,14 @@ struct BlogListView: View { private var contentVStack: some View { List { - pinnedSection - unPinnedSection + if isSearching { + ForEach(sites, id: \.domain) { site in + siteButton(site: site) + } + } else { + pinnedSection + unPinnedSection + } } .listStyle(.grouped) } @@ -60,13 +69,9 @@ struct BlogListView: View { ) if !pinnedSites.isEmpty { Section { - ForEach( - pinnedSites, - id: \.domain) { site in - siteButton( - site: site - ) - } + ForEach(pinnedSites, id: \.domain) { site in + siteButton(site: site) + } } header: { sectionHeader( title: "Pinned sites" @@ -89,13 +94,9 @@ struct BlogListView: View { ) if !unPinnedSites.isEmpty { Section { - ForEach( - unPinnedSites, - id: \.domain) { site in - siteButton( - site: site - ) - } + ForEach(unPinnedSites, id: \.domain) { site in + siteButton(site: site) + } } header: { sectionHeader( title: "All sites" diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 3b8fd1bc4802..8f458fdfafac 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -5,6 +5,24 @@ struct SiteSwitcherView: View { @State private var isEditing: Bool = false private let selectionCallback: ((String) -> Void) private let addSiteCallback: (() -> Void) + @State private var searchText = "" + @State private var isSearching = false + + var searchResults: [BlogListView.Site] { + if searchText.isEmpty { + return SiteSwitcherReducer.allBlogs().compactMap { + .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) + } + } else { + return SiteSwitcherReducer.allBlogs() + .filter { + $0.url!.lowercased().contains(searchText.lowercased()) || $0.title!.lowercased().contains(searchText.lowercased()) + } + .compactMap { + .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) + } + } + } init(selectionCallback: @escaping ((String) -> Void), addSiteCallback: @escaping (() -> Void)) { @@ -13,24 +31,34 @@ struct SiteSwitcherView: View { } var body: some View { - if #available(iOS 16.0, *) { + if #available(iOS 17.0, *) { NavigationStack { VStack { blogListView - addSiteButtonVStack + if !isSearching { + addSiteButtonVStack + } } } + .searchable(text: $searchText, isPresented: $isSearching) } else { - // Fallback on earlier versions + NavigationView { + VStack { + blogListView + if !isSearching { + addSiteButtonVStack + } + } + } + .searchable(text: $searchText) } } private var blogListView: some View { BlogListView( - sites: SiteSwitcherReducer.allBlogs().compactMap { - .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) - }, + sites: searchResults, isEditing: $isEditing, + isSearching: $isSearching, selectionCallback: selectionCallback ) .toolbar { diff --git a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift index 0730e40c01a4..52aeb1f3afed 100644 --- a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift @@ -85,11 +85,6 @@ class MySitesCoordinator: NSObject { return navigationController }() - @objc - private(set) lazy var blogListViewController: BlogListViewController = { - BlogListViewController(configuration: .defaultConfig, meScenePresenter: self.meScenePresenter) - }() - private lazy var mySiteViewController: MySiteViewController = { makeMySiteViewController() }() From 2661e6b69d6b159fd66db4f474c8bf6b66570f7a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Tue, 2 Apr 2024 19:36:42 +0200 Subject: [PATCH 011/116] Add recents to `BlogListView` --- .../BlogList/BlogListReducer.swift | 65 +++++++++++---- .../Site Picker/BlogList/BlogListView.swift | 82 +++++++++++-------- .../Site Picker/SiteSwitcherReducer.swift | 11 ++- .../Blog/Site Picker/SiteSwitcherView.swift | 9 +- 4 files changed, 112 insertions(+), 55 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index 435cab4f8b62..0c93df074bce 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -1,8 +1,10 @@ enum BlogListReducer { private enum Constants { static let pinnedDomainsKey = "site_switcher_pinned_domains_key" + static let recentDomainsKey = "site_switcher_recent_domains_key" static let jsonEncoder = JSONEncoder() static let jsonDecoder = JSONDecoder() + static let recentsTotalLimit = 8 } static func pinnedSites( @@ -16,40 +18,73 @@ enum BlogListReducer { } } - static func unPinnedSites( + static func allSites( allSites: [BlogListView.Site], - pinnedDomains: Set + pinnedDomains: Set, + recentDomains: [String] ) -> [BlogListView.Site] { allSites.filter { - !pinnedDomains.contains( - $0.domain - ) + !pinnedDomains.contains($0.domain) && !recentDomains.contains($0.domain) + } + } + + static func recentSites( + allSites: [BlogListView.Site], + recentDomains: [String] + ) -> [BlogListView.Site] { + allSites.filter { + recentDomains.contains($0.domain) } } static func pinnedDomains( repository: UserPersistentRepository = UserPersistentStoreFactory.instance() - ) -> Set? { + ) -> Set { if let data = repository.object(forKey: Constants.pinnedDomainsKey) as? Data, let decodedDomains = try? Constants.jsonDecoder.decode(Set.self, from: data) { return decodedDomains } - return nil + return [] } static func togglePinnedDomain( repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), domain: String ) { - if var decodedPinnedDomains = pinnedDomains() { - decodedPinnedDomains = decodedPinnedDomains.symmetricDifference([domain]) - let encodedDomain = try? Constants.jsonEncoder.encode(decodedPinnedDomains) - repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) - } else { - var freshPinnedDomains: Set = [domain] - let encodedDomain = try? Constants.jsonEncoder.encode(freshPinnedDomains) - repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) + let tempPinnedDomains = pinnedDomains().symmetricDifference([domain]) + let encodedDomain = try? Constants.jsonEncoder.encode(tempPinnedDomains) + repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) + } + + static func recentDomains( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance() + ) -> [String] { + if let data = repository.object(forKey: Constants.recentDomainsKey) as? Data, + let decodedDomains = try? Constants.jsonDecoder.decode([String].self, from: data) { + return decodedDomains + } + + return [] + } + + static func didSelectDomain( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + domain: String + ) { + guard !pinnedDomains().contains(domain) else { + return + } + + var tempRecentDomains = recentDomains() + tempRecentDomains.removeAll { $0 == domain } + tempRecentDomains.insert(domain, at: 0) + + if tempRecentDomains.count > Constants.recentsTotalLimit { + tempRecentDomains = tempRecentDomains.dropLast(tempRecentDomains.count - Constants.recentsTotalLimit) } + + let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) + repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 7640fc2b588a..be0fbd49ee01 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -14,18 +14,23 @@ struct BlogListView: View { @Binding private var isEditing: Bool @Binding private var isSearching: Bool - @State private var pinnedDomains: Set? + @State private var pinnedDomains: Set + @State private var recentDomains: [String] private let sites: [Site] + private let currentDomain: String? private let selectionCallback: ((String) -> Void) init( sites: [Site], + currentDomain: String?, isEditing: Binding, isSearching: Binding, selectionCallback: @escaping ((String) -> Void) ) { self.sites = sites + self.currentDomain = currentDomain self.pinnedDomains = BlogListReducer.pinnedDomains() + self.recentDomains = BlogListReducer.recentDomains() self._isEditing = isEditing self._isSearching = isSearching self.selectionCallback = selectionCallback @@ -44,11 +49,12 @@ struct BlogListView: View { List { if isSearching { ForEach(sites, id: \.domain) { site in - siteButton(site: site) + siteButton(site: site) } } else { pinnedSection - unPinnedSection + recentsSection + allSitesSection } } .listStyle(.grouped) @@ -65,7 +71,7 @@ struct BlogListView: View { private var pinnedSection: some View { let pinnedSites = BlogListReducer.pinnedSites( allSites: sites, - pinnedDomains: pinnedDomains ?? [] + pinnedDomains: pinnedDomains ) if !pinnedSites.isEmpty { Section { @@ -77,24 +83,25 @@ struct BlogListView: View { title: "Pinned sites" ) .listRowInsets(EdgeInsets( - top: Length.Padding.medium, - leading: Length.Padding.double, + top: .DS.Padding.medium, + leading: .DS.Padding.double, bottom: 0, - trailing: Length.Padding.double) + trailing: .DS.Padding.double) ) } } } @ViewBuilder - private var unPinnedSection: some View { - let unPinnedSites = BlogListReducer.unPinnedSites( + private var allSitesSection: some View { + let allSites = BlogListReducer.allSites( allSites: sites, - pinnedDomains: pinnedDomains ?? [] + pinnedDomains: pinnedDomains, + recentDomains: recentDomains ) - if !unPinnedSites.isEmpty { + if !allSites.isEmpty { Section { - ForEach(unPinnedSites, id: \.domain) { site in + ForEach(allSites, id: \.domain) { site in siteButton(site: site) } } header: { @@ -105,6 +112,25 @@ struct BlogListView: View { } } + @ViewBuilder + private var recentsSection: some View { + let recentSites = BlogListReducer.recentSites( + allSites: sites, + recentDomains: recentDomains + ) + if !recentSites.isEmpty { + Section { + ForEach(recentSites, id: \.domain) { site in + siteButton(site: site) + } + } header: { + sectionHeader( + title: "Recent sites" + ) + } + } + } + private func siteButton(site: Site) -> some View { Button { if isEditing { @@ -113,29 +139,24 @@ struct BlogListView: View { pinnedDomains = BlogListReducer.pinnedDomains() } } else { + BlogListReducer.didSelectDomain(domain: site.domain) selectionCallback(site.domain) } } label: { siteHStack(site: site) } - .buttonStyle(BlogListButtonStyle()) .listRowSeparator(.hidden) - .listRowInsets( - .init( - top: Length.Padding.single, - leading: 0, - bottom: Length.Padding.single, - trailing: 0 - ) + .listRowBackground( + currentDomain == site.domain + ? Color.DS.Background.secondary + : Color.DS.Background.primary ) - .listRowBackground(Color.DS.Background.primary) } private func siteHStack(site: Site) -> some View { HStack(spacing: 0) { AvatarsView(style: .single(site.imageURL)) - .padding(.leading, Length.Padding.double) - .padding(.trailing, Length.Padding.split) + .padding(.trailing, .DS.Padding.split) textsVStack(title: site.title, domain: site.domain) @@ -145,10 +166,10 @@ struct BlogListView: View { pinIcon( domain: site.domain ) - .padding(.trailing, Length.Padding.double) + .padding(.trailing, .DS.Padding.double) } } - .padding(.vertical, Length.Padding.half) +// .padding(.vertical, .DS.Padding.half) } private func textsVStack(title: String, domain: String) -> some View { @@ -164,12 +185,12 @@ struct BlogListView: View { .foregroundStyle(Color.DS.Foreground.secondary) .layoutPriority(2) .lineLimit(1) - .padding(.top, Length.Padding.half) + .padding(.top, .DS.Padding.half) } } private func pinIcon(domain: String) -> some View { - if pinnedDomains?.contains(domain) == true { + if pinnedDomains.contains(domain) == true { Image(systemName: "pin.fill") .imageScale(.small) .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) @@ -181,10 +202,3 @@ struct BlogListView: View { } } - -private struct BlogListButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .background(configuration.isPressed ? Color.DS.Background.secondary : Color.DS.Background.primary) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift index 1a3a9f46c50d..c283ee7ea4a9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift @@ -1,11 +1,18 @@ enum SiteSwitcherReducer { static func allBlogs() -> [Blog] { + return dataSource().filteredBlogs + } + + static func selectedBlog() -> Blog? { + return RootViewCoordinator.sharedPresenter.currentOrLastBlog() + } + + private static func dataSource() -> BlogListDataSource { // Utilize existing DataSource class to fetch blogs. let config = BlogListConfiguration.defaultConfig let dataSource = BlogListDataSource() dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains - - return dataSource.filteredBlogs + return dataSource } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 8f458fdfafac..f05f38d8ef10 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -8,7 +8,7 @@ struct SiteSwitcherView: View { @State private var searchText = "" @State private var isSearching = false - var searchResults: [BlogListView.Site] { + var sites: [BlogListView.Site] { if searchText.isEmpty { return SiteSwitcherReducer.allBlogs().compactMap { .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) @@ -56,7 +56,8 @@ struct SiteSwitcherView: View { private var blogListView: some View { BlogListView( - sites: searchResults, + sites: sites, + currentDomain: SiteSwitcherReducer.selectedBlog()?.url, isEditing: $isEditing, isSearching: $isSearching, selectionCallback: selectionCallback @@ -81,13 +82,13 @@ struct SiteSwitcherView: View { } private var addSiteButtonVStack: some View { - VStack(spacing: Length.Padding.medium) { + VStack(spacing: .DS.Padding.medium) { Divider() .background(Color.DS.Foreground.secondary) DSButton(title: "Add a site", style: .init(emphasis: .primary, size: .large)) { addSiteCallback() } - .padding(.horizontal, Length.Padding.medium) + .padding(.horizontal, .DS.Padding.medium) } .background(Color.DS.Background.primary) } From d0d7303b78235ef799b46aa7f19a845f4c3efb1c Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Tue, 2 Apr 2024 21:39:43 +0200 Subject: [PATCH 012/116] Update search visibility --- .../ViewRelated/Blog/Site Picker/SiteSwitcherView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index f05f38d8ef10..ab7646c5f1ea 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -40,7 +40,13 @@ struct SiteSwitcherView: View { } } } - .searchable(text: $searchText, isPresented: $isSearching) + .searchable( + text: $searchText, + isPresented: $isSearching, + placement: .navigationBarDrawer( + displayMode: .always + ) + ) } else { NavigationView { VStack { From 62466609a86c56f394085fd39befbc0ec5a0bc99 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 13:00:47 +0200 Subject: [PATCH 013/116] Make recents-pinned mutually exclusive --- .../BlogList/BlogListReducer.swift | 22 +++++++++++++++++-- .../Site Picker/BlogList/BlogListView.swift | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index 0c93df074bce..d905645a890c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -32,9 +32,13 @@ enum BlogListReducer { allSites: [BlogListView.Site], recentDomains: [String] ) -> [BlogListView.Site] { - allSites.filter { - recentDomains.contains($0.domain) + var sites: [BlogListView.Site] = [] + for domain in recentDomains { + if let recentSite = allSites.first(where: { $0.domain == domain }) { + sites.append(recentSite) + } } + return sites } static func pinnedDomains( @@ -53,6 +57,20 @@ enum BlogListReducer { domain: String ) { let tempPinnedDomains = pinnedDomains().symmetricDifference([domain]) + + if tempPinnedDomains.contains(domain) { + var tempRecentDomains = recentDomains() + let beforeRemoveCount = tempRecentDomains.count + tempRecentDomains.removeAll { recentDomain in + recentDomain == domain + } + + if tempRecentDomains.count != beforeRemoveCount { + let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) + repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) + } + } + let encodedDomain = try? Constants.jsonEncoder.encode(tempPinnedDomains) repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index be0fbd49ee01..9203505931cb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -137,6 +137,7 @@ struct BlogListView: View { withAnimation { BlogListReducer.togglePinnedDomain(domain: site.domain) pinnedDomains = BlogListReducer.pinnedDomains() + recentDomains = BlogListReducer.recentDomains() } } else { BlogListReducer.didSelectDomain(domain: site.domain) @@ -169,7 +170,6 @@ struct BlogListView: View { .padding(.trailing, .DS.Padding.double) } } -// .padding(.vertical, .DS.Padding.half) } private func textsVStack(title: String, domain: String) -> some View { From dd54ab72eef5942a8dba64676614456ad3e9bca3 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 14:23:23 +0200 Subject: [PATCH 014/116] Update return type from Set to Array to retain order --- .../BlogList/BlogListReducer.swift | 52 +++++++++++++++---- .../Site Picker/BlogList/BlogListView.swift | 8 +-- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index d905645a890c..e77c70841d05 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -1,5 +1,10 @@ enum BlogListReducer { - private enum Constants { + struct PinnedDomain: Codable, Equatable { + let domain: String + let isRecent: Bool + } + + /*private*/ enum Constants { static let pinnedDomainsKey = "site_switcher_pinned_domains_key" static let recentDomainsKey = "site_switcher_recent_domains_key" static let jsonEncoder = JSONEncoder() @@ -7,9 +12,17 @@ enum BlogListReducer { static let recentsTotalLimit = 8 } + static func syncCachedValues( + allSites: [BlogListView.Site], + pinnedDomains: [String], + recentDomains: [String] + ) { + + } + static func pinnedSites( allSites: [BlogListView.Site], - pinnedDomains: Set + pinnedDomains: [String] ) -> [BlogListView.Site] { allSites.filter { pinnedDomains.contains( @@ -20,7 +33,7 @@ enum BlogListReducer { static func allSites( allSites: [BlogListView.Site], - pinnedDomains: Set, + pinnedDomains: [String], recentDomains: [String] ) -> [BlogListView.Site] { allSites.filter { @@ -43,32 +56,49 @@ enum BlogListReducer { static func pinnedDomains( repository: UserPersistentRepository = UserPersistentStoreFactory.instance() - ) -> Set { + ) -> [PinnedDomain] { if let data = repository.object(forKey: Constants.pinnedDomainsKey) as? Data, - let decodedDomains = try? Constants.jsonDecoder.decode(Set.self, from: data) { - return decodedDomains + let decodedDomains = try? Constants.jsonDecoder.decode([PinnedDomain].self, from: data) { + return decodedDomains } return [] } - static func togglePinnedDomain( + static func toggleDomainPin( repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), domain: String ) { - let tempPinnedDomains = pinnedDomains().symmetricDifference([domain]) + var tempPinnedDomains = pinnedDomains() + let existingPinnedDomain = tempPinnedDomains.first { pinnedDomain in + pinnedDomain.domain == domain + } - if tempPinnedDomains.contains(domain) { + if let existingPinnedDomain { + // Pinned -> All/Recent + if existingPinnedDomain.isRecent { + var tempRecentDomains = recentDomains() + tempRecentDomains.insert(domain, at: 0) + + let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) + repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) + } + tempPinnedDomains.removeAll(where: { $0 == existingPinnedDomain }) + } else { + // All/Recent -> Pinned var tempRecentDomains = recentDomains() let beforeRemoveCount = tempRecentDomains.count tempRecentDomains.removeAll { recentDomain in recentDomain == domain } - if tempRecentDomains.count != beforeRemoveCount { + let didRemoveFromRecent = tempRecentDomains.count != beforeRemoveCount + if didRemoveFromRecent { let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) } + + tempPinnedDomains.append(.init(domain: domain, isRecent: didRemoveFromRecent)) } let encodedDomain = try? Constants.jsonEncoder.encode(tempPinnedDomains) @@ -90,7 +120,7 @@ enum BlogListReducer { repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), domain: String ) { - guard !pinnedDomains().contains(domain) else { + guard !pinnedDomains().compactMap({ $0.domain }).contains(domain) else { return } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 9203505931cb..dd6b1ae2f9f4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -14,7 +14,7 @@ struct BlogListView: View { @Binding private var isEditing: Bool @Binding private var isSearching: Bool - @State private var pinnedDomains: Set + @State private var pinnedDomains: [String] @State private var recentDomains: [String] private let sites: [Site] private let currentDomain: String? @@ -29,7 +29,7 @@ struct BlogListView: View { ) { self.sites = sites self.currentDomain = currentDomain - self.pinnedDomains = BlogListReducer.pinnedDomains() + self.pinnedDomains = BlogListReducer.pinnedDomains().compactMap({ $0.domain }) self.recentDomains = BlogListReducer.recentDomains() self._isEditing = isEditing self._isSearching = isSearching @@ -135,8 +135,8 @@ struct BlogListView: View { Button { if isEditing { withAnimation { - BlogListReducer.togglePinnedDomain(domain: site.domain) - pinnedDomains = BlogListReducer.pinnedDomains() + BlogListReducer.toggleDomainPin(domain: site.domain) + pinnedDomains = BlogListReducer.pinnedDomains().compactMap({ $0.domain }) recentDomains = BlogListReducer.recentDomains() } } else { From df680c7d9f6bf0a14a6ff62ba6d31d6530013b27 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 16:24:08 +0200 Subject: [PATCH 015/116] Add `BlogListReducerTests` --- .../BlogList/BlogListReducer.swift | 14 ++-- WordPress/WordPress.xcodeproj/project.pbxproj | 12 +++ .../WordPressTest/BlogListReducerTests.swift | 83 +++++++++++++++++++ 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 WordPress/WordPressTest/BlogListReducerTests.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index e77c70841d05..2c83201d9177 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -4,7 +4,7 @@ enum BlogListReducer { let isRecent: Bool } - /*private*/ enum Constants { + enum Constants { static let pinnedDomainsKey = "site_switcher_pinned_domains_key" static let recentDomainsKey = "site_switcher_recent_domains_key" static let jsonEncoder = JSONEncoder() @@ -69,7 +69,7 @@ enum BlogListReducer { repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), domain: String ) { - var tempPinnedDomains = pinnedDomains() + var tempPinnedDomains = pinnedDomains(repository: repository) let existingPinnedDomain = tempPinnedDomains.first { pinnedDomain in pinnedDomain.domain == domain } @@ -77,7 +77,7 @@ enum BlogListReducer { if let existingPinnedDomain { // Pinned -> All/Recent if existingPinnedDomain.isRecent { - var tempRecentDomains = recentDomains() + var tempRecentDomains = recentDomains(repository: repository) tempRecentDomains.insert(domain, at: 0) let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) @@ -86,14 +86,14 @@ enum BlogListReducer { tempPinnedDomains.removeAll(where: { $0 == existingPinnedDomain }) } else { // All/Recent -> Pinned - var tempRecentDomains = recentDomains() + var tempRecentDomains = recentDomains(repository: repository) let beforeRemoveCount = tempRecentDomains.count tempRecentDomains.removeAll { recentDomain in recentDomain == domain } let didRemoveFromRecent = tempRecentDomains.count != beforeRemoveCount - if didRemoveFromRecent { + if didRemoveFromRecent { let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) } @@ -120,11 +120,11 @@ enum BlogListReducer { repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), domain: String ) { - guard !pinnedDomains().compactMap({ $0.domain }).contains(domain) else { + guard !pinnedDomains(repository: repository).compactMap({ $0.domain }).contains(domain) else { return } - var tempRecentDomains = recentDomains() + var tempRecentDomains = recentDomains(repository: repository) tempRecentDomains.removeAll { $0 == domain } tempRecentDomains.insert(domain, at: 0) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 99c4e96da3c3..85431eb3596e 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -344,6 +344,7 @@ 088134FF2A56C5240027C086 /* CompliancePopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */; }; 0885A3671E837AFE00619B4D /* URLIncrementalFilenameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */; }; 088B89891DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */; }; + 088CAD4E2BBD8223005996DE /* BlogListReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */; }; 088CC594282BEC41007B9421 /* TooltipPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CC593282BEC41007B9421 /* TooltipPresenter.swift */; }; 089D4EBE2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json in Resources */ = {isa = PBXBuildFile; fileRef = 089D4EBD2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json */; }; 08A250F828D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */; }; @@ -6113,6 +6114,7 @@ 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverViewModelTests.swift; sourceTree = ""; }; 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLIncrementalFilenameTests.swift; sourceTree = ""; }; 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPostCardContentLabel.swift; sourceTree = ""; }; + 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogListReducerTests.swift; sourceTree = ""; }; 088CC593282BEC41007B9421 /* TooltipPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipPresenter.swift; sourceTree = ""; }; 089D4EBD2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "notifications-like-multiple-avatar.json"; sourceTree = ""; }; 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailInfoViewController.swift; sourceTree = ""; }; @@ -10260,6 +10262,14 @@ path = EUUSCompliance; sourceTree = ""; }; + 088CAD4C2BBD81ED005996DE /* SiteSwitcher */ = { + isa = PBXGroup; + children = ( + 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */, + ); + name = SiteSwitcher; + sourceTree = ""; + }; 089F087F1CE25D30009909F2 /* Controllers */ = { isa = PBXGroup; children = ( @@ -16883,6 +16893,7 @@ 436D55EE2115CB3D00CEAA33 /* RegisterDomain */, B5AEEC7E1ACAD088008BF2A4 /* Services */, 73178C2021BEE09300E37C9A /* SiteCreation */, + 088CAD4C2BBD81ED005996DE /* SiteSwitcher */, 40E7FEC32211DF490032834E /* Stats */, D88A6490208D79F1008AE9BC /* Stock Photos */, 0186358F2A85374E00915532 /* Support */, @@ -23856,6 +23867,7 @@ E1EBC3731C118ED200F638E0 /* ImmuTableTest.swift in Sources */, F5C00EAE242179780047846F /* WeekdaysHeaderViewTests.swift in Sources */, 24C69AC22612467C00312D9A /* UserSettingsTestsObjc.m in Sources */, + 088CAD4E2BBD8223005996DE /* BlogListReducerTests.swift in Sources */, E1AB5A091E0BF31E00574B4E /* ArrayTests.swift in Sources */, 570B037722F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift in Sources */, C8567496243F3D37001A995E /* TenorResultsPageTests.swift in Sources */, diff --git a/WordPress/WordPressTest/BlogListReducerTests.swift b/WordPress/WordPressTest/BlogListReducerTests.swift new file mode 100644 index 000000000000..ce0aecea3503 --- /dev/null +++ b/WordPress/WordPressTest/BlogListReducerTests.swift @@ -0,0 +1,83 @@ +import XCTest +@testable import WordPress + +final class BlogListReducerTests: XCTestCase { + private enum Constants { + static let jsonEncoder = JSONEncoder() + static let jsonDecoder = JSONDecoder() + } + + private let repository = InMemoryUserDefaults() + private static let suiteName = "TestSuite_BlogListReducerTests" + + // MARK: - Helper Methods + private func encodeAndStore(_ object: T, forKey key: String) { + if let data = try? Constants.jsonEncoder.encode(object) { + repository.set(data, forKey: key) + } + } + + // MARK: - Tests for Retrieval Functions + func testPinnedDomainsWithNoData() { + XCTAssertTrue(BlogListReducer.pinnedDomains(repository: repository).isEmpty) + } + + func testPinnedDomainsWithValidData() { + let pinnedDomains = [BlogListReducer.PinnedDomain(domain: "example.com", isRecent: true)] + encodeAndStore(pinnedDomains, forKey: BlogListReducer.Constants.pinnedDomainsKey) + + let result = BlogListReducer.pinnedDomains(repository: repository) + XCTAssertEqual(result, pinnedDomains) + } + + func testRecentDomainsWithNoData() { + XCTAssertTrue(BlogListReducer.recentDomains(repository: repository).isEmpty) + } + + func testRecentDomainsWithValidData() { + let recentDomains = ["example.com"] + encodeAndStore(recentDomains, forKey: BlogListReducer.Constants.recentDomainsKey) + + let result = BlogListReducer.recentDomains(repository: repository) + XCTAssertEqual(result, recentDomains) + } + + // MARK: - Tests for Domain Toggling + func testToggleDomainPinAdd() { + let domain = "example.com" + BlogListReducer.toggleDomainPin(repository: repository, domain: domain) + let result = BlogListReducer.pinnedDomains(repository: repository) + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.domain, domain) + } + + func testToggleDomainPinRemove() { + let domain = "example.com" + encodeAndStore([BlogListReducer.PinnedDomain(domain: domain, isRecent: false)], forKey: BlogListReducer.Constants.pinnedDomainsKey) + BlogListReducer.toggleDomainPin(repository: repository, domain: domain) + XCTAssertTrue(BlogListReducer.pinnedDomains(repository: repository).isEmpty) + } + + // MARK: - Tests for Domain Selection + func testDidSelectDomainAlreadyPinned() { + let domain = "example.com" + encodeAndStore([BlogListReducer.PinnedDomain(domain: domain, isRecent: false)], forKey: BlogListReducer.Constants.pinnedDomainsKey) + BlogListReducer.didSelectDomain(repository: repository, domain: domain) + // Ensure no change to recent domains if the domain is already pinned + XCTAssertTrue(BlogListReducer.recentDomains(repository: repository).isEmpty) + } + + func testDidSelectDomainAddToRecent() { + let domain = "example.com" + BlogListReducer.didSelectDomain(repository: repository, domain: domain) + let result = BlogListReducer.recentDomains(repository: repository) + XCTAssertEqual(result, [domain]) + } + + func testDidSelectDomainRespectsRecentsLimit() { + let domains = (1...10).map { "example\($0).com" } + domains.forEach { BlogListReducer.didSelectDomain(repository: repository, domain: $0) } + let result = BlogListReducer.recentDomains(repository: repository) + XCTAssertEqual(result.count, BlogListReducer.Constants.recentsTotalLimit) + } +} From 8745fc47a8272084c0d630094f32b82da7bbd7e3 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 17:12:31 +0200 Subject: [PATCH 016/116] Add more retrieval tests --- .../Site Picker/BlogList/BlogListView.swift | 2 +- .../WordPressTest/BlogListReducerTests.swift | 109 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index dd6b1ae2f9f4..05d0faae820d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -6,7 +6,7 @@ struct BlogListView: View { static let imageDiameter: CGFloat = 40 } - struct Site { + struct Site: Equatable { let title: String let domain: String let imageURL: URL? diff --git a/WordPress/WordPressTest/BlogListReducerTests.swift b/WordPress/WordPressTest/BlogListReducerTests.swift index ce0aecea3503..5bc103a477ed 100644 --- a/WordPress/WordPressTest/BlogListReducerTests.swift +++ b/WordPress/WordPressTest/BlogListReducerTests.swift @@ -42,6 +42,99 @@ final class BlogListReducerTests: XCTestCase { XCTAssertEqual(result, recentDomains) } + func testPinnedSites() { + let sites: [BlogListView.Site] = [ + .init( + title: "1", + domain: "example1.com", + imageURL: nil + ), + .init( + title: "2", + domain: "example2.com", + imageURL: nil + ), + .init( + title: "3", + domain: "example3.com", + imageURL: nil + ) + ] + let pinnedDomains: [String] = ["example1.com", "wordpress.com"] + let result = BlogListReducer.pinnedSites(allSites: sites, pinnedDomains: pinnedDomains) + + XCTAssertEqual( + result, + [ + BlogListView.Site( + title: "1", + domain: "example1.com", + imageURL: nil + ) + ] + ) + } + + func testAllSitesExcludesPinnedAndRecent() { + let sites: [BlogListView.Site] = [ + .init( + title: "1", + domain: "example1.com", + imageURL: nil + ), + .init( + title: "2", + domain: "example2.com", + imageURL: nil + ), + .init( + title: "3", + domain: "example3.com", + imageURL: nil + ) + ] + + let result = BlogListReducer.allSites( + allSites: sites, + pinnedDomains: ["example2.com"], + recentDomains: ["example3.com"] + ) + + XCTAssertEqual(result, [BlogListView.Site(title: "1", domain: "example1.com", imageURL: nil)]) + } + + func testRecentSites() { + let sites: [BlogListView.Site] = [ + .init( + title: "1", + domain: "example1.com", + imageURL: nil + ), + .init( + title: "2", + domain: "example2.com", + imageURL: nil + ), + .init( + title: "3", + domain: "example3.com", + imageURL: nil + ) + ] + + let recentDomains = ["example2.com", "example1.com"] + + let result = BlogListReducer.recentSites(allSites: sites, recentDomains: recentDomains) + + XCTAssertEqual( + result, + [ + .init(title: "2", domain: "example2.com", imageURL: nil), + .init(title: "1", domain: "example1.com", imageURL: nil) + ] + ) + } + // MARK: - Tests for Domain Toggling func testToggleDomainPinAdd() { let domain = "example.com" @@ -53,7 +146,12 @@ final class BlogListReducerTests: XCTestCase { func testToggleDomainPinRemove() { let domain = "example.com" - encodeAndStore([BlogListReducer.PinnedDomain(domain: domain, isRecent: false)], forKey: BlogListReducer.Constants.pinnedDomainsKey) + encodeAndStore( + [ + BlogListReducer.PinnedDomain(domain: domain, isRecent: false) + ], + forKey: BlogListReducer.Constants.pinnedDomainsKey + ) BlogListReducer.toggleDomainPin(repository: repository, domain: domain) XCTAssertTrue(BlogListReducer.pinnedDomains(repository: repository).isEmpty) } @@ -61,7 +159,12 @@ final class BlogListReducerTests: XCTestCase { // MARK: - Tests for Domain Selection func testDidSelectDomainAlreadyPinned() { let domain = "example.com" - encodeAndStore([BlogListReducer.PinnedDomain(domain: domain, isRecent: false)], forKey: BlogListReducer.Constants.pinnedDomainsKey) + encodeAndStore( + [ + BlogListReducer.PinnedDomain(domain: domain, isRecent: false) + ], + forKey: BlogListReducer.Constants.pinnedDomainsKey + ) BlogListReducer.didSelectDomain(repository: repository, domain: domain) // Ensure no change to recent domains if the domain is already pinned XCTAssertTrue(BlogListReducer.recentDomains(repository: repository).isEmpty) @@ -80,4 +183,6 @@ final class BlogListReducerTests: XCTestCase { let result = BlogListReducer.recentDomains(repository: repository) XCTAssertEqual(result.count, BlogListReducer.Constants.recentsTotalLimit) } + + } From 01113c102ed55b1c32ddeaac550d44723815f61f Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 18:05:18 +0200 Subject: [PATCH 017/116] Add syncing mechanism --- .../BlogList/BlogListReducer.swift | 51 +++++++++++++++++-- .../Site Picker/BlogList/BlogListView.swift | 5 +- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index 2c83201d9177..de3855a53a13 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -13,11 +13,56 @@ enum BlogListReducer { } static func syncCachedValues( - allSites: [BlogListView.Site], - pinnedDomains: [String], - recentDomains: [String] + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + allSites: [BlogListView.Site] + ) { + syncPinnedDomains(repository: repository, allSites: allSites) + syncRecentDomains(repository: repository, allSites: allSites) + } + + private static func syncPinnedDomains( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + allSites: [BlogListView.Site] ) { + var tempPinnedDomains = pinnedDomains(repository: repository) + let initialPinnedDomainsCount = tempPinnedDomains.count + + for pinnedDomain in tempPinnedDomains { + if !allSites.compactMap({ $0.domain }).contains(pinnedDomain.domain) { + let beforeRemoveCount = tempPinnedDomains.count + tempPinnedDomains.removeAll { current in + current.domain == pinnedDomain.domain + } + + if initialPinnedDomainsCount != tempPinnedDomains.count { + let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempPinnedDomains) + repository.set(encodedRecentDomains, forKey: Constants.pinnedDomainsKey) + } + } + } + } + + private static func syncRecentDomains( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + allSites: [BlogListView.Site] + ) { + var tempRecentDomains = recentDomains(repository: repository) + let initialRecentDomainsCount = tempRecentDomains.count + for recentDomain in tempRecentDomains { + if !allSites.compactMap({ $0.domain }).contains(recentDomain) { + let beforeRemoveCount = tempRecentDomains.count + + tempRecentDomains.removeAll { current in + current == recentDomain + } + + if initialRecentDomainsCount != tempRecentDomains.count { + let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) + repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) + } + } + } } static func pinnedSites( diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 05d0faae820d..e222cd116d96 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -58,6 +58,9 @@ struct BlogListView: View { } } .listStyle(.grouped) + .task { + BlogListReducer.syncCachedValues(allSites: sites) + } } private func sectionHeader(title: String) -> some View { @@ -85,7 +88,7 @@ struct BlogListView: View { .listRowInsets(EdgeInsets( top: .DS.Padding.medium, leading: .DS.Padding.double, - bottom: 0, + bottom: .DS.Padding.half, trailing: .DS.Padding.double) ) } From 91bb4f0941bd6fc1ec89a5357cb3abf4c64c7d02 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 18:16:02 +0200 Subject: [PATCH 018/116] Add cancel button to SiteSwitcherView --- .../Blog/Site Picker/SiteSwitcherView.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index ab7646c5f1ea..8e200a081f37 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -7,6 +7,7 @@ struct SiteSwitcherView: View { private let addSiteCallback: (() -> Void) @State private var searchText = "" @State private var isSearching = false + @Environment(\.dismiss) private var dismiss var sites: [BlogListView.Site] { if searchText.isEmpty { @@ -69,12 +70,29 @@ struct SiteSwitcherView: View { selectionCallback: selectionCallback ) .toolbar { - editButton + ToolbarItem(placement: .navigationBarLeading) { + cancelButton + } + ToolbarItem(placement: .navigationBarTrailing) { + editButton + } } .navigationTitle("Switch Site") .navigationBarTitleDisplayMode(.inline) } + private var cancelButton: some View { + Button(action: { + dismiss() + }, label: { + Text("Cancel") + .style(.bodyLarge(.regular)) + .foregroundStyle( + Color.DS.Foreground.primary + ) + }) + } + private var editButton: some View { Button(action: { isEditing.toggle() From 6b31977cad0b3a24510f3cf53023cc5e6cde5dcc Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 18:22:34 +0200 Subject: [PATCH 019/116] Add localization --- .../Site Picker/BlogList/BlogListView.swift | 28 +++++++++++-- .../Blog/Site Picker/SiteSwitcherView.swift | 42 +++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index e222cd116d96..a0d0443200ce 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -83,7 +83,7 @@ struct BlogListView: View { } } header: { sectionHeader( - title: "Pinned sites" + title: Strings.pinnedSectionTitle ) .listRowInsets(EdgeInsets( top: .DS.Padding.medium, @@ -109,7 +109,7 @@ struct BlogListView: View { } } header: { sectionHeader( - title: "All sites" + title: Strings.allSitesSectionTitle ) } } @@ -128,7 +128,7 @@ struct BlogListView: View { } } header: { sectionHeader( - title: "Recent sites" + title: Strings.recentsSectionTitle ) } } @@ -205,3 +205,25 @@ struct BlogListView: View { } } + +private extension BlogListView { + enum Strings { + static let pinnedSectionTitle = NSLocalizedString( + "site_switcher_pinned_section_title", + value: "Pinned sites", + comment: "Pinned section title for site switcher." + ) + + static let recentsSectionTitle = NSLocalizedString( + "site_switcher_recents_section_title", + value: "Recent sites", + comment: "Recents section title for site switcher." + ) + + static let allSitesSectionTitle = NSLocalizedString( + "site_switcher_all_sites_section_title", + value: "All sites", + comment: "All sites section title for site switcher." + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 8e200a081f37..f1069cc8a590 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -77,7 +77,7 @@ struct SiteSwitcherView: View { editButton } } - .navigationTitle("Switch Site") + .navigationTitle(Strings.navigationTitle) .navigationBarTitleDisplayMode(.inline) } @@ -85,7 +85,7 @@ struct SiteSwitcherView: View { Button(action: { dismiss() }, label: { - Text("Cancel") + Text(Strings.navigationDismissButtonTitle) .style(.bodyLarge(.regular)) .foregroundStyle( Color.DS.Foreground.primary @@ -97,7 +97,7 @@ struct SiteSwitcherView: View { Button(action: { isEditing.toggle() }, label: { - Text(isEditing ? "Done": "Edit") + Text(isEditing ? Strings.navigationDoneButtonTitle: Strings.navigationEditButtonTitle) .style(.bodyLarge(.regular)) .foregroundStyle( Color.DS.Foreground.primary @@ -109,7 +109,7 @@ struct SiteSwitcherView: View { VStack(spacing: .DS.Padding.medium) { Divider() .background(Color.DS.Foreground.secondary) - DSButton(title: "Add a site", style: .init(emphasis: .primary, size: .large)) { + DSButton(title: Strings.addSiteButtonTitle, style: .init(emphasis: .primary, size: .large)) { addSiteCallback() } .padding(.horizontal, .DS.Padding.medium) @@ -117,3 +117,37 @@ struct SiteSwitcherView: View { .background(Color.DS.Background.primary) } } + +private extension SiteSwitcherView { + enum Strings { + static let navigationTitle = NSLocalizedString( + "site_switcher_title", + value: "Choose a site", + comment: "Title for site switcher screen." + ) + + static let navigationDismissButtonTitle = NSLocalizedString( + "site_switcher_dismiss_button_title", + value: "Cancel", + comment: "Dismiss button title above the search." + ) + + static let navigationEditButtonTitle = NSLocalizedString( + "site_switcher_edit_button_title", + value: "Edit", + comment: "Edit button title above the search." + ) + + static let navigationDoneButtonTitle = NSLocalizedString( + "site_switcher_done_button_title", + value: "Done", + comment: "Done button title above the search." + ) + + static let addSiteButtonTitle = NSLocalizedString( + "site_switcher_cta_title", + value: "Add a site", + comment: "CTA title for the site switcher screen." + ) + } +} From c6c235a6b4a1562ef55761ee0372fb8ce639d68e Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 3 Apr 2024 18:35:37 +0200 Subject: [PATCH 020/116] Update section paddings --- .../Blog/Site Picker/BlogList/BlogListView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index a0d0443200ce..e9bdbc90bc4a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -111,6 +111,12 @@ struct BlogListView: View { sectionHeader( title: Strings.allSitesSectionTitle ) + .listRowInsets(EdgeInsets( + top: 0, + leading: .DS.Padding.double, + bottom: .DS.Padding.half, + trailing: .DS.Padding.double) + ) } } } @@ -130,6 +136,12 @@ struct BlogListView: View { sectionHeader( title: Strings.recentsSectionTitle ) + .listRowInsets(EdgeInsets( + top: 0, + leading: .DS.Padding.double, + bottom: .DS.Padding.half, + trailing: .DS.Padding.double) + ) } } } From e6ccbdb7729484179a84cf87f16aa07998947ecd Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 13:00:07 +0200 Subject: [PATCH 021/116] Extract common sync code --- .../BlogList/BlogListReducer.swift | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index de3855a53a13..9035ee572a5a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -24,47 +24,49 @@ enum BlogListReducer { repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), allSites: [BlogListView.Site] ) { - var tempPinnedDomains = pinnedDomains(repository: repository) - let initialPinnedDomainsCount = tempPinnedDomains.count - - for pinnedDomain in tempPinnedDomains { - if !allSites.compactMap({ $0.domain }).contains(pinnedDomain.domain) { - let beforeRemoveCount = tempPinnedDomains.count - - tempPinnedDomains.removeAll { current in - current.domain == pinnedDomain.domain - } - - if initialPinnedDomainsCount != tempPinnedDomains.count { - let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempPinnedDomains) - repository.set(encodedRecentDomains, forKey: Constants.pinnedDomainsKey) - } - } - } + syncDomains( + repository: repository, + allSites: allSites, + domainsToUpdate: pinnedDomains().compactMap({ $0.domain }), + defaultsKey: Constants.pinnedDomainsKey + ) } private static func syncRecentDomains( repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), allSites: [BlogListView.Site] ) { - var tempRecentDomains = recentDomains(repository: repository) - let initialRecentDomainsCount = tempRecentDomains.count - for recentDomain in tempRecentDomains { - if !allSites.compactMap({ $0.domain }).contains(recentDomain) { - let beforeRemoveCount = tempRecentDomains.count + syncDomains( + repository: repository, + allSites: allSites, + domainsToUpdate: recentDomains(), + defaultsKey: Constants.recentDomainsKey + ) + } - tempRecentDomains.removeAll { current in - current == recentDomain + private static func syncDomains( + repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), + allSites: [BlogListView.Site], + domainsToUpdate: [String], + defaultsKey: String + ) { + var tempDomains = domainsToUpdate + let initialDomainsCount = tempDomains.count + for domain in domainsToUpdate { + if !allSites.compactMap({ $0.domain }).contains(domain) { + tempDomains.removeAll { current in + current == domain } - if initialRecentDomainsCount != tempRecentDomains.count { - let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) - repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) + if initialDomainsCount != tempDomains.count { + let encodedDomains = try? Constants.jsonEncoder.encode(tempDomains) + repository.set(encodedDomains, forKey: defaultsKey) } } } } + static func pinnedSites( allSites: [BlogListView.Site], pinnedDomains: [String] @@ -155,7 +157,7 @@ enum BlogListReducer { ) -> [String] { if let data = repository.object(forKey: Constants.recentDomainsKey) as? Data, let decodedDomains = try? Constants.jsonDecoder.decode([String].self, from: data) { - return decodedDomains + return decodedDomains } return [] From 80c49314122d9e9e15ee5b5111bd383d05565807 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 13:05:31 +0200 Subject: [PATCH 022/116] Fix swiftlint error --- WordPress/WordPressTest/BlogListReducerTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/WordPressTest/BlogListReducerTests.swift b/WordPress/WordPressTest/BlogListReducerTests.swift index 5bc103a477ed..153c1469c7d9 100644 --- a/WordPress/WordPressTest/BlogListReducerTests.swift +++ b/WordPress/WordPressTest/BlogListReducerTests.swift @@ -183,6 +183,4 @@ final class BlogListReducerTests: XCTestCase { let result = BlogListReducer.recentDomains(repository: repository) XCTAssertEqual(result.count, BlogListReducer.Constants.recentsTotalLimit) } - - } From 132be22cda3edb23a0b9d0abfcd74f47122efb3b Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 13:53:44 +0200 Subject: [PATCH 023/116] Update search bar margin and add border to avatar --- .../Site Picker/BlogList/BlogListView.swift | 24 ++++++++++++++----- .../Blog/Site Picker/SiteSwitcherView.swift | 6 +---- .../List/NotificationsList/AvatarsView.swift | 4 ++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index e9bdbc90bc4a..c99d4d77d8df 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -16,19 +16,17 @@ struct BlogListView: View { @Binding private var isSearching: Bool @State private var pinnedDomains: [String] @State private var recentDomains: [String] + @State private var pressedDomains: Set = [] private let sites: [Site] - private let currentDomain: String? private let selectionCallback: ((String) -> Void) init( sites: [Site], - currentDomain: String?, isEditing: Binding, isSearching: Binding, selectionCallback: @escaping ((String) -> Void) ) { self.sites = sites - self.currentDomain = currentDomain self.pinnedDomains = BlogListReducer.pinnedDomains().compactMap({ $0.domain }) self.recentDomains = BlogListReducer.recentDomains() self._isEditing = isEditing @@ -162,10 +160,13 @@ struct BlogListView: View { siteHStack(site: site) } .listRowSeparator(.hidden) + .buttonStyle(SelectedButtonStyle(onPress: { isPressed in + pressedDomains = pressedDomains.symmetricDifference([site.domain]) + })) .listRowBackground( - currentDomain == site.domain - ? Color.DS.Background.secondary - : Color.DS.Background.primary + pressedDomains.contains( + site.domain + ) ? Color.DS.Background.secondary : Color.DS.Background.primary ) } @@ -239,3 +240,14 @@ private extension BlogListView { ) } } + +private struct SelectedButtonStyle: ButtonStyle { + var onPress: (Bool) -> Void + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .onChange(of: configuration.isPressed) { newValue in + onPress(newValue) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index f1069cc8a590..59f167266926 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -43,10 +43,7 @@ struct SiteSwitcherView: View { } .searchable( text: $searchText, - isPresented: $isSearching, - placement: .navigationBarDrawer( - displayMode: .always - ) + isPresented: $isSearching ) } else { NavigationView { @@ -64,7 +61,6 @@ struct SiteSwitcherView: View { private var blogListView: some View { BlogListView( sites: sites, - currentDomain: SiteSwitcherReducer.selectedBlog()?.url, isEditing: $isEditing, isSearching: $isSearching, selectionCallback: selectionCallback diff --git a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift index f06e2ea76e04..815e2926da60 100644 --- a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift +++ b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift @@ -49,6 +49,10 @@ struct AvatarsView: View { switch style { case let .single(primaryURL): avatar(url: primaryURL) + .overlay { + Circle() + .stroke(Color.DS.Foreground.primary.opacity(0.1), lineWidth: 0.5) + } case let .double(primaryURL, secondaryURL): doubleAvatarView( primaryURL: primaryURL, From cd288a3c913028e3fccd2ae2e917809bd6a1f5de Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 14:07:35 +0200 Subject: [PATCH 024/116] Fix top padding on pinned section --- .../ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index c99d4d77d8df..a462d15179ad 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -84,7 +84,7 @@ struct BlogListView: View { title: Strings.pinnedSectionTitle ) .listRowInsets(EdgeInsets( - top: .DS.Padding.medium, + top: 0, leading: .DS.Padding.double, bottom: .DS.Padding.half, trailing: .DS.Padding.double) From 9361f7c56c3714939262991e837fd89355a277ec Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 14:14:41 +0200 Subject: [PATCH 025/116] Remove spacing above divider --- .../ViewRelated/Blog/Site Picker/SiteSwitcherView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 59f167266926..af519c94e880 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -34,7 +34,7 @@ struct SiteSwitcherView: View { var body: some View { if #available(iOS 17.0, *) { NavigationStack { - VStack { + VStack(spacing: 0) { blogListView if !isSearching { addSiteButtonVStack @@ -47,7 +47,7 @@ struct SiteSwitcherView: View { ) } else { NavigationView { - VStack { + VStack(spacing: 0) { blogListView if !isSearching { addSiteButtonVStack From 7cde64b85f0aec09cf3ebac87d90f1137468eeda Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 5 Apr 2024 16:31:49 +0200 Subject: [PATCH 026/116] Remove empty line to satisfy swiftlint --- .../ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift index 9035ee572a5a..e4cc7145d7e0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift @@ -66,7 +66,6 @@ enum BlogListReducer { } } - static func pinnedSites( allSites: [BlogListView.Site], pinnedDomains: [String] From c3bd132d9292c7f896faf6843423bf3407ae7843 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 11 Apr 2024 18:34:59 +0200 Subject: [PATCH 027/116] Add `lastUsed` and `pinnedDate` to Blog --- WordPress/Classes/Models/Blog.h | 4 ++++ WordPress/Classes/Models/Blog.m | 2 ++ .../WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Models/Blog.h b/WordPress/Classes/Models/Blog.h index eba4a4650d04..b51745c46ef6 100644 --- a/WordPress/Classes/Models/Blog.h +++ b/WordPress/Classes/Models/Blog.h @@ -173,6 +173,10 @@ typedef NS_ENUM(NSInteger, SiteVisibility) { @property (nonatomic, strong, readwrite, nullable) NSDictionary *capabilities; @property (nonatomic, strong, readwrite, nullable) NSSet *quickStartTours; @property (nonatomic, strong, readwrite, nullable) NSNumber *quickStartTypeValue; + +// Site Switcher Convenience Properties +@property (nonatomic, strong, readwrite, nullable) NSDate *pinnedDate; +@property (nonatomic, strong, readwrite, nullable) NSDate *lastUsed; /// The blog's user ID for the current user @property (nonatomic, strong, readwrite, nullable) NSNumber *userID; /// Disk quota for site, this is only available for WP.com sites diff --git a/WordPress/Classes/Models/Blog.m b/WordPress/Classes/Models/Blog.m index 4fe64d3f35d1..feddd8d5b41a 100644 --- a/WordPress/Classes/Models/Blog.m +++ b/WordPress/Classes/Models/Blog.m @@ -93,6 +93,8 @@ @implementation Blog @dynamic quotaSpaceUsed; @dynamic pageTemplateCategories; @dynamic publicizeInfo; +@dynamic pinnedDate; +@dynamic lastUsed; @synthesize isSyncingPosts; @synthesize isSyncingPages; diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents index e724a3442ad2..0be53a479d3c 100644 --- a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents +++ b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 153.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -144,9 +144,11 @@ + + From 0c91c5df027ca82901f837cdb08f31b8792570ee Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 11 Apr 2024 18:35:38 +0200 Subject: [PATCH 028/116] Add BlogListViewModel and remove BlogListReducer --- .../BlogList/BlogListReducer.swift | 184 ------------------ .../Site Picker/BlogList/BlogListView.swift | 106 +++++----- .../BlogList/BlogListViewModel.swift | 107 ++++++++++ .../Site Picker/SiteSwitcherReducer.swift | 18 -- WordPress/WordPress.xcodeproj/project.pbxproj | 26 +-- 5 files changed, 162 insertions(+), 279 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift create mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift deleted file mode 100644 index e4cc7145d7e0..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListReducer.swift +++ /dev/null @@ -1,184 +0,0 @@ -enum BlogListReducer { - struct PinnedDomain: Codable, Equatable { - let domain: String - let isRecent: Bool - } - - enum Constants { - static let pinnedDomainsKey = "site_switcher_pinned_domains_key" - static let recentDomainsKey = "site_switcher_recent_domains_key" - static let jsonEncoder = JSONEncoder() - static let jsonDecoder = JSONDecoder() - static let recentsTotalLimit = 8 - } - - static func syncCachedValues( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - allSites: [BlogListView.Site] - ) { - syncPinnedDomains(repository: repository, allSites: allSites) - syncRecentDomains(repository: repository, allSites: allSites) - } - - private static func syncPinnedDomains( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - allSites: [BlogListView.Site] - ) { - syncDomains( - repository: repository, - allSites: allSites, - domainsToUpdate: pinnedDomains().compactMap({ $0.domain }), - defaultsKey: Constants.pinnedDomainsKey - ) - } - - private static func syncRecentDomains( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - allSites: [BlogListView.Site] - ) { - syncDomains( - repository: repository, - allSites: allSites, - domainsToUpdate: recentDomains(), - defaultsKey: Constants.recentDomainsKey - ) - } - - private static func syncDomains( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - allSites: [BlogListView.Site], - domainsToUpdate: [String], - defaultsKey: String - ) { - var tempDomains = domainsToUpdate - let initialDomainsCount = tempDomains.count - for domain in domainsToUpdate { - if !allSites.compactMap({ $0.domain }).contains(domain) { - tempDomains.removeAll { current in - current == domain - } - - if initialDomainsCount != tempDomains.count { - let encodedDomains = try? Constants.jsonEncoder.encode(tempDomains) - repository.set(encodedDomains, forKey: defaultsKey) - } - } - } - } - - static func pinnedSites( - allSites: [BlogListView.Site], - pinnedDomains: [String] - ) -> [BlogListView.Site] { - allSites.filter { - pinnedDomains.contains( - $0.domain - ) - } - } - - static func allSites( - allSites: [BlogListView.Site], - pinnedDomains: [String], - recentDomains: [String] - ) -> [BlogListView.Site] { - allSites.filter { - !pinnedDomains.contains($0.domain) && !recentDomains.contains($0.domain) - } - } - - static func recentSites( - allSites: [BlogListView.Site], - recentDomains: [String] - ) -> [BlogListView.Site] { - var sites: [BlogListView.Site] = [] - for domain in recentDomains { - if let recentSite = allSites.first(where: { $0.domain == domain }) { - sites.append(recentSite) - } - } - return sites - } - - static func pinnedDomains( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance() - ) -> [PinnedDomain] { - if let data = repository.object(forKey: Constants.pinnedDomainsKey) as? Data, - let decodedDomains = try? Constants.jsonDecoder.decode([PinnedDomain].self, from: data) { - return decodedDomains - } - - return [] - } - - static func toggleDomainPin( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - domain: String - ) { - var tempPinnedDomains = pinnedDomains(repository: repository) - let existingPinnedDomain = tempPinnedDomains.first { pinnedDomain in - pinnedDomain.domain == domain - } - - if let existingPinnedDomain { - // Pinned -> All/Recent - if existingPinnedDomain.isRecent { - var tempRecentDomains = recentDomains(repository: repository) - tempRecentDomains.insert(domain, at: 0) - - let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) - repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) - } - tempPinnedDomains.removeAll(where: { $0 == existingPinnedDomain }) - } else { - // All/Recent -> Pinned - var tempRecentDomains = recentDomains(repository: repository) - let beforeRemoveCount = tempRecentDomains.count - tempRecentDomains.removeAll { recentDomain in - recentDomain == domain - } - - let didRemoveFromRecent = tempRecentDomains.count != beforeRemoveCount - if didRemoveFromRecent { - let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) - repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) - } - - tempPinnedDomains.append(.init(domain: domain, isRecent: didRemoveFromRecent)) - } - - let encodedDomain = try? Constants.jsonEncoder.encode(tempPinnedDomains) - repository.set(encodedDomain, forKey: Constants.pinnedDomainsKey) - } - - static func recentDomains( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance() - ) -> [String] { - if let data = repository.object(forKey: Constants.recentDomainsKey) as? Data, - let decodedDomains = try? Constants.jsonDecoder.decode([String].self, from: data) { - return decodedDomains - } - - return [] - } - - static func didSelectDomain( - repository: UserPersistentRepository = UserPersistentStoreFactory.instance(), - domain: String - ) { - guard !pinnedDomains(repository: repository).compactMap({ $0.domain }).contains(domain) else { - return - } - - var tempRecentDomains = recentDomains(repository: repository) - tempRecentDomains.removeAll { $0 == domain } - tempRecentDomains.insert(domain, at: 0) - - if tempRecentDomains.count > Constants.recentsTotalLimit { - tempRecentDomains = tempRecentDomains.dropLast(tempRecentDomains.count - Constants.recentsTotalLimit) - } - - let encodedRecentDomains = try? Constants.jsonEncoder.encode(tempRecentDomains) - repository.set(encodedRecentDomains, forKey: Constants.recentDomainsKey) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index a462d15179ad..20ad67e979d2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -7,6 +7,7 @@ struct BlogListView: View { } struct Site: Equatable { + let id: NSNumber? let title: String let domain: String let imageURL: URL? @@ -14,23 +15,20 @@ struct BlogListView: View { @Binding private var isEditing: Bool @Binding private var isSearching: Bool - @State private var pinnedDomains: [String] - @State private var recentDomains: [String] + @Binding private var searchText: String + @StateObject var viewModel: BlogListViewModel = BlogListViewModel() @State private var pressedDomains: Set = [] - private let sites: [Site] - private let selectionCallback: ((String) -> Void) + private let selectionCallback: ((NSNumber) -> Void) init( - sites: [Site], isEditing: Binding, isSearching: Binding, - selectionCallback: @escaping ((String) -> Void) + searchText: Binding, + selectionCallback: @escaping ((NSNumber) -> Void) ) { - self.sites = sites - self.pinnedDomains = BlogListReducer.pinnedDomains().compactMap({ $0.domain }) - self.recentDomains = BlogListReducer.recentDomains() self._isEditing = isEditing self._isSearching = isSearching + self._searchText = searchText self.selectionCallback = selectionCallback } @@ -46,19 +44,19 @@ struct BlogListView: View { private var contentVStack: some View { List { if isSearching { - ForEach(sites, id: \.domain) { site in + ForEach(viewModel.searchSites, id: \.id) { site in siteButton(site: site) } + .onChange(of: searchText) { newValue in + viewModel.updateSearchText(newValue) + } } else { pinnedSection recentsSection - allSitesSection + allRemainingSitesSection } } .listStyle(.grouped) - .task { - BlogListReducer.syncCachedValues(allSites: sites) - } } private func sectionHeader(title: String) -> some View { @@ -70,13 +68,9 @@ struct BlogListView: View { @ViewBuilder private var pinnedSection: some View { - let pinnedSites = BlogListReducer.pinnedSites( - allSites: sites, - pinnedDomains: pinnedDomains - ) - if !pinnedSites.isEmpty { + if !viewModel.pinnedSites.isEmpty { Section { - ForEach(pinnedSites, id: \.domain) { site in + ForEach(viewModel.pinnedSites, id: \.domain) { site in siteButton(site: site) } } header: { @@ -94,20 +88,15 @@ struct BlogListView: View { } @ViewBuilder - private var allSitesSection: some View { - let allSites = BlogListReducer.allSites( - allSites: sites, - pinnedDomains: pinnedDomains, - recentDomains: recentDomains - ) - if !allSites.isEmpty { + private var allRemainingSitesSection: some View { + if !viewModel.allRemainingSites.isEmpty { Section { - ForEach(allSites, id: \.domain) { site in + ForEach(viewModel.allRemainingSites, id: \.domain) { site in siteButton(site: site) } } header: { sectionHeader( - title: Strings.allSitesSectionTitle + title: Strings.allRemainingSitesSectionTitle ) .listRowInsets(EdgeInsets( top: 0, @@ -121,13 +110,9 @@ struct BlogListView: View { @ViewBuilder private var recentsSection: some View { - let recentSites = BlogListReducer.recentSites( - allSites: sites, - recentDomains: recentDomains - ) - if !recentSites.isEmpty { + if !viewModel.recentSites.isEmpty { Section { - ForEach(recentSites, id: \.domain) { site in + ForEach(viewModel.recentSites, id: \.domain) { site in siteButton(site: site) } } header: { @@ -144,30 +129,31 @@ struct BlogListView: View { } } + @ViewBuilder private func siteButton(site: Site) -> some View { - Button { - if isEditing { - withAnimation { - BlogListReducer.toggleDomainPin(domain: site.domain) - pinnedDomains = BlogListReducer.pinnedDomains().compactMap({ $0.domain }) - recentDomains = BlogListReducer.recentDomains() + if let siteID = site.id { + Button { + if isEditing { + withAnimation { + viewModel.togglePinnedSite(siteID: siteID) + } + } else { + viewModel.siteSelected(siteID: siteID) + selectionCallback(siteID) } - } else { - BlogListReducer.didSelectDomain(domain: site.domain) - selectionCallback(site.domain) + } label: { + siteHStack(site: site) } - } label: { - siteHStack(site: site) + .listRowSeparator(.hidden) + .buttonStyle(SelectedButtonStyle(onPress: { isPressed in + pressedDomains = pressedDomains.symmetricDifference([site.domain]) + })) + .listRowBackground( + pressedDomains.contains( + site.domain + ) ? Color.DS.Background.secondary : Color.DS.Background.primary + ) } - .listRowSeparator(.hidden) - .buttonStyle(SelectedButtonStyle(onPress: { isPressed in - pressedDomains = pressedDomains.symmetricDifference([site.domain]) - })) - .listRowBackground( - pressedDomains.contains( - site.domain - ) ? Color.DS.Background.secondary : Color.DS.Background.primary - ) } private func siteHStack(site: Site) -> some View { @@ -180,9 +166,7 @@ struct BlogListView: View { Spacer() if isEditing { - pinIcon( - domain: site.domain - ) + pinIcon(site: site) .padding(.trailing, .DS.Padding.double) } } @@ -205,8 +189,8 @@ struct BlogListView: View { } } - private func pinIcon(domain: String) -> some View { - if pinnedDomains.contains(domain) == true { + private func pinIcon(site: Site) -> some View { + if viewModel.pinnedSites.contains(site) { Image(systemName: "pin.fill") .imageScale(.small) .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) @@ -233,7 +217,7 @@ private extension BlogListView { comment: "Recents section title for site switcher." ) - static let allSitesSectionTitle = NSLocalizedString( + static let allRemainingSitesSectionTitle = NSLocalizedString( "site_switcher_all_sites_section_title", value: "All sites", comment: "All sites section title for site switcher." diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift new file mode 100644 index 000000000000..35ee9984c240 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -0,0 +1,107 @@ +import SwiftUI + +final class BlogListViewModel: ObservableObject { + @Published var recentSites: [BlogListView.Site] + @Published var pinnedSites: [BlogListView.Site] + @Published var allRemainingSites: [BlogListView.Site] + @Published var searchSites: [BlogListView.Site] + + private let contextManager: ContextManager + private let dataSource: BlogListDataSource + + init(contextManager: ContextManager = ContextManager.sharedInstance()) { + self.contextManager = contextManager + self.dataSource = Self.createDataSource(contextManager: contextManager) + pinnedSites = Self.filteredPinnedSites(allBlogs: dataSource.filteredBlogs) + recentSites = Self.filteredRecentSites(allBlogs: dataSource.filteredBlogs) + allRemainingSites = Self.filteredAllRemainingSites(allBlogs: dataSource.filteredBlogs) + searchSites = dataSource.filteredBlogs.compactMap(BlogListView.Site.init) + } + + private static func createDataSource(contextManager: ContextManager) -> BlogListDataSource { + // Utilize existing DataSource class to fetch blogs. + let config = BlogListConfiguration.defaultConfig + let dataSource = BlogListDataSource(contextManager: contextManager) + dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites + dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains + return dataSource + } + + var allBlogs: [Blog] { + return dataSource.filteredBlogs + } + + func updateSearchText(_ newText: String) { + if newText.isEmpty { + searchSites = allBlogs.compactMap(BlogListView.Site.init) + } else { + searchSites = allBlogs + .filter { + $0.url?.lowercased().contains(newText.lowercased()) == true + || $0.title?.lowercased().contains(newText.lowercased()) == true + } + .compactMap(BlogListView.Site.init) + } + } + + func togglePinnedSite(siteID: NSNumber?) { + guard let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { + return + } + + blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil + pinnedSites = Self.filteredPinnedSites(allBlogs: allBlogs) + + let beforeRecentsCount = recentSites.count + recentSites = Self.filteredRecentSites(allBlogs: allBlogs) + + if recentSites.count == beforeRecentsCount { + allRemainingSites = Self.filteredAllRemainingSites(allBlogs: allBlogs) + } + contextManager.saveContextAndWait(contextManager.mainContext) + } + + func siteSelected(siteID: NSNumber?) { + guard let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { + return + } + + blog.lastUsed = Date() + recentSites = Self.filteredRecentSites(allBlogs: allBlogs) + contextManager.saveContextAndWait(contextManager.mainContext) + } + + private static func filteredAllRemainingSites(allBlogs: [Blog]) -> [BlogListView.Site] { + allBlogs.filter({ $0.pinnedDate == nil && $0.lastUsed == nil }).compactMap(BlogListView.Site.init) + } + + private static func filteredRecentSites(allBlogs: [Blog]) -> [BlogListView.Site] { + allBlogs + .filter({ $0.pinnedDate == nil && $0.lastUsed != nil }) + .sorted(by: { $0.lastUsed! > $1.lastUsed! }) // Force-unwrapping due to the null check on line above + .prefix(8) + .compactMap(BlogListView.Site.init) + } + + private static func filteredPinnedSites(allBlogs: [Blog]) -> [BlogListView.Site] { + allBlogs + .filter({ $0.pinnedDate != nil }) + .sorted(by: { $0.pinnedDate! > $1.pinnedDate! }) // Force-unwrapping due to the null check on line above + .compactMap(BlogListView.Site.init) + } + + private func selectedBlog() -> Blog? { + return RootViewCoordinator.sharedPresenter.currentOrLastBlog() + } +} + +extension BlogListView.Site { + init(blog: Blog) { + self.init( + id: blog.dotComID, + title: blog.title ?? "", + domain: blog.url ?? "", + imageURL: blog.hasIcon ? URL(string: blog.icon!) : nil + ) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift deleted file mode 100644 index c283ee7ea4a9..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherReducer.swift +++ /dev/null @@ -1,18 +0,0 @@ -enum SiteSwitcherReducer { - static func allBlogs() -> [Blog] { - return dataSource().filteredBlogs - } - - static func selectedBlog() -> Blog? { - return RootViewCoordinator.sharedPresenter.currentOrLastBlog() - } - - private static func dataSource() -> BlogListDataSource { - // Utilize existing DataSource class to fetch blogs. - let config = BlogListConfiguration.defaultConfig - let dataSource = BlogListDataSource() - dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites - dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains - return dataSource - } -} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 85431eb3596e..d660acbd08e6 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -284,12 +284,8 @@ 08216FD51CDBF96000304BA7 /* MenuItemTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */; }; 0822C3F52BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; 0822C3F62BA1EA2100C53B50 /* BlogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */; }; - 0822C3F72BA1EA2100C53B50 /* BlogListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */; }; - 0822C3F82BA1EA2100C53B50 /* BlogListReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */; }; 0822C3F92BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */; }; 0822C3FA2BA1EA2100C53B50 /* SiteSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */; }; - 0822C3FD2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; - 0822C3FE2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */; }; 08240C2F2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */; }; 082635BB1CEA69280088030C /* MenuItemsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 082635BA1CEA69280088030C /* MenuItemsViewController.m */; }; 0828D7FA1E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828D7F91E6E09AE00C7C7D4 /* WPAppAnalytics+Media.swift */; }; @@ -344,7 +340,7 @@ 088134FF2A56C5240027C086 /* CompliancePopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */; }; 0885A3671E837AFE00619B4D /* URLIncrementalFilenameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */; }; 088B89891DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */; }; - 088CAD4E2BBD8223005996DE /* BlogListReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */; }; + 088CAD4E2BBD8223005996DE /* BlogListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CAD4D2BBD8223005996DE /* BlogListViewModelTests.swift */; }; 088CC594282BEC41007B9421 /* TooltipPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088CC593282BEC41007B9421 /* TooltipPresenter.swift */; }; 089D4EBE2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json in Resources */ = {isa = PBXBuildFile; fileRef = 089D4EBD2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json */; }; 08A250F828D9E87600F50420 /* CommentDetailInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */; }; @@ -376,6 +372,8 @@ 08BBA3512A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BBA34F2A792B4B00BDCF32 /* DashboardGoogleDomainsCardCell.swift */; }; 08C3886A1ED78EE70057BE49 /* Media+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 08C388691ED78EE70057BE49 /* Media+Extensions.m */; }; 08C42C31281807880034720B /* ReaderSubscribeCommentsActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C42C30281807880034720B /* ReaderSubscribeCommentsActionTests.swift */; }; + 08C6FB492BC6E8530037457C /* BlogListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C6FB482BC6E8530037457C /* BlogListViewModel.swift */; }; + 08C6FB4A2BC6E8530037457C /* BlogListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C6FB482BC6E8530037457C /* BlogListViewModel.swift */; }; 08CBC77929AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */; }; 08CBC77A29AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */; }; 08CC677E1C49B65A00153AD7 /* MenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 08CC67791C49B65A00153AD7 /* MenuItem.m */; }; @@ -6064,9 +6062,7 @@ 08216FC61CDBF96000304BA7 /* MenuItemTypeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemTypeViewController.h; sourceTree = ""; }; 08216FC71CDBF96000304BA7 /* MenuItemTypeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemTypeViewController.m; sourceTree = ""; }; 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListView.swift; sourceTree = ""; }; - 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlogListReducer.swift; sourceTree = ""; }; 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteSwitcherView.swift; sourceTree = ""; }; - 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSwitcherReducer.swift; sourceTree = ""; }; 08240C2D2AB8A2DD00E7AEA8 /* AllDomainsListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDomainsListCardView.swift; sourceTree = ""; }; 082635B91CEA69280088030C /* MenuItemsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItemsViewController.h; sourceTree = ""; }; 082635BA1CEA69280088030C /* MenuItemsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuItemsViewController.m; sourceTree = ""; }; @@ -6114,7 +6110,7 @@ 088134FE2A56C5240027C086 /* CompliancePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompliancePopoverViewModelTests.swift; sourceTree = ""; }; 0885A3661E837AFE00619B4D /* URLIncrementalFilenameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLIncrementalFilenameTests.swift; sourceTree = ""; }; 088B89881DA6F93B000E8DEF /* ReaderPostCardContentLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderPostCardContentLabel.swift; sourceTree = ""; }; - 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogListReducerTests.swift; sourceTree = ""; }; + 088CAD4D2BBD8223005996DE /* BlogListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogListViewModelTests.swift; sourceTree = ""; }; 088CC593282BEC41007B9421 /* TooltipPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipPresenter.swift; sourceTree = ""; }; 089D4EBD2B7BBE0D0009CF2F /* notifications-like-multiple-avatar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "notifications-like-multiple-avatar.json"; sourceTree = ""; }; 08A250F728D9E87600F50420 /* CommentDetailInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDetailInfoViewController.swift; sourceTree = ""; }; @@ -6141,6 +6137,7 @@ 08C388681ED78EE70057BE49 /* Media+Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Media+Extensions.h"; sourceTree = ""; }; 08C388691ED78EE70057BE49 /* Media+Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Media+Extensions.m"; sourceTree = ""; }; 08C42C30281807880034720B /* ReaderSubscribeCommentsActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSubscribeCommentsActionTests.swift; sourceTree = ""; }; + 08C6FB482BC6E8530037457C /* BlogListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogListViewModel.swift; sourceTree = ""; }; 08CBC77829AE6CC4000026E7 /* SiteCreationEmptySiteTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCreationEmptySiteTemplate.swift; sourceTree = ""; }; 08CC67771C49B52E00153AD7 /* WordPress 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 45.xcdatamodel"; sourceTree = ""; }; 08CC67781C49B65A00153AD7 /* MenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuItem.h; sourceTree = ""; }; @@ -10182,8 +10179,8 @@ 0822C3FB2BA1EA2400C53B50 /* BlogList */ = { isa = PBXGroup; children = ( - 0822C3F32BA1EA2100C53B50 /* BlogListReducer.swift */, 0822C3F22BA1EA2000C53B50 /* BlogListView.swift */, + 08C6FB482BC6E8530037457C /* BlogListViewModel.swift */, ); path = BlogList; sourceTree = ""; @@ -10265,7 +10262,7 @@ 088CAD4C2BBD81ED005996DE /* SiteSwitcher */ = { isa = PBXGroup; children = ( - 088CAD4D2BBD8223005996DE /* BlogListReducerTests.swift */, + 088CAD4D2BBD8223005996DE /* BlogListViewModelTests.swift */, ); name = SiteSwitcher; sourceTree = ""; @@ -18438,7 +18435,6 @@ children = ( 0822C3FB2BA1EA2400C53B50 /* BlogList */, 0822C3F42BA1EA2100C53B50 /* SiteSwitcherView.swift */, - 0822C3FC2BA20DAF00C53B50 /* SiteSwitcherReducer.swift */, FA73D7E42798765B00DF24B3 /* SitePickerViewController.swift */, FAAEFADF2B1E29F0004AE802 /* SitePickerViewController+SiteActions.swift */, FA73D7E827987BA500DF24B3 /* SitePickerViewController+SiteIcon.swift */, @@ -22883,6 +22879,7 @@ 98921EF721372E12004949AA /* MediaCoordinator.swift in Sources */, 9A9E3FA3230D5F0A00909BC4 /* StatsStackViewCell.swift in Sources */, F5A738C3244E7A6F00EDE065 /* ReaderTagsTableViewModel.swift in Sources */, + 08C6FB492BC6E8530037457C /* BlogListViewModel.swift in Sources */, 837B49DB283C2AE80061A657 /* BloggingPromptSettingsReminderDays+CoreDataClass.swift in Sources */, 73C8F06421BEEF3400DDDF7E /* SiteAssemblyService.swift in Sources */, B5176CC11CDCE1B90083CF2D /* ManagedPerson.swift in Sources */, @@ -23149,7 +23146,6 @@ 329F8E5824DDBD11002A5311 /* ReaderTopicCollectionViewCoordinator.swift in Sources */, 5948AD0E1AB734F2006E8882 /* WPAppAnalytics.m in Sources */, B53AD9BF1BE9584B009AB87E /* SettingsSelectionViewController.m in Sources */, - 0822C3F72BA1EA2100C53B50 /* BlogListReducer.swift in Sources */, 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */, FAD7626429F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift in Sources */, 24351254264DCA08009BB2B6 /* Secrets.swift in Sources */, @@ -23260,7 +23256,6 @@ B532D4EA199D4357006E4DF6 /* NoteBlockHeaderTableViewCell.swift in Sources */, 24ADA24C24F9A4CB001B5DAE /* RemoteFeatureFlagStore.swift in Sources */, 836498CE281735CC00A2C170 /* BloggingPromptsHeaderView.swift in Sources */, - 0822C3FD2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */, 3F43703F2893201400475B6E /* JetpackOverlayViewController.swift in Sources */, B09879762B572E1F0048256D /* StatsTrafficDatePickerViewModel.swift in Sources */, 319D6E8519E44F7F0013871C /* SuggestionsTableViewCell.m in Sources */, @@ -23867,7 +23862,7 @@ E1EBC3731C118ED200F638E0 /* ImmuTableTest.swift in Sources */, F5C00EAE242179780047846F /* WeekdaysHeaderViewTests.swift in Sources */, 24C69AC22612467C00312D9A /* UserSettingsTestsObjc.m in Sources */, - 088CAD4E2BBD8223005996DE /* BlogListReducerTests.swift in Sources */, + 088CAD4E2BBD8223005996DE /* BlogListViewModelTests.swift in Sources */, E1AB5A091E0BF31E00574B4E /* ArrayTests.swift in Sources */, 570B037722F1FFF6009D8411 /* PostCoordinatorFailedPostsFetcherTests.swift in Sources */, C8567496243F3D37001A995E /* TenorResultsPageTests.swift in Sources */, @@ -24289,7 +24284,6 @@ FE4DC5A8293A84E6008F322F /* MigrationDeepLinkRouter.swift in Sources */, 77DFF0892B68386800FA561D /* BooleanUserDefaultsDebugViewModel.swift in Sources */, 80DB57992AF99E0900C728FF /* BlogListConfiguration.swift in Sources */, - 0822C3FE2BA20DAF00C53B50 /* SiteSwitcherReducer.swift in Sources */, FA4B203629A786460089FE68 /* BlazeEventsTracker.swift in Sources */, FABB20EA2602FC2C00C8785C /* ActivityTypeSelectorViewController.swift in Sources */, FABB20EB2602FC2C00C8785C /* ActivityActionsParser.swift in Sources */, @@ -24953,7 +24947,6 @@ 4A1E77CD2989F2F7006281CC /* WPAccount+DeduplicateBlogs.swift in Sources */, FA98B61D29A3DB840071AAE8 /* BlazeHelper.swift in Sources */, 0830538D2B2732E400B889FE /* DynamicDashboardCard.swift in Sources */, - 0822C3F82BA1EA2100C53B50 /* BlogListReducer.swift in Sources */, FABB22C32602FC2C00C8785C /* ReaderTagsFooter.swift in Sources */, 4A2C73F52A95856000ACE79E /* PostRepository.swift in Sources */, 98DCF4A6275945E00008630F /* ReaderDetailNoCommentCell.swift in Sources */, @@ -25832,6 +25825,7 @@ FE7FAABF299A998F0032A6F2 /* EventTracker.swift in Sources */, FABB25272602FC2C00C8785C /* UIImage+Exporters.swift in Sources */, 0133A7BF2A8CEADD00B36E58 /* SupportCoordinator.swift in Sources */, + 08C6FB4A2BC6E8530037457C /* BlogListViewModel.swift in Sources */, FABB25282602FC2C00C8785C /* PostStatsTitleCell.swift in Sources */, FABB25292602FC2C00C8785C /* CommentService.m in Sources */, FE50965D2A20D0F300DDD071 /* CommentTableHeaderView.swift in Sources */, From f585baeb58b2eb190b81c3f9efa1ebd8f97169c6 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 11 Apr 2024 18:35:59 +0200 Subject: [PATCH 029/116] Add `BlogListViewModelTests` --- .../Blog/Blog List/BlogListDataSource.swift | 9 +- .../Blog/Blog List/BlogListViewController.m | 2 +- .../BlogSelectorViewController.m | 2 +- .../SitePickerViewController.swift | 4 +- .../Blog/Site Picker/SiteSwitcherView.swift | 23 +-- WordPress/WordPressTest/BlogBuilder.swift | 10 + .../WordPressTest/BlogListReducerTests.swift | 186 ------------------ .../BlogListViewModelTests.swift | 101 ++++++++++ 8 files changed, 125 insertions(+), 212 deletions(-) delete mode 100644 WordPress/WordPressTest/BlogListReducerTests.swift create mode 100644 WordPress/WordPressTest/BlogListViewModelTests.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift index 6520eb004bd2..ec7129b64729 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift @@ -69,7 +69,10 @@ private struct LoggedInDataSourceMapper: BlogListDataSourceMapper { } class BlogListDataSource: NSObject { - override init() { + private let contextManager: ContextManager + + init(contextManager: ContextManager = ContextManager.sharedInstance()) { + self.contextManager = contextManager super.init() // We can't decide if we're using recent sites until the results controller // is configured and we have a list of blogs, so we have to update this right @@ -199,8 +202,8 @@ class BlogListDataSource: NSObject { // MARK: - Internal properties - fileprivate let resultsController: NSFetchedResultsController = { - let context = ContextManager.sharedInstance().mainContext + fileprivate lazy var resultsController: NSFetchedResultsController = { + let context = contextManager.mainContext let request = NSFetchRequest(entityName: NSStringFromClass(Blog.self)) request.sortDescriptors = [ NSSortDescriptor(key: "accountForDefaultBlog.userID", ascending: false), diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m index bd309ecba5f9..73c185d472a0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m @@ -62,7 +62,7 @@ - (instancetype)initWithConfiguration:(BlogListConfiguration *)configuration - (void)configureDataSource { - self.dataSource = [BlogListDataSource new]; + self.dataSource = [BlogListDataSource init]; self.dataSource.shouldShowDisclosureIndicator = NO; self.dataSource.shouldHideSelfHostedSites = self.configuration.shouldHideSelfHostedSites; self.dataSource.shouldHideBlogsNotSupportingDomains = self.configuration.shouldHideBlogsNotSupportingDomains; diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m index ac114656c7b1..199eda2b3323 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m @@ -61,7 +61,7 @@ - (instancetype)initWithSelectedBlogDotComID:(nullable NSNumber *)dotComID - (void)configureDataSource { - self.dataSource = [BlogListDataSource new]; + self.dataSource = [BlogListDataSource init]; self.dataSource.selecting = YES; self.dataSource.selectedBlogId = self.selectedObjectID; __weak __typeof(self) weakSelf = self; diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index b524f6cabe86..76bc1fa26932 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -97,8 +97,8 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { var dismissAction: (() -> Void)? = nil let hostingController = UIHostingController( rootView: SiteSwitcherView( - selectionCallback: { [weak self] selectedDomain in - guard let selectedBlog = SiteSwitcherReducer.allBlogs().first(where: { $0.url == selectedDomain }) else { + selectionCallback: { [weak self] siteID in + guard let selectedBlog = BlogListViewModel().allBlogs.first(where: { $0.dotComID == siteID }) else { return } self?.switchToBlog(selectedBlog) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index af519c94e880..09cb57cd5be0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -3,29 +3,14 @@ import DesignSystem struct SiteSwitcherView: View { @State private var isEditing: Bool = false - private let selectionCallback: ((String) -> Void) + private let selectionCallback: ((NSNumber) -> Void) private let addSiteCallback: (() -> Void) + private let blogListViewModel = BlogListViewModel() @State private var searchText = "" @State private var isSearching = false @Environment(\.dismiss) private var dismiss - var sites: [BlogListView.Site] { - if searchText.isEmpty { - return SiteSwitcherReducer.allBlogs().compactMap { - .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) - } - } else { - return SiteSwitcherReducer.allBlogs() - .filter { - $0.url!.lowercased().contains(searchText.lowercased()) || $0.title!.lowercased().contains(searchText.lowercased()) - } - .compactMap { - .init(title: $0.title!, domain: $0.url!, imageURL: $0.hasIcon ? URL(string: $0.icon!) : nil) - } - } - } - - init(selectionCallback: @escaping ((String) -> Void), + init(selectionCallback: @escaping ((NSNumber) -> Void), addSiteCallback: @escaping (() -> Void)) { self.selectionCallback = selectionCallback self.addSiteCallback = addSiteCallback @@ -60,9 +45,9 @@ struct SiteSwitcherView: View { private var blogListView: some View { BlogListView( - sites: sites, isEditing: $isEditing, isSearching: $isSearching, + searchText: $searchText, selectionCallback: selectionCallback ) .toolbar { diff --git a/WordPress/WordPressTest/BlogBuilder.swift b/WordPress/WordPressTest/BlogBuilder.swift index b59bb98910e2..6c5697aab4bf 100644 --- a/WordPress/WordPressTest/BlogBuilder.swift +++ b/WordPress/WordPressTest/BlogBuilder.swift @@ -136,6 +136,16 @@ final class BlogBuilder { return self } + func with(pinnedDate: Date?) -> Self { + blog.pinnedDate = pinnedDate + return self + } + + func with(lastUsed: Date?) -> Self { + blog.lastUsed = lastUsed + return self + } + func with(url: String) -> Self { blog.url = url diff --git a/WordPress/WordPressTest/BlogListReducerTests.swift b/WordPress/WordPressTest/BlogListReducerTests.swift deleted file mode 100644 index 153c1469c7d9..000000000000 --- a/WordPress/WordPressTest/BlogListReducerTests.swift +++ /dev/null @@ -1,186 +0,0 @@ -import XCTest -@testable import WordPress - -final class BlogListReducerTests: XCTestCase { - private enum Constants { - static let jsonEncoder = JSONEncoder() - static let jsonDecoder = JSONDecoder() - } - - private let repository = InMemoryUserDefaults() - private static let suiteName = "TestSuite_BlogListReducerTests" - - // MARK: - Helper Methods - private func encodeAndStore(_ object: T, forKey key: String) { - if let data = try? Constants.jsonEncoder.encode(object) { - repository.set(data, forKey: key) - } - } - - // MARK: - Tests for Retrieval Functions - func testPinnedDomainsWithNoData() { - XCTAssertTrue(BlogListReducer.pinnedDomains(repository: repository).isEmpty) - } - - func testPinnedDomainsWithValidData() { - let pinnedDomains = [BlogListReducer.PinnedDomain(domain: "example.com", isRecent: true)] - encodeAndStore(pinnedDomains, forKey: BlogListReducer.Constants.pinnedDomainsKey) - - let result = BlogListReducer.pinnedDomains(repository: repository) - XCTAssertEqual(result, pinnedDomains) - } - - func testRecentDomainsWithNoData() { - XCTAssertTrue(BlogListReducer.recentDomains(repository: repository).isEmpty) - } - - func testRecentDomainsWithValidData() { - let recentDomains = ["example.com"] - encodeAndStore(recentDomains, forKey: BlogListReducer.Constants.recentDomainsKey) - - let result = BlogListReducer.recentDomains(repository: repository) - XCTAssertEqual(result, recentDomains) - } - - func testPinnedSites() { - let sites: [BlogListView.Site] = [ - .init( - title: "1", - domain: "example1.com", - imageURL: nil - ), - .init( - title: "2", - domain: "example2.com", - imageURL: nil - ), - .init( - title: "3", - domain: "example3.com", - imageURL: nil - ) - ] - let pinnedDomains: [String] = ["example1.com", "wordpress.com"] - let result = BlogListReducer.pinnedSites(allSites: sites, pinnedDomains: pinnedDomains) - - XCTAssertEqual( - result, - [ - BlogListView.Site( - title: "1", - domain: "example1.com", - imageURL: nil - ) - ] - ) - } - - func testAllSitesExcludesPinnedAndRecent() { - let sites: [BlogListView.Site] = [ - .init( - title: "1", - domain: "example1.com", - imageURL: nil - ), - .init( - title: "2", - domain: "example2.com", - imageURL: nil - ), - .init( - title: "3", - domain: "example3.com", - imageURL: nil - ) - ] - - let result = BlogListReducer.allSites( - allSites: sites, - pinnedDomains: ["example2.com"], - recentDomains: ["example3.com"] - ) - - XCTAssertEqual(result, [BlogListView.Site(title: "1", domain: "example1.com", imageURL: nil)]) - } - - func testRecentSites() { - let sites: [BlogListView.Site] = [ - .init( - title: "1", - domain: "example1.com", - imageURL: nil - ), - .init( - title: "2", - domain: "example2.com", - imageURL: nil - ), - .init( - title: "3", - domain: "example3.com", - imageURL: nil - ) - ] - - let recentDomains = ["example2.com", "example1.com"] - - let result = BlogListReducer.recentSites(allSites: sites, recentDomains: recentDomains) - - XCTAssertEqual( - result, - [ - .init(title: "2", domain: "example2.com", imageURL: nil), - .init(title: "1", domain: "example1.com", imageURL: nil) - ] - ) - } - - // MARK: - Tests for Domain Toggling - func testToggleDomainPinAdd() { - let domain = "example.com" - BlogListReducer.toggleDomainPin(repository: repository, domain: domain) - let result = BlogListReducer.pinnedDomains(repository: repository) - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.domain, domain) - } - - func testToggleDomainPinRemove() { - let domain = "example.com" - encodeAndStore( - [ - BlogListReducer.PinnedDomain(domain: domain, isRecent: false) - ], - forKey: BlogListReducer.Constants.pinnedDomainsKey - ) - BlogListReducer.toggleDomainPin(repository: repository, domain: domain) - XCTAssertTrue(BlogListReducer.pinnedDomains(repository: repository).isEmpty) - } - - // MARK: - Tests for Domain Selection - func testDidSelectDomainAlreadyPinned() { - let domain = "example.com" - encodeAndStore( - [ - BlogListReducer.PinnedDomain(domain: domain, isRecent: false) - ], - forKey: BlogListReducer.Constants.pinnedDomainsKey - ) - BlogListReducer.didSelectDomain(repository: repository, domain: domain) - // Ensure no change to recent domains if the domain is already pinned - XCTAssertTrue(BlogListReducer.recentDomains(repository: repository).isEmpty) - } - - func testDidSelectDomainAddToRecent() { - let domain = "example.com" - BlogListReducer.didSelectDomain(repository: repository, domain: domain) - let result = BlogListReducer.recentDomains(repository: repository) - XCTAssertEqual(result, [domain]) - } - - func testDidSelectDomainRespectsRecentsLimit() { - let domains = (1...10).map { "example\($0).com" } - domains.forEach { BlogListReducer.didSelectDomain(repository: repository, domain: $0) } - let result = BlogListReducer.recentDomains(repository: repository) - XCTAssertEqual(result.count, BlogListReducer.Constants.recentsTotalLimit) - } -} diff --git a/WordPress/WordPressTest/BlogListViewModelTests.swift b/WordPress/WordPressTest/BlogListViewModelTests.swift new file mode 100644 index 000000000000..1eedd4cb692d --- /dev/null +++ b/WordPress/WordPressTest/BlogListViewModelTests.swift @@ -0,0 +1,101 @@ +import XCTest +@testable import WordPress + +final class BlogListViewModelTests: CoreDataTestCase { + private var viewModel: BlogListViewModel! + + override func setUp() { + super.setUp() + + viewModel = BlogListViewModel(contextManager: contextManager) + } + + // MARK: - Tests for Retrieval Functions + func testPinnedSitesWithNoData() { + XCTAssertTrue(viewModel.pinnedSites.isEmpty) + } + + func testPinnedSitesWithValidData() throws { + let siteID = 34984 + let blog = BlogBuilder(mainContext) + .with(dotComID: siteID) + .with(pinnedDate: Date()) + .build() + try mainContext.save() + + viewModel = BlogListViewModel(contextManager: contextManager) + + XCTAssertEqual(viewModel.pinnedSites.first?.id, 34984) + } + + func testRecentSitesWithNoData() { + XCTAssertTrue(viewModel.recentSites.isEmpty) + } + + func testRecentSitesWithValidData() throws { + let siteID = 34984 + let blog = BlogBuilder(mainContext) + .with(dotComID: siteID) + .with(lastUsed: Date()) + .with(pinnedDate: nil) + .build() + try mainContext.save() + + viewModel = BlogListViewModel(contextManager: contextManager) + + XCTAssertEqual(viewModel.recentSites.first?.id, siteID as NSNumber) + XCTAssertEqual(viewModel.recentSites.count, 1) + } + + func testAllRemainingSitesWithNoData() { + XCTAssertTrue(viewModel.allRemainingSites.isEmpty) + } + + func testAllRemainingSitesWithValidData() throws { + let siteID1 = 34984 + let blog1 = BlogBuilder(mainContext) + .with(dotComID: siteID1) + .with(lastUsed: Date()) + .build() + + let siteID2 = 13287 + let blog2 = BlogBuilder(mainContext) + .with(dotComID: siteID2) + .with(pinnedDate: Date()) + .build() + + let siteID3 = 43788 + let blog3 = BlogBuilder(mainContext) + .with(dotComID: siteID3) + .build() + try mainContext.save() + + viewModel = BlogListViewModel(contextManager: contextManager) + + XCTAssertEqual(viewModel.allRemainingSites.first?.id, siteID3 as NSNumber) + XCTAssertEqual(viewModel.allRemainingSites.count, 1) + } + + func testTogglePinnedSiteUpdatesPinnedSites() throws { + let blog = BlogBuilder(mainContext).build() + try mainContext.save() + + viewModel.togglePinnedSite(siteID: blog.dotComID) + + XCTAssertEqual(viewModel.pinnedSites.first?.id, blog.dotComID) + XCTAssertEqual(viewModel.pinnedSites.count, 1) + } + + func testSiteSelectedUpdatesLastUsedDate() throws { + let siteID = 4839 + let blog = BlogBuilder(mainContext) + .with(dotComID: siteID) + .with(lastUsed: nil) + .build() + try mainContext.save() + + viewModel.siteSelected(siteID: siteID as NSNumber) + + XCTAssertEqual(viewModel.recentSites.first?.id, siteID as NSNumber) + } +} From c3a43dab0d7bce7a24c48488ec334c4cdcd0457b Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 15:55:46 +0200 Subject: [PATCH 030/116] Add vector placeholder image --- .../DesignSystem/Foundation/IconName.swift | 1 + .../Vector.imageset/Contents.json | 16 +++++++++ .../Icons.xcassets/Vector.imageset/Vector.pdf | Bin 0 -> 1883 bytes .../List/NotificationsList/AvatarsView.swift | 34 +++++++++++++----- .../NotificationsTableViewCellContent.swift | 21 ++++++++--- 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json create mode 100644 Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Vector.pdf diff --git a/Modules/Sources/DesignSystem/Foundation/IconName.swift b/Modules/Sources/DesignSystem/Foundation/IconName.swift index 438c0fb7c4d0..8fca1d9258d8 100644 --- a/Modules/Sources/DesignSystem/Foundation/IconName.swift +++ b/Modules/Sources/DesignSystem/Foundation/IconName.swift @@ -14,6 +14,7 @@ public enum IconName: String, CaseIterable { case blockShare = "block.share" case starFill = "star.fill" case starOutline = "star.outline" + case vector = "vector" } // MARK: - Load Image diff --git a/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json new file mode 100644 index 000000000000..4bdc4947b421 --- /dev/null +++ b/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Vector.pdf b/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Vector.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d4c9dab9b0a3c604cbf432efb232f52c313ee618 GIT binary patch literal 1883 zcmZuyO;6iE5WVlOmI)D`= zlQ%o>&4)ccxm{hKt0)&jfrReoF9C3I0rC2Iytx~z(YSwV`l&0BQ7Y{PmzVA%OuMyV zA~}lxY}(!U2q}UaJSc{-H`PJJEAw|>o6XG?Ebr$3>OuSw@mj%d5GtA^B#L?CoUOM$ zcGU+Vqa@WyxxmO^WJ$izFx0ImiBA<|9mImn05YXwCqv9pqnb~TPqD!+KF&xj-eC>e zOc5aFz)(&+kjyOha!3fM#pA32S)`}r;UH8RDQ7E2XVyDjX2*m$q=-t#(g>xAv5az0 zokoeZ6et;GjFGUUbI6!DqUmH_+6xe)7#S5lI-y%JFxFV%!nq47un`%}iL+^hCYq8A z9cd@#PbtvERgyYpM~UX-#B#}NCKLebAblMAM;J$LRL#u6>QgTcFrXBw*ZP)->kA5PHCK0HGSL0o+39J%J*B+NGRLA2tVu=~v~FWPi= zsku$55}EJ<#H}-1pdZ?{8wa@mfeRI{`1`-#2IAXlXFTx5Z1>ew_YC)8&vBc=V;d}D z+L)nx>T5HAUD+E?x9r-{w5VwJFoCyKj~HkV;U1fGf z)vq9pB_2aMDPBXF41r@`?Hkhz`}+I1!gibv-NBr}>3)0Wni6liZUkHK)ZqAT|KDKt U)9bD3$Eh5ZWlEf!tiN8r0H some View { ZStack { avatar(url: secondaryURL) - .padding(.trailing, Constants.doubleAvatarHorizontalOffset * scale) + .padding(.trailing, doubleAvatarHorizontalOffset * scale) avatar(url: primaryURL) .avatarBorderOverlay() - .padding(.leading, Constants.doubleAvatarHorizontalOffset * scale) + .padding(.leading, doubleAvatarHorizontalOffset * scale) } } @@ -114,6 +120,16 @@ struct AvatarsView: View { } .padding(.top, .DS.Padding.split) } + + private var placeholderZStack: some View { + ZStack { + Color.DS.Background.secondary + Image.DS.icon(named: .vector) + .resizable() + .frame(width: 18, height: 18) + .tint(.DS.Background.secondary) + } + } } extension AvatarsView.Style { diff --git a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift index 1ff1c947dd7e..8ea4fb14a683 100644 --- a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift +++ b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift @@ -81,16 +81,27 @@ fileprivate extension NotificationsTableViewCellContent { if info.shouldShowIndicator { indicator .padding(.horizontal, .DS.Padding.single) - AvatarsView(style: info.avatarStyle) - .offset(x: -info.avatarStyle.leadingOffset) + AvatarsView( + style: info.avatarStyle, + placeholderImage: placeholderImage + ) + .offset(x: -info.avatarStyle.leadingOffset) } else { - AvatarsView(style: info.avatarStyle) - .offset(x: -info.avatarStyle.leadingOffset) - .padding(.leading, .DS.Padding.medium) + AvatarsView( + style: info.avatarStyle, + placeholderImage: placeholderImage + ) + .offset(x: -info.avatarStyle.leadingOffset) + .padding(.leading, .DS.Padding.medium) } } } + private var placeholderImage: Image { + Image("gravatar") + .resizable() + } + private var indicator: some View { Circle() .fill(Color.DS.Background.brand(isJetpack: AppConfiguration.isJetpack)) From 0750b074af1a43fa13b7b59999b37aa30be9e6a3 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 16:33:56 +0200 Subject: [PATCH 031/116] Add new tracks --- .../Utility/Analytics/WPAnalyticsEvent.swift | 9 +++++ .../BlogList/BlogListViewModel.swift | 36 ++++++++++++++++++- .../Blog/Site Picker/SiteSwitcherView.swift | 7 +++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index 815be3340a47..1f5fb9f6ba40 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -279,6 +279,9 @@ import Foundation case siteSwitcherAddSiteTapped case siteSwitcherSearchPerformed case siteSwitcherToggleBlogVisible + case siteSwitcherToggledPinTapped + case siteSwitcherPinUpdated + case siteSwitcherSiteTapped // Post List case postListItemSelected @@ -1059,6 +1062,12 @@ import Foundation return "site_switcher_search_performed" case .siteSwitcherToggleBlogVisible: return "site_switcher_toggle_blog_visible" + case .siteSwitcherToggledPinTapped: + return "site_switcher_toggled_pin_tapped" + case .siteSwitcherPinUpdated: + return "site_switcher_pin_updated" + case .siteSwitcherSiteTapped: + return "site_switcher_site_tapped" // Post List case .postListItemSelected: diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 35ee9984c240..bca0cdcadacb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -8,9 +8,13 @@ final class BlogListViewModel: ObservableObject { private let contextManager: ContextManager private let dataSource: BlogListDataSource + private let eventTracker: EventTracker - init(contextManager: ContextManager = ContextManager.sharedInstance()) { + init(contextManager: ContextManager = ContextManager.sharedInstance(), + eventTracker: EventTracker = DefaultEventTracker() + ) { self.contextManager = contextManager + self.eventTracker = eventTracker self.dataSource = Self.createDataSource(contextManager: contextManager) pinnedSites = Self.filteredPinnedSites(allBlogs: dataSource.filteredBlogs) recentSites = Self.filteredRecentSites(allBlogs: dataSource.filteredBlogs) @@ -50,6 +54,15 @@ final class BlogListViewModel: ObservableObject { } blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil + + eventTracker.track( + .siteSwitcherPinUpdated, + properties: [ + "blog_id": blog.dotComID ?? "", + "pinned": blog.pinnedDate == nil + ] + ) + pinnedSites = Self.filteredPinnedSites(allBlogs: allBlogs) let beforeRecentsCount = recentSites.count @@ -66,11 +79,32 @@ final class BlogListViewModel: ObservableObject { return } + trackSiteSelected(blog: blog) + blog.lastUsed = Date() recentSites = Self.filteredRecentSites(allBlogs: allBlogs) contextManager.saveContextAndWait(contextManager.mainContext) } + private func trackSiteSelected(blog: Blog) { + let sectionName: String + + if blog.pinnedDate != nil { + sectionName = "pinned" + } else if blog.lastUsed != nil { + sectionName = "recent" + } else { + sectionName = "all" + } + + eventTracker.track( + .siteSwitcherSiteTapped, + properties: [ + "section": sectionName, + ] + ) + } + private static func filteredAllRemainingSites(allBlogs: [Blog]) -> [BlogListView.Site] { allBlogs.filter({ $0.pinnedDate == nil && $0.lastUsed == nil }).compactMap(BlogListView.Site.init) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 09cb57cd5be0..f2c7d6561ca0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -6,14 +6,18 @@ struct SiteSwitcherView: View { private let selectionCallback: ((NSNumber) -> Void) private let addSiteCallback: (() -> Void) private let blogListViewModel = BlogListViewModel() + private let eventTracker: EventTracker @State private var searchText = "" @State private var isSearching = false @Environment(\.dismiss) private var dismiss init(selectionCallback: @escaping ((NSNumber) -> Void), - addSiteCallback: @escaping (() -> Void)) { + addSiteCallback: @escaping (() -> Void), + eventTracker: EventTracker = DefaultEventTracker() + ) { self.selectionCallback = selectionCallback self.addSiteCallback = addSiteCallback + self.eventTracker = eventTracker } var body: some View { @@ -77,6 +81,7 @@ struct SiteSwitcherView: View { private var editButton: some View { Button(action: { isEditing.toggle() + eventTracker.track(.siteSwitcherToggledPinTapped, properties: ["state": isEditing ? "edit" : "done"]) }, label: { Text(isEditing ? Strings.navigationDoneButtonTitle: Strings.navigationEditButtonTitle) .style(.bodyLarge(.regular)) From 5069420f408afc6d611832bf85c6c446da4ca67f Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 17:04:22 +0200 Subject: [PATCH 032/116] Add remote feature flag --- .../BuildInformation/RemoteFeatureFlag.swift | 7 +++++ .../BlogList/BlogListViewModel.swift | 20 ++++++++------ .../SitePickerViewController.swift | 26 ++++++++++++++++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 254297c541ff..4b1b574dfa21 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -29,6 +29,7 @@ enum RemoteFeatureFlag: Int, CaseIterable { case siteMonitoring case syncPublishing case readerDiscoverEndpoint + case siteSwitcherRedesign var defaultValue: Bool { switch self { @@ -86,6 +87,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest] case .readerDiscoverEndpoint: return true + case .siteSwitcherRedesign: + return true } } @@ -147,6 +150,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "_sync_publishing" case .readerDiscoverEndpoint: return "reader_discover_new_endpoint" + case .siteSwitcherRedesign: + return "site_switcher_redesign" } } @@ -206,6 +211,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "Synchronous Publishing" case .readerDiscoverEndpoint: return "Reader Discover New Endpoint" + case .siteSwitcherRedesign + return "Site Switcher Redesign" } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index bca0cdcadacb..11d16eada8f3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -53,15 +53,9 @@ final class BlogListViewModel: ObservableObject { return } - blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil + trackPinned(blog: blog) - eventTracker.track( - .siteSwitcherPinUpdated, - properties: [ - "blog_id": blog.dotComID ?? "", - "pinned": blog.pinnedDate == nil - ] - ) + blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil pinnedSites = Self.filteredPinnedSites(allBlogs: allBlogs) @@ -86,6 +80,16 @@ final class BlogListViewModel: ObservableObject { contextManager.saveContextAndWait(contextManager.mainContext) } + private func trackPinned(blog: Blog) { + eventTracker.track( + .siteSwitcherPinUpdated, + properties: [ + "blog_id": blog.dotComID ?? "", + "pinned": blog.pinnedDate == nil + ] + ) + } + private func trackSiteSelected(blog: Blog) { let sectionName: String diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 76bc1fa26932..9b6c870175b2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -94,6 +94,16 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { } func siteSwitcherTapped() { + if RemoteFeatureFlag.siteSwitcherRedesign.enabled() { + presentNewSiteSwitcher() + } else { + presentLegacySiteSwitcher() + } + + WPAnalytics.track(.mySiteSiteSwitcherTapped) + } + + private func presentNewSiteSwitcher() { var dismissAction: (() -> Void)? = nil let hostingController = UIHostingController( rootView: SiteSwitcherView( @@ -115,8 +125,22 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { } } present(hostingController, animated: true) + } - WPAnalytics.track(.mySiteSiteSwitcherTapped) + private func presentLegacySiteSwitcher() { + let blogListController = BlogListViewController(configuration: .defaultConfig, meScenePresenter: meScenePresenter) + + blogListController.blogSelected = { [weak self] controller, selectedBlog in + guard let self else { return } + self.switchToBlog(selectedBlog) + controller.dismiss(animated: true) { + self.onBlogListDismiss?() + } + } + + let navigationController = UINavigationController(rootViewController: blogListController) + navigationController.modalPresentationStyle = .formSheet + present(navigationController, animated: true) } func visitSiteTapped() { From 78115036b0bc2822a7e87b4b8804fb244354b2d7 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 17:15:25 +0200 Subject: [PATCH 033/116] Fix obj-c call site for the data source --- .../Classes/Utility/BuildInformation/RemoteFeatureFlag.swift | 2 +- .../Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift | 2 +- .../Classes/ViewRelated/Blog/Blog List/BlogListViewController.m | 2 +- .../ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 4b1b574dfa21..91f13bb1cf78 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -211,7 +211,7 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "Synchronous Publishing" case .readerDiscoverEndpoint: return "Reader Discover New Endpoint" - case .siteSwitcherRedesign + case .siteSwitcherRedesign: return "Site Switcher Redesign" } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift index ec7129b64729..b882a4bc4672 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListDataSource.swift @@ -71,7 +71,7 @@ private struct LoggedInDataSourceMapper: BlogListDataSourceMapper { class BlogListDataSource: NSObject { private let contextManager: ContextManager - init(contextManager: ContextManager = ContextManager.sharedInstance()) { + @objc init(contextManager: ContextManager = ContextManager.sharedInstance()) { self.contextManager = contextManager super.init() // We can't decide if we're using recent sites until the results controller diff --git a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m index 73c185d472a0..062c05ed9fe9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog List/BlogListViewController.m @@ -62,7 +62,7 @@ - (instancetype)initWithConfiguration:(BlogListConfiguration *)configuration - (void)configureDataSource { - self.dataSource = [BlogListDataSource init]; + self.dataSource = [[BlogListDataSource alloc] initWithContextManager: [ContextManager sharedInstance]]; self.dataSource.shouldShowDisclosureIndicator = NO; self.dataSource.shouldHideSelfHostedSites = self.configuration.shouldHideSelfHostedSites; self.dataSource.shouldHideBlogsNotSupportingDomains = self.configuration.shouldHideBlogsNotSupportingDomains; diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m index 199eda2b3323..36934c39e295 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Selector/BlogSelectorViewController.m @@ -61,7 +61,7 @@ - (instancetype)initWithSelectedBlogDotComID:(nullable NSNumber *)dotComID - (void)configureDataSource { - self.dataSource = [BlogListDataSource init]; + self.dataSource = [[BlogListDataSource alloc] initWithContextManager: [ContextManager sharedInstance]]; self.dataSource.selecting = YES; self.dataSource.selectedBlogId = self.selectedObjectID; __weak __typeof(self) weakSelf = self; From 464632862a0021f6709a239da0271fb49079fe52 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 17:25:21 +0200 Subject: [PATCH 034/116] Add tilt to pin icon --- .../ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift | 5 ++--- .../Blog/Site Picker/SitePickerViewController.swift | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 20ad67e979d2..48973b2a1e58 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -167,7 +167,6 @@ struct BlogListView: View { if isEditing { pinIcon(site: site) - .padding(.trailing, .DS.Padding.double) } } } @@ -192,12 +191,12 @@ struct BlogListView: View { private func pinIcon(site: Site) -> some View { if viewModel.pinnedSites.contains(site) { Image(systemName: "pin.fill") - .imageScale(.small) .foregroundStyle(Color.DS.Background.brand(isJetpack: true)) + .rotationEffect(.degrees(45)) } else { Image(systemName: "pin") - .imageScale(.small) .foregroundStyle(Color.DS.Foreground.secondary) + .rotationEffect(.degrees(45)) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 9b6c870175b2..402b517dd64a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -100,7 +100,6 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { presentLegacySiteSwitcher() } - WPAnalytics.track(.mySiteSiteSwitcherTapped) } private func presentNewSiteSwitcher() { @@ -125,6 +124,7 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { } } present(hostingController, animated: true) + WPAnalytics.track(.siteSwitcherAddSiteTapped) } private func presentLegacySiteSwitcher() { @@ -141,6 +141,7 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { let navigationController = UINavigationController(rootViewController: blogListController) navigationController.modalPresentationStyle = .formSheet present(navigationController, animated: true) + WPAnalytics.track(.mySiteSiteSwitcherTapped) } func visitSiteTapped() { From db83a6a630e4c3f2e0a0cc27a7725c432e7fe63d Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Fri, 12 Apr 2024 17:28:55 +0200 Subject: [PATCH 035/116] Add leading padding to pin icon --- .../ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 48973b2a1e58..01bb3f3bbebd 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -167,6 +167,7 @@ struct BlogListView: View { if isEditing { pinIcon(site: site) + .padding(.leading, .DS.Padding.single) } } } From ad5f5c26cb86249c900e9ccc9ef8e6f789f3655d Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 17 Apr 2024 18:05:39 +0200 Subject: [PATCH 036/116] Update `BlogListViewModel` to use fetchedControllers --- .../Site Picker/BlogList/BlogListView.swift | 27 +-- .../BlogList/BlogListViewModel.swift | 164 ++++++++++++------ .../Blog/Site Picker/SiteSwitcherView.swift | 4 +- .../BlogListViewModelTests.swift | 12 +- 4 files changed, 126 insertions(+), 81 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 20ad67e979d2..96d4a67dc6b3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -4,6 +4,12 @@ import DesignSystem struct BlogListView: View { private enum Constants { static let imageDiameter: CGFloat = 40 + static let sectionInsets = EdgeInsets( + top: .DS.Padding.half, + leading: .DS.Padding.double, + bottom: -.DS.Padding.half, + trailing: .DS.Padding.double + ) } struct Site: Equatable { @@ -77,12 +83,7 @@ struct BlogListView: View { sectionHeader( title: Strings.pinnedSectionTitle ) - .listRowInsets(EdgeInsets( - top: 0, - leading: .DS.Padding.double, - bottom: .DS.Padding.half, - trailing: .DS.Padding.double) - ) + .listRowInsets(Constants.sectionInsets) } } } @@ -98,12 +99,7 @@ struct BlogListView: View { sectionHeader( title: Strings.allRemainingSitesSectionTitle ) - .listRowInsets(EdgeInsets( - top: 0, - leading: .DS.Padding.double, - bottom: .DS.Padding.half, - trailing: .DS.Padding.double) - ) + .listRowInsets(Constants.sectionInsets) } } } @@ -119,12 +115,7 @@ struct BlogListView: View { sectionHeader( title: Strings.recentsSectionTitle ) - .listRowInsets(EdgeInsets( - top: 0, - leading: .DS.Padding.double, - bottom: .DS.Padding.half, - trailing: .DS.Padding.double) - ) + .listRowInsets(Constants.sectionInsets) } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 35ee9984c240..8a034fb654b4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -1,34 +1,23 @@ import SwiftUI -final class BlogListViewModel: ObservableObject { - @Published var recentSites: [BlogListView.Site] - @Published var pinnedSites: [BlogListView.Site] - @Published var allRemainingSites: [BlogListView.Site] - @Published var searchSites: [BlogListView.Site] +final class BlogListViewModel: NSObject, ObservableObject { + @Published var recentSites: [BlogListView.Site] = [] + @Published var pinnedSites: [BlogListView.Site] = [] + @Published var allRemainingSites: [BlogListView.Site] = [] + @Published var searchSites: [BlogListView.Site] = [] + var allBlogs: [Blog] = [] + + private var pinnedSitesController: NSFetchedResultsController? + private var recentSitesController: NSFetchedResultsController? + private var allRemainingSitesController: NSFetchedResultsController? + private var allBlogsController: NSFetchedResultsController? private let contextManager: ContextManager - private let dataSource: BlogListDataSource init(contextManager: ContextManager = ContextManager.sharedInstance()) { self.contextManager = contextManager - self.dataSource = Self.createDataSource(contextManager: contextManager) - pinnedSites = Self.filteredPinnedSites(allBlogs: dataSource.filteredBlogs) - recentSites = Self.filteredRecentSites(allBlogs: dataSource.filteredBlogs) - allRemainingSites = Self.filteredAllRemainingSites(allBlogs: dataSource.filteredBlogs) - searchSites = dataSource.filteredBlogs.compactMap(BlogListView.Site.init) - } - - private static func createDataSource(contextManager: ContextManager) -> BlogListDataSource { - // Utilize existing DataSource class to fetch blogs. - let config = BlogListConfiguration.defaultConfig - let dataSource = BlogListDataSource(contextManager: contextManager) - dataSource.shouldHideSelfHostedSites = config.shouldHideSelfHostedSites - dataSource.shouldHideBlogsNotSupportingDomains = config.shouldHideBlogsNotSupportingDomains - return dataSource - } - - var allBlogs: [Blog] { - return dataSource.filteredBlogs + super.init() + setupFetchedResultsControllers() } func updateSearchText(_ newText: String) { @@ -50,14 +39,6 @@ final class BlogListViewModel: ObservableObject { } blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil - pinnedSites = Self.filteredPinnedSites(allBlogs: allBlogs) - - let beforeRecentsCount = recentSites.count - recentSites = Self.filteredRecentSites(allBlogs: allBlogs) - - if recentSites.count == beforeRecentsCount { - allRemainingSites = Self.filteredAllRemainingSites(allBlogs: allBlogs) - } contextManager.saveContextAndWait(contextManager.mainContext) } @@ -67,32 +48,8 @@ final class BlogListViewModel: ObservableObject { } blog.lastUsed = Date() - recentSites = Self.filteredRecentSites(allBlogs: allBlogs) contextManager.saveContextAndWait(contextManager.mainContext) } - - private static func filteredAllRemainingSites(allBlogs: [Blog]) -> [BlogListView.Site] { - allBlogs.filter({ $0.pinnedDate == nil && $0.lastUsed == nil }).compactMap(BlogListView.Site.init) - } - - private static func filteredRecentSites(allBlogs: [Blog]) -> [BlogListView.Site] { - allBlogs - .filter({ $0.pinnedDate == nil && $0.lastUsed != nil }) - .sorted(by: { $0.lastUsed! > $1.lastUsed! }) // Force-unwrapping due to the null check on line above - .prefix(8) - .compactMap(BlogListView.Site.init) - } - - private static func filteredPinnedSites(allBlogs: [Blog]) -> [BlogListView.Site] { - allBlogs - .filter({ $0.pinnedDate != nil }) - .sorted(by: { $0.pinnedDate! > $1.pinnedDate! }) // Force-unwrapping due to the null check on line above - .compactMap(BlogListView.Site.init) - } - - private func selectedBlog() -> Blog? { - return RootViewCoordinator.sharedPresenter.currentOrLastBlog() - } } extension BlogListView.Site { @@ -105,3 +62,98 @@ extension BlogListView.Site { ) } } + +extension BlogListViewModel: NSFetchedResultsControllerDelegate { + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + if controller == pinnedSitesController { + pinnedSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) + } else if controller == recentSitesController { + recentSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) + } else if controller == allRemainingSitesController { + allRemainingSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) + } else if controller == allBlogsController { + allBlogs = controller.fetchedObjects as? [Blog] ?? [] + searchSites = allBlogs.compactMap(BlogListView.Site.init) + } + } +} + +extension BlogListViewModel { + private func setupFetchedResultsControllers() { + pinnedSitesController = createResultsController( + with: NSPredicate(format: "pinnedDate != nil"), + descriptor: NSSortDescriptor(key: "pinnedDate", ascending: false) + ) + recentSitesController = createResultsController( + with: NSPredicate(format: "lastUsed != nil AND pinnedDate == nil"), + descriptor: NSSortDescriptor(key: "lastUsed", ascending: false), + fetchLimit: 8 + ) + allRemainingSitesController = createResultsController( + with: NSPredicate(format: "lastUsed == nil AND pinnedDate == nil"), + descriptor: NSSortDescriptor(key: "settings.name", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) + ) + allBlogsController = createResultsController( + with: nil, + descriptor: NSSortDescriptor(key: "accountForDefaultBlog.userID", ascending: false) + ) + + [ + pinnedSitesController, + recentSitesController, + allRemainingSitesController, + allBlogsController + ].forEach { [weak self] controller in + controller?.delegate = self + try? controller?.performFetch() + } + self.updatePublishedSitesFromControllers() + } + + private func updatePublishedSitesFromControllers() { + pinnedSites = filteredBlogs(resultsController: pinnedSitesController).compactMap( + BlogListView.Site.init + ) + recentSites = filteredBlogs(resultsController: recentSitesController).compactMap( + BlogListView.Site.init + ) + allRemainingSites = filteredBlogs(resultsController: allRemainingSitesController).compactMap( + BlogListView.Site.init + ) + allBlogs = filteredBlogs(resultsController: allBlogsController) + } + + private func filteredBlogs(resultsController: NSFetchedResultsController?) -> [Blog] { + guard var blogs = resultsController?.fetchedObjects else { + return [] + } + if BlogListConfiguration.defaultConfig.shouldHideSelfHostedSites { + blogs = blogs.filter { $0.isAccessibleThroughWPCom() } + } + if BlogListConfiguration.defaultConfig.shouldHideBlogsNotSupportingDomains { + blogs = blogs.filter { $0.supports(.domains) } + } + return blogs + } + + private func createResultsController( + with predicate: NSPredicate?, + descriptor: NSSortDescriptor, + fetchLimit: Int? = nil + ) -> NSFetchedResultsController { + let context = contextManager.mainContext + let request = NSFetchRequest(entityName: NSStringFromClass(Blog.self)) + request.predicate = predicate + request.sortDescriptors = [descriptor] + if let fetchLimit { + request.fetchLimit = fetchLimit + } + let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) + do { + try controller.performFetch() + } catch { + fatalError("Error fetching blogs list: \(error)") + } + return controller + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 09cb57cd5be0..a60eee1722ca 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -21,6 +21,7 @@ struct SiteSwitcherView: View { NavigationStack { VStack(spacing: 0) { blogListView + .offset(y: isSearching ? -.DS.Padding.medium : 0) if !isSearching { addSiteButtonVStack } @@ -28,7 +29,8 @@ struct SiteSwitcherView: View { } .searchable( text: $searchText, - isPresented: $isSearching + isPresented: $isSearching, + placement: .navigationBarDrawer(displayMode: .always) ) } else { NavigationView { diff --git a/WordPress/WordPressTest/BlogListViewModelTests.swift b/WordPress/WordPressTest/BlogListViewModelTests.swift index 1eedd4cb692d..06822b2ecd83 100644 --- a/WordPress/WordPressTest/BlogListViewModelTests.swift +++ b/WordPress/WordPressTest/BlogListViewModelTests.swift @@ -17,7 +17,7 @@ final class BlogListViewModelTests: CoreDataTestCase { func testPinnedSitesWithValidData() throws { let siteID = 34984 - let blog = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID) .with(pinnedDate: Date()) .build() @@ -34,7 +34,7 @@ final class BlogListViewModelTests: CoreDataTestCase { func testRecentSitesWithValidData() throws { let siteID = 34984 - let blog = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID) .with(lastUsed: Date()) .with(pinnedDate: nil) @@ -53,19 +53,19 @@ final class BlogListViewModelTests: CoreDataTestCase { func testAllRemainingSitesWithValidData() throws { let siteID1 = 34984 - let blog1 = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID1) .with(lastUsed: Date()) .build() let siteID2 = 13287 - let blog2 = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID2) .with(pinnedDate: Date()) .build() let siteID3 = 43788 - let blog3 = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID3) .build() try mainContext.save() @@ -88,7 +88,7 @@ final class BlogListViewModelTests: CoreDataTestCase { func testSiteSelectedUpdatesLastUsedDate() throws { let siteID = 4839 - let blog = BlogBuilder(mainContext) + let _ = BlogBuilder(mainContext) .with(dotComID: siteID) .with(lastUsed: nil) .build() From 5942cf79ae1a48501880961001b389939406ffe7 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 17 Apr 2024 18:07:12 +0200 Subject: [PATCH 037/116] Change saveContextAndWait calls with save --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 8a034fb654b4..5d27c7de3c1c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -39,7 +39,7 @@ final class BlogListViewModel: NSObject, ObservableObject { } blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil - contextManager.saveContextAndWait(contextManager.mainContext) + contextManager.save(contextManager.mainContext) } func siteSelected(siteID: NSNumber?) { @@ -48,7 +48,7 @@ final class BlogListViewModel: NSObject, ObservableObject { } blog.lastUsed = Date() - contextManager.saveContextAndWait(contextManager.mainContext) + contextManager.save(contextManager.mainContext) } } From 929e91419fcb2141258697501258cd40f599cca3 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Wed, 17 Apr 2024 18:08:51 +0200 Subject: [PATCH 038/116] Add initial search populating call --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 5d27c7de3c1c..4318530d10ef 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -121,6 +121,7 @@ extension BlogListViewModel { BlogListView.Site.init ) allBlogs = filteredBlogs(resultsController: allBlogsController) + searchSites = allBlogs.compactMap(BlogListView.Site.init) } private func filteredBlogs(resultsController: NSFetchedResultsController?) -> [Blog] { From 89da7b1a0740fd381b42fe73eeb6a10901f1d5bd Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 18 Apr 2024 14:30:24 +0200 Subject: [PATCH 039/116] Add default recent selection and fix excess recents disappearing issue --- .../Site Picker/BlogList/BlogListView.swift | 6 +++ .../BlogList/BlogListViewModel.swift | 44 +++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 96d4a67dc6b3..e0304bb48166 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -42,8 +42,14 @@ struct BlogListView: View { if #available(iOS 16.0, *) { contentVStack .scrollContentBackground(.hidden) + .onAppear { + viewModel.viewAppeared() + } } else { contentVStack + .onAppear { + viewModel.viewAppeared() + } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 4318530d10ef..5a86251e09f9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -34,22 +34,60 @@ final class BlogListViewModel: NSObject, ObservableObject { } func togglePinnedSite(siteID: NSNumber?) { - guard let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { + guard let siteID, let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { return } + + let isCurrentlyPinned = blog.pinnedDate == nil + + if isCurrentlyPinned { + moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: blog) + } + + blog.pinnedDate = isCurrentlyPinned ? nil : Date() - blog.pinnedDate = blog.pinnedDate == nil ? Date() : nil contextManager.save(contextManager.mainContext) } func siteSelected(siteID: NSNumber?) { - guard let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { + guard let siteID, let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { return } blog.lastUsed = Date() + + updateExcessRecentBlogsIfNeeded(selectedSiteID: siteID) + + contextManager.save(contextManager.mainContext) + } + + private func moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: Blog) { + if let recentBlogs = recentSitesController?.fetchedObjects, + recentBlogs.count == 8 { + pinnedBlog.lastUsed = nil + } + } + + private func updateExcessRecentBlogsIfNeeded(selectedSiteID: NSNumber) { + if let recentBlogs = recentSitesController?.fetchedObjects, + recentBlogs.count == 8, + let lastBlog = recentBlogs.last, + !recentBlogs.compactMap({ $0.dotComID }).contains(selectedSiteID) { + lastBlog.lastUsed = nil + } + } + + func viewAppeared() { + if recentSites.isEmpty && pinnedSites.isEmpty { + selectedBlog()?.lastUsed = Date() + } + contextManager.save(contextManager.mainContext) } + + private func selectedBlog() -> Blog? { + return RootViewCoordinator.sharedPresenter.currentOrLastBlog() + } } extension BlogListView.Site { From c49c936efb1219f65390d32a92f714d9b3ff4d8a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 18 Apr 2024 14:43:56 +0200 Subject: [PATCH 040/116] Add call to `RecentSitesService` --- .../ViewRelated/Blog/Site Picker/SitePickerViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index 76bc1fa26932..1d2d637883cd 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -102,6 +102,7 @@ extension SitePickerViewController: BlogDetailHeaderViewDelegate { return } self?.switchToBlog(selectedBlog) + RecentSitesService().touch(blog: selectedBlog) // Dismiss hosting controller with completion block dismissAction?() }, addSiteCallback: { [weak self] in From b7504441e0cb3e24d3eb5df462b13fd3de12882a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 18 Apr 2024 14:57:31 +0200 Subject: [PATCH 041/116] Fix lint --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 5a86251e09f9..638a5235d0c1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -37,7 +37,6 @@ final class BlogListViewModel: NSObject, ObservableObject { guard let siteID, let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { return } - let isCurrentlyPinned = blog.pinnedDate == nil if isCurrentlyPinned { From 9a908627d325eeffaeec869c2c06118645b0c8e3 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 18 Apr 2024 16:04:48 +0200 Subject: [PATCH 042/116] Fix icon name and foreground color --- .../Contents.json | 0 .../{Vector.imageset => vector.imageset}/Vector.pdf | Bin .../Views/List/NotificationsList/AvatarsView.swift | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename Modules/Sources/DesignSystem/Foundation/Icons.xcassets/{Vector.imageset => vector.imageset}/Contents.json (100%) rename Modules/Sources/DesignSystem/Foundation/Icons.xcassets/{Vector.imageset => vector.imageset}/Vector.pdf (100%) diff --git a/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json b/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/vector.imageset/Contents.json similarity index 100% rename from Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Contents.json rename to Modules/Sources/DesignSystem/Foundation/Icons.xcassets/vector.imageset/Contents.json diff --git a/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Vector.pdf b/Modules/Sources/DesignSystem/Foundation/Icons.xcassets/vector.imageset/Vector.pdf similarity index 100% rename from Modules/Sources/DesignSystem/Foundation/Icons.xcassets/Vector.imageset/Vector.pdf rename to Modules/Sources/DesignSystem/Foundation/Icons.xcassets/vector.imageset/Vector.pdf diff --git a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift index 44628a2cc486..2365bb6a4d06 100644 --- a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift +++ b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarsView.swift @@ -127,7 +127,7 @@ struct AvatarsView: View { Image.DS.icon(named: .vector) .resizable() .frame(width: 18, height: 18) - .tint(.DS.Background.secondary) + .tint(.DS.Foreground.tertiary) } } } From bdd4c353eb4e538fd338d6e230bd365db11d7f5a Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Thu, 18 Apr 2024 16:43:35 +0200 Subject: [PATCH 043/116] Fix unit tests by correcting the `isCurrentlyPinned` boolean --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 6 +++--- WordPress/WordPressTest/BlogListViewModelTests.swift | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 638a5235d0c1..937305810117 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -37,7 +37,7 @@ final class BlogListViewModel: NSObject, ObservableObject { guard let siteID, let blog = allBlogs.first(where: { $0.dotComID == siteID }) else { return } - let isCurrentlyPinned = blog.pinnedDate == nil + let isCurrentlyPinned = blog.pinnedDate != nil if isCurrentlyPinned { moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: blog) @@ -45,7 +45,7 @@ final class BlogListViewModel: NSObject, ObservableObject { blog.pinnedDate = isCurrentlyPinned ? nil : Date() - contextManager.save(contextManager.mainContext) + contextManager.saveContextAndWait(contextManager.mainContext) } func siteSelected(siteID: NSNumber?) { @@ -57,7 +57,7 @@ final class BlogListViewModel: NSObject, ObservableObject { updateExcessRecentBlogsIfNeeded(selectedSiteID: siteID) - contextManager.save(contextManager.mainContext) + contextManager.saveContextAndWait(contextManager.mainContext) } private func moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: Blog) { diff --git a/WordPress/WordPressTest/BlogListViewModelTests.swift b/WordPress/WordPressTest/BlogListViewModelTests.swift index 06822b2ecd83..71df9425fa45 100644 --- a/WordPress/WordPressTest/BlogListViewModelTests.swift +++ b/WordPress/WordPressTest/BlogListViewModelTests.swift @@ -77,12 +77,15 @@ final class BlogListViewModelTests: CoreDataTestCase { } func testTogglePinnedSiteUpdatesPinnedSites() throws { - let blog = BlogBuilder(mainContext).build() + let id = 23948 + let blog = BlogBuilder(mainContext) + .with(dotComID: id) + .build() try mainContext.save() viewModel.togglePinnedSite(siteID: blog.dotComID) - XCTAssertEqual(viewModel.pinnedSites.first?.id, blog.dotComID) + XCTAssertEqual(viewModel.pinnedSites.first?.id, id as NSNumber) XCTAssertEqual(viewModel.pinnedSites.count, 1) } From 8f4c6bf4a8d9ba5c66a39e64f2556db5019842e0 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Sun, 21 Apr 2024 16:33:54 +0200 Subject: [PATCH 044/116] Remove obsolete property and rename localization keys --- .../Blog/Site Picker/BlogList/BlogListView.swift | 6 +++--- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 2 +- .../ViewRelated/Blog/Site Picker/SiteSwitcherView.swift | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index f165d9de6c74..653fd88154ae 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -203,19 +203,19 @@ struct BlogListView: View { private extension BlogListView { enum Strings { static let pinnedSectionTitle = NSLocalizedString( - "site_switcher_pinned_section_title", + "site_switcher.pinned_section.title", value: "Pinned sites", comment: "Pinned section title for site switcher." ) static let recentsSectionTitle = NSLocalizedString( - "site_switcher_recents_section_title", + "site_switcher.recents_section.title", value: "Recent sites", comment: "Recents section title for site switcher." ) static let allRemainingSitesSectionTitle = NSLocalizedString( - "site_switcher_all_sites_section_title", + "site_switcher.all_sites_section.title", value: "All sites", comment: "All sites section title for site switcher." ) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index e9e0dfb5d7cc..2e8a478abacc 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -135,7 +135,7 @@ extension BlogListView.Site { id: blog.dotComID, title: blog.title ?? "", domain: blog.url ?? "", - imageURL: blog.hasIcon ? URL(string: blog.icon!) : nil + imageURL: blog.hasIcon ? URL(string: blog.icon ?? "") : nil ) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index a9b8055aa775..51ebd2974cf7 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -5,7 +5,6 @@ struct SiteSwitcherView: View { @State private var isEditing: Bool = false private let selectionCallback: ((NSNumber) -> Void) private let addSiteCallback: (() -> Void) - private let blogListViewModel = BlogListViewModel() private let eventTracker: EventTracker @State private var searchText = "" @State private var isSearching = false From d99c93f26080f0156b979cb47bc97ff86426136c Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Mon, 22 Apr 2024 16:08:43 +0200 Subject: [PATCH 045/116] Resolve search list getting refreshed in edit mode --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 2e8a478abacc..572981fa972f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -150,7 +150,9 @@ extension BlogListViewModel: NSFetchedResultsControllerDelegate { allRemainingSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) } else if controller == allBlogsController { allBlogs = controller.fetchedObjects as? [Blog] ?? [] - searchSites = allBlogs.compactMap(BlogListView.Site.init) + if searchSites.isEmpty { + searchSites = allBlogs.compactMap(BlogListView.Site.init) + } } } } From 9a8ec6d7d8305887767212ec4a73ba7199d4a822 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Mon, 22 Apr 2024 17:26:48 +0200 Subject: [PATCH 046/116] Add accessibility identifier to add site button --- .../ViewRelated/Blog/Site Picker/SiteSwitcherView.swift | 1 + WordPress/UITestsFoundation/Screens/MySitesScreen.swift | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift index 51ebd2974cf7..aeacd3d1d3a0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SiteSwitcherView.swift @@ -99,6 +99,7 @@ struct SiteSwitcherView: View { DSButton(title: Strings.addSiteButtonTitle, style: .init(emphasis: .primary, size: .large)) { addSiteCallback() } + .accessibilityIdentifier("add-site-button") .padding(.horizontal, .DS.Padding.medium) } .background(Color.DS.Background.primary) diff --git a/WordPress/UITestsFoundation/Screens/MySitesScreen.swift b/WordPress/UITestsFoundation/Screens/MySitesScreen.swift index 8d8b068dc4f8..0f0063187de5 100644 --- a/WordPress/UITestsFoundation/Screens/MySitesScreen.swift +++ b/WordPress/UITestsFoundation/Screens/MySitesScreen.swift @@ -24,7 +24,7 @@ public class MySitesScreen: ScreenObject { var addSelfHostedSiteButton: XCUIElement { addSelfHostedSiteButtonGetter(app) } var cancelButton: XCUIElement { cancelButtonGetter(app) } var mySitesLabel: XCUIElement { mySitesLabelGetter(app) } - var plusButton: XCUIElement { plusButtonGetter(app) } + var addASiteButton: XCUIElement { plusButtonGetter(app) } init(app: XCUIApplication = XCUIApplication()) throws { try super.init( @@ -38,14 +38,14 @@ public class MySitesScreen: ScreenObject { } public func addSelfHostedSite() throws -> LoginSiteAddressScreen { - plusButton.tap() + addASiteButton.tap() addSelfHostedSiteButton.tap() return try LoginSiteAddressScreen() } @discardableResult public func tapPlusButton() throws -> SiteIntentScreen { - plusButton.tap() + addASiteButton.tap() return try SiteIntentScreen() } From 8bc80ef71576f17f7a20939fc095f8d6b0ffcf92 Mon Sep 17 00:00:00 2001 From: alpavanoglu Date: Mon, 22 Apr 2024 20:10:16 +0200 Subject: [PATCH 047/116] Disable remote flag by default --- .../Classes/Utility/BuildInformation/RemoteFeatureFlag.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 80938c6ce842..98d3947b0063 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -90,7 +90,7 @@ enum RemoteFeatureFlag: Int, CaseIterable { case .readerDiscoverEndpoint: return true case .siteSwitcherRedesign: - return true + return false case .readingPreferences: return true case .readingPreferencesFeedback: From 3b64f06760f46319e639391b36550ceaf7f40d5d Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 16:41:44 -0400 Subject: [PATCH 048/116] Add new PublishDatePickerViewController --- .../Post/PostSettingsViewController.m | 3 +- ...eprecatedPrepublishingViewController.swift | 4 +- .../PrepublishingViewController.swift | 6 +- .../PublishDatePickerViewController.swift | 201 ++++++++++++++++++ .../PublishSettingsViewController.swift | 3 +- .../SchedulingDatePickerViewController.swift | 84 -------- WordPress/WordPress.xcodeproj/project.pbxproj | 12 +- 7 files changed, 216 insertions(+), 97 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift delete mode 100644 WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 704cf606316f..b7019a7d678d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1049,7 +1049,8 @@ - (WPTextFieldTableViewCell *)getWPTableViewTextFieldCell - (void)showPublishSchedulingController { - ImmuTableViewController *vc = [PublishSettingsController viewControllerWithPost:self.apost]; + BOOL isRequired = self.apost.status == PostStatusPublish || self.apost.status == PostStatusScheduled; + UIViewController *vc = [PublishDatePickerHelper makeDatePickerWithPost:self.apost isRequired:isRequired]; [self.navigationController pushViewController:vc animated:YES]; } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift index 70852458ce29..a08d68321fad 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift @@ -91,7 +91,7 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView } else { if let sheetController = navigationController.sheetPresentationController { if #available(iOS 16, *) { - sheetController.detents = [.custom { _ in 510 }, .large()] + sheetController.detents = [.custom { _ in 530 }, .large()] } else { sheetController.detents = [.medium(), .large()] } @@ -412,7 +412,7 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView } func didTapSchedule(_ indexPath: IndexPath) { - let viewController = SchedulingDatePickerViewController.make(viewModel: publishSettingsViewModel) { [weak self] date in + let viewController = PublishDatePickerViewController.make(viewModel: publishSettingsViewModel) { [weak self] date in WPAnalytics.track(.editorPostScheduledChanged, properties: Constants.analyticsDefaultProperty) self?.publishSettingsViewModel.setDate(date) self?.reloadData() diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 60c5a85543c4..84b7459642dc 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -77,7 +77,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource } else { if let sheetController = navigationController.sheetPresentationController { if #available(iOS 16, *) { - sheetController.detents = [.custom { _ in 510 }, .large()] + sheetController.detents = [.custom { _ in 530 }, .large()] } else { sheetController.detents = [.medium(), .large()] } @@ -411,13 +411,13 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource } func didTapSchedule(_ indexPath: IndexPath) { - let viewController = SchedulingDatePickerViewController() - viewController.configuration = SchedulingDatePickerConfiguration(date: viewModel.publishDate, timeZone: post.blog.timeZone ?? TimeZone.current) { [weak self] date in + let configuration = PublishDatePickerConfiguration(date: viewModel.publishDate, timeZone: post.blog.timeZone ?? TimeZone.current) { [weak self] date in WPAnalytics.track(.editorPostScheduledChanged, properties: Constants.analyticsDefaultProperty) self?.viewModel.publishDate = date self?.reloadData() self?.updatePublishButtonLabel() } + let viewController = PublishDatePickerViewController(configuration: configuration) navigationController?.pushViewController(viewController, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift new file mode 100644 index 000000000000..4896c92b50bc --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -0,0 +1,201 @@ +import Foundation +import Gridicons +import UIKit +import SwiftUI + +struct PublishDatePickerConfiguration { + var date: Date? { + didSet { updated(date) } + } + /// If set to `true`, the user will no longer be able to remove the selection. + var isRequired = false + var timeZone: TimeZone + var updated: (Date?) -> Void +} + +private extension PublishDatePickerConfiguration { + var isCurrentTimeZone: Bool { + timeZone.secondsFromGMT() == TimeZone.current.secondsFromGMT() + } +} + +final class PublishDatePickerViewController: UIHostingController { + init(configuration: PublishDatePickerConfiguration) { + if configuration.isRequired && configuration.date == nil { + wpAssertionFailure("initial date value missing") + } + super.init(rootView: PublishDatePickerView(configuration: configuration)) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = Strings.title + } +} + +/// - warning: deprecated (kahu-offline-mode) +extension PublishDatePickerViewController { + static func make(viewModel: PublishSettingsViewModel, onDateUpdated: @escaping (Date?) -> Void) -> PublishDatePickerViewController { + PublishDatePickerViewController(configuration: .init( + date: viewModel.date, + isRequired: viewModel.isRequired, + timeZone: viewModel.timeZone, + updated: onDateUpdated + )) + } +} + +/// - warning: deprecated (kahu-offline-mode) +final class PublishDatePickerHelper: NSObject { + @objc class func makeDatePicker(post: AbstractPost, isRequired: Bool) -> UIViewController { + var viewModel = PublishSettingsViewModel(post: post) + return PublishDatePickerViewController.make(viewModel: viewModel) { date in + viewModel.setDate(date) + } + } +} + +struct PublishDatePickerView: View { + @State var configuration: PublishDatePickerConfiguration + + var body: some View { + Form { + Section { + dateRow + datePickerRow + if let date = configuration.date, !configuration.isCurrentTimeZone { + makeTimeZoneMismatchWarningView(date: date) + } + } header: { + Color.clear.frame(height: 0) // Reducing the top inset + } + if !configuration.isCurrentTimeZone { + Section { + timeZoneRow + } + } + } + .environment(\.defaultMinListHeaderHeight, 0) + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) + .tint(Color(uiColor: .brand)) + } + + private var dateRow: some View { + HStack { + Image(systemName: "calendar") + .foregroundStyle(.secondary) + VStack(alignment: .leading) { + Text(Strings.date) + .font(.subheadline) + Text(selectedValue) + .font(.footnote) + .foregroundStyle(.secondary) + } + .lineLimit(1) + + Spacer() + + if configuration.date != nil, !configuration.isRequired { + Button(role: .destructive, action: { + configuration.date = nil + }) { + Image(systemName: "minus.circle.fill") + } + } + } + } + + private var timeZoneRow: some View { + HStack { + Text(Strings.timeZone) + Spacer() + Text(getLocalizedTimeZoneDescription(for: configuration.timeZone)) + .truncationMode(.middle) + .foregroundStyle(.secondary) + }.lineLimit(1) + } + + private var datePickerRow: some View { + DatePicker(Strings.date, selection: Binding(get: { + configuration.date ?? Date() + }, set: { + configuration.date = $0 + }), displayedComponents: [.date, .hourAndMinute]) + .environment(\.timeZone, configuration.timeZone) + .datePickerStyle(.graphical) + .labelsHidden() + .listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)) + } + + private func makeTimeZoneMismatchWarningView(date: Date) -> some View { + HStack { + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(.secondary.opacity(0.75)) + Text(Strings.footerCurrentTimezone + "\n" + "\(formattedString(from: date, timeZone: .current)) (\(getLocalizedTimeOffset(for: .current)))") + .font(.footnote) + .foregroundColor(.secondary) + } + .padding(.vertical, 2) + } + + private var selectedValue: String { + guard let date = configuration.date else { + return Strings.immediately + } + let value = formattedString(from: date, timeZone: configuration.timeZone) + guard !configuration.isCurrentTimeZone else { + return value + } + return "\(value) (\(getLocalizedTimeOffset(for: configuration.timeZone)))" + } +} + +private func formattedString(from date: Date, timeZone: TimeZone?) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.timeZone = timeZone + return dateFormatter.string(from: date) +} + +private func getLocalizedTimeZoneDescription(for timeZone: TimeZone) -> String { + let name = timeZone.localizedName(for: .shortGeneric, locale: .current) ?? "" + let offset = getLocalizedTimeOffset(for: timeZone) + guard name != offset else { return name } // "GMT" will just say "GMT (GMT)" + return "\(name) (\(offset))" +} + +private func getLocalizedTimeOffset(for timeZone: TimeZone) -> String { + let timeZoneFormatter = DateFormatter() + timeZoneFormatter.dateFormat = "O" + timeZoneFormatter.timeZone = timeZone + return timeZoneFormatter.string(from: Date()) +} + +private enum Strings { + static let title = NSLocalizedString("publishDatePicker.title", value: "Publish Date", comment: "Post publish date picker") + static let date = NSLocalizedString("publishDatePicker.date", value: "Publish Date", comment: "Post publish date picker title for date cell") + static let immediately = NSLocalizedString("publishDatePicker.immediately", value: "Immediately", comment: "Post publish date picker: selected value placeholder when no date is selected and the post will be published immediately") + static let timeZone = NSLocalizedString("publishDatePicker.timeZone", value: "Time Zone", comment: "Post publish time zone cell title") + static let removePublishDate = NSLocalizedString("publishDatePicker.removePublishDate", value: "Remove Publish Date", comment: "Title for button in publish date picker") + static let selectPublishDate = NSLocalizedString("publishDatePicker.selectPublishDate", value: "Select Publish Date", comment: "Title for button in publish date picker") + static let footerCurrentTimezone = NSLocalizedString("publishDatePicker.footerCurrentTimezone", value: "The date in your current time zone:", comment: "Post publish date picker footer view when the selected date time zone is different from your current time zone; followed by the time in the current time zone.") +} + +#Preview("Current Time Zone") { + NavigationView { + PublishDatePickerView(configuration: .init(date: nil, timeZone: .current, updated: { _ in })) + } +} + +#Preview("Other Time Zone") { + NavigationView { + PublishDatePickerView(configuration: .init(date: Date(), timeZone: .init(identifier: "Europe/London")!, updated: { _ in })) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift index dbfb2dc95ee0..0c662af3d4f0 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift @@ -57,6 +57,7 @@ struct PublishSettingsViewModel { private let post: AbstractPost + var isRequired: Bool { (post.original ?? post).status == .publish } let dateFormatter: DateFormatter let dateTimeFormatter: DateFormatter @@ -227,7 +228,7 @@ private struct DateAndTimeRow: ImmuTableRow { func dateTimeCalendarViewController(with model: PublishSettingsViewModel) -> (ImmuTableRow) -> UIViewController { return { [weak self] _ in - let viewController = SchedulingDatePickerViewController.make(viewModel: model) { [weak self] date in + let viewController = PublishDatePickerViewController.make(viewModel: model) { [weak self] date in WPAnalytics.track(.editorPostScheduledChanged, properties: ["via": "settings"]) self?.viewModel.setDate(date) NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: ImmuTableViewController.modelChangedNotification), object: nil) diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift deleted file mode 100644 index 0ad844896403..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/SchedulingDatePickerViewController.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import Gridicons -import UIKit - -struct SchedulingDatePickerConfiguration { - var date: Date? - var timeZone: TimeZone - var updated: (Date?) -> Void -} - -final class SchedulingDatePickerViewController: UIViewController { - var configuration: SchedulingDatePickerConfiguration? - - private lazy var datePickerView: UIDatePicker = { - let datePicker = UIDatePicker() - datePicker.preferredDatePickerStyle = .inline - datePicker.calendar = Calendar.current - if let timeZone = configuration?.timeZone { - datePicker.timeZone = timeZone - } - datePicker.date = configuration?.date ?? Date() - datePicker.translatesAutoresizingMaskIntoConstraints = false - datePicker.addTarget(self, action: #selector(datePickerValueChanged(sender:)), for: .valueChanged) - datePicker.tintColor = UIColor.primary - return datePicker - }() - - override func viewDidLoad() { - super.viewDidLoad() - - title = Strings.title - - datePickerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(datePickerView) - NSLayoutConstraint.activate([ - datePickerView.topAnchor.constraint(equalTo: view.topAnchor), - datePickerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - datePickerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - view.backgroundColor = .systemBackground - - updateNavigationItems() - } - - @objc private func buttonNowTapped() { - setDate(nil) - navigationController?.popViewController(animated: true) - } - - @objc private func datePickerValueChanged(sender: UIDatePicker) { - setDate(sender.date) - } - - private func setDate(_ date: Date?) { - configuration?.date = date - configuration?.updated(date) - updateNavigationItems() - } - - private func updateNavigationItems() { - if configuration?.date != nil { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: Strings.now, style: .plain, target: self, action: #selector(buttonNowTapped)) - } else { - navigationItem.rightBarButtonItem = nil - } - } -} - -extension SchedulingDatePickerViewController { - static func make(viewModel: PublishSettingsViewModel, onDateUpdated: @escaping (Date?) -> Void) -> SchedulingDatePickerViewController { - let viewController = SchedulingDatePickerViewController() - viewController.configuration = SchedulingDatePickerConfiguration( - date: viewModel.date, - timeZone: viewModel.timeZone, - updated: onDateUpdated - ) - return viewController - } -} - -private enum Strings { - static let title = NSLocalizedString("publishDatePicker.title", value: "Publish Date", comment: "Post publish date picker") - static let now = NSLocalizedString("publishDatePicker.now", value: "Now", comment: "The Now button that clears the date selection") -} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ec3b266da040..e42635492e4a 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -260,8 +260,8 @@ 02BE5CC02281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BE5CBF2281B53F00E351BA /* RegisterDomainDetailsViewModelLoadingStateTests.swift */; }; 02BF30532271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BF30522271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift */; }; 02D75D9922793EA2003FF09A /* BlogDetailsSectionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D75D9822793EA2003FF09A /* BlogDetailsSectionFooterView.swift */; }; - 03216EC6279946CA00D444CA /* SchedulingDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */; }; - 03216EC7279946CA00D444CA /* SchedulingDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */; }; + 03216EC6279946CA00D444CA /* PublishDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216EC5279946CA00D444CA /* PublishDatePickerViewController.swift */; }; + 03216EC7279946CA00D444CA /* PublishDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03216EC5279946CA00D444CA /* PublishDatePickerViewController.swift */; }; 069A4AA62664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; 069A4AA72664448F00413FA9 /* GutenbergFeaturedImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */; }; 080C44A91CE14A9F00B3A02F /* MenuDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */; }; @@ -6041,7 +6041,7 @@ 02BF30522271D7F000616558 /* DomainCreditRedemptionSuccessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainCreditRedemptionSuccessViewController.swift; sourceTree = ""; }; 02BF978AFC1EFE50CFD558C2 /* Pods-JetpackStatsWidgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release.xcconfig"; sourceTree = ""; }; 02D75D9822793EA2003FF09A /* BlogDetailsSectionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDetailsSectionFooterView.swift; sourceTree = ""; }; - 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchedulingDatePickerViewController.swift; sourceTree = ""; }; + 03216EC5279946CA00D444CA /* PublishDatePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishDatePickerViewController.swift; sourceTree = ""; }; 069A4AA52664448F00413FA9 /* GutenbergFeaturedImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFeaturedImageHelper.swift; sourceTree = ""; }; 080C449D1CE14A9F00B3A02F /* MenuDetailsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MenuDetailsViewController.h; sourceTree = ""; }; 080C449E1CE14A9F00B3A02F /* MenuDetailsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MenuDetailsViewController.m; sourceTree = ""; }; @@ -18175,7 +18175,7 @@ F511F8A32356A4F400895E73 /* PublishSettingsViewController.swift */, F5660D06235D114500020B1E /* CalendarCollectionView.swift */, F5660D08235D1CDD00020B1E /* CalendarMonthView.swift */, - 03216EC5279946CA00D444CA /* SchedulingDatePickerViewController.swift */, + 03216EC5279946CA00D444CA /* PublishDatePickerViewController.swift */, F59AAC15235EA46D00385EE6 /* LightNavigationController.swift */, F57402A6235FF9C300374346 /* SchedulingDate+Helpers.swift */, ); @@ -23161,7 +23161,7 @@ 0CDEC40C2A2FAF0500BB3A91 /* DashboardBlazeCampaignsCardView.swift in Sources */, FAD7626429F1480E00C09583 /* DashboardActivityLogCardCell+ActivityPresenter.swift in Sources */, 24351254264DCA08009BB2B6 /* Secrets.swift in Sources */, - 03216EC6279946CA00D444CA /* SchedulingDatePickerViewController.swift in Sources */, + 03216EC6279946CA00D444CA /* PublishDatePickerViewController.swift in Sources */, 7E4123C420F4097B00DF8486 /* FormattableContentActionCommand.swift in Sources */, 17A28DCB2052FB5D00EA6D9E /* AuthorFilterViewController.swift in Sources */, 3F946C592684DD8E00B946F6 /* BloggingRemindersActions.swift in Sources */, @@ -24405,7 +24405,7 @@ FABB212F2602FC2C00C8785C /* PostingActivityViewController.swift in Sources */, 4AD5656D28E3D0670054C676 /* ReaderPost+Helper.swift in Sources */, FABB21302602FC2C00C8785C /* PostCardStatusViewModel.swift in Sources */, - 03216EC7279946CA00D444CA /* SchedulingDatePickerViewController.swift in Sources */, + 03216EC7279946CA00D444CA /* PublishDatePickerViewController.swift in Sources */, 018635852A8109DE00915532 /* SupportChatBotViewController.swift in Sources */, FAD2544226116CEA00EDAF88 /* AppStyleGuide.swift in Sources */, FABB21312602FC2C00C8785C /* SharingAuthorizationHelper.m in Sources */, From 32998731bfa9f1a23fe8b9ba089157cf8079fe70 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 16:41:55 -0400 Subject: [PATCH 049/116] Fix notices in PostCoordinator --- .../Services/PostCoordinator+Notices.swift | 17 +++++++++-------- .../Classes/Services/PostCoordinator.swift | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator+Notices.swift b/WordPress/Classes/Services/PostCoordinator+Notices.swift index 35efbd9be7b4..b8e1fb46e8aa 100644 --- a/WordPress/Classes/Services/PostCoordinator+Notices.swift +++ b/WordPress/Classes/Services/PostCoordinator+Notices.swift @@ -1,7 +1,7 @@ import UIKit extension PostCoordinator { - static func makeUploadSuccessNotice(for post: AbstractPost, isExistingPost: Bool = false) -> Notice { + static func makeUploadSuccessNotice(for post: AbstractPost, previousStatus: AbstractPost.Status? = nil) -> Notice { var message: String { let title = post.titleForDisplay() ?? "" if !title.isEmpty { @@ -10,18 +10,19 @@ extension PostCoordinator { return post.blog.displayURL as String? ?? "" } let isPublished = post.status == .publish - return Notice(title: Strings.publishSuccessTitle(for: post, isExistingPost: isExistingPost), + let isUpdated = post.status == previousStatus + return Notice(title: Strings.publishSuccessTitle(for: post, isUpdated: isUpdated), message: message, feedbackType: .success, - notificationInfo: makeUploadSuccessNotificationInfo(for: post, isExistingPost: isExistingPost), + notificationInfo: makeUploadSuccessNotificationInfo(for: post, isUpdated: isUpdated), actionTitle: isPublished ? Strings.view : nil, actionHandler: { _ in PostNoticeNavigationCoordinator.presentPostEpilogue(for: post) }) } - private static func makeUploadSuccessNotificationInfo(for post: AbstractPost, isExistingPost: Bool) -> NoticeNotificationInfo { - let status = Strings.publishSuccessTitle(for: post, isExistingPost: isExistingPost) + private static func makeUploadSuccessNotificationInfo(for post: AbstractPost, isUpdated: Bool) -> NoticeNotificationInfo { + let status = Strings.publishSuccessTitle(for: post, isUpdated: isUpdated) var title: String { let title = post.titleForDisplay() ?? "" guard !title.isEmpty else { @@ -46,10 +47,10 @@ extension PostCoordinator { private enum Strings { static let view = NSLocalizedString("postNotice.view", value: "View", comment: "Button title. Displays a summary / sharing screen for a specific post.") - static func publishSuccessTitle(for post: AbstractPost, isExistingPost: Bool = false) -> String { + static func publishSuccessTitle(for post: AbstractPost, isUpdated: Bool = false) -> String { switch post { case let post as Post: - guard !isExistingPost else { + guard !isUpdated else { return NSLocalizedString("postNotice.postUpdated", value: "Post updated", comment: "Title of notification displayed when a post has been successfully updated.") } switch post.status { @@ -63,7 +64,7 @@ private enum Strings { return NSLocalizedString("postNotice.postPublished", value: "Post published", comment: "Title of notification displayed when a post has been successfully published.") } case let page as Page: - guard !isExistingPost else { + guard !isUpdated else { return NSLocalizedString("postNotice.pageUpdated", value: "Page updated", comment: "Title of notification displayed when a page has been successfully updated.") } switch page.status { diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index b87cf8fc2b32..8c3e4d6f9fca 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -228,9 +228,9 @@ class PostCoordinator: NSObject { defer { resumeSyncing(for: post) } do { - let isExistingPost = post.hasRemote() + let previousStatus = post.status try await PostRepository()._save(post, changes: changes) - show(PostCoordinator.makeUploadSuccessNotice(for: post, isExistingPost: isExistingPost)) + show(PostCoordinator.makeUploadSuccessNotice(for: post, previousStatus: previousStatus)) return post } catch { trackError(error, operation: "post-save") From 81fd7d58b5e258f28aacc886c9bba3646b24ba7d Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 16:43:09 -0400 Subject: [PATCH 050/116] Remove unused PublishSettingsViewController --- .../PublishSettingsViewController.swift | 155 ------------------ 1 file changed, 155 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift index 0c662af3d4f0..043ba80a838c 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishSettingsViewController.swift @@ -3,10 +3,6 @@ import CocoaLumberjack import WordPressShared import WordPressFlux -enum PublishSettingsCell: CaseIterable { - case dateTime -} - struct PublishSettingsViewModel { enum State { case scheduled(Date) @@ -73,15 +69,6 @@ struct PublishSettingsViewModel { dateTimeFormatter = SiteDateFormatters.dateFormatter(for: timeZone, dateStyle: .medium, timeStyle: .short) } - var cells: [PublishSettingsCell] { - switch state { - case .published, .immediately: - return [PublishSettingsCell.dateTime] - case .scheduled: - return PublishSettingsCell.allCases - } - } - var date: Date? { switch state { case .scheduled(let date), .published(let date): @@ -122,145 +109,3 @@ struct PublishSettingsViewModel { state = State(post: post) } } - -private struct DateAndTimeRow: ImmuTableRow { - static let cell = ImmuTableCell.class(WPTableViewCellValue1.self) - - let title: String - let detail: String - let action: ImmuTableAction? - let accessibilityIdentifier: String - - init(title: String, detail: String, accessibilityIdentifier: String, action: @escaping ImmuTableAction) { - self.title = title - self.detail = detail - self.accessibilityIdentifier = accessibilityIdentifier - self.action = action - } - - func configureCell(_ cell: UITableViewCell) { - cell.textLabel?.text = title - cell.detailTextLabel?.text = detail - cell.selectionStyle = .none - cell.accessoryType = .none - cell.accessibilityIdentifier = accessibilityIdentifier - - WPStyleGuide.configureTableViewCell(cell) - } -} - -@objc class PublishSettingsController: NSObject, SettingsController { - var trackingKey: String { - return "publish_settings" - } - - @objc class func viewController(post: AbstractPost) -> ImmuTableViewController { - let controller = PublishSettingsController(post: post) - let viewController = ImmuTableViewController(controller: controller, style: .insetGrouped) - controller.viewController = viewController - return viewController - } - - var noticeMessage: String? - - let title = NSLocalizedString("Publish", comment: "Title for the publish settings view") - - var immuTableRows: [ImmuTableRow.Type] { - return [ - EditableTextRow.self - ] - } - - private weak var viewController: ImmuTableViewController? - - private var viewModel: PublishSettingsViewModel - - init(post: AbstractPost) { - viewModel = PublishSettingsViewModel(post: post) - } - - func tableViewModelWithPresenter(_ presenter: ImmuTablePresenter) -> ImmuTable { - return mapViewModel(viewModel, presenter: presenter) - } - - func refreshModel() { - // Don't need to refresh the model here - // This method is required by SettingsController but we don't need to respond to external updates on this screen - } - - func mapViewModel(_ viewModel: PublishSettingsViewModel, presenter: ImmuTablePresenter) -> ImmuTable { - - let rows: [ImmuTableRow] = viewModel.cells.map { cell in - switch cell { - case .dateTime: - return DateAndTimeRow( - title: NSLocalizedString("Date and Time", comment: "Date and Time"), - detail: viewModel.detailString, - accessibilityIdentifier: "Date and Time Row", - action: UIDevice.isPad() ? presenter.present(dateTimeCalendarViewController(with: viewModel)) : presenter.push(dateTimeCalendarViewController(with: viewModel)) - ) - } - } - - let footerText: String? - - if let date = viewModel.date { - let publishedOnString = viewModel.dateTimeFormatter.string(from: date) - - let offsetInHours = viewModel.timeZone.secondsFromGMT(for: date) / 60 / 60 - let offsetTimeZone = OffsetTimeZone(offset: Float(offsetInHours)) - let offsetLabel = offsetTimeZone.label - - switch viewModel.state { - case .scheduled, .immediately: - footerText = String.localizedStringWithFormat("Post will be published on %@ in your site timezone (%@)", publishedOnString, offsetLabel) - case .published: - footerText = String.localizedStringWithFormat("Post was published on %@ in your site timezone (%@)", publishedOnString, offsetLabel) - } - } else { - footerText = nil - } - - return ImmuTable(sections: [ - ImmuTableSection(rows: rows, footerText: footerText) - ]) - } - - func dateTimeCalendarViewController(with model: PublishSettingsViewModel) -> (ImmuTableRow) -> UIViewController { - return { [weak self] _ in - let viewController = PublishDatePickerViewController.make(viewModel: model) { [weak self] date in - WPAnalytics.track(.editorPostScheduledChanged, properties: ["via": "settings"]) - self?.viewModel.setDate(date) - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: ImmuTableViewController.modelChangedNotification), object: nil) - } - - if UIDevice.isPad() { - let navigation = UINavigationController(rootViewController: viewController) - - if UIAccessibility.isVoiceOverRunning { - let closeButton = UIBarButtonItem(systemItem: .close, primaryAction: .init(handler: { [weak navigation] _ in - navigation?.dismiss(animated: true) - })) - viewController.navigationItem.leftBarButtonItem = closeButton - } - - navigation.modalPresentationStyle = .popover - if let popoverController = navigation.popoverPresentationController { - popoverController.sourceView = self?.viewController?.tableView - popoverController.sourceRect = self?.rectForSelectedRow() ?? .zero - } - return navigation - } - - return viewController - } - } - - private func rectForSelectedRow() -> CGRect? { - guard let viewController = viewController, - let selectedIndexPath = viewController.tableView.indexPathForSelectedRow else { - return nil - } - return viewController.tableView.rectForRow(at: selectedIndexPath) - } -} From 24323362853f45a47dd8f7cb886332611749a070 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 17:26:34 -0400 Subject: [PATCH 051/116] Update new prepublishing sheet to use modern navigation bar styles --- .../Extensions/UINavigationController+Helpers.swift | 9 +++++++++ .../Post/Prepublishing/PrepublishingViewController.swift | 6 +++++- .../Scheduling/PublishDatePickerViewController.swift | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Extensions/UINavigationController+Helpers.swift b/WordPress/Classes/Extensions/UINavigationController+Helpers.swift index 13a3e306f6e6..663abda61d80 100644 --- a/WordPress/Classes/Extensions/UINavigationController+Helpers.swift +++ b/WordPress/Classes/Extensions/UINavigationController+Helpers.swift @@ -54,11 +54,20 @@ extension UINavigationController { extension UIViewController { func configureDefaultNavigationBarAppearance() { + var textAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.appBarText] + let largeTitleTextAttributes: [NSAttributedString.Key: Any] = [.font: WPStyleGuide.navigationBarLargeFont] + + textAttributes[.font] = WPStyleGuide.navigationBarStandardFont + let standardAppearance = UINavigationBarAppearance() standardAppearance.configureWithDefaultBackground() + standardAppearance.titleTextAttributes = textAttributes + standardAppearance.largeTitleTextAttributes = largeTitleTextAttributes let scrollEdgeAppearance = UINavigationBarAppearance() scrollEdgeAppearance.configureWithTransparentBackground() + scrollEdgeAppearance.titleTextAttributes = textAttributes + scrollEdgeAppearance.largeTitleTextAttributes = largeTitleTextAttributes navigationItem.standardAppearance = standardAppearance navigationItem.compactAppearance = standardAppearance diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 84b7459642dc..2890f268ba30 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -72,6 +72,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource func presentAsSheet(from presentingViewController: UIViewController) { let navigationController = UINavigationController(rootViewController: self) + navigationController.navigationBar.isTranslucent = true // Reset to default if UIDevice.isPad() { navigationController.modalPresentationStyle = .formSheet } else { @@ -351,7 +352,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource (self.post as! Post).tags = tags self.reloadData() } - + tagPickerViewController.configureDefaultNavigationBarAppearance() navigationController?.pushViewController(tagPickerViewController, animated: true) } @@ -371,6 +372,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource categoriesViewController.onCategoriesChanged = { [weak self] in self?.tableView.reloadData() } + categoriesViewController.configureDefaultNavigationBarAppearance() navigationController?.pushViewController(categoriesViewController, animated: true) } @@ -394,6 +396,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource } let viewController = UIHostingController(rootView: view) viewController.title = PostVisibilityPicker.title + viewController.configureDefaultNavigationBarAppearance() navigationController?.pushViewController(viewController, animated: true) } @@ -418,6 +421,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource self?.updatePublishButtonLabel() } let viewController = PublishDatePickerViewController(configuration: configuration) + viewController.configureDefaultNavigationBarAppearance() navigationController?.pushViewController(viewController, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index 4896c92b50bc..ccfe4db20108 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -80,6 +80,7 @@ struct PublishDatePickerView: View { } } } + .padding(.top, -8) .environment(\.defaultMinListHeaderHeight, 0) .navigationTitle(Strings.title) .navigationBarTitleDisplayMode(.inline) From 8b60d64cef8276fc654d9b08de376d2157c4d772 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 17:37:56 -0400 Subject: [PATCH 052/116] Pop the screen when date is removed --- .../Post/Prepublishing/PrepublishingViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 2890f268ba30..54428dcb9539 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -419,6 +419,9 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource self?.viewModel.publishDate = date self?.reloadData() self?.updatePublishButtonLabel() + if date == nil { + self?.navigationController?.popViewController(animated: true) + } } let viewController = PublishDatePickerViewController(configuration: configuration) viewController.configureDefaultNavigationBarAppearance() From 34c96039a66e912f4c50b238f36f33b0be2efeac Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 22 Apr 2024 17:38:25 -0400 Subject: [PATCH 053/116] Fix button clear style --- .../Post/Scheduling/PublishDatePickerViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index ccfe4db20108..11216f3e0555 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -107,7 +107,9 @@ struct PublishDatePickerView: View { configuration.date = nil }) { Image(systemName: "minus.circle.fill") + .foregroundStyle(.red) } + .buttonStyle(.plain) } } } From d8b64b2d6a8b3cf1d48d40ea2e462fd15dbaf61f Mon Sep 17 00:00:00 2001 From: Chris McGraw <2454408+wargcm@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:45:11 -0400 Subject: [PATCH 054/116] Add actions to Reader tag cells --- .../ViewRelated/Reader/ReaderHelpers.swift | 3 + .../ViewRelated/Reader/ReaderMenuAction.swift | 12 +- .../Reader/ReaderShowMenuAction.swift | 11 +- .../Reader/ReaderStreamViewController.swift | 2 +- .../Reader/ReaderTagCardCell.swift | 117 ++++++------------ .../ViewRelated/Reader/ReaderTagCardCell.xib | 3 + .../Reader/ReaderTagCardCellViewModel.swift | 100 +++++++++++++++ .../ViewRelated/Reader/ReaderTagCell.swift | 44 ++++++- .../ViewRelated/Reader/ReaderTagCell.xib | 6 + .../Reader/ReaderTagCellViewModel.swift | 46 +++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 12 ++ 11 files changed, 263 insertions(+), 93 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift create mode 100644 WordPress/Classes/ViewRelated/Reader/ReaderTagCellViewModel.swift diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderHelpers.swift b/WordPress/Classes/ViewRelated/Reader/ReaderHelpers.swift index e0fb342ee1ed..d43ea891f6b4 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderHelpers.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderHelpers.swift @@ -35,6 +35,7 @@ struct ReaderNotificationKeys { enum ReaderPostMenuSource { case card case details + case tagCard var description: String { switch self { @@ -42,6 +43,8 @@ enum ReaderPostMenuSource { return "post_card" case .details: return "post_details" + case .tagCard: + return "post_tag_card" } } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderMenuAction.swift b/WordPress/Classes/ViewRelated/Reader/ReaderMenuAction.swift index 153b92591b73..5dad962a93d3 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderMenuAction.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderMenuAction.swift @@ -11,7 +11,8 @@ final class ReaderMenuAction { anchor: UIView, vc: UIViewController, source: ReaderPostMenuSource, - followCommentsService: FollowCommentsService + followCommentsService: FollowCommentsService, + showAdditionalItems: Bool = false ) { self.execute( post: post, @@ -20,7 +21,8 @@ final class ReaderMenuAction { anchor: .view(anchor), vc: vc, source: source, - followCommentsService: followCommentsService + followCommentsService: followCommentsService, + showAdditionalItems: showAdditionalItems ) } @@ -30,7 +32,8 @@ final class ReaderMenuAction { anchor: ReaderShowMenuAction.PopoverAnchor, vc: UIViewController, source: ReaderPostMenuSource, - followCommentsService: FollowCommentsService + followCommentsService: FollowCommentsService, + showAdditionalItems: Bool = false ) { let siteTopic: ReaderSiteTopic? = post.isFollowing ? (try? ReaderSiteTopic.lookup(withSiteID: post.siteID, in: context)) : nil @@ -42,7 +45,8 @@ final class ReaderMenuAction { anchor: anchor, vc: vc, source: source, - followCommentsService: followCommentsService + followCommentsService: followCommentsService, + showAdditionalItems: showAdditionalItems ) } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderShowMenuAction.swift b/WordPress/Classes/ViewRelated/Reader/ReaderShowMenuAction.swift index 82ab8b5a28c0..9d72ed702aed 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderShowMenuAction.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderShowMenuAction.swift @@ -14,7 +14,8 @@ final class ReaderShowMenuAction { anchor: PopoverAnchor, vc: UIViewController, source: ReaderPostMenuSource, - followCommentsService: FollowCommentsService + followCommentsService: FollowCommentsService, + showAdditionalItems: Bool = false ) { // Create the action sheet @@ -22,7 +23,7 @@ final class ReaderShowMenuAction { alertController.addCancelActionWithTitle(ReaderPostMenuButtonTitles.cancel, handler: nil) // Block site button - if shouldShowBlockSiteMenuItem(readerTopic: readerTopic, post: post) { + if shouldShowBlockSiteMenuItem(readerTopic: readerTopic, post: post) || showAdditionalItems { let handler: (UIAlertAction) -> Void = { action in guard let post: ReaderPost = ReaderActionHelpers.existingObject(for: post.objectID, in: context) else { return @@ -43,7 +44,7 @@ final class ReaderShowMenuAction { } // Block user button - if shouldShowBlockUserMenuItem(topic: readerTopic, post: post) { + if shouldShowBlockUserMenuItem(topic: readerTopic, post: post) || showAdditionalItems { let handler: (UIAlertAction) -> Void = { _ in guard let post: ReaderPost = ReaderActionHelpers.existingObject(for: post.objectID, in: context) else { return @@ -68,7 +69,7 @@ final class ReaderShowMenuAction { } // Report post button - if shouldShowReportPostMenuItem(readerTopic: readerTopic, post: post) { + if shouldShowReportPostMenuItem(readerTopic: readerTopic, post: post) || showAdditionalItems { alertController.addActionWithTitle(ReaderPostMenuButtonTitles.reportPost, style: .destructive, handler: { (action: UIAlertAction) in @@ -79,7 +80,7 @@ final class ReaderShowMenuAction { } // Report user button - if shouldShowReportUserMenuItem(readerTopic: readerTopic, post: post) { + if shouldShowReportUserMenuItem(readerTopic: readerTopic, post: post) || showAdditionalItems { let handler: (UIAlertAction) -> Void = { _ in guard let post: ReaderPost = ReaderActionHelpers.existingObject(for: post.objectID, in: context) else { return diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift index bfb656e7b22a..1b124aea883a 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift @@ -1624,7 +1624,7 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { func cell(for tag: ReaderTagTopic) -> UITableViewCell { let cell = tableConfiguration.tagCell(tableView) - cell.configure(with: tag, isLoggedIn: isLoggedIn) + cell.configure(parent: self, tag: tag, isLoggedIn: isLoggedIn) cell.selectionStyle = .none return cell } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift index f3254d3ae7d6..a52489884646 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift @@ -1,40 +1,31 @@ -class ReaderTagCardCell: UITableViewCell, UICollectionViewDelegate { - - private typealias DataSource = UICollectionViewDiffableDataSource - private typealias Snapshot = NSDiffableDataSourceSnapshot +class ReaderTagCardCell: UITableViewCell { @IBOutlet private weak var tagButton: UIButton! @IBOutlet private weak var collectionView: UICollectionView! - @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint! + @IBOutlet private weak var collectionViewHeightConstraint: NSLayoutConstraint! + + private var viewModel: ReaderTagCardCellViewModel? + + var cellSize: CGSize { + let isAccessibilityCategory = traitCollection.preferredContentSizeCategory.isAccessibilityCategory + let isPad = traitCollection.userInterfaceIdiom == .pad - private lazy var dataSource: DataSource = { - DataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, objectID in - guard let post = try? ContextManager.shared.mainContext.existingObject(with: objectID) as? ReaderPost, - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellIdentifier, for: indexPath) as? ReaderTagCell else { - return UICollectionViewCell() - } - cell.configure(with: post, isLoggedIn: self?.isLoggedIn ?? AccountHelper.isLoggedIn) - return cell + switch (isAccessibilityCategory, isPad) { + case (true, true): + return Constants.padLargeCellSize + case (false, true): + return Constants.padDefaultCellSize + case (true, false): + return Constants.phoneLargeCellSize + case (false, false): + return Constants.phoneDefaultCellSize } - }() - private lazy var resultsController: NSFetchedResultsController = { - let fetchRequest = NSFetchRequest(entityName: ReaderPost.classNameWithoutNamespaces()) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sortRank", ascending: true)] - fetchRequest.fetchLimit = Constants.displayPostLimit - let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, - managedObjectContext: ContextManager.shared.mainContext, - sectionNameKeyPath: nil, - cacheName: nil) - resultsController.delegate = self - return resultsController - }() - private var isLoggedIn: Bool = false + } override func awakeFromNib() { super.awakeFromNib() registerTagCell() setupButtonStyles() - collectionView.delegate = self accessibilityElements = [tagButton, collectionView].compactMap { $0 } collectionViewHeightConstraint.constant = cellSize.height } @@ -44,34 +35,34 @@ class ReaderTagCardCell: UITableViewCell, UICollectionViewDelegate { collectionViewHeightConstraint.constant = cellSize.height } - func configure(with tag: ReaderTagTopic, isLoggedIn: Bool) { - self.isLoggedIn = isLoggedIn + func configure(parent: UIViewController, tag: ReaderTagTopic, isLoggedIn: Bool) { + weak var weakSelf = self + viewModel = ReaderTagCardCellViewModel(parent: parent, + tag: tag, + collectionView: collectionView, + isLoggedIn: isLoggedIn, + cellSize: weakSelf?.cellSize) + viewModel?.fetchTagTopics() tagButton.setTitle(tag.title, for: .normal) - resultsController.fetchRequest.predicate = NSPredicate(format: "topic = %@ AND isSiteBlocked = NO", tag) - try? resultsController.performFetch() } + + @IBAction private func onTagButtonTapped(_ sender: Any) { + viewModel?.onTagButtonTapped() + } + + struct Constants { + static let phoneDefaultCellSize = CGSize(width: 240, height: 297) + static let phoneLargeCellSize = CGSize(width: 240, height: 500) + static let padDefaultCellSize = CGSize(width: 480, height: 600) + static let padLargeCellSize = CGSize(width: 480, height: 900) + } + } // MARK: - Private methods private extension ReaderTagCardCell { - var cellSize: CGSize { - let isAccessibilityCategory = traitCollection.preferredContentSizeCategory.isAccessibilityCategory - let isPad = traitCollection.userInterfaceIdiom == .pad - - switch (isAccessibilityCategory, isPad) { - case (true, true): - return Constants.padLargeCellSize - case (false, true): - return Constants.padDefaultCellSize - case (true, false): - return Constants.phoneLargeCellSize - case (false, false): - return Constants.phoneDefaultCellSize - } - } - func setupButtonStyles() { var buttonConfig = UIButton.Configuration.filled() buttonConfig.cornerStyle = .capsule @@ -86,37 +77,7 @@ private extension ReaderTagCardCell { func registerTagCell() { let nib = UINib(nibName: ReaderTagCell.classNameWithoutNamespaces(), bundle: nil) - collectionView.register(nib, forCellWithReuseIdentifier: Constants.cellIdentifier) - } - - struct Constants { - static let cellIdentifier = ReaderTagCell.classNameWithoutNamespaces() - static let displayPostLimit = 10 - static let phoneDefaultCellSize = CGSize(width: 240, height: 297) - static let phoneLargeCellSize = CGSize(width: 240, height: 500) - static let padDefaultCellSize = CGSize(width: 480, height: 600) - static let padLargeCellSize = CGSize(width: 480, height: 900) - } - -} - -// MARK: - NSFetchedResultsControllerDelegate - -extension ReaderTagCardCell: NSFetchedResultsControllerDelegate { - - func controller(_ controller: NSFetchedResultsController, - didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - dataSource.apply(snapshot as Snapshot, animatingDifferences: false) - } - -} - -// MARK: - UICollectionViewDelegateFlowLayout - -extension ReaderTagCardCell: UICollectionViewDelegateFlowLayout { - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return cellSize + collectionView.register(nib, forCellWithReuseIdentifier: ReaderTagCell.classNameWithoutNamespaces()) } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.xib b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.xib index 3a99bc3a7f31..a157831b3d78 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.xib +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.xib @@ -20,6 +20,9 @@ + + + diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift new file mode 100644 index 000000000000..b57a0dfdec41 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -0,0 +1,100 @@ + +class ReaderTagCardCellViewModel: NSObject { + + private typealias DataSource = UICollectionViewDiffableDataSource + private typealias Snapshot = NSDiffableDataSourceSnapshot + + private weak var parentViewController: UIViewController? + private let slug: String + private weak var collectionView: UICollectionView? + private let isLoggedIn: Bool + private let cellSize: () -> CGSize? + + private lazy var dataSource: DataSource? = { + guard let collectionView else { + return nil + } + return DataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, objectID in + guard let post = try? ContextManager.shared.mainContext.existingObject(with: objectID) as? ReaderPost, + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReaderTagCell.classNameWithoutNamespaces(), for: indexPath) as? ReaderTagCell else { + return UICollectionViewCell() + } + cell.configure(parent: self?.parentViewController, + post: post, + isLoggedIn: self?.isLoggedIn ?? AccountHelper.isLoggedIn) + return cell + } + }() + + private lazy var resultsController: NSFetchedResultsController = { + let fetchRequest = NSFetchRequest(entityName: ReaderPost.classNameWithoutNamespaces()) + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sortRank", ascending: false)] + fetchRequest.fetchLimit = Constants.displayPostLimit + let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, + managedObjectContext: ContextManager.shared.mainContext, + sectionNameKeyPath: nil, + cacheName: nil) + resultsController.delegate = self + return resultsController + }() + + init(parent: UIViewController?, tag: ReaderTagTopic, collectionView: UICollectionView?, isLoggedIn: Bool, cellSize: @escaping @autoclosure () -> CGSize?) { + self.parentViewController = parent + self.slug = tag.slug + self.collectionView = collectionView + self.isLoggedIn = isLoggedIn + self.cellSize = cellSize + + super.init() + + resultsController.fetchRequest.predicate = NSPredicate(format: "topic = %@ AND isSiteBlocked = NO", tag) + collectionView?.delegate = self + } + + func fetchTagTopics() { + try? resultsController.performFetch() + } + + func onTagButtonTapped() { + let controller = ReaderStreamViewController.controllerWithTagSlug(slug) + parentViewController?.navigationController?.pushViewController(controller, animated: true) + } + + struct Constants { + static let displayPostLimit = 10 + } + +} + +// MARK: - NSFetchedResultsControllerDelegate + +extension ReaderTagCardCellViewModel: NSFetchedResultsControllerDelegate { + + func controller(_ controller: NSFetchedResultsController, + didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + dataSource?.apply(snapshot as Snapshot, animatingDifferences: false) + } + +} + +// MARK: - UICollectionViewDelegate + +extension ReaderTagCardCellViewModel: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let post = resultsController.object(at: indexPath) + let controller = ReaderDetailViewController.controllerWithPost(post) + parentViewController?.navigationController?.pushViewController(controller, animated: true) + } + +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension ReaderTagCardCellViewModel: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return cellSize() ?? .zero + } + +} diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift index e1e079d2b2d9..845133758be1 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift @@ -12,15 +12,14 @@ class ReaderTagCell: UICollectionViewCell { @IBOutlet private weak var menuButton: UIButton! private lazy var imageLoader = ImageLoader(imageView: featuredImageView) + private var viewModel: ReaderTagCellViewModel? override func awakeFromNib() { super.awakeFromNib() setupStyles() contentStackView.setCustomSpacing(0, after: featuredImageView) - likeButton.setTitle(NSLocalizedString("reader.tags.button.like", - value: "Like", - comment: "Text for the 'Like' button on the reader tag cell."), - for: .normal) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onSiteTitleTapped)) + headerStackView.addGestureRecognizer(tapGesture) } override func prepareForReuse() { @@ -29,7 +28,9 @@ class ReaderTagCell: UICollectionViewCell { resetHiddenViews() } - func configure(with post: ReaderPost, isLoggedIn: Bool) { + func configure(parent: UIViewController?, post: ReaderPost, isLoggedIn: Bool) { + viewModel = ReaderTagCellViewModel(parent: parent, post: post, isLoggedIn: isLoggedIn) + let blogName = post.blogNameForDisplay() let postDate = post.shortDateForDisplay() let postTitle = post.titleForDisplay() @@ -47,9 +48,34 @@ class ReaderTagCell: UICollectionViewCell { titleLabel.isHidden = postTitle == nil summaryLabel.isHidden = postSummary == nil countsLabel.isHidden = postCounts == nil + + configureLikeButton(with: post) loadFeaturedImage(with: post) } + @objc private func onSiteTitleTapped() { + viewModel?.onSiteTitleTapped() + } + + @IBAction private func onLikeButtonTapped(_ sender: Any) { + viewModel?.onLikeButtonTapped() + } + + @IBAction private func onMenuButtonTapped(_ sender: UIButton) { + viewModel?.onMenuButtonTapped(with: sender) + } + + private struct Constants { + static let likeText = NSLocalizedString("reader.tags.button.like", + value: "Like", + comment: "Text for the 'Like' button on the reader tag cell.") + static let likedText = NSLocalizedString("reader.tags.button.liked", + value: "Liked", + comment: "Text for the 'Liked' button on the reader tag cell.") + static let likeButtonImage = UIImage(named: "icon-reader-star-outline")?.withRenderingMode(.alwaysTemplate) + static let likedButtonImage = UIImage(named: "icon-reader-star-fill")?.withRenderingMode(.alwaysTemplate) + } + } // MARK: - Private methods @@ -92,4 +118,12 @@ private extension ReaderTagCell { likeButton.isHidden = false } + func configureLikeButton(with post: ReaderPost) { + let isLiked = post.isLiked + likeButton.setTitle(isLiked ? Constants.likedText : Constants.likeText, for: .normal) + likeButton.setImage(isLiked ? Constants.likedButtonImage : Constants.likeButtonImage, for: .normal) + likeButton.tintColor = isLiked ? .jetpackGreen : .secondaryLabel + likeButton.setTitleColor(likeButton.tintColor, for: .normal) + } + } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib index 13063bc75865..5d0911de876b 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib @@ -81,6 +81,9 @@ + + + @@ -93,6 +96,9 @@ + + + diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCellViewModel.swift new file mode 100644 index 000000000000..d4b473bbe7af --- /dev/null +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCellViewModel.swift @@ -0,0 +1,46 @@ + +struct ReaderTagCellViewModel { + + private weak var parentViewController: UIViewController? + private let post: ReaderPost + private let isLoggedIn: Bool + private var followCommentsService: FollowCommentsService? + + init(parent: UIViewController?, post: ReaderPost, isLoggedIn: Bool) { + self.parentViewController = parent + self.post = post + self.isLoggedIn = isLoggedIn + } + + func onSiteTitleTapped() { + guard let parentViewController else { + return + } + ReaderHeaderAction().execute(post: post, origin: parentViewController) + } + + func onLikeButtonTapped() { + ReaderLikeAction().execute(with: post) + } + + mutating func onMenuButtonTapped(with anchor: UIView) { + guard let parentViewController = parentViewController as? ReaderStreamViewController, + let followCommentsService = FollowCommentsService(post: post) else { + return + } + self.followCommentsService = followCommentsService + + ReaderMenuAction(logged: isLoggedIn).execute( + post: post, + context: parentViewController.viewContext, + readerTopic: parentViewController.readerTopic, + anchor: anchor, + vc: parentViewController, + source: .tagCard, + followCommentsService: followCommentsService, + showAdditionalItems: true + ) + // TODO: Analytics + } + +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ec3b266da040..6373d5b46c6b 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2272,6 +2272,10 @@ 83A337A22A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */; }; 83B1D037282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; }; 83B1D038282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; }; + 83BF48BB2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BF48BA2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift */; }; + 83BF48BC2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BF48BA2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift */; }; + 83BF48BE2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BF48BD2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift */; }; + 83BF48BF2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BF48BD2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift */; }; 83BFAE482A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */; }; 83BFAE492A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */; }; 83BFAE502A6EBF9900C7B683 /* DashboardJetpackSocialCardCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BFAE4F2A6EBF9900C7B683 /* DashboardJetpackSocialCardCellTests.swift */; }; @@ -7661,6 +7665,8 @@ 839B150A2795DEE0009F5E77 /* UIView+Margins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Margins.swift"; sourceTree = ""; }; 83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSiteHeaderView.swift; sourceTree = ""; }; 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsAttribution.swift; sourceTree = ""; }; + 83BF48BA2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTagCardCellViewModel.swift; sourceTree = ""; }; + 83BF48BD2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTagCellViewModel.swift; sourceTree = ""; }; 83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardJetpackSocialCardCell.swift; sourceTree = ""; }; 83BFAE4F2A6EBF9900C7B683 /* DashboardJetpackSocialCardCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardJetpackSocialCardCellTests.swift; sourceTree = ""; }; 83C972DF281C45AB0049E1FE /* Post+BloggingPrompts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+BloggingPrompts.swift"; sourceTree = ""; }; @@ -12720,8 +12726,10 @@ D88106F620C0C9A8001D2F00 /* ReaderSavedPostUndoCell.xib */, 83317ED52BC71BA8001AD2F4 /* ReaderTagCardCell.swift */, 83317ED82BC71CEB001AD2F4 /* ReaderTagCardCell.xib */, + 83BF48BA2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift */, 83317EDB2BC72DB7001AD2F4 /* ReaderTagCell.swift */, 83317EDE2BC72DED001AD2F4 /* ReaderTagCell.xib */, + 83BF48BD2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift */, ); name = Cards; sourceTree = ""; @@ -22223,6 +22231,7 @@ E6F2788121BC1A4A008B4DB5 /* PlanGroup.swift in Sources */, 08216FCE1CDBF96000304BA7 /* MenuItemPostsViewController.m in Sources */, 4322A20D203E1885004EA740 /* SignupUsernameTableViewController.swift in Sources */, + 83BF48BB2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift in Sources */, 098B8576275E76FE004D299F /* AppLocalizedString.swift in Sources */, 7E7BEF7022E1AED8009A880D /* Blog+Editor.swift in Sources */, 1756F1DF2822BB6F00CD0915 /* SparklineView.swift in Sources */, @@ -22540,6 +22549,7 @@ 46C984682527863E00988BB9 /* LayoutPickerAnalyticsEvent.swift in Sources */, 7E442FCD20F6AB9C00DEACA5 /* ActivityRange.swift in Sources */, 9AF9551821A1D7970057827C /* DiffAbstractValue+Attributes.swift in Sources */, + 83BF48BE2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift in Sources */, FE50965C2A20D0F300DDD071 /* CommentTableHeaderView.swift in Sources */, 8BD66ED42787530C00CCD95A /* PostsCardViewModel.swift in Sources */, 80EF928D280E83110064A971 /* QuickStartToursCollection.swift in Sources */, @@ -24997,6 +25007,7 @@ FABB22DD2602FC2C00C8785C /* RevisionsTableViewCell.swift in Sources */, E6D6A1312683ABE6004C24A7 /* ReaderSubscribeCommentsAction.swift in Sources */, F49B9A06293A21BF000CEFCE /* MigrationAnalyticsTracker.swift in Sources */, + 83BF48BF2BD6FA3000C0E1A1 /* ReaderTagCellViewModel.swift in Sources */, E62CE58F26B1D14200C9D147 /* AccountService+Cookies.swift in Sources */, 0C391E622A3002950040EA91 /* BlazeCampaignStatusView.swift in Sources */, FABB22DF2602FC2C00C8785C /* PlanDetailViewModel.swift in Sources */, @@ -25999,6 +26010,7 @@ FECA443028350B7800D01F15 /* PromptRemindersScheduler.swift in Sources */, FABB25982602FC2C00C8785C /* JetpackRestoreHeaderView.swift in Sources */, FABB25992602FC2C00C8785C /* StoryboardLoadable.swift in Sources */, + 83BF48BC2BD6F03000C0E1A1 /* ReaderTagCardCellViewModel.swift in Sources */, FABB259A2602FC2C00C8785C /* ReaderBlockSiteAction.swift in Sources */, FABB259C2602FC2C00C8785C /* ReaderPostService+RelatedPosts.swift in Sources */, FABB259D2602FC2C00C8785C /* Blog+Capabilities.swift in Sources */, From e99a3ebe789905e53cf72d88c0716c44663a9ece Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 23 Apr 2024 13:25:46 +0300 Subject: [PATCH 055/116] Move StatSection related localized strings into StatSection --- .../ViewRelated/Stats/Helpers/StatSection.swift | 3 +++ .../Subscribers/StatsSubscribersViewModel.swift | 14 +++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift index 092bbf09e044..0eb08d3d1a72 100644 --- a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift +++ b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift @@ -450,6 +450,7 @@ static let searchTerm = NSLocalizedString("Search Term", comment: "Label for list of search term") static let period = NSLocalizedString("Period", comment: "Label for date periods.") static let file = NSLocalizedString("File", comment: "Label for list of file downloads.") + static let emailsSummary = NSLocalizedString("stats.subscribers.emailsSummary.column.title", value: "Latest emails", comment: "A title for table's column that shows a name of an email") } struct DataSubtitles { @@ -459,6 +460,8 @@ static let since = NSLocalizedString("Since", comment: "Label for time period in list of followers.") static let clicks = NSLocalizedString("Clicks", comment: "Label for number of clicks.") static let downloads = NSLocalizedString("Downloads", comment: "Label for number of file downloads.") + static let emailsSummaryOpens = NSLocalizedString("stats.subscribers.emailsSummary.column.opens", value: "Opens", comment: "A title for table's column that shows a number of email openings") + static let emailsSummaryClicks = NSLocalizedString("stats.subscribers.emailsSummary.column.clicks", value: "Clicks", comment: "A title for table's column that shows a number of times a post was opened from an email") } struct TabTitles { diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift index 5d55a741f2bb..76bb4138d665 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift @@ -62,9 +62,9 @@ private extension StatsSubscribersViewModel { case .success(let emailsSummary): return [ TopTotalsPeriodStatsRow( - itemSubtitle: Strings.titleColumn, - dataSubtitle: Strings.opensColumn, - secondDataSubtitle: Strings.clicksColumn, + itemSubtitle: StatSection.ItemSubtitles.emailsSummary, + dataSubtitle: StatSection.DataSubtitles.emailsSummaryOpens, + secondDataSubtitle: StatSection.DataSubtitles.emailsSummaryClicks, dataRows: emailsSummaryDataRows(emailsSummary), statSection: .subscribersEmailsSummary, siteStatsPeriodDelegate: viewMoreDelegate @@ -87,11 +87,3 @@ private extension StatsSubscribersViewModel { } } } - -private extension StatsSubscribersViewModel { - struct Strings { - static let titleColumn = NSLocalizedString("stats.subscribers.emailsSummary.column.title", value: "Latest emails", comment: "A title for table's column that shows a name of an email") - static let opensColumn = NSLocalizedString("stats.subscribers.emailsSummary.column.opens", value: "Opens", comment: "A title for table's column that shows a number of email openings") - static let clicksColumn = NSLocalizedString("stats.subscribers.emailsSummary.column.clicks", value: "Clicks", comment: "A title for table's column that shows a number of times a post was opened from an email") - } -} From 80b34f0acfcfd45fadf487828d6137f8866137e2 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 23 Apr 2024 13:26:24 +0300 Subject: [PATCH 056/116] Add helpers to StatsSubscribersStore State to make it easier reusable with legacy store compatible code --- .../Subscribers/StatsSubscribersStore.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift index 0f570b66c8e8..1fe9714019aa 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift @@ -56,5 +56,27 @@ extension StatsSubscribersStore { case loading case success(Value) case error + + var data: Value? { + switch self { + case .success(let data): + return data + default: + return nil + } + } + + var storeFetchingStatus: StoreFetchingStatus { + switch self { + case .idle: + return .idle + case .loading: + return .loading + case .success: + return .success + case .error: + return .error + } + } } } From 91d98d99a28322b72e3ecb0f1a4ffeaca9a17455 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 23 Apr 2024 13:28:00 +0300 Subject: [PATCH 057/116] Add subscribersEmailsSummary section support to SiteStatsDetails Use StatsSubscribersStore to updateEmailsSummary and observe emailsSummary --- .../SiteStatsDetailsViewModel.swift | 46 ++++++++++++++++++- .../Stats/SiteStatsTableViewCells.swift | 6 ++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift index 3d0cdffb337d..bc0fa39e1c7a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsDetailsViewModel.swift @@ -1,5 +1,6 @@ import Foundation import WordPressFlux +import Combine /// The view model used by SiteStatsDetailTableViewController to show /// all data for a selected stat. @@ -24,6 +25,9 @@ class SiteStatsDetailsViewModel: Observable { private var periodReceipt: Receipt? private var periodChangeReceipt: Receipt? + private let subscribersStore: StatsSubscribersStoreProtocol + private var cancellables: Set = [] + private var selectedDate: Date? private var selectedPeriod: StatsPeriodUnit? private var postID: Int? @@ -35,11 +39,13 @@ class SiteStatsDetailsViewModel: Observable { init(detailsDelegate: SiteStatsDetailsDelegate, referrerDelegate: SiteStatsReferrerDelegate, insightsStore: StatsInsightsStore, - periodStore: StatsPeriodStore) { + periodStore: StatsPeriodStore, + subscribersStore: StatsSubscribersStoreProtocol = StatsSubscribersStore()) { self.detailsDelegate = detailsDelegate self.referrerDelegate = referrerDelegate self.insightsStore = insightsStore self.periodStore = periodStore + self.subscribersStore = subscribersStore } // MARK: - Data Fetching @@ -72,6 +78,13 @@ class SiteStatsDetailsViewModel: Observable { self?.emitChange() } periodReceipt = periodStore.query(.postStats(postID: postID)) + } else if statSection == .subscribersEmailsSummary { + subscribersStore.emailsSummary + .sink { [weak self] _ in + self?.emitChange() + } + .store(in: &cancellables) + refreshEmailsSummary() } else { DDLogError("Stats Details cannot be loaded for StatSection: \(statSection)") } @@ -91,6 +104,8 @@ class SiteStatsDetailsViewModel: Observable { return true } return periodStore.fetchingFailed(for: .postStats(postID: postID)) + } else if statSection == .subscribersEmailsSummary { + return subscribersStore.emailsSummary.value == .error } else { DDLogError("Stats Details cannot be loaded for StatSection: \(statSection)") return true @@ -127,6 +142,8 @@ class SiteStatsDetailsViewModel: Observable { return periodStore.isFetchingFileDownloads case .postStatsMonthsYears, .postStatsAverageViews: return periodStore.isFetchingPostStats(for: postID) + case .subscribersEmailsSummary: + return subscribersStore.emailsSummary.value == .loading default: return false } @@ -290,6 +307,15 @@ class SiteStatsDetailsViewModel: Observable { rows.append(contentsOf: postStatsRows(forAverages: true, status: status)) return rows } + case .subscribersEmailsSummary: + return periodImmuTable(for: subscribersStore.emailsSummary.value.storeFetchingStatus) { status in + var rows = [any HashableImmutableRow]() + rows.append(DetailSubtitlesHeaderRow(itemSubtitle: StatSection.ItemSubtitles.emailsSummary, + dataSubtitle: StatSection.DataSubtitles.emailsSummaryOpens, + secondDataSubtitle: StatSection.DataSubtitles.emailsSummaryClicks)) + rows.append(contentsOf: dataRowsFor(emailsSummaryPosts(), status: status)) + return rows + } default: return ImmuTableDiffableDataSourceSnapshot() } @@ -392,6 +418,10 @@ class SiteStatsDetailsViewModel: Observable { } ActionDispatcher.dispatch(PeriodAction.refreshPeriod(query: .postStats(postID: postID))) } + + func refreshEmailsSummary() { + subscribersStore.updateEmailsSummary(quantity: 30, sortField: .opens) + } } // MARK: - Private Extension @@ -831,6 +861,20 @@ private extension SiteStatsDetailsViewModel { return yearRows } + // MARK: - Emails Summary + + func emailsSummaryPosts() -> [StatsTotalRowData] { + let emailsSummaryPosts = subscribersStore.emailsSummary.value.data?.posts ?? [] + + return emailsSummaryPosts.map { + StatsTotalRowData(name: $0.title, + data: $0.opens.abbreviatedString(), + secondData: $0.clicks.abbreviatedString(), + multiline: false, + statSection: .subscribersEmailsSummary) + } + } + // MARK: - Helpers func dataRowsFor(_ rowsData: [StatsTotalRowData], status: StoreFetchingStatus = .idle) -> [DetailDataRow] { diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index 1b6c228b26ec..e88ac2829055 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -814,6 +814,7 @@ struct DetailSubtitlesHeaderRow: HashableImmutableRow { let itemSubtitle: String let dataSubtitle: String + var secondDataSubtitle: String? = nil let action: ImmuTableAction? = nil func configureCell(_ cell: UITableViewCell) { @@ -822,12 +823,13 @@ struct DetailSubtitlesHeaderRow: HashableImmutableRow { return } - cell.configure(itemSubtitle: itemSubtitle, dataSubtitle: dataSubtitle, dataRows: [], forDetails: true) + cell.configure(itemSubtitle: itemSubtitle, dataSubtitle: dataSubtitle, secondDataSubtitle: secondDataSubtitle, dataRows: [], forDetails: true) } static func == (lhs: DetailSubtitlesHeaderRow, rhs: DetailSubtitlesHeaderRow) -> Bool { return lhs.itemSubtitle == rhs.itemSubtitle && - lhs.dataSubtitle == rhs.dataSubtitle + lhs.dataSubtitle == rhs.dataSubtitle && + lhs.secondDataSubtitle == rhs.secondDataSubtitle } } From bba514a959e7c9988ce3ed711dcf776f05767ebe Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 23 Apr 2024 13:28:29 +0300 Subject: [PATCH 058/116] Present SiteStatsDetailTableViewController from Subscribers --- .../Stats/Subscribers/StatsSubscribersViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift index f4b50d0e1c0a..ef38c88f60a6 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift @@ -78,7 +78,8 @@ final class StatsSubscribersViewController: SiteStatsBaseTableViewController { extension StatsSubscribersViewController: SiteStatsPeriodDelegate { func viewMoreSelectedForStatSection(_ statSection: StatSection) { - // TODO - DDLogInfo("\(statSection) selected") + let detailTableViewController = SiteStatsDetailTableViewController.loadFromStoryboard() + detailTableViewController.configure(statSection: statSection) + navigationController?.pushViewController(detailTableViewController, animated: true) } } From fd868003571a5bff9ab37842192847a026b4d246 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 07:45:03 -0400 Subject: [PATCH 059/116] Update UI tests --- .../Screens/Editor/EditorPostSettings.swift | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift index 8e025f64a20a..e96795157d64 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift @@ -52,13 +52,12 @@ public class EditorPostSettings: ScreenObject { } private let backButtonGetter: (XCUIApplication) -> XCUIElement? = { - $0.navigationBars.lastMatch?.buttons.element(boundBy: 0) + $0.navigationBars["Publish Date"].buttons.element(boundBy: 0) } var categoriesSection: XCUIElement { categoriesSectionGetter(app) } var chooseFromMediaButton: XCUIElement { chooseFromMediaButtonGetter(app) } var currentFeaturedImage: XCUIElement { currentFeaturedImageGetter(app) } - var dateSelector: XCUIElement { dateSelectorGetter(app) } var closeButton: XCUIElement { closeButtonGetter(app) } var backButton: XCUIElement? { backButtonGetter(app) } var featuredImageButton: XCUIElement { featuredImageButtonGetter(app) } @@ -148,7 +147,6 @@ public class EditorPostSettings: ScreenObject { @discardableResult public func updatePublishDateToFutureDate() -> Self { publishDateButton.tap() - dateSelector.tap() let currentMonth = monthLabel.value as! String // Selects the first day of the next month @@ -159,25 +157,11 @@ public class EditorPostSettings: ScreenObject { if nextMonth != currentMonth { firstCalendarDayButton.tapUntil(.selected, failureMessage: "First Day button not selected!") } - - if UIDevice.current.userInterfaceIdiom == .pad { - // Dismiss popover by tapping outside of it. There is a sheet covering - // the screen and a popover and both are "PopoverDismissRegion", so - // we need to find the first hittable. - app.otherElements.matching(identifier: "PopoverDismissRegion") - .allElementsBoundByIndex - .first(where: \.isHittable)? - .tap() - app.navigationBars["Publish"].buttons.element(boundBy: 0).tap() - } else { - app.navigationBars["Publish Date"].buttons.element(boundBy: 0).tap() - } - return self } public func closePublishDateSelector() -> Self { - navigateBack() + backButton?.tap() return self } } From a6df7447088706554c5b5bebec31cb8b0e2c2711 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 08:07:10 -0400 Subject: [PATCH 060/116] Fix an issue with a -logout-at-launch option not removing self-hosted sites --- WordPress/Classes/System/UITestConfigurator.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WordPress/Classes/System/UITestConfigurator.swift b/WordPress/Classes/System/UITestConfigurator.swift index eb102277ae41..2274b69435ee 100644 --- a/WordPress/Classes/System/UITestConfigurator.swift +++ b/WordPress/Classes/System/UITestConfigurator.swift @@ -22,10 +22,20 @@ struct UITestConfigurator { private static func logoutAtLaunch() { if CommandLine.arguments.contains("-logout-at-launch") { + removeSelfHostedSites() AccountHelper.logOutDefaultWordPressComAccount() } } + private static func removeSelfHostedSites() { + let context = ContextManager.shared.mainContext + let service = BlogService(coreDataStack: ContextManager.shared) + let blogs = try? BlogQuery().hostedByWPCom(false).blogs(in: context) + for blog in blogs ?? [] { + service.remove(blog) + } + } + private static func disableCompliancePopover() { if CommandLine.arguments.contains("-ui-testing") { UserDefaults.standard.didShowCompliancePopup = true From 0ec81429ffc5f4cc28c4a31dfcff0ea23038c4fd Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 10:04:45 -0400 Subject: [PATCH 061/116] Remove Done button from tags picker --- .../ViewRelated/Post/PostTagPickerViewController.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift index 103eed3b95ac..e3703791b4e4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift @@ -107,9 +107,6 @@ class PostTagPickerViewController: UIViewController { textViewContainer.layer.masksToBounds = false keyboardObserver.tableView = tableView - - let doneButton = UIBarButtonItem(title: NSLocalizedString("Done", comment: "Done button title"), style: .plain, target: self, action: #selector(doneButtonPressed)) - navigationItem.setRightBarButton(doneButton, animated: false) } override func viewWillAppear(_ animated: Bool) { @@ -146,10 +143,6 @@ class PostTagPickerViewController: UIViewController { textViewContainer.layer.borderColor = UIColor.divider.cgColor } - @objc func doneButtonPressed() { - navigationController?.popViewController(animated: true) - } - fileprivate func reloadTableData() { tableView.reloadData() } From e6a7f2ef491e734b90d70b17cf0da212d2ab2d86 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 10:07:46 -0400 Subject: [PATCH 062/116] Update the plus button in PostCategoriesViewController --- .../Post/Categories/PostCategoriesViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift b/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift index 5a6f8002f240..6525533f3853 100644 --- a/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Categories/PostCategoriesViewController.swift @@ -69,7 +69,7 @@ import Foundation refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshCategoriesWithInteraction), for: .valueChanged) - let rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "icon-post-add"), style: .plain, target: self, action: #selector(showAddNewCategory)) + let rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(showAddNewCategory)) switch selectionMode { case .post: From ea6fc86283f1b4de16b9664d894762133bb67b6c Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 10:14:14 -0400 Subject: [PATCH 063/116] Rename showPublishDatePicker --- .../Classes/ViewRelated/Post/PostSettingsViewController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index b7019a7d678d..d60a1a381ae1 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -599,7 +599,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else if (cell.tag == PostSettingsRowTags) { [self showTagsPicker]; } else if (cell.tag == PostSettingsRowPublishDate) { - [self showPublishSchedulingController]; + [self showPublishDatePicker]; } else if (cell.tag == PostSettingsRowStatus) { [self showPostStatusSelector]; } else if (cell.tag == PostSettingsRowVisibility) { @@ -1047,7 +1047,7 @@ - (WPTextFieldTableViewCell *)getWPTableViewTextFieldCell return cell; } -- (void)showPublishSchedulingController +- (void)showPublishDatePicker { BOOL isRequired = self.apost.status == PostStatusPublish || self.apost.status == PostStatusScheduled; UIViewController *vc = [PublishDatePickerHelper makeDatePickerWithPost:self.apost isRequired:isRequired]; From af5f41e076536021129f328dcf6e2f0889dfa529 Mon Sep 17 00:00:00 2001 From: Hassaan El-Garem Date: Tue, 23 Apr 2024 18:26:58 +0200 Subject: [PATCH 064/116] Update: remove `allRemainingSitesController` --- .../Site Picker/BlogList/BlogListView.swift | 44 ++++++++-------- .../BlogList/BlogListViewModel.swift | 52 ++----------------- 2 files changed, 28 insertions(+), 68 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index 653fd88154ae..cd33acda5696 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -13,10 +13,14 @@ struct BlogListView: View { } struct Site: Equatable { - let id: NSNumber? + let id: NSNumber let title: String let domain: String let imageURL: URL? + + static func ==(lhs: Site, rhs: Site) -> Bool { + return lhs.id == rhs.id + } } @Binding private var isEditing: Bool @@ -128,29 +132,27 @@ struct BlogListView: View { @ViewBuilder private func siteButton(site: Site) -> some View { - if let siteID = site.id { - Button { - if isEditing { - withAnimation { - viewModel.togglePinnedSite(siteID: siteID) - } - } else { - viewModel.siteSelected(siteID: siteID) - selectionCallback(siteID) + Button { + if isEditing { + withAnimation { + viewModel.togglePinnedSite(siteID: site.id) } - } label: { - siteHStack(site: site) + } else { + viewModel.siteSelected(siteID: site.id) + selectionCallback(site.id) } - .listRowSeparator(.hidden) - .buttonStyle(SelectedButtonStyle(onPress: { isPressed in - pressedDomains = pressedDomains.symmetricDifference([site.domain]) - })) - .listRowBackground( - pressedDomains.contains( - site.domain - ) ? Color.DS.Background.secondary : Color.DS.Background.primary - ) + } label: { + siteHStack(site: site) } + .listRowSeparator(.hidden) + .buttonStyle(SelectedButtonStyle(onPress: { isPressed in + pressedDomains = pressedDomains.symmetricDifference([site.domain]) + })) + .listRowBackground( + pressedDomains.contains( + site.domain + ) ? Color.DS.Background.secondary : Color.DS.Background.primary + ) } private func siteHStack(site: Site) -> some View { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 572981fa972f..fe0e2c12083e 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -9,7 +9,6 @@ final class BlogListViewModel: NSObject, ObservableObject { private var pinnedSitesController: NSFetchedResultsController? private var recentSitesController: NSFetchedResultsController? - private var allRemainingSitesController: NSFetchedResultsController? private var allBlogsController: NSFetchedResultsController? private let contextManager: ContextManager @@ -44,9 +43,6 @@ final class BlogListViewModel: NSObject, ObservableObject { let isCurrentlyPinned = blog.pinnedDate != nil trackPinned(blog: blog) - if isCurrentlyPinned { - moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: blog) - } blog.pinnedDate = isCurrentlyPinned ? nil : Date() @@ -62,8 +58,6 @@ final class BlogListViewModel: NSObject, ObservableObject { blog.lastUsed = Date() - updateExcessRecentBlogsIfNeeded(selectedSiteID: siteID) - contextManager.saveContextAndWait(contextManager.mainContext) } @@ -96,26 +90,6 @@ final class BlogListViewModel: NSObject, ObservableObject { ) } - private static func filteredAllRemainingSites(allBlogs: [Blog]) -> [BlogListView.Site] { - allBlogs.filter({ $0.pinnedDate == nil && $0.lastUsed == nil }).compactMap(BlogListView.Site.init) - } - - private func moveRecentPinnedSiteToRemainingSitesIfNeeded(pinnedBlog: Blog) { - if let recentBlogs = recentSitesController?.fetchedObjects, - recentBlogs.count == 8 { - pinnedBlog.lastUsed = nil - } - } - - private func updateExcessRecentBlogsIfNeeded(selectedSiteID: NSNumber) { - if let recentBlogs = recentSitesController?.fetchedObjects, - recentBlogs.count == 8, - let lastBlog = recentBlogs.last, - !recentBlogs.compactMap({ $0.dotComID }).contains(selectedSiteID) { - lastBlog.lastUsed = nil - } - } - func viewAppeared() { if recentSites.isEmpty && pinnedSites.isEmpty { selectedBlog()?.lastUsed = Date() @@ -132,7 +106,7 @@ final class BlogListViewModel: NSObject, ObservableObject { extension BlogListView.Site { init(blog: Blog) { self.init( - id: blog.dotComID, + id: blog.dotComID ?? 0, title: blog.title ?? "", domain: blog.url ?? "", imageURL: blog.hasIcon ? URL(string: blog.icon ?? "") : nil @@ -142,18 +116,7 @@ extension BlogListView.Site { extension BlogListViewModel: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - if controller == pinnedSitesController { - pinnedSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) - } else if controller == recentSitesController { - recentSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) - } else if controller == allRemainingSitesController { - allRemainingSites = (controller.fetchedObjects as? [Blog] ?? []).compactMap(BlogListView.Site.init) - } else if controller == allBlogsController { - allBlogs = controller.fetchedObjects as? [Blog] ?? [] - if searchSites.isEmpty { - searchSites = allBlogs.compactMap(BlogListView.Site.init) - } - } + updatePublishedSitesFromControllers() } } @@ -168,10 +131,6 @@ extension BlogListViewModel { descriptor: NSSortDescriptor(key: "lastUsed", ascending: false), fetchLimit: 8 ) - allRemainingSitesController = createResultsController( - with: NSPredicate(format: "lastUsed == nil AND pinnedDate == nil"), - descriptor: NSSortDescriptor(key: "settings.name", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) - ) allBlogsController = createResultsController( with: nil, descriptor: NSSortDescriptor(key: "accountForDefaultBlog.userID", ascending: false) @@ -180,7 +139,6 @@ extension BlogListViewModel { [ pinnedSitesController, recentSitesController, - allRemainingSitesController, allBlogsController ].forEach { [weak self] controller in controller?.delegate = self @@ -196,10 +154,10 @@ extension BlogListViewModel { recentSites = filteredBlogs(resultsController: recentSitesController).compactMap( BlogListView.Site.init ) - allRemainingSites = filteredBlogs(resultsController: allRemainingSitesController).compactMap( - BlogListView.Site.init - ) allBlogs = filteredBlogs(resultsController: allBlogsController) + allRemainingSites = allBlogs.compactMap(BlogListView.Site.init).filter({ site in + return !pinnedSites.contains(site) && !recentSites.contains(site) + }) searchSites = allBlogs.compactMap(BlogListView.Site.init) } From ddc0a6866d2b78762637c3b1b6a4190c910a7a8d Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 18:20:54 -0400 Subject: [PATCH 065/116] Remove labels from post create/update dates --- .../ViewRelated/Post/AbstractPostHelper.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift index c533e2a44fd6..daa0aa078832 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostHelper.swift @@ -7,29 +7,33 @@ enum AbstractPostHelper { } static func getLocalizedStatusWithDate(for post: AbstractPost) -> String? { + _getLocalizedStatusWithDate(for: post)?.capitalized(with: .current) + } + + private static func _getLocalizedStatusWithDate(for post: AbstractPost) -> String? { let timeZone = post.blog.timeZone switch post.status { case .scheduled: if let dateCreated = post.dateCreated { - return String(format: Strings.scheduled, dateCreated.mediumStringWithTime(timeZone: timeZone)) + return dateCreated.mediumStringWithTime(timeZone: timeZone) } case .publish, .publishPrivate: if let dateCreated = post.dateCreated { - return String(format: Strings.published, dateCreated.toMediumString(inTimeZone: timeZone)) + return dateCreated.toMediumString(inTimeZone: timeZone) } case .trash: if let dateModified = post.dateModified { - return String(format: Strings.trashed, dateModified.toMediumString(inTimeZone: timeZone)) + return dateModified.toMediumString(inTimeZone: timeZone) } default: break } if let dateModified = post.dateModified { - return String(format: Strings.edited, dateModified.toMediumString(inTimeZone: timeZone)) + return dateModified.toMediumString(inTimeZone: timeZone) } if let dateCreated = post.dateCreated { - return String(format: Strings.created, dateCreated.toMediumString(inTimeZone: timeZone)) + return dateCreated.toMediumString(inTimeZone: timeZone) } return nil } @@ -50,11 +54,3 @@ enum AbstractPostHelper { return string } } - -private enum Strings { - static let published = NSLocalizedString("post.publishedTimeAgo", value: "Published %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") - static let scheduled = NSLocalizedString("post.scheduledForDate", value: "Scheduled %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") - static let created = NSLocalizedString("post.createdTimeAgo", value: "Created %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") - static let edited = NSLocalizedString("post.editedTimeAgo", value: "Edited %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") - static let trashed = NSLocalizedString("post.trashedTimeAgo", value: "Trashed %@", comment: "Post status and date for list cells with %@ a placeholder for the date.") -} From a5fee1f23bdba3cd39ec70266904784ceac1e244 Mon Sep 17 00:00:00 2001 From: Hassaan El-Garem Date: Wed, 24 Apr 2024 02:08:26 +0200 Subject: [PATCH 066/116] Update: fix search results not persisting when core data changes are triggered --- .../Site Picker/BlogList/BlogListView.swift | 2 +- .../BlogList/BlogListViewModel.swift | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift index cd33acda5696..79d9da99bf8c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListView.swift @@ -64,7 +64,7 @@ struct BlogListView: View { siteButton(site: site) } .onChange(of: searchText) { newValue in - viewModel.updateSearchText(newValue) + viewModel.searchQueryChanged(newValue) } } else { pinnedSection diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index fe0e2c12083e..5f28d5408e9b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -6,6 +6,7 @@ final class BlogListViewModel: NSObject, ObservableObject { @Published var allRemainingSites: [BlogListView.Site] = [] @Published var searchSites: [BlogListView.Site] = [] var allBlogs: [Blog] = [] + private var searchText: String = "" private var pinnedSitesController: NSFetchedResultsController? private var recentSitesController: NSFetchedResultsController? @@ -23,17 +24,9 @@ final class BlogListViewModel: NSObject, ObservableObject { setupFetchedResultsControllers() } - func updateSearchText(_ newText: String) { - if newText.isEmpty { - searchSites = allBlogs.compactMap(BlogListView.Site.init) - } else { - searchSites = allBlogs - .filter { - $0.url?.lowercased().contains(newText.lowercased()) == true - || $0.title?.lowercased().contains(newText.lowercased()) == true - } - .compactMap(BlogListView.Site.init) - } + func searchQueryChanged(_ newText: String) { + searchText = newText + updateSearchResults() } func togglePinnedSite(siteID: NSNumber?) { @@ -158,7 +151,7 @@ extension BlogListViewModel { allRemainingSites = allBlogs.compactMap(BlogListView.Site.init).filter({ site in return !pinnedSites.contains(site) && !recentSites.contains(site) }) - searchSites = allBlogs.compactMap(BlogListView.Site.init) + updateSearchResults() } private func filteredBlogs(resultsController: NSFetchedResultsController?) -> [Blog] { @@ -174,6 +167,19 @@ extension BlogListViewModel { return blogs } + private func updateSearchResults() { + if searchText.isEmpty { + searchSites = allBlogs.compactMap(BlogListView.Site.init) + } else { + searchSites = allBlogs + .filter { + $0.url?.lowercased().contains(searchText.lowercased()) == true + || $0.title?.lowercased().contains(searchText.lowercased()) == true + } + .compactMap(BlogListView.Site.init) + } + } + private func createResultsController( with predicate: NSPredicate?, descriptor: NSSortDescriptor, From 9c54c28165cb0ccbd18cad7a6c9e74a415c3fedd Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Mon, 22 Apr 2024 17:02:02 -0400 Subject: [PATCH 067/116] Add Subscriber chart Shows subscriber growth over time --- Podfile | 2 +- Podfile.lock | 8 +- .../Stats/Helpers/StatSection.swift | 4 + .../Stats/SiteStatsTableViewCells.swift | 27 +++++++ .../Subscribers/StatsSubscribersCache.swift | 4 + .../StatsSubscribersChartCell.swift | 58 +++++++++++++++ .../Subscribers/StatsSubscribersChartCell.xib | 39 ++++++++++ .../StatsSubscribersLineChart.swift | 36 +++++++++ .../Subscribers/StatsSubscribersStore.swift | 31 ++++++++ .../StatsSubscribersViewController.swift | 1 + .../StatsSubscribersViewModel.swift | 73 +++++++++++++------ WordPress/WordPress.xcodeproj/project.pbxproj | 18 +++++ .../StatsSubscribersViewModelTests.swift | 34 ++++++++- 13 files changed, 306 insertions(+), 29 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift create mode 100644 WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib create mode 100644 WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersLineChart.swift diff --git a/Podfile b/Podfile index e22a398d7f77..53eaf4d457b1 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '1a0955f6cbc20b258f82be7887d6e8e56a6fbce3' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: 'a7ccb2e6810eb9d546f9a85cd30057be88a9760b' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index d43866c313b0..85f58742392c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `1a0955f6cbc20b258f82be7887d6e8e56a6fbce3`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `a7ccb2e6810eb9d546f9a85cd30057be88a9760b`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 1a0955f6cbc20b258f82be7887d6e8e56a6fbce3 + :commit: a7ccb2e6810eb9d546f9a85cd30057be88a9760b :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 1a0955f6cbc20b258f82be7887d6e8e56a6fbce3 + :commit: a7ccb2e6810eb9d546f9a85cd30057be88a9760b :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 2ee1661530a005e266006f993a65e84299509019 +PODFILE CHECKSUM: 0733776e0690892a2d9f7dab87c32abd8f2c8567 COCOAPODS: 1.15.2 diff --git a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift index 092bbf09e044..b23c212fa67e 100644 --- a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift +++ b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift @@ -34,6 +34,7 @@ case postStatsMonthsYears case postStatsAverageViews case postStatsRecentWeeks + case subscribersChart case subscribersEmailsSummary static let allInsights: [StatSection] = [ @@ -143,6 +144,8 @@ return PostStatsHeaders.averageViewsPerDay case .postStatsRecentWeeks: return PostStatsHeaders.recentWeeks + case .subscribersChart: + return SubscribersHeaders.chart case .subscribersEmailsSummary: return SubscribersHeaders.emailsSummaryStats default: @@ -430,6 +433,7 @@ } struct SubscribersHeaders { + static let chart = NSLocalizedString("stats.subscribers.chart.title", value: "Subscribers", comment: "Stats 'Subscribers' card header, contains chart") static let emailsSummaryStats = NSLocalizedString("stats.subscribers.emailsSummaryCard.title", value: "Emails", comment: "Stats 'Emails' card header") } diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index 0025c010efab..d88c15216578 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -77,6 +77,33 @@ struct ViewsVisitorsRow: StatsHashableImmuTableRow { } } +struct SubscriberChartRow: StatsHashableImmuTableRow { + typealias CellType = StatsSubscribersChartCell + + static let cell: ImmuTableCell = { + return ImmuTableCell.nib(CellType.defaultNib, CellType.self) + }() + + let action: ImmuTableAction? = nil + let chartData: LineChartDataConvertible + let chartStyling: LineChartStyling + let xAxisDates: [Date] + let statSection: StatSection? + + static func == (lhs: SubscriberChartRow, rhs: SubscriberChartRow) -> Bool { + return lhs.xAxisDates == rhs.xAxisDates + } + + func configureCell(_ cell: UITableViewCell) { + + guard let cell = cell as? CellType else { + return + } + + cell.configure(row: self) + } +} + struct CellHeaderRow: StatsHashableImmuTableRow { typealias CellType = StatsCellHeader diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersCache.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersCache.swift index db11cb0a3c51..bc3b009534ac 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersCache.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersCache.swift @@ -26,5 +26,9 @@ final class StatsSubscribersCache { static func emailsSummary(quantity: Int, sortField: String, sortOrder: String, siteId: NSNumber) -> CacheKey { return .init(record: .subscribersEmailsSummary, key: "\(quantity) \(sortField) \(sortOrder)", siteID: siteId) } + + static func chartSummary(unit: String, siteId: NSNumber) -> CacheKey { + return .init(record: .subscribersChart, key: unit, siteID: siteId) + } } } diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift new file mode 100644 index 000000000000..9c34e07f54c1 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift @@ -0,0 +1,58 @@ + +import UIKit + +class StatsSubscribersChartCell: StatsBaseCell, NibLoadable { + private typealias Style = WPStyleGuide.Stats + + @IBOutlet weak var chartView: UIView! + + private var chartData: LineChartDataConvertible! + private var chartStyling: LineChartStyling! + private var xAxisDates: [Date]! + + override func awakeFromNib() { + super.awakeFromNib() + + Style.configureCell(self) + } + + func configure(row: SubscriberChartRow) { + + statSection = .subscribersChart + + self.chartData = row.chartData + self.chartStyling = row.chartStyling + self.xAxisDates = row.xAxisDates + + configureChartView() + } +} + +private extension StatsSubscribersChartCell { + + func configureChartView() { + let configuration = StatsLineChartConfiguration(data: chartData, + styling: chartStyling, + analyticsGranularity: .days, + indexToHighlight: 0, + xAxisDates: xAxisDates) + let lineChartView = StatsLineChartView(configuration: configuration) + + resetChartContainerView() + chartView.addSubview(lineChartView) + chartView.accessibilityElements = [lineChartView] + + NSLayoutConstraint.activate([ + lineChartView.leadingAnchor.constraint(equalTo: chartView.leadingAnchor), + lineChartView.trailingAnchor.constraint(equalTo: chartView.trailingAnchor), + lineChartView.topAnchor.constraint(equalTo: chartView.topAnchor), + lineChartView.bottomAnchor.constraint(equalTo: chartView.bottomAnchor) + ]) + } + + func resetChartContainerView() { + for subview in chartView.subviews { + subview.removeFromSuperview() + } + } +} diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib new file mode 100644 index 000000000000..dd0cad1996d5 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersLineChart.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersLineChart.swift new file mode 100644 index 000000000000..5a0eb66dcdda --- /dev/null +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersLineChart.swift @@ -0,0 +1,36 @@ +import Foundation +import DGCharts + +private struct SubscriberLineChartData: LineChartDataConvertible { + let accessibilityDescription: String + let lineChartData: LineChartData +} + +class StatsSubscribersLineChart { + + let lineChartData: LineChartDataConvertible + let lineChartStyling: LineChartStyling + + init(counts: [Int]) { + let chartEntries = counts.enumerated().map { index, count in + ChartDataEntry(x: Double(index), y: Double(count)) + } + let dataSet = LineChartDataSet(entries: chartEntries) + let chartData = LineChartData(dataSets: [dataSet]) + lineChartData = SubscriberLineChartData(accessibilityDescription: "Subscriber Charts", lineChartData: chartData) + lineChartStyling = SubscribersLineChartStyling() + } +} + +// MARK: - StatsSubscribersLineChartStyling + +private struct SubscribersLineChartStyling: LineChartStyling { + let primaryLineColor: UIColor = UIColor(light: .muriel(name: .blue, .shade50), dark: .muriel(name: .blue, .shade50)) + let secondaryLineColor: UIColor? = nil + let primaryHighlightColor: UIColor? = UIColor(red: 209.0/255.0, green: 209.0/255.0, blue: 214.0/255.0, alpha: 1.0) + let labelColor: UIColor = UIColor(light: .secondaryLabel, dark: .tertiaryLabel) + let legendColor: UIColor? = nil + let legendTitle: String? = nil + let lineColor: UIColor = .neutral(.shade5) + let yAxisValueFormatter: AxisValueFormatter = VerticalAxisFormatter() +} diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift index 0f570b66c8e8..7aea01edc758 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift @@ -4,7 +4,9 @@ import WordPressKit protocol StatsSubscribersStoreProtocol { var emailsSummary: CurrentValueSubject, Never> { get } + var chartSummary: CurrentValueSubject, Never> { get } func updateEmailsSummary(quantity: Int, sortField: StatsEmailsSummaryData.SortField) + func updateChartSummary() } struct StatsSubscribersStore: StatsSubscribersStoreProtocol { @@ -13,6 +15,7 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol { private let statsService: StatsServiceRemoteV2 var emailsSummary: CurrentValueSubject, Never> = .init(.idle) + var chartSummary: CurrentValueSubject, Never> = .init(.idle) init() { self.siteID = SiteStatsInformation.sharedInstance.siteID ?? 0 @@ -48,6 +51,34 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol { } } } + + func updateChartSummary() { + guard chartSummary.value != .loading else { return } + + let unit = StatsSubscribersSummaryData.Unit.day + let cacheKey = StatsSubscribersCache.CacheKey.chartSummary(unit: unit.rawValue, siteId: siteID) + let cachedData: StatsSubscribersSummaryData? = cache.getValue(key: cacheKey) + + if let cachedData = cachedData { + self.chartSummary.send(.success(cachedData)) + } else { + chartSummary.send(.loading) + } + + statsService.getSubscribers(unit: unit) { result in + DispatchQueue.main.async { + switch result { + case .success(let data): + cache.setValue(data, key: cacheKey) + self.chartSummary.send(.success(data)) + case .failure: + if cachedData == nil { + self.chartSummary.send(.error) + } + } + } + } + } } extension StatsSubscribersStore { diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift index f4b50d0e1c0a..4510cba642a3 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewController.swift @@ -69,6 +69,7 @@ final class StatsSubscribersViewController: SiteStatsBaseTableViewController { func tableRowTypes() -> [ImmuTableRow.Type] { return [ + SubscriberChartRow.self, TopTotalsPeriodStatsRow.self, StatsGhostTopImmutableRow.self, StatsErrorRow.self diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift index 5d55a741f2bb..9555e5c3dc8f 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift @@ -14,12 +14,19 @@ final class StatsSubscribersViewModel { } func refreshData() { + store.updateChartSummary() store.updateEmailsSummary(quantity: 10, sortField: .postId) } // MARK: - Lifecycle func addObservers() { + Publishers.MergeMany(store.chartSummary) + .removeDuplicates() + .sink { [weak self] _ in + self?.updateTableViewSnapshot() + } + .store(in: &cancellables) Publishers.MergeMany(store.emailsSummary) .removeDuplicates() .sink { [weak self] _ in @@ -37,45 +44,69 @@ final class StatsSubscribersViewModel { private extension StatsSubscribersViewModel { func updateTableViewSnapshot() { - let snapshot = ImmuTableDiffableDataSourceSnapshot.multiSectionSnapshot( - emailsSummaryRows() - ) + let rows: [any StatsHashableImmuTableRow] = [ + chartRow(), + emailsSummaryRow(), + ] + let snapshot = ImmuTableDiffableDataSourceSnapshot.multiSectionSnapshot(rows) + tableViewSnapshot.send(snapshot) } - func loadingRows(_ section: StatSection) -> [any StatsHashableImmuTableRow] { - return [StatsGhostTopImmutableRow(statSection: section)] + func loadingRow(_ section: StatSection) -> any StatsHashableImmuTableRow { + return StatsGhostTopImmutableRow(statSection: section) + } + + func errorRow(_ section: StatSection) -> any StatsHashableImmuTableRow { + return StatsErrorRow(rowStatus: .error, statType: .subscribers, statSection: section) } +} + +// MARK: - Chart - func errorRows(_ section: StatSection) -> [any StatsHashableImmuTableRow] { - return [StatsErrorRow(rowStatus: .error, statType: .subscribers, statSection: section)] +private extension StatsSubscribersViewModel { + func chartRow() -> any StatsHashableImmuTableRow { + switch store.chartSummary.value { + case .loading, .idle: + return loadingRow(.subscribersChart) + case .success(let chartSummary): + let xAxisDates = chartSummary.history.map { $0.date } + let viewsChart = StatsSubscribersLineChart(counts: chartSummary.history.map { $0.count }) + let row = SubscriberChartRow( + chartData: viewsChart.lineChartData, + chartStyling: viewsChart.lineChartStyling, + xAxisDates: xAxisDates, + statSection: .subscribersChart + ) + return row + case .error: + return errorRow(.subscribersChart) + } } } // MARK: - Emails Summary private extension StatsSubscribersViewModel { - func emailsSummaryRows() -> [any StatsHashableImmuTableRow] { + func emailsSummaryRow() -> any StatsHashableImmuTableRow { switch store.emailsSummary.value { case .loading, .idle: - return loadingRows(.subscribersEmailsSummary) + return loadingRow(.subscribersEmailsSummary) case .success(let emailsSummary): - return [ - TopTotalsPeriodStatsRow( - itemSubtitle: Strings.titleColumn, - dataSubtitle: Strings.opensColumn, - secondDataSubtitle: Strings.clicksColumn, - dataRows: emailsSummaryDataRows(emailsSummary), - statSection: .subscribersEmailsSummary, - siteStatsPeriodDelegate: viewMoreDelegate - ) - ] + return TopTotalsPeriodStatsRow( + itemSubtitle: Strings.titleColumn, + dataSubtitle: Strings.opensColumn, + secondDataSubtitle: Strings.clicksColumn, + dataRows: emailsSummaryDataRow(emailsSummary), + statSection: .subscribersEmailsSummary, + siteStatsPeriodDelegate: viewMoreDelegate + ) case .error: - return errorRows(.subscribersEmailsSummary) + return errorRow(.subscribersEmailsSummary) } } - func emailsSummaryDataRows(_ emailsSummary: StatsEmailsSummaryData) -> [StatsTotalRowData] { + func emailsSummaryDataRow(_ emailsSummary: StatsEmailsSummaryData) -> [StatsTotalRowData] { return emailsSummary.posts.map { StatsTotalRowData( name: $0.title, diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 8c2f0f99bd25..0a34d45bd89e 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2797,6 +2797,12 @@ B026DAB02A96D9E900995410 /* support_chat_error_handler.js in Resources */ = {isa = PBXBuildFile; fileRef = B026DAAF2A96D9E900995410 /* support_chat_error_handler.js */; }; B026DAB12A96D9E900995410 /* support_chat_error_handler.js in Resources */ = {isa = PBXBuildFile; fileRef = B026DAAF2A96D9E900995410 /* support_chat_error_handler.js */; }; B030FE0A27EBF0BC000F6F5E /* SiteCreationIntentTracksEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B030FE0927EBF0BC000F6F5E /* SiteCreationIntentTracksEventTests.swift */; }; + B038A81C2BD70FCA00763731 /* StatsSubscribersChartCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B038A81B2BD70FCA00763731 /* StatsSubscribersChartCell.xib */; }; + B038A81D2BD70FCA00763731 /* StatsSubscribersChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B038A81A2BD70FCA00763731 /* StatsSubscribersChartCell.swift */; }; + B038A81E2BD70FCA00763731 /* StatsSubscribersChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B038A81A2BD70FCA00763731 /* StatsSubscribersChartCell.swift */; }; + B038A81F2BD70FCA00763731 /* StatsSubscribersChartCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B038A81B2BD70FCA00763731 /* StatsSubscribersChartCell.xib */; }; + B038A8212BD7164F00763731 /* StatsSubscribersLineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = B038A8202BD7164F00763731 /* StatsSubscribersLineChart.swift */; }; + B038A8222BD7164F00763731 /* StatsSubscribersLineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = B038A8202BD7164F00763731 /* StatsSubscribersLineChart.swift */; }; B03B9234250BC593000A40AF /* SuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03B9233250BC593000A40AF /* SuggestionService.swift */; }; B03B9236250BC5FD000A40AF /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03B9235250BC5FD000A40AF /* Suggestion.swift */; }; B0637527253E7CEC00FD45D2 /* SuggestionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0637526253E7CEB00FD45D2 /* SuggestionsTableView.swift */; }; @@ -8171,6 +8177,9 @@ AEE082892681C23C00DCF54B /* GutenbergRefactoredGalleryUploadProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergRefactoredGalleryUploadProcessorTests.swift; sourceTree = ""; }; B026DAAF2A96D9E900995410 /* support_chat_error_handler.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = support_chat_error_handler.js; sourceTree = ""; }; B030FE0927EBF0BC000F6F5E /* SiteCreationIntentTracksEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCreationIntentTracksEventTests.swift; sourceTree = ""; }; + B038A81A2BD70FCA00763731 /* StatsSubscribersChartCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsSubscribersChartCell.swift; sourceTree = ""; }; + B038A81B2BD70FCA00763731 /* StatsSubscribersChartCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatsSubscribersChartCell.xib; sourceTree = ""; }; + B038A8202BD7164F00763731 /* StatsSubscribersLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsSubscribersLineChart.swift; sourceTree = ""; }; B03B9233250BC593000A40AF /* SuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionService.swift; sourceTree = ""; }; B03B9235250BC5FD000A40AF /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = ""; }; B0637526253E7CEB00FD45D2 /* SuggestionsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SuggestionsTableView.swift; path = Suggestions/SuggestionsTableView.swift; sourceTree = ""; }; @@ -9921,6 +9930,9 @@ 01011E842BD27E47003B5C2B /* StatsSubscribersViewModel.swift */, 019C5B8C2BD6570D00A69DB0 /* StatsSubscribersStore.swift */, 019C5B932BD6917600A69DB0 /* StatsSubscribersCache.swift */, + B038A81A2BD70FCA00763731 /* StatsSubscribersChartCell.swift */, + B038A81B2BD70FCA00763731 /* StatsSubscribersChartCell.xib */, + B038A8202BD7164F00763731 /* StatsSubscribersLineChart.swift */, ); path = Subscribers; sourceTree = ""; @@ -19819,6 +19831,7 @@ 1761F18226209AEE000815EF /* jetpack-green-icon-app-60x60@3x.png in Resources */, 83317ED92BC71CEB001AD2F4 /* ReaderTagCardCell.xib in Resources */, FE3E83E626A58646008CE851 /* ListSimpleOverlayView.xib in Resources */, + B038A81C2BD70FCA00763731 /* StatsSubscribersChartCell.xib in Resources */, 401A3D022027DBD80099A127 /* PluginListCell.xib in Resources */, 17222D83261DDDF90047B163 /* celadon-classic-icon-app-60x60@2x.png in Resources */, 981C34912183871200FC2683 /* SiteStatsDashboard.storyboard in Resources */, @@ -20301,6 +20314,7 @@ F46597B128E6605E00D5F49A /* neu-green-icon-app-76@2x.png in Resources */, F41E4E9728F20802001880C6 /* white-on-pink-icon-app-76@2x.png in Resources */, F41E4EB828F225DB001880C6 /* stroke-dark-icon-app-60@3x.png in Resources */, + B038A81F2BD70FCA00763731 /* StatsSubscribersChartCell.xib in Resources */, F46597EA28E6698D00D5F49A /* spectrum-on-black-icon-app-76.png in Resources */, 98A047762821D069001B4E2D /* BloggingPromptsViewController.storyboard in Resources */, F41E4ECF28F23E00001880C6 /* green-on-white-icon-app-83.5@2x.png in Resources */, @@ -21931,6 +21945,7 @@ C7AFF87C283D5CF4000E01DF /* QRLoginVerifyCoordinator.swift in Sources */, FA25FA212609AA9C0005E08F /* AppConfiguration.swift in Sources */, 83EF3D7B2937D703000AF9BF /* SharedDataIssueSolver.swift in Sources */, + B038A81D2BD70FCA00763731 /* StatsSubscribersChartCell.swift in Sources */, 436D55DB210F862A00CEAA33 /* NibReusable.swift in Sources */, F49B9A09293A3243000CEFCE /* MigrationEvent.swift in Sources */, 98563DDD21BF30C40006F5E9 /* TabbedTotalsCell.swift in Sources */, @@ -23262,6 +23277,7 @@ 98E14A3C27C9712D007B0896 /* NotificationCommentDetailViewController.swift in Sources */, 9A8ECE122254A3260043C8DA /* JetpackRemoteInstallState.swift in Sources */, 40D7823A206AEA880015A3A1 /* Scheduler.swift in Sources */, + B038A8212BD7164F00763731 /* StatsSubscribersLineChart.swift in Sources */, 436D562E2117347C00CEAA33 /* RegisterDomainDetailsViewModel+RowDefinitions.swift in Sources */, E6FACB1E1EC675E300284AC7 /* GravatarProfile.swift in Sources */, D8212CB720AA7703008E8AE8 /* ReaderShareAction.swift in Sources */, @@ -24991,6 +25007,7 @@ FABB22CF2602FC2C00C8785C /* AssembledSiteView.swift in Sources */, FABB22D02602FC2C00C8785C /* ReaderTopicService+FollowedInterests.swift in Sources */, FABB22D12602FC2C00C8785C /* ReaderRecommendedSiteCardCell.swift in Sources */, + B038A8222BD7164F00763731 /* StatsSubscribersLineChart.swift in Sources */, FABB22D22602FC2C00C8785C /* PostEditorState.swift in Sources */, FABB22D32602FC2C00C8785C /* UICollectionViewCell+Tint.swift in Sources */, FABB22D42602FC2C00C8785C /* ManagedPerson+CoreDataProperties.swift in Sources */, @@ -25610,6 +25627,7 @@ 0CED200D2B68425A00E6DD52 /* WebKitView.swift in Sources */, FABB247F2602FC2C00C8785C /* StockPhotosPageable.swift in Sources */, FABB24802602FC2C00C8785C /* JetpackRestoreStatusViewController.swift in Sources */, + B038A81E2BD70FCA00763731 /* StatsSubscribersChartCell.swift in Sources */, FABB24812602FC2C00C8785C /* BindableTapGestureRecognizer.swift in Sources */, FABB24822602FC2C00C8785C /* ReaderSearchSuggestion.swift in Sources */, DCF892CA282FA37100BB71E1 /* SiteStatsBaseTableViewController.swift in Sources */, diff --git a/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift b/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift index 1b3edcc3c382..e839500d489f 100644 --- a/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift +++ b/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift @@ -24,17 +24,39 @@ final class StatsSubscribersViewModelTests: XCTestCase { }) .store(in: &cancellables) - store.emailsSummary.send(.loading) + store.chartSummary.send(.loading) wait(for: [expectation], timeout: 1) } + func testTableViewSnapshot_chartSummaryLoaded() throws { + let expectation = expectation(description: "Chart section should be loading") + var subscriberChartRow: SubscriberChartRow? + sut.tableViewSnapshot + .sink(receiveValue: { snapshot in + if let row = snapshot.itemIdentifiers.first?.immuTableRow as? SubscriberChartRow { + subscriberChartRow = row + expectation.fulfill() + } + }) + .store(in: &cancellables) + + let chartSummary = StatsSubscribersSummaryData(history: [ + .init(date: Date(), count: 1), + .init(date: Date(), count: 2), + ]) + store.chartSummary.send(.success(chartSummary)) + + wait(for: [expectation], timeout: 1) + XCTAssertNotNil(subscriberChartRow?.chartData) + } + func testTableViewSnapshot_emailsSummaryLoaded() throws { - let expectation = expectation(description: "First section should be loading") + let expectation = expectation(description: "Email section should be loading") var emailsSummaryRow: TopTotalsPeriodStatsRow? sut.tableViewSnapshot .sink(receiveValue: { snapshot in - if let row = snapshot.itemIdentifiers.first?.immuTableRow as? TopTotalsPeriodStatsRow { + if let row = snapshot.itemIdentifiers.last?.immuTableRow as? TopTotalsPeriodStatsRow { emailsSummaryRow = row expectation.fulfill() } @@ -56,9 +78,15 @@ final class StatsSubscribersViewModelTests: XCTestCase { } private class StatsSubscribersStoreMock: StatsSubscribersStoreProtocol { + var chartSummary: CurrentValueSubject, Never> = .init(.idle) var emailsSummary: CurrentValueSubject, Never> = .init(.idle) + var updateChartSummaryCalled = false var updateEmailsSummaryCalled = false + func updateChartSummary() { + updateChartSummaryCalled = false + } + func updateEmailsSummary(quantity: Int, sortField: StatsEmailsSummaryData.SortField) { updateEmailsSummaryCalled = true } From f3bf11701380c259d49ee68ca4af4c682bf41c6f Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Tue, 23 Apr 2024 22:28:20 -0400 Subject: [PATCH 068/116] Fixed chart cell title --- Podfile | 2 +- Podfile.lock | 8 ++++---- .../Stats/Subscribers/StatsSubscribersChartCell.xib | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Podfile b/Podfile index 53eaf4d457b1..56cb4d795e6a 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: 'a7ccb2e6810eb9d546f9a85cd30057be88a9760b' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '3197069de70b1e2b57ca8f399896edfdd908fe4a' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index 85f58742392c..18b720e6293a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `a7ccb2e6810eb9d546f9a85cd30057be88a9760b`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `3197069de70b1e2b57ca8f399896edfdd908fe4a`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: a7ccb2e6810eb9d546f9a85cd30057be88a9760b + :commit: 3197069de70b1e2b57ca8f399896edfdd908fe4a :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: a7ccb2e6810eb9d546f9a85cd30057be88a9760b + :commit: 3197069de70b1e2b57ca8f399896edfdd908fe4a :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 0733776e0690892a2d9f7dab87c32abd8f2c8567 +PODFILE CHECKSUM: 137d276a703acdcfe3339bcea95bdd41f68761f4 COCOAPODS: 1.15.2 diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib index dd0cad1996d5..255292322f4a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.xib @@ -18,20 +18,21 @@ - + - + + From 56ea19f19e646438ace37c9bedb8d50bb118e5e7 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 07:51:50 -0400 Subject: [PATCH 069/116] Gray xmark --- .../Post/Prepublishing/PrepublishingViewController.swift | 3 --- .../Post/Scheduling/PublishDatePickerViewController.swift | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 54428dcb9539..2890f268ba30 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -419,9 +419,6 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource self?.viewModel.publishDate = date self?.reloadData() self?.updatePublishButtonLabel() - if date == nil { - self?.navigationController?.popViewController(animated: true) - } } let viewController = PublishDatePickerViewController(configuration: configuration) viewController.configureDefaultNavigationBarAppearance() diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index 11216f3e0555..951aa59a2151 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -103,12 +103,10 @@ struct PublishDatePickerView: View { Spacer() if configuration.date != nil, !configuration.isRequired { - Button(role: .destructive, action: { - configuration.date = nil - }) { - Image(systemName: "minus.circle.fill") - .foregroundStyle(.red) + Button(action: { configuration.date = nil }) { + Image(systemName: "xmark.circle.fill") } + .foregroundStyle(Color.secondary) .buttonStyle(.plain) } } From 1e55275e98c9682decabbc12d3dfaa795a685f3d Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:45:48 +0700 Subject: [PATCH 070/116] Add remote fetching for each tag in the Tags stream --- .../Reader/ReaderStreamViewController.swift | 21 ++++++++++- .../Reader/ReaderTagCardCell.swift | 4 +- .../Reader/ReaderTagCardCellViewModel.swift | 37 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift index 1b124aea883a..276b3a98b062 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift @@ -219,6 +219,12 @@ import AutomatticTracks private var removedPosts = Set() private var showConfirmation = true + // NOTE: This is currently a workaround for the 'Your Tags' stream use case. + // + // The set object flags each tag in the stream so that we know whether or not we've fetched the remote data for the tag. + // We need to ensure that we only fetch the remote data once per tag to avoid the resultsController from refreshing the table view indefinitely. + private var tagStreamSyncTracker = Set() + // MARK: - Factory Methods /// Convenience method for instantiating an instance of ReaderStreamViewController @@ -901,6 +907,12 @@ import AutomatticTracks /// Handles the user initiated pull to refresh action. /// @objc func handleRefresh(_ sender: UIRefreshControl) { + if contentType == .tags { + // NOTE: This is a workaround. + // Allow all tags to re-fetch posts. + tagStreamSyncTracker.removeAll() + } + if !canSync() { cleanupAfterSync() @@ -1624,7 +1636,14 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { func cell(for tag: ReaderTagTopic) -> UITableViewCell { let cell = tableConfiguration.tagCell(tableView) - cell.configure(parent: self, tag: tag, isLoggedIn: isLoggedIn) + + // check whether we should sync the tag's posts. + let shouldSync = !tagStreamSyncTracker.contains(tag.slug) + if shouldSync { + tagStreamSyncTracker.insert(tag.slug) + } + + cell.configure(parent: self, tag: tag, isLoggedIn: isLoggedIn, shouldSyncRemotely: shouldSync) cell.selectionStyle = .none return cell } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift index a52489884646..ee37b3b33194 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift @@ -35,14 +35,14 @@ class ReaderTagCardCell: UITableViewCell { collectionViewHeightConstraint.constant = cellSize.height } - func configure(parent: UIViewController, tag: ReaderTagTopic, isLoggedIn: Bool) { + func configure(parent: UIViewController, tag: ReaderTagTopic, isLoggedIn: Bool, shouldSyncRemotely: Bool = false) { weak var weakSelf = self viewModel = ReaderTagCardCellViewModel(parent: parent, tag: tag, collectionView: collectionView, isLoggedIn: isLoggedIn, cellSize: weakSelf?.cellSize) - viewModel?.fetchTagTopics() + viewModel?.fetchTagTopics(syncRemotely: shouldSyncRemotely) tagButton.setTitle(tag.title, for: .normal) } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift index b57a0dfdec41..e2696f255aa8 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -4,12 +4,17 @@ class ReaderTagCardCellViewModel: NSObject { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot + private let coreDataStack: CoreDataStackSwift private weak var parentViewController: UIViewController? private let slug: String private weak var collectionView: UICollectionView? private let isLoggedIn: Bool private let cellSize: () -> CGSize? + private lazy var readerPostService: ReaderPostService = { + .init(coreDataStack: coreDataStack) + }() + private lazy var dataSource: DataSource? = { guard let collectionView else { return nil @@ -38,11 +43,17 @@ class ReaderTagCardCellViewModel: NSObject { return resultsController }() - init(parent: UIViewController?, tag: ReaderTagTopic, collectionView: UICollectionView?, isLoggedIn: Bool, cellSize: @escaping @autoclosure () -> CGSize?) { + init(parent: UIViewController?, + tag: ReaderTagTopic, + collectionView: UICollectionView?, + isLoggedIn: Bool, + coreDataStack: CoreDataStackSwift = ContextManager.shared, + cellSize: @escaping @autoclosure () -> CGSize?) { self.parentViewController = parent self.slug = tag.slug self.collectionView = collectionView self.isLoggedIn = isLoggedIn + self.coreDataStack = coreDataStack self.cellSize = cellSize super.init() @@ -51,8 +62,28 @@ class ReaderTagCardCellViewModel: NSObject { collectionView?.delegate = self } - func fetchTagTopics() { - try? resultsController.performFetch() + func fetchTagTopics(syncRemotely: Bool) { + guard let topic = try? ReaderTagTopic.lookup(withSlug: slug, in: coreDataStack.mainContext) else { + return + } + + let onRemoteFetchComplete = { [weak self] in + try? self?.resultsController.performFetch() + } + + guard syncRemotely else { + onRemoteFetchComplete() + return + } + + // TODO: Add loading state. + + readerPostService.fetchPosts(for: topic, earlierThan: Date()) { _, _ in + onRemoteFetchComplete() + } failure: { _ in + // try to show local contents even if the request failed. + onRemoteFetchComplete() + } } func onTagButtonTapped() { From 3984c2339fc40904b6f1135201cee331b0bc6345 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 13:05:20 -0400 Subject: [PATCH 071/116] Fix keyboard height issue --- .../PrepublishingViewController.swift | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 2890f268ba30..c3496e4682c8 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -121,6 +121,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource configureHeader() configureTableView() + configurePublishButton() observePostConflictResolved() WPStyleGuide.applyBorderStyle(footerSeparator) @@ -130,14 +131,25 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource let stackView = UIStackView(arrangedSubviews: [ headerView, tableView, - footerSeparator, - setupPublishButton() + footerSeparator ]) stackView.axis = .vertical - view.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - view.pinSubviewToSafeArea(stackView) + let contentView = VStack { + PrepublishingStackView(view: stackView) + PublishButton(viewModel: publishButtonViewModel) + .tint(Color(uiColor: .primary)) + .padding() // TODO: ok padding? + }.ignoresSafeArea(.keyboard) + + // Making the entire view `UIHostingController` to make sure keyboard + // avoidance can be disabled (see https://steipete.com/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/?utm_campaign=%20SwiftUI%20Weekly&utm_medium=email&utm_source=Revue%20newsletter) + let hostingViewController = UIHostingController(rootView: contentView) + addChild(hostingViewController) + + view.addSubview(hostingViewController.view) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.pinSubviewToSafeArea(hostingViewController.view) view.backgroundColor = .systemBackground @@ -168,23 +180,12 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource tableView.rowHeight = UITableView.automaticDimension } - private func setupPublishButton() -> UIView { - let footerView = UIView() - - let hostingViewController = UIHostingController(rootView: PublishButton(viewModel: publishButtonViewModel).tint(Color(uiColor: .primary))) - addChild(hostingViewController) - - footerView.addSubview(hostingViewController.view) - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false - footerView.pinSubviewToSafeArea(hostingViewController.view, insets: Constants.nuxButtonInsets) - + private func configurePublishButton() { updatePublishButtonLabel() updatePublishButtonState() mediaPollingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in self?.updatePublishButtonState() } - - return footerView } private func observePostConflictResolved() { @@ -582,6 +583,18 @@ private final class PrepublishingViewModel { } } +private struct PrepublishingStackView: UIViewRepresentable { + let view: UIStackView + + func makeUIView(context: Context) -> some UIView { + view + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + // Do nothing + } +} + private enum Strings { static let publish = NSLocalizedString("prepublishing.publish", value: "Publish", comment: "Primary button label in the pre-publishing sheet") static let schedule = NSLocalizedString("prepublishing.schedule", value: "Schedule", comment: "Primary button label in the pre-publishing shee") From ffd83ea9c867f4e87a2a0f2ec732c4811499ea4c Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 13:10:58 -0400 Subject: [PATCH 072/116] Revert PublishButton integration in DeprecatedPrepublishingViewController --- ...eprecatedPrepublishingViewController.swift | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift index a08d68321fad..99120c85a7a7 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/DeprecatedPrepublishingViewController.swift @@ -2,7 +2,6 @@ import UIKit import WordPressAuthenticator import Combine import WordPressUI -import SwiftUI /// - warning: deprecated (kahu-offline-mode) enum PrepublishingIdentifier { @@ -55,11 +54,14 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView let tableView = UITableView(frame: .zero, style: .plain) private let footerSeparator = UIView() - private weak var titleField: UITextField? + let publishButton: NUXButton = { + let nuxButton = NUXButton() + nuxButton.isPrimary = true + nuxButton.accessibilityIdentifier = "publish" + return nuxButton + }() - private lazy var publishButtonViewModel = PublishButtonViewModel(title: "Publish") { [weak self] in - self?.buttonPublishTapped() - } + private weak var titleField: UITextField? /// Determines whether the text has been first responder already. If it has, don't force it back on the user unless it's been selected by them. private var hasSelectedText: Bool = false @@ -163,13 +165,11 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView private func setupPublishButton() -> UIView { let footerView = UIView() + footerView.addSubview(publishButton) + publishButton.translatesAutoresizingMaskIntoConstraints = false + footerView.pinSubviewToSafeArea(publishButton, insets: Constants.nuxButtonInsets) - let hostingViewController = UIHostingController(rootView: PublishButton(viewModel: publishButtonViewModel).tint(Color(uiColor: .primary))) - addChild(hostingViewController) - - footerView.addSubview(hostingViewController.view) - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false - footerView.pinSubviewToSafeArea(hostingViewController.view, insets: Constants.nuxButtonInsets) + publishButton.addTarget(self, action: #selector(publish), for: .touchUpInside) updatePublishButtonLabel() @@ -424,12 +424,11 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView // MARK: - Publish Button private func updatePublishButtonLabel() { - publishButtonViewModel.title = post.isScheduled() ? Strings.schedule : Strings.publish + publishButton.setTitle(post.isScheduled() ? Strings.schedule : Strings.publish, for: .normal) } - private func buttonPublishTapped() { + @objc func publish(_ sender: UIButton) { didTapPublish = true - let completion = getCompletion() navigationController?.dismiss(animated: true) { WPAnalytics.track(.editorPostPublishNowTapped) @@ -437,24 +436,6 @@ final class DeprecatedPrepublishingViewController: UIViewController, UITableView } } - private func setLoading(_ isLoading: Bool) { - publishButtonViewModel.state = isLoading ? .loading : .default - isModalInPresentation = isLoading - view.isUserInteractionEnabled = !isLoading - - var subviews: [UIView] = [view] - while let view = subviews.popLast() { - switch view { - case let control as UIControl: - control.isEnabled = !isLoading - case let cell as UITableViewCell: - isLoading ? cell.disable() : cell.enable() - default: - subviews += view.subviews - } - } - } - // MARK: - Password Prompt private func showPasswordAlert() { From 10948d19d1973eda6732bdbe14c4b6cfa0a1eef6 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 13:16:46 -0400 Subject: [PATCH 073/116] Remove footer separator --- .../PrepublishingViewController.swift | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index c3496e4682c8..c612759bc386 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -36,7 +36,6 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource private let headerView = PrepublishingHeaderView() let tableView = UITableView(frame: .zero, style: .plain) - private let footerSeparator = UIView() private lazy var publishButtonViewModel = PublishButtonViewModel(title: "Publish") { [weak self] in self?.buttonPublishTapped() @@ -124,14 +123,11 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource configurePublishButton() observePostConflictResolved() - WPStyleGuide.applyBorderStyle(footerSeparator) - title = "" let stackView = UIStackView(arrangedSubviews: [ headerView, - tableView, - footerSeparator + tableView ]) stackView.axis = .vertical @@ -139,7 +135,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource PrepublishingStackView(view: stackView) PublishButton(viewModel: publishButtonViewModel) .tint(Color(uiColor: .primary)) - .padding() // TODO: ok padding? + .padding() }.ignoresSafeArea(.keyboard) // Making the entire view `UIHostingController` to make sure keyboard @@ -195,12 +191,6 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource .store(in: &cancellables) } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - footerSeparator.isHidden = tableView.contentSize.height < tableView.bounds.height - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) From 4c58c296d6296b53a2cf8d8609d26ab221e2c2bc Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 15:10:57 -0400 Subject: [PATCH 074/116] Add PostSettingsRowPendingReview --- .../Utility/Analytics/WPAnalyticsEvent.swift | 3 ++ .../Post/PostSettingsViewController.m | 31 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index c4da563173ab..62f27284bf57 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -26,6 +26,7 @@ import Foundation case editorPostPublishTap case editorPostPublishDismissed case editorPostScheduledChanged + case editorPostPendingReviewChanged case editorPostTitleChanged case editorPostVisibilityChanged case editorPostTagsChanged @@ -627,6 +628,8 @@ import Foundation return "editor_post_publish_dismissed" case .editorPostScheduledChanged: return "editor_post_scheduled_changed" + case .editorPostPendingReviewChanged: + return "editor_post_pending_review_changed" case .editorPostTitleChanged: return "editor_post_title_changed" case .editorPostVisibilityChanged: diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index d60a1a381ae1..f06495e83909 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -28,6 +28,7 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { PostSettingsRowAuthor, PostSettingsRowPublishDate, PostSettingsRowStatus, + PostSettingsRowPendingReview, PostSettingsRowVisibility, PostSettingsRowPassword, PostSettingsRowFormat, @@ -50,7 +51,7 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { static NSString *const TableViewActivityCellIdentifier = @"TableViewActivityCellIdentifier"; static NSString *const TableViewProgressCellIdentifier = @"TableViewProgressCellIdentifier"; static NSString *const TableViewFeaturedImageCellIdentifier = @"TableViewFeaturedImageCellIdentifier"; -static NSString *const TableViewStickyPostCellIdentifier = @"TableViewStickyPostCellIdentifier"; +static NSString *const TableViewToggleCellIdentifier = @"TableViewToggleCellIdentifier"; static NSString *const TableViewGenericCellIdentifier = @"TableViewGenericCellIdentifier"; @@ -138,7 +139,7 @@ - (void)viewDidLoad [self.tableView registerNib:[UINib nibWithNibName:@"WPTableViewActivityCell" bundle:nil] forCellReuseIdentifier:TableViewActivityCellIdentifier]; [self.tableView registerClass:[WPProgressTableViewCell class] forCellReuseIdentifier:TableViewProgressCellIdentifier]; [self.tableView registerClass:[PostFeaturedImageCell class] forCellReuseIdentifier:TableViewFeaturedImageCellIdentifier]; - [self.tableView registerClass:[SwitchTableViewCell class] forCellReuseIdentifier:TableViewStickyPostCellIdentifier]; + [self.tableView registerClass:[SwitchTableViewCell class] forCellReuseIdentifier:TableViewToggleCellIdentifier]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TableViewGenericCellIdentifier]; self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 0.0, 44.0)]; // add some vertical padding @@ -670,7 +671,18 @@ - (void)configureMetaSectionRows [metaRows addObject:@(PostSettingsRowAuthor)]; } - if (![RemoteFeature enabled:RemoteFeatureFlagSyncPublishing] || !self.isDraftOrPending) { + if ([RemoteFeature enabled:RemoteFeatureFlagSyncPublishing]) { + if (self.isDraftOrPending) { + [metaRows addObject:@(PostSettingsRowPendingReview)]; + } else { + [metaRows addObject:@(PostSettingsRowPublishDate)]; + [metaRows addObjectsFromArray:@[ @(PostSettingsRowStatus), + @(PostSettingsRowVisibility) ]]; + if (self.apost.password) { + [metaRows addObject:@(PostSettingsRowPassword)]; + } + } + } else { [metaRows addObject:@(PostSettingsRowPublishDate)]; [metaRows addObjectsFromArray:@[ @(PostSettingsRowStatus), @(PostSettingsRowVisibility) ]]; @@ -739,6 +751,17 @@ - (UITableViewCell *)configureMetaPostMetaCellForIndexPath:(NSIndexPath *)indexP } else if (row == PostSettingsRowPassword) { cell = [self configurePasswordCell]; + } else if (row == PostSettingsRowPendingReview) { + // Pending Review + __weak __typeof(self) weakSelf = self; + SwitchTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewToggleCellIdentifier]; + cell.name = NSLocalizedStringWithDefaultValue(@"postSettings.pendingReview", nil, [NSBundle mainBundle], @"Pending review", @"The 'Pending Review' setting of the post"); + cell.on = [self.post.status isEqualToString:PostStatusPending]; + cell.onChange = ^(BOOL newValue) { + [WPAnalytics trackEvent:WPAnalyticsEventEditorPostPendingReviewChanged properties:@{@"via": @"settings"}]; + weakSelf.post.status = newValue ? PostStatusPending : PostStatusDraft; + }; + return cell; } return cell; @@ -826,7 +849,7 @@ - (UITableViewCell *)configureStickyPostCellForIndexPath:(NSIndexPath *)indexPat { __weak __typeof(self) weakSelf = self; - SwitchTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewStickyPostCellIdentifier]; + SwitchTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewToggleCellIdentifier]; cell.name = NSLocalizedString(@"Stick post to the front page", @"This is the cell title."); cell.on = self.post.isStickyPost; cell.onChange = ^(BOOL newValue) { From 8ded263b15277a415311efec2b5aeb79aea118d1 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 17:10:53 -0400 Subject: [PATCH 075/116] Remove status field from Post Settings for publishing posts and pages --- .../Classes/ViewRelated/Post/PostSettingsViewController.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index f06495e83909..16e5ba3ad5dd 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -27,6 +27,7 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { PostSettingsRowTags, PostSettingsRowAuthor, PostSettingsRowPublishDate, + // - warning: deprecated (kahu-offline-mode) PostSettingsRowStatus, PostSettingsRowPendingReview, PostSettingsRowVisibility, @@ -675,9 +676,10 @@ - (void)configureMetaSectionRows if (self.isDraftOrPending) { [metaRows addObject:@(PostSettingsRowPendingReview)]; } else { - [metaRows addObject:@(PostSettingsRowPublishDate)]; - [metaRows addObjectsFromArray:@[ @(PostSettingsRowStatus), - @(PostSettingsRowVisibility) ]]; + [metaRows addObjectsFromArray:@[ + @(PostSettingsRowPublishDate), + @(PostSettingsRowVisibility) + ]]; if (self.apost.password) { [metaRows addObject:@(PostSettingsRowPassword)]; } From b59394dae9506a04875fd2c648fd7989d7516b01 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 23 Apr 2024 17:27:42 -0400 Subject: [PATCH 076/116] Enable async saving for post settings for pending posts --- .../ViewRelated/Post/PostSettingsViewController+Swift.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 2c85c3c83ec1..e624c4fb12c5 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -110,7 +110,7 @@ extension PostSettingsViewController { Task { @MainActor in do { let coordinator = PostCoordinator.shared - if apost.original().status == .draft { + if coordinator.isSyncAllowed(for: apost) { coordinator.setNeedsSync(for: apost) } else { try await coordinator._save(apost) From dda86486cc49a56883aedb25cb1ef93b3889d93f Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 11:46:05 -0400 Subject: [PATCH 077/116] Fix an issue with fixLocalMediaURLs not being called --- .../Extensions/AbstractPost+fixLocalMediaURLs.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/Extensions/AbstractPost+fixLocalMediaURLs.swift b/WordPress/Classes/Extensions/AbstractPost+fixLocalMediaURLs.swift index a68e14291d2d..565ddd27d0f4 100644 --- a/WordPress/Classes/Extensions/AbstractPost+fixLocalMediaURLs.swift +++ b/WordPress/Classes/Extensions/AbstractPost+fixLocalMediaURLs.swift @@ -1,15 +1,11 @@ import Foundation extension AbstractPost { - /// If a post is in the failed state it can contain references to local files. - /// When updating the app through the App Store the local paths can change. - /// This will fix any outdated local path with the correct one. - /// + /// When updating the app through the App Store or installing a new version + /// of the app from Xcode, the local paths can change. This will fix any + /// outdated local path with the correct one. func fixLocalMediaURLs() { - guard isFailed, - var content = self.content else { - return - } + guard var content = self.content else { return } media.forEach { media in guard !media.hasRemote else { From 532544575925f2058006d5893aa7efb9efc29420 Mon Sep 17 00:00:00 2001 From: Chris McGraw <2454408+wargcm@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:21:44 -0400 Subject: [PATCH 078/116] Add guard bounds check --- .../ViewRelated/Reader/ReaderTagCardCellViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift index b57a0dfdec41..41f85cf7753a 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -82,6 +82,10 @@ extension ReaderTagCardCellViewModel: NSFetchedResultsControllerDelegate { extension ReaderTagCardCellViewModel: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let sectionInfo = resultsController.sections?[safe: indexPath.section], + indexPath.row < sectionInfo.numberOfObjects else { + return + } let post = resultsController.object(at: indexPath) let controller = ReaderDetailViewController.controllerWithPost(post) parentViewController?.navigationController?.pushViewController(controller, animated: true) From e030eebf7ad50fc4b310e7aa0930b00f2221768a Mon Sep 17 00:00:00 2001 From: Chris McGraw <2454408+wargcm@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:22:06 -0400 Subject: [PATCH 079/116] Rename function to be more accurate --- WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift | 2 +- .../Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift index a52489884646..1ed02f32a84f 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift @@ -42,7 +42,7 @@ class ReaderTagCardCell: UITableViewCell { collectionView: collectionView, isLoggedIn: isLoggedIn, cellSize: weakSelf?.cellSize) - viewModel?.fetchTagTopics() + viewModel?.fetchTagPosts() tagButton.setTitle(tag.title, for: .normal) } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift index 41f85cf7753a..42f994763795 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -51,7 +51,7 @@ class ReaderTagCardCellViewModel: NSObject { collectionView?.delegate = self } - func fetchTagTopics() { + func fetchTagPosts() { try? resultsController.performFetch() } From b866c320aea20a122845a1f768c3d60875d2bd1b Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 16:30:20 -0400 Subject: [PATCH 080/116] Add a way to retry or delete the uploads --- .../Classes/Services/MediaCoordinator.swift | 20 +++++++ .../Post/PostMediaUploadStatusView.swift | 55 +++++++++++++++---- .../Post/PostMediaUploadViewModel.swift | 44 ++++++++++++++- 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index 7ab1208969db..c43d65300d32 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -798,6 +798,26 @@ extension MediaCoordinator { }, for: nil) } + + /// Returns `true` if the error can't be resolved by simply retrying and + /// requires user interventions, for example, making more room in the + /// Media library. + static func isTerminalError(_ error: Error) -> Bool { + let nsError = error as NSError + switch nsError.domain { + case MediaServiceErrorDomain: + switch nsError.code { + case MediaServiceError.fileDoesNotExist.rawValue, + MediaServiceError.fileLargerThanMaxFileSize.rawValue, + MediaServiceError.fileLargerThanDiskQuotaAvailable.rawValue: + return true + default: + return false + } + default: + return false + } + } } extension Media { diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift index fe4f7235a804..55ba288ac4ee 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift @@ -7,12 +7,7 @@ struct PostMediaUploadStatusView: View { let onCloseTapped: () -> Void var body: some View { - List { - ForEach(viewModel.uploads) { - MediaUploadStatusView(viewModel: $0) - } - } - .listStyle(.plain) + contents .toolbar { ToolbarItem(placement: .cancellationAction) { Button(Strings.close, action: onCloseTapped) @@ -21,6 +16,21 @@ struct PostMediaUploadStatusView: View { .navigationTitle(Strings.title) .navigationBarTitleDisplayMode(.inline) } + + @ViewBuilder + private var contents: some View { + if viewModel.uploads.isEmpty { + Text(Strings.empty) + .foregroundStyle(.secondary) + } else { + List { + ForEach(viewModel.uploads) { + MediaUploadStatusView(viewModel: $0) + } + } + .listStyle(.plain) + } + } } private struct MediaUploadStatusView: View { @@ -41,23 +51,45 @@ private struct MediaUploadStatusView: View { Text(viewModel.details) .font(.footnote) .foregroundStyle(.secondary) - .lineLimit(1) + .lineLimit(3) } Spacer() switch viewModel.state { - case .uploaded: - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(.secondary.opacity(0.5)) case .uploading: MediaUploadProgressView(progress: viewModel.fractionCompleted) + makeMenu() + case .failed: + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.red) + makeMenu() + case .uploaded: + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.secondary.opacity(0.33)) } } .task { await viewModel.loadThumbnail() } } + + @ViewBuilder + private func makeMenu() -> some View { + Menu { + Button(action: viewModel.buttonRetryTapped) { + Label(Strings.retry, systemImage: "arrow.clockwise") + } + Button(role: .destructive, action: viewModel.buttonRemoveTapped) { + Label(Strings.remove, systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis") + .font(.footnote) + .foregroundStyle(.secondary.opacity(0.75)) + } + .buttonStyle(.plain) + } } private struct MediaUploadProgressView: View { @@ -99,5 +131,8 @@ private struct MediaThubmnailImageView: View { private enum Strings { static let title = NSLocalizedString("postMediaUploadStatusView.title", value: "Media Uploads", comment: "Title for post media upload status view") + static let empty = NSLocalizedString("postMediaUploadStatusView.noPendingUploads", value: "No pending uploads", comment: "Placeholder text in postMediaUploadStatusView when no uploads remain") static let close = NSLocalizedString("postMediaUploadStatusView.close", value: "Close", comment: "Close button in postMediaUploadStatusView") + static let retry = NSLocalizedString("postMediaUploadStatusView.retry", value: "Retry", comment: "Retry upload button in postMediaUploadStatusView") + static let remove = NSLocalizedString("postMediaUploadStatusView.remove", value: "Remove", comment: "Remove media button in postMediaUploadStatusView") } diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift index f219be60eafe..2a52817e2623 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift @@ -1,9 +1,10 @@ import Foundation import SwiftUI +import Combine /// Manages media upload for the given revision of the post. final class PostMediaUploadViewModel: ObservableObject { - let uploads: [MediaUploadViewModel] + private(set) var uploads: [MediaUploadViewModel] @Published private(set) var totalFileSize: Int64 = 0 @Published private(set) var fractionCompleted = 0.0 @@ -14,6 +15,7 @@ final class PostMediaUploadViewModel: ObservableObject { private let post: AbstractPost private let coordinator: MediaCoordinator private weak var timer: Timer? + private var cancellables: [AnyCancellable] = [] deinit { timer?.invalidate() @@ -33,6 +35,19 @@ final class PostMediaUploadViewModel: ObservableObject { timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in self?.update() } + + post.publisher(for: \.media).sink { [weak self] in + self?.didUpdateMedia($0) + }.store(in: &cancellables) + } + + private func didUpdateMedia(_ media: Set) { + let remainingObjectIDs = Set(media.map(\.objectID)) + withAnimation { + uploads.removeAll { viewModel in + !remainingObjectIDs.contains(viewModel.id) + } + } } private func update() { @@ -42,7 +57,7 @@ final class PostMediaUploadViewModel: ObservableObject { totalFileSize = uploads.map(\.fileSize).reduce(0, +) fractionCompleted = uploads.map(\.fractionCompleted).reduce(0, +) / Double(uploads.count) - completedUploadsCount = uploads.filter({ $0.state == .uploaded }).count + completedUploadsCount = uploads.filter(\.isCompleted).count } } @@ -84,9 +99,15 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { MediaImageService.isThubmnailSupported(for: media.mediaType) ? 40 : 24 } + var isCompleted: Bool { + if case .uploaded = state { return true } + return false + } + enum State { - case uploaded case uploading + case failed(Error) + case uploaded } deinit { @@ -107,6 +128,12 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { self.state = media.isUploadNeeded ? .uploading : .uploaded self.fileSize = media.filesize?.int64Value ?? 0 // Should never be `0` + if let error = media.error, MediaCoordinator.isTerminalError(error) { + self.details = error.localizedDescription + self.state = .failed(error) + return // No retry + } + if media.remoteStatus == .failed, retryTimer == nil { retryTimer = Timer.scheduledTimer(withTimeInterval: nextRetryDelay, repeats: false) { [weak self] _ in self?.retry() } } @@ -152,6 +179,17 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { // Continue showing placeholder } } + + // MARK: - Menu + + func buttonRetryTapped() { + retry() + } + + func buttonRemoveTapped() { + coordinator.cancelUploadAndDeleteMedia(media) + // TODO: notify Gutenberg or update the post in a different way + } } private extension Media { From 42c455b283224ac995a0b281083d151c5eee8763 Mon Sep 17 00:00:00 2001 From: Chris McGraw <2454408+wargcm@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:02:06 -0400 Subject: [PATCH 081/116] Prevent gap markers from displaying --- .../Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift index 42f994763795..314e33c3b2fe 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -30,6 +30,7 @@ class ReaderTagCardCellViewModel: NSObject { let fetchRequest = NSFetchRequest(entityName: ReaderPost.classNameWithoutNamespaces()) fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sortRank", ascending: false)] fetchRequest.fetchLimit = Constants.displayPostLimit + fetchRequest.includesSubentities = false let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: ContextManager.shared.mainContext, sectionNameKeyPath: nil, From 747bda30214805b56c9b44532b312defe054d000 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 17:35:05 -0400 Subject: [PATCH 082/116] Update PublishButton design and use failure state --- .../Post/PostMediaUploadStatusView.swift | 2 +- .../Post/PostMediaUploadViewModel.swift | 5 + .../PrepublishingViewController.swift | 15 +- .../Post/Prepublishing/PublishButton.swift | 148 +++++++++--------- 4 files changed, 93 insertions(+), 77 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift index 55ba288ac4ee..d5f06ad06a48 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift @@ -92,7 +92,7 @@ private struct MediaUploadStatusView: View { } } -private struct MediaUploadProgressView: View { +struct MediaUploadProgressView: View { let progress: Double var body: some View { diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift index 2a52817e2623..069ad1eac284 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift @@ -104,6 +104,11 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { return false } + var error: Error? { + if case .failed(let error) = state { return error } + return nil + } + enum State { case uploading case failed(Error) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index c612759bc386..307138bcc48c 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -437,8 +437,12 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource guard !viewModel.isCompleted else { return nil } - let progress = PublishButtonState.Progress(completed: Int64(Double(viewModel.totalFileSize) * viewModel.fractionCompleted), total: viewModel.totalFileSize) - return .uploading(title: Strings.uploadingMedia + ": \(viewModel.completedUploadsCount) / \(viewModel.uploads.count)", progress: progress, onInfoTapped: { [weak self] in + if let error = viewModel.uploads.lazy.map(\.error).first { + return .failed(title: Strings.mediaUploadFailedTitle, details: error?.localizedDescription) { [weak self] in + self?.buttonShowUploadInfoTapped() + } + } + return .uploading(title: Strings.uploadingMedia, details: Strings.uploadMediaRemaining(count: viewModel.uploads.count - viewModel.completedUploadsCount), progress: viewModel.fractionCompleted, onInfoTapped: { [weak self] in self?.buttonShowUploadInfoTapped() }) } @@ -596,4 +600,11 @@ private enum Strings { static let jetpackSocial = NSLocalizedString("prepublishing.jetpackSocial", value: "Jetpack Social", comment: "Label for a cell in the pre-publishing sheet") static let immediately = NSLocalizedString("prepublishing.publishDateImmediately", value: "Immediately", comment: "Placeholder value for a publishing date in the prepublishing sheet when the date is not selected") static let uploadingMedia = NSLocalizedString("prepublishing.uploadingMedia", value: "Uploading media", comment: "Title for a publish button state in the pre-publishing sheet") + private static let uploadMediaOneItemRemaining = NSLocalizedString("prepublishing.uploadMediaOneItemRemaining", value: "%@ item remaining", comment: "Details label for a publish button state in the pre-publishing sheet") + private static let uploadMediaManyItemsRemaining = NSLocalizedString("prepublishing.uploadMediaManyItemsRemaining", value: "%@ items remaining", comment: "Details label for a publish button state in the pre-publishing sheet") + static func uploadMediaRemaining(count: Int) -> String { + String(format: count == 1 ? Strings.uploadMediaOneItemRemaining : Strings.uploadMediaManyItemsRemaining, count.description) + } + static let mediaUploadFailedTitle = NSLocalizedString("prepublishing.mediaUploadFailedTitle", value: "Failed to upload media", comment: "Title for a publish button state in the pre-publishing sheet") + static let mediaUploadFailedDetails = NSLocalizedString("prepublishing.mediaUploadFailedDetails", value: "Some of the uploads failed", comment: "Details for a publish button state in the pre-publishing sheet") } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift index 6e8d05c4b527..cc7c2ec0b810 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift @@ -17,69 +17,79 @@ struct PublishButton: View { .buttonBorderShape(.roundedRectangle(radius: 8)) .accessibilityIdentifier("publish") - switch viewModel.state { - case .default: - EmptyView() - case .loading: - ProgressView() - .tint(Color.secondary) - case let .uploading(title, progress, onInfoTapped): - let content = HStack(spacing: 10) { - ProgressView() - .tint(Color.secondary) - - VStack(alignment: .leading) { - Text(title) - .font(.subheadline.weight(.medium)) - if let progress { - Text(Strings.progress(progress)) - .foregroundStyle(Color.secondary) - .font(.footnote) - .monospacedDigit() - } - } - .lineLimit(1) - .foregroundStyle(Color.primary) - - Spacer() - - if onInfoTapped != nil { - Image(systemName: "info.circle") - .foregroundStyle(.secondary) - } - }.padding(.horizontal) + stateView + } + } - if let onInfoTapped { - Button(action: onInfoTapped) { content } + @ViewBuilder + private var stateView: some View { + switch viewModel.state { + case .default: + EmptyView() + case .loading: + ProgressView() + .tint(Color.secondary) + case let .uploading(title, details, progress, onInfoTapped): + let content = HStack(spacing: 10) { + if let progress { + MediaUploadProgressView(progress: progress) + .frame(width: Constants.accessoryViewWidth) } else { - content + ProgressView() + .foregroundStyle(.secondary) + .frame(width: Constants.accessoryViewWidth) } - case let .failed(title, details, onRetryTapped): - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(Color.red) - VStack(alignment: .leading) { - Text(title) - .font(.subheadline.weight(.medium)) - .foregroundStyle(.primary) - if let details { - Text(details) - .font(.footnote) - .foregroundStyle(.secondary) - } - } - .lineLimit(1) - - Spacer() + makeDetailsView(title: title, details: details) + Spacer() + if onInfoTapped != nil { + chevronUpView + } + }.padding(.horizontal) - if let onRetryTapped { - Button(Strings.retry, action: onRetryTapped) - .font(.subheadline) - } + if let onInfoTapped { + Button(action: onInfoTapped) { content } + } else { + content + } + case let .failed(title, details, onInfoTapped): + let content = HStack(spacing: 10) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(Color.red) + .frame(width: Constants.accessoryViewWidth) + makeDetailsView(title: title, details: details) + Spacer() + if onInfoTapped != nil { + chevronUpView } - .padding(.horizontal) + }.padding(.horizontal) + + if let onInfoTapped { + Button(action: onInfoTapped) { content } + } else { + content + } + } + } + + private func makeDetailsView(title: String, details: String?) -> some View { + VStack(alignment: .leading) { + Text(title) + .font(.subheadline.weight(.medium)) + .foregroundStyle(.primary) + if let details { + Text(details) + .font(.footnote) + .foregroundStyle(.secondary) } } + .tint(.primary) + .lineLimit(1) + } + + private var chevronUpView: some View { + Image(systemName: "chevron.up") + .font(.subheadline.weight(.semibold)) + .tint(Color.secondary) } private var isDisabled: Bool { @@ -90,6 +100,10 @@ struct PublishButton: View { } } +private enum Constants { + static let accessoryViewWidth: CGFloat = 22 +} + final class PublishButtonViewModel: ObservableObject { @Published var title: String @Published var state: PublishButtonState = .default @@ -105,31 +119,17 @@ final class PublishButtonViewModel: ObservableObject { enum PublishButtonState { case `default` case loading - case uploading(title: String, progress: Progress?, onInfoTapped: (() -> Void)? = nil) - case failed(title: String, details: String? = nil, onRetryTapped: (() -> Void)? = nil) - - struct Progress { - var completed: Int64 - var total: Int64 - } -} - -private enum Strings { - static func progress(_ progress: PublishButtonState.Progress) -> String { - let format = NSLocalizedString("publishButton.progress", value: "%@ of %@", comment: "Shows the download or upload progress with two parameters: preformatted completed and total bytes") - return String(format: format, ByteCountFormatter.string(fromByteCount: progress.completed, countStyle: .file), ByteCountFormatter.string(fromByteCount: progress.total, countStyle: .file)) - } - - static let retry = NSLocalizedString("publishButton.retry", value: "Retry", comment: "Retry button title") + case uploading(title: String, details: String, progress: Double?, onInfoTapped: (() -> Void)? = nil) + case failed(title: String, details: String? = nil, onInfoTapped: (() -> Void)? = nil) } #Preview { VStack(spacing: 16) { PublishButton(viewModel: .init(title: "Publish", state: .default) {}) PublishButton(viewModel: .init(title: "Publish", state: .loading) {}) - PublishButton(viewModel: .init(title: "Publish", state: .uploading(title: "Uploading media...", progress: .init(completed: 100, total: 2000))) {}) + PublishButton(viewModel: .init(title: "Publish", state: .uploading(title: "Uploading media...", details: "2 items remaining", progress: 0.2)) {}) PublishButton(viewModel: .init(title: "Publish", state: .failed(title: "Failed to upload media")) {}) - PublishButton(viewModel: .init(title: "Publish", state: .failed(title: "Failed to upload media", details: "Not connected to Internet", onRetryTapped: {})) {}) + PublishButton(viewModel: .init(title: "Publish", state: .failed(title: "Failed to upload media", details: "Not connected to Internet", onInfoTapped: {})) {}) } .padding() } From 2db1999fb48f05d978a1dd9623742a5308e3dd8f Mon Sep 17 00:00:00 2001 From: Hassaan El-Garem Date: Wed, 24 Apr 2024 23:43:13 +0200 Subject: [PATCH 083/116] Update: Sort all blogs alphabetically --- .../Blog/Site Picker/BlogList/BlogListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift index 5f28d5408e9b..0e34ae6455ee 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift @@ -126,7 +126,7 @@ extension BlogListViewModel { ) allBlogsController = createResultsController( with: nil, - descriptor: NSSortDescriptor(key: "accountForDefaultBlog.userID", ascending: false) + descriptor: NSSortDescriptor(key: "settings.name", ascending: true, selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ) [ From 79a5aba87d5c9cb51b4b6f9105121e8270d85b69 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 17:41:04 -0400 Subject: [PATCH 084/116] Add global retry button --- .../Post/PostMediaUploadStatusView.swift | 43 +++++++------------ .../Post/PostMediaUploadViewModel.swift | 25 ++++++----- .../PrepublishingViewController.swift | 4 +- .../Post/Prepublishing/PublishButton.swift | 2 +- 4 files changed, 31 insertions(+), 43 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift index d5f06ad06a48..7bda95f5353c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift @@ -8,13 +8,22 @@ struct PostMediaUploadStatusView: View { var body: some View { contents - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button(Strings.close, action: onCloseTapped) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(Strings.close, action: onCloseTapped) + } + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button(action: viewModel.buttonRetryTapped) { + Label(Strings.retryUploads, systemImage: "arrow.clockwise") + }.disabled(viewModel.isButtonRetryDisabled) + } label: { + Image(systemName: "ellipsis") + } + } } - } - .navigationTitle(Strings.title) - .navigationBarTitleDisplayMode(.inline) + .navigationTitle(Strings.title) + .navigationBarTitleDisplayMode(.inline) } @ViewBuilder @@ -59,11 +68,9 @@ private struct MediaUploadStatusView: View { switch viewModel.state { case .uploading: MediaUploadProgressView(progress: viewModel.fractionCompleted) - makeMenu() case .failed: Image(systemName: "exclamationmark.circle.fill") .foregroundStyle(.red) - makeMenu() case .uploaded: Image(systemName: "checkmark.circle.fill") .foregroundStyle(.secondary.opacity(0.33)) @@ -73,23 +80,6 @@ private struct MediaUploadStatusView: View { await viewModel.loadThumbnail() } } - - @ViewBuilder - private func makeMenu() -> some View { - Menu { - Button(action: viewModel.buttonRetryTapped) { - Label(Strings.retry, systemImage: "arrow.clockwise") - } - Button(role: .destructive, action: viewModel.buttonRemoveTapped) { - Label(Strings.remove, systemImage: "trash") - } - } label: { - Image(systemName: "ellipsis") - .font(.footnote) - .foregroundStyle(.secondary.opacity(0.75)) - } - .buttonStyle(.plain) - } } struct MediaUploadProgressView: View { @@ -133,6 +123,5 @@ private enum Strings { static let title = NSLocalizedString("postMediaUploadStatusView.title", value: "Media Uploads", comment: "Title for post media upload status view") static let empty = NSLocalizedString("postMediaUploadStatusView.noPendingUploads", value: "No pending uploads", comment: "Placeholder text in postMediaUploadStatusView when no uploads remain") static let close = NSLocalizedString("postMediaUploadStatusView.close", value: "Close", comment: "Close button in postMediaUploadStatusView") - static let retry = NSLocalizedString("postMediaUploadStatusView.retry", value: "Retry", comment: "Retry upload button in postMediaUploadStatusView") - static let remove = NSLocalizedString("postMediaUploadStatusView.remove", value: "Remove", comment: "Remove media button in postMediaUploadStatusView") + static let retryUploads = NSLocalizedString("postMediaUploadStatusView.retryUploads", value: "Retry Uploads", comment: "Retry upload button in postMediaUploadStatusView") } diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift index 069ad1eac284..e2f941e2f56d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift @@ -17,6 +17,10 @@ final class PostMediaUploadViewModel: ObservableObject { private weak var timer: Timer? private var cancellables: [AnyCancellable] = [] + var isButtonRetryDisabled: Bool { + !(uploads.contains(where: { $0.error != nil })) + } + deinit { timer?.invalidate() } @@ -59,6 +63,12 @@ final class PostMediaUploadViewModel: ObservableObject { fractionCompleted = uploads.map(\.fractionCompleted).reduce(0, +) / Double(uploads.count) completedUploadsCount = uploads.filter(\.isCompleted).count } + + func buttonRetryTapped() { + for upload in uploads { + upload.retry() + } + } } /// Manages individual media upload. @@ -133,7 +143,7 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { self.state = media.isUploadNeeded ? .uploading : .uploaded self.fileSize = media.filesize?.int64Value ?? 0 // Should never be `0` - if let error = media.error, MediaCoordinator.isTerminalError(error) { + if media.remoteStatus == .failed, let error = media.error, MediaCoordinator.isTerminalError(error) { self.details = error.localizedDescription self.state = .failed(error) return // No retry @@ -157,7 +167,7 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { } } - private func retry() { + fileprivate func retry() { retryTimer = nil coordinator.retryMedia(media) } @@ -184,17 +194,6 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { // Continue showing placeholder } } - - // MARK: - Menu - - func buttonRetryTapped() { - retry() - } - - func buttonRemoveTapped() { - coordinator.cancelUploadAndDeleteMedia(media) - // TODO: notify Gutenberg or update the post in a different way - } } private extension Media { diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 307138bcc48c..a3609b006c72 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -437,8 +437,8 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource guard !viewModel.isCompleted else { return nil } - if let error = viewModel.uploads.lazy.map(\.error).first { - return .failed(title: Strings.mediaUploadFailedTitle, details: error?.localizedDescription) { [weak self] in + if let error = viewModel.uploads.lazy.compactMap(\.error).first { + return .failed(title: Strings.mediaUploadFailedTitle, details: error.localizedDescription) { [weak self] in self?.buttonShowUploadInfoTapped() } } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift index cc7c2ec0b810..36323adda086 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift @@ -101,7 +101,7 @@ struct PublishButton: View { } private enum Constants { - static let accessoryViewWidth: CGFloat = 22 + static let accessoryViewWidth: CGFloat = 20 } final class PublishButtonViewModel: ObservableObject { From edf3de98e8c982fdaaac2bc9d7a63b921cb60a40 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 18:01:31 -0400 Subject: [PATCH 085/116] Bette way to display errors --- .../Post/Prepublishing/PrepublishingViewController.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index a3609b006c72..a8e9ae52f82d 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -437,8 +437,10 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource guard !viewModel.isCompleted else { return nil } - if let error = viewModel.uploads.lazy.compactMap(\.error).first { - return .failed(title: Strings.mediaUploadFailedTitle, details: error.localizedDescription) { [weak self] in + let errors = viewModel.uploads.compactMap(\.error) + if !errors.isEmpty { + let details = errors.count == 1 ? errors[0].localizedDescription : String(format: Strings.mediaUploadFailedDetailsMultipleFailures, errors.count.description) + return .failed(title: Strings.mediaUploadFailedTitle, details: details) { [weak self] in self?.buttonShowUploadInfoTapped() } } @@ -606,5 +608,5 @@ private enum Strings { String(format: count == 1 ? Strings.uploadMediaOneItemRemaining : Strings.uploadMediaManyItemsRemaining, count.description) } static let mediaUploadFailedTitle = NSLocalizedString("prepublishing.mediaUploadFailedTitle", value: "Failed to upload media", comment: "Title for a publish button state in the pre-publishing sheet") - static let mediaUploadFailedDetails = NSLocalizedString("prepublishing.mediaUploadFailedDetails", value: "Some of the uploads failed", comment: "Details for a publish button state in the pre-publishing sheet") + static let mediaUploadFailedDetailsMultipleFailures = NSLocalizedString("prepublishing.mediaUploadFailedDetails", value: "%@ items failed to upload", comment: "Details for a publish button state in the pre-publishing sheet; count as a parameter") } From b38b09383b5c1a13478e621b3e38d5e2e9591dd5 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 25 Apr 2024 08:25:39 +0300 Subject: [PATCH 086/116] Update podfile --- Podfile | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index 4b49e344fa34..a5dda3b97bfc 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '3291ab9b60e88b1bc78f50422efd3b38c30c0eb3' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index fd12483d9fa8..84ba5bea8268 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `3291ab9b60e88b1bc78f50422efd3b38c30c0eb3`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 3291ab9b60e88b1bc78f50422efd3b38c30c0eb3 + :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 3291ab9b60e88b1bc78f50422efd3b38c30c0eb3 + :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: d5e8587901ad6400bb90366661ff541a6f27f08a +PODFILE CHECKSUM: 4ac1d35f8415bdc8d4c8e39d6aa7a9f6dab5933d COCOAPODS: 1.15.2 From 579498ebeb6807f98cd44ddcdeed92f32fcdb33d Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 25 Apr 2024 08:56:15 +0300 Subject: [PATCH 087/116] Update WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift --- WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift index 2e22e1470783..6c0c00c7ab24 100644 --- a/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift +++ b/WordPress/Classes/ViewRelated/Stats/Helpers/StatSection.swift @@ -461,7 +461,7 @@ static let comments = NSLocalizedString("Comments", comment: "Label for number of comments.") static let views = NSLocalizedString("Views", comment: "Label for number of views.") static let followers = NSLocalizedString("Followers", comment: "Label for number of followers.") - static let since = NSLocalizedString("stats.section.dataSubtitles.subscriber since", value: "Subscriber since", comment: "Table column title that shows the date since the user became a subscriber.") + static let since = NSLocalizedString("stats.section.dataSubtitles.subscriberSince", value: "Subscriber since", comment: "Table column title that shows the date since the user became a subscriber.") static let clicks = NSLocalizedString("Clicks", comment: "Label for number of clicks.") static let downloads = NSLocalizedString("Downloads", comment: "Label for number of file downloads.") static let emailsSummaryOpens = NSLocalizedString("stats.subscribers.emailsSummary.column.opens", value: "Opens", comment: "A title for table's column that shows a number of email openings") From 5fc12375978f3caacc31dd1f49ef2effe35b34fa Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:33:11 +0700 Subject: [PATCH 088/116] Implement ghost loading for the ReaderTagCardCell --- .../Reader/ReaderTagCardCell.swift | 87 +++++++++++++++++++ .../Reader/ReaderTagCardCellViewModel.swift | 17 +++- .../ViewRelated/Reader/ReaderTagCell.swift | 16 ++++ .../ViewRelated/Reader/ReaderTagCell.xib | 2 + 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift index ee37b3b33194..55e68208c476 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCell.swift @@ -1,9 +1,32 @@ +import WordPressUI + class ReaderTagCardCell: UITableViewCell { @IBOutlet private weak var tagButton: UIButton! @IBOutlet private weak var collectionView: UICollectionView! @IBOutlet private weak var collectionViewHeightConstraint: NSLayoutConstraint! + // A 'fake' collection view that's displayed over the actual collection view. + // This view will be displaying the loading animation while the cell is in loading state. + // + // We can't call our ghost functions on the actual collection view because it uses a + // diffable data source, which cannot be "hot-swapped". + // See: https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource + private lazy var ghostableCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.clipsToBounds = false + + let cellNibName = ReaderTagCell.classNameWithoutNamespaces() + let nib = UINib(nibName: cellNibName, bundle: nil) + collectionView.register(nib, forCellWithReuseIdentifier: cellNibName) + + return collectionView + }() + private var viewModel: ReaderTagCardCellViewModel? var cellSize: CGSize { @@ -28,11 +51,16 @@ class ReaderTagCardCell: UITableViewCell { setupButtonStyles() accessibilityElements = [tagButton, collectionView].compactMap { $0 } collectionViewHeightConstraint.constant = cellSize.height + + // disable ghost animation on the actual collection view to prevent its data source and delegate + // from being overridden. + collectionView?.isGhostableDisabled = true } override func prepareForReuse() { super.prepareForReuse() collectionViewHeightConstraint.constant = cellSize.height + hideGhostLoading() // clean up attached ghost views, if any } func configure(parent: UIViewController, tag: ReaderTagTopic, isLoggedIn: Bool, shouldSyncRemotely: Bool = false) { @@ -41,7 +69,9 @@ class ReaderTagCardCell: UITableViewCell { tag: tag, collectionView: collectionView, isLoggedIn: isLoggedIn, + viewDelegate: self, cellSize: weakSelf?.cellSize) + viewModel?.fetchTagTopics(syncRemotely: shouldSyncRemotely) tagButton.setTitle(tag.title, for: .normal) } @@ -59,6 +89,18 @@ class ReaderTagCardCell: UITableViewCell { } +// MARK: - ReaderTagCardCellViewModelDelegate + +extension ReaderTagCardCell: ReaderTagCardCellViewModelDelegate { + func showLoading() { + showGhostLoading() + } + + func hideLoading() { + hideGhostLoading() + } +} + // MARK: - Private methods private extension ReaderTagCardCell { @@ -80,4 +122,49 @@ private extension ReaderTagCardCell { collectionView.register(nib, forCellWithReuseIdentifier: ReaderTagCell.classNameWithoutNamespaces()) } + /// Injects a "fake" UICollectionView for the loading state animation. + func showGhostLoading() { + guard let collectionView, + let containerView = collectionView.superview, + ghostableCollectionView.superview == nil else { + return + } + + // setup the 'fake' collection view. + containerView.addSubview(ghostableCollectionView) + + // pin it directly over the current collection view. + NSLayoutConstraint.activate([ + ghostableCollectionView.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor), + ghostableCollectionView.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor), + ghostableCollectionView.topAnchor.constraint(equalTo: collectionView.topAnchor), + ghostableCollectionView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) + ]) + + // Important: since the size are fixed, we want to make sure that we feed the exact item size + // so that it perfectly overlays on top of the "actual" collection view. + if let flowLayout = ghostableCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { + flowLayout.itemSize = cellSize + } + + let options = GhostOptions(reuseIdentifier: ReaderTagCell.classNameWithoutNamespaces(), rowsPerSection: [3]) + let style = GhostStyle(beatDuration: GhostStyle.Defaults.beatDuration, + beatStartColor: .placeholderElement, + beatEndColor: .placeholderElementFaded) + + ghostableCollectionView.removeGhostContent() + ghostableCollectionView.displayGhostContent(options: options, style: style) + ghostableCollectionView.isScrollEnabled = false + } + + func hideGhostLoading() { + // ensure that the ghostable collection view is present in the view hierarchy. + guard ghostableCollectionView.superview != nil else { + return + } + + ghostableCollectionView.removeGhostContent() + ghostableCollectionView.removeFromSuperview() + } + } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift index e2696f255aa8..191b1a36be65 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCardCellViewModel.swift @@ -1,9 +1,16 @@ +protocol ReaderTagCardCellViewModelDelegate: NSObjectProtocol { + func showLoading() + func hideLoading() +} + class ReaderTagCardCellViewModel: NSObject { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot + weak var viewDelegate: ReaderTagCardCellViewModelDelegate? = nil + private let coreDataStack: CoreDataStackSwift private weak var parentViewController: UIViewController? private let slug: String @@ -47,12 +54,14 @@ class ReaderTagCardCellViewModel: NSObject { tag: ReaderTagTopic, collectionView: UICollectionView?, isLoggedIn: Bool, + viewDelegate: ReaderTagCardCellViewModelDelegate?, coreDataStack: CoreDataStackSwift = ContextManager.shared, cellSize: @escaping @autoclosure () -> CGSize?) { self.parentViewController = parent self.slug = tag.slug self.collectionView = collectionView self.isLoggedIn = isLoggedIn + self.viewDelegate = viewDelegate self.coreDataStack = coreDataStack self.cellSize = cellSize @@ -67,6 +76,8 @@ class ReaderTagCardCellViewModel: NSObject { return } + viewDelegate?.showLoading() + let onRemoteFetchComplete = { [weak self] in try? self?.resultsController.performFetch() } @@ -76,8 +87,6 @@ class ReaderTagCardCellViewModel: NSObject { return } - // TODO: Add loading state. - readerPostService.fetchPosts(for: topic, earlierThan: Date()) { _, _ in onRemoteFetchComplete() } failure: { _ in @@ -103,7 +112,9 @@ extension ReaderTagCardCellViewModel: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - dataSource?.apply(snapshot as Snapshot, animatingDifferences: false) + dataSource?.apply(snapshot as Snapshot, animatingDifferences: false) { [weak self] in + self?.viewDelegate?.hideLoading() + } } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift index 845133758be1..17e9d1e50837 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift @@ -1,3 +1,5 @@ +import WordPressUI + class ReaderTagCell: UICollectionViewCell { @IBOutlet private weak var contentStackView: UIStackView! @@ -10,6 +12,8 @@ class ReaderTagCell: UICollectionViewCell { @IBOutlet private weak var countsLabel: UILabel! @IBOutlet private weak var likeButton: UIButton! @IBOutlet private weak var menuButton: UIButton! + @IBOutlet weak var spacerView: UIView! + @IBOutlet weak var countsLabelSpacerView: UIView! private lazy var imageLoader = ImageLoader(imageView: featuredImageView) private var viewModel: ReaderTagCellViewModel? @@ -20,6 +24,9 @@ class ReaderTagCell: UICollectionViewCell { contentStackView.setCustomSpacing(0, after: featuredImageView) let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onSiteTitleTapped)) headerStackView.addGestureRecognizer(tapGesture) + + spacerView.isGhostableDisabled = true + countsLabelSpacerView.isGhostableDisabled = true } override func prepareForReuse() { @@ -127,3 +134,12 @@ private extension ReaderTagCell { } } + +extension ReaderTagCell: GhostableView { + + func ghostAnimationWillStart() { + siteLabel.text = "Site name" + likeButton.setTitle("", for: .normal) + } + +} diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib index 5d0911de876b..fe227266f4a1 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.xib @@ -118,12 +118,14 @@ + + From ce437e60cb6aa2344cef4729b985ec473eeff063 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:47:36 +0700 Subject: [PATCH 089/116] Fix featured image view ghost state --- WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift index 17e9d1e50837..3c8f672500c5 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift @@ -138,6 +138,11 @@ private extension ReaderTagCell { extension ReaderTagCell: GhostableView { func ghostAnimationWillStart() { + // The ghost loading animation only works on leaf subviews. + // `CachedAnimatedImageView` by default injects an activity indicator as a subview into the image view, + // therefore causing the `GhostLayer` to not be applied to the image view. + featuredImageView.subviews.forEach { $0.removeFromSuperview() } + siteLabel.text = "Site name" likeButton.setTitle("", for: .normal) } From 5811f103519ef37115a67cdd55ecf092cd914e27 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:55:11 +0700 Subject: [PATCH 090/116] Fix like button insets in ghost state --- .../Classes/ViewRelated/Reader/ReaderTagCell.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift index 3c8f672500c5..3e4ede23b710 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderTagCell.swift @@ -141,10 +141,16 @@ extension ReaderTagCell: GhostableView { // The ghost loading animation only works on leaf subviews. // `CachedAnimatedImageView` by default injects an activity indicator as a subview into the image view, // therefore causing the `GhostLayer` to not be applied to the image view. - featuredImageView.subviews.forEach { $0.removeFromSuperview() } + featuredImageView?.subviews.forEach { $0.removeFromSuperview() } - siteLabel.text = "Site name" - likeButton.setTitle("", for: .normal) + siteLabel?.text = "Site name" + + var configuration = UIButton.Configuration.plain() + configuration.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 15.0) + configuration.imagePadding = .zero + configuration.imagePlacement = .leading + likeButton?.configuration = configuration + likeButton?.setTitle("", for: .normal) } } From 9f779961ae23fea44bb8ce017fd9b1c99fd2d448 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 25 Apr 2024 13:38:27 +0300 Subject: [PATCH 091/116] Retain StatsService when calling api --- .../Remote service/StatsWidgetsService.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift index 79d3d1f02ea9..e9ae2757fd9f 100644 --- a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift @@ -3,6 +3,8 @@ import JetpackStatsWidgetsCore /// Type that wraps the backend request for new stats class StatsWidgetsService { + private var service: StatsServiceRemoteV2? + typealias ResultType = HomeWidgetData private enum State { @@ -31,11 +33,7 @@ class StatsWidgetsService { state = .loading do { - let token = try SFHFKeychainUtils.getPasswordForUsername(AppConfiguration.Widget.Stats.keychainTokenKey, - andServiceName: AppConfiguration.Widget.Stats.keychainServiceName, - accessGroup: WPAppKeychainAccessGroup) - let wpApi = WordPressComRestApi(oAuthToken: token) - let service = StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: widgetData.siteID, siteTimezone: widgetData.timeZone) + let service = try createStatsService(for: widgetData) // handle fetching depending on concrete type // we need to do like this as there is no unique service call @@ -176,3 +174,16 @@ private extension Date { return addingTimeInterval(delta) } } + +private extension StatsWidgetsService { + private func createStatsService(for widgetData: HomeWidgetData) throws -> StatsServiceRemoteV2 { + let token = try SFHFKeychainUtils.getPasswordForUsername(AppConfiguration.Widget.Stats.keychainTokenKey, + andServiceName: AppConfiguration.Widget.Stats.keychainServiceName, + accessGroup: WPAppKeychainAccessGroup) + let wpApi = WordPressComRestApi(oAuthToken: token) + let service = StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: widgetData.siteID, siteTimezone: widgetData.timeZone) + self.service = service + + return service + } +} From 0f00b1e88c5f961fff35ddda57f8821d07c1f575 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 25 Apr 2024 07:57:01 -0400 Subject: [PATCH 092/116] Push upload status in a navigation stack --- .../ViewRelated/Post/PostMediaUploadStatusView.swift | 4 ---- .../Post/Prepublishing/PrepublishingViewController.swift | 8 ++------ .../ViewRelated/Post/Prepublishing/PublishButton.swift | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift index 7bda95f5353c..b4e06a8099de 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift @@ -4,14 +4,10 @@ import SwiftUI /// Displays upload progress for the media for the given post. struct PostMediaUploadStatusView: View { @ObservedObject var viewModel: PostMediaUploadViewModel - let onCloseTapped: () -> Void var body: some View { contents .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button(Strings.close, action: onCloseTapped) - } ToolbarItem(placement: .topBarTrailing) { Menu { Button(action: viewModel.buttonRetryTapped) { diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index a8e9ae52f82d..abb52f435374 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -450,13 +450,9 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource } private func buttonShowUploadInfoTapped() { - let view = PostMediaUploadStatusView(viewModel: uploadsViewModel) { [weak self] in - self?.dismiss(animated: true, completion: nil) - } + let view = PostMediaUploadStatusView(viewModel: uploadsViewModel) let host = UIHostingController(rootView: view) - let navigation = UINavigationController(rootViewController: host) - navigation.navigationBar.isTranslucent = true // Reset to default - present(navigation, animated: true) + navigationController?.pushViewController(host, animated: true) } private func updatePublishButtonLabel() { diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift index 36323adda086..e9cb2e1dfa15 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift @@ -87,7 +87,7 @@ struct PublishButton: View { } private var chevronUpView: some View { - Image(systemName: "chevron.up") + Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .tint(Color.secondary) } From 19d635dd8166cc01e2e11877459c519cfb358892 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 25 Apr 2024 15:32:58 +0300 Subject: [PATCH 093/116] Create, retain, and release StatsService during each service acll --- .../Remote service/StatsWidgetsService.swift | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift index e9ae2757fd9f..c234af7f3189 100644 --- a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift @@ -32,29 +32,21 @@ class StatsWidgetsService { } state = .loading - do { - let service = try createStatsService(for: widgetData) - - // handle fetching depending on concrete type - // we need to do like this as there is no unique service call - if let widgetData = widgetData as? HomeWidgetTodayData { - fetchTodayStats(service: service, widgetData: widgetData, completion: completion) - } else if let widgetData = widgetData as? HomeWidgetAllTimeData { - fetchAllTimeStats(service: service, widgetData: widgetData, completion: completion) - } else if let widgetData = widgetData as? HomeWidgetThisWeekData { - fetchThisWeekStats(service: service, widgetData: widgetData, completion: completion) - } - } catch { - completion(.failure(error)) - self.state = .error + // handle fetching depending on concrete type + // we need to do like this as there is no unique service call + if let widgetData = widgetData as? HomeWidgetTodayData { + fetchTodayStats(widgetData: widgetData, completion: completion) + } else if let widgetData = widgetData as? HomeWidgetAllTimeData { + fetchAllTimeStats(widgetData: widgetData, completion: completion) + } else if let widgetData = widgetData as? HomeWidgetThisWeekData { + fetchThisWeekStats(widgetData: widgetData, completion: completion) } } - private func fetchTodayStats(service: StatsServiceRemoteV2, - widgetData: HomeWidgetTodayData, + private func fetchTodayStats(widgetData: HomeWidgetTodayData, completion: @escaping (Result) -> Void) { - service.getInsight { [weak self] (insight: StatsTodayInsight?, error) in + getInsight(widgetData: widgetData) { [weak self] (insight: StatsTodayInsight?, error) in guard let self = self else { return } @@ -89,11 +81,10 @@ class StatsWidgetsService { } } - private func fetchAllTimeStats(service: StatsServiceRemoteV2, - widgetData: HomeWidgetAllTimeData, + private func fetchAllTimeStats(widgetData: HomeWidgetAllTimeData, completion: @escaping (Result) -> Void) { - service.getInsight { [weak self] (insight: StatsAllTimesInsight?, error) in + getInsight(widgetData: widgetData) { [weak self] (insight: StatsAllTimesInsight?, error) in guard let self = self else { return @@ -124,8 +115,7 @@ class StatsWidgetsService { } } - private func fetchThisWeekStats(service: StatsServiceRemoteV2, - widgetData: HomeWidgetThisWeekData, + private func fetchThisWeekStats(widgetData: HomeWidgetThisWeekData, completion: @escaping (Result) -> Void) { // Get the current date in the site's time zone. @@ -133,8 +123,10 @@ class StatsWidgetsService { let weekEndingDate = Date().convert(from: siteTimeZone).normalizedDate() // Include an extra day. It's needed to get the dailyChange for the last day. - service.getData(for: .day, endingOn: weekEndingDate, - limit: ThisWeekWidgetStats.maxDaysToDisplay + 1) { [weak self] (summary: StatsSummaryTimeIntervalData?, error: Error?) in + getData(widgetData: widgetData, + for: .day, + endingOn: weekEndingDate, + limit: ThisWeekWidgetStats.maxDaysToDisplay + 1) { [weak self] (summary: StatsSummaryTimeIntervalData?, error: Error?) in guard let self = self else { return @@ -176,14 +168,43 @@ private extension Date { } private extension StatsWidgetsService { + private func getInsight(widgetData: HomeWidgetData, limit: Int = 10, completion: @escaping ((InsightType?, Error?) -> Void)) { + do { + self.service = try createStatsService(for: widgetData) + + self.service?.getInsight(limit: limit, completion: { [weak self] in + completion($0, $1) + self?.service = nil + }) + } catch { + completion(nil, error) + self.state = .error + } + } + + private func getData(widgetData: HomeWidgetData, + for period: StatsPeriodUnit, + unit: StatsPeriodUnit? = nil, + endingOn: Date, + limit: Int = 10, + completion: @escaping ((TimeStatsType?, Error?) -> Void)) { + do { + self.service = try createStatsService(for: widgetData) + self.service?.getData(for: period, unit: unit, endingOn: endingOn, limit: limit, completion: { [weak self] in + completion($0, $1) + self?.service = nil + }) + } catch { + completion(nil, error) + self.state = .error + } + } + private func createStatsService(for widgetData: HomeWidgetData) throws -> StatsServiceRemoteV2 { let token = try SFHFKeychainUtils.getPasswordForUsername(AppConfiguration.Widget.Stats.keychainTokenKey, andServiceName: AppConfiguration.Widget.Stats.keychainServiceName, accessGroup: WPAppKeychainAccessGroup) let wpApi = WordPressComRestApi(oAuthToken: token) - let service = StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: widgetData.siteID, siteTimezone: widgetData.timeZone) - self.service = service - - return service + return StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: widgetData.siteID, siteTimezone: widgetData.timeZone) } } From 7403a3ffe0f470a7be76af57fe64b4111bc6db16 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 25 Apr 2024 18:21:29 +0300 Subject: [PATCH 094/116] Update StatsWidgetsService.swift --- .../Remote service/StatsWidgetsService.swift | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift index c234af7f3189..050d1ced6f0d 100644 --- a/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/JetpackStatsWidgets/Remote service/StatsWidgetsService.swift @@ -140,12 +140,22 @@ class StatsWidgetsService { } let summaryData = summary?.summaryData.reversed() ?? [] - let newWidgetData = HomeWidgetThisWeekData(siteID: widgetData.siteID, - siteName: widgetData.siteName, - url: widgetData.url, - timeZone: widgetData.timeZone, - date: Date(), - stats: ThisWeekWidgetStats(days: ThisWeekWidgetStats.daysFrom(summaryData: summaryData.map { ThisWeekWidgetStats.Input(periodStartDate: $0.periodStartDate, viewsCount: $0.viewsCount) }))) + let newWidgetData = HomeWidgetThisWeekData( + siteID: widgetData.siteID, + siteName: widgetData.siteName, + url: widgetData.url, + timeZone: widgetData.timeZone, + date: Date(), + stats: ThisWeekWidgetStats( + days: ThisWeekWidgetStats.daysFrom( + summaryData: summaryData.map { + ThisWeekWidgetStats.Input( + periodStartDate: $0.periodStartDate, + viewsCount: $0.viewsCount) + } + ) + ) + ) completion(.success(newWidgetData)) DispatchQueue.global().async { @@ -168,10 +178,13 @@ private extension Date { } private extension StatsWidgetsService { - private func getInsight(widgetData: HomeWidgetData, limit: Int = 10, completion: @escaping ((InsightType?, Error?) -> Void)) { + private func getInsight( + widgetData: HomeWidgetData, + limit: Int = 10, + completion: @escaping ((InsightType?, Error?) -> Void) + ) { do { self.service = try createStatsService(for: widgetData) - self.service?.getInsight(limit: limit, completion: { [weak self] in completion($0, $1) self?.service = nil @@ -182,12 +195,14 @@ private extension StatsWidgetsService { } } - private func getData(widgetData: HomeWidgetData, - for period: StatsPeriodUnit, - unit: StatsPeriodUnit? = nil, - endingOn: Date, - limit: Int = 10, - completion: @escaping ((TimeStatsType?, Error?) -> Void)) { + private func getData( + widgetData: HomeWidgetData, + for period: StatsPeriodUnit, + unit: StatsPeriodUnit? = nil, + endingOn: Date, + limit: Int = 10, + completion: @escaping ((TimeStatsType?, Error?) -> Void) + ) { do { self.service = try createStatsService(for: widgetData) self.service?.getData(for: period, unit: unit, endingOn: endingOn, limit: limit, completion: { [weak self] in @@ -201,9 +216,11 @@ private extension StatsWidgetsService { } private func createStatsService(for widgetData: HomeWidgetData) throws -> StatsServiceRemoteV2 { - let token = try SFHFKeychainUtils.getPasswordForUsername(AppConfiguration.Widget.Stats.keychainTokenKey, - andServiceName: AppConfiguration.Widget.Stats.keychainServiceName, - accessGroup: WPAppKeychainAccessGroup) + let token = try SFHFKeychainUtils.getPasswordForUsername( + AppConfiguration.Widget.Stats.keychainTokenKey, + andServiceName: AppConfiguration.Widget.Stats.keychainServiceName, + accessGroup: WPAppKeychainAccessGroup + ) let wpApi = WordPressComRestApi(oAuthToken: token) return StatsServiceRemoteV2(wordPressComRestApi: wpApi, siteID: widgetData.siteID, siteTimezone: widgetData.timeZone) } From a5d1630434daa01d70b4e26122c25589a1a5b136 Mon Sep 17 00:00:00 2001 From: "Tanner W. Stokes" Date: Thu, 25 Apr 2024 12:55:17 -0400 Subject: [PATCH 095/116] Release script: Update gutenberg-mobile ref 1.118.0 --- Gutenberg/config.yml | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gutenberg/config.yml b/Gutenberg/config.yml index f4f7ca7ea9ae..55aa4e8fdf7e 100644 --- a/Gutenberg/config.yml +++ b/Gutenberg/config.yml @@ -9,6 +9,6 @@ # # LOCAL_GUTENBERG=../my-gutenberg-fork bundle exec pod install ref: - tag: v1.117.0 + commit: b5afe640373f481d9224ff5df4fdd77a0c81bf59 github_org: wordpress-mobile repo_name: gutenberg-mobile diff --git a/Podfile.lock b/Podfile.lock index 84ba5bea8268..c30ea3c014e1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,7 +27,7 @@ PODS: - Gifu (3.3.1) - Gravatar (1.0.1) - Gridicons (1.2.0) - - Gutenberg (1.117.0) + - Gutenberg (1.118.0) - JTAppleCalendar (8.0.5) - Kanvas (1.4.9): - CropViewController @@ -107,7 +107,7 @@ DEPENDENCIES: - Gifu (= 3.3.1) - Gravatar (= 1.0.1) - Gridicons (~> 1.2) - - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec`) + - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-b5afe640373f481d9224ff5df4fdd77a0c81bf59.podspec`) - JTAppleCalendar (~> 8.0.5) - Kanvas (~> 1.4.4) - MediaEditor (>= 1.2.2, ~> 1.2) @@ -175,7 +175,7 @@ EXTERNAL SOURCES: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 Gutenberg: - :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec + :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-b5afe640373f481d9224ff5df4fdd77a0c81bf59.podspec WordPressKit: :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git @@ -207,7 +207,7 @@ SPEC CHECKSUMS: Gifu: 416d4e38c4c2fed012f019e0a1d3ffcb58e5b842 Gravatar: 51437de6811c1d8d6f60c52985f2ca00a85cfc8e Gridicons: 4455b9f366960121430e45997e32112ae49ffe1d - Gutenberg: 2249ee2bdac042ce129297aee696c7926930bc00 + Gutenberg: 74bc722d74e2276557fe1d851a1eef0df963bb33 JTAppleCalendar: 16c6501b22cb27520372c28b0a2e0b12c8d0cd73 Kanvas: cc027f8058de881a4ae2b5aa5f05037b6d054d08 MediaEditor: d08314cfcbfac74361071a306b4bc3a39b3356ae From 298a577edeb91ea67ff0d3989f908767bc43a492 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 24 Apr 2024 20:28:37 -0400 Subject: [PATCH 096/116] Rename PostSyncStateViewModel.unsynced (+1 squashed commit) Squashed commits: [6a85881a44] Add PostCoordinator.isTerminalError --- .../Classes/Services/PostCoordinator.swift | 9 +++++++-- .../Post/PostSyncStateViewModel.swift | 18 +++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 8c3e4d6f9fca..4ed5a1be2f12 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -600,7 +600,7 @@ class PostCoordinator: NSObject { worker.error = error postDidUpdateNotification(for: operation.post) - if shouldScheduleRetry(for: error) { + if !PostCoordinator.isTerminalError(error) { let delay = worker.nextRetryDelay worker.retryTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self, weak worker] _ in guard let self, let worker else { return } @@ -617,7 +617,9 @@ class PostCoordinator: NSObject { } } - private func shouldScheduleRetry(for error: Error) -> Bool { + /// Returns `true` if the error can't be resolved by simply retrying and + /// requires user interventions, for example, resolving a conflict. + static func isTerminalError(_ error: Error) -> Bool { if let saveError = error as? PostRepository.PostSaveError { switch saveError { case .deleted: @@ -626,6 +628,9 @@ class PostCoordinator: NSObject { return false } } + if let error = error as? SavingError, case .mediaFailure(_, let error) = error { + return MediaCoordinator.isTerminalError(error) + } return true } diff --git a/WordPress/Classes/ViewRelated/Post/PostSyncStateViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSyncStateViewModel.swift index 31205d3eb90e..db1e2da8600c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSyncStateViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSyncStateViewModel.swift @@ -3,8 +3,9 @@ import Foundation final class PostSyncStateViewModel { enum State { case idle - // Has unsynced changes - case unsynced + /// Syncing changes in the background. + case syncing + /// Actively updating the post: moving to trash, restoring, etc. case uploading case offlineChanges case failed @@ -31,9 +32,8 @@ final class PostSyncStateViewModel { return .uploading } if let error = PostCoordinator.shared.syncError(for: post.original()) { - if let saveError = error as? PostRepository.PostSaveError, - case .conflict = saveError { - return .failed // Terminal error + if PostCoordinator.isTerminalError(error) { + return .failed } if let urlError = (error as NSError).underlyingErrors.first as? URLError, urlError.code == .notConnectedToInternet { @@ -41,7 +41,7 @@ final class PostSyncStateViewModel { } } if PostCoordinator.shared.isSyncNeeded(for: post) { - return .unsynced + return .syncing } return .idle } @@ -66,7 +66,7 @@ final class PostSyncStateViewModel { } var isShowingIndicator: Bool { - state == .uploading || state == .unsynced + state == .uploading || state == .syncing } var iconInfo: (image: UIImage?, color: UIColor)? { @@ -75,7 +75,7 @@ final class PostSyncStateViewModel { return (UIImage(systemName: "wifi.slash"), UIColor.listIcon) case .failed: return (UIImage.gridicon(.notice), UIColor.error) - case .idle, .uploading, .unsynced: + case .idle, .uploading, .syncing: return nil } } @@ -87,7 +87,7 @@ final class PostSyncStateViewModel { switch state { case .offlineChanges: return Strings.offlineChanges - case .failed, .idle, .uploading, .unsynced: + case .failed, .idle, .uploading, .syncing: return nil } } From 7b4724ff2954c851c051fd49eeecc4352ffa2801 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 25 Apr 2024 10:38:02 -0400 Subject: [PATCH 097/116] Flip condition in isTerminalError --- WordPress/Classes/Services/PostCoordinator.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 4ed5a1be2f12..a9e4fbc5e83b 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -622,16 +622,14 @@ class PostCoordinator: NSObject { static func isTerminalError(_ error: Error) -> Bool { if let saveError = error as? PostRepository.PostSaveError { switch saveError { - case .deleted: - return false - case .conflict: - return false + case .deleted, .conflict: + return true } } if let error = error as? SavingError, case .mediaFailure(_, let error) = error { return MediaCoordinator.isTerminalError(error) } - return true + return false } private func didRetryTimerFire(for worker: SyncWorker) { From 92e4782d034e61f28a30958c6825db015dde42d2 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Thu, 25 Apr 2024 14:42:23 -0400 Subject: [PATCH 098/116] Improved data fetching --- Podfile | 2 +- Podfile.lock | 8 ++++---- .../Subscribers/StatsSubscribersStore.swift | 12 +++++------ .../StatsSubscribersViewModel.swift | 20 ++++++++----------- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Podfile b/Podfile index 56cb4d795e6a..c3fef31d32d9 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '3197069de70b1e2b57ca8f399896edfdd908fe4a' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '1b312425f37a0acee9027494f30adf8af69a6ce7' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index 18b720e6293a..6644923f9a53 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `3197069de70b1e2b57ca8f399896edfdd908fe4a`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `1b312425f37a0acee9027494f30adf8af69a6ce7`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 3197069de70b1e2b57ca8f399896edfdd908fe4a + :commit: 1b312425f37a0acee9027494f30adf8af69a6ce7 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 3197069de70b1e2b57ca8f399896edfdd908fe4a + :commit: 1b312425f37a0acee9027494f30adf8af69a6ce7 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 137d276a703acdcfe3339bcea95bdd41f68761f4 +PODFILE CHECKSUM: 25817355a3abf615b1ed495e9e58acf75647c5b3 COCOAPODS: 1.15.2 diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift index 7aea01edc758..fcd6cdf349f3 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersStore.swift @@ -55,8 +55,8 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol { func updateChartSummary() { guard chartSummary.value != .loading else { return } - let unit = StatsSubscribersSummaryData.Unit.day - let cacheKey = StatsSubscribersCache.CacheKey.chartSummary(unit: unit.rawValue, siteId: siteID) + let unit = StatsPeriodUnit.day + let cacheKey = StatsSubscribersCache.CacheKey.chartSummary(unit: unit.stringValue, siteId: siteID) let cachedData: StatsSubscribersSummaryData? = cache.getValue(key: cacheKey) if let cachedData = cachedData { @@ -65,13 +65,13 @@ struct StatsSubscribersStore: StatsSubscribersStoreProtocol { chartSummary.send(.loading) } - statsService.getSubscribers(unit: unit) { result in + statsService.getData(for: unit, endingOn: StatsDataHelper.currentDateForSite(), limit: 30) { (data: StatsSubscribersSummaryData?, error: Error?) in DispatchQueue.main.async { - switch result { - case .success(let data): + if let data = data { cache.setValue(data, key: cacheKey) self.chartSummary.send(.success(data)) - case .failure: + } + else { if cachedData == nil { self.chartSummary.send(.error) } diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift index 9555e5c3dc8f..c6483803e61b 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift @@ -21,18 +21,14 @@ final class StatsSubscribersViewModel { // MARK: - Lifecycle func addObservers() { - Publishers.MergeMany(store.chartSummary) - .removeDuplicates() - .sink { [weak self] _ in - self?.updateTableViewSnapshot() - } - .store(in: &cancellables) - Publishers.MergeMany(store.emailsSummary) - .removeDuplicates() - .sink { [weak self] _ in - self?.updateTableViewSnapshot() - } - .store(in: &cancellables) + Publishers.CombineLatest( + store.chartSummary.removeDuplicates(), + store.emailsSummary.removeDuplicates() + ) + .sink { [weak self] value in + self?.updateTableViewSnapshot() + } + .store(in: &cancellables) } func removeObservers() { From 7c297e713b4410d0f3637585452d9a9e3b731c6e Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 25 Apr 2024 14:57:45 -0400 Subject: [PATCH 099/116] Always enable retry button --- .../Classes/ViewRelated/Post/PostMediaUploadStatusView.swift | 2 +- .../Classes/ViewRelated/Post/PostMediaUploadViewModel.swift | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift index b4e06a8099de..c819574334b8 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadStatusView.swift @@ -12,7 +12,7 @@ struct PostMediaUploadStatusView: View { Menu { Button(action: viewModel.buttonRetryTapped) { Label(Strings.retryUploads, systemImage: "arrow.clockwise") - }.disabled(viewModel.isButtonRetryDisabled) + } } label: { Image(systemName: "ellipsis") } diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift index e2f941e2f56d..7d7615c1bb41 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadViewModel.swift @@ -17,10 +17,6 @@ final class PostMediaUploadViewModel: ObservableObject { private weak var timer: Timer? private var cancellables: [AnyCancellable] = [] - var isButtonRetryDisabled: Bool { - !(uploads.contains(where: { $0.error != nil })) - } - deinit { timer?.invalidate() } @@ -168,6 +164,7 @@ final class MediaUploadViewModel: ObservableObject, Identifiable { } fileprivate func retry() { + retryTimer?.invalidate() retryTimer = nil coordinator.retryMedia(media) } From e113b0b2f2f199a22b46e4cd5396902db3bdf9a4 Mon Sep 17 00:00:00 2001 From: "Tanner W. Stokes" Date: Thu, 25 Apr 2024 15:09:38 -0400 Subject: [PATCH 100/116] Release script: Update gutenberg-mobile ref 1.118.0 --- Gutenberg/config.yml | 2 +- Podfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gutenberg/config.yml b/Gutenberg/config.yml index 55aa4e8fdf7e..54867b2a8753 100644 --- a/Gutenberg/config.yml +++ b/Gutenberg/config.yml @@ -9,6 +9,6 @@ # # LOCAL_GUTENBERG=../my-gutenberg-fork bundle exec pod install ref: - commit: b5afe640373f481d9224ff5df4fdd77a0c81bf59 + tag: v1.118.0 github_org: wordpress-mobile repo_name: gutenberg-mobile diff --git a/Podfile.lock b/Podfile.lock index c30ea3c014e1..f2ba6d42f457 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -107,7 +107,7 @@ DEPENDENCIES: - Gifu (= 3.3.1) - Gravatar (= 1.0.1) - Gridicons (~> 1.2) - - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-b5afe640373f481d9224ff5df4fdd77a0c81bf59.podspec`) + - Gutenberg (from `https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.118.0.podspec`) - JTAppleCalendar (~> 8.0.5) - Kanvas (~> 1.4.4) - MediaEditor (>= 1.2.2, ~> 1.2) @@ -175,7 +175,7 @@ EXTERNAL SOURCES: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 Gutenberg: - :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-b5afe640373f481d9224ff5df4fdd77a0c81bf59.podspec + :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.118.0.podspec WordPressKit: :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git @@ -207,7 +207,7 @@ SPEC CHECKSUMS: Gifu: 416d4e38c4c2fed012f019e0a1d3ffcb58e5b842 Gravatar: 51437de6811c1d8d6f60c52985f2ca00a85cfc8e Gridicons: 4455b9f366960121430e45997e32112ae49ffe1d - Gutenberg: 74bc722d74e2276557fe1d851a1eef0df963bb33 + Gutenberg: 3117b6fe578fb7f0bb75377ba96f436ee05c30fe JTAppleCalendar: 16c6501b22cb27520372c28b0a2e0b12c8d0cd73 Kanvas: cc027f8058de881a4ae2b5aa5f05037b6d054d08 MediaEditor: d08314cfcbfac74361071a306b4bc3a39b3356ae From 25184c54efb19e34cfac345eb359881058514975 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Thu, 25 Apr 2024 15:38:12 -0400 Subject: [PATCH 101/116] Merge in trunk and fixed issues --- Podfile | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index a5dda3b97bfc..6563351ef510 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '8e020f825d8b21bb40c6cab324e97ca69f3090c9' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index 84ba5bea8268..db2387e8ddbf 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `8e020f825d8b21bb40c6cab324e97ca69f3090c9`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 + :commit: 8e020f825d8b21bb40c6cab324e97ca69f3090c9 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 14aa53a2e1cfa764e3e9e3e91d1f39f4ef09e098 + :commit: 8e020f825d8b21bb40c6cab324e97ca69f3090c9 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 4ac1d35f8415bdc8d4c8e39d6aa7a9f6dab5933d +PODFILE CHECKSUM: 55b1b06689c2f550323787c0568cb237bc11499a COCOAPODS: 1.15.2 From 1dc5fc0f3e71cb6c5f65e20bc3f0c3d2192cc836 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:30:50 +0700 Subject: [PATCH 102/116] remove brightness filter on blockquotes --- .../Classes/ViewRelated/Reader/Detail/WebView/reader.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/WebView/reader.css b/WordPress/Classes/ViewRelated/Reader/Detail/WebView/reader.css index 4c79c2ac8c81..58d25d2fb0ee 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/WebView/reader.css +++ b/WordPress/Classes/ViewRelated/Reader/Detail/WebView/reader.css @@ -75,7 +75,6 @@ figure.wp-block-embed.is-type-video { /* Temporary override for Reader Customization */ .reader-full-post__story-content blockquote { - color: var(--color-text) !important; - border-left: 3px solid var(--color-text) !important; - filter: brightness(250%); + color: var(--color-text); + border-left: 3px solid var(--color-neutral-50); } From 40fa62ce8603cb4a4998105ce3d586fe770264f0 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 26 Apr 2024 03:05:44 +0700 Subject: [PATCH 103/116] Fix colors for comment view Included in this commit: * Fix a missing semicolon from the injected CSS variables which caused --link-color to be ignored. * Fix logic error in one of the computed variables. * Remove unneeded entries from the richCommentStyle.css. * Tidy up the logic to make it easy to clean up once the feature flag is removed. --- .../WebCommentContentRenderer.swift | 22 +++++++++---------- .../Reader/Theme/ReaderDisplaySetting.swift | 11 ++++++++++ WordPress/Resources/HTML/richCommentStyle.css | 10 --------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Comments/ContentRenderer/WebCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/ContentRenderer/WebCommentContentRenderer.swift index 555102e8f11c..2da0c3dc4c64 100644 --- a/WordPress/Classes/ViewRelated/Comments/ContentRenderer/WebCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/ContentRenderer/WebCommentContentRenderer.swift @@ -139,28 +139,27 @@ private extension WebCommentContentRenderer { ReaderDisplaySetting.customizationEnabled ? displaySetting.color.foreground : .text } - var highlightColor: UIColor { - ReaderDisplaySetting.customizationEnabled ? Constants.highlightColor : displaySetting.color.foreground - } - var mentionBackgroundColor: UIColor { - ReaderDisplaySetting.customizationEnabled ? Constants.mentionBackgroundColor : .clear + guard ReaderDisplaySetting.customizationEnabled else { + return Constants.mentionBackgroundColor + } + + return displaySetting.color == .system ? Constants.mentionBackgroundColor : displaySetting.color.secondaryBackground } var linkColor: UIColor { guard ReaderDisplaySetting.customizationEnabled else { - return highlightColor + return Constants.highlightColor } - return displaySetting.color == .system ? .muriel(color: .init(name: .blue)) : displaySetting.color.foreground + return displaySetting.color == .system ? Constants.highlightColor : displaySetting.color.foreground } var secondaryBackgroundColor: UIColor { - guard ReaderDisplaySetting.customizationEnabled, - displaySetting.color != .system else { + guard ReaderDisplaySetting.customizationEnabled else { return .secondarySystemBackground } - return displaySetting.color.border + return displaySetting.color.secondaryBackground } /// Cache the HTML template format. We only need read the template once. @@ -229,8 +228,7 @@ private extension WebCommentContentRenderer { return """ :root { --text-color: \(textColor.color(for: trait).cssRGBAString()); - --text-secondary-color: \(displaySetting.color.secondaryForeground.color(for: trait).cssRGBAString()) - --primary-color: \(highlightColor.color(for: trait).cssRGBAString()); + --text-secondary-color: \(displaySetting.color.secondaryForeground.color(for: trait).cssRGBAString()); --link-color: \(linkColor.color(for: trait).cssRGBAString()); --mention-background-color: \(mentionBackgroundColor.color(for: trait).cssRGBAString()); --background-secondary-color: \(secondaryBackgroundColor.color(for: trait).cssRGBAString()); diff --git a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift index 971630eaa75b..ad7c6ee428d5 100644 --- a/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift +++ b/WordPress/Classes/ViewRelated/Reader/Theme/ReaderDisplaySetting.swift @@ -180,6 +180,17 @@ struct ReaderDisplaySetting: Codable, Equatable { } } + var secondaryBackground: UIColor { + switch self { + case .system: + return .secondarySystemBackground + case .evening, .oled, .hacker: + return foreground.withAlphaComponent(0.15) // slightly higher contrast for dark themes. + default: + return foreground.withAlphaComponent(0.1) + } + } + var border: UIColor { switch self { case .system: diff --git a/WordPress/Resources/HTML/richCommentStyle.css b/WordPress/Resources/HTML/richCommentStyle.css index e02f0a926948..99b7d902ba56 100644 --- a/WordPress/Resources/HTML/richCommentStyle.css +++ b/WordPress/Resources/HTML/richCommentStyle.css @@ -27,19 +27,9 @@ color-scheme: light dark; /* custom variables. */ - --primary-color: rgba(6, 117, 196, 1); - --mention-background-color: rgba(2, 103, 255, .1); --monospace-font: ui-monospace, monospace; } -/* overrides for dark color scheme. */ -@media(prefers-color-scheme: dark) { - :root { - --primary-color: rgba(57, 156, 227, 1); - --mention-background-color: rgba(2, 103, 255, .2); - } -} - /* disable WebKit text inflation algorithm to prevent text size increase on orientation change. */ html { -webkit-text-size-adjust: none; From 75a3048c9499516f3bb388ef76ca1cf54c8091d9 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 25 Apr 2024 16:01:13 -0700 Subject: [PATCH 104/116] =?UTF-8?q?Update=20app=20translations=20=E2=80=93?= =?UTF-8?q?=20`Localizable.strings`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WordPress/Resources/ar.lproj/Localizable.strings | 14 +++++++++++++- WordPress/Resources/es.lproj/Localizable.strings | 11 ++++++++++- WordPress/Resources/he.lproj/Localizable.strings | 5 ++++- WordPress/Resources/id.lproj/Localizable.strings | 5 ++++- WordPress/Resources/it.lproj/Localizable.strings | 5 ++++- WordPress/Resources/ja.lproj/Localizable.strings | 5 ++++- WordPress/Resources/ko.lproj/Localizable.strings | 5 ++++- WordPress/Resources/ru.lproj/Localizable.strings | 5 ++++- WordPress/Resources/tr.lproj/Localizable.strings | 4 ++-- .../Resources/zh-Hans.lproj/Localizable.strings | 5 ++++- .../Resources/zh-Hant.lproj/Localizable.strings | 5 ++++- 11 files changed, 57 insertions(+), 12 deletions(-) diff --git a/WordPress/Resources/ar.lproj/Localizable.strings b/WordPress/Resources/ar.lproj/Localizable.strings index ea16ca7106b3..c41b11f71927 100644 --- a/WordPress/Resources/ar.lproj/Localizable.strings +++ b/WordPress/Resources/ar.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-22 20:54:07+0000 */ +/* Translation-Revision-Date: 2024-04-24 12:15:26+0000 */ /* Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5)))); */ /* Generator: GlotPress/4.0.1 */ /* Language: ar */ @@ -8987,6 +8987,9 @@ translators: %s: Select control option value e.g: \"Auto, 25%\". */ /* Short status description */ "blazeCampaign.status.rejected" = "مرفوض"; +/* Short status description */ +"blazeCampaign.status.scheduled" = "تمت الجدولة"; + /* Title for budget stats view */ "blazeCampaigns.budget" = "الميزانية"; @@ -9213,6 +9216,9 @@ Example: Reply to Pamela Nguyen */ /* Boolean User Defaults debug menu item */ "debugMenu.booleanUserDefaults" = "الإعدادات الافتراضية للمستخدمين في ما يتعلق بالقيم المنطقية"; +/* Boolean User Defaults Debug Menu screen title */ +"debugMenu.booleanUserDefaults.title" = "الإعدادات الافتراضية للمستخدمين في ما يتعلق بالقيم المنطقية"; + /* Feature flags menu item */ "debugMenu.featureFlags" = "تمييز العلامات"; @@ -11002,6 +11008,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Describes that the slider is used to customize the text size in the Reader. */ "reader.preferences.control.sizeSlider.description" = "الحجم"; +/* Text for a small label in the navigation bar that hints that this is an experimental feature. + +The enclosing angled brackets ('<' and '>') are decorative and only intended as a flavor. +Feel free to replace it with other bracket types that you think looks better for the locale. */ +"reader.preferences.navBar.experimental.label" = "<تجريبي>"; + /* Text format for the feedback line text, to be displayed in the preview section. %1$@ is a placeholder for a call-to-action that completes the line, which will be filled programmatically. Example: 'This is a new feature still in development. To help us improve it send your feedback.' */ diff --git a/WordPress/Resources/es.lproj/Localizable.strings b/WordPress/Resources/es.lproj/Localizable.strings index 17b58211c6b9..a057b0907e88 100644 --- a/WordPress/Resources/es.lproj/Localizable.strings +++ b/WordPress/Resources/es.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:56:09+0000 */ +/* Translation-Revision-Date: 2024-04-25 09:34:01+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: es */ @@ -11008,6 +11008,12 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Describes that the slider is used to customize the text size in the Reader. */ "reader.preferences.control.sizeSlider.description" = "Tamaño"; +/* Text for a small label in the navigation bar that hints that this is an experimental feature. + +The enclosing angled brackets ('<' and '>') are decorative and only intended as a flavor. +Feel free to replace it with other bracket types that you think looks better for the locale. */ +"reader.preferences.navBar.experimental.label" = "«Experimental»"; + /* Text format for the feedback line text, to be displayed in the preview section. %1$@ is a placeholder for a call-to-action that completes the line, which will be filled programmatically. Example: 'This is a new feature still in development. To help us improve it send your feedback.' */ @@ -11204,6 +11210,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "Entradas relacionadas"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "Copiar enlace"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Descartar"; diff --git a/WordPress/Resources/he.lproj/Localizable.strings b/WordPress/Resources/he.lproj/Localizable.strings index 8bf37857bbec..b8e5fc67722a 100644 --- a/WordPress/Resources/he.lproj/Localizable.strings +++ b/WordPress/Resources/he.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:54:09+0000 */ +/* Translation-Revision-Date: 2024-04-24 14:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: he_IL */ @@ -11198,6 +11198,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "פוסטים קשורים"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "להעתיק את הקישור"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "ביטול"; diff --git a/WordPress/Resources/id.lproj/Localizable.strings b/WordPress/Resources/id.lproj/Localizable.strings index 2f4064c087e9..5cbd12183103 100644 --- a/WordPress/Resources/id.lproj/Localizable.strings +++ b/WordPress/Resources/id.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:55:40+0000 */ +/* Translation-Revision-Date: 2024-04-23 09:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: id */ @@ -11204,6 +11204,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "Pos Terkait"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "Salin Tautan"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Tutup"; diff --git a/WordPress/Resources/it.lproj/Localizable.strings b/WordPress/Resources/it.lproj/Localizable.strings index 1ffa3a966977..57ed843e6413 100644 --- a/WordPress/Resources/it.lproj/Localizable.strings +++ b/WordPress/Resources/it.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-17 15:38:23+0000 */ +/* Translation-Revision-Date: 2024-04-23 11:54:09+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: it */ @@ -11156,6 +11156,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "Articoli correlati"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "Copia link"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Ignora"; diff --git a/WordPress/Resources/ja.lproj/Localizable.strings b/WordPress/Resources/ja.lproj/Localizable.strings index 73683fe69156..73732f7d5d4f 100644 --- a/WordPress/Resources/ja.lproj/Localizable.strings +++ b/WordPress/Resources/ja.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-17 15:44:43+0000 */ +/* Translation-Revision-Date: 2024-04-23 10:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.1 */ /* Language: ja_JP */ @@ -11186,6 +11186,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "関連記事"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "リンクをコピー"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "閉じる"; diff --git a/WordPress/Resources/ko.lproj/Localizable.strings b/WordPress/Resources/ko.lproj/Localizable.strings index b0b7adb2a24c..ccd23ee16ba1 100644 --- a/WordPress/Resources/ko.lproj/Localizable.strings +++ b/WordPress/Resources/ko.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:55:50+0000 */ +/* Translation-Revision-Date: 2024-04-23 09:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.1 */ /* Language: ko_KR */ @@ -11159,6 +11159,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "관련 게시물"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "링크 복사"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "해제"; diff --git a/WordPress/Resources/ru.lproj/Localizable.strings b/WordPress/Resources/ru.lproj/Localizable.strings index 69c4d406b158..9504422f3ea2 100644 --- a/WordPress/Resources/ru.lproj/Localizable.strings +++ b/WordPress/Resources/ru.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:56:25+0000 */ +/* Translation-Revision-Date: 2024-04-23 17:54:09+0000 */ /* Plural-Forms: nplurals=3; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2); */ /* Generator: GlotPress/4.0.1 */ /* Language: ru */ @@ -11204,6 +11204,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "Похожие записи"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "Копировать ссылку"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Закрыть"; diff --git a/WordPress/Resources/tr.lproj/Localizable.strings b/WordPress/Resources/tr.lproj/Localizable.strings index b1c5f8f3b2f0..826d92cd4cbe 100644 --- a/WordPress/Resources/tr.lproj/Localizable.strings +++ b/WordPress/Resources/tr.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-22 12:54:07+0000 */ +/* Translation-Revision-Date: 2024-04-23 15:54:08+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: tr */ @@ -11205,7 +11205,7 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ "relatedPostsSettings.title" = "İlgili yazılar"; /* Title for the \"Copy Link\" action in Share Sheet. */ -"share.sheet.copy.link.title" = "Bağlantıyı kopyalayın"; +"share.sheet.copy.link.title" = "Bağlantıyı Kopyala"; /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Kapat"; diff --git a/WordPress/Resources/zh-Hans.lproj/Localizable.strings b/WordPress/Resources/zh-Hans.lproj/Localizable.strings index c63c231b1166..593d3aefb38c 100644 --- a/WordPress/Resources/zh-Hans.lproj/Localizable.strings +++ b/WordPress/Resources/zh-Hans.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:54:29+0000 */ +/* Translation-Revision-Date: 2024-04-23 09:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.1 */ /* Language: zh_CN */ @@ -11183,6 +11183,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "相关文章"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "复制链接"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "忽略"; diff --git a/WordPress/Resources/zh-Hant.lproj/Localizable.strings b/WordPress/Resources/zh-Hant.lproj/Localizable.strings index 61b37f10823c..1417562ed26a 100644 --- a/WordPress/Resources/zh-Hant.lproj/Localizable.strings +++ b/WordPress/Resources/zh-Hant.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-04-19 07:53:58+0000 */ +/* Translation-Revision-Date: 2024-04-23 09:54:08+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/4.0.1 */ /* Language: zh_TW */ @@ -11189,6 +11189,9 @@ Refer to: `reader.preferences.preview.body.feedback.format` */ /* Title for screen that allows configuration of your blog/site related posts settings. */ "relatedPostsSettings.title" = "相關文章"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "複製連結"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "關閉"; From e00afaac10c6b1044d1c093d0d33794c03b2c7f7 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 25 Apr 2024 16:01:18 -0700 Subject: [PATCH 105/116] Update WordPress metadata translations --- fastlane/metadata/es-ES/release_notes.txt | 4 ++++ fastlane/metadata/fr-FR/release_notes.txt | 4 ++++ fastlane/metadata/he/release_notes.txt | 4 ++++ fastlane/metadata/id/release_notes.txt | 4 ++++ fastlane/metadata/it/release_notes.txt | 4 ++++ fastlane/metadata/ja/release_notes.txt | 4 ++++ fastlane/metadata/ko/release_notes.txt | 4 ++++ fastlane/metadata/ru/release_notes.txt | 4 ++++ fastlane/metadata/sv/release_notes.txt | 4 ++++ fastlane/metadata/zh-Hans/release_notes.txt | 4 ++++ fastlane/metadata/zh-Hant/release_notes.txt | 4 ++++ 11 files changed, 44 insertions(+) create mode 100644 fastlane/metadata/es-ES/release_notes.txt create mode 100644 fastlane/metadata/fr-FR/release_notes.txt create mode 100644 fastlane/metadata/he/release_notes.txt create mode 100644 fastlane/metadata/id/release_notes.txt create mode 100644 fastlane/metadata/it/release_notes.txt create mode 100644 fastlane/metadata/ja/release_notes.txt create mode 100644 fastlane/metadata/ko/release_notes.txt create mode 100644 fastlane/metadata/ru/release_notes.txt create mode 100644 fastlane/metadata/sv/release_notes.txt create mode 100644 fastlane/metadata/zh-Hans/release_notes.txt create mode 100644 fastlane/metadata/zh-Hant/release_notes.txt diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..1ed2aec62c5b --- /dev/null +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -0,0 +1,4 @@ +Cargando notas de la versión… +Cargando notas de la versión… +Cargando notas de la versión… +(Solo era una broma, no hay actualizaciones nuevas. Nos vemos dentro de dos semanas). diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..74cee608cbd0 --- /dev/null +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -0,0 +1,4 @@ +Chargement des notes de version… +Chargement des notes de version… +Chargement des notes de version… +(On plaisante, il n'y a pas de mise à jour. Rendez-vous dans deux semaines !) diff --git a/fastlane/metadata/he/release_notes.txt b/fastlane/metadata/he/release_notes.txt new file mode 100644 index 000000000000..0f77b5f21fd4 --- /dev/null +++ b/fastlane/metadata/he/release_notes.txt @@ -0,0 +1,4 @@ +טוען הערות לשחרור גרסה... +טוען הערות לשחרור גרסה... +טוען הערות לשחרור גרסה... +(סתם, בצחוק... אין עדכונים חדשים. נתראה בעוד שבועיים!) diff --git a/fastlane/metadata/id/release_notes.txt b/fastlane/metadata/id/release_notes.txt new file mode 100644 index 000000000000..5bbfb95b98e3 --- /dev/null +++ b/fastlane/metadata/id/release_notes.txt @@ -0,0 +1,4 @@ +Memuat catatan rilis… +Memuat catatan rilis… +Memuat catatan rilis… +(Bercanda, tidak ada pembaruan baru. Sampai jumpa dua minggu lagi!) diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt new file mode 100644 index 000000000000..5eabf8bcd031 --- /dev/null +++ b/fastlane/metadata/it/release_notes.txt @@ -0,0 +1,4 @@ +Caricamento note di rilascio... +Caricamento note di rilascio... +Caricamento note di rilascio... +(Era uno scherzo, nessun nuovo aggiornamento. Ci vediamo tra due settimane.) diff --git a/fastlane/metadata/ja/release_notes.txt b/fastlane/metadata/ja/release_notes.txt new file mode 100644 index 000000000000..784734162122 --- /dev/null +++ b/fastlane/metadata/ja/release_notes.txt @@ -0,0 +1,4 @@ +リリースノートを読み込み中… +リリースノートを読み込み中… +リリースノートを読み込み中… +(冗談です、新たな更新はありません。 2週間後にお会いしましょう !) diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt new file mode 100644 index 000000000000..134dc8fb244b --- /dev/null +++ b/fastlane/metadata/ko/release_notes.txt @@ -0,0 +1,4 @@ +릴리스 노트를 로드하는 중... +릴리스 노트를 로드하는 중... +릴리스 노트를 로드하는 중... +(농담입니다. 새로운 업데이트는 없습니다. 2주 후에 뵙겠습니다!) diff --git a/fastlane/metadata/ru/release_notes.txt b/fastlane/metadata/ru/release_notes.txt new file mode 100644 index 000000000000..cb10a7485325 --- /dev/null +++ b/fastlane/metadata/ru/release_notes.txt @@ -0,0 +1,4 @@ +Загрузка заметок о выпуске… +Загрузка заметок о выпуске… +Загрузка заметок о выпуске… +(Шутка. Пока никаких обновлений. Увидимся через пару недель!) diff --git a/fastlane/metadata/sv/release_notes.txt b/fastlane/metadata/sv/release_notes.txt new file mode 100644 index 000000000000..df32b8039f78 --- /dev/null +++ b/fastlane/metadata/sv/release_notes.txt @@ -0,0 +1,4 @@ +Versionskommentarer läses in… +Versionskommentarer läses in… +Versionskommentarer läses in… +(Vi skojar bara, inga nya uppdateringar. Vi ses om två veckor.) diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..b4a43ac84ebc --- /dev/null +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -0,0 +1,4 @@ +版本说明正在加载…… +版本说明正在加载…… +版本说明正在加载…… +(开个玩笑,并没有新更新。 两周后见!) diff --git a/fastlane/metadata/zh-Hant/release_notes.txt b/fastlane/metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..5e57e027e4bb --- /dev/null +++ b/fastlane/metadata/zh-Hant/release_notes.txt @@ -0,0 +1,4 @@ +版本資訊載入中... +版本資訊載入中... +版本資訊載入中... +(開玩笑的,這次沒有更新。 兩週後再見!) From 6d63977529c04e88c86b850760077f1f68555e82 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 25 Apr 2024 16:01:21 -0700 Subject: [PATCH 106/116] Update Jetpack metadata translations --- fastlane/jetpack_metadata/de-DE/release_notes.txt | 2 ++ fastlane/jetpack_metadata/es-ES/release_notes.txt | 2 ++ fastlane/jetpack_metadata/fr-FR/release_notes.txt | 2 ++ fastlane/jetpack_metadata/he/release_notes.txt | 2 ++ fastlane/jetpack_metadata/id/release_notes.txt | 2 ++ fastlane/jetpack_metadata/it/release_notes.txt | 2 ++ fastlane/jetpack_metadata/ja/release_notes.txt | 2 ++ fastlane/jetpack_metadata/ko/release_notes.txt | 2 ++ fastlane/jetpack_metadata/nl-NL/release_notes.txt | 2 ++ fastlane/jetpack_metadata/ru/release_notes.txt | 2 ++ fastlane/jetpack_metadata/sv/release_notes.txt | 2 ++ fastlane/jetpack_metadata/zh-Hans/release_notes.txt | 2 ++ fastlane/jetpack_metadata/zh-Hant/release_notes.txt | 2 ++ 13 files changed, 26 insertions(+) create mode 100644 fastlane/jetpack_metadata/de-DE/release_notes.txt create mode 100644 fastlane/jetpack_metadata/es-ES/release_notes.txt create mode 100644 fastlane/jetpack_metadata/fr-FR/release_notes.txt create mode 100644 fastlane/jetpack_metadata/he/release_notes.txt create mode 100644 fastlane/jetpack_metadata/id/release_notes.txt create mode 100644 fastlane/jetpack_metadata/it/release_notes.txt create mode 100644 fastlane/jetpack_metadata/ja/release_notes.txt create mode 100644 fastlane/jetpack_metadata/ko/release_notes.txt create mode 100644 fastlane/jetpack_metadata/nl-NL/release_notes.txt create mode 100644 fastlane/jetpack_metadata/ru/release_notes.txt create mode 100644 fastlane/jetpack_metadata/sv/release_notes.txt create mode 100644 fastlane/jetpack_metadata/zh-Hans/release_notes.txt create mode 100644 fastlane/jetpack_metadata/zh-Hant/release_notes.txt diff --git a/fastlane/jetpack_metadata/de-DE/release_notes.txt b/fastlane/jetpack_metadata/de-DE/release_notes.txt new file mode 100644 index 000000000000..765439b38d3a --- /dev/null +++ b/fastlane/jetpack_metadata/de-DE/release_notes.txt @@ -0,0 +1,2 @@ +Wir haben Leseeinstellungen hinzugefügt, damit du deinen Reader-Bildschirm anpassen kannst. Wähle deine bevorzugte Farbe, Schriftart und Größe aus, um dein Leseerlebnis ganz nach deinen Wünschen zu gestalten. +Zudem haben wir den Tab „Einsichten“ für besseres Laden und Scrollen aktualisiert. Kein Flimmern, keine Abstürze, alles läuft super. diff --git a/fastlane/jetpack_metadata/es-ES/release_notes.txt b/fastlane/jetpack_metadata/es-ES/release_notes.txt new file mode 100644 index 000000000000..8e315b70ce0d --- /dev/null +++ b/fastlane/jetpack_metadata/es-ES/release_notes.txt @@ -0,0 +1,2 @@ +Hemos añadido preferencias de lectura para que puedas personalizar tu pantalla Lector. Selecciona el color, la fuente y el tamaño que más te gusten para una experiencia de lectura a la altura de tus expectativas. +También hemos actualizado la pestaña Detalles para que se cargue y te puedes desplazar mejor. Sin parpadeos, sin interrupciones, cero problemas. diff --git a/fastlane/jetpack_metadata/fr-FR/release_notes.txt b/fastlane/jetpack_metadata/fr-FR/release_notes.txt new file mode 100644 index 000000000000..09e626a36266 --- /dev/null +++ b/fastlane/jetpack_metadata/fr-FR/release_notes.txt @@ -0,0 +1,2 @@ +Nous avons ajouté des préférences de lecture pour que vous puissiez personnaliser l’écran de votre Lecteur. Sélectionnez la couleur, la police et la taille de votre choix pour une expérience de lecture au gré de vos envies. +Nous avons également mis à jour l’onglet Tendances pour un chargement et un défilement améliorés. Pas de vacillement, pas de panne, pas de problème. diff --git a/fastlane/jetpack_metadata/he/release_notes.txt b/fastlane/jetpack_metadata/he/release_notes.txt new file mode 100644 index 000000000000..32a5d7a12f22 --- /dev/null +++ b/fastlane/jetpack_metadata/he/release_notes.txt @@ -0,0 +1,2 @@ +הוספנו העדפות קריאה כדי לאפשר לך להתאים אישית את המסך של ה-Reader. ניתן לבחור צבע, גופן וגודל מועדפים לחוויית הקריאה בהתאם למה שמתאים לך. +בנוסף, עדכנו את הלשונית 'תובנות' כדי לשפר את הטעינה והגלילה. אין הבהובים, אין שבירות שורה, אין בעיות. diff --git a/fastlane/jetpack_metadata/id/release_notes.txt b/fastlane/jetpack_metadata/id/release_notes.txt new file mode 100644 index 000000000000..c72e27a2fd28 --- /dev/null +++ b/fastlane/jetpack_metadata/id/release_notes.txt @@ -0,0 +1,2 @@ +Kami menambahkan preferensi pembaca supaya tampilan Pembaca dapat diatur. Pilih warna, font, dan ukuran yang disukai untuk pengalaman membaca yang sesuai selera. +Kami juga memperbarui tab Insights agar dapat dimuat dan digulir lebih lancar. Tanpa kedipan, tanpa hambatan, tanpa masalah. diff --git a/fastlane/jetpack_metadata/it/release_notes.txt b/fastlane/jetpack_metadata/it/release_notes.txt new file mode 100644 index 000000000000..7d8dab246447 --- /dev/null +++ b/fastlane/jetpack_metadata/it/release_notes.txt @@ -0,0 +1,2 @@ +Abbiamo aggiunto le preferenze di lettura per permetterti di personalizzare lo schermo del lettore. Scegli il colore, il carattere e le dimensioni che preferisci per l'esperienza di lettura che meglio si adatta alle tue esigenze. +Abbiamo inoltre aggiornato la scheda Panoramica migliorandone il caricamento e lo scorrimento. Nessuno sfarfallio, nessun intoppo, nessun problema. diff --git a/fastlane/jetpack_metadata/ja/release_notes.txt b/fastlane/jetpack_metadata/ja/release_notes.txt new file mode 100644 index 000000000000..30b89786fcc8 --- /dev/null +++ b/fastlane/jetpack_metadata/ja/release_notes.txt @@ -0,0 +1,2 @@ +閲覧の設定を追加し、「読者」画面をカスタマイズできるようにしました。 お好きな色、フォント、サイズを選択して、お好みの環境で閲覧できます。 +また、「統計概要」タブを更新し、読み込みとスクロールを改善しました。 ちらつきや崩れなどの問題が発生しなくなりました。 diff --git a/fastlane/jetpack_metadata/ko/release_notes.txt b/fastlane/jetpack_metadata/ko/release_notes.txt new file mode 100644 index 000000000000..28b32b0ba465 --- /dev/null +++ b/fastlane/jetpack_metadata/ko/release_notes.txt @@ -0,0 +1,2 @@ +판독기 화면을 사용자 정의할 수 있도록 읽기 기본 설정이 추가되었습니다. 가장 좋아하는 색상, 글꼴, 크기를 선택하여 내 분위기에 맞는 읽기 경험을 만드세요. +또한 업데이트를 통해 인사이트 탭의 로딩과 스크롤이 개선되어 깜박이거나 깨지는 문제가 없습니다. diff --git a/fastlane/jetpack_metadata/nl-NL/release_notes.txt b/fastlane/jetpack_metadata/nl-NL/release_notes.txt new file mode 100644 index 000000000000..366259a9dc96 --- /dev/null +++ b/fastlane/jetpack_metadata/nl-NL/release_notes.txt @@ -0,0 +1,2 @@ +We hebben leesvoorkeuren toegevoegd zodat je je Reader kan aanpassen. Kies de kleur, het lettertype en de lettergrootte die jij het fijnst vindt, voor een leeservaring die bij jou past. +We hebben ook het tabblad Inzichten bijgewerkt zodat het beter laadt en scrolt. Geen geknipper, geen gebreken, geen probleem. diff --git a/fastlane/jetpack_metadata/ru/release_notes.txt b/fastlane/jetpack_metadata/ru/release_notes.txt new file mode 100644 index 000000000000..fb80c21f1ab9 --- /dev/null +++ b/fastlane/jetpack_metadata/ru/release_notes.txt @@ -0,0 +1,2 @@ +Мы добавили настройки читательских предпочтений, чтобы вы могли настроить экран «Чтиво». Выберите цвет, шрифт и размер по своему вкусу, чтобы получать максимум удовольствия от чтения. +После обновления вкладка «Обзор» быстрее загружается и лучше прокручивается. Больше никакого мигания, никаких сбоев, никаких проблем. diff --git a/fastlane/jetpack_metadata/sv/release_notes.txt b/fastlane/jetpack_metadata/sv/release_notes.txt new file mode 100644 index 000000000000..b0d663ba0a42 --- /dev/null +++ b/fastlane/jetpack_metadata/sv/release_notes.txt @@ -0,0 +1,2 @@ +Vi har lagt till läsinställningar så att du kan anpassa din läsarskärm. Välj färg, typsnitt och storlek efter eget tycke för en läsupplevelse som passar dig. +Vi har också uppdaterat fliken Insikter för bättre inläsning och bläddring. Inget flimrande, inga krascher, inga problem. diff --git a/fastlane/jetpack_metadata/zh-Hans/release_notes.txt b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt new file mode 100644 index 000000000000..6fa34186e5c2 --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hans/release_notes.txt @@ -0,0 +1,2 @@ +我们已添加阅读偏好设置,因此您可以对阅读器屏幕进行自定义。 挑选您最喜欢的颜色、字体和尺寸,打造符合您心境的阅读体验。 +我们还更新了“数据分析”选项卡,以提高加载速度和滚动顺畅度。 无闪烁、无中断、没问题。 diff --git a/fastlane/jetpack_metadata/zh-Hant/release_notes.txt b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt new file mode 100644 index 000000000000..bdfdf66a7402 --- /dev/null +++ b/fastlane/jetpack_metadata/zh-Hant/release_notes.txt @@ -0,0 +1,2 @@ +我們新增了閱讀喜好選項,方便你自訂閱讀器畫面。 你可以自由選擇最喜歡的顏色、字型和大小,打造符合個人風格的閱讀體驗。 +我們也更新了「洞察報告」分頁,使載入和捲動功能變得更順暢, 不會閃爍、不會中斷,也不會發生其他問題。 From ad83ce45ef56c30d2239c8126369234503858a49 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 25 Apr 2024 16:01:34 -0700 Subject: [PATCH 107/116] Bump version number --- config/Version.internal.xcconfig | 2 +- config/Version.public.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/Version.internal.xcconfig b/config/Version.internal.xcconfig index fa429437df09..abd85e5b49f2 100644 --- a/config/Version.internal.xcconfig +++ b/config/Version.internal.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 24.7.0.20240423 +VERSION_LONG = 24.7.0.20240425 VERSION_SHORT = 24.7 diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index 266a988a92af..11df0fd35b18 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 24.7.0.2 +VERSION_LONG = 24.7.0.3 VERSION_SHORT = 24.7 From 9cc7ba2532e92788340987f6e70530631e9f3148 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Thu, 25 Apr 2024 19:21:56 -0400 Subject: [PATCH 108/116] Fix subscriber chart for new sites --- Podfile | 2 +- Podfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index 6563351ef510..347b48ac481a 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '8e020f825d8b21bb40c6cab324e97ca69f3090c9' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '7f47dbc791d59d0b5ad8c958441c3060b784ec43' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index db2387e8ddbf..7aa52ef0ab73 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `8e020f825d8b21bb40c6cab324e97ca69f3090c9`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `7f47dbc791d59d0b5ad8c958441c3060b784ec43`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 8e020f825d8b21bb40c6cab324e97ca69f3090c9 + :commit: 7f47dbc791d59d0b5ad8c958441c3060b784ec43 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 8e020f825d8b21bb40c6cab324e97ca69f3090c9 + :commit: 7f47dbc791d59d0b5ad8c958441c3060b784ec43 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 55b1b06689c2f550323787c0568cb237bc11499a +PODFILE CHECKSUM: 7b5d6ae9b49c6c663e1bcd975a044d4a451ea86e COCOAPODS: 1.15.2 From 938ad44404e80ac34e8120956acc98e08c236a0a Mon Sep 17 00:00:00 2001 From: Chris McGraw <2454408+wargcm@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:32:22 -0400 Subject: [PATCH 109/116] Prevent reloading stream view when menu is fetched --- .../Reader/Tab Navigation/ReaderTabItem.swift | 10 +--------- .../Reader/Tab Navigation/ReaderTabView.swift | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabItem.swift b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabItem.swift index 344d602a9ac3..cc2ea5cff079 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabItem.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabItem.swift @@ -1,6 +1,5 @@ struct ReaderTabItem: FilterTabBarItem, Hashable { - let id = UUID() let shouldHideStreamFilters: Bool let shouldHideSettingsButton: Bool let shouldHideTagFilter: Bool @@ -20,13 +19,6 @@ struct ReaderTabItem: FilterTabBarItem, Hashable { shouldHideTagFilter = content.topicType == .organization || FeatureFlag.readerTagsFeed.enabled } - static func == (lhs: ReaderTabItem, rhs: ReaderTabItem) -> Bool { - return lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } } // MARK: - Localized titles @@ -88,7 +80,7 @@ enum ReaderContentType { case topic } -struct ReaderContent { +struct ReaderContent: Hashable { private(set) var topic: ReaderAbstractTopic? let type: ReaderContentType diff --git a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift index fe6663ec7a3b..bebe19498dd3 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabView.swift @@ -20,6 +20,8 @@ class ReaderTabView: UIView { private var filteredTabs: [(index: Int, topic: ReaderAbstractTopic)] = [] private var previouslySelectedIndex: Int = 0 + private var currentTabItems: [ReaderTabItem] = [] + private weak var childController: UIViewController? private var discoverIndex: Int? { return viewModel.tabItems.firstIndex(where: { $0.content.topicType == .discover }) @@ -55,7 +57,11 @@ class ReaderTabView: UIView { } viewModel.onTabBarItemsDidChange { [weak self] tabItems, index in + if self?.childController != nil && self?.currentTabItems == tabItems { + return + } self?.addContentToContainerView(index: index) + self?.currentTabItems = tabItems } setupViewElements() @@ -119,6 +125,8 @@ extension ReaderTabView { controller.add(childController) containerView.pinSubviewToAllEdges(childController.view) + self.childController = childController + if viewModel.shouldShowCommentSpotlight { let title = NSLocalizedString("Comment to start making connections.", comment: "Hint for users to grow their audience by commenting on other blogs.") childController.displayNotice(title: title) From f6454c727cc6ed5cc103c7fa6bb30402131451fc Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Thu, 25 Apr 2024 20:05:05 -0400 Subject: [PATCH 110/116] Fixed unit test --- WordPress/WordPressTest/StatsSubscribersViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift b/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift index 6dfe29b4ddc4..1d5e02906c62 100644 --- a/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift +++ b/WordPress/WordPressTest/StatsSubscribersViewModelTests.swift @@ -43,7 +43,7 @@ final class StatsSubscribersViewModelTests: XCTestCase { let chartSummary = StatsSubscribersSummaryData(history: [ .init(date: Date(), count: 1), .init(date: Date(), count: 2), - ]) + ], period: .day, periodEndDate: Date()) store.chartSummary.send(.success(chartSummary)) wait(for: [expectation], timeout: 1) From 4c6e847dec17137293b642fa43aa015184dfe67e Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Fri, 26 Apr 2024 02:40:45 -0700 Subject: [PATCH 111/116] Bump version number --- config/Version.internal.xcconfig | 2 +- config/Version.public.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/Version.internal.xcconfig b/config/Version.internal.xcconfig index abd85e5b49f2..5034b5fabc8f 100644 --- a/config/Version.internal.xcconfig +++ b/config/Version.internal.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 24.7.0.20240425 +VERSION_LONG = 24.7.0.20240426 VERSION_SHORT = 24.7 diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index 11df0fd35b18..d066ec1b6130 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 24.7.0.3 +VERSION_LONG = 24.7.0.4 VERSION_SHORT = 24.7 From d108984cd0f36328d893a37c438720f057440ba3 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Fri, 26 Apr 2024 04:44:03 -0700 Subject: [PATCH 112/116] =?UTF-8?q?Update=20app=20translations=20=E2=80=93?= =?UTF-8?q?=20`Localizable.strings`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WordPress/Resources/pt-BR.lproj/Localizable.strings | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPress/Resources/pt-BR.lproj/Localizable.strings b/WordPress/Resources/pt-BR.lproj/Localizable.strings index 6ff81ad1d097..c92baca30684 100644 --- a/WordPress/Resources/pt-BR.lproj/Localizable.strings +++ b/WordPress/Resources/pt-BR.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-02-21 21:32:55+0000 */ +/* Translation-Revision-Date: 2024-04-26 11:37:57+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/4.0.1 */ /* Language: pt_BR */ @@ -9535,6 +9535,9 @@ with the filter chip button. */ /* Reader navigation menu item for the Subscriptions filter */ "reader.navigation.menu.subscriptions" = "Assinaturas"; +/* Reader navigation menu item for the Tags filter */ +"reader.navigation.menu.tags" = "Suas tags"; + /* Reader search button accessibility label. */ "reader.navigation.search.button.label" = "Pesquisar"; @@ -9761,6 +9764,9 @@ Example: given a notice format "Following %@" and empty site name, this will be /* Title for the tooltip anchor. */ "readerDetail.tooltipAnchorTitle.accessibilityLabel" = "Novo"; +/* Title for the \"Copy Link\" action in Share Sheet. */ +"share.sheet.copy.link.title" = "Copiar link"; + /* User action to dismiss media options. */ "shareExtension.editor.attachmentActions.dismiss" = "Dispensar"; From 716ae3545d5f5b8cda58fac83c7fd11c9a2296fd Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Fri, 26 Apr 2024 04:44:08 -0700 Subject: [PATCH 113/116] Update WordPress metadata translations --- fastlane/metadata/pt-BR/release_notes.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fastlane/metadata/pt-BR/release_notes.txt diff --git a/fastlane/metadata/pt-BR/release_notes.txt b/fastlane/metadata/pt-BR/release_notes.txt new file mode 100644 index 000000000000..15cccf1b48b0 --- /dev/null +++ b/fastlane/metadata/pt-BR/release_notes.txt @@ -0,0 +1,4 @@ +Carregando notas da versão… +Carregando notas da versão… +Carregando notas da versão… +Brincadeira, não tem atualização. Até daqui a duas semanas! From b3f0327371e118cadb14c38f9dcdf8a8d63506e9 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Fri, 26 Apr 2024 04:44:24 -0700 Subject: [PATCH 114/116] Bump version number --- config/Version.public.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index d066ec1b6130..6bd61e34d953 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 24.7.0.4 +VERSION_LONG = 24.7.0.5 VERSION_SHORT = 24.7 From fdea4d4de7191328b79d87887749d401be03a0c5 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Fri, 26 Apr 2024 10:24:18 -0400 Subject: [PATCH 115/116] Fix chart row section --- Podfile | 2 +- Podfile.lock | 8 ++++---- .../Stats/Subscribers/StatsSubscribersChartCell.swift | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Podfile b/Podfile index 347b48ac481a..93dd406677a9 100644 --- a/Podfile +++ b/Podfile @@ -56,7 +56,7 @@ end def wordpress_kit # pod 'WordPressKit', '~> 17.0.0' - pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '7f47dbc791d59d0b5ad8c958441c3060b784ec43' + pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '77aee91d607cb8b86d4356c0aebfb3977ff1fcc7' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: '' # pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: '' # pod 'WordPressKit', path: '../WordPressKit-iOS' diff --git a/Podfile.lock b/Podfile.lock index 7aa52ef0ab73..fa52f0297ff6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -121,7 +121,7 @@ DEPENDENCIES: - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19.11) - WordPressAuthenticator (>= 9.0.8, ~> 9.0) - - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `7f47dbc791d59d0b5ad8c958441c3060b784ec43`) + - WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `77aee91d607cb8b86d4356c0aebfb3977ff1fcc7`) - WordPressShared (from `https://github.com/wordpress-mobile/WordPress-iOS-Shared.git`, commit `688ee5e4efddc1fc23626626ef17b7e929bdafb0`) - WordPressUI (~> 1.16) - ZendeskSupportSDK (= 5.3.0) @@ -177,7 +177,7 @@ EXTERNAL SOURCES: Gutenberg: :podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.117.0.podspec WordPressKit: - :commit: 7f47dbc791d59d0b5ad8c958441c3060b784ec43 + :commit: 77aee91d607cb8b86d4356c0aebfb3977ff1fcc7 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -188,7 +188,7 @@ CHECKOUT OPTIONS: :git: https://github.com/wordpress-mobile/FSInteractiveMap.git :tag: 0.2.0 WordPressKit: - :commit: 7f47dbc791d59d0b5ad8c958441c3060b784ec43 + :commit: 77aee91d607cb8b86d4356c0aebfb3977ff1fcc7 :git: https://github.com/wordpress-mobile/WordPressKit-iOS.git WordPressShared: :commit: 688ee5e4efddc1fc23626626ef17b7e929bdafb0 @@ -239,6 +239,6 @@ SPEC CHECKSUMS: ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 7b5d6ae9b49c6c663e1bcd975a044d4a451ea86e +PODFILE CHECKSUM: 4bbf2ae7c80a5f39db237e7c3514872e9f7eb3ca COCOAPODS: 1.15.2 diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift index 9c34e07f54c1..728d1cf49cd2 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersChartCell.swift @@ -17,8 +17,7 @@ class StatsSubscribersChartCell: StatsBaseCell, NibLoadable { } func configure(row: SubscriberChartRow) { - - statSection = .subscribersChart + statSection = row.statSection self.chartData = row.chartData self.chartStyling = row.chartStyling From 55cb3a76bf33fbfc7393ab990d630f21c97b41b9 Mon Sep 17 00:00:00 2001 From: Paul Von Schrottky Date: Fri, 26 Apr 2024 10:42:13 -0400 Subject: [PATCH 116/116] Handle updated subscriber counts --- .../Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift | 4 +++- .../Stats/Subscribers/StatsSubscribersViewModel.swift | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index ea4da9e03a77..0a1fa9fd74c1 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -85,13 +85,15 @@ struct SubscriberChartRow: StatsHashableImmuTableRow { }() let action: ImmuTableAction? = nil + let history: [StatsSubscribersSummaryData.SubscriberData] let chartData: LineChartDataConvertible let chartStyling: LineChartStyling let xAxisDates: [Date] let statSection: StatSection? static func == (lhs: SubscriberChartRow, rhs: SubscriberChartRow) -> Bool { - return lhs.xAxisDates == rhs.xAxisDates + return lhs.xAxisDates == rhs.xAxisDates && + lhs.history == rhs.history } func configureCell(_ cell: UITableViewCell) { diff --git a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift index 9ef86b40b6de..59b3a9330319 100644 --- a/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Subscribers/StatsSubscribersViewModel.swift @@ -70,6 +70,7 @@ private extension StatsSubscribersViewModel { let viewsChart = StatsSubscribersLineChart(counts: chartSummary.history.map { $0.count }) return [ SubscriberChartRow( + history: chartSummary.history, chartData: viewsChart.lineChartData, chartStyling: viewsChart.lineChartStyling, xAxisDates: xAxisDates,