From 1c6f8dc06fe88ed79a3c31bc67e70dc99e8fb688 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 4 Sep 2026 13:49:46 +1200 Subject: [PATCH 1/4] Format ThemeBrowserCell with swift-format --- .../ViewRelated/Themes/ThemeBrowserCell.swift | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift index abde73120c2c..3ee12c0bff21 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift @@ -13,7 +13,7 @@ public enum ThemeAction { case tryCustomize case view - static func activeActionsForTheme(_ theme: Theme) ->[ThemeAction] { + static func activeActionsForTheme(_ theme: Theme) -> [ThemeAction] { if theme.custom { if theme.hasDetailsURL() { return [customize, details] @@ -23,7 +23,7 @@ public enum ThemeAction { return [customize, details, support] } - static func inactiveActionsForTheme(_ theme: Theme) ->[ThemeAction] { + static func inactiveActionsForTheme(_ theme: Theme) -> [ThemeAction] { if theme.custom { if theme.hasDetailsURL() { return [tryCustomize, activate, details] @@ -109,9 +109,12 @@ open class ThemeBrowserCell: UICollectionViewCell { override open var isHighlighted: Bool { didSet { let alphaFinal: CGFloat = isHighlighted ? 0.3 : 0 - UIView.animate(withDuration: 0.2, animations: { [weak self] in - self?.highlightView.alpha = alphaFinal - }) + UIView.animate( + withDuration: 0.2, + animations: { [weak self] in + self?.highlightView.alpha = alphaFinal + } + ) } } @@ -220,20 +223,25 @@ open class ThemeBrowserCell: UICollectionViewCell { } fileprivate func refreshScreenshotImage(_ imageUrl: String) { - let imageUrlWithWidth = imageUrlForWidth( imageUrl: imageUrl ) + let imageUrlWithWidth = imageUrlForWidth(imageUrl: imageUrl) let screenshotUrl = URL(string: imageUrlWithWidth) imageView.backgroundColor = Styles.placeholderColor activityView.startAnimating() - imageView.downloadImage(from: screenshotUrl, success: { [weak self] _ in - self?.showScreenshot() - }, failure: { [weak self] error in - if let error = error as NSError?, error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled { + imageView.downloadImage( + from: screenshotUrl, + success: { [weak self] _ in + self?.showScreenshot() + }, + failure: { [weak self] error in + let nsError = error as NSError? + if nsError?.domain == NSURLErrorDomain && nsError?.code == NSURLErrorCancelled { return } DDLogError("Error loading theme screenshot: \(String(describing: error?.localizedDescription))") self?.showPlaceholder() - }) + } + ) } // MARK: - Actions @@ -245,13 +253,17 @@ open class ThemeBrowserCell: UICollectionViewCell { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let themeActions = theme.isCurrentTheme() ? ThemeAction.activeActionsForTheme(theme) : ThemeAction.inactiveActionsForTheme(theme) + let themeActions = + theme.isCurrentTheme() + ? ThemeAction.activeActionsForTheme(theme) : ThemeAction.inactiveActionsForTheme(theme) themeActions.forEach { themeAction in - alertController.addActionWithTitle(themeAction.title, - style: .default, - handler: { (_: UIAlertAction) in - themeAction.present(theme, presenter) - }) + alertController.addActionWithTitle( + themeAction.title, + style: .default, + handler: { (_: UIAlertAction) in + themeAction.present(theme, presenter) + } + ) } let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel action title") @@ -290,7 +302,10 @@ extension ThemeBrowserCell: Accessible { private func prepareActionButtonForVoiceOver() { actionButton.isAccessibilityElement = true - actionButton.accessibilityLabel = NSLocalizedString("More", comment: "Action button to display more available options") + actionButton.accessibilityLabel = NSLocalizedString( + "More", + comment: "Action button to display more available options" + ) actionButton.accessibilityTraits = .button } From 1adddafe1fda4e114654ca1175a1f13fede87a36 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 4 Sep 2026 14:06:28 +1200 Subject: [PATCH 2/4] Show a loading indicator when activating a theme from the grid Activating from the theme grid gave no feedback because the spinner was only attached to the preview screen's Activate bar button, which does not exist in that flow. The browser now tracks the activating theme and the grid cell replaces its "..." button with a spinner while the request runs. The state lives in the view controller so it survives cell reuse and reloads, and a second activation is ignored while one is in flight. --- .../ViewRelated/Themes/ThemeBrowserCell.swift | 31 +++++++++++++++++++ .../Themes/ThemeBrowserViewController.swift | 11 ++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift index 3ee12c0bff21..ae7c55d6bdd8 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift @@ -100,6 +100,29 @@ open class ThemeBrowserCell: UICollectionViewCell { @objc open var showPriceInformation: Bool = false open weak var presenter: ThemePresenter? + /// Replaces the action button with a spinner while the theme is being activated. + var isActivating: Bool = false { + didSet { + actionButton.isHidden = isActivating + if isActivating { + activatingIndicator.startAnimating() + } else { + activatingIndicator.stopAnimating() + } + } + } + + private lazy var activatingIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.hidesWhenStopped = true + indicator.accessibilityLabel = NSLocalizedString( + "themeBrowser.cell.activating", + value: "Activating", + comment: "Accessibility label for the spinner shown while a theme is being activated" + ) + return indicator + }() + fileprivate var placeholderImage = UIImage(named: "theme-loading") fileprivate var activeEllipsisImage = UIImage(named: "icon-menu-ellipsis-white") fileprivate var inactiveEllipsisImage = UIImage(named: "icon-menu-ellipsis") @@ -123,6 +146,13 @@ open class ThemeBrowserCell: UICollectionViewCell { actionButton.isExclusiveTouch = true + infoBar.addSubview(activatingIndicator) + activatingIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + activatingIndicator.centerXAnchor.constraint(equalTo: actionButton.centerXAnchor), + activatingIndicator.centerYAnchor.constraint(equalTo: actionButton.centerYAnchor) + ]) + layer.cornerRadius = 12 layer.cornerCurve = .continuous layer.borderColor = UIColor.separator.cgColor @@ -138,6 +168,7 @@ open class ThemeBrowserCell: UICollectionViewCell { theme = nil presenter = nil showPriceInformation = false + isActivating = false } fileprivate func refreshGUI() { diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index d35b74e7dbc4..763fb0050244 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -252,6 +252,9 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou fileprivate var presentingTheme: Theme? + /// The theme whose activation request is in flight. Drives the grid cell spinner across cell reuse and reloads. + private var activatingThemeId: String? + private var noResultsViewController: NoResultsViewController? private struct NoResultsTitles { @@ -620,6 +623,7 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou cell.presenter = self cell.theme = themeAtIndexPath(indexPath) + cell.isActivating = activatingThemeId != nil && cell.theme?.themeId == activatingThemeId if sections[indexPath.section] == .themes { syncMoreThemesIfNeeded(indexPath) @@ -939,10 +943,12 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou @objc var onWebkitViewControllerClose: (() -> Void)? @objc open func activateTheme(_ theme: Theme?) { - guard let theme, !theme.isCurrentTheme() else { + guard let theme, !theme.isCurrentTheme(), activatingThemeId == nil else { return } + activatingThemeId = theme.themeId + collectionView?.reloadData() updateActivateButton(isLoading: true) _ = themeService.activate( @@ -955,6 +961,7 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou blog: self?.blog ) + self?.activatingThemeId = nil self?.collectionView?.reloadData() let successTitle = NSLocalizedString( @@ -998,6 +1005,8 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou comment: "Title of alert when theme activation fails" ) + self?.activatingThemeId = nil + self?.collectionView?.reloadData() self?.activityIndicator.stopAnimating() self?.activateButton?.customView = nil From 70e98c7be5da5db20e4a1a283e6745239560da65 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 4 Sep 2026 14:06:28 +1200 Subject: [PATCH 3/4] Make the theme preview Activate spinner visible The spinner replacing the Activate bar button was forced to white, which is invisible on the preview's light navigation bar. Use the default color instead. --- .../Classes/ViewRelated/Themes/ThemeBrowserViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 763fb0050244..2eb18d042a8b 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -205,8 +205,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou fileprivate var activityIndicator: UIActivityIndicatorView = { let indicatorView = UIActivityIndicatorView(style: .medium) indicatorView.frame = themesLoaderFrame - //TODO update color with white headers - indicatorView.color = .white indicatorView.startAnimating() return indicatorView }() From 9075e2022d4aebc74a0dbb36f690907d80fac744 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 4 Sep 2026 14:03:35 +1200 Subject: [PATCH 4/4] Remove the "Manage site" action from the theme activated alert It only popped the theme browser, which the back button already does. --- .../Themes/ThemeBrowserViewController.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 2eb18d042a8b..56a32f530a0a 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -971,10 +971,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou comment: "Message of alert when theme activation succeeds" ) let successMessage = String(format: successFormat, theme?.name ?? "", theme?.author ?? "") - let manageTitle = NSLocalizedString( - "Manage site", - comment: "Return to blog screen action when theme activation succeeds" - ) self?.updateActivateButton(isLoading: false) @@ -983,13 +979,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou message: successMessage, preferredStyle: .alert ) - alertController.addActionWithTitle( - manageTitle, - style: .default, - handler: { [weak self] (_: UIAlertAction) in - _ = self?.navigationController?.popViewController(animated: true) - } - ) alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() },