diff --git a/Sources/Keystone/WordPress.h b/Sources/Keystone/WordPress.h index a872eef38340..cbfa826b92b9 100644 --- a/Sources/Keystone/WordPress.h +++ b/Sources/Keystone/WordPress.h @@ -12,7 +12,6 @@ FOUNDATION_EXPORT const unsigned char WordPressVersionString[]; #import #import -#import #import #import diff --git a/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSectionIndexTests.swift b/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSectionIndexTests.swift deleted file mode 100644 index 3641a5a5a90e..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSectionIndexTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -import XCTest -@testable import WordPress - -class BlogDetailsSectionIndexTests: XCTestCase { - func testFindingExistingSectionIndex() { - let blogDetailsViewController = BlogDetailsViewController() - let sections = [ - BlogDetailsSection(title: nil, andRows: [], category: .general), - BlogDetailsSection(title: nil, andRows: [], category: .domainCredit) - ] - let sectionIndex = blogDetailsViewController.findSectionIndex(sections: sections, category: .general) - XCTAssertEqual(sectionIndex, 0) - } - - func testFindingNonExistingSectionIndex() { - let blogDetailsViewController = BlogDetailsViewController() - let sections = [ - BlogDetailsSection(title: nil, andRows: [], category: .general), - BlogDetailsSection(title: nil, andRows: [], category: .domainCredit) - ] - let sectionIndex = blogDetailsViewController.findSectionIndex(sections: sections, category: .external) - XCTAssertEqual(sectionIndex, NSNotFound) - } - - func testFindingSectionIndexFromEmptySections() { - let blogDetailsViewController = BlogDetailsViewController() - let sectionIndex = blogDetailsViewController.findSectionIndex(sections: [], category: .external) - XCTAssertEqual(sectionIndex, NSNotFound) - } -} diff --git a/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSubsectionToSectionCategoryTests.swift b/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSubsectionToSectionCategoryTests.swift deleted file mode 100644 index 3d9140902503..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Blog/BlogDetailsSubsectionToSectionCategoryTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -import XCTest -@testable import WordPress -@testable import WordPressData - -class BlogDetailsSubsectionToSectionCategoryTests: CoreDataTestCase { - var blog: Blog! - - override func setUp() { - blog = BlogBuilder(contextManager.mainContext).build() - } - - func testEachSubsectionToSectionCategory() { - let blogDetailsViewController = BlogDetailsViewController() - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .domainCredit, blog: blog), .domainCredit) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .stats, blog: blog), .general) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .activity, blog: blog), .jetpack) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .pages, blog: blog), .content) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .posts, blog: blog), .content) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .media, blog: blog), .content) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .comments, blog: blog), .content) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .themes, blog: blog), .personalize) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .customize, blog: blog), .personalize) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .sharing, blog: blog), .configure) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .people, blog: blog), .configure) - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .plugins, blog: blog), .configure) - } - - func testEachSubsectionToSectionCategoryForJetpack() { - let blogDetailsViewController = BlogDetailsViewController() - let blog = BlogBuilder(contextManager.mainContext) - .set(blogOption: "is_wpforteams_site", value: false) - .withAnAccount() - .with(isAdmin: true) - .build() - - XCTAssertEqual(blogDetailsViewController.sectionCategory(subsection: .stats, blog: blog), .jetpack) - } -} diff --git a/WordPress/Classes/Apps/Reader/ReaderRootViewPresenter.swift b/WordPress/Classes/Apps/Reader/ReaderRootViewPresenter.swift index 48a3771262b1..8caf7d83e3bb 100644 --- a/WordPress/Classes/Apps/Reader/ReaderRootViewPresenter.swift +++ b/WordPress/Classes/Apps/Reader/ReaderRootViewPresenter.swift @@ -18,7 +18,7 @@ final class ReaderRootViewPresenter: RootViewPresenter { // TODO: (reader) optional? } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) { // TODO: (reader) optional? } diff --git a/WordPress/Classes/System/Root View/RootViewPresenter.swift b/WordPress/Classes/System/Root View/RootViewPresenter.swift index 8d8e8cb0c7ed..dd5ca0c38952 100644 --- a/WordPress/Classes/System/Root View/RootViewPresenter.swift +++ b/WordPress/Classes/System/Root View/RootViewPresenter.swift @@ -9,7 +9,7 @@ protocol RootViewPresenter: AnyObject { func currentlyVisibleBlog() -> Blog? func showMySitesTab() - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) func showReader(path: ReaderNavigationPath?) @@ -28,7 +28,7 @@ extension RootViewPresenter { showBlogDetails(for: blog, then: nil, userInfo: [:]) } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind) { showBlogDetails(for: blog, then: subsection, userInfo: [:]) } @@ -47,7 +47,7 @@ extension RootViewPresenter { } var userInfo: [AnyHashable: Any] = [:] if let source { - userInfo[BlogDetailsViewController.userInfoSourceKey()] = NSNumber(value: source.rawValue) + userInfo[BlogDetailsUserInfoKeys.source] = NSNumber(value: source.rawValue) } showBlogDetails(for: blog, then: .stats) } diff --git a/WordPress/Classes/System/Root View/SplitViewRootPresenter+Site.swift b/WordPress/Classes/System/Root View/SplitViewRootPresenter+Site.swift index 5862d913cab0..f9acaa5df7d5 100644 --- a/WordPress/Classes/System/Root View/SplitViewRootPresenter+Site.swift +++ b/WordPress/Classes/System/Root View/SplitViewRootPresenter+Site.swift @@ -49,7 +49,7 @@ class SiteSplitViewContent: SiteMenuViewControllerDelegate, SplitViewDisplayable } } - func showSubsection(_ subsection: BlogDetailsSubsection, userInfo: [AnyHashable: Any]) { + func showSubsection(_ subsection: BlogDetailsRowKind, userInfo: [String: Any]) { siteMenuVC.showSubsection(subsection, userInfo: userInfo) } } diff --git a/WordPress/Classes/System/Root View/SplitViewRootPresenter.swift b/WordPress/Classes/System/Root View/SplitViewRootPresenter.swift index f3bc50161616..fc1ca988e09d 100644 --- a/WordPress/Classes/System/Root View/SplitViewRootPresenter.swift +++ b/WordPress/Classes/System/Root View/SplitViewRootPresenter.swift @@ -241,7 +241,7 @@ final class SplitViewRootPresenter: RootViewPresenter { return siteContent?.blog } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) { if splitVC.isCollapsed { tabBarVC.showBlogDetails(for: blog, then: subsection, userInfo: userInfo) } else { diff --git a/WordPress/Classes/System/Root View/StaticScreensTabBarWrapper.swift b/WordPress/Classes/System/Root View/StaticScreensTabBarWrapper.swift index 5a6acdf99aba..c10532a6d667 100644 --- a/WordPress/Classes/System/Root View/StaticScreensTabBarWrapper.swift +++ b/WordPress/Classes/System/Root View/StaticScreensTabBarWrapper.swift @@ -20,7 +20,7 @@ class StaticScreensTabBarWrapper: RootViewPresenter { tabBarController.currentlySelectedScreen() } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) { tabBarController.showBlogDetails(for: blog, then: subsection, userInfo: userInfo) } diff --git a/WordPress/Classes/System/Root View/WPTabBarController+RootViewPresenter.swift b/WordPress/Classes/System/Root View/WPTabBarController+RootViewPresenter.swift index d8a7ddb9bb49..76cad8d9d5c0 100644 --- a/WordPress/Classes/System/Root View/WPTabBarController+RootViewPresenter.swift +++ b/WordPress/Classes/System/Root View/WPTabBarController+RootViewPresenter.swift @@ -12,7 +12,7 @@ extension WPTabBarController: RootViewPresenter { return self } - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) { mySitesCoordinator.showBlogDetails(for: blog, then: subsection, userInfo: userInfo) } diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 6d2bb589fb15..adeafbcbbfcb 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -7,7 +7,6 @@ #import "BlogService.h" #import "BlogSyncFacade.h" -#import "BlogDetailsViewController.h" #import "CommentService.h" #import "CommentsViewController.h" diff --git a/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift b/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift index 27f6fcc5c16a..f1ad41d6c679 100644 --- a/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift +++ b/WordPress/Classes/Utility/Universal Links/Routes+MySites.swift @@ -128,7 +128,7 @@ extension MySitesRoute: NavigationAction { presenter.showBlogDetails(for: blog, then: .plugins) case .managePlugins: presenter.showBlogDetails(for: blog, then: .plugins, userInfo: [ - BlogDetailsViewController.userInfoShowManagemenetScreenKey(): true + BlogDetailsUserInfoKeys.showManagePlugins: true ]) case .siteMonitoring: presenter.showSiteMonitoring(for: blog, selectedTab: .metrics) @@ -143,13 +143,13 @@ extension MySitesRoute: NavigationAction { private extension RootViewPresenter { func showMediaPicker(for blog: Blog) { showBlogDetails(for: blog, then: .media, userInfo: [ - BlogDetailsViewController.userInfoShowPickerKey(): true + BlogDetailsUserInfoKeys.showPicker: true ]) } func showSiteMonitoring(for blog: Blog, selectedTab: SiteMonitoringTab) { showBlogDetails(for: blog, then: .siteMonitoring, userInfo: [ - BlogDetailsViewController.userInfoSiteMonitoringTabKey(): selectedTab.rawValue + BlogDetailsUserInfoKeys.siteMonitoringTab: selectedTab.rawValue ]) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift index 3504546d8815..eec5ab2892bb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift @@ -112,10 +112,9 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab let statsVC = StatsHostingViewController.makeStatsViewController(for: blog) parentViewController.show(statsVC, sender: nil) case .more: - let viewController = BlogDetailsViewController() + let viewController = BlogDetailsViewController(blog: blog) viewController.isScrollEnabled = true - viewController.tableView.isScrollEnabled = true - viewController.blog = blog + viewController.tableView?.isScrollEnabled = true viewController.presentationDelegate = self self.blogDetailsViewController = viewController self.parentViewController?.show(viewController, sender: nil) @@ -137,7 +136,7 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab // MARK: - DashboardQuickActionsCardCell (BlogDetailsPresentationDelegate) extension DashboardQuickActionsCardCell: BlogDetailsPresentationDelegate { - func showBlogDetailsSubsection(_ subsection: BlogDetailsSubsection) { + func showBlogDetailsSubsection(_ subsection: BlogDetailsRowKind) { self.blogDetailsViewController?.showDetailView(for: subsection) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift new file mode 100644 index 000000000000..f391864ded68 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsTableViewModel.swift @@ -0,0 +1,1315 @@ +import Foundation +import UIKit +import WordPressLegacy +import WordPressShared +import WordPressSharedObjC +import WordPressUI + +private struct Section { + let title: String? + let rows: [Row] + let footerTitle: String? + let category: SectionCategory + + init( + title: String? = nil, + rows: [Row], + footerTitle: String? = nil, + category: SectionCategory + ) { + self.title = title + self.rows = rows + self.footerTitle = footerTitle + self.category = category + } +} + +@objc public final class BlogDetailsTableViewModel: NSObject { + private var blog: Blog + private weak var tableView: UITableView? + private weak var viewController: BlogDetailsViewController? + private var sections: [Section] = [] + + var restorableSelectedRow: BlogDetailsRowKind? { + didSet { + if let row = restorableSelectedRow, + let section = sections.first(where: { $0.rows.contains { $0.kind == row } }), + [.jetpackBrandingCard, .domainCredit].contains(section.category) { + restorableSelectedRow = nil + } + } + } + + var restorableSelectedIndexPath: IndexPath? { + restorableSelectedRow.flatMap(indexPath(for:)) + } + + var gravatarIcon: UIImage? { + didSet { + if let indexPath = self.indexPath(for: .me) { + tableView?.reloadRows(at: [indexPath], with: .automatic) + } + } + } + + var useSiteMenuStyle = false + + @objc public init(blog: Blog, viewController: BlogDetailsViewController) { + self.blog = blog + self.viewController = viewController + super.init() + } + + @objc public func configure(tableView: UITableView) { + self.tableView = tableView + + // Register standard cells + tableView.register(WPTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.standard) + tableView.register(WPTableViewCellValue1.self, forCellReuseIdentifier: CellIdentifiers.plan) + tableView.register(WPTableViewCellValue1.self, forCellReuseIdentifier: CellIdentifiers.settings) + tableView.register(WPTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.removeSite) + + // Register header/footer views + tableView.register(BlogDetailsSectionFooterView.self, forHeaderFooterViewReuseIdentifier: CellIdentifiers.sectionFooter) + + // Register special card cells + tableView.register(MigrationSuccessCell.self, forCellReuseIdentifier: CellIdentifiers.migrationSuccess) + tableView.register(JetpackBrandingMenuCardCell.self, forCellReuseIdentifier: CellIdentifiers.jetpackBrandingCard) + tableView.register(JetpackRemoteInstallTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.jetpackInstall) + tableView.register(SotWTableViewCell.self, forCellReuseIdentifier: CellIdentifiers.sotWCard) + + tableView.delegate = self + tableView.dataSource = self + } + + @objc public func viewWillAppear() { + if !isSplitViewDisplayed { + restorableSelectedRow = nil + } + } + + @objc public func configureTableViewData() { + guard let viewController else { return } + + var newSections: [Section] = [] + + if viewController.shouldShowSotW2023Card() { + newSections.append(Section(rows: [], category: .sotW2023Card)) + } + + if viewController.shouldShowJetpackInstallCard() { + newSections.append(Section(rows: [], category: .jetpackInstallCard)) + } + + if viewController.shouldShowTopJetpackBrandingMenuCard { + newSections.append(Section(rows: [], category: .jetpackBrandingCard)) + } + + if viewController.isDashboardEnabled() && isSplitViewDisplayed { + newSections.append(buildHomeSection()) + } + + if AppConfiguration.isWordPress { + if viewController.shouldAddJetpackSection() { + newSections.append(buildJetpackSection()) + } + + if viewController.shouldAddGeneralSection() { + newSections.append(buildGeneralSection()) + } + + newSections.append(buildPublishTypeSection()) + + if viewController.shouldAddPersonalizeSection() { + newSections.append(buildPersonalizeSection()) + } + + newSections.append(buildConfigurationSection()) + newSections.append(buildExternalSection()) + } else { + newSections.append(buildContentSection()) + + if let trafficSection = buildTrafficSection() { + newSections.append(trafficSection) + } + + newSections.append(contentsOf: buildMaintenanceSections()) + } + + if blog.supports(.removable) { + newSections.append(buildRemoveSiteSection()) + } + + if viewController.shouldShowBottomJetpackBrandingMenuCard { + newSections.append(Section(rows: [], category: .jetpackBrandingCard)) + } + + sections = newSections + } + + private var isSplitViewDisplayed: Bool { + viewController?.isSidebarModeEnabled ?? false + } + + func defaultSubsection() -> BlogDetailsRowKind { + if !JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() { + return .posts + } + if let viewController, viewController.isDashboardEnabled() { + return .home + } + return .stats + } + + func optimumScrollPosition(for indexPath: IndexPath) -> UITableView.ScrollPosition { + guard let tableView, !isSplitViewDisplayed else { return .none } + + let cellRect = tableView.rectForRow(at: indexPath) + return CGRectContainsRect(tableView.bounds, cellRect) ? .none : .middle + } + + @objc public func reloadTableViewPreservingSelection() { + guard let tableView else { return } + + let previousSelection = tableView.indexPathForSelectedRow + tableView.reloadData() + + if isSplitViewDisplayed, let indexPath = restorableSelectedIndexPath { + tableView.selectRow(at: indexPath, animated: false, scrollPosition: optimumScrollPosition(for: indexPath)) + + if previousSelection != indexPath { + sections[indexPath.section].rows[indexPath.row].action?([:]) + } + } + } + + @objc public func showInitialDetailsForBlog() { + guard isSplitViewDisplayed else { return } + + let row = defaultSubsection() + self.restorableSelectedRow = row + + self.showDetailView(for: row) + } + + @objc func numberOfSections() -> Int { + sections.count + } + + func showDetailViewForMe(userInfo: [String: Any]) -> MeViewController { + guard let viewController else { + wpAssertionFailure("The view controller should not be nil") + return MeViewController() + } + restorableSelectedRow = .me + return viewController.showMe() + } + + func showDetailView(for row: BlogDetailsRowKind, userInfo: [String: Any] = [:]) { + for (sectionIndex, section) in sections.enumerated() { + for (rowIndex, rowItem) in section.rows.enumerated() where rowItem.kind == row { + let indexPath = IndexPath(row: rowIndex, section: sectionIndex) + + if rowItem.showsSelectionState { + restorableSelectedRow = row + + tableView?.selectRow(at: indexPath, animated: false, scrollPosition: optimumScrollPosition(for: indexPath)) + } + + // Call the row's action + rowItem.action?(userInfo) + return + } + } + } + + func indexPath(for row: BlogDetailsRowKind) -> IndexPath? { + for (sectionIndex, section) in sections.enumerated() { + for (rowIndex, rowItem) in section.rows.enumerated() where rowItem.kind == row { + return IndexPath(row: rowIndex, section: sectionIndex) + } + } + return nil + } +} + +extension BlogDetailsTableViewModel: UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + sections.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard section < sections.count else { return 0 } + + switch sections[section].category { + case .sotW2023Card, .jetpackInstallCard, .migrationSuccess, .jetpackBrandingCard: + // The "card" sections do not set the `rows` property. It's hard-coded to show specific types of cards. + wpAssert(sections[section].rows.count == 0) + return 1 + default: + return sections[section].rows.count + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard indexPath.section < sections.count else { + return UITableViewCell() + } + + let section = sections[indexPath.section] + let cell: UITableViewCell + + switch section.category { + case .sotW2023Card: + cell = configureSotWCell(tableView: tableView) + case .jetpackInstallCard: + cell = configureJetpackInstallCell(tableView: tableView) + case .migrationSuccess: + cell = configureMigrationSuccessCell(tableView: tableView) + case .jetpackBrandingCard: + cell = configureJetpackBrandingCell(tableView: tableView) + default: + if indexPath.row < section.rows.count { + let row = section.rows[indexPath.row] + cell = configureStandardCell(tableView: tableView, indexPath: indexPath, row: row) + } else { + cell = UITableViewCell() + } + } + + if useSiteMenuStyle { + configureForDisplayingOnSiteMenu(cell) + } + + return cell + } + + public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard section < sections.count else { return nil } + return sections[section].title + } + + private func configureForDisplayingOnSiteMenu(_ cell: UITableViewCell) { + cell.textLabel?.font = .preferredFont(forTextStyle: .body) + cell.backgroundColor = .clear + cell.selectedBackgroundView = { + let backgroundView = UIView() + backgroundView.backgroundColor = .secondarySystemFill + backgroundView.layer.cornerRadius = DesignConstants.radius(.large) + backgroundView.layer.cornerCurve = .continuous + + let container = UIView() + container.addSubview(backgroundView) + backgroundView.pinEdges(insets: UIEdgeInsets(.horizontal, 16)) + return container + }() + cell.focusStyle = .custom + cell.focusEffect = nil + } +} + +extension BlogDetailsTableViewModel: UITableViewDelegate { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.section < sections.count else { return } + let section = sections[indexPath.section] + + guard indexPath.row < section.rows.count else { return } + let row = section.rows[indexPath.row] + + row.action?([:]) + + if row.showsSelectionState { + restorableSelectedRow = row.kind + } else { + if !isSplitViewDisplayed { + tableView.deselectRow(at: indexPath, animated: true) + } else if let indexPath = restorableSelectedIndexPath { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } + } + } + + public func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + let isNewSelection = (indexPath != tableView.indexPathForSelectedRow) + return isNewSelection ? indexPath : nil + } + + public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + guard section < sections.count else { return 0 } + let detailSection = sections[section] + let isLastSection = section == sections.count - 1 + let hasTitle = !(detailSection.footerTitle?.isEmpty ?? true) + + if hasTitle { + return UITableView.automaticDimension + } + if isLastSection { + return 40.0 + } + return 0 + } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard section < sections.count else { return 0 } + let detailSection = sections[section] + let hasTitle = !(detailSection.title?.isEmpty ?? true) + + if useSiteMenuStyle { + return hasTitle ? 48 : 0 + } + + return hasTitle ? 40.0 : 20.0 + } + + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard useSiteMenuStyle else { return nil } + + guard let title = self.tableView(tableView, titleForHeaderInSection: section) else { return nil } + + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .headline) + label.text = title + + let headerView = UIView() + headerView.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20), + label.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -8), + label.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: 20) + ]) + return headerView + } + + public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + guard section < sections.count, + let footerTitle = sections[section].footerTitle, + !footerTitle.isEmpty else { + return nil + } + + guard let footerView = tableView.dequeueReusableHeaderFooterView( + withIdentifier: CellIdentifiers.sectionFooterIdentifier + ) as? BlogDetailsSectionFooterView else { + return nil + } + + let shouldShowExtraSpacing = (section + 1 < sections.count) && (sections[section + 1].title != nil) + footerView.updateUI(title: footerTitle, shouldShowExtraSpacing: shouldShowExtraSpacing) + return footerView + } +} + +private extension BlogDetailsTableViewModel { + func configureStandardCell( + tableView: UITableView, + indexPath: IndexPath, + row: Row + ) -> UITableViewCell { + let identifier = switch row.kind { + case .removeSite: + CellIdentifiers.removeSite + case .jetpackSettings, .siteSettings, .domain: + CellIdentifiers.settings + default: + CellIdentifiers.standard + } + let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) + + cell.accessibilityHint = row.accessibilityHint + cell.accessoryView = nil + cell.textLabel?.textAlignment = .natural + + if row.kind == .removeSite { + cell.accessoryType = .none + WPStyleGuide.configureTableViewDestructiveActionCell(cell) + } else { + if row.showsDisclosureIndicator { + cell.accessoryType = isSplitViewDisplayed ? .none : .disclosureIndicator + } else { + cell.accessoryType = .none + } + WPStyleGuide.configureTableViewCell(cell) + } + + cell.textLabel?.text = row.title + cell.accessibilityIdentifier = row.accessibilityIdentifier ?? identifier + cell.detailTextLabel?.text = row.detail + cell.imageView?.image = row.image + cell.imageView?.tintColor = row.imageColor + + if let accessoryView = row.accessoryView { + cell.accessoryView = accessoryView + } + + return cell + } + + func configureSotWCell(tableView: UITableView) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CellIdentifiers.sotWCard + ) as? SotWTableViewCell else { + return UITableViewCell() + } + + cell.configure { [weak viewController] in + viewController?.configureTableViewData() + viewController?.reloadTableViewPreservingSelection() + } + + return cell + } + + func configureJetpackInstallCell(tableView: UITableView) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CellIdentifiers.jetpackInstall + ) as? JetpackRemoteInstallTableViewCell, + let viewController else { + return UITableViewCell() + } + + cell.configure(blog: blog, viewController: viewController) + return cell + } + + func configureMigrationSuccessCell(tableView: UITableView) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CellIdentifiers.migrationSuccess + ) as? MigrationSuccessCell, + let viewController else { + return UITableViewCell() + } + + if viewController.isSidebarModeEnabled { + cell.configureForSidebarMode() + } + cell.configure(with: viewController) + return cell + } + + func configureJetpackBrandingCell(tableView: UITableView) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: CellIdentifiers.jetpackBrandingCard + ) as? JetpackBrandingMenuCardCell, + let viewController else { + return UITableViewCell() + } + + cell.configure(with: viewController) + return cell + } +} + +private extension BlogDetailsTableViewModel { + func buildHomeSection() -> Section { + return Section(rows: [Row.home(viewController: viewController)], category: .home) + } + + func buildContentSection() -> Section { + var rows: [Row] = [] + + rows.append(Row.posts(viewController: viewController)) + + if blog.supports(.pages) { + rows.append(Row.pages(viewController: viewController)) + } + + rows.append(Row.media(viewController: viewController)) + rows.append(Row.comments(viewController: viewController)) + + let title = isSplitViewDisplayed ? nil : BlogDetailsViewController.Strings.contentSectionTitle + return Section(title: title, rows: rows, category: .content) + } + + func buildRemoveSiteSection() -> Section { + return Section(rows: [Row.removeSite(viewController: viewController)], category: .removeSite) + } + + func buildJetpackSection() -> Section { + var rows: [Row] = [] + + if blog.isViewingStatsAllowed() { + rows.append(Row.stats(viewController: viewController)) + } + + if blog.supports(.activity) && !blog.isWPForTeams() { + rows.append(Row.activityLog(viewController: viewController)) + } + + if blog.isBackupsAllowed() { + rows.append(Row.backup(viewController: viewController)) + } + + if blog.isScanAllowed() { + rows.append(Row.scan(viewController: viewController)) + } + + if blog.supports(.jetpackSettings) { + rows.append(Row.jetpackSettings(viewController: viewController)) + } + + if viewController?.shouldShowBlaze() == true { + rows.append(Row.blaze(viewController: viewController)) + } + + let title = if blog.supports(.jetpackSettings) { + NSLocalizedString("Jetpack", comment: "Section title for the publish table section in the blog details screen") + } else { + "" + } + + return Section(title: title, rows: rows, category: .jetpack) + } + + func buildGeneralSection() -> Section { + var rows: [Row] = [] + + if blog.isViewingStatsAllowed() { + rows.append(Row.stats(viewController: viewController)) + } + + if blog.supports(.activity) && !blog.isWPForTeams() { + rows.append(Row.activity(viewController: viewController)) + } + + if viewController?.shouldShowBlaze() == true { + rows.append(Row.blaze(viewController: viewController)) + } + + return Section(rows: rows, category: .general) + } + + func buildPublishTypeSection() -> Section { + var rows: [Row] = [] + + rows.append(Row.posts(viewController: viewController)) + rows.append(Row.media(viewController: viewController)) + + if blog.supports(.pages) { + rows.append(Row.pages(viewController: viewController)) + } + + rows.append(Row.comments(viewController: viewController)) + + let title = NSLocalizedString("Publish", comment: "Section title for the publish table section in the blog details screen") + return Section(title: title, rows: rows, category: .content) + } + + func buildPersonalizeSection() -> Section { + var rows: [Row] = [] + + if blog.supports(.themeBrowsing) && !blog.isWPForTeams() { + rows.append(Row.themes(viewController: viewController)) + } + + if blog.supports(.menus) { + rows.append(Row.menus(viewController: viewController)) + } + + let title = NSLocalizedString("Personalize", comment: "Section title for the personalize table section in the blog details screen.") + return Section(title: title, rows: rows, category: .personalize) + } + + func buildConfigurationSection() -> Section { + guard let viewController else { + return Section(title: "Configure", rows: [], category: .configure) + } + + var rows: [Row] = [] + + // Me row + if viewController.shouldAddMeRow() { + rows.append(Row.me(icon: gravatarIcon, viewController: viewController)) + // Note: Gravatar image download would be handled by viewController + } + + // Sharing row + if viewController.shouldAddSharingRow() { + rows.append(Row.sharing(viewController: viewController)) + } + + // People row + if viewController.shouldAddPeopleRow() { + rows.append(Row.people(viewController: viewController)) + } + + // Users row + if viewController.shouldAddUsersRow() { + rows.append(Row.users(viewController: viewController)) + } + + // Plugins row + if viewController.shouldAddPluginsRow() { + rows.append(Row.plugins(viewController: viewController)) + } + + // Site Settings row (always included) + rows.append(Row.siteSettings(viewController: viewController)) + + // Domains row + if viewController.shouldAddDomainRegistrationRow() { + rows.append(Row.domains(viewController: viewController)) + } + + let title = NSLocalizedString("Configure", comment: "Section title for the configure table section in the blog details screen") + return Section(title: title, rows: rows, category: .configure) + } + + func buildExternalSection() -> Section { + guard let viewController else { + return Section(title: "External", rows: [], category: .external) + } + + var rows: [Row] = [] + + rows.append(Row.viewSite(viewController: viewController)) + + if shouldDisplayLinkToWPAdmin(for: blog) { + rows.append(Row.admin(viewController: viewController, blog: blog)) + } + + let title = NSLocalizedString("External", comment: "Section title for the external table section in the blog details screen") + return Section(title: title, rows: rows, category: .external) + } + + func buildTrafficSection() -> Section? { + guard let viewController else { return nil } + + var rows: [Row] = [] + + if blog.isViewingStatsAllowed() { + rows.append(Row.stats(viewController: viewController)) + } + + if viewController.shouldShowSubscribersRow { + rows.append(Row.subscribers(viewController: viewController)) + } + + if viewController.shouldAddSharingRow() { + rows.append(Row.social(viewController: viewController)) + } + + if viewController.shouldShowBlaze() { + rows.append(Row.blaze(viewController: viewController)) + } + + if rows.isEmpty { + return nil + } + + let title = BlogDetailsViewController.Strings.trafficSectionTitle + return Section(title: title, rows: rows, category: .traffic) + } + + func buildMaintenanceSections() -> [Section] { + guard let viewController else { return [] } + + var sections: [Section] = [] + var firstSectionRows: [Row] = [] + var secondSectionRows: [Row] = [] + var thirdSectionRows: [Row] = [] + + // First section: Activity, Backup, Scan, Site Monitoring + if blog.supports(.activity) && !blog.isWPForTeams() { + firstSectionRows.append(Row.activityLog(viewController: viewController)) + } + + if blog.isBackupsAllowed() { + firstSectionRows.append(Row.backup(viewController: viewController)) + } + + if blog.isScanAllowed() { + firstSectionRows.append(Row.scan(viewController: viewController)) + } + + if RemoteFeatureFlag.siteMonitoring.enabled() && blog.supports(.siteMonitoring) { + firstSectionRows.append(Row.siteMonitoring(viewController: viewController)) + } + + // Second section: People, Users, Plugins, Themes, Menus, Domains, Application Passwords, Site Settings + if viewController.shouldAddPeopleRow() { + secondSectionRows.append(Row.people(viewController: viewController)) + } + + if viewController.shouldAddUsersRow() { + secondSectionRows.append(Row.users(viewController: viewController)) + } + + if viewController.shouldAddPluginsRow() { + secondSectionRows.append(Row.plugins(viewController: viewController)) + } + + if blog.supports(.themeBrowsing) && !blog.isWPForTeams() { + secondSectionRows.append(Row.themes(viewController: viewController)) + } + + if blog.supports(.menus) { + secondSectionRows.append(Row.menus(viewController: viewController)) + } + + if viewController.shouldAddDomainRegistrationRow() { + secondSectionRows.append(Row.domains(viewController: viewController)) + } + + if FeatureFlag.allowApplicationPasswords.enabled { + secondSectionRows.append(Row.applicationPasswords(viewController: viewController)) + } + + // Site Settings (always included) + secondSectionRows.append(Row.siteSettings(viewController: viewController)) + + // Third section: WP Admin + if shouldDisplayLinkToWPAdmin(for: blog) { + thirdSectionRows.append(Row.admin(viewController: viewController, blog: blog)) + } + + // Build sections with proper titles + let sectionTitle = BlogDetailsViewController.Strings.maintenanceSectionTitle + var shouldAddSectionTitle = true + + if !firstSectionRows.isEmpty { + sections.append(Section( + title: sectionTitle, + rows: firstSectionRows, + category: .maintenance + )) + shouldAddSectionTitle = false + } + + if !secondSectionRows.isEmpty { + sections.append(Section( + title: shouldAddSectionTitle ? sectionTitle : nil, + rows: secondSectionRows, + category: .maintenance + )) + shouldAddSectionTitle = false + } + + if !thirdSectionRows.isEmpty { + sections.append(Section( + title: shouldAddSectionTitle ? sectionTitle : nil, + rows: thirdSectionRows, + category: .maintenance + )) + } + + return sections + } + + // MARK: - Helper Methods + + private func shouldDisplayLinkToWPAdmin(for blog: Blog) -> Bool { + if !blog.isHostedAtWPcom { + return true + } + // For .com users, check if account was created before HideWPAdminDate + let hideWPAdminDateString = "2015-09-07T00:00:00Z" + guard let hideWPAdminDate = ISO8601DateFormatter().date(from: hideWPAdminDateString) else { + return false + } + let context = ContextManager.shared.mainContext + guard let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: context), + let dateCreated = defaultAccount.dateCreated else { + return false + } + return dateCreated < hideWPAdminDate + } +} + +enum BlogDetailsUserInfoKeys { + static let source = "source" + static let showPicker = "show-picker" + static let showManagePlugins = "show-manage-plugins" + static let siteMonitoringTab = "site-monitoring-tab" +} + +// MARK: - Table view content + +private enum SectionCategory { + case reminders + case domainCredit + case home + case general + case jetpack + case personalize + case configure + case external + case removeSite + case migrationSuccess + case jetpackBrandingCard + case jetpackInstallCard + case sotW2023Card + case content + case traffic + case maintenance +} + +enum BlogDetailsRowKind { + case reminders + case domain + case stats + case posts + case customize + case themes + case media + case pages + case activity + case backup + case scan + case jetpackSettings + case me + case comments + case sharing + case people + case subscribers + case plugins + case home + case migrationSuccess + case jetpackBrandingCard + case blaze + case menu + case applicationPasswords + case siteMonitoring + case viewSite + case admin + case siteSettings + case removeSite +} + +private struct Row { + let kind: BlogDetailsRowKind + let title: String + let accessibilityIdentifier: String? + let accessibilityHint: String? + let image: UIImage? + let imageColor: UIColor? + let accessoryView: UIView? + let detail: String? + let showsSelectionState: Bool + let showsDisclosureIndicator: Bool + let action: (([String: Any]) -> Void)? + + init( + kind: BlogDetailsRowKind, + title: String, + accessibilityIdentifier: String? = nil, + accessibilityHint: String? = nil, + image: UIImage?, + imageColor: UIColor? = .label, + accessoryView: UIView? = nil, + detail: String? = nil, + showsSelectionState: Bool = true, + showsDisclosureIndicator: Bool = true, + action: (([String: Any]) -> Void)? = nil, + ) { + self.title = title + self.accessibilityIdentifier = accessibilityIdentifier + self.accessibilityHint = accessibilityHint + self.image = imageColor == nil ? image : image?.withRenderingMode(.alwaysTemplate) + self.imageColor = imageColor + self.accessoryView = accessoryView + self.detail = detail + self.showsSelectionState = showsSelectionState + self.showsDisclosureIndicator = showsDisclosureIndicator + self.action = action + self.kind = kind + } +} + +extension Row { + static func home(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .home, + title: NSLocalizedString("Home", comment: "Noun. Links to a blog's dashboard screen."), + accessibilityIdentifier: "Home Row", + image: UIImage(named: "site-menu-home"), + action: { [weak viewController] _ in + viewController?.showDashboard() + } + ) + } + + static func posts(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .posts, + title: NSLocalizedString("Posts", comment: "Noun. Title. Links to the blog's Posts screen."), + accessibilityIdentifier: "Blog Post Row", + image: (UIImage(named: "site-menu-posts"))?.imageFlippedForRightToLeftLayoutDirection(), + action: { [weak viewController] userInfo in + // When called from showDetailView, use .link as source (matching Objective-C behavior) + // When called from direct tap, use .row (default behavior) + let source: BlogDetailsNavigationSource = userInfo.isEmpty ? .row : .link + viewController?.showPostList(from: source) + } + ) + } + + static func pages(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .pages, + title: NSLocalizedString("Pages", comment: "Noun. Title. Links to the blog's Pages screen."), + accessibilityIdentifier: "Site Pages Row", + image: UIImage(named: "site-menu-pages"), + action: { [weak viewController] userInfo in + // When called from showDetailView, use .link as source (matching Objective-C behavior) + // When called from direct tap, use .row (default behavior) + let source: BlogDetailsNavigationSource = userInfo.isEmpty ? .row : .link + viewController?.showPageList(from: source) + } + ) + } + + static func media(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .media, + title: NSLocalizedString("Media", comment: "Noun. Title. Links to the blog's Media library."), + accessibilityIdentifier: "Media Row", + image: UIImage(named: "site-menu-media"), + action: { [weak viewController] userInfo in + let showPicker = (userInfo[BlogDetailsUserInfoKeys.showPicker] as? NSNumber)?.boolValue ?? false + viewController?.showMediaLibrary(from: .link, showPicker: showPicker) + } + ) + } + + static func comments(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .comments, + title: NSLocalizedString("Comments", comment: "Noun. Title. Links to the blog's Comments screen."), + image: (UIImage(named: "site-menu-comments"))?.imageFlippedForRightToLeftLayoutDirection(), + action: { [weak viewController] userInfo in + // When called from showDetailView, use .link as source (matching Objective-C behavior) + // When called from direct tap, use .row (default behavior) + let source: BlogDetailsNavigationSource = userInfo.isEmpty ? .row : .link + viewController?.showComments(from: source) + } + ) + } + + static func removeSite(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .removeSite, + title: NSLocalizedString("Remove Site", comment: "Button to remove a site from the app"), + image: nil, + showsSelectionState: false, + action: { [weak viewController] _ in + viewController?.tableView?.deselectSelectedRowWithAnimation(true) + viewController?.showRemoveSiteAlert() + } + ) + } + + static func stats(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .stats, + title: NSLocalizedString("Stats", comment: "Noun. Abbv. of Statistics. Links to a blog's Stats screen."), + accessibilityIdentifier: "Stats Row", + image: UIImage(named: "site-menu-stats"), + action: { [weak viewController] userInfo in + let sourceValue = userInfo[BlogDetailsUserInfoKeys.source] as? NSNumber + let source = sourceValue.map { BlogDetailsNavigationSource(rawValue: $0.intValue) ?? .link } ?? .link + viewController?.showStats(from: source) + } + ) + } + + static func activityLog(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .activity, + title: NSLocalizedString("Activity Log", comment: "Noun. Links to a blog's Activity screen."), + accessibilityIdentifier: "Activity Log Row", + image: UIImage(named: "site-menu-activity"), + action: { [weak viewController] _ in + viewController?.showActivity() + } + ) + } + + static func activity(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .activity, + title: NSLocalizedString("Activity", comment: "Noun. Links to a blog's Activity screen."), + image: UIImage(named: "site-menu-activity"), + action: { [weak viewController] _ in + viewController?.showActivity() + } + ) + } + + static func backup(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .backup, + title: NSLocalizedString("Backup", comment: "Noun. Links to a blog's Jetpack Backups screen."), + accessibilityIdentifier: "Backup Row", + image: UIImage.gridicon(.cloudOutline), + action: { [weak viewController] _ in + viewController?.showBackup() + } + ) + } + + static func scan(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .scan, + title: NSLocalizedString("Scan", comment: "Noun. Links to a blog's Jetpack Scan screen."), + accessibilityIdentifier: "Scan Row", + image: UIImage(named: "jetpack-scan-menu-icon"), + action: { [weak viewController] _ in + viewController?.showScan() + } + ) + } + + static func jetpackSettings(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .jetpackSettings, + title: NSLocalizedString("Jetpack Settings", comment: "Noun. Title. Links to the blog's Settings screen."), + accessibilityIdentifier: "Jetpack Settings Row", + image: UIImage(named: "site-menu-settings"), + action: { [weak viewController] _ in + viewController?.showJetpackSettings() + } + ) + } + + static func blaze(viewController: BlogDetailsViewController?) -> Row { + let iconSize = CGSize(width: 24.0, height: 24.0) + let blazeIcon = UIImage(named: "icon-blaze")?.resized(to: iconSize, format: .scaleAspectFit) + return Row( + kind: .blaze, + title: NSLocalizedString("Blaze", comment: "Noun. Links to a blog's Blaze screen."), + accessibilityIdentifier: "Blaze Row", + image: blazeIcon?.imageFlippedForRightToLeftLayoutDirection(), + imageColor: nil, + showsSelectionState: RemoteFeatureFlag.blazeManageCampaigns.enabled(), + action: { [weak viewController] _ in + viewController?.showBlaze() + } + ) + } + + static func themes(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .themes, + title: NSLocalizedString("Themes", comment: "Themes option in the blog details"), + image: UIImage(named: "site-menu-themes"), + action: { [weak viewController] _ in + viewController?.showThemes() + } + ) + } + + static func menus(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .menu, + title: NSLocalizedString("Menus", comment: "Menus option in the blog details"), + image: UIImage.gridicon(.menus).imageFlippedForRightToLeftLayoutDirection(), + action: { [weak viewController] _ in + viewController?.showMenus() + } + ) + } + + static func me(icon: UIImage?, viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .me, + title: NSLocalizedString("Me", comment: "Noun. Title. Links to the Me screen."), + image: icon ?? UIImage.gridicon(.userCircle), + action: { [weak viewController] _ in + viewController?.showMe() + } + ) + } + + static func sharing(viewController: BlogDetailsViewController?) -> Row { + let sharingTitle = AppConfiguration.isWordPress + ? NSLocalizedString("Sharing", comment: "Noun. Title. Links to a blog's sharing options.") + : BlogDetailsViewController.Strings.socialRowTitle + return Row( + kind: .sharing, + title: sharingTitle, + image: UIImage(named: "site-menu-social"), + action: { [weak viewController] userInfo in + // When called from showDetailView, use .link as source (matching Objective-C behavior) + // When called from direct tap, use .row (default behavior) + let source: BlogDetailsNavigationSource = userInfo.isEmpty ? .row : .link + viewController?.showSharing(from: source) + } + ) + } + + static func people(viewController: BlogDetailsViewController?) -> Row { + let title = viewController?.shouldShowSubscribersRow == true + ? BlogDetailsViewController.Strings.users + : NSLocalizedString("People", comment: "Noun. Title. Links to the people management feature.") + return Row( + kind: .people, + title: title, + accessibilityIdentifier: "Users Row", + image: UIImage(named: "site-menu-people"), + action: { [weak viewController] _ in + viewController?.showPeople() + } + ) + } + + static func subscribers(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .subscribers, + title: BlogDetailsViewController.Strings.subscribers, + image: UIImage(named: "wpl-mail"), + action: { [weak viewController] _ in + MainActor.assumeIsolated { + guard let viewController else { return } + guard let blog = SubscribersBlog(blog: viewController.blog) else { + return wpAssertionFailure("incompatible blog") + } + let vc = SubscribersViewController(blog: blog) + viewController.presentationDelegate?.presentBlogDetailsViewController(vc) + } + } + ) + } + + static func users(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .people, + title: NSLocalizedString("Users", comment: "Noun. Title. Links to the user management feature."), + accessibilityIdentifier: "Users Row", + image: UIImage(named: "site-menu-people"), + action: { [weak viewController] _ in + viewController?.showUsers() + } + ) + } + + static func plugins(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .plugins, + title: NSLocalizedString("Plugins", comment: "Noun. Title. Links to the plugin management feature."), + image: UIImage(named: "site-menu-plugins"), + action: { [weak viewController] userInfo in + let showManagement = (userInfo[BlogDetailsUserInfoKeys.showManagePlugins] as? NSNumber)?.boolValue ?? false + if showManagement { + viewController?.showManagePluginsScreen() + } else { + viewController?.showPlugins() + } + } + ) + } + + static func siteSettings(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .siteSettings, + title: NSLocalizedString("Site Settings", comment: "Noun. Title. Links to the blog's Settings screen."), + accessibilityIdentifier: "Settings Row", + image: UIImage(named: "site-menu-settings"), + action: { [weak viewController] _ in + viewController?.showSettings(from: .row) + } + ) + } + + static func domains(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .domain, + title: NSLocalizedString("Domains", comment: "Noun. Title. Links to the Domains screen."), + accessibilityIdentifier: "Domains Row", + image: UIImage(named: "site-menu-domains"), + action: { [weak viewController] _ in + viewController?.showDomains(from: .row) + } + ) + } + + static func viewSite(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .viewSite, + title: NSLocalizedString("View Site", comment: "Action title. Opens the user's site in an in-app browser"), + image: UIImage.gridicon(.globe), + showsSelectionState: false, + action: { [weak viewController] _ in + viewController?.showViewSite(from: .row) + } + ) + } + + static func admin(viewController: BlogDetailsViewController?, blog: Blog) -> Row { + let adminTitle: String + if blog.isHostedAtWPcom { + adminTitle = NSLocalizedString("Dashboard", comment: "Action title. Noun. Opens the user's WordPress.com dashboard in an external browser.") + } else { + adminTitle = NSLocalizedString("WP Admin", comment: "Action title. Noun. Opens the user's WordPress Admin in an external browser.") + } + + let iconSize = CGSize(width: 17.0, height: 17.0) + let accessoryImage = UIImage.gridicon(.external, size: iconSize).imageFlippedForRightToLeftLayoutDirection() + let accessoryView = UIImageView(image: accessoryImage) + accessoryView.tintColor = WPStyleGuide.cellGridiconAccessoryColor() + + return Row( + kind: .admin, + title: adminTitle, + image: UIImage.gridicon(.mySites), + accessoryView: accessoryView, + showsSelectionState: false, + action: { [weak viewController] _ in + viewController?.showViewAdmin() + viewController?.tableView?.deselectSelectedRowWithAnimation(true) + } + ) + } + + static func social(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .sharing, + title: BlogDetailsViewController.Strings.socialRowTitle, + image: UIImage(named: "site-menu-social"), + action: { [weak viewController] userInfo in + // When called from showDetailView, use .link as source (matching Objective-C behavior) + // When called from direct tap, use .row (default behavior) + let source: BlogDetailsNavigationSource = userInfo.isEmpty ? .row : .link + viewController?.showSharing(from: source) + } + ) + } + + static func siteMonitoring(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .siteMonitoring, + title: BlogDetailsViewController.Strings.siteMonitoringRowTitle, + accessibilityIdentifier: "Site Monitoring Row", + image: UIImage(named: "tool"), + action: { [weak viewController] userInfo in + let selectedTab = userInfo[BlogDetailsUserInfoKeys.siteMonitoringTab] as? NSNumber + viewController?.showSiteMonitoring(selectedTab: selectedTab) + } + ) + } + + static func applicationPasswords(viewController: BlogDetailsViewController?) -> Row { + Row( + kind: .applicationPasswords, + title: NSLocalizedString("Application Passwords", comment: "Link to Application Passwords section"), + accessibilityIdentifier: "Application Passwords Row", + image: UIImage(systemName: "key"), + action: { [weak viewController] _ in + viewController?.showApplicationPasswords() + } + ) + } +} + +private enum CellIdentifiers { + static let standard = "BlogDetailsCell" + static let plan = "BlogDetailsPlanCell" + static let settings = "BlogDetailsSettingsCell" + static let removeSite = "BlogDetailsRemoveSiteCell" + static let sectionFooter = "BlogDetailsSectionFooterView" + static let sectionFooterIdentifier = "BlogDetailsSectionFooterIdentifier" + static let migrationSuccess = "BlogDetailsMigrationSuccessCellIdentifier" + static let jetpackBrandingCard = "BlogDetailsJetpackBrandingCardCellIdentifier" + static let jetpackInstall = "BlogDetailsJetpackInstallCardCellIdentifier" + static let sotWCard = "BlogDetailsSotWCardCellIdentifier" +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Activity.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Activity.swift index fb843f03835c..7b7f7fcff7f0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Activity.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Activity.swift @@ -71,7 +71,7 @@ extension BlogDetailsViewController: SearchableActivityConvertable { return displayURL } - @objc public func createUserActivity() { + public func createUserActivity() { registerUserActivity() } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift index 6c58ba656488..7dc0df773b0a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift @@ -5,7 +5,7 @@ import Gravatar extension BlogDetailsViewController { - @objc public func downloadGravatarImage(for row: BlogDetailsRow, forceRefresh: Bool = false) { + public func downloadGravatarImage(forceRefresh: Bool = false) { guard let email = blog.account?.email else { return } @@ -16,21 +16,19 @@ extension BlogDetailsViewController { return } - row.image = gravatarIcon - self?.reloadMeRow() + self?.tableViewModel?.gravatarIcon = gravatarIcon } } - @objc public func observeGravatarImageUpdate() { + public func observeGravatarImageUpdate() { NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar(_:)), name: .GravatarQEAvatarUpdateNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateGravatarImage(_:)), name: .GravatarImageUpdateNotification, object: nil) } @objc private func refreshAvatar(_ notification: Foundation.Notification) { - guard let meRow, - let email = blog.account?.email, + guard let email = blog.account?.email, notification.userInfoHasEmail(email) else { return } - downloadGravatarImage(for: meRow, forceRefresh: true) + downloadGravatarImage(forceRefresh: true) } @objc private func updateGravatarImage(_ notification: Foundation.Notification) { @@ -43,13 +41,7 @@ extension BlogDetailsViewController { } ImageCache.shared.setImage(image, forKey: url.absoluteString) - meRow?.image = gravatarIcon - reloadMeRow() - } - - private func reloadMeRow() { - let meIndexPath = indexPath(for: .me) - tableView.reloadRows(at: [meIndexPath], with: .automatic) + tableViewModel?.gravatarIcon = gravatarIcon } private enum Metrics { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+SectionHelpers.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+SectionHelpers.swift index a93893cc5f56..ab22c308e11c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+SectionHelpers.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+SectionHelpers.swift @@ -6,115 +6,66 @@ import WordPressUI import WordPressAPI import WordPressCore -extension Array where Element: BlogDetailsSection { - fileprivate func findSectionIndex(of category: BlogDetailsSectionCategory) -> Int? { - return firstIndex(where: { $0.category == category }) - } -} - -extension BlogDetailsSubsection { - func sectionCategory(for blog: Blog) -> BlogDetailsSectionCategory { - switch self { - case .domainCredit: - return .domainCredit - case .activity, .jetpackSettings, .siteMonitoring: - return .jetpack - case .stats where blog.shouldShowJetpackSection: - return .jetpack - case .stats where !blog.shouldShowJetpackSection: - return .general - case .pages, .posts, .media, .comments: - return .content - case .themes, .customize: - return .personalize - case .me, .sharing, .people, .plugins: - return .configure - case .home: - return .home - default: - fatalError() - } - } -} - extension BlogDetailsViewController { - @objc public func findSectionIndex(sections: [BlogDetailsSection], category: BlogDetailsSectionCategory) -> Int { - return sections.findSectionIndex(of: category) ?? NSNotFound - } - - @objc public func sectionCategory(subsection: BlogDetailsSubsection, blog: Blog) -> BlogDetailsSectionCategory { - return subsection.sectionCategory(for: blog) - } - - @objc public func defaultSubsection() -> BlogDetailsSubsection { - if !JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() { - return .posts - } - if isDashboardEnabled() { - return .home - } - return .stats - } - - @objc public func shouldAddJetpackSection() -> Bool { + public func shouldAddJetpackSection() -> Bool { guard JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() else { return false } return blog.shouldShowJetpackSection } - @objc public func shouldAddGeneralSection() -> Bool { + public func shouldAddGeneralSection() -> Bool { guard JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() else { return false } return blog.shouldShowJetpackSection == false } - @objc public func shouldAddPersonalizeSection() -> Bool { + public func shouldAddPersonalizeSection() -> Bool { guard JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() else { return false } return blog.supports(.themeBrowsing) || blog.supports(.menus) } - @objc public func shouldAddMeRow() -> Bool { + public func shouldAddMeRow() -> Bool { JetpackFeaturesRemovalCoordinator.currentAppUIType == .simplified && !isSidebarModeEnabled } - @objc public func shouldAddSharingRow() -> Bool { + public func shouldAddSharingRow() -> Bool { guard JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() else { return false } return blog.supports(.sharing) } - @objc public func shouldAddPeopleRow() -> Bool { + public func shouldAddPeopleRow() -> Bool { guard JetpackFeaturesRemovalCoordinator.shouldShowJetpackFeatures() else { return false } return blog.supports(.people) } - @objc public func shouldAddUsersRow() -> Bool { + public func shouldAddUsersRow() -> Bool { // Only admin users can list users. - FeatureFlag.selfHostedSiteUserManagement.enabled && blog.isSelfHosted && blog.isAdmin + return FeatureFlag.selfHostedSiteUserManagement.enabled && blog.isSelfHosted && blog.isAdmin } - @objc public func shouldAddPluginsRow() -> Bool { + public func shouldAddPluginsRow() -> Bool { return blog.supports(.pluginManagement) } - @objc public func shouldAddDomainRegistrationRow() -> Bool { + public func shouldAddDomainRegistrationRow() -> Bool { return FeatureFlag.domainRegistration.enabled && blog.supports(.domains) } - @objc public func showUsers() { - guard let presentationDelegate, let userId = self.blog.userID?.intValue else { + public func showUsers() { + guard let presentationDelegate, let userId = blog.userID?.intValue else { return } let feature = NSLocalizedString("applicationPasswordRequired.feature.users", value: "User Management", comment: "Feature name for managing users in the app") - let rootView = ApplicationPasswordRequiredView(blog: self.blog, localizedFeatureName: feature, presentingViewController: self) { client in + let rootView = ApplicationPasswordRequiredView(blog: blog, localizedFeatureName: feature, presentingViewController: self) { client in let service = UserService(client: client) let applicationPasswordService = ApplicationPasswordService(api: client, currentUserId: userId) return UserListView(currentUserId: Int32(userId), userService: service, applicationTokenListDataProvider: applicationPasswordService) @@ -122,7 +73,7 @@ extension BlogDetailsViewController { presentationDelegate.presentBlogDetailsViewController(UIHostingController(rootView: rootView)) } - @objc public func showManagePluginsScreen() { + public func showManagePluginsScreen() { guard blog.supports(.pluginManagement), let site = JetpackSiteRef(blog: blog) else { return @@ -133,7 +84,7 @@ extension BlogDetailsViewController { let viewController: UIViewController if Feature.enabled(.pluginManagementOverhaul) { let feature = NSLocalizedString("applicationPasswordRequired.feature.plugins", value: "Plugin Management", comment: "Feature name for managing plugins in the app") - let rootView = ApplicationPasswordRequiredView(blog: self.blog, localizedFeatureName: feature, presentingViewController: self) { client in + let rootView = ApplicationPasswordRequiredView(blog: blog, localizedFeatureName: feature, presentingViewController: self) { client in let service = PluginService(client: client, wordpressCoreVersion: wordpressCoreVersion) InstalledPluginsListView(service: service) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Strings.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Strings.swift index a8e2b14d58fb..5eb38be2fd0c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Strings.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Strings.swift @@ -1,39 +1,36 @@ import Foundation -@objc(BlogDetailsViewControllerStrings) -public class objc_BlogDetailsViewController_Strings: NSObject { +extension BlogDetailsViewController { - @objc public class func contentSectionTitle() -> String { Strings.contentSectionTitle } - @objc public class func trafficSectionTitle() -> String { Strings.trafficSectionTitle } - @objc public class func maintenanceSectionTitle() -> String { Strings.maintenanceSectionTitle } - @objc public class func socialRowTitle() -> String { Strings.socialRowTitle } - @objc public class func siteMonitoringRowTitle() -> String { Strings.siteMonitoringRowTitle } -} + enum Strings { + static let contentSectionTitle = NSLocalizedString( + "my-site.menu.content.section.title", + value: "Content", + comment: "Section title for the content table section in the blog details screen" + ) + static let trafficSectionTitle = NSLocalizedString( + "my-site.menu.traffic.section.title", + value: "Traffic", + comment: "Section title for the traffic table section in the blog details screen" + ) + static let maintenanceSectionTitle = NSLocalizedString( + "my-site.menu.maintenance.section.title", + value: "Maintenance", + comment: "Section title for the maintenance table section in the blog details screen" + ) + static let socialRowTitle = NSLocalizedString( + "my-site.menu.social.row.title", + value: "Social", + comment: "Title for the social row in the blog details screen" + ) + static let siteMonitoringRowTitle = NSLocalizedString( + "my-site.menu.site-monitoring.row.title", + value: "Site Monitoring", + comment: "Title for the site monitoring row in the blog details screen" + ) + + static let users = NSLocalizedString("mySite.menu.users", value: "Users", comment: "Title for the menu item") + static let subscribers = NSLocalizedString("mySite.menu.subscribers", value: "Subscribers", comment: "Title for the menu item") + } -private enum Strings { - static let contentSectionTitle = NSLocalizedString( - "my-site.menu.content.section.title", - value: "Content", - comment: "Section title for the content table section in the blog details screen" - ) - static let trafficSectionTitle = NSLocalizedString( - "my-site.menu.traffic.section.title", - value: "Traffic", - comment: "Section title for the traffic table section in the blog details screen" - ) - static let maintenanceSectionTitle = NSLocalizedString( - "my-site.menu.maintenance.section.title", - value: "Maintenance", - comment: "Section title for the maintenance table section in the blog details screen" - ) - static let socialRowTitle = NSLocalizedString( - "my-site.menu.social.row.title", - value: "Social", - comment: "Title for the social row in the blog details screen" - ) - static let siteMonitoringRowTitle = NSLocalizedString( - "my-site.menu.site-monitoring.row.title", - value: "Site Monitoring", - comment: "Title for the site monitoring row in the blog details screen" - ) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift index 0e6222631b08..02fb439d33b3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift @@ -9,37 +9,15 @@ import WordPressCore // MARK: - BlogDetailsViewController (Misc) extension BlogDetailsViewController { - @objc public var shouldShowSubscribersRow: Bool { + public var shouldShowSubscribersRow: Bool { blog.supports(.people) } - @objc public func makeSubscribersRow() -> BlogDetailsRow { - BlogDetailsRow(title: Strings.subscribers, image: UIImage(named: "wpl-mail") ?? UIImage()) { [weak self] in - guard let self else { return } - guard let blog = SubscribersBlog(blog: self.blog) else { - return wpAssertionFailure("incompatible blog") - } - let vc = SubscribersViewController(blog: blog) - self.presentationDelegate?.presentBlogDetailsViewController(vc) - } - } - - @objc public func makePeopleRow() -> BlogDetailsRow { - let row = BlogDetailsRow( - title: shouldShowSubscribersRow ? Strings.users : NSLocalizedString("People", comment: "Noun. Title. Links to the people management feature."), - image: UIImage(named: "site-menu-people") ?? UIImage() - ) { [weak self] in - self?.showPeople() - } - row.accessibilityIdentifier = "Users Row" - return row - } - - @objc public func isDashboardEnabled() -> Bool { - return JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() && blog.isAccessibleThroughWPCom() + public func isDashboardEnabled() -> Bool { + JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() && blog.isAccessibleThroughWPCom() } - @objc public func confirmRemoveSite() { + public func confirmRemoveSite() { let blogService = BlogService(coreDataStack: ContextManager.shared) blogService.remove(blog) @@ -57,19 +35,32 @@ extension BlogDetailsViewController { navigationController?.popToRootViewController(animated: true) } - @objc public func shouldShowJetpackInstallCard() -> Bool { + public func shouldShowJetpackInstallCard() -> Bool { !WPDeviceIdentification.isiPad() && JetpackInstallPluginHelper.shouldShowCard(for: blog) } - @objc public func shouldShowBlaze() -> Bool { - BlazeHelper.isBlazeFlagEnabled() && self.blog.supports(.blaze) + public func shouldShowBlaze() -> Bool { + BlazeHelper.isBlazeFlagEnabled() && blog.supports(.blaze) } } // MARK: - BlogDetailsViewController (Navigation) extension BlogDetailsViewController { - @objc public func showDashboard() { + func showDetailView(for row: BlogDetailsRowKind, userInfo: [String: Any] = [:]) { + self.tableViewModel?.showDetailView(for: row, userInfo: userInfo) + } + + func showDetailViewForMe(userInfo: [String: Any]) -> MeViewController { + guard let tableViewModel else { + wpAssertionFailure("tableViewModel can not be nil") + return MeViewController() + } + + return tableViewModel.showDetailViewForMe(userInfo: userInfo) + } + + public func showDashboard() { if isSidebarModeEnabled { let controller = MySiteViewController.make(forBlog: blog, isSidebarModeEnabled: true) presentationDelegate?.presentBlogDetailsViewController(controller) @@ -81,7 +72,6 @@ extension BlogDetailsViewController { } } - @objc(showPostListFromSource:) public func showPostList(from source: BlogDetailsNavigationSource) { trackEvent(.openedPosts, from: source) let controller = PostListViewController.controllerWithBlog(blog) @@ -89,7 +79,6 @@ extension BlogDetailsViewController { presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc(showPageListFromSource:) public func showPageList(from source: BlogDetailsNavigationSource) { trackEvent(.openedPages, from: source) let controller = PageListViewController.controllerWithBlog(blog) @@ -97,19 +86,16 @@ extension BlogDetailsViewController { presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc(showMediaLibraryFromSource:) public func showMediaLibrary(from source: BlogDetailsNavigationSource) { showMediaLibrary(from: source, showPicker: false) } - @objc(showMediaLibraryFromSource:showPicker:) public func showMediaLibrary(from source: BlogDetailsNavigationSource, showPicker: Bool) { trackEvent(.openedMediaLibrary, from: source) let controller = SiteMediaViewController(blog: blog, showPicker: showPicker) presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc(showSettingsFromSource:) public func showSettings(from source: BlogDetailsNavigationSource) { trackEvent(.openedSiteSettings, from: source) @@ -124,7 +110,7 @@ extension BlogDetailsViewController { settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem( systemItem: .done, primaryAction: UIAction { [weak self] _ in - self?.tableView.deselectSelectedRowWithAnimation(true) + self?.tableView?.deselectSelectedRowWithAnimation(true) self?.dismiss(animated: true, completion: nil) } ) @@ -137,21 +123,21 @@ extension BlogDetailsViewController { } } - @objc @discardableResult + @discardableResult public func showMe() -> MeViewController { let controller = MeViewController() presentationDelegate?.presentBlogDetailsViewController(controller) return controller } - @objc public func showPeople() { + public func showPeople() { guard let controller = PeopleViewController.withJPBannerForBlog(blog) else { return wpAssertionFailure("failed to instantiate") } presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc public func showActivity() { + public func showActivity() { let controller = ActivityLogsViewController(blog: blog) controller.navigationItem.largeTitleDisplayMode = .never presentationDelegate?.presentBlogDetailsViewController(controller) @@ -159,7 +145,7 @@ extension BlogDetailsViewController { WPAnalytics.track(.activityLogViewed, withProperties: [WPAppAnalyticsKeyTapSource: "site_menu"]) } - @objc public func showBlaze() { + public func showBlaze() { BlazeEventsTracker.trackEntryPointTapped(for: .menuItem) if RemoteFeature.enabled(.blazeManageCampaigns) { @@ -170,12 +156,12 @@ extension BlogDetailsViewController { } } - @objc public func showScan() { + public func showScan() { let scanVC = JetpackScanViewController.withJPBannerForBlog(blog) presentationDelegate?.presentBlogDetailsViewController(scanVC) } - @objc public func showBackup() { + public func showBackup() { let controller = BackupsViewController(blog: blog) controller.navigationItem.largeTitleDisplayMode = .never @@ -184,7 +170,7 @@ extension BlogDetailsViewController { WPAnalytics.track(.backupListOpened) } - @objc public func showThemes() { + public func showThemes() { WPAppAnalytics.track(.themesAccessedThemeBrowser, blog: blog) let themesVC = ThemeBrowserViewController.browserWithBlog(blog) themesVC.hidesBottomBarWhenPushed = true @@ -192,13 +178,12 @@ extension BlogDetailsViewController { presentationDelegate?.presentBlogDetailsViewController(jpWrappedViewController) } - @objc public func showMenus() { + public func showMenus() { WPAppAnalytics.track(.menusAccessed, blog: blog) let menusVC = MenusViewController.withJPBannerForBlog(blog) presentationDelegate?.presentBlogDetailsViewController(menusVC) } - @objc(showCommentsFromSource:) public func showComments(from source: BlogDetailsNavigationSource) { trackEvent(.openedComments, from: source) @@ -231,7 +216,7 @@ extension BlogDetailsViewController { } } - @objc public func showPlugins() { + public func showPlugins() { WPAppAnalytics.track(.openedPluginDirectory, blog: blog) if Feature.enabled(.pluginManagementOverhaul) { @@ -247,7 +232,6 @@ extension BlogDetailsViewController { presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc(showStatsFromSource:) public func showStats(from source: BlogDetailsNavigationSource) { trackEvent(.statsAccessed, from: source) @@ -272,7 +256,6 @@ extension BlogDetailsViewController { return StatsHostingViewController.makeStatsViewController(for: blog) } - @objc(showDomainsFromSource:) public func showDomains(from source: BlogDetailsNavigationSource) { guard let presentationDelegate else { return wpAssertionFailure("presentationDelegate mising") @@ -280,13 +263,12 @@ extension BlogDetailsViewController { DomainsDashboardCoordinator.presentDomainsDashboard(with: presentationDelegate, source: source.string, blog: blog) } - @objc public func showJetpackSettings() { + public func showJetpackSettings() { let controller = JetpackSettingsViewController(blog: blog) controller.navigationItem.largeTitleDisplayMode = .never presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc(showSharingFromSource:) public func showSharing(from source: BlogDetailsNavigationSource) { let sharingVC: UIViewController @@ -302,7 +284,6 @@ extension BlogDetailsViewController { presentationDelegate?.presentBlogDetailsViewController(sharingVC) } - @objc(showViewSiteFromSource:) public func showViewSite(from source: BlogDetailsNavigationSource) { trackEvent(.openedViewSite, from: source) @@ -325,7 +306,7 @@ extension BlogDetailsViewController { present(navigationController, animated: true, completion: nil) } - @objc public func showViewAdmin() { + public func showViewAdmin() { WPAppAnalytics.track(.openedViewAdmin, blog: blog) let dashboardPath: String @@ -339,17 +320,17 @@ extension BlogDetailsViewController { UIApplication.shared.open(url, options: [:], completionHandler: nil) } - @objc public func showSiteMonitoring() { + public func showSiteMonitoring() { showSiteMonitoring(selectedTab: nil) } - @objc public func showSiteMonitoring(selectedTab: NSNumber?) { + public func showSiteMonitoring(selectedTab: NSNumber?) { let selectedTab = selectedTab.flatMap { SiteMonitoringTab(rawValue: $0.intValue) } let controller = SiteMonitoringViewController(blog: blog, selectedTab: selectedTab) presentationDelegate?.presentBlogDetailsViewController(controller) } - @objc public func showApplicationPasswords() { + public func showApplicationPasswords() { let feature = NSLocalizedString("applicationPasswordRequired.feature.applicationPasswords", value: "Application Passwords Management", comment: "Feature name for managing application passwords in the app") let view = ApplicationPasswordRequiredView(blog: blog, localizedFeatureName: feature, presentingViewController: self) { ApplicationTokenListView(dataProvider: ApplicationPasswordService(api: $0)) @@ -361,7 +342,7 @@ extension BlogDetailsViewController { // MARK: - BlogDetailsViewController (Tracking) extension BlogDetailsViewController { - @objc public func trackEvent(_ event: WPAnalyticsStat, from source: BlogDetailsNavigationSource) { + public func trackEvent(_ event: WPAnalyticsStat, from source: BlogDetailsNavigationSource) { var properties: [String: Any] = [ WPAppAnalyticsKeyTapSource: source.string, WPAppAnalyticsKeyTabSource: "site_menu" @@ -373,7 +354,7 @@ extension BlogDetailsViewController { } } -@objc public enum BlogDetailsNavigationSource: Int { +public enum BlogDetailsNavigationSource: Int { case button = 0 case row = 1 case link = 2 @@ -402,13 +383,8 @@ private enum Constants { static let calypsoDashboardPath = "https://wordpress.com/home/" } -private enum Strings { - static let users = NSLocalizedString("mySite.menu.users", value: "Users", comment: "Title for the menu item") - static let subscribers = NSLocalizedString("mySite.menu.subscribers", value: "Subscribers", comment: "Title for the menu item") -} - // Necessary data that's required to get an application application from a given site. -@objc public class ApplicationPasswordAuthenticationInfo: NSObject { +public class ApplicationPasswordAuthenticationInfo: NSObject { public let siteAddress: String public let siteDetails: AutoDiscoveryAttemptSuccess public let siteUsername: String diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h deleted file mode 100644 index e6deeee201e5..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.h +++ /dev/null @@ -1,151 +0,0 @@ -#import - -@class Blog; -@class BlogDetailHeaderView; -@class IntrinsicTableView; -@class MeViewController; -@class ApplicationPasswordAuthenticationInfo; -@protocol BlogDetailHeader; - -typedef NS_ENUM(NSUInteger, BlogDetailsSectionCategory) { - BlogDetailsSectionCategoryReminders, - BlogDetailsSectionCategoryDomainCredit, - BlogDetailsSectionCategoryHome, - BlogDetailsSectionCategoryGeneral, - BlogDetailsSectionCategoryJetpack, - BlogDetailsSectionCategoryPersonalize, - BlogDetailsSectionCategoryConfigure, - BlogDetailsSectionCategoryExternal, - BlogDetailsSectionCategoryRemoveSite, - BlogDetailsSectionCategoryMigrationSuccess, - BlogDetailsSectionCategoryJetpackBrandingCard, - BlogDetailsSectionCategoryJetpackInstallCard, - BlogDetailsSectionCategorySotW2023Card, - BlogDetailsSectionCategoryContent, - BlogDetailsSectionCategoryTraffic, - BlogDetailsSectionCategoryMaintenance, -}; - -typedef NS_ENUM(NSUInteger, BlogDetailsSubsection) { - BlogDetailsSubsectionReminders, - BlogDetailsSubsectionDomainCredit, - BlogDetailsSubsectionStats, - BlogDetailsSubsectionPosts, - BlogDetailsSubsectionCustomize, - BlogDetailsSubsectionThemes, - BlogDetailsSubsectionMedia, - BlogDetailsSubsectionPages, - BlogDetailsSubsectionActivity, - BlogDetailsSubsectionJetpackSettings, - BlogDetailsSubsectionMe, - BlogDetailsSubsectionComments, - BlogDetailsSubsectionSharing, - BlogDetailsSubsectionPeople, - BlogDetailsSubsectionPlugins, - BlogDetailsSubsectionHome, - BlogDetailsSubsectionMigrationSuccess, - BlogDetailsSubsectionJetpackBrandingCard, - BlogDetailsSubsectionBlaze, - BlogDetailsSubsectionSiteMonitoring -}; - -@interface BlogDetailsSection : NSObject - -@property (nonatomic, strong, nullable, readonly) NSString *title; -@property (nonatomic, strong, nonnull, readonly) NSArray *rows; -@property (nonatomic, strong, nullable, readonly) NSString *footerTitle; -@property (nonatomic, readonly) BlogDetailsSectionCategory category; - -- (instancetype _Nonnull)initWithTitle:(NSString * __nullable)title andRows:(NSArray * __nonnull)rows category:(BlogDetailsSectionCategory)category; -- (instancetype _Nonnull)initWithTitle:(NSString * __nullable)title rows:(NSArray * __nonnull)rows footerTitle:(NSString * __nullable)footerTitle category:(BlogDetailsSectionCategory)category; - -@end - - -@interface BlogDetailsRow : NSObject - -@property (nonatomic, strong, nonnull) NSString *title; -@property (nonatomic, strong, nonnull) NSString *identifier; -@property (nonatomic, strong, nullable) NSString *accessibilityIdentifier; -@property (nonatomic, strong, nullable) NSString *accessibilityHint; -@property (nonatomic, strong, nonnull) UIImage *image; -@property (nonatomic, strong, nullable) UIColor *imageColor; -@property (nonatomic, strong, nullable) UIView *accessoryView; -@property (nonatomic, strong, nullable) NSString *detail; -@property (nonatomic) BOOL showsSelectionState; -@property (nonatomic) BOOL forDestructiveAction; -@property (nonatomic) BOOL showsDisclosureIndicator; -@property (nonatomic, copy, nullable) void (^callback)(void); - -- (instancetype _Nonnull)initWithTitle:(NSString * __nonnull)title - image:(UIImage * __nonnull)image - callback:(void(^_Nullable)(void))callback; - -- (instancetype _Nonnull)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - image:(UIImage * __nonnull)image - callback:(void(^_Nullable)(void))callback; - -- (instancetype _Nonnull)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - accessibilityHint:(NSString *__nullable)accessibilityHint - image:(UIImage * __nonnull)image - callback:(void(^_Nullable)(void))callback; - -- (instancetype _Nonnull)initWithTitle:(NSString * __nonnull)title - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - image:(UIImage * __nonnull)image - imageColor:(UIColor * __nullable)imageColor - callback:(void(^_Nullable)(void))callback; - -- (instancetype _Nonnull)initWithTitle:(NSString * __nonnull)title - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - image:(UIImage * __nonnull)image - imageColor:(UIColor * __nullable)imageColor - renderingMode:(UIImageRenderingMode)renderingMode - callback:(void(^_Nullable)(void))callback; - -@end - -@protocol ScenePresenter; - -@protocol BlogDetailsPresentationDelegate -- (void)presentBlogDetailsViewController:(UIViewController * __nonnull)viewController; -@end - -@interface BlogDetailsViewController : UIViewController - -@property (nonatomic, strong, nonnull) Blog * blog; -@property (nonatomic, strong, readwrite) UITableView * _Nonnull tableView; -@property (nonatomic) BOOL isScrollEnabled; -@property (nonatomic, weak, nullable) id presentationDelegate; -@property (nonatomic, strong, nullable) BlogDetailsRow *meRow; - -/// A new display mode for the displaying it as part of the site menu. -@property (nonatomic) BOOL isSidebarModeEnabled; - -@property (nonatomic, weak) UIViewController *presentedSiteSettingsViewController; - -- (id _Nonnull)init; -- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section; -- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section userInfo:(nonnull NSDictionary *)userInfo; -- (NSIndexPath * _Nonnull)indexPathForSubsection:(BlogDetailsSubsection)subsection; -- (void)reloadTableViewPreservingSelection; -- (void)configureTableViewData; - -- (nonnull MeViewController *)showDetailViewForMeSubsectionWithUserInfo:(nonnull NSDictionary *)userInfo; - -- (void)switchToBlog:(nonnull Blog *)blog; -- (void)showInitialDetailsForBlog; -- (void)updateTableView:(nullable void(^)(void))completion; -- (void)preloadMetadata; -- (void)pulledToRefreshWith:(nonnull UIRefreshControl *)refreshControl onCompletion:(nullable void(^)(void))completion; - -+ (nonnull NSString *)userInfoShowPickerKey; -+ (nonnull NSString *)userInfoSiteMonitoringTabKey; -+ (nonnull NSString *)userInfoShowManagemenetScreenKey; -+ (nonnull NSString *)userInfoSourceKey; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m deleted file mode 100644 index ef6d372204f4..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ /dev/null @@ -1,1803 +0,0 @@ -#import "BlogDetailsViewController.h" - -#import "AccountService.h" -#import "BlogService.h" -#import "CommentsViewController.h" -#import "SiteSettingsViewController.h" -#import "SharingViewController.h" -#import "StatsViewController.h" -#import "WPAppAnalytics.h" -#import "WordPress-Swift.h" -#import "MenusViewController.h" - -@import Gridicons; -@import Reachability; -@import WordPressData; -@import WordPressShared; - -static NSString *const BlogDetailsCellIdentifier = @"BlogDetailsCell"; -static NSString *const BlogDetailsPlanCellIdentifier = @"BlogDetailsPlanCell"; -static NSString *const BlogDetailsSettingsCellIdentifier = @"BlogDetailsSettingsCell"; -static NSString *const BlogDetailsRemoveSiteCellIdentifier = @"BlogDetailsRemoveSiteCell"; -static NSString *const BlogDetailsSectionFooterIdentifier = @"BlogDetailsSectionFooterView"; -static NSString *const BlogDetailsMigrationSuccessCellIdentifier = @"BlogDetailsMigrationSuccessCell"; -static NSString *const BlogDetailsJetpackBrandingCardCellIdentifier = @"BlogDetailsJetpackBrandingCardCellIdentifier"; -static NSString *const BlogDetailsJetpackInstallCardCellIdentifier = @"BlogDetailsJetpackInstallCardCellIdentifier"; -static NSString *const BlogDetailsSotWCardCellIdentifier = @"BlogDetailsSotWCardCellIdentifier"; - -CGFloat const BlogDetailGridiconSize = 24.0; -CGFloat const BlogDetailGridiconAccessorySize = 17.0; -CGFloat const BlogDetailSectionTitleHeaderHeight = 40.0; -CGFloat const BlogDetailSectionsSpacing = 20.0; -CGFloat const BlogDetailSectionFooterHeight = 40.0; -NSTimeInterval const PreloadingCacheTimeout = 60.0 * 5; // 5 minutes -NSString * const HideWPAdminDate = @"2015-09-07T00:00:00Z"; - -CGFloat const BlogDetailReminderSectionHeaderHeight = 8.0; -CGFloat const BlogDetailReminderSectionFooterHeight = 1.0; - -#pragma mark - Helper Classes for Blog Details view model. - -@implementation NSMutableArray (NullableObjects) - -- (void)addNullableObject:(nullable id)anObject { - if (anObject != nil) { - [self addObject:anObject]; - } -} - -@end - -@implementation BlogDetailsRow - -- (instancetype)initWithTitle:(NSString * __nonnull)title - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - image:image - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:identifier - accessibilityIdentifier:nil - image:image - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - accessibilityIdentifier:accessibilityIdentifier - image:image - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:identifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:nil - image:image - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - accessibilityHint:(NSString *__nullable)accessibilityHint - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:identifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:accessibilityHint - image:image - imageColor:[UIColor labelColor] - renderingMode:UIImageRenderingModeAlwaysTemplate - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - accessibilityHint:(NSString *__nullable)accessibilityHint - image:(UIImage * __nonnull)image - callback:(void(^)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:accessibilityHint - image:image - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - image:(UIImage *)image - imageColor:(UIColor *)imageColor - callback:(void (^)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:nil - image:image - imageColor:imageColor - renderingMode:UIImageRenderingModeAlwaysTemplate - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString *)title - accessibilityIdentifier:(NSString *)accessibilityIdentifier - image:(UIImage *)image - imageColor:(UIColor *)imageColor - renderingMode:(UIImageRenderingMode)renderingMode - callback:(void (^)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:nil - image:image - imageColor:imageColor - renderingMode:renderingMode - callback:callback]; -} - - -- (instancetype)initWithTitle:(NSString * __nonnull)title - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - accessibilityHint:(NSString * __nullable)accessibilityHint - image:(UIImage * __nonnull)image - imageColor:(UIColor * __nullable)imageColor - callback:(void(^_Nullable)(void))callback -{ - return [self initWithTitle:title - identifier:BlogDetailsCellIdentifier - accessibilityIdentifier:accessibilityIdentifier - accessibilityHint:nil - image:image - imageColor:imageColor - renderingMode:UIImageRenderingModeAlwaysTemplate - callback:callback]; -} - -- (instancetype)initWithTitle:(NSString * __nonnull)title - identifier:(NSString * __nonnull)identifier - accessibilityIdentifier:(NSString *__nullable)accessibilityIdentifier - accessibilityHint:(NSString *__nullable)accessibilityHint - image:(UIImage * __nonnull)image - imageColor:(UIColor * __nullable)imageColor - renderingMode:(UIImageRenderingMode)renderingMode - callback:(void(^)(void))callback -{ - self = [super init]; - if (self) { - _title = title; - _image = [image imageWithRenderingMode:renderingMode]; - _imageColor = imageColor; - _callback = callback; - _identifier = identifier; - _accessibilityIdentifier = accessibilityIdentifier; - _accessibilityHint = accessibilityHint; - _showsSelectionState = YES; - _showsDisclosureIndicator = YES; - } - return self; -} - -@end - -@implementation BlogDetailsSection -- (instancetype)initWithTitle:(NSString *)title - andRows:(NSArray *)rows - category:(BlogDetailsSectionCategory)category -{ - return [self initWithTitle:title rows:rows footerTitle:nil category:category]; -} - -- (instancetype)initWithTitle:(NSString *)title - rows:(NSArray *)rows - footerTitle:(NSString *)footerTitle - category:(BlogDetailsSectionCategory)category -{ - self = [super init]; - if (self) { - _title = title; - _rows = rows; - _footerTitle = footerTitle; - _category = category; - } - return self; -} -@end - -#pragma mark - - -@interface BlogDetailsViewController () - -@property (nonatomic, strong) NSArray *headerViewHorizontalConstraints; -@property (nonatomic, strong) NSArray *tableSections; -@property (nonatomic, strong) BlogService *blogService; - -/// Used to restore the tableview selection during state restoration, and -/// also when switching between a collapsed and expanded split view controller presentation -@property (nonatomic, strong) NSIndexPath *restorableSelectedIndexPath; -@property (nonatomic) BlogDetailsSectionCategory selectedSectionCategory; - -@property (nonatomic) BOOL hasLoggedDomainCreditPromptShownEvent; - -@end - -@implementation BlogDetailsViewController -@synthesize restorableSelectedIndexPath = _restorableSelectedIndexPath; - -#pragma mark = Lifecycle Methods - -- (instancetype)init -{ - self = [super init]; - - if (self) { - self.isScrollEnabled = false; - } - - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - if (self.isSidebarModeEnabled) { - _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; - } else if (self.isScrollEnabled) { - _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped]; - } else { - _tableView = [[IntrinsicTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped]; - self.tableView.scrollEnabled = false; - } - self.tableView.delegate = self; - self.tableView.dataSource = self; - self.tableView.translatesAutoresizingMaskIntoConstraints = false; - if (self.isSidebarModeEnabled) { - self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; - self.additionalSafeAreaInsets = UIEdgeInsetsMake(0, 8, 0, 0); // Left inset - } - [self.view addSubview:self.tableView]; - [self.view pinSubviewToAllEdges:self.tableView]; - - UIRefreshControl *refreshControl = [UIRefreshControl new]; - [refreshControl addTarget:self action:@selector(pulledToRefresh) forControlEvents:UIControlEventValueChanged]; - self.tableView.refreshControl = refreshControl; - - self.tableView.accessibilityIdentifier = @"Blog Details Table"; - - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - [WPStyleGuide configureAutomaticHeightRowsFor:self.tableView]; - - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:BlogDetailsCellIdentifier]; - [self.tableView registerClass:[WPTableViewCellValue1 class] forCellReuseIdentifier:BlogDetailsPlanCellIdentifier]; - [self.tableView registerClass:[WPTableViewCellValue1 class] forCellReuseIdentifier:BlogDetailsSettingsCellIdentifier]; - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:BlogDetailsRemoveSiteCellIdentifier]; - [self.tableView registerClass:[BlogDetailsSectionFooterView class] forHeaderFooterViewReuseIdentifier:BlogDetailsSectionFooterIdentifier]; - [self.tableView registerClass:[MigrationSuccessCell class] forCellReuseIdentifier:BlogDetailsMigrationSuccessCellIdentifier]; - [self.tableView registerClass:[JetpackBrandingMenuCardCell class] forCellReuseIdentifier:BlogDetailsJetpackBrandingCardCellIdentifier]; - [self.tableView registerClass:[JetpackRemoteInstallTableViewCell class] forCellReuseIdentifier:BlogDetailsJetpackInstallCardCellIdentifier]; - [self.tableView registerClass:[SotWTableViewCell class] forCellReuseIdentifier:BlogDetailsSotWCardCellIdentifier]; - - self.tableView.cellLayoutMarginsFollowReadableWidth = YES; - - self.hasLoggedDomainCreditPromptShownEvent = NO; - - self.blogService = [[BlogService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - [self preloadMetadata]; - - if (self.blog.account && !self.blog.account.userID) { - // User's who upgrade may not have a userID recorded. - AccountService *acctService = [[AccountService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - [acctService updateUserDetailsForAccount:self.blog.account success:nil failure:nil]; - } - - [self observeManagedObjectContextObjectsDidChangeNotification]; - - [self observeGravatarImageUpdate]; - - [self registerForTraitChanges:@[[UITraitHorizontalSizeClass self]] withAction:@selector(handleTraitChanges)]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self observeWillEnterForegroundNotification]; - - if (!self.isSplitViewDisplayed) { - self.restorableSelectedIndexPath = nil; - } - - // Configure and reload table data when appearing to ensure pending comment count is updated - [self configureTableViewData]; - - [self reloadTableViewPreservingSelection]; - [self preloadBlogData]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - [self createUserActivity]; - - [WPAnalytics trackEvent: WPAnalyticsEventMySiteSiteMenuShown]; - - if ([self shouldShowJetpackInstallCard]) { - [WPAnalytics trackEvent:WPAnalyticsEventJetpackInstallFullPluginCardViewed - properties:@{WPAppAnalyticsKeyTabSource: @"site_menu"}]; - } - - if ([self shouldShowBlaze]) { - [ObjCBridge trackBlazeEntryPointDisplayedWithSource:BlazeSourceMenuItem]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [self stopObservingWillEnterForegroundNotification]; -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; -} - -- (void)handleTraitChanges -{ - // Required to add / remove "Home" section when switching between regular and compact width - [self configureTableViewData]; - - // Required to update disclosure indicators depending on split view status - [self reloadTableViewPreservingSelection]; -} - -- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section -{ - [self showDetailViewForSubsection:section userInfo:@{}]; -} - -- (void)showDetailViewForSubsection:(BlogDetailsSubsection)section userInfo:(NSDictionary *)userInfo -{ - NSIndexPath *indexPath = [self indexPathForSubsection:section]; - - switch (section) { - case BlogDetailsSubsectionReminders: - case BlogDetailsSubsectionDomainCredit: - case BlogDetailsSubsectionHome: - case BlogDetailsSubsectionMigrationSuccess: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showDashboard]; - break; - case BlogDetailsSubsectionJetpackBrandingCard: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - break; - case BlogDetailsSubsectionStats: { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - NSNumber *sourceValue = userInfo[[BlogDetailsViewController userInfoSourceKey]]; - BlogDetailsNavigationSource source = sourceValue ? sourceValue.unsignedIntegerValue : BlogDetailsNavigationSourceLink; - [self showStatsFromSource:source]; - break; - } - case BlogDetailsSubsectionPosts: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showPostListFromSource:BlogDetailsNavigationSourceLink]; - break; - case BlogDetailsSubsectionThemes: - case BlogDetailsSubsectionCustomize: - if ([self.blog supports:BlogFeatureThemeBrowsing] || [self.blog supports:BlogFeatureMenus]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showThemes]; - } - break; - case BlogDetailsSubsectionMedia: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - BOOL showPicker = userInfo[[BlogDetailsViewController userInfoShowPickerKey]] ?: NO; - [self showMediaLibraryFromSource:BlogDetailsNavigationSourceLink showPicker: showPicker]; - break; - case BlogDetailsSubsectionPages: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showPageListFromSource:BlogDetailsNavigationSourceLink]; - break; - case BlogDetailsSubsectionActivity: - if ([self.blog supports:BlogFeatureActivity]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showActivity]; - } - break; - case BlogDetailsSubsectionBlaze: - if ([self shouldShowBlaze]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showBlaze]; - } - break; - case BlogDetailsSubsectionJetpackSettings: - if ([self.blog supports:BlogFeatureActivity]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showJetpackSettings]; - } - break; - case BlogDetailsSubsectionComments: - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showCommentsFromSource:BlogDetailsNavigationSourceLink]; - break; - case BlogDetailsSubsectionMe: - [self showDetailViewForMeSubsectionWithUserInfo: userInfo]; - break; - case BlogDetailsSubsectionSharing: - if ([self.blog supports:BlogFeatureSharing]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showSharingFromSource:BlogDetailsNavigationSourceLink]; - } - break; - case BlogDetailsSubsectionPeople: - if ([self.blog supports:BlogFeaturePeople]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showPeople]; - } else if ([self.blog selfHostedSiteRestApi]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showUsers]; - } - break; - case BlogDetailsSubsectionPlugins: - if ([self.blog supports:BlogFeaturePluginManagement]) { - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - BOOL showManagemnet = userInfo[[BlogDetailsViewController userInfoShowManagemenetScreenKey]] ?: NO; - if (showManagemnet) { - [self showManagePluginsScreen]; - } else { - [self showPlugins]; - } - } - break; - case BlogDetailsSubsectionSiteMonitoring: - if ([RemoteFeature enabled:RemoteFeatureFlagSiteMonitoring] && [self.blog supports:BlogFeatureSiteMonitoring]) { - NSNumber *selectedTab = userInfo[[BlogDetailsViewController userInfoSiteMonitoringTabKey]]; - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - [self showSiteMonitoringWithSelectedTab:selectedTab]; - } - break; - - } -} - -- (MeViewController *)showDetailViewForMeSubsectionWithUserInfo:(NSDictionary *)userInfo { - NSIndexPath *indexPath = [self indexPathForSubsection:BlogDetailsSubsectionMe]; - self.restorableSelectedIndexPath = indexPath; - [self.tableView selectRowAtIndexPath:indexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:indexPath]]; - return [self showMe]; -} - -// MARK: Todo: this needs to adjust based on the existence of the QSv2 section -- (NSIndexPath *)indexPathForSubsection:(BlogDetailsSubsection)subsection -{ - BlogDetailsSectionCategory sectionCategory = [self sectionCategoryWithSubsection:subsection blog: self.blog]; - NSInteger section = [self findSectionIndexWithSections:self.tableSections category:sectionCategory]; - switch (subsection) { - case BlogDetailsSubsectionReminders: - case BlogDetailsSubsectionHome: - case BlogDetailsSubsectionMigrationSuccess: - case BlogDetailsSubsectionJetpackBrandingCard: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionDomainCredit: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionStats: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionActivity: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionSiteMonitoring: - return [NSIndexPath indexPathForRow:2 inSection:section]; - case BlogDetailsSubsectionBlaze: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionJetpackSettings: - return [NSIndexPath indexPathForRow:1 inSection:section]; - case BlogDetailsSubsectionPosts: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionThemes: - case BlogDetailsSubsectionCustomize: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionMedia: - return [NSIndexPath indexPathForRow:2 inSection:section]; - case BlogDetailsSubsectionPages: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionComments: - return [NSIndexPath indexPathForRow:3 inSection:section]; - case BlogDetailsSubsectionMe: - case BlogDetailsSubsectionSharing: - return [NSIndexPath indexPathForRow:0 inSection:section]; - case BlogDetailsSubsectionPeople: - return [NSIndexPath indexPathForRow:1 inSection:section]; - case BlogDetailsSubsectionPlugins: - return [NSIndexPath indexPathForRow:2 inSection:section]; - - } -} - -#pragma mark - Properties - -- (NSIndexPath *)restorableSelectedIndexPath -{ - if (!_restorableSelectedIndexPath) { - // If nil, default to stats subsection. - BlogDetailsSubsection subsection = [self defaultSubsection]; - self.selectedSectionCategory = [self sectionCategoryWithSubsection:subsection blog: self.blog]; - NSUInteger section = [self findSectionIndexWithSections:self.tableSections category:self.selectedSectionCategory]; - _restorableSelectedIndexPath = [NSIndexPath indexPathForRow:0 inSection:section]; - } - - return _restorableSelectedIndexPath; -} - -- (void)setRestorableSelectedIndexPath:(NSIndexPath *)restorableSelectedIndexPath -{ - if (restorableSelectedIndexPath != nil && restorableSelectedIndexPath.section < [self.tableSections count]) { - BlogDetailsSection *section = [self.tableSections objectAtIndex:restorableSelectedIndexPath.section]; - switch (section.category) { - case BlogDetailsSectionCategoryJetpackBrandingCard: - case BlogDetailsSectionCategoryDomainCredit: { - _restorableSelectedIndexPath = nil; - } - break; - default: { - self.selectedSectionCategory = section.category; - _restorableSelectedIndexPath = restorableSelectedIndexPath; - } - break; - } - return; - } - - _restorableSelectedIndexPath = nil; -} - -#pragma mark - iOS 10 bottom padding - -- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)sectionNum { - BlogDetailsSection *section = self.tableSections[sectionNum]; - BOOL isLastSection = sectionNum == self.tableSections.count - 1; - BOOL hasTitle = section.footerTitle != nil && ![section.footerTitle isEmpty]; - if (hasTitle) { - return UITableViewAutomaticDimension; - } - if (isLastSection) { - return BlogDetailSectionFooterHeight; - } - return 0; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionNum { - BlogDetailsSection *section = self.tableSections[sectionNum]; - BOOL hasTitle = section.title != nil && ![section.title isEmpty]; - - if (hasTitle) { - return BlogDetailSectionTitleHeaderHeight; - } - return BlogDetailSectionsSpacing; -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { - BlogDetailsSection *detailSection = self.tableSections[section]; - NSString *footerTitle = detailSection.footerTitle; - if (footerTitle != nil) { - BlogDetailsSectionFooterView *footerView = (BlogDetailsSectionFooterView *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:BlogDetailsSectionFooterIdentifier]; - // If the next section has title, gives extra spacing between two sections. - BOOL shouldShowExtraSpacing = (self.tableSections.count > section + 1) ? (self.tableSections[section + 1].title != nil): NO; - [footerView updateUIWithTitle:footerTitle shouldShowExtraSpacing:shouldShowExtraSpacing]; - return footerView; - } - - return nil; -} - -#pragma mark - Rows - -- (BlogDetailsRow *)postsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Posts", @"Noun. Title. Links to the blog's Posts screen.") - accessibilityIdentifier:@"Blog Post Row" - image:[[UIImage imageNamed:@"site-menu-posts"] imageFlippedForRightToLeftLayoutDirection] - callback:^{ - [weakSelf showPostListFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)pagesRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Pages", @"Noun. Title. Links to the blog's Pages screen.") - accessibilityIdentifier:@"Site Pages Row" - image:[UIImage imageNamed:@"site-menu-pages"] - callback:^{ - [weakSelf showPageListFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)mediaRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Media", @"Noun. Title. Links to the blog's Media library.") - accessibilityIdentifier:@"Media Row" - image:[UIImage imageNamed:@"site-menu-media"] - callback:^{ - [weakSelf showMediaLibraryFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)commentsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Comments", @"Noun. Title. Links to the blog's Comments screen.") - image:[[UIImage imageNamed:@"site-menu-comments"] imageFlippedForRightToLeftLayoutDirection] - callback:^{ - [weakSelf showCommentsFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)statsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *statsRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Stats", @"Noun. Abbv. of Statistics. Links to a blog's Stats screen.") - accessibilityIdentifier:@"Stats Row" - image:[UIImage imageNamed:@"site-menu-stats"] - callback:^{ - [weakSelf showStatsFromSource:BlogDetailsNavigationSourceRow]; - }]; - return statsRow; -} - -- (BlogDetailsRow *)blazeRow -{ - __weak __typeof(self) weakSelf = self; - CGSize iconSize = CGSizeMake(BlogDetailGridiconSize, BlogDetailGridiconSize); - UIImage *blazeIcon = [[UIImage imageNamed:@"icon-blaze"] resizedTo:iconSize format:ScalingModeScaleAspectFit]; - BlogDetailsRow *blazeRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Blaze", @"Noun. Links to a blog's Blaze screen.") - accessibilityIdentifier:@"Blaze Row" - image:[blazeIcon imageFlippedForRightToLeftLayoutDirection] - imageColor:nil - renderingMode:UIImageRenderingModeAlwaysOriginal - callback:^{ - [weakSelf showBlaze]; - }]; - blazeRow.showsSelectionState = [RemoteFeature enabled:RemoteFeatureFlagBlazeManageCampaigns]; - return blazeRow; -} - -- (BlogDetailsRow *)socialRow -{ - __weak __typeof(self) weakSelf = self; - - NSString *title = ObjCBridge.isWordPress - ? NSLocalizedString(@"Sharing", @"Noun. Title. Links to a blog's sharing options.") - : [BlogDetailsViewControllerStrings socialRowTitle]; - - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:title - image:[UIImage imageNamed:@"site-menu-social"] - callback:^{ - [weakSelf showSharingFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)siteMonitoringRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:[BlogDetailsViewControllerStrings siteMonitoringRowTitle] - accessibilityIdentifier:@"Site Monitoring Row" - image:[UIImage imageNamed:@"tool"] - callback:^{ - [weakSelf showSiteMonitoring]; - }]; - return row; -} - -- (BlogDetailsRow *)activityRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Activity Log", @"Noun. Links to a blog's Activity screen.") - accessibilityIdentifier:@"Activity Log Row" - image:[UIImage imageNamed:@"site-menu-activity"] - callback:^{ - [weakSelf showActivity]; - }]; - return row; -} - -- (BlogDetailsRow *)backupRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Backup", @"Noun. Links to a blog's Jetpack Backups screen.") - accessibilityIdentifier:@"Backup Row" - image:[UIImage gridiconOfType:GridiconTypeCloudOutline] - callback:^{ - [weakSelf showBackup]; - }]; - return row; -} - -- (BlogDetailsRow *)scanRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Scan", @"Noun. Links to a blog's Jetpack Scan screen.") - accessibilityIdentifier:@"Scan Row" - image:[UIImage imageNamed:@"jetpack-scan-menu-icon"] - callback:^{ - [weakSelf showScan]; - }]; - return row; -} - -- (BlogDetailsRow *)usersRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Users", @"Noun. Title. Links to the user management feature.") - accessibilityIdentifier:@"Users Row" - image:[UIImage imageNamed:@"site-menu-people"] - callback:^{ - [weakSelf showUsers]; - }]; - return row; -} - -- (BlogDetailsRow *)pluginsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Plugins", @"Noun. Title. Links to the plugin management feature.") - image:[UIImage imageNamed:@"site-menu-plugins"] - callback:^{ - [weakSelf showPlugins]; - }]; - return row; -} - -- (BlogDetailsRow *)themesRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Themes", @"Themes option in the blog details") - image:[UIImage imageNamed:@"site-menu-themes"] - callback:^{ - [weakSelf showThemes]; - }]; - return row; -} - -- (BlogDetailsRow *)menuRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Menus", @"Menus option in the blog details") - image:[[UIImage gridiconOfType:GridiconTypeMenus] imageFlippedForRightToLeftLayoutDirection] - callback:^{ - [weakSelf showMenus]; - }]; - return row; -} - -- (BlogDetailsRow *)domainsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Domains", @"Noun. Title. Links to the Domains screen.") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Domains Row" - image:[UIImage imageNamed:@"site-menu-domains"] - callback:^{ - [weakSelf showDomainsFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)applicationPasswordRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Application Passwords", comment: @"Link to Application Passwords section") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Application Passwords Row" - image:[UIImage systemImageNamed:@"key"] - callback:^{ - [weakSelf showApplicationPasswords]; - }]; - return row; -} - -- (BlogDetailsRow *)siteSettingsRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Site Settings", @"Noun. Title. Links to the blog's Settings screen.") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Settings Row" - image:[UIImage imageNamed:@"site-menu-settings"] - callback:^{ - [weakSelf showSettingsFromSource:BlogDetailsNavigationSourceRow]; - }]; - return row; -} - -- (BlogDetailsRow *)adminRow -{ - __weak __typeof(self) weakSelf = self; - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:[self adminRowTitle] - image:[UIImage gridiconOfType:GridiconTypeMySites] - callback:^{ - [weakSelf showViewAdmin]; - [weakSelf.tableView deselectSelectedRowWithAnimation:YES]; - }]; - UIImage *image = [[UIImage gridiconOfType:GridiconTypeExternal withSize:CGSizeMake(BlogDetailGridiconAccessorySize, BlogDetailGridiconAccessorySize)] imageFlippedForRightToLeftLayoutDirection]; - UIImageView *accessoryView = [[UIImageView alloc] initWithImage:image]; - accessoryView.tintColor = [WPStyleGuide cellGridiconAccessoryColor]; // Match disclosure icon color. - row.accessoryView = accessoryView; - row.showsSelectionState = NO; - return row; -} - -#pragma mark - Data Model setup - -- (void)reloadTableViewPreservingSelection -{ - // Configure and reload table data when appearing to ensure pending comment count is updated - [self.tableView reloadData]; - - // Check if the last selected category index needs to be updated after a dynamic section is activated and displayed. - // and Use Domain are dynamic section, which means they can be removed or hidden at any time. - NSUInteger sectionIndex = [self findSectionIndexWithSections:self.tableSections category:self.selectedSectionCategory]; - - if (sectionIndex != NSNotFound && self.restorableSelectedIndexPath.section != sectionIndex) { - BlogDetailsSection *section = [self.tableSections objectAtIndex:sectionIndex]; - - NSUInteger row = 0; - - // Use Domain cases we want to select the first row on the next available section - switch (section.category) { - case BlogDetailsSectionCategoryJetpackBrandingCard: - case BlogDetailsSectionCategoryDomainCredit: { - BlogDetailsSubsection subsection = [self defaultSubsection]; - BlogDetailsSectionCategory category = [self sectionCategoryWithSubsection:subsection blog: self.blog]; - sectionIndex = [self findSectionIndexWithSections:self.tableSections category:category]; - } - break; - default: - row = self.restorableSelectedIndexPath.row; - break; - } - - self.restorableSelectedIndexPath = [NSIndexPath indexPathForRow:row inSection:sectionIndex]; - } - - BOOL isValidIndexPath = self.restorableSelectedIndexPath.section < self.tableView.numberOfSections && - self.restorableSelectedIndexPath.row < [self.tableView numberOfRowsInSection:self.restorableSelectedIndexPath.section]; - if (isValidIndexPath && [self isSplitViewDisplayed]) { - // And finally we'll reselect the selected row, if there is one - [self.tableView selectRowAtIndexPath:self.restorableSelectedIndexPath - animated:NO - scrollPosition:[self optimumScrollPositionForIndexPath:self.restorableSelectedIndexPath]]; - } -} - -- (UITableViewScrollPosition)optimumScrollPositionForIndexPath:(NSIndexPath *)indexPath -{ - if (self.isSidebarModeEnabled) { - return UITableViewScrollPositionNone; - } - // Try and avoid scrolling if not necessary - CGRect cellRect = [self.tableView rectForRowAtIndexPath:indexPath]; - BOOL cellIsNotFullyVisible = !CGRectContainsRect(self.tableView.bounds, cellRect); - return (cellIsNotFullyVisible) ? UITableViewScrollPositionMiddle : UITableViewScrollPositionNone; -} - -- (void)configureTableViewData -{ - NSMutableArray *marr = [NSMutableArray array]; - - // TODO: Add the SoTW card here. - if ([self shouldShowSotW2023Card]) { - [marr addNullableObject:[self sotw2023SectionViewModel]]; - } - - if ([self shouldShowJetpackInstallCard]) { - [marr addNullableObject:[self jetpackInstallSectionViewModel]]; - } - - if (self.shouldShowTopJetpackBrandingMenuCard == YES) { - [marr addNullableObject:[self jetpackCardSectionViewModel]]; - } - - if ([self isDashboardEnabled] && [self isSplitViewDisplayed]) { - [marr addNullableObject:[self homeSectionViewModel]]; - } - - if (ObjCBridge.isWordPress) { - if ([self shouldAddJetpackSection]) { - [marr addNullableObject:[self jetpackSectionViewModel]]; - } - - if ([self shouldAddGeneralSection]) { - [marr addNullableObject:[self generalSectionViewModel]]; - } - - [marr addNullableObject:[self publishTypeSectionViewModel]]; - - if ([self shouldAddPersonalizeSection]) { - [marr addNullableObject:[self personalizeSectionViewModel]]; - } - - [marr addNullableObject:[self configurationSectionViewModel]]; - [marr addNullableObject:[self externalSectionViewModel]]; - } else { - [marr addNullableObject:[self contentSectionViewModel]]; - [marr addNullableObject:[self trafficSectionViewModel]]; - [marr addObjectsFromArray:[self maintenanceSectionViewModel]]; - } - - if ([self.blog supports:BlogFeatureRemovable]) { - [marr addNullableObject:[self removeSiteSectionViewModel]]; - } - - if (self.shouldShowBottomJetpackBrandingMenuCard == YES) { - [marr addNullableObject:[self jetpackCardSectionViewModel]]; - } - - // Assign non mutable copy. - self.tableSections = [NSArray arrayWithArray:marr]; -} - -- (Boolean)isSplitViewDisplayed { - return self.isSidebarModeEnabled; -} - -/// This section is available on Jetpack only. -- (BlogDetailsSection *)contentSectionViewModel -{ - NSMutableArray *rows = [NSMutableArray array]; - - [rows addObject:[self postsRow]]; - if ([self.blog supports:BlogFeaturePages]) { - [rows addObject:[self pagesRow]]; - } - [rows addObject:[self mediaRow]]; - [rows addObject:[self commentsRow]]; - - NSString *title = self.isSidebarModeEnabled ? nil : [BlogDetailsViewControllerStrings contentSectionTitle]; - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryContent]; -} - -/// This section is available on Jetpack only. -- (BlogDetailsSection *)trafficSectionViewModel -{ - // Init rows - NSMutableArray *rows = [NSMutableArray array]; - - // Stats row - if ([self.blog isViewingStatsAllowed]) { - [rows addObject:[self statsRow]]; - } - - if ([self shouldShowSubscribersRow]) { - [rows addObject:[self makeSubscribersRow]]; - } - - // Social row - if ([self shouldAddSharingRow]) { - [rows addObject:[self socialRow]]; - } - - // Blaze row - if ([self shouldShowBlaze]) { - [rows addObject:[self blazeRow]]; - } - - if (rows.count == 0) { - return nil; - } - - // Return - NSString *title = [BlogDetailsViewControllerStrings trafficSectionTitle]; - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryTraffic]; -} - -/// Returns a list of sections. Available on Jetpack only. -- (NSArray *)maintenanceSectionViewModel -{ - // Init array - NSMutableArray *sections = [NSMutableArray array]; - NSMutableArray *firstSectionRows = [NSMutableArray array]; - NSMutableArray *secondSectionRows = [NSMutableArray array]; - NSMutableArray *thirdSectionRows = [NSMutableArray array]; - - // The 1st section - if ([self.blog supports:BlogFeatureActivity] && ![self.blog isWPForTeams]) { - [firstSectionRows addObject:[self activityRow]]; - } - if ([self.blog isBackupsAllowed]) { - [firstSectionRows addObject:[self backupRow]]; - } - if ([self.blog isScanAllowed]) { - [firstSectionRows addObject:[self scanRow]]; - } - if ([RemoteFeature enabled:RemoteFeatureFlagSiteMonitoring] && [self.blog supports:BlogFeatureSiteMonitoring]) { - [firstSectionRows addObject:[self siteMonitoringRow]]; - } - - // The 2nd section - if ([self shouldAddPeopleRow]) { - [secondSectionRows addObject:[self makePeopleRow]]; - } - if ([self shouldAddUsersRow]) { - [secondSectionRows addObject:[self usersRow]]; - } - if ([self shouldAddPluginsRow]) { - [secondSectionRows addObject:[self pluginsRow]]; - } - if ([self.blog supports:BlogFeatureThemeBrowsing] && ![self.blog isWPForTeams]) { - [secondSectionRows addObject:[self themesRow]]; - } - if ([self.blog supports:BlogFeatureMenus]) { - [secondSectionRows addObject:[self menuRow]]; - } - if ([self shouldAddDomainRegistrationRow]) { - [secondSectionRows addObject:[self domainsRow]]; - } - if ([Feature enabled:FeatureFlagAllowApplicationPasswords]) { - [secondSectionRows addObject:[self applicationPasswordRow]]; - } - - [secondSectionRows addObject:[self siteSettingsRow]]; - - // Third section - if ([self shouldDisplayLinkToWPAdmin]) { - [thirdSectionRows addObject:[self adminRow]]; - } - - // Add sections - NSString *sectionTitle = [BlogDetailsViewControllerStrings maintenanceSectionTitle]; - BOOL shouldAddSectionTitle = YES; - if ([firstSectionRows count] > 0) { - BlogDetailsSection *section = [[BlogDetailsSection alloc] initWithTitle:sectionTitle - andRows:firstSectionRows - category:BlogDetailsSectionCategoryMaintenance]; - [sections addObject:section]; - shouldAddSectionTitle = NO; - } - if ([secondSectionRows count] > 0) { - NSString *title = shouldAddSectionTitle ? sectionTitle : nil; - BlogDetailsSection *section = [[BlogDetailsSection alloc] initWithTitle:title - andRows:secondSectionRows - category:BlogDetailsSectionCategoryMaintenance]; - [sections addObject:section]; - shouldAddSectionTitle = NO; - } - if ([thirdSectionRows count] > 0) { - NSString *title = shouldAddSectionTitle ? sectionTitle : nil; - BlogDetailsSection *section = [[BlogDetailsSection alloc] initWithTitle:title - andRows:thirdSectionRows - category:BlogDetailsSectionCategoryMaintenance]; - [sections addObject:section]; - } - - // Return - return sections; -} - -- (BlogDetailsSection *)homeSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Home", @"Noun. Links to a blog's dashboard screen.") - accessibilityIdentifier:@"Home Row" - image:[UIImage imageNamed:@"site-menu-home"] - callback:^{ - [weakSelf showDashboard]; - }]]; - - return [[BlogDetailsSection alloc] initWithTitle:nil andRows:rows category:BlogDetailsSectionCategoryHome]; -} - -- (BlogDetailsSection *)generalSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - - if ([self.blog isViewingStatsAllowed]) { - [rows addObject:[self statsRow]]; - } - - if ([self.blog supports:BlogFeatureActivity] && ![self.blog isWPForTeams]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Activity", @"Noun. Links to a blog's Activity screen.") - image:[UIImage imageNamed:@"site-menu-activity"] - callback:^{ - [weakSelf showActivity]; - }]]; - } - - if ([self shouldShowBlaze]) { - [rows addObject:[self blazeRow]]; - } - - if (rows.count == 0) { - return nil; - } - - return [[BlogDetailsSection alloc] initWithTitle:nil andRows:rows category:BlogDetailsSectionCategoryGeneral]; -} - -- (BlogDetailsSection *)jetpackSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - - if ([self.blog isViewingStatsAllowed]) { - [rows addObject:[self statsRow]]; - } - - if ([self.blog supports:BlogFeatureActivity] && ![self.blog isWPForTeams]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Activity Log", @"Noun. Links to a blog's Activity screen.") - accessibilityIdentifier:@"Activity Log Row" - image:[UIImage imageNamed:@"site-menu-activity"] - callback:^{ - [weakSelf showActivity]; - }]]; - } - - - if ([self.blog isBackupsAllowed]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Backup", @"Noun. Links to a blog's Jetpack Backups screen.") - accessibilityIdentifier:@"Backup Row" - image:[UIImage gridiconOfType:GridiconTypeCloudOutline] - callback:^{ - [weakSelf showBackup]; - }]]; - } - - if ([self.blog isScanAllowed]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Scan", @"Noun. Links to a blog's Jetpack Scan screen.") - accessibilityIdentifier:@"Scan Row" - image:[UIImage imageNamed:@"jetpack-scan-menu-icon"] - callback:^{ - [weakSelf showScan]; - }]]; - } - - if ([self.blog supports:BlogFeatureJetpackSettings]) { - BlogDetailsRow *settingsRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Jetpack Settings", @"Noun. Title. Links to the blog's Settings screen.") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Jetpack Settings Row" - image:[UIImage imageNamed:@"site-menu-settings"] - callback:^{ - [weakSelf showJetpackSettings]; - }]; - - [rows addObject:settingsRow]; - } - - if ([self shouldShowBlaze]) { - [rows addObject:[self blazeRow]]; - } - - if (rows.count == 0) { - return nil; - } - - NSString *title = @""; - - if ([self.blog supports:BlogFeatureJetpackSettings]) { - title = NSLocalizedString(@"Jetpack", @"Section title for the publish table section in the blog details screen"); - } - - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryJetpack]; -} - -- (BlogDetailsSection *)publishTypeSectionViewModel -{ - NSMutableArray *rows = [NSMutableArray array]; - - [rows addObject:[self postsRow]]; - [rows addObject:[self mediaRow]]; - if ([self.blog supports:BlogFeaturePages]) { - [rows addObject:[self pagesRow]]; - } - [rows addObject:[self commentsRow]]; - - NSString *title = NSLocalizedString(@"Publish", @"Section title for the publish table section in the blog details screen"); - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryContent]; -} - -- (BlogDetailsSection *)personalizeSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - if ([self.blog supports:BlogFeatureThemeBrowsing] && ![self.blog isWPForTeams]) { - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Themes", @"Themes option in the blog details") - image:[UIImage imageNamed:@"site-menu-themes"] - callback:^{ - [weakSelf showThemes]; - }]; - [rows addObject:row]; - } - if ([self.blog supports:BlogFeatureMenus]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Menus", @"Menus option in the blog details") - image:[[UIImage gridiconOfType:GridiconTypeMenus] imageFlippedForRightToLeftLayoutDirection] - callback:^{ - [weakSelf showMenus]; - }]]; - } - NSString *title =NSLocalizedString(@"Personalize", @"Section title for the personalize table section in the blog details screen."); - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryPersonalize]; -} - -- (BlogDetailsSection *)configurationSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - - if ([self shouldAddMeRow]) { - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Me", @"Noun. Title. Links to the Me screen.") - image:[UIImage gridiconOfType:GridiconTypeUserCircle] - callback:^{ - [weakSelf showMe]; - }]; - [self downloadGravatarImageFor:row forceRefresh: NO]; - self.meRow = row; - [rows addObject:row]; - } - - if ([self shouldAddSharingRow]) { - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Sharing", @"Noun. Title. Links to a blog's sharing options.") - image:[UIImage imageNamed:@"site-menu-social"] - callback:^{ - [weakSelf showSharingFromSource:BlogDetailsNavigationSourceRow]; - }]; - [rows addObject:row]; - } - - if ([self shouldAddPeopleRow]) { - [rows addObject:[self makePeopleRow]]; - } - - if ([self shouldAddUsersRow]) { - [rows addObject:[self usersRow]]; - } - - if ([self shouldAddPluginsRow]) { - [rows addObject:[[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Plugins", @"Noun. Title. Links to the plugin management feature.") - image:[UIImage imageNamed:@"site-menu-plugins"] - callback:^{ - [weakSelf showPlugins]; - }]]; - } - - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Site Settings", @"Noun. Title. Links to the blog's Settings screen.") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Settings Row" - image:[UIImage imageNamed:@"site-menu-settings"] - callback:^{ - [weakSelf showSettingsFromSource:BlogDetailsNavigationSourceRow]; - }]; - - [rows addObject:row]; - - if ([self shouldAddDomainRegistrationRow]) { - BlogDetailsRow *domainsRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Domains", @"Noun. Title. Links to the Domains screen.") - identifier:BlogDetailsSettingsCellIdentifier - accessibilityIdentifier:@"Domains Row" - image:[UIImage imageNamed:@"site-menu-domains"] - callback:^{ - [weakSelf showDomainsFromSource:BlogDetailsNavigationSourceRow]; - }]; - [rows addObject:domainsRow]; - } - - NSString *title = NSLocalizedString(@"Configure", @"Section title for the configure table section in the blog details screen"); - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryConfigure]; -} - -- (BlogDetailsSection *)externalSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - BlogDetailsRow *viewSiteRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"View Site", @"Action title. Opens the user's site in an in-app browser") - image:[UIImage gridiconOfType:GridiconTypeGlobe] - callback:^{ - [weakSelf showViewSiteFromSource:BlogDetailsNavigationSourceRow]; - }]; - viewSiteRow.showsSelectionState = NO; - [rows addObject:viewSiteRow]; - - if ([self shouldDisplayLinkToWPAdmin]) { - BlogDetailsRow *row = [[BlogDetailsRow alloc] initWithTitle:[self adminRowTitle] - image:[UIImage gridiconOfType:GridiconTypeMySites] - callback:^{ - [weakSelf showViewAdmin]; - [weakSelf.tableView deselectSelectedRowWithAnimation:YES]; - }]; - UIImage *image = [[UIImage gridiconOfType:GridiconTypeExternal withSize:CGSizeMake(BlogDetailGridiconAccessorySize, BlogDetailGridiconAccessorySize)] imageFlippedForRightToLeftLayoutDirection]; - UIImageView *accessoryView = [[UIImageView alloc] initWithImage:image]; - accessoryView.tintColor = [WPStyleGuide cellGridiconAccessoryColor]; // Match disclosure icon color. - row.accessoryView = accessoryView; - row.showsSelectionState = NO; - [rows addObject:row]; - } - - NSString *title = NSLocalizedString(@"External", @"Section title for the external table section in the blog details screen"); - return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryExternal]; -} - -- (BlogDetailsSection *)removeSiteSectionViewModel -{ - __weak __typeof(self) weakSelf = self; - NSMutableArray *rows = [NSMutableArray array]; - BlogDetailsRow *removeSiteRow = [[BlogDetailsRow alloc] initWithTitle:NSLocalizedString(@"Remove Site", @"Button to remove a site from the app") - identifier:BlogDetailsRemoveSiteCellIdentifier - image:nil - callback:^{ - [weakSelf.tableView deselectSelectedRowWithAnimation:YES]; - [weakSelf showRemoveSiteAlert]; - }]; - removeSiteRow.showsSelectionState = NO; - removeSiteRow.forDestructiveAction = YES; - [rows addObject:removeSiteRow]; - - return [[BlogDetailsSection alloc] initWithTitle:nil andRows:rows category:BlogDetailsSectionCategoryRemoveSite]; - -} - -- (NSString *)adminRowTitle -{ - if (self.blog.isHostedAtWPcom) { - return NSLocalizedString(@"Dashboard", @"Action title. Noun. Opens the user's WordPress.com dashboard in an external browser."); - } else { - return NSLocalizedString(@"WP Admin", @"Action title. Noun. Opens the user's WordPress Admin in an external browser."); - } -} - -// Non .com users and .com user whose accounts were created -// before LastWPAdminAccessDate should have access to WPAdmin -- (BOOL)shouldDisplayLinkToWPAdmin -{ - if (!self.blog.isHostedAtWPcom) { - return YES; - } - NSDate *hideWPAdminDate = [NSDate dateWithISO8601String:HideWPAdminDate]; - NSManagedObjectContext *context = [[ContextManager sharedInstance] mainContext]; - WPAccount *defaultAccount = [WPAccount lookupDefaultWordPressComAccountInContext:context]; - return [defaultAccount.dateCreated compare:hideWPAdminDate] == NSOrderedAscending; -} - -#pragma mark Site Switching - -- (void)switchToBlog:(Blog*)blog -{ - self.blog = blog; - [self showInitialDetailsForBlog]; - [self.tableView reloadData]; - [self preloadMetadata]; -} - -- (void)showInitialDetailsForBlog -{ - if (![self isSplitViewDisplayed]) { - return; - } - - self.restorableSelectedIndexPath = nil; - - BlogDetailsSubsection subsection = [self defaultSubsection]; - switch (subsection) { - case BlogDetailsSubsectionHome: - [self showDetailViewForSubsection:BlogDetailsSubsectionHome]; - break; - case BlogDetailsSubsectionStats: - [self showDetailViewForSubsection:BlogDetailsSubsectionStats]; - break; - case BlogDetailsSubsectionPosts: - [self showDetailViewForSubsection: BlogDetailsSubsectionPosts]; - break; - default: - break; - } -} - -#pragma mark - Table view data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return self.tableSections.count; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - BlogDetailsSection *detailSection = [self.tableSections objectAtIndex:section]; - return [detailSection.rows count]; -} - -- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath -{ - BlogDetailsSection *section = [self.tableSections objectAtIndex:indexPath.section]; - BlogDetailsRow *row = [section.rows objectAtIndex:indexPath.row]; - cell.textLabel.text = row.title; - cell.accessibilityIdentifier = row.accessibilityIdentifier ?: row.identifier; - cell.detailTextLabel.text = row.detail; - cell.imageView.image = row.image; - cell.imageView.tintColor = row.imageColor; - if (row.accessoryView) { - cell.accessoryView = row.accessoryView; - } -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - BlogDetailsSection *section = [self.tableSections objectAtIndex:indexPath.section]; - - if (section.category == BlogDetailsSectionCategorySotW2023Card) { - SotWTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsSotWCardCellIdentifier]; - __weak __typeof(self) weakSelf = self; - [cell configureOnCardHidden:^{ - [weakSelf configureTableViewData]; - [weakSelf reloadTableViewPreservingSelection]; - }]; - - return cell; - } - - if (section.category == BlogDetailsSectionCategoryJetpackInstallCard) { - JetpackRemoteInstallTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsJetpackInstallCardCellIdentifier]; - [cell configureWithBlog:self.blog viewController:self]; - return cell; - } - - if (section.category == BlogDetailsSectionCategoryMigrationSuccess) { - MigrationSuccessCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsMigrationSuccessCellIdentifier]; - if (self.isSidebarModeEnabled) { - [cell configureForSidebarMode]; - } - [cell configureWithViewController:self]; - return cell; - } - - if (section.category == BlogDetailsSectionCategoryJetpackBrandingCard) { - JetpackBrandingMenuCardCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogDetailsJetpackBrandingCardCellIdentifier]; - [cell configureWithViewController:self]; - return cell; - } - - BlogDetailsRow *row = [section.rows objectAtIndex:indexPath.row]; - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.identifier]; - - if (cell == nil) { - DDLogError(@"Cell with identifier '%@' at index path '%@' is nil", row.identifier, indexPath); - } - - cell.accessibilityHint = row.accessibilityHint; - cell.accessoryView = nil; - cell.textLabel.textAlignment = NSTextAlignmentNatural; - - if (row.forDestructiveAction) { - cell.accessoryType = UITableViewCellAccessoryNone; - [WPStyleGuide configureTableViewDestructiveActionCell:cell]; - } else { - if (row.showsDisclosureIndicator) { - cell.accessoryType = [self isSplitViewDisplayed] ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; - } else { - cell.accessoryType = UITableViewCellAccessoryNone; - } - [WPStyleGuide configureTableViewCell:cell]; - } - - [self configureCell:cell atIndexPath:indexPath]; - - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - BlogDetailsSection *section = [self.tableSections objectAtIndex:indexPath.section]; - BlogDetailsRow *row = [section.rows objectAtIndex:indexPath.row]; - row.callback(); - - if (row.showsSelectionState) { - self.restorableSelectedIndexPath = indexPath; - } else { - if (![self isSplitViewDisplayed]) { - // Deselect current row when not in split view layout - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - } else { - // Reselect the previous row - [tableView selectRowAtIndexPath:self.restorableSelectedIndexPath - animated:YES - scrollPosition:UITableViewScrollPositionNone]; - } - } -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - BlogDetailsSection *detailSection = [self.tableSections objectAtIndex:section]; - return detailSection.title; -} - -- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath -{ - BOOL isNewSelection = (indexPath != tableView.indexPathForSelectedRow); - - if (isNewSelection) { - return indexPath; - } else { - return nil; - } -} - -#pragma mark - Private methods - -- (void)preloadBlogData -{ - // only preload on wifi - if ([ReachabilityUtils.internetReachability isReachableViaWiFi] == false) { - return; - } - - [self preloadComments]; - [self preloadMetadata]; - [self preloadDomains]; -} - -- (void)preloadComments -{ - CommentService *commentService = [[CommentService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - - if ([CommentService shouldRefreshCacheFor:self.blog]) { - [commentService syncCommentsForBlog:self.blog withStatus:CommentStatusFilterAll success:nil failure:nil]; - } -} - -- (void)preloadMetadata -{ - __weak __typeof(self) weakSelf = self; - [self.blogService syncBlogAndAllMetadata:self.blog - completionHandler:^{ - [weakSelf configureTableViewData]; - [weakSelf reloadTableViewPreservingSelection]; - }]; -} - -- (void)preloadDomains -{ - if (![self shouldAddDomainRegistrationRow]) { - return; - } - - [self.blogService refreshDomainsFor:self.blog - success:nil - failure:nil]; -} - -#pragma mark - Remove Site - -- (void)showRemoveSiteAlert -{ - NSString *model = [[UIDevice currentDevice] localizedModel]; - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to continue?\n All site data will be removed from your %@.", @"Title for the remove site confirmation alert, %@ will be replaced with iPhone/iPad/iPod Touch"), model]; - NSString *cancelTitle = NSLocalizedString(@"Cancel", nil); - NSString *destructiveTitle = NSLocalizedString(@"Remove Site", @"Button to remove a site from the app"); - - UIAlertControllerStyle alertStyle = [UIDevice isPad] ? UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet; - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil - message:message - preferredStyle:alertStyle]; - - [alertController addCancelActionWithTitle:cancelTitle handler:nil]; - [alertController addDestructiveActionWithTitle:destructiveTitle handler:^(UIAlertAction * __unused action) { - [self confirmRemoveSite]; - }]; - - [self presentViewController:alertController animated:YES completion:nil]; -} - -#pragma mark - Notification handlers - -- (void)handleDataModelChange:(NSNotification *)note -{ - NSSet *deletedObjects = note.userInfo[NSDeletedObjectsKey]; - if ([deletedObjects containsObject:self.blog]) { - [self.navigationController popToRootViewControllerAnimated:NO]; - return; - } - - if (self.blog.account == nil || self.blog.account.isDeleted) { - // No need to reload this screen if the blog's account is deleted (i.e. during logout) - return; - } - - NSSet *updatedObjects = note.userInfo[NSUpdatedObjectsKey]; - if ([updatedObjects containsObject:self.blog] || [updatedObjects containsObject:self.blog.settings]) { - [self configureTableViewData]; - [self reloadTableViewPreservingSelection]; - } -} - -- (void)handleWillEnterForegroundNotification:(NSNotification *)note -{ - [self configureTableViewData]; - [self reloadTableViewPreservingSelection]; -} - -- (void)observeManagedObjectContextObjectsDidChangeNotification -{ - NSManagedObjectContext *context = [[ContextManager sharedInstance] mainContext]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleDataModelChange:) - name:NSManagedObjectContextObjectsDidChangeNotification - object:context]; -} - -- (void)observeWillEnterForegroundNotification -{ - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleWillEnterForegroundNotification:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; -} - -- (void)stopObservingWillEnterForegroundNotification -{ - [[NSNotificationCenter defaultCenter] removeObserver:self - name:UIApplicationWillEnterForegroundNotification - object:nil]; -} - -#pragma mark - UIViewControllerTransitioningDelegate - -- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source -{ - if ([presented isKindOfClass:[FancyAlertViewController class]]) { - return [[FancyAlertPresentationController alloc] initWithPresentedViewController:presented - presentingViewController:presenting]; - } - - return nil; -} - -#pragma mark - UIAdaptivePresentationControllerDelegate - -- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController { - if (presentationController.presentedViewController == self.presentedSiteSettingsViewController) { - [self.tableView deselectSelectedRowWithAnimation:YES]; - } -} - -#pragma mark - Domain Registration - -- (void)updateTableViewAndHeader -{ - [self updateTableView:^{}]; -} - -/// This method syncs the blog and its metadata, then reloads the table view. -/// -- (void)updateTableView:(void(^)(void))completion -{ - __weak __typeof(self) weakSelf = self; - [self.blogService syncBlogAndAllMetadata:self.blog - completionHandler: - ^{ - [weakSelf configureTableViewData]; - [weakSelf reloadTableViewPreservingSelection]; - completion(); - }]; -} - -#pragma mark - Pull To Refresh - -- (void)pulledToRefresh { - [self pulledToRefreshWith:self.tableView.refreshControl onCompletion:^{}]; -} - -- (void)pulledToRefreshWith:(UIRefreshControl *)refreshControl onCompletion:( void(^)(void))completion { - - [self updateTableView: ^{ - // WORKAROUND: if we don't dispatch this asynchronously, the refresh end animation is clunky. - // To recognize if we can remove this, simply remove the dispatch_async call and test pulling - // down to refresh the site. - dispatch_async(dispatch_get_main_queue(), ^(void){ - [refreshControl endRefreshing]; - - completion(); - }); - }]; -} - -#pragma mark - Constants - -+ (NSString *)userInfoShowPickerKey { - return @"show-picker"; -} - -+ (NSString *)userInfoSiteMonitoringTabKey { - return @"site-monitoring-tab"; -} - -+ (NSString *)userInfoShowManagemenetScreenKey { - return @"show-manage-plugins"; -} - -+ (NSString *)userInfoSourceKey { - return @"source"; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift new file mode 100644 index 000000000000..8d60c775c0e7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.swift @@ -0,0 +1,302 @@ +import UIKit +import WordPressData +import WordPressShared +import WordPressUI +import Reachability +import Gridicons + +public protocol BlogDetailsPresentationDelegate: AnyObject { + func presentBlogDetailsViewController(_ viewController: UIViewController) +} + +public class BlogDetailsViewController: UIViewController { + + public var blog: Blog + public private(set) var tableView: UITableView? + public private(set) var tableViewModel: BlogDetailsTableViewModel? + public var isScrollEnabled = false + public weak var presentationDelegate: BlogDetailsPresentationDelegate? + public var isSidebarModeEnabled = false + public weak var presentedSiteSettingsViewController: UIViewController? + + private lazy var blogService = BlogService(coreDataStack: ContextManager.shared) + private var hasLoggedDomainCreditPromptShownEvent = false + + init(blog: Blog) { + self.blog = blog + self.isScrollEnabled = false + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + let tableView: UITableView + if isSidebarModeEnabled { + tableView = UITableView(frame: .zero, style: .grouped) + } else if isScrollEnabled { + tableView = UITableView(frame: .zero, style: .insetGrouped) + } else { + tableView = IntrinsicTableView(frame: .zero, style: .insetGrouped) + tableView.isScrollEnabled = false + } + self.tableView = tableView + + tableViewModel = BlogDetailsTableViewModel(blog: blog, viewController: self) + tableViewModel?.configure(tableView: tableView) + + tableView.translatesAutoresizingMaskIntoConstraints = false + + if isSidebarModeEnabled { + tableView.separatorStyle = .none + additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) + } + + view.addSubview(tableView) + view.pinSubviewToAllEdges(tableView) + + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(pulledToRefreshTriggered), for: .valueChanged) + tableView.refreshControl = refreshControl + + tableView.accessibilityIdentifier = "Blog Details Table" + tableView.cellLayoutMarginsFollowReadableWidth = true + + WPStyleGuide.configureColors(view: view, tableView: tableView) + WPStyleGuide.configureAutomaticHeightRows(for: tableView) + + hasLoggedDomainCreditPromptShownEvent = false + preloadMetadata() + + if let account = blog.account, account.userID == nil { + let service = AccountService(coreDataStack: ContextManager.shared) + service.updateUserDetails(for: account, success: nil, failure: nil) + } + + observeManagedObjectContextObjectsDidChangeNotification() + observeGravatarImageUpdate() + downloadGravatarImage() + + registerForTraitChanges([UITraitHorizontalSizeClass.self], action: #selector(handleTraitChanges)) + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + observeWillEnterForegroundNotification() + tableViewModel?.viewWillAppear() + // Configure and reload table data when appearing to ensure pending comment count is updated + configureTableViewData() + reloadTableViewPreservingSelection() + preloadBlogData() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + createUserActivity() + + WPAnalytics.track(.mySiteSiteMenuShown) + + if shouldShowJetpackInstallCard() { + WPAnalytics.track(.jetpackInstallFullPluginCardViewed, properties: [WPAppAnalyticsKeyTabSource: "site_menu"]) + } + + if shouldShowBlaze() { + BlazeEventsTracker.trackEntryPointDisplayed(for: .menuItem) + } + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + stopObservingWillEnterForegroundNotification() + } + + @objc private func handleTraitChanges() { + configureTableViewData() + reloadTableViewPreservingSelection() + } + + public func reloadTableViewPreservingSelection() { + tableViewModel?.reloadTableViewPreservingSelection() + } + + public func configureTableViewData() { + tableViewModel?.configureTableViewData() + } + + public func `switch`(to blog: Blog) { + self.blog = blog + showInitialDetailsForBlog() + tableView?.reloadData() + preloadMetadata() + } + + public func showInitialDetailsForBlog() { + tableViewModel?.showInitialDetailsForBlog() + } + + public func updateTableView(completion: (() -> Void)?) { + let completionBlock = completion ?? {} + blogService.syncBlogAndAllMetadata(blog) { [weak self] in + self?.configureTableViewData() + self?.reloadTableViewPreservingSelection() + completionBlock() + } + } + + public func pulledToRefresh(with refreshControl: UIRefreshControl, onCompletion completion: (() -> Void)?) { + let completionBlock = completion ?? {} + updateTableView { [weak refreshControl] in + DispatchQueue.main.async { + refreshControl?.endRefreshing() + completionBlock() + } + } + } + + private func preloadBlogData() { + // only preload on wifi + guard ReachabilityUtils.internetReachability?.isReachableViaWiFi() == true else { + return + } + + preloadComments() + preloadMetadata() + preloadDomains() + } + + private func preloadComments() { + let commentService = CommentService(coreDataStack: ContextManager.shared) + + if CommentService.shouldRefreshCache(for: blog) { + commentService.syncComments(for: blog, withStatus: CommentStatusFilterAll, success: nil, failure: nil) + } + } + + public func preloadMetadata() { + blogService.syncBlogAndAllMetadata(blog) { [weak self] in + self?.configureTableViewData() + self?.reloadTableViewPreservingSelection() + } + } + + private func preloadDomains() { + guard shouldAddDomainRegistrationRow() else { + return + } + + blogService.refreshDomains(for: blog, success: nil, failure: nil) + } + + public func showRemoveSiteAlert() { + let model = UIDevice.current.localizedModel + let message = String(format: NSLocalizedString( + "Are you sure you want to continue?\n All site data will be removed from your %@.", + comment: "Title for the remove site confirmation alert, %@ will be replaced with iPhone/iPad/iPod Touch" + ), model) + + let destructiveTitle = NSLocalizedString("Remove Site", comment: "Button to remove a site from the app") + + let alertStyle: UIAlertController.Style = UIDevice.isPad() ? .alert : .actionSheet + let alertController = UIAlertController(title: nil, message: message, preferredStyle: alertStyle) + + alertController.addCancelActionWithTitle(SharedStrings.Button.cancel, handler: nil) + alertController.addDestructiveActionWithTitle(destructiveTitle) { [weak self] _ in + self?.confirmRemoveSite() + } + + present(alertController, animated: true) + } + + @objc private func pulledToRefreshTriggered(_ control: UIRefreshControl) { + pulledToRefresh(with: control, onCompletion: {}) + } + + @objc private func handleDataModelChange(_ notification: NSNotification) { + guard let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set else { + return + } + + if deletedObjects.contains(blog) { + navigationController?.popToRootViewController(animated: false) + return + } + + if blog.account == nil || blog.account?.isDeleted == true { + return + } + + guard let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set else { + return + } + + if updatedObjects.contains(blog) || (blog.settings != nil && updatedObjects.contains(blog.settings!)) { + configureTableViewData() + reloadTableViewPreservingSelection() + } + } + + @objc private func handleWillEnterForeground(_ notification: NSNotification) { + configureTableViewData() + reloadTableViewPreservingSelection() + } + + private func observeManagedObjectContextObjectsDidChangeNotification() { + let context = ContextManager.shared.mainContext + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDataModelChange(_:)), + name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, + object: context + ) + } + + private func observeWillEnterForegroundNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillEnterForeground(_:)), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + private func stopObservingWillEnterForegroundNotification() { + NotificationCenter.default.removeObserver( + self, + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } +} + +extension BlogDetailsViewController: UIViewControllerTransitioningDelegate { + + public func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController + ) -> UIPresentationController? { + if presented is FancyAlertViewController { + return FancyAlertPresentationController( + presentedViewController: presented, + presenting: presenting + ) + } + return nil + } + +} + +extension BlogDetailsViewController: UIAdaptivePresentationControllerDelegate { + + public func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + if presentationController.presentedViewController == presentedSiteSettingsViewController { + tableView?.deselectSelectedRowWithAnimation(true) + } + } + +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift index af05d5e37153..74e6fec88fef 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/SoTW 2023/SOTWCardView.swift @@ -141,16 +141,6 @@ public class SotWTableViewCell: UITableViewCell { extension BlogDetailsViewController { - @objc public func sotw2023SectionViewModel() -> BlogDetailsSection { - let row = BlogDetailsRow() - row.callback = {} - let section = BlogDetailsSection(title: nil, - rows: [row], - footerTitle: nil, - category: .sotW2023Card) - return section - } - @objc public func shouldShowSotW2023Card() -> Bool { guard AppConfiguration.isWordPress && RemoteFeatureFlag.wordPressSotWCard.enabled() else { return false diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift index 3b3209f02140..cfe7039a1109 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift @@ -673,10 +673,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite } private func makeBlogDetailsViewController(for blog: Blog) -> BlogDetailsViewController { - let blogDetailsViewController = BlogDetailsViewController() - blogDetailsViewController.blog = blog - - return blogDetailsViewController + BlogDetailsViewController(blog: blog) } private func showSitePicker(for blog: Blog) { @@ -726,7 +723,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite case .siteMenu: blogDetailsViewController?.blog = blog blogDetailsViewController?.configureTableViewData() - blogDetailsViewController?.tableView.reloadData() + blogDetailsViewController?.tableView?.reloadData() blogDetailsViewController?.preloadMetadata() blogDetailsViewController?.showInitialDetailsForBlog() case .dashboard: @@ -900,12 +897,12 @@ extension MySiteViewController: BlogDetailsPresentationDelegate { /// - Parameters: /// - subsection: The specific subsection to show. /// - func showBlogDetailsSubsection(_ subsection: BlogDetailsSubsection, userInfo: [AnyHashable: Any] = [:]) { + func showBlogDetailsSubsection(_ subsection: BlogDetailsRowKind, userInfo: [String: Any] = [:]) { blogDetailsViewController?.showDetailView(for: subsection, userInfo: userInfo) } func showBlogDetailsMeSubsection() -> MeViewController? { - blogDetailsViewController?.showDetailViewForMeSubsection(userInfo: [:]) + blogDetailsViewController?.showDetailViewForMe(userInfo: [:]) } // TODO: Refactor presentation from routes diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift index 9c9d23b46e12..859beff5da6a 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Menu Card/BlogDetailsViewController+JetpackBrandingMenuCard.swift @@ -12,25 +12,6 @@ extension BlogDetailsViewController { return presenter.shouldShowBottomCard() } - @objc public func jetpackCardSectionViewModel() -> BlogDetailsSection { - let row = BlogDetailsRow() - row.callback = { [weak self] in - self?.showJetpackOverlay() - } - return BlogDetailsSection( - title: nil, - rows: [row], - footerTitle: nil, - category: .jetpackBrandingCard - ) - } - - private func showJetpackOverlay() { - let presenter = JetpackBrandingMenuCardPresenter(blog: blog) - JetpackFeaturesRemovalCoordinator.presentOverlayIfNeeded(in: self, source: .card, blog: blog) - presenter.trackCardTapped() - } - func reloadTableView() { configureTableViewData() reloadTableViewPreservingSelection() diff --git a/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallTableViewCell.swift b/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallTableViewCell.swift index dd47c8b06d84..b193c28a285d 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallTableViewCell.swift @@ -70,16 +70,6 @@ public class JetpackRemoteInstallTableViewCell: UITableViewCell { extension BlogDetailsViewController: JetpackRemoteInstallDelegate { - @objc public func jetpackInstallSectionViewModel() -> BlogDetailsSection { - let row = BlogDetailsRow() - row.callback = {} - let section = BlogDetailsSection(title: nil, - rows: [row], - footerTitle: nil, - category: .jetpackInstallCard) - return section - } - func jetpackRemoteInstallCompleted() { dismiss(animated: true) } diff --git a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift index a0eb6cbafcd2..fad661dc9586 100644 --- a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift @@ -67,7 +67,7 @@ public class MySitesCoordinator: NSObject { // MARK: - Blog Details - func showBlogDetails(for blog: Blog, then subsection: BlogDetailsSubsection?, userInfo: [AnyHashable: Any]) { + func showBlogDetails(for blog: Blog, then subsection: BlogDetailsRowKind?, userInfo: [String: Any]) { showRootViewController() mySiteViewController.blog = blog diff --git a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift index 5a99fd08ae55..4fc08737eb51 100644 --- a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift +++ b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift @@ -9,7 +9,7 @@ protocol SiteMenuViewControllerDelegate: AnyObject { /// The site menu for the split view navigation. final class SiteMenuViewController: UIViewController { let blog: Blog - private let blogDetailsVC = SiteMenuListViewController() + private let blogDetailsVC: SiteMenuListViewController weak var delegate: SiteMenuViewControllerDelegate? @@ -19,6 +19,7 @@ final class SiteMenuViewController: UIViewController { init(blog: Blog) { self.blog = blog + blogDetailsVC = SiteMenuListViewController(blog: blog) super.init(nibName: nil, bundle: nil) } @@ -79,57 +80,16 @@ final class SiteMenuViewController: UIViewController { tipObserver = nil } - func showSubsection(_ subsection: BlogDetailsSubsection, userInfo: [AnyHashable: Any]) { + func showSubsection(_ subsection: BlogDetailsRowKind, userInfo: [String: Any]) { blogDetailsVC.showDetailView(for: subsection, userInfo: userInfo) } } // Updates the `BlogDetailsViewController` style to match the native sidebar style. private final class SiteMenuListViewController: BlogDetailsViewController { - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let title = super.tableView(tableView, titleForHeaderInSection: section) - return title == nil ? 0 : 48 - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let title = super.tableView(tableView, titleForHeaderInSection: section) else { - return nil - } - let label = UILabel() - label.font = UIFont.preferredFont(forTextStyle: .headline) - label.text = title - - let headerView = UIView() - headerView.addSubview(label) - label.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20), - label.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -8), - label.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: 20) - ]) - return headerView - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = super.tableView(tableView, cellForRowAt: indexPath) - - cell.textLabel?.font = .preferredFont(forTextStyle: .body) - cell.backgroundColor = .clear - cell.selectedBackgroundView = { - let backgroundView = UIView() - backgroundView.backgroundColor = .secondarySystemFill - backgroundView.layer.cornerRadius = DesignConstants.radius(.large) - backgroundView.layer.cornerCurve = .continuous - - let container = UIView() - container.addSubview(backgroundView) - backgroundView.pinEdges(insets: UIEdgeInsets(.horizontal, 16)) - return container - }() - cell.focusStyle = .custom - cell.focusEffect = nil - - return cell + override func viewDidLoad() { + super.viewDidLoad() + self.tableViewModel?.useSiteMenuStyle = true } } diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController.m b/WordPress/Classes/ViewRelated/System/WPTabBarController.m index 8edb52b323a7..9a500db73f44 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController.m +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController.m @@ -3,7 +3,6 @@ #import "AccountService.h" #import "BlogService.h" -#import "BlogDetailsViewController.h" #import "WPAppAnalytics.h" #import "WordPress-Swift.h" @import WordPressData; diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index db6900d00345..f7f8cf2a6e96 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1131,7 +1131,6 @@ Utility/Sharing/WPActivityDefaults.h, Utility/UIAlertControllerProxy.h, Utility/WPError.h, - "ViewRelated/Blog/Blog Details/BlogDetailsViewController.h", ViewRelated/Blog/Sharing/SharingViewController.h, "ViewRelated/Blog/Site Settings/SettingTableViewCell.h", "ViewRelated/Blog/Site Settings/SiteSettingsViewController.h",