From 58127605554fce020f7fc2de670f17c684f516f8 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 09:46:33 -0500 Subject: [PATCH 001/101] Rename ImageLoadingController --- WordPress/Classes/Utility/Media/AsyncImageView.swift | 4 ++-- ...eViewController.swift => ImageLoadingController.swift} | 2 +- .../Utility/Media/UIImageView+ImageDownloader.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename WordPress/Classes/Utility/Media/{ImageViewController.swift => ImageLoadingController.swift} (98%) diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index 307e3ae6e94e..b7bcea67c2f2 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -9,7 +9,7 @@ final class AsyncImageView: UIView { private let imageView = GIFImageView() private var errorView: UIImageView? private var spinner: UIActivityIndicatorView? - private let controller = ImageViewController() + private let controller = ImageLoadingController() enum LoadingStyle { /// Shows a secondary background color during the download. @@ -88,7 +88,7 @@ final class AsyncImageView: UIView { controller.setImage(with: imageURL, host: host, size: size, completion: completion) } - private func setState(_ state: ImageViewController.State) { + private func setState(_ state: ImageLoadingController.State) { imageView.isHidden = true errorView?.isHidden = true spinner?.stopAnimating() diff --git a/WordPress/Classes/Utility/Media/ImageViewController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift similarity index 98% rename from WordPress/Classes/Utility/Media/ImageViewController.swift rename to WordPress/Classes/Utility/Media/ImageLoadingController.swift index 053e6018b072..a226bd4219c1 100644 --- a/WordPress/Classes/Utility/Media/ImageViewController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -4,7 +4,7 @@ import WordPressMedia /// A convenience class for managing image downloads for individual views. @MainActor -final class ImageViewController { +final class ImageLoadingController { var downloader: ImageDownloader = .shared var onStateChanged: (State) -> Void = { _ in } diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index 303c9a9f60f4..45ab051e78ab 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -31,11 +31,11 @@ struct ImageViewExtensions { controller.setImage(with: imageURL, host: host, size: size, completion: completion) } - var controller: ImageViewController { - if let controller = objc_getAssociatedObject(imageView, ImageViewExtensions.controllerKey) as? ImageViewController { + var controller: ImageLoadingController { + if let controller = objc_getAssociatedObject(imageView, ImageViewExtensions.controllerKey) as? ImageLoadingController { return controller } - let controller = ImageViewController() + let controller = ImageLoadingController() controller.onStateChanged = { [weak imageView] in guard let imageView else { return } setState($0, for: imageView) @@ -44,7 +44,7 @@ struct ImageViewExtensions { return controller } - private func setState(_ state: ImageViewController.State, for imageView: UIImageView) { + private func setState(_ state: ImageLoadingController.State, for imageView: UIImageView) { switch state { case .loading: break From d807dfe6adbca53a1c2140771dc791818de4187f Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 20:40:52 -0500 Subject: [PATCH 002/101] Add LightboxViewController to replace WPImageViewController --- .../LightboxImagePageViewController.swift | 75 ++++++++++ .../Lightbox/LightboxImageScrollView.swift | 132 ++++++++++++++++++ .../Media/Lightbox/LightboxItem.swift | 7 + .../Lightbox/LightboxViewController.swift | 118 ++++++++++++++++ 4 files changed, 332 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift new file mode 100644 index 000000000000..34c3e8899dad --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -0,0 +1,75 @@ +import UIKit +import WordPressUI + +final class LightboxImagePageViewController: UIViewController { + private(set) var scrollView = LightboxImageScrollView() + private let controller = ImageLoadingController() + private let image: LightboxItem + private let activityIndicator = UIActivityIndicatorView() + private var errorView: UIImageView? + + init(image: LightboxItem) { + self.image = image + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(scrollView) + + activityIndicator.hidesWhenStopped = true + view.addSubview(activityIndicator) + activityIndicator.pinCenter() + + scrollView.onDismissTapped = { [weak self] in + self?.parent?.presentingViewController?.dismiss(animated: true) + } + + controller.onStateChanged = { [weak self] in + self?.setState($0) + } + + controller.setImage(with: image.sourceURL, host: image.host) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if scrollView.frame != view.bounds { + scrollView.frame = view.bounds + scrollView.configureLayout() + } + } + + private func setState(_ state: ImageLoadingController.State) { + switch state { + case .loading: + if scrollView.imageView.image == nil { + activityIndicator.startAnimating() + } + case .success(let image): + activityIndicator.stopAnimating() + scrollView.configure(with: image) + case .failure: + activityIndicator.stopAnimating() + makeErrorView().isHidden = false + } + } + + private func makeErrorView() -> UIImageView { + if let errorView { + return errorView + } + let errorView = UIImageView(image: UIImage(systemName: "exclamationmark.triangle")) + errorView.tintColor = .separator + view.addSubview(errorView) + errorView.pinCenter() + self.errorView = errorView + return errorView + } +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift new file mode 100644 index 000000000000..5cd72ac7c9a5 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift @@ -0,0 +1,132 @@ +import UIKit +import Gifu +import WordPressUI + +final class LightboxImageScrollView: UIScrollView, UIScrollViewDelegate { + let imageView = GIFImageView() + + var onDismissTapped: (() -> Void)? + + // MARK: - Initializers + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Configuration + + func configure(with image: UIImage) { + imageView.configure(image: image) + configureImageView() + } + + private func setupView() { + addSubview(imageView) + + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = true + + delegate = self + isMultipleTouchEnabled = true + minimumZoomScale = 1 + maximumZoomScale = 3 + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + + let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didRecognizeDoubleTap)) + doubleTapRecognizer.numberOfTapsRequired = 2 + doubleTapRecognizer.numberOfTouchesRequired = 1 + addGestureRecognizer(doubleTapRecognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didRecognizeTap)) + addGestureRecognizer(tapRecognizer) + + tapRecognizer.require(toFail: doubleTapRecognizer) + } + + // MARK: Recognizers + + @objc private func didRecognizeDoubleTap(_ recognizer: UITapGestureRecognizer) { + let zoomScale = zoomScale > minimumZoomScale ? minimumZoomScale : maximumZoomScale + let width = bounds.size.width / zoomScale + let height = bounds.size.height / zoomScale + + let location = recognizer.location(in: imageView) + let x = location.x - (width / 2.0) + let y = location.y - (height / 2.0) + + let rect = CGRect(x: x, y: y, width: width, height: height) + zoom(to: rect, animated: true) + } + + @objc private func didRecognizeTap(_ recognizer: UITapGestureRecognizer) { + onDismissTapped?() + } + + // MARK: Layout + + func configureLayout() { + contentSize = bounds.size + imageView.frame = bounds + zoomScale = minimumZoomScale + + configureImageView() + } + + private func configureImageView() { + guard let image = imageView.image else { + return centerImageView() + } + + let imageViewSize = imageView.frame.size + let imageSize = image.size + let actualImageSize: CGSize + + if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height { + actualImageSize = CGSize( + width: imageViewSize.width, + height: imageViewSize.width / imageSize.width * imageSize.height) + } else { + actualImageSize = CGSize( + width: imageViewSize.height / imageSize.height * imageSize.width, + height: imageViewSize.height) + } + + imageView.frame = CGRect(origin: CGPoint.zero, size: actualImageSize) + + centerImageView() + } + + private func centerImageView() { + var newFrame = imageView.frame + if newFrame.size.width < bounds.size.width { + newFrame.origin.x = (bounds.size.width - newFrame.size.width) / 2.0 + } else { + newFrame.origin.x = 0.0 + } + + if newFrame.size.height < bounds.size.height { + newFrame.origin.y = (bounds.size.height - newFrame.size.height) / 2.0 + } else { + newFrame.origin.y = 0.0 + } + imageView.frame = newFrame + } + + // MARK: UIScrollViewDelegate + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + imageView + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + centerImageView() + } +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift new file mode 100644 index 000000000000..9d6d58d3b251 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -0,0 +1,7 @@ +import Foundation +import WordPressMedia + +struct LightboxItem { + let sourceURL: URL + var host: MediaHost? +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift new file mode 100644 index 000000000000..5b7b331fbaf8 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -0,0 +1,118 @@ +import UIKit +import WordPressMedia +import WordPressUI +import UniformTypeIdentifiers + +/// A fullscreen preview of a set of media assets. +final class LightboxViewController: UIViewController { + private var pageVC: LightboxImagePageViewController? + private var items: [LightboxItem] + + init(items: [LightboxItem]) { + assert(items.count == 1, "Current API supports only one item at a time") + self.items = items + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .black + + if let item = items.first { + show(item) + } + + addCloseButton() + } + + private func show(_ item: LightboxItem) { + let pageVC = LightboxImagePageViewController(image: item) + pageVC.willMove(toParent: self) + addChild(pageVC) + view.addSubview(pageVC.view) + pageVC.view.pinEdges() + pageVC.didMove(toParent: self) + self.pageVC = pageVC + } + + private func addCloseButton() { + let button = UIButton(type: .system) + let image = UIImage(systemName: "xmark.circle.fill")? + .withConfiguration(UIImage.SymbolConfiguration(font: .systemFont(ofSize: 22, weight: .medium))) + .applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [.lightGray, .opaqueSeparator.withAlphaComponent(0.2)])) + button.setImage(image, for: []) + button.addTarget(self, action: #selector(buttonCloseTapped), for: .primaryActionTriggered) + button.accessibilityLabel = SharedStrings.Button.close + view.addSubview(button) + button.pinEdges([.top, .trailing], to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(.all, 8)) + } + + @objc private func buttonCloseTapped() { + presentingViewController?.dismiss(animated: true) + } + + // MARK: Presentation + + func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView) { + if #available(iOS 18.0, *) { + let options = UIViewController.Transition.ZoomOptions() + options.alignmentRectProvider = { context in + // For more info, see https://douglashill.co/zoom-transitions/#Zooming-to-only-part-of-the-destination-view + let detailViewController = context.zoomedViewController as! LightboxViewController + let detailsView: UIView = detailViewController.pageVC?.scrollView.imageView ?? detailViewController.view + return detailsView.convert(detailsView.bounds, to: detailViewController.view) + } + preferredTransition = .zoom(options: options) { context in + souceItemProvider(context.zoomedViewController) + } + } else { + modalTransitionStyle = .crossDissolve + } + } + + func configureZoomTransition(sourceView: UIView) { + configureZoomTransition { _ in sourceView } + } +} + +@available(iOS 17, *) +#Preview { + UINavigationController(rootViewController: LightboxDemoViewController()) +} + +/// An example of ``LightboxController`` usage. +final class LightboxDemoViewController: UIViewController { + let imageView = UIImageView() + let images: [LightboxItem] = [ + LightboxItem(sourceURL: URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")!) + ] + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(imageView) + imageView.pinCenter() + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 120), + imageView.heightAnchor.constraint(equalToConstant: 80), + ]) + + Task { @MainActor in + imageView.image = try? await ImageDownloader.shared.image(from: images[0].sourceURL) + } + + imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) + imageView.isUserInteractionEnabled = true + } + + @objc private func imageTapped() { + let lightboxVC = LightboxViewController(items: images) + lightboxVC.configureZoomTransition(sourceView: imageView) + present(lightboxVC, animated: true) + } +} From 0d05ae8c94cfc05eb93aa6a42def7944511934cf Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 20:47:16 -0500 Subject: [PATCH 003/101] Integrate LightboxViewController in Reader --- .../Media/Lightbox/LightboxViewController.swift | 4 ++-- .../Reader/Detail/ReaderDetailCoordinator.swift | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 5b7b331fbaf8..1478e0ff6686 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -58,7 +58,7 @@ final class LightboxViewController: UIViewController { // MARK: Presentation - func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView) { + func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView?) { if #available(iOS 18.0, *) { let options = UIViewController.Transition.ZoomOptions() options.alignmentRectProvider = { context in @@ -75,7 +75,7 @@ final class LightboxViewController: UIViewController { } } - func configureZoomTransition(sourceView: UIView) { + func configureZoomTransition(sourceView: UIView?) { configureZoomTransition { _ in sourceView } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 4aa343b2e725..6fa7c685325b 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -1,5 +1,6 @@ import Foundation import WordPressShared +import WordPressMedia import Combine class ReaderDetailCoordinator { @@ -283,12 +284,10 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let imageViewController = WPImageViewController(url: url) - imageViewController.readerPost = post - imageViewController.modalTransitionStyle = .crossDissolve - imageViewController.modalPresentationStyle = .fullScreen - - viewController?.present(imageViewController, animated: true) + let image = LightboxItem(sourceURL: url, host: post.map(MediaHost.init)) + let lightboxVC = LightboxViewController(items: [image]) + lightboxVC.configureZoomTransition(sourceView: nil) + viewController?.present(lightboxVC, animated: true) } /// Open the postURL in a separated view controller From 14071169d1d7b2e7ce9ac4f0562e45201d9d51a5 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:03:07 -0500 Subject: [PATCH 004/101] Add Media support in LightboxViewController --- .../Media/ImageLoadingController.swift | 26 +++++++++++++++++++ .../LightboxImagePageViewController.swift | 17 +++++++++--- .../Media/Lightbox/LightboxItem.swift | 7 ++++- .../Lightbox/LightboxViewController.swift | 11 ++++---- .../Detail/ReaderDetailCoordinator.swift | 4 +-- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/Utility/Media/ImageLoadingController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift index a226bd4219c1..a1e0a2c41933 100644 --- a/WordPress/Classes/Utility/Media/ImageLoadingController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -6,6 +6,7 @@ import WordPressMedia @MainActor final class ImageLoadingController { var downloader: ImageDownloader = .shared + var service: MediaImageService = .shared var onStateChanged: (State) -> Void = { _ in } private(set) var task: Task? @@ -61,4 +62,29 @@ final class ImageLoadingController { } } } + + func setImage( + with media: Media, + size: MediaImageService.ImageSize + ) { + task?.cancel() + + if let image = service.getCachedThumbnail(for: .init(media), size: size) { + onStateChanged(.success(image)) + } else { + onStateChanged(.loading) + task = Task { @MainActor [service, weak self] in + do { + let image = try await service.image(for: media, size: size) + // This line guarantees that if you cancel on the main thread, + // none of the `onStateChanged` callbacks get called. + guard !Task.isCancelled else { return } + self?.onStateChanged(.success(image)) + } catch { + guard !Task.isCancelled else { return } + self?.onStateChanged(.failure(error)) + } + } + } + } } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index 34c3e8899dad..9666a6c95bf4 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -4,12 +4,12 @@ import WordPressUI final class LightboxImagePageViewController: UIViewController { private(set) var scrollView = LightboxImageScrollView() private let controller = ImageLoadingController() - private let image: LightboxItem + private let item: LightboxItem private let activityIndicator = UIActivityIndicatorView() private var errorView: UIImageView? - init(image: LightboxItem) { - self.image = image + init(item: LightboxItem) { + self.item = item super.init(nibName: nil, bundle: nil) } @@ -34,7 +34,7 @@ final class LightboxImagePageViewController: UIViewController { self?.setState($0) } - controller.setImage(with: image.sourceURL, host: image.host) + startFetching() } override func viewDidLayoutSubviews() { @@ -46,6 +46,15 @@ final class LightboxImagePageViewController: UIViewController { } } + private func startFetching() { + switch item { + case .asset(let asset): + controller.setImage(with: asset.sourceURL, host: asset.host) + case .media(let media): + controller.setImage(with: media, size: .original) + } + } + private func setState(_ state: ImageLoadingController.State) { switch state { case .loading: diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift index 9d6d58d3b251..da374bd737cd 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -1,7 +1,12 @@ import Foundation import WordPressMedia -struct LightboxItem { +enum LightboxItem { + case asset(LightboxAsset) + case media(Media) +} + +struct LightboxAsset { let sourceURL: URL var host: MediaHost? } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 1478e0ff6686..5456272b07a5 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -31,7 +31,7 @@ final class LightboxViewController: UIViewController { } private func show(_ item: LightboxItem) { - let pageVC = LightboxImagePageViewController(image: item) + let pageVC = LightboxImagePageViewController(item: item) pageVC.willMove(toParent: self) addChild(pageVC) view.addSubview(pageVC.view) @@ -87,9 +87,10 @@ final class LightboxViewController: UIViewController { /// An example of ``LightboxController`` usage. final class LightboxDemoViewController: UIViewController { - let imageView = UIImageView() - let images: [LightboxItem] = [ - LightboxItem(sourceURL: URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")!) + private let imageView = UIImageView() + private let imageURL = URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")! + private let images: [LightboxItem] = [ + .asset(LightboxAsset(sourceURL: imageURL)) ] override func viewDidLoad() { @@ -103,7 +104,7 @@ final class LightboxDemoViewController: UIViewController { ]) Task { @MainActor in - imageView.image = try? await ImageDownloader.shared.image(from: images[0].sourceURL) + imageView.image = try? await ImageDownloader.shared.image(from: imageURL) } imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 6fa7c685325b..d4faad148602 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -284,8 +284,8 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let image = LightboxItem(sourceURL: url, host: post.map(MediaHost.init)) - let lightboxVC = LightboxViewController(items: [image]) + let image = LightboxAsset(sourceURL: url, host: post.map(MediaHost.init)) + let lightboxVC = LightboxViewController(items: [.asset(image)]) lightboxVC.configureZoomTransition(sourceView: nil) viewController?.present(lightboxVC, animated: true) } From 79c49aed855c5ee32e1b831536c015ea52f019c5 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:06:27 -0500 Subject: [PATCH 005/101] Add convenience init to LightboxViewController --- .../Media/Lightbox/LightboxViewController.swift | 10 ++++++---- .../Reader/Detail/ReaderDetailCoordinator.swift | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 5456272b07a5..bdb6c72a12a9 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -8,6 +8,11 @@ final class LightboxViewController: UIViewController { private var pageVC: LightboxImagePageViewController? private var items: [LightboxItem] + convenience init(sourceURL: URL, host: MediaHost? = nil) { + let asset = LightboxAsset(sourceURL: sourceURL, host: host) + self.init(items: [.asset(asset)]) + } + init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items @@ -89,9 +94,6 @@ final class LightboxViewController: UIViewController { final class LightboxDemoViewController: UIViewController { private let imageView = UIImageView() private let imageURL = URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")! - private let images: [LightboxItem] = [ - .asset(LightboxAsset(sourceURL: imageURL)) - ] override func viewDidLoad() { super.viewDidLoad() @@ -112,7 +114,7 @@ final class LightboxDemoViewController: UIViewController { } @objc private func imageTapped() { - let lightboxVC = LightboxViewController(items: images) + let lightboxVC = LightboxViewController(sourceURL: imageURL) lightboxVC.configureZoomTransition(sourceView: imageView) present(lightboxVC, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index d4faad148602..c3188087320c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -284,8 +284,8 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let image = LightboxAsset(sourceURL: url, host: post.map(MediaHost.init)) - let lightboxVC = LightboxViewController(items: [.asset(image)]) + let host = post.map(MediaHost.init) + let lightboxVC = LightboxViewController(sourceURL: url, host: host) lightboxVC.configureZoomTransition(sourceView: nil) viewController?.present(lightboxVC, animated: true) } From 246b122965a5b0a6cee587d8057db76627da7301 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:22:23 -0500 Subject: [PATCH 006/101] Integrate LightboxViewController in SiteMedia --- .../ViewRelated/Cells/MediaItemHeaderView.swift | 2 +- .../Media/Lightbox/LightboxViewController.swift | 11 +++++++++++ .../ViewRelated/Media/MediaItemViewController.swift | 9 ++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift index 9c1d66b83e9b..6c5b67854f6a 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift @@ -4,7 +4,7 @@ import WordPressShared import WordPressMedia final class MediaItemHeaderView: UIView { - private let imageView = CachedAnimatedImageView() + let imageView = CachedAnimatedImageView() private let errorView = UIImageView() private let videoIconView = PlayIconView() private let loadingIndicator = UIActivityIndicatorView(style: .large) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index bdb6c72a12a9..e025ddca948b 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -8,11 +8,18 @@ final class LightboxViewController: UIViewController { private var pageVC: LightboxImagePageViewController? private var items: [LightboxItem] + /// A thumbnail to display during transition and for the initial image download. + var thumbnail: UIImage? + convenience init(sourceURL: URL, host: MediaHost? = nil) { let asset = LightboxAsset(sourceURL: sourceURL, host: host) self.init(items: [.asset(asset)]) } + convenience init(media: Media) { + self.init(items: [.media(media)]) + } + init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items @@ -42,6 +49,10 @@ final class LightboxViewController: UIViewController { view.addSubview(pageVC.view) pageVC.view.pinEdges() pageVC.didMove(toParent: self) + if let thumbnail { + pageVC.scrollView.configure(with: thumbnail) + self.thumbnail = nil + } self.pageVC = pageVC } diff --git a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift index 2d1e29ddb088..79cff2797f91 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift @@ -179,11 +179,10 @@ final class MediaItemViewController: UITableViewController { } private func presentImageViewControllerForMedia() { - let controller = WPImageViewController(media: self.media) - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .fullScreen - - self.present(controller, animated: true) + let controller = LightboxViewController(media: media) + controller.thumbnail = headerView.imageView.image + controller.configureZoomTransition(sourceView: headerView.imageView) + present(controller, animated: true) } private func presentVideoViewControllerForMedia() { From 6ed44545ac0bf0ff68e0cf7704cf6834ecd1c999 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:30:51 -0500 Subject: [PATCH 007/101] Integrate LightboxViewController in ReaderDetailsCoordinator (cover image) --- .../Reader/Detail/ReaderDetailCoordinator.swift | 11 ++++++----- .../WordPressTest/ReaderDetailCoordinatorTests.swift | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index c3188087320c..b342169fe6dd 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -493,11 +493,12 @@ class ReaderDetailCoordinator { guard let post, let imageURL = post.featuredImage.flatMap(URL.init) else { return } - let controller = WPImageViewController(url: imageURL) - controller.readerPost = post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .fullScreen - viewController?.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(with: post)) + MainActor.assumeIsolated { + lightboxVC.thumbnail = sender.image + } + lightboxVC.configureZoomTransition(sourceView: sender) + viewController?.present(lightboxVC, animated: true) } private func followSite(completion: @escaping () -> Void) { diff --git a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift index ea25945dabb2..e7861fc233af 100644 --- a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift +++ b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift @@ -194,7 +194,7 @@ class ReaderDetailCoordinatorTests: CoreDataTestCase { coordinator.handle(URL(string: "https://wordpress.com/image.png")!) - expect(viewMock.didCallPresentWith).to(beAKindOf(WPImageViewController.self)) + expect(viewMock.didCallPresentWith).to(beAKindOf(LightboxViewController.self)) } /// Present an URL in a new Reader Detail screen From 95399b04b7dd36403fb2d03626946a4df2122125 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 08:34:50 -0500 Subject: [PATCH 008/101] INtegrate in DefaultContentCoordinator --- WordPress/Classes/Utility/ContentCoordinator.swift | 7 +++---- .../Media/Lightbox/LightboxImagePageViewController.swift | 2 ++ .../Classes/ViewRelated/Media/Lightbox/LightboxItem.swift | 1 + .../Media/Lightbox/LightboxViewController.swift | 8 ++++++-- .../Reader/Detail/ReaderDetailCoordinator.swift | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index 27d63494accd..8da84d65c2a8 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -142,10 +142,9 @@ struct DefaultContentCoordinator: ContentCoordinator { } func displayFullscreenImage(_ image: UIImage) { - let imageViewController = WPImageViewController(image: image) - imageViewController.modalTransitionStyle = .crossDissolve - imageViewController.modalPresentationStyle = .fullScreen - controller?.present(imageViewController, animated: true) + let lightboxVC = LightboxViewController(.image(image)) + lightboxVC.configureZoomTransition() + controller?.present(lightboxVC, animated: true) } func displayPlugin(withSlug pluginSlug: String, on siteSlug: String) throws { diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index 9666a6c95bf4..f18934f37aac 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -48,6 +48,8 @@ final class LightboxImagePageViewController: UIViewController { private func startFetching() { switch item { + case .image(let image): + setState(.success(image)) case .asset(let asset): controller.setImage(with: asset.sourceURL, host: asset.host) case .media(let media): diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift index da374bd737cd..69e37075929e 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -2,6 +2,7 @@ import Foundation import WordPressMedia enum LightboxItem { + case image(UIImage) case asset(LightboxAsset) case media(Media) } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index e025ddca948b..d4fb52a3efe1 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -20,7 +20,11 @@ final class LightboxViewController: UIViewController { self.init(items: [.media(media)]) } - init(items: [LightboxItem]) { + convenience init(_ item: LightboxItem) { + self.init(items: [item]) + } + + private init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items super.init(nibName: nil, bundle: nil) @@ -91,7 +95,7 @@ final class LightboxViewController: UIViewController { } } - func configureZoomTransition(sourceView: UIView?) { + func configureZoomTransition(sourceView: UIView? = nil) { configureZoomTransition { _ in sourceView } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index b342169fe6dd..e87f08a5818a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -286,7 +286,7 @@ class ReaderDetailCoordinator { let host = post.map(MediaHost.init) let lightboxVC = LightboxViewController(sourceURL: url, host: host) - lightboxVC.configureZoomTransition(sourceView: nil) + lightboxVC.configureZoomTransition() viewController?.present(lightboxVC, animated: true) } From b3a84ac3e0f60334c31f847ea3abf53392332fad Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 08:49:37 -0500 Subject: [PATCH 009/101] Integrate LightboxViewController in Guteberg --- .../Networking/MediaHost+AbstractPost.swift | 21 ----- .../Classes/Networking/MediaHost+Blog.swift | 38 --------- .../Networking/MediaHost+Extensions.swift | 80 +++++++++++++++++++ .../Networking/MediaHost+ReaderPost.swift | 32 -------- .../Classes/Utility/Media/ImageLoader.swift | 9 +-- .../Blaze/Overlay/BlazePostPreviewView.swift | 6 +- .../RichCommentContentRenderer.swift | 2 +- .../Gutenberg/GutenbergViewController.swift | 17 +--- .../NewGutenbergViewController.swift | 17 +--- .../Pages/Views/PageListCell.swift | 4 +- .../Post/Views/PostCompactCell.swift | 6 +- .../ViewRelated/Post/Views/PostListCell.swift | 4 +- .../Detail/ReaderDetailCoordinator.swift | 2 +- .../Views/ReaderDetailFeaturedImageView.swift | 2 +- 14 files changed, 97 insertions(+), 143 deletions(-) delete mode 100644 WordPress/Classes/Networking/MediaHost+AbstractPost.swift delete mode 100644 WordPress/Classes/Networking/MediaHost+Blog.swift create mode 100644 WordPress/Classes/Networking/MediaHost+Extensions.swift delete mode 100644 WordPress/Classes/Networking/MediaHost+ReaderPost.swift diff --git a/WordPress/Classes/Networking/MediaHost+AbstractPost.swift b/WordPress/Classes/Networking/MediaHost+AbstractPost.swift deleted file mode 100644 index d9a9b41a3d1f..000000000000 --- a/WordPress/Classes/Networking/MediaHost+AbstractPost.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `AbstractPost`. -/// -extension MediaHost { - enum AbstractPostError: Swift.Error { - case baseInitializerError(error: BlogError) - } - - init(with post: AbstractPost, failure: (AbstractPostError) -> ()) { - self.init( - with: post.blog, - failure: { error in - // We just associate a post with the underlying error for simpler debugging. - failure(AbstractPostError.baseInitializerError(error: error)) - } - ) - } -} diff --git a/WordPress/Classes/Networking/MediaHost+Blog.swift b/WordPress/Classes/Networking/MediaHost+Blog.swift deleted file mode 100644 index a1e1411b0ed9..000000000000 --- a/WordPress/Classes/Networking/MediaHost+Blog.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { - enum BlogError: Swift.Error { - case baseInitializerError(error: Error) - } - - init(with blog: Blog) { - self.init(with: blog) { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - } - } - - init(with blog: Blog, failure: (BlogError) -> ()) { - let isAtomic = blog.isAtomic() - self.init(with: blog, isAtomic: isAtomic, failure: failure) - } - - init(with blog: Blog, isAtomic: Bool, failure: (BlogError) -> ()) { - self.init( - isAccessibleThroughWPCom: blog.isAccessibleThroughWPCom(), - isPrivate: blog.isPrivate(), - isAtomic: isAtomic, - siteID: blog.dotComID?.intValue, - username: blog.usernameForSite, - authToken: blog.authToken, - failure: { error in - // We just associate a blog with the underlying error for simpler debugging. - failure(BlogError.baseInitializerError(error: error)) - } - ) - } -} diff --git a/WordPress/Classes/Networking/MediaHost+Extensions.swift b/WordPress/Classes/Networking/MediaHost+Extensions.swift new file mode 100644 index 000000000000..ef6a44815f0d --- /dev/null +++ b/WordPress/Classes/Networking/MediaHost+Extensions.swift @@ -0,0 +1,80 @@ +import Foundation +import WordPressMedia + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `AbstractPost`. +/// +extension MediaHost { + init(_ post: AbstractPost) { + self.init(with: post.blog, failure: { error in + // We just associate a post with the underlying error for simpler debugging. + WordPressAppDelegate.crashLogging?.logError(error) + }) + } +} + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `Blog`. +/// +extension MediaHost { + enum BlogError: Swift.Error { + case baseInitializerError(error: Error) + } + + init(with blog: Blog) { + self.init(with: blog) { error in + // We'll log the error, so we know it's there, but we won't halt execution. + WordPressAppDelegate.crashLogging?.logError(error) + } + } + + init(with blog: Blog, failure: (BlogError) -> ()) { + let isAtomic = blog.isAtomic() + self.init(with: blog, isAtomic: isAtomic, failure: failure) + } + + init(with blog: Blog, isAtomic: Bool, failure: (BlogError) -> ()) { + self.init( + isAccessibleThroughWPCom: blog.isAccessibleThroughWPCom(), + isPrivate: blog.isPrivate(), + isAtomic: isAtomic, + siteID: blog.dotComID?.intValue, + username: blog.usernameForSite, + authToken: blog.authToken, + failure: { error in + // We just associate a blog with the underlying error for simpler debugging. + failure(BlogError.baseInitializerError(error: error)) + } + ) + } +} + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `Blog`. +/// +extension MediaHost { + init(_ post: ReaderPost) { + let isAccessibleThroughWPCom = post.isWPCom || post.isJetpack + + // This is the only way in which we can obtain the username and authToken here. + // It'd be nice if all data was associated with an account instead, for transparency + // and cleanliness of the code - but this'll have to do for now. + + // We allow a nil account in case the user connected only self-hosted sites. + let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) + let username = account?.username + let authToken = account?.authToken + + self.init( + isAccessibleThroughWPCom: isAccessibleThroughWPCom, + isPrivate: post.isBlogPrivate, + isAtomic: post.isBlogAtomic, + siteID: post.siteID?.intValue, + username: username, + authToken: authToken, + failure: { error in + WordPressAppDelegate.crashLogging?.logError(error) + } + ) + } +} diff --git a/WordPress/Classes/Networking/MediaHost+ReaderPost.swift b/WordPress/Classes/Networking/MediaHost+ReaderPost.swift deleted file mode 100644 index 70be952500ec..000000000000 --- a/WordPress/Classes/Networking/MediaHost+ReaderPost.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { - init(with post: ReaderPost) { - let isAccessibleThroughWPCom = post.isWPCom || post.isJetpack - - // This is the only way in which we can obtain the username and authToken here. - // It'd be nice if all data was associated with an account instead, for transparency - // and cleanliness of the code - but this'll have to do for now. - - // We allow a nil account in case the user connected only self-hosted sites. - let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) - let username = account?.username - let authToken = account?.authToken - - self.init( - isAccessibleThroughWPCom: isAccessibleThroughWPCom, - isPrivate: post.isBlogPrivate, - isAtomic: post.isBlogAtomic, - siteID: post.siteID?.intValue, - username: username, - authToken: authToken, - failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - } - ) - } -} diff --git a/WordPress/Classes/Utility/Media/ImageLoader.swift b/WordPress/Classes/Utility/Media/ImageLoader.swift index 09c65e8e208a..edfbfe771365 100644 --- a/WordPress/Classes/Utility/Media/ImageLoader.swift +++ b/WordPress/Classes/Utility/Media/ImageLoader.swift @@ -83,18 +83,13 @@ import WordPressMedia @objc(loadImageWithURL:fromPost:preferredSize:placeholder:success:error:) func loadImage(with url: URL, from post: AbstractPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - let host = MediaHost(with: post, failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - }) - + let host = MediaHost(post) loadImage(with: url, from: host, preferredSize: size, placeholder: placeholder, success: success, error: error) } @objc(loadImageWithURL:fromReaderPost:preferredSize:placeholder:success:error:) func loadImage(with url: URL, from readerPost: ReaderPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - - let host = MediaHost(with: readerPost) - loadImage(with: url, from: host, preferredSize: size, placeholder: placeholder, success: success, error: error) + loadImage(with: url, from: MediaHost(readerPost), preferredSize: size, placeholder: placeholder, success: success, error: error) } /// Load an image from a specific post, using the given URL. Supports animated images (gifs) as well. diff --git a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift index 3cdbd57745c3..c84e478be3a7 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift @@ -96,13 +96,9 @@ final class BlazePostPreviewView: UIView { if let url = post.featuredImageURL { featuredImageView.isHidden = false - let host = MediaHost(with: post, failure: { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - }) let preferredSize = CGSize(width: featuredImageView.frame.width, height: featuredImageView.frame.height) .scaled(by: UITraitCollection.current.displayScale) - featuredImageView.setImage(with: url, host: host, size: preferredSize) + featuredImageView.setImage(with: url, host: MediaHost(post), size: preferredSize) } else { featuredImageView.isHidden = true diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift index 11bdb6bae598..51f042e12672 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift @@ -79,7 +79,7 @@ private extension RichCommentContentRenderer { WordPressAppDelegate.crashLogging?.logError(error) }) } else if let post = comment.post as? ReaderPost, post.isBlogPrivate { - return MediaHost(with: post) + return MediaHost(post) } return .publicSite diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 8d886b989925..758bf6f4abff 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia import Gutenberg import Aztec import WordPressFlux @@ -897,19 +898,9 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } func gutenbergDidRequestImagePreview(with fullSizeUrl: URL, thumbUrl: URL?) { - navigationController?.definesPresentationContext = true - - let controller: WPImageViewController - if let image = AnimatedImageCache.shared.cachedStaticImage(url: fullSizeUrl) { - controller = WPImageViewController(image: image) - } else { - controller = WPImageViewController(externalMediaURL: fullSizeUrl) - } - - controller.post = self.post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .overCurrentContext - self.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: fullSizeUrl, host: MediaHost(post)) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) } func gutenbergDidRequestUnsupportedBlockFallback(for block: Block) { diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index e55b31d38e84..8b8516e62d7c 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia import AutomatticTracks import GutenbergKit import SafariServices @@ -452,19 +453,9 @@ extension NewGutenbergViewController { // TODO: are we going to show this natively? func gutenbergDidRequestImagePreview(with fullSizeUrl: URL, thumbUrl: URL?) { - navigationController?.definesPresentationContext = true - - let controller: WPImageViewController - if let image = AnimatedImageCache.shared.cachedStaticImage(url: fullSizeUrl) { - controller = WPImageViewController(image: image) - } else { - controller = WPImageViewController(externalMediaURL: fullSizeUrl) - } - - controller.post = self.post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .overCurrentContext - self.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: fullSizeUrl, host: MediaHost(post)) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) } // TODO: reimplement diff --git a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift index da0e1fda2c86..7d6534431d1c 100644 --- a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift +++ b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift @@ -62,9 +62,7 @@ final class PageListCell: UITableViewCell, AbstractPostListCell, PostSearchResul featuredImageView.isHidden = viewModel.imageURL == nil if let imageURL = viewModel.imageURL { - let host = MediaHost(with: viewModel.page) { error in - WordPressAppDelegate.crashLogging?.logError(error) - } + let host = MediaHost(viewModel.page) let thumbnailURL = MediaImageService.getResizedImageURL(for: imageURL, blog: viewModel.page.blog, size: Constants.imageSize.scaled(by: UIScreen.main.scale)) featuredImageView.setImage(with: thumbnailURL, host: host) } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift index 5d5229ffc34f..0dbfe52398c5 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift @@ -78,11 +78,7 @@ final class PostCompactCell: UITableViewCell, Reusable { if let post, let url = post.featuredImageURL { featuredImageView.isHidden = false - let host = MediaHost(with: post, failure: { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - }) - + let host = MediaHost(post) let targetSize = Constants.imageSize.scaled(by: traitCollection.displayScale) featuredImageView.setImage(with: url, host: host, size: targetSize) } else { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift index 6d68e35f1264..8ec4cb3c89dd 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift @@ -75,9 +75,7 @@ final class PostListCell: UITableViewCell, AbstractPostListCell, PostSearchResul featuredImageView.isHidden = viewModel.imageURL == nil featuredImageView.layer.opacity = viewModel.syncStateViewModel.isEditable ? 1 : 0.25 if let imageURL = viewModel.imageURL { - let host = MediaHost(with: viewModel.post) { error in - WordPressAppDelegate.crashLogging?.logError(error) - } + let host = MediaHost(viewModel.post) let thumbnailURL = MediaImageService.getResizedImageURL(for: imageURL, blog: viewModel.post.blog, size: Constants.imageSize.scaled(by: UIScreen.main.scale)) featuredImageView.setImage(with: thumbnailURL, host: host) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index e87f08a5818a..c037dfef9f3a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -493,7 +493,7 @@ class ReaderDetailCoordinator { guard let post, let imageURL = post.featuredImage.flatMap(URL.init) else { return } - let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(with: post)) + let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(post)) MainActor.assumeIsolated { lightboxVC.thumbnail = sender.image } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 05ea92bd8e68..063a836debc5 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -223,7 +223,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { completionHandler(CGSize(width: 1000, height: 1000 * ReaderPostCell.coverAspectRatio)) } - imageView.setImage(with: imageURL, host: MediaHost(with: post)) { [weak self] result in + imageView.setImage(with: imageURL, host: MediaHost(post)) { [weak self] result in guard let self else { return } switch result { case .success: From 8fbeb6696e94e8c6ec29a2c720a0e41480cde268 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 09:01:25 -0500 Subject: [PATCH 010/101] Integrate LightboxViewController in ExternalMediaPickerViewController --- .../ExternalMediaPickerViewController.swift | 1 + .../Lightbox/LightboxViewController.swift | 19 +++++++++++---- .../Preview/MediaPreviewController.swift | 23 +++++++++---------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift index f57cefc59aa4..62ad231900e3 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift @@ -222,6 +222,7 @@ final class ExternalMediaPickerViewController: UIViewController, UICollectionVie let viewController = MediaPreviewController() viewController.dataSource = self let navigation = UINavigationController(rootViewController: viewController) + navigation.modalPresentationStyle = .fullScreen present(navigation, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index d4fb52a3efe1..3904df25c070 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -11,6 +11,13 @@ final class LightboxViewController: UIViewController { /// A thumbnail to display during transition and for the initial image download. var thumbnail: UIImage? + var configuration: Configuration + + struct Configuration { + var backgroundColor: UIColor = .black + var showsCloseButton = true + } + convenience init(sourceURL: URL, host: MediaHost? = nil) { let asset = LightboxAsset(sourceURL: sourceURL, host: host) self.init(items: [.asset(asset)]) @@ -20,13 +27,14 @@ final class LightboxViewController: UIViewController { self.init(items: [.media(media)]) } - convenience init(_ item: LightboxItem) { + convenience init(_ item: LightboxItem, configuration: Configuration = .init()) { self.init(items: [item]) } - private init(items: [LightboxItem]) { + private init(items: [LightboxItem], configuration: Configuration = .init()) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items + self.configuration = configuration super.init(nibName: nil, bundle: nil) } @@ -37,13 +45,14 @@ final class LightboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .black + view.backgroundColor = configuration.backgroundColor if let item = items.first { show(item) } - - addCloseButton() + if configuration.showsCloseButton { + addCloseButton() + } } private func show(_ item: LightboxItem) { diff --git a/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift index 6407c281c991..294ea77c9e06 100644 --- a/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift @@ -22,6 +22,8 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground + configureNavigationItems() configurePageViewController() updateNavigationForCurrentViewController() @@ -52,26 +54,27 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo } } - private func makePageViewController(at index: Int) -> MediaPreviewItemViewController? { + private func makePageViewController(at index: Int) -> LightboxViewController? { guard index >= 0 && index < numberOfItems, let item = dataSource?.previewController(self, previewItemAt: index) else { return nil } - let viewController = MediaPreviewItemViewController(externalMediaURL: item.url) - viewController.shouldDismissWithGestures = false - viewController.index = index + let viewController = LightboxViewController(sourceURL: item.url) + viewController.configuration.showsCloseButton = false + viewController.configuration.backgroundColor = .systemBackground + viewController.view.tag = index return viewController } // MARK: - UIPageViewControllerDataSource func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - let index = (viewController as! MediaPreviewItemViewController).index + let index = (viewController as! LightboxViewController).view.tag return makePageViewController(at: index - 1) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - let index = (viewController as! MediaPreviewItemViewController).index + let index = (viewController as! LightboxViewController).view.tag return makePageViewController(at: index + 1) } @@ -82,17 +85,13 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo } private func updateNavigationForCurrentViewController() { - guard let viewController = pageViewController.viewControllers?.first as? MediaPreviewItemViewController else { + guard let viewController = pageViewController.viewControllers?.first as? LightboxViewController else { return } - navigationItem.title = String(format: Strings.title, String(viewController.index + 1), String(numberOfItems)) + navigationItem.title = String(format: Strings.title, String(viewController.view.tag + 1), String(numberOfItems)) } } -private final class MediaPreviewItemViewController: WPImageViewController { - var index = 0 -} - private enum Strings { static let title = NSLocalizedString("mediaPreview.NofM", value: "%@ of %@", comment: "Navigation title for media preview. Example: 1 of 3") } From 8f7dd1653e039ceb146ee76ce5e9e15ad2cd2077 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:07:08 -0500 Subject: [PATCH 011/101] Integrate LightboxViewController in PostSettingsViewController (featured image) --- .../PostSettingsViewController+Swift.swift | 39 +++++++++++++++++++ .../Post/PostSettingsViewController.m | 25 +++++------- .../PostSettingsViewController_Internal.h | 2 + 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index b781321cfb0f..6cd78ab87e76 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -251,6 +251,45 @@ extension PostSettingsViewController { } } +// MARK: - PostSettingsViewController (Featued Image) + +extension PostSettingsViewController { + @objc func showFeaturedImageSelector() { + guard let featuredImage = apost.featuredImage else { + return wpAssertionFailure("featured image missing") + } + + let lightboxVC = LightboxViewController(media: featuredImage) + lightboxVC.configuration.backgroundColor = .systemBackground + lightboxVC.configuration.showsCloseButton = false + lightboxVC.edgesForExtendedLayout = [] + + lightboxVC.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction { [weak self] _ in + self?.dismiss(animated: true) + }) + + lightboxVC.toolbarItems = [ + UIBarButtonItem(title: SharedStrings.Button.remove, image: UIImage(systemName: "trash"), target: self, action: #selector(buttonRemoveFeaturedImageTapped)) + ] + + let navigationVC = UINavigationController(rootViewController: lightboxVC) + navigationVC.isToolbarHidden = false + navigationVC.view.backgroundColor = .systemBackground + self.present(navigationVC, animated: true) + } + + @objc private func buttonRemoveFeaturedImageTapped(_ sender: UIBarButtonItem) { + let alert = UIAlertController(title: Strings.confirmFeaturedImageRemoval, message: nil, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: SharedStrings.Button.cancel, style: .cancel)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.remove, style: .destructive, handler: { [weak self] _ in + self?.removeFeaturedImage() + })) + alert.popoverPresentationController?.sourceItem = sender + (presentedViewController ?? self).present(alert, animated: true) + } +} + private enum Strings { + static let confirmFeaturedImageRemoval = NSLocalizedString("postSettings.confirmFeaturedImageRemovalAlert.title", value: "Remove this Featured Image?", comment: "Prompt when removing a featured image from a post") static let warningPostWillBePublishedAlertMessage = NSLocalizedString("postSettings.warningPostWillBePublishedAlertMessage", value: "By changing the visibility to 'Private', the post will be published immediately", comment: "An alert message explaning that by changing the visibility to private, the post will be published immediately to your site") } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 24aeecebf6af..91ab80619591 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1030,21 +1030,6 @@ - (void)showEditShareMessageController [self.navigationController pushViewController:vc animated:YES]; } -- (void)showFeaturedImageSelector -{ - if (self.apost.featuredImage && self.featuredImage) { - FeaturedImageViewController *featuredImageVC; - if (self.animatedFeaturedImageData) { - featuredImageVC = [[FeaturedImageViewController alloc] initWithGifData:self.animatedFeaturedImageData]; - } else { - featuredImageVC = [[FeaturedImageViewController alloc] initWithImage:self.featuredImage]; - } - featuredImageVC.delegate = self; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:featuredImageVC]; - [self presentViewController:navigationController animated:YES completion:nil]; - } -} - - (void)showEditSlugController { SettingsMultiTextViewController *vc = [[SettingsMultiTextViewController alloc] initWithText:self.apost.slugForDisplay @@ -1238,4 +1223,14 @@ - (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageView [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; } +- (void)removeFeaturedImage { + [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; + self.featuredImage = nil; + self.animatedFeaturedImageData = nil; + [self.apost setFeaturedImage:nil]; + [self dismissViewControllerAnimated:YES completion:nil]; + [self.tableView reloadData]; + [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; +} + @end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h index d6ed3545b9cc..25cd07f874ba 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -28,4 +28,6 @@ typedef enum { @property (nullable, nonatomic, strong) WPProgressTableViewCell *progressCell; +- (void)removeFeaturedImage; + @end From 8096de955f05b517f1b0ffed87bcc37a5a8ac2d3 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:08:39 -0500 Subject: [PATCH 012/101] Remove FeaturedImageViewController (ObjC) --- .../Post/FeaturedImageViewController.h | 13 -- .../Post/FeaturedImageViewController.m | 127 ------------------ .../Post/PostSettingsViewController.m | 17 +-- 3 files changed, 2 insertions(+), 155 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m diff --git a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h b/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h deleted file mode 100644 index c7d4133fd7b2..000000000000 --- a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -#import "WPImageViewController.h" - -@class FeaturedImageViewController; - -@protocol FeaturedImageViewControllerDelegate -- (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageViewController *)controller; -@end - -@interface FeaturedImageViewController : WPImageViewController - -@property (weak, nonatomic) id delegate; - -@end diff --git a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m b/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m deleted file mode 100644 index 7bbf9dcabca0..000000000000 --- a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m +++ /dev/null @@ -1,127 +0,0 @@ -#import "FeaturedImageViewController.h" - -#import "Media.h" -#import "WordPress-Swift.h" - - -@interface FeaturedImageViewController () - -@property (nonatomic, strong) NSURL *url; -@property (nonatomic, strong) UIImage *image; - -@property (nonatomic, strong) UIBarButtonItem *doneButton; -@property (nonatomic, strong) UIBarButtonItem *removeButton; - -@end - -@implementation FeaturedImageViewController - -@dynamic url; -@dynamic image; - -#pragma mark - Life Cycle Methods - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.title = NSLocalizedString(@"Featured Image", @"Title for the Featured Image view"); - self.view.backgroundColor = [UIColor murielBasicBackground]; - self.navigationItem.leftBarButtonItems = @[self.doneButton]; - self.navigationItem.rightBarButtonItems = @[self.removeButton]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Super class will hide the status bar by default - [self hideBars:NO animated:NO]; - - // Called here to be sure the view is complete in case we need to present a popover from the toolbar. - [self loadImage]; -} - -#pragma mark - Appearance Related Methods - -- (UIBarButtonItem *)doneButton -{ - if (!_doneButton) { - _doneButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", @"Label for confirm feature image of a post") - style:UIBarButtonItemStylePlain - target:self - action:@selector(confirmFeaturedImage)]; - } - return _doneButton; -} - -- (UIBarButtonItem *)removeButton -{ - if (!_removeButton) { - UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Remove", @"Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post.") - style:UIBarButtonItemStylePlain - target:self - action:@selector(removeFeaturedImage)]; - NSString *title = NSLocalizedString(@"Remove Featured Image", @"Accessibility Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post."); - button.accessibilityLabel = title; - button.accessibilityIdentifier = @"Remove Featured Image"; - _removeButton = button; - } - - return _removeButton; -} - - -- (void)hideBars:(BOOL)hide animated:(BOOL)animated -{ - [super hideBars:hide animated:animated]; - - if (self.navigationController.navigationBarHidden != hide) { - [self.navigationController setNavigationBarHidden:hide animated:animated]; - } - - [self centerImage]; - [UIView animateWithDuration:0.3 animations:^{ - if (hide) { - self.view.backgroundColor = [UIColor blackColor]; - } else { - self.view.backgroundColor = [UIColor murielBasicBackground]; - } - }]; -} - -#pragma mark - Action Methods - -- (void)handleImageTapped:(UITapGestureRecognizer *)tgr -{ - BOOL hide = !self.navigationController.navigationBarHidden; - [self hideBars:hide animated:YES]; -} - -- (void)removeFeaturedImage -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Remove this Featured Image?", @"Prompt when removing a featured image from a post") - message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - [alertController addActionWithTitle:NSLocalizedString(@"Cancel", "Cancel a prompt") - style:UIAlertActionStyleCancel - handler:nil]; - [alertController addActionWithTitle:NSLocalizedString(@"Remove", @"Remove an image/posts/etc") - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * __unused alertAction) { - if (self.delegate) { - [self.delegate FeaturedImageViewControllerOnRemoveImageButtonPressed:self]; - } - }]; - alertController.popoverPresentationController.barButtonItem = self.removeButton; - [self presentViewController:alertController animated:YES completion:nil]; - -} - -- (void)confirmFeaturedImage -{ - [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; -} - - -@end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 91ab80619591..afc9825e1b19 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1,6 +1,5 @@ #import "PostSettingsViewController.h" #import "PostSettingsViewController_Internal.h" -#import "FeaturedImageViewController.h" #import "Media.h" #import "PostFeaturedImageCell.h" #import "SettingsSelectionViewController.h" @@ -53,8 +52,7 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { @interface PostSettingsViewController () +PostCategoriesViewControllerDelegate, PostFeaturedImageCellDelegate> @property (nonatomic, strong) AbstractPost *apost; @property (nonatomic, strong) NSArray *postMetaSectionRows; @@ -1210,18 +1208,7 @@ - (void)updateFeaturedImageCell:(PostFeaturedImageCell *)cell [self.tableView reloadSections:featuredImageSectionSet withRowAnimation:UITableViewRowAnimationNone]; } -#pragma mark - FeaturedImageViewControllerDelegate - -- (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageViewController *)controller -{ - [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; - self.featuredImage = nil; - self.animatedFeaturedImageData = nil; - [self.apost setFeaturedImage:nil]; - [self dismissViewControllerAnimated:YES completion:nil]; - [self.tableView reloadData]; - [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; -} +#pragma mark - Featured Image - (void)removeFeaturedImage { [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; From 88c7b13b2d76f81b4cf3eee067e1319d48f99b76 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:38:29 -0500 Subject: [PATCH 013/101] Rewrite PostFeaturedImageCell --- .../ViewRelated/Cells/PostFeaturedImageCell.h | 21 ---- .../ViewRelated/Cells/PostFeaturedImageCell.m | 96 ------------------- .../Cells/PostFeaturedImageCell.swift | 27 ++++++ .../Post/PostSettingsViewController.m | 53 +--------- 4 files changed, 30 insertions(+), 167 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h delete mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m create mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h deleted file mode 100644 index ccbca15a7e47..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h +++ /dev/null @@ -1,21 +0,0 @@ -@import WordPressShared; - -@class AbstractPost; -@class PostFeaturedImageCell; - -@protocol PostFeaturedImageCellDelegate -- (void)postFeatureImageCellDidFinishLoadingImage:(nonnull PostFeaturedImageCell *)cell; -- (void)postFeatureImageCell:(nonnull PostFeaturedImageCell *)cell didFinishLoadingAnimatedImageWithData:(nullable NSData *)animationData; -- (void)postFeatureImageCell:(nonnull PostFeaturedImageCell *)cell didFinishLoadingImageWithError:(nullable NSError *)error; -@end - -@interface PostFeaturedImageCell : WPTableViewCell - -extern CGFloat const PostFeaturedImageCellMargin; - -@property (weak, nonatomic, nullable) id delegate; -@property (strong, nonatomic, readonly, nullable) UIImage *image; - -- (void)setImageWithURL:(nonnull NSURL *)url inPost:(nonnull AbstractPost *)post withSize:(CGSize)size; - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m deleted file mode 100644 index 40331ca999b7..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m +++ /dev/null @@ -1,96 +0,0 @@ -#import "PostFeaturedImageCell.h" -#import "WordPress-Swift.h" - -CGFloat const PostFeaturedImageCellMargin = 15.0f; - -@interface PostFeaturedImageCell () - -@property (nonatomic, strong) CachedAnimatedImageView *featuredImageView; -@property (nonatomic, strong) ImageLoader *imageLoader; - -@end - -@implementation PostFeaturedImageCell - -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; - if (self) { - [self setup]; - } - return self; -} - -- (void)setup -{ - [self layoutImageView]; - _imageLoader = [[ImageLoader alloc] initWithImageView:self.featuredImageView gifStrategy:GIFStrategyLargeGIFs]; - self.accessibilityLabel = NSLocalizedString(@"A featured image is set. Tap to change it.", @"Label for image that is set as a feature image for post/page"); - self.accessibilityIdentifier = @"CurrentFeaturedImage"; -} - -- (void)setImageWithURL:(NSURL *)url inPost:(AbstractPost *)post withSize:(CGSize)size -{ - __weak PostFeaturedImageCell *weakSelf = self; - [self.imageLoader loadImageWithURL:url fromPost:post preferredSize:size placeholder:nil success:^{ - [weakSelf informDelegateImageLoaded]; - } error:^(NSError * _Nullable error) { - if (weakSelf && weakSelf.delegate) { - [weakSelf.delegate postFeatureImageCell:weakSelf didFinishLoadingImageWithError:error]; - } - }]; -} - -- (void)informDelegateImageLoaded -{ - if (self.delegate == nil) { - return; - } - - if (self.featuredImageView.animatedGifData) { - [self.delegate postFeatureImageCell:self didFinishLoadingAnimatedImageWithData:self.featuredImageView.animatedGifData]; - } else { - [self.delegate postFeatureImageCellDidFinishLoadingImage:self]; - } -} - -- (UIImage *)image -{ - return self.featuredImageView.image; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - [self.featuredImageView prepForReuse]; -} - -#pragma mark - Helpers - -- (CachedAnimatedImageView *)featuredImageView -{ - if (!_featuredImageView) { - _featuredImageView = [[CachedAnimatedImageView alloc] init]; - _featuredImageView.contentMode = UIViewContentModeScaleAspectFill; - _featuredImageView.clipsToBounds = YES; - _featuredImageView.translatesAutoresizingMaskIntoConstraints = NO; - } - - return _featuredImageView; -} - -- (void)layoutImageView -{ - UIView *imageView = self.featuredImageView; - - [self.contentView addSubview:imageView]; - UILayoutGuide *readableGuide = self.contentView.readableContentGuide; - [NSLayoutConstraint activateConstraints:@[ - [imageView.leadingAnchor constraintEqualToAnchor:readableGuide.leadingAnchor], - [imageView.trailingAnchor constraintEqualToAnchor:readableGuide.trailingAnchor], - [imageView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:PostFeaturedImageCellMargin], - [imageView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-PostFeaturedImageCellMargin] - ]]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift new file mode 100644 index 000000000000..461d23fd669f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift @@ -0,0 +1,27 @@ +import UIKit +import WordPressUI +import WordPressMedia + +final class PostFeaturedImageCell: UITableViewCell { + let featuredImageView = AsyncImageView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + featuredImageView.configuration.loadingStyle = .spinner + + contentView.addSubview(featuredImageView) + featuredImageView.pinEdges() + NSLayoutConstraint.activate([ + featuredImageView.heightAnchor.constraint(equalTo: featuredImageView.widthAnchor, multiplier: ReaderPostCell.coverAspectRatio) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func setImage(withURL url: URL, post: AbstractPost) { + featuredImageView.setImage(with: url, host: MediaHost(post)) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index afc9825e1b19..711d82dbce99 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1,7 +1,6 @@ #import "PostSettingsViewController.h" #import "PostSettingsViewController_Internal.h" #import "Media.h" -#import "PostFeaturedImageCell.h" #import "SettingsSelectionViewController.h" #import "SharingDetailViewController.h" #import "WPTableViewActivityCell.h" @@ -40,7 +39,6 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { }; static CGFloat CellHeight = 44.0f; -static CGFloat LoadingIndicatorHeight = 28.0f; static NSString *const PostSettingsAnalyticsTrackingSource = @"post_settings"; static NSString *const TableViewActivityCellIdentifier = @"TableViewActivityCellIdentifier"; @@ -52,13 +50,12 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { @interface PostSettingsViewController () +PostCategoriesViewControllerDelegate> @property (nonatomic, strong) AbstractPost *apost; @property (nonatomic, strong) NSArray *postMetaSectionRows; @property (nonatomic, strong) NSArray *formatsList; @property (nonatomic, strong) UIImage *featuredImage; -@property (nonatomic, strong) NSData *animatedFeaturedImageData; @property (nonatomic, readonly) CGSize featuredImageSize; @@ -443,11 +440,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa if (sectionId == PostSettingsSectionFeaturedImage) { if ([self isUploadingMedia]) { - return CellHeight + (2.f * PostFeaturedImageCellMargin); - } else if (self.featuredImage) { - return self.featuredImageSize.height + 2.f * PostFeaturedImageCellMargin; - } else { - return LoadingIndicatorHeight + 2.f * PostFeaturedImageCellMargin; + return CellHeight; } } @@ -717,10 +710,7 @@ - (UITableViewCell *)cellForFeaturedImageUploadProgressAtIndexPath:(NSIndexPath - (UITableViewCell *)cellForFeaturedImageWithURL:(nonnull NSURL *)featuredURL atIndexPath:(NSIndexPath *)indexPath { PostFeaturedImageCell *featuredImageCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; - featuredImageCell.delegate = self; - [WPStyleGuide configureTableViewCell:featuredImageCell]; - - [featuredImageCell setImageWithURL:featuredURL inPost:self.apost withSize:self.featuredImageSize]; + [featuredImageCell setImageWithURL:featuredURL post:self.apost]; featuredImageCell.tag = PostSettingsRowFeaturedImage; return featuredImageCell; } @@ -1090,7 +1080,6 @@ - (void)showTagsPicker - (CGSize)featuredImageSize { CGFloat width = CGRectGetWidth(self.view.frame); - width = width - (PostFeaturedImageCellMargin * 2); // left and right cell margins CGFloat height = ceilf(width * 0.66); return CGSizeMake(width, height); } @@ -1173,47 +1162,11 @@ - (void)postCategoriesViewController:(PostCategoriesViewController *)controller } } -#pragma mark - PostFeaturedImageCellDelegate - -- (void)postFeatureImageCell:(PostFeaturedImageCell *)cell didFinishLoadingAnimatedImageWithData:(NSData *)animationData -{ - if (self.animatedFeaturedImageData == nil) { - self.animatedFeaturedImageData = animationData; - [self updateFeaturedImageCell:cell]; - } -} - -- (void)postFeatureImageCellDidFinishLoadingImage:(PostFeaturedImageCell *)cell -{ - self.animatedFeaturedImageData = nil; - if (!self.featuredImage) { - [self updateFeaturedImageCell:cell]; - } -} - -- (void)postFeatureImageCell:(PostFeaturedImageCell *)cell didFinishLoadingImageWithError:(NSError *)error -{ - self.featuredImage = nil; - if (error) { - NSIndexPath *featureImageCellPath = [NSIndexPath indexPathForRow:0 inSection:[self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]]; - [self featuredImageFailedLoading:featureImageCellPath withError:error]; - } -} - -- (void)updateFeaturedImageCell:(PostFeaturedImageCell *)cell -{ - self.featuredImage = cell.image; - NSInteger featuredImageSection = [self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]; - NSIndexSet *featuredImageSectionSet = [NSIndexSet indexSetWithIndex:featuredImageSection]; - [self.tableView reloadSections:featuredImageSectionSet withRowAnimation:UITableViewRowAnimationNone]; -} - #pragma mark - Featured Image - (void)removeFeaturedImage { [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; self.featuredImage = nil; - self.animatedFeaturedImageData = nil; [self.apost setFeaturedImage:nil]; [self dismissViewControllerAnimated:YES completion:nil]; [self.tableView reloadData]; From da7b29a063f7515d2f75204b4a9a0129413b2a11 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:01:25 -0500 Subject: [PATCH 014/101] Integrate LightboxViewController in ReaderCommentsViewController --- .../Comments/ReaderCommentsViewController.m | 39 +------------------ .../ReaderCommentsViewController.swift | 28 ++++++++++++- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m index d7b27018dace..2caa6da68a6d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m @@ -5,7 +5,6 @@ #import "ReaderPost.h" #import "ReaderPostService.h" #import "UIView+Subviews.h" -#import "WPImageViewController.h" #import "WPTableViewHandler.h" #import "SuggestionsTableView.h" #import "WordPress-Swift.h" @@ -1269,30 +1268,12 @@ - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRan - (void)richContentView:(WPRichContentView *)richContentView didReceiveImageAction:(WPRichTextImage *)image { - UIViewController *controller = nil; - BOOL isSupportedNatively = [WPImageViewController isUrlSupported:image.linkURL]; - - if (image.imageView.animatedGifData) { - controller = [[WPImageViewController alloc] initWithGifData:image.imageView.animatedGifData]; - } else if (isSupportedNatively) { - controller = [[WPImageViewController alloc] initWithImage:image.imageView.image andURL:image.linkURL]; - } else if (image.linkURL) { - [self presentWebViewControllerWithURL:image.linkURL]; - return; - } else if (image.imageView.image) { - controller = [[WPImageViewController alloc] initWithImage:image.imageView.image]; - } - - if (controller) { - controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - controller.modalPresentationStyle = UIModalPresentationFullScreen; - [self presentViewController:controller animated:YES completion:nil]; - } + [self showFullScreenImage:image from:richContentView]; } - (void)interactWithURL:(NSURL *)URL { - [self presentWebViewControllerWithURL:URL]; + [self presentWebViewControllerWith:URL]; } - (BOOL)richContentViewShouldUpdateLayoutForAttachments:(WPRichContentView *)richContentView @@ -1310,22 +1291,6 @@ - (void)richContentViewDidUpdateLayoutForAttachments:(WPRichContentView *)richCo [self updateTableViewForAttachments]; } -- (void)presentWebViewControllerWithURL:(NSURL *)URL -{ - NSURL *linkURL = URL; - NSURLComponents *components = [NSURLComponents componentsWithString:[URL absoluteString]]; - if (!components.host) { - linkURL = [components URLRelativeToURL:[NSURL URLWithString:self.post.blogURL]]; - } - - WebViewControllerConfiguration *configuration = [[WebViewControllerConfiguration alloc] initWithUrl:linkURL]; - [configuration authenticateWithDefaultAccount]; - [configuration setAddsWPComReferrer:YES]; - UIViewController *webViewController = [WebViewControllerFactory controllerWithConfiguration:configuration source:@"reader_comments"]; - UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; - [self presentViewController:navController animated:YES completion:nil]; -} - - (void)textViewDidChangeSelection:(UITextView *)textView { if (!textView.selectedTextRange.isEmpty) { diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift index 53fe1a417810..5cd7277542ad 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift @@ -8,7 +8,7 @@ extension NSNotification.Name { static let ReaderCommentModifiedNotification = NSNotification.Name(rawValue: "ReaderCommentModifiedNotification") } -@objc public extension ReaderCommentsViewController { +@objc extension ReaderCommentsViewController { func shouldShowSuggestions(for siteID: NSNumber?) -> Bool { guard let siteID, let blog = Blog.lookup(withID: siteID, in: ContextManager.shared.mainContext) else { return false } return SuggestionService.shared.shouldShowSuggestions(for: blog) @@ -26,6 +26,32 @@ extension NSNotification.Name { navigationController?.pushViewController(controller, animated: true) } + @objc func showFullScreenImage(_ image: WPRichTextImage, from contentView: WPRichContentView) { + if let contentURL = image.contentURL { + let lightboxVC = LightboxViewController(sourceURL: contentURL) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) + } else if let linkURL = image.linkURL { + presentWebViewController(with: linkURL) + } + } + + @objc func presentWebViewController(with url: URL) { + var linkURL = url + if let components = URLComponents(string: url.absoluteString), components.host == nil { + linkURL = components.url(relativeTo: URL(string: self.post.blogURL)) ?? linkURL + } + let configuration = WebViewControllerConfiguration(url: linkURL) + configuration.authenticateWithDefaultAccount() + configuration.addsWPComReferrer = true + let webVC = WebViewControllerFactory.controller( + configuration: configuration, + source: "reader_comments" + ) + let navigationVC = UINavigationController(rootViewController: webVC) + self.present(navigationVC, animated: true, completion: nil) + } + // MARK: New Comment Threads func configuredHeaderView(for tableView: UITableView) -> UIView { From 3354d85e6047d179d7c3948a8dfef36e614b8d34 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:38:58 -0500 Subject: [PATCH 015/101] Update WPRichTextImage to use AsyncImageView --- .../Utility/Media/AsyncImageView.swift | 10 +++ .../Views/WPRichText/WPRichContentView.swift | 2 +- .../Views/WPRichText/WPRichTextImage.swift | 71 +++++-------------- .../WPRichTextMediaAttachment.swift | 6 +- 4 files changed, 31 insertions(+), 58 deletions(-) diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index b7bcea67c2f2..5f071c43ae31 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -30,6 +30,8 @@ final class AsyncImageView: UIView { /// By default, `background`. var loadingStyle = LoadingStyle.background + + var passTouchesToSuperview = false } var configuration = Configuration() { @@ -145,6 +147,14 @@ final class AsyncImageView: UIView { self.errorView = errorView return errorView } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if configuration.passTouchesToSuperview && self.bounds.contains(point) { + // Pass the touch to the superview + return nil + } + return super.hitTest(point, with: event) + } } extension GIFImageView { diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift index e9901f908a62..223f26f637bc 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift @@ -268,7 +268,7 @@ extension WPRichContentView: WPTextAttachmentManagerDelegate { /// fileprivate func richTextImage(with size: CGSize, _ url: URL, _ attachment: WPTextAttachment) -> WPRichTextImage { let image = WPRichTextImage(frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - image.addTarget(self, action: #selector(type(of: self).handleImageTapped(_:)), for: .touchUpInside) + image.addTarget(self, action: #selector(handleImageTapped), for: .touchUpInside) image.contentURL = url image.linkURL = linkURLForImageAttachment(attachment) return image diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift index 1eed7a9b8b71..3853c08f8701 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift @@ -1,22 +1,17 @@ import UIKit import WordPressMedia +import Gifu -open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { +class WPRichTextImage: UIControl, WPRichTextMediaAttachment { // MARK: Properties var contentURL: URL? var linkURL: URL? - @objc fileprivate(set) var imageView: CachedAnimatedImageView + @objc fileprivate(set) var imageView: AsyncImageView - fileprivate lazy var imageLoader: ImageLoader = { - let imageLoader = ImageLoader(imageView: imageView, gifStrategy: .largeGIFs) - imageLoader.photonQuality = Constants.readerPhotonQuality - return imageLoader - }() - - override open var frame: CGRect { + override var frame: CGRect { didSet { // If Voice Over is enabled, the OS will query for the accessibilityPath // to know what region of the screen to highlight. If the path is nil @@ -28,12 +23,9 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { // MARK: Lifecycle - deinit { - imageView.clean() - } - override init(frame: CGRect) { - imageView = CachedAnimatedImageView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height)) + imageView = AsyncImageView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height)) + imageView.configuration.passTouchesToSuperview = true imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] imageView.contentMode = .scaleAspectFit imageView.isAccessibilityElement = true @@ -43,26 +35,8 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { addSubview(imageView) } - required public init?(coder aDecoder: NSCoder) { - imageView = aDecoder.decodeObject(forKey: UIImage.classNameWithoutNamespaces()) as! CachedAnimatedImageView - contentURL = aDecoder.decodeObject(forKey: "contentURL") as! URL? - linkURL = aDecoder.decodeObject(forKey: "linkURL") as! URL? - - super.init(coder: aDecoder) - } - - override open func encode(with aCoder: NSCoder) { - aCoder.encode(imageView, forKey: UIImage.classNameWithoutNamespaces()) - - if let url = contentURL { - aCoder.encode(url, forKey: "contentURL") - } - - if let url = linkURL { - aCoder.encode(url, forKey: "linkURL") - } - - super.encode(with: aCoder) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } // MARK: Public Methods @@ -83,33 +57,22 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { return } - let successHandler: (() -> Void)? = { - onSuccess?() - } - - let errorHandler: ((Error?) -> Void)? = { error in - onError?(error) + imageView.setImage(with: contentURL, host: host) { result in + switch result { + case .success: onSuccess?() + case .failure(let error): onError?(error) + } } - - imageLoader.loadImage(with: contentURL, from: host, preferredSize: size, placeholder: nil, success: successHandler, error: errorHandler) } func contentSize() -> CGSize { - let size = imageView.intrinsicContentSize - guard size.height > 0, size.width > 0 else { - return CGSize(width: 1.0, height: 1.0) + guard let size = imageView.image?.size, size.height > 0, size.width > 0 else { + return CGSize(width: 44.0, height: 44.0) } - return imageView.intrinsicContentSize + return size } func clean() { - imageView.clean() - imageView.prepForReuse() - } -} - -private extension WPRichTextImage { - enum Constants { - static let readerPhotonQuality: UInt = 65 + imageView.prepareForReuse() } } diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift index 6d549bd0c03c..d8b0e5a3bd2d 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift @@ -1,8 +1,8 @@ import Foundation @objc protocol WPRichTextMediaAttachment: NSObjectProtocol { - var contentURL: URL? {get set} - var linkURL: URL? {get set} - var frame: CGRect {get set} + var contentURL: URL? { get set } + var linkURL: URL? { get set } + var frame: CGRect { get set } func contentSize() -> CGSize } From c3993e08ee1148e85f5dc0ae5470a4a07407a81c Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:43:42 -0500 Subject: [PATCH 016/101] Automatically pick thumbnail when available --- .../Media/Lightbox/LightboxViewController.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 3904df25c070..ec1058a8fe75 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -106,7 +106,23 @@ final class LightboxViewController: UIViewController { func configureZoomTransition(sourceView: UIView? = nil) { configureZoomTransition { _ in sourceView } + if let sourceView, thumbnail == nil { + MainActor.assumeIsolated { + thumbnail = getThumbnail(fromSourceView: sourceView) + } + } + } +} + +@MainActor +private func getThumbnail(fromSourceView sourceView: UIView) -> UIImage? { + if let imageView = sourceView as? AsyncImageView { + return imageView.image + } + if let imageView = sourceView as? UIImageView { + return imageView.image } + return nil } @available(iOS 17, *) From 36b392e616016dfe2a18dfd57cc34983eaddeae5 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:47:35 -0500 Subject: [PATCH 017/101] Remove WPImageViewController --- .../System/WordPress-Bridging-Header.h | 1 - .../WPImageViewController+Swift.swift | 21 - .../Controllers/WPImageViewController.h | 33 -- .../Controllers/WPImageViewController.m | 536 ------------------ 4 files changed, 591 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 53304de77aec..cce86c7a3683 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -85,7 +85,6 @@ #import "WPAuthTokenIssueSolver.h" #import "WPUploadStatusButton.h" #import "WPError.h" -#import "WPImageViewController.h" #import "WPStyleGuide+Pages.h" #import "WPStyleGuide+WebView.h" #import "WPTableViewHandler.h" diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift deleted file mode 100644 index 5101200f9094..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift +++ /dev/null @@ -1,21 +0,0 @@ -import UIKit -import WordPressMedia - -extension WPImageViewController { - @objc func loadOriginalImage(for media: Media, success: @escaping (UIImage) -> Void, failure: @escaping (Error) -> Void) { - Task { @MainActor in - do { - let image = try await MediaImageService.shared.image(for: media, size: .original) - success(image) - } catch { - failure(error) - } - } - } - - @objc func startAnimationIfNeeded(for image: UIImage, in imageView: CachedAnimatedImageView?) { - if let gif = image as? AnimatedImage, let data = gif.gifData { - imageView?.animate(withGIFData: data) - } - } -} diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h deleted file mode 100644 index 09e4d8252ad7..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h +++ /dev/null @@ -1,33 +0,0 @@ -#import - -@import Photos; - -@class Media; -@class AbstractPost; -@class ReaderPost; - -NS_ASSUME_NONNULL_BEGIN -@interface WPImageViewController : UIViewController - -@property (nonatomic, assign) BOOL shouldDismissWithGestures; -@property (nonatomic, weak) AbstractPost* post; -@property (nonatomic, weak) ReaderPost* readerPost; - -- (instancetype)initWithImage:(UIImage *)image; -- (instancetype)initWithURL:(NSURL *)url; -- (instancetype)initWithMedia:(Media *)media; - -- (instancetype)initWithGifData:(NSData *)data; -- (instancetype)initWithExternalMediaURL:(NSURL *)url; - -- (instancetype)initWithImage:(nullable UIImage *)image andURL:(nullable NSURL *)url; -- (instancetype)initWithImage:(nullable UIImage *)image andMedia:(nullable Media *)media; - -- (void)loadImage; -- (void)hideBars:(BOOL)hide animated:(BOOL)animated; -- (void)centerImage; - -+ (BOOL)isUrlSupported:(NSURL *)url; - -@end -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m deleted file mode 100644 index a6fa1e690641..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m +++ /dev/null @@ -1,536 +0,0 @@ -#import "WPImageViewController.h" -#import "WordPress-Swift.h" -@import Gridicons; - -static CGFloat const MaximumZoomScale = 4.0; -static CGFloat const MinimumZoomScale = 0.1; - -@interface WPImageViewController () - -@property (nonatomic, strong) NSURL *url; -@property (nonatomic, strong) UIImage *image; -@property (nonatomic, strong) Media *media; -@property (nonatomic, strong) NSData *data; -@property (nonatomic) BOOL isExternal; - -@property (nonatomic, assign) BOOL isLoadingImage; -@property (nonatomic, assign) BOOL isFirstLayout; -@property (nonatomic, strong) UIScrollView *scrollView; -@property (nonatomic, strong) CachedAnimatedImageView *imageView; -@property (nonatomic, strong) ImageLoader *imageLoader; -@property (nonatomic, assign) BOOL shouldHideStatusBar; -@property (nonatomic, strong) CircularProgressView *activityIndicatorView; - -@property (nonatomic) FlingableViewHandler *flingableViewHandler; -@property (nonatomic, strong) UITapGestureRecognizer *singleTapGesture; -@property (nonatomic, strong) UITapGestureRecognizer *doubleTapGesture; - -@end - -@implementation WPImageViewController - -#pragma mark - LifeCycle Methods - -- (instancetype)initWithImage:(UIImage *)image -{ - return [self initWithImage:image andURL:nil]; -} - -- (instancetype)initWithURL:(NSURL *)url -{ - return [self initWithImage:nil andURL:url]; -} - -- (instancetype)initWithMedia:(Media *)media -{ - return [self initWithImage:nil andMedia:media]; -} - -- (instancetype)initWithGifData:(NSData *)data -{ - self = [super init]; - if (self) { - _data = data; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image andURL:(NSURL *)url -{ - self = [super init]; - if (self) { - _image = [image copy]; - _url = url; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image andMedia:(Media *)media -{ - self = [super init]; - if (self) { - _image = [image copy]; - _media = media; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithExternalMediaURL:(NSURL *)url -{ - self = [super init]; - if (self) { - _image = nil; - _url = url; - _isExternal = YES; - [self commonInit]; - } - return self; -} - -- (void)commonInit -{ - _shouldDismissWithGestures = YES; - _isFirstLayout = YES; -} - -- (void)setIsLoadingImage:(BOOL)isLoadingImage -{ - _isLoadingImage = isLoadingImage; - - if (isLoadingImage) { - [self.activityIndicatorView startAnimating]; - } else { - [self.activityIndicatorView stopAnimating]; - } -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.view.backgroundColor = [UIColor blackColor]; - CGRect frame = self.view.frame; - frame = CGRectMake(0.0f, 0.0f, frame.size.width, frame.size.height); - - [self setupScrollView:frame]; - [self setupImageViewWidth:frame]; - [self setupImageLoader]; - - self.doubleTapGesture = [self setupTapGestureWithNumberOfTaps:2 onView:self.imageView]; - self.singleTapGesture = [self setupTapGestureWithNumberOfTaps:1 onView:self.scrollView]; - [self.singleTapGesture requireGestureRecognizerToFail:self.doubleTapGesture]; - - [self setupFlingableView]; - [self setupActivityIndicator]; - [self layoutActivityIndicator]; - - [self setupAccessibility]; - - [self loadImage]; -} - -- (void)setupActivityIndicator -{ - self.activityIndicatorView = [[CircularProgressView alloc] initWithStyle:CircularProgressViewStyleWhite]; - AccessoryView *errorView = [[AccessoryView alloc] init]; - errorView.imageView.image = [UIImage gridiconOfType:GridiconTypeNoticeOutline]; - errorView.label.text = NSLocalizedString(@"Error", @"Generic error."); - self.activityIndicatorView.errorView = errorView; -} - -- (void)layoutActivityIndicator -{ - self.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; - [self.view addSubview:self.activityIndicatorView]; - NSArray *constraints = @[ - [self.activityIndicatorView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], - [self.activityIndicatorView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor] - ]; - - [NSLayoutConstraint activateConstraints:constraints]; -} - -- (void)setupFlingableView -{ - self.flingableViewHandler = [[FlingableViewHandler alloc] initWithTargetView:self.scrollView]; - self.flingableViewHandler.delegate = self; - self.flingableViewHandler.isActive = self.shouldDismissWithGestures; -} - -- (UITapGestureRecognizer *)setupTapGestureWithNumberOfTaps:(NSInteger)taps onView:(UIView*)view -{ - UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleTapGesture:)]; - [gesture setNumberOfTapsRequired:taps]; - [view addGestureRecognizer:gesture]; - return gesture; -} - -- (void)setupScrollView:(CGRect)frame { - self.scrollView = [[UIScrollView alloc] initWithFrame:frame]; - self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; - self.scrollView.maximumZoomScale = MaximumZoomScale; - self.scrollView.minimumZoomScale = MinimumZoomScale; - self.scrollView.scrollsToTop = NO; - self.scrollView.delegate = self; - [self.view addSubview:self.scrollView]; -} - -- (void)setupImageViewWidth:(CGRect)frame -{ - self.imageView = [[CachedAnimatedImageView alloc] initWithFrame:frame]; - self.imageView.gifStrategy = GIFStrategyLargeGIFs; - self.imageView.contentMode = UIViewContentModeScaleAspectFit; - self.imageView.shouldShowLoadingIndicator = NO; - self.imageView.userInteractionEnabled = YES; - [self.scrollView addSubview:self.imageView]; -} - -- (void)setupImageLoader -{ - self.imageLoader = [[ImageLoader alloc] initWithImageView:self.imageView gifStrategy:GIFStrategyLargeGIFs]; -} - -- (void)loadImage -{ - if (self.isLoadingImage) { - return; - } - - if (self.image != nil) { - [self updateImageView]; - } else if (self.url && self.isExternal) { - [self loadImageFromExternalURL]; - } else if (self.url) { - [self loadImageFromURL]; - } else if (self.media) { - [self loadImageFromMedia]; - } else if (self.data) { - [self loadImageFromGifData]; - } -} - -- (void)updateImageView -{ - self.imageView.image = self.image; - [self.imageView sizeToFit]; - self.scrollView.contentSize = self.imageView.image.size; - [self centerImage]; - -} - -- (void)loadImageFromURL -{ - self.isLoadingImage = YES; - __weak __typeof__(self) weakSelf = self; - if (self.readerPost != NULL) { - [self.imageLoader loadImageWithURL:self.url fromReaderPost:self.readerPost preferredSize:CGSizeZero placeholder:self.image success:^{ - weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; - [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; - } else { - [_imageView downloadImageUsingRequest:[NSURLRequest requestWithURL:self.url] - placeholderImage:self.image - success:^(UIImage *image) { - weakSelf.image = image; - [weakSelf updateImageView]; - weakSelf.isLoadingImage = NO; - } failure:^(NSError *error) { - DDLogError(@"Error loading image: %@", error); - [weakSelf.activityIndicatorView showError]; - }]; - } -} - -- (void)loadImageFromMedia -{ - self.imageView.image = self.image; - self.isLoadingImage = YES; - [self.activityIndicatorView startAnimating]; - - __weak __typeof__(self) weakSelf = self; - [self loadOriginalImageFor:self.media success:^(UIImage * _Nonnull image) { - weakSelf.isLoadingImage = NO; - weakSelf.image = image; - [weakSelf updateImageView]; - [weakSelf startAnimationIfNeededFor:image in:weakSelf.imageView]; - } failure:^(NSError * _Nonnull error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; -} - -- (void)loadImageFromGifData -{ - self.isLoadingImage = YES; - - __weak __typeof__(self) weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - self.image = [[UIImage alloc] initWithData: self.data]; - [weakSelf updateImageView]; - }); - [self.imageView setAnimatedImage:self.data success:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - weakSelf.isLoadingImage = NO; - }); - }]; -} - -- (void)loadImageFromExternalURL -{ - self.isLoadingImage = YES; - - __weak __typeof__(self) weakSelf = self; - [self.imageLoader loadImageWithURL:self.url - fromPost:self.post - preferredSize:CGSizeZero - placeholder:nil - success:^{ - weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; - [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self hideBars:YES animated:animated]; -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - if (self.isFirstLayout) { - [self centerImage]; - self.isFirstLayout = NO; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [self hideBars:NO animated:animated]; -} - -- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator -{ - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - [coordinator animateAlongsideTransition:^(id _Nonnull __unused context) { - [self centerImage]; - } completion:nil]; -} - -- (BOOL)prefersHomeIndicatorAutoHidden -{ - return self.shouldHideStatusBar; -} - -#pragma mark - Instance Methods - -- (void)setShouldDismissWithGestures:(BOOL)shouldDismissWithGestures -{ - _shouldDismissWithGestures = shouldDismissWithGestures; - self.flingableViewHandler.isActive = shouldDismissWithGestures; -} - -- (void)hideBars:(BOOL)hide animated:(BOOL)animated -{ - self.shouldHideStatusBar = hide; - - // Force an update of the status bar appearance and visiblity - if (animated) { - [UIView animateWithDuration:0.3 - animations:^{ - [self setNeedsStatusBarAppearanceUpdate]; - [self setNeedsUpdateOfHomeIndicatorAutoHidden]; - }]; - } else { - [self setNeedsStatusBarAppearanceUpdate]; - - [self setNeedsUpdateOfHomeIndicatorAutoHidden]; - } -} - -- (void)centerImage -{ - CGFloat scaleWidth = CGRectGetWidth(self.scrollView.frame) / self.imageView.image.size.width; - CGFloat scaleHeight = CGRectGetHeight(self.scrollView.frame) / self.imageView.image.size.height; - - self.scrollView.minimumZoomScale = MIN(scaleWidth, scaleHeight); - self.scrollView.zoomScale = self.scrollView.minimumZoomScale; - - [self scrollViewDidZoom:self.scrollView]; -} - -- (void)handleTapGesture:(UITapGestureRecognizer *)tapGesture -{ - if ([tapGesture isEqual:self.singleTapGesture]) { - [self handleImageTappedWith:tapGesture]; - } else if ([tapGesture isEqual:self.doubleTapGesture]) { - [self handleImageDoubleTappedWidth:tapGesture]; - } -} - -- (void)handleImageTappedWith:(UITapGestureRecognizer *)tgr -{ - if (self.shouldDismissWithGestures) { - [self dismissViewControllerAnimated:YES completion:nil]; - } -} - -- (void)handleImageDoubleTappedWidth:(UITapGestureRecognizer *)tgr -{ - if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale) { - [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; - return; - } - - CGPoint point = [tgr locationInView:self.imageView]; - CGSize size = self.scrollView.frame.size; - - CGFloat w = size.width / self.scrollView.maximumZoomScale; - CGFloat h = size.height / self.scrollView.maximumZoomScale; - CGFloat x = point.x - (w / 2.0f); - CGFloat y = point.y - (h / 2.0f); - - CGRect rect = CGRectMake(x, y, w, h); - [self.scrollView zoomToRect:rect animated:YES]; -} - -#pragma mark - UIScrollView Delegate - -- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView -{ - if (self.imageView.image) { - return self.imageView; - } - return nil; -} - -- (void)scrollViewDidZoom:(UIScrollView *)scrollView -{ - CGSize size = scrollView.frame.size; - CGRect frame = self.imageView.frame; - - if (frame.size.width < size.width) { - frame.origin.x = (size.width - frame.size.width) / 2; - } else { - frame.origin.x = 0; - } - - if (frame.size.height < size.height) { - frame.origin.y = (size.height - frame.size.height) / 2; - } else { - frame.origin.y = 0; - } - - self.imageView.frame = frame; - - [self updateFlingableViewHandlerActiveState]; -} - -- (void)updateFlingableViewHandlerActiveState -{ - if (!self.shouldDismissWithGestures) { - return; - } - BOOL isScrollViewZoomedOut = (self.scrollView.zoomScale == self.scrollView.minimumZoomScale); - - self.flingableViewHandler.isActive = isScrollViewZoomedOut; -} - -#pragma mark - Status bar management - -- (BOOL)prefersStatusBarHidden -{ - return self.shouldHideStatusBar; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return UIStatusBarStyleLightContent; -} - -- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation -{ - return UIStatusBarAnimationFade; -} - -#pragma mark - Static Helpers - -+ (BOOL)isUrlSupported:(NSURL *)url -{ - // Safeguard - if (!url) { - return NO; - } - - // We only support: PNG + JPG + JPEG + GIF - NSString *absoluteURL = url.absoluteString; - - NSArray *types = @[@".png", @".jpg", @".gif", @".jpeg"]; - for (NSString *type in types) { - if (NSNotFound != [[absoluteURL lowercaseString] rangeOfString:type].location) { - return YES; - } - } - - return NO; -} - -#pragma mark - FlingableViewHandlerDelegate - -- (void)flingableViewHandlerDidBeginRecognizingGesture:(FlingableViewHandler *)handler -{ - self.scrollView.multipleTouchEnabled = NO; -} - -- (void)flingableViewHandlerDidEndRecognizingGesture:(FlingableViewHandler *)handler { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissViewControllerAnimated:YES completion:nil]; - }); -} - -- (void)flingableViewHandlerWasCancelled:(FlingableViewHandler *)handler -{ - self.scrollView.multipleTouchEnabled = YES; -} - -#pragma mark - Accessibility - -- (void)setupAccessibility -{ - self.imageView.isAccessibilityElement = YES; - self.imageView.accessibilityTraits = UIAccessibilityTraitImage; - - if (self.media != nil && self.media.title != nil) { - self.imageView.accessibilityLabel = [NSString stringWithFormat:NSLocalizedString(@"Fullscreen view of image %@. Double tap to dismiss", @"Accessibility label for when image is shown to user in full screen, with instructions on how to dismiss the screen. Placeholder is the title of the image"), self.media.title]; - } - else { - self.imageView.accessibilityLabel = NSLocalizedString(@"Fullscreen view of image. Double tap to dismiss", @"Accessibility label for when image is shown to user in full screen, with instructions on how to dismiss the screen"); - } - -} - -- (BOOL)accessibilityPerformEscape -{ - // Dismiss when self receives the VoiceOver escape gesture (Z). This does not seem to happen - // automatically if self is presented modally by itself (i.e. not inside a - // UINavigationController). - [self dismissViewControllerAnimated:YES completion:nil]; - return YES; -} - -@end From 342e63e197c4347e4063d475cee5049c07ec5145 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:53:27 -0500 Subject: [PATCH 018/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 8e89ce34b262..b19d2671241a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,6 @@ 25.7 ----- +* [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] 25.6 ----- From d613c05db5b1b7009cf2fc6a2e1434611b124837 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:00:04 -0500 Subject: [PATCH 019/101] Remove ImageLoader --- .../Classes/Utility/Media/ImageLoader.swift | 302 ------------------ 1 file changed, 302 deletions(-) delete mode 100644 WordPress/Classes/Utility/Media/ImageLoader.swift diff --git a/WordPress/Classes/Utility/Media/ImageLoader.swift b/WordPress/Classes/Utility/Media/ImageLoader.swift deleted file mode 100644 index edfbfe771365..000000000000 --- a/WordPress/Classes/Utility/Media/ImageLoader.swift +++ /dev/null @@ -1,302 +0,0 @@ -import MobileCoreServices -import AlamofireImage -import AutomatticTracks -import WordPressShared -import WordPressMedia - -/// Class used together with `CachedAnimatedImageView` to facilitate the loading of both -/// still images and animated gifs. -/// -/// - warning: Deprecated, please use `AsyncImageView` or `.wp` extensions for `UIImageView`. -@objc class ImageLoader: NSObject { - typealias ImageLoaderSuccessBlock = () -> Void - typealias ImageLoaderFailureBlock = (Error?) -> Void - - // MARK: Public Fields - - public var photonQuality: UInt { - get { - return selectedPhotonQuality - } - set(newPhotonQuality) { - selectedPhotonQuality = min(max(newPhotonQuality, Constants.minPhotonQuality), Constants.maxPhotonQuality) - } - } - - // MARK: - Image Dimensions Support - typealias ImageLoaderDimensionsBlock = (ImageDimensionFormat, CGSize) -> Void - - /// Called if the imageLoader is able to determine the image format, and dimensions - /// for the image prior to it being downloaded. - /// Note: Set the property prior to calling any load method - public var imageDimensionsHandler: ImageLoaderDimensionsBlock? - private var imageDimensionsFetcher: ImageDimensionsFetcher? = nil - - // MARK: Private Fields - - private unowned let imageView: CachedAnimatedImageView - private let loadingIndicator: ActivityIndicatorType - - private var successHandler: ImageLoaderSuccessBlock? - private var errorHandler: ImageLoaderFailureBlock? - private var placeholder: UIImage? - private var selectedPhotonQuality: UInt = Constants.defaultPhotonQuality - - @objc init(imageView: CachedAnimatedImageView, gifStrategy: GIFStrategy = .mediumGIFs) { - self.imageView = imageView - imageView.gifStrategy = gifStrategy - - let loadingIndicator = CircularProgressView(style: .primary) - loadingIndicator.backgroundColor = .clear - self.loadingIndicator = loadingIndicator - - super.init() - - imageView.addLoadingIndicator(self.loadingIndicator, style: .fullView) - } - - /// Removes the gif animation and prevents it from animate again. - /// Call this in a table/collection cell's `prepareForReuse()`. - /// - @objc func prepareForReuse() { - imageView.prepForReuse() - } - - /// Load an image from a specific post, using the given URL. Supports animated images (gifs) as well. - /// - /// - Parameters: - /// - url: The URL to load the image from. - /// - host: The `MediaHost` of the image. - /// - size: The preferred size of the image to load. - /// - func loadImage(with url: URL, from host: MediaHost, preferredSize size: CGSize = .zero) { - if url.isFileURL { - downloadImage(from: url) - } else if url.isGif { - loadGif(with: url, from: host, preferredSize: size) - } else { - imageView.clean() - loadStaticImage(with: url, from: host, preferredSize: size) - } - } - - @objc(loadImageWithURL:fromPost:preferredSize:placeholder:success:error:) - func loadImage(with url: URL, from post: AbstractPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - - let host = MediaHost(post) - loadImage(with: url, from: host, preferredSize: size, placeholder: placeholder, success: success, error: error) - } - - @objc(loadImageWithURL:fromReaderPost:preferredSize:placeholder:success:error:) - func loadImage(with url: URL, from readerPost: ReaderPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - loadImage(with: url, from: MediaHost(readerPost), preferredSize: size, placeholder: placeholder, success: success, error: error) - } - - /// Load an image from a specific post, using the given URL. Supports animated images (gifs) as well. - /// - /// - Parameters: - /// - url: The URL to load the image from. - /// - host: The host of the image. - /// - size: The preferred size of the image to load. You can pass height 0 to set width and preserve aspect ratio. - /// - placeholder: A placeholder to show while the image is loading. - /// - success: A closure to be called if the image was loaded successfully. - /// - error: A closure to be called if there was an error loading the image. - func loadImage(with url: URL, from host: MediaHost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - - self.placeholder = placeholder - successHandler = success - errorHandler = error - - loadImage(with: url, from: host, preferredSize: size) - } - - // MARK: - Private helpers - - /// Load an animated image from the given URL. - /// - private func loadGif(with url: URL, from host: MediaHost, preferredSize size: CGSize = .zero) { - let mediaAuthenticator = MediaRequestAuthenticator() - mediaAuthenticator.authenticatedRequest( - for: url, - from: host, - onComplete: { request in - self.downloadGif(from: request) - }, - onFailure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - self.callErrorHandler(with: error) - }) - } - - /// Load a static image from the given URL. - /// - private func loadStaticImage(with url: URL, from host: MediaHost, preferredSize size: CGSize = .zero) { - let finalURL: URL - - switch host { - case .publicSite: fallthrough - case .privateSelfHostedSite: - finalURL = url - case .publicWPComSite: fallthrough - case .privateAtomicWPComSite: - finalURL = photonUrl(with: url, preferredSize: size) - case .privateWPComSite: - finalURL = privateImageURL(with: url, from: host, preferredSize: size) - } - - let mediaRequestAuthenticator = MediaRequestAuthenticator() - - mediaRequestAuthenticator.authenticatedRequest(for: finalURL, from: host, onComplete: { request in - self.downloadImage(from: request) - }) { error in - WordPressAppDelegate.crashLogging?.logError(error) - self.callErrorHandler(with: error) - } - } - - /// Constructs the URL for an image from a private post hosted in WPCom. - /// - private func privateImageURL(with url: URL, from host: MediaHost, preferredSize size: CGSize) -> URL { - let scale = UIScreen.main.scale - let scaledSize = CGSize(width: size.width * scale, height: size.height * scale) - let scaledURL = WPImageURLHelper.imageURLWithSize(scaledSize, forImageURL: url) - - return scaledURL - } - - /// Gets the photon URL with the specified size, or returns the passed `URL` - /// - private func photonUrl(with url: URL, preferredSize size: CGSize) -> URL { - guard let photonURL = getPhotonUrl(for: url, size: size) else { - return url - } - - return photonURL - } - - /// Triggers the image dimensions fetcher if the `imageDimensionsHandler` property is set - private func calculateImageDimensionsIfNeeded(from request: URLRequest) { - guard let imageDimensionsHandler else { - return - } - - let fetcher = ImageDimensionsFetcher(request: request, success: { (format, size) in - guard let size, size != .zero else { - return - } - - DispatchQueue.main.async { - imageDimensionsHandler(format, size) - } - }) - - fetcher.start() - - imageDimensionsFetcher = fetcher - } - - /// Stop the image dimension calculation - private func cancelImageDimensionCalculation() { - imageDimensionsFetcher?.cancel() - imageDimensionsFetcher = nil - } - - /// Download the animated image from the given URL Request. - /// - private func downloadGif(from request: URLRequest) { - calculateImageDimensionsIfNeeded(from: request) - - imageView.startLoadingAnimation() - imageView.setAnimatedImage(request, placeholderImage: placeholder, success: { [weak self] in - self?.callSuccessHandler() - }) { [weak self] (error) in - self?.callErrorHandler(with: error) - } - } - - /// Downloads the image from the given URL Request. - /// - private func downloadImage(from request: URLRequest) { - calculateImageDimensionsIfNeeded(from: request) - - imageView.startLoadingAnimation() - imageView.af.setImage(withURLRequest: request, completion: { [weak self] dataResponse in - guard let self else { - return - } - - switch dataResponse.result { - case .success: - self.callSuccessHandler() - case .failure(let error): - self.callErrorHandler(with: error) - } - }) - } - - /// Downloads the image from the given URL. - /// - private func downloadImage(from url: URL) { - let request = URLRequest(url: url) - downloadImage(from: request) - } - - private func callSuccessHandler() { - cancelImageDimensionCalculation() - - imageView.stopLoadingAnimation() - guard successHandler != nil else { - return - } - DispatchQueue.main.async { - self.successHandler?() - } - } - - private func callErrorHandler(with error: Error?) { - if let error, (error as NSError).code == NSURLErrorCancelled { - return - } - - cancelImageDimensionCalculation() - - DispatchQueue.main.async { [weak self] in - guard let self else { - return - } - - if self.imageView.shouldShowLoadingIndicator { - (self.loadingIndicator as? CircularProgressView)?.state = .error - } - - self.errorHandler?(error) - } - } -} - -// MARK: - Loading Media object - -extension ImageLoader { - private func getPhotonUrl(for url: URL, size: CGSize) -> URL? { - var finalSize = size - if url.isGif { - // Photon helper sets the size to load the retina version. We don't want that for gifs - let scale = UIScreen.main.scale - finalSize = CGSize(width: size.width / scale, height: size.height / scale) - } - return PhotonImageURLHelper.photonURL(with: finalSize, - forImageURL: url, - forceResize: true, - imageQuality: selectedPhotonQuality) - } -} - -// MARK: - Constants - -private extension ImageLoader { - enum Constants { - static let minPhotonQuality: UInt = 1 - static let maxPhotonQuality: UInt = 100 - static let defaultPhotonQuality: UInt = 80 - } -} From 2ac4c2ea13ccd38e23584749b30642ff5ea0283b Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:03:00 -0500 Subject: [PATCH 020/101] Remove ImageDimensionParser --- .../ImageDimensionFetcher.swift | 87 ------ .../ImageDimensionParser.swift | 268 ------------------ WordPress/WordPress.xcodeproj/project.pbxproj | 54 +--- .../ImageDimensionParserTests.swift | 65 ----- .../WordPressTest/Test Images/100x100-png | Bin 4036 -> 0 bytes .../WordPressTest/Test Images/100x100.gif | Bin 156 -> 0 bytes .../WordPressTest/Test Images/100x100.jpg | Bin 4620 -> 0 bytes .../WordPressTest/Test Images/invalid-gif.gif | Bin 155 -> 0 bytes .../Test Images/invalid-jpeg-header.jpg | Bin 12 -> 0 bytes .../Test Images/valid-gif-header.gif | 1 - .../Test Images/valid-jpeg-header.jpg | Bin 12 -> 0 bytes .../Test Images/valid-png-header | Bin 24 -> 0 bytes .../{Test Images => }/iphone-photo.heic | Bin 13 files changed, 1 insertion(+), 474 deletions(-) delete mode 100644 WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionFetcher.swift delete mode 100644 WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionParser.swift delete mode 100644 WordPress/WordPressTest/ImageDimensionParserTests.swift delete mode 100644 WordPress/WordPressTest/Test Images/100x100-png delete mode 100644 WordPress/WordPressTest/Test Images/100x100.gif delete mode 100644 WordPress/WordPressTest/Test Images/100x100.jpg delete mode 100755 WordPress/WordPressTest/Test Images/invalid-gif.gif delete mode 100755 WordPress/WordPressTest/Test Images/invalid-jpeg-header.jpg delete mode 100755 WordPress/WordPressTest/Test Images/valid-gif-header.gif delete mode 100755 WordPress/WordPressTest/Test Images/valid-jpeg-header.jpg delete mode 100755 WordPress/WordPressTest/Test Images/valid-png-header rename WordPress/WordPressTest/{Test Images => }/iphone-photo.heic (100%) diff --git a/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionFetcher.swift b/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionFetcher.swift deleted file mode 100644 index 28d4a693619d..000000000000 --- a/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionFetcher.swift +++ /dev/null @@ -1,87 +0,0 @@ -import UIKit - -class ImageDimensionsFetcher: NSObject, URLSessionDataDelegate { - // Helpful typealiases for the closures - public typealias CompletionHandler = (ImageDimensionFormat, CGSize?) -> Void - public typealias ErrorHandler = (Error?) -> Void - - let completionHandler: CompletionHandler - let errorHandler: ErrorHandler? - - // Internal use properties - private let request: URLRequest - private var task: URLSessionDataTask? = nil - private let parser: ImageDimensionParser - private var session: URLSession? = nil - - deinit { - cancel() - } - - init(request: URLRequest, - success: @escaping CompletionHandler, - error: ErrorHandler? = nil, - imageParser: ImageDimensionParser = ImageDimensionParser()) { - self.request = request - self.completionHandler = success - self.errorHandler = error - self.parser = imageParser - - super.init() - } - - /// Starts the calculation process - func start() { - let config = URLSessionConfiguration.default - let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) - let task = session.dataTask(with: request) - task.resume() - - self.task = task - self.session = session - } - - func cancel() { - session?.invalidateAndCancel() - task?.cancel() - } - - // MARK: - URLSessionDelegate - public func urlSession(_ session: URLSession, task dataTask: URLSessionTask, didCompleteWithError error: Error?) { - // Don't trigger an error if we cancelled the task - if let error, (error as NSError).code == NSURLErrorCancelled { - return - } - - self.errorHandler?(error) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - // Add the downloaded data to the parser - parser.append(bytes: data) - - // Wait for the format to be detected - guard let format = parser.format else { - return - } - - // Check if the format is unsupported - guard format != .unsupported else { - completionHandler(format, nil) - - // We can't parse unsupported images, cancel the download - cancel() - return - } - - // Wait for the image size - guard let size = parser.imageSize else { - return - } - - completionHandler(format, size) - - // The image size has been calculated, stop downloading - cancel() - } -} diff --git a/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionParser.swift b/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionParser.swift deleted file mode 100644 index de2726fe5a58..000000000000 --- a/WordPress/Classes/Utility/Image Dimension Parser/ImageDimensionParser.swift +++ /dev/null @@ -1,268 +0,0 @@ -import UIKit - -class ImageDimensionParser { - private(set) var format: ImageDimensionFormat? - private(set) var imageSize: CGSize? = nil - - private var data: Data - - init(with data: Data = Data()) { - self.data = data - - parse() - } - - public func append(bytes: Data) { - data.append(contentsOf: bytes) - - parse() - } - - private func parse() { - guard - let format = ImageDimensionFormat(with: data) - else { - return - } - - self.format = format - imageSize = dimensions(with: data) - - guard imageSize != nil else { - return - } - } - - // MARK: - Dimension Calculating - private func dimensions(with data: Data) -> CGSize? { - switch format { - case .png: return pngSize(with: data) - case .gif: return gifSize(with: data) - case .jpeg: return jpegSize(with: data) - - default: return nil - } - } - - // MARK: - PNG Parsing - private func pngSize(with data: Data) -> CGSize? { - // Bail out if the data size is too small to read the header - let chunkSize = PNGConstants.chunkSize - let ihdrStart = PNGConstants.headerSize + chunkSize - - // The min length needed to read the width / height - let minLength = ihdrStart + chunkSize * 3 - - guard data.count >= minLength else { - return nil - } - - // Validate the header to make sure the width/height is in the correct spot - guard data.subdata(start: ihdrStart, length: chunkSize) == PNGConstants.IHDR else { - return nil - } - - // Width is immediately after the IHDR header - let widthOffset = ihdrStart + chunkSize - - // Height is after the width - let heightOffset = widthOffset + chunkSize - - // Height and width are stored as 32 bit ints - // http://www.libpng.org/pub/png/spec/1.0/PNG-Chunks.html - // ^ The maximum for each is (2^31)-1 in order to accommodate languages that have difficulty with unsigned 4-byte values. - let width = CFSwapInt32(data[widthOffset, chunkSize] as UInt32) - let height = CFSwapInt32(data[heightOffset, chunkSize] as UInt32) - - return CGSize(width: Int(width), height: Int(height)) - } - - private struct PNGConstants { - // PNG header size is 8 bytes - static let headerSize = 8 - - // PNG is broken up into 4 byte chunks, except for the header - static let chunkSize = 4 - - // IHDR header: // https://www.w3.org/TR/PNG/#11IHDR - static let IHDR = Data([0x49, 0x48, 0x44, 0x52]) - } - - // MARK: - GIF Parsing - private func gifSize(with data: Data) -> CGSize? { - // Bail out if the data size is too small to read the header - let valueSize = GIFConstants.valueSize - let headerSize = GIFConstants.headerSize - - // Min length we need to read is the header size + 4 bytes - let minLength = headerSize + valueSize * 3 - - guard data.count >= minLength else { - return nil - } - - // The width appears directly after the header, and the height after that. - let widthOffset = headerSize - let heightOffset = widthOffset - - // Reads the "logical screen descriptor" which appears after the GIF header block - let width: UInt16 = data[widthOffset, valueSize] - let height: UInt16 = data[heightOffset, valueSize] - - return CGSize(width: Int(width), height: Int(height)) - } - - private struct GIFConstants { - // http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - - // The GIF header size is 6 bytes - static let headerSize = 6 - - // The height and width are stored as 2 byte values - static let valueSize = 2 - } - - // MARK: - JPEG Parsing - private struct JPEGConstants { - static let blockSize: UInt16 = 256 - - // 16 bytes skips the header and the first block - static let minDataCount = 16 - - static let valueSize = 2 - static let heightOffset = 5 - - // JFIF{NULL} - static let jfifHeader = Data([0x4A, 0x46, 0x49, 0x46, 0x00]) - } - - private func jpegSize(with data: Data) -> CGSize? { - // Bail out if the data size is too small to read the header - guard data.count > JPEGConstants.minDataCount else { - return nil - } - - // Adapted from: - // - https://web.archive.org/web/20131016210645/http://www.64lines.com/jpeg-width-height - - var i = JPEGConstants.jfifHeader.count - 1 - - let blockSize: UInt16 = JPEGConstants.blockSize - - // Retrieve the block length of the first block since the first block will not contain the size of file - var block_length = UInt16(data[i]) * blockSize + UInt16(data[i+1]) - - while i < data.count { - i += Int(block_length) - - // Protect again out of bounds issues - // 10 = the max size we need to read all the values from below - if i + 10 >= data.count { - return nil - } - - // Check that we are truly at the start of another block - if data[i] != 0xFF { - return nil - } - - // SOFn marker - let marker = data[i+1] - - let isValidMarker = (marker >= 0xC0 && marker <= 0xC3) || - (marker >= 0xC5 && marker <= 0xC7) || - (marker >= 0xC9 && marker <= 0xCB) || - (marker >= 0xCD && marker <= 0xCF) - - if isValidMarker { - // "Start of frame" marker which contains the file size - let valueSize = JPEGConstants.valueSize - let heightOffset = i + JPEGConstants.heightOffset - let widthOffset = heightOffset + valueSize - - let height = CFSwapInt16(data[heightOffset, valueSize] as UInt16) - let width = CFSwapInt16(data[widthOffset, valueSize] as UInt16) - - return CGSize(width: Int(width), height: Int(height)) - } - - // Go to the next block - i += 2 // Skip the block marker - block_length = UInt16(data[i]) * blockSize + UInt16(data[i+1]) - } - - return nil - } -} - -// MARK: - ImageFormat -enum ImageDimensionFormat { - // WordPress supported image formats: - // https://wordpress.com/support/images/ - // https://codex.wordpress.org/Uploading_Files - case jpeg - case png - case gif - case unsupported - - init?(with data: Data) { - if data.headerIsEqual(to: FileMarker.jpeg) { - self = .jpeg - } - else if data.headerIsEqual(to: FileMarker.gif) { - self = .gif - } - else if data.headerIsEqual(to: FileMarker.png) { - self = .png - } - else if data.count < FileMarker.png.count { - return nil - } - else { - self = .unsupported - } - } - - // File type markers denote the type of image in the first few bytes of the file - private struct FileMarker { - // https://en.wikipedia.org/wiki/JPEG_Network_Graphics - static let png = Data([0x89, 0x50, 0x4E, 0x47]) - - // https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format - // FFD8 = SOI, APP0 marker - static let jpeg = Data([0xFF, 0xD8, 0xFF]) - - // https://en.wikipedia.org/wiki/GIF - static let gif = Data([0x47, 0x49, 0x46, 0x38]) //GIF8 - } -} - -// MARK: - Private: Extensions -private extension Data { - func headerData(with length: Int) -> Data { - return subdata(start: 0, length: length) - } - - func headerIsEqual(to value: Data) -> Bool { - // Prevent any out of bounds issues - if count < value.count { - return false - } - - let header = headerData(with: value.count) - - return header == value - } - - func subdata(start: Int, length: Int) -> Data { - return subdata(in: start ..< start + length) - } - - subscript(range: Range) -> UInt16 { - return subdata(in: range).withUnsafeBytes { $0.load(as: UInt16.self) } - } - - subscript(start: Int, length: Int) -> T { - return self[start.. Data { - let url = Bundle(for: ImageDimensionParserTests.self).url(forResource: name, withExtension: nil)! - return try! Data(contentsOf: url) - } -} diff --git a/WordPress/WordPressTest/Test Images/100x100-png b/WordPress/WordPressTest/Test Images/100x100-png deleted file mode 100644 index 3fc17a04dfc58ac82b6d7555d5c1919e8c6dfe75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4036 zcmeHJi9eM67JtSbi6kXqjJ3jK$(D62A^SEIVVGeoGsBD!*_TL@%AU!%b|I9AkS$V; zEU!I{EZJ3vtanuReY@|y_dmG5=X0KOe&?L;S$@Csd_GT{naKrq))TA%0I(bA>sZjZ z>prnC(cZX^$yNZsI)>KPHZ#!H2Ala}QD`qD0O-dhr!t$}8{!FD-YZbN1Y^jJ8e^Dc zNaTXCk-_{rq9Wl@Trk$t3RXODoK)crC%bOz!;G&jT4QX>rQ++M9T0NKO-6I`#~Pnm z>b9oq6_?*+j$%zHYEzVYxlhbtT$E(}N+))pvdtH5Rjwf{Boi0D#m)&FFvpW$PQ4N- z>FB_Pb+m5!f5&lX=FEH7b=&Uk?u8zeYS4U14;bnxB||5W!lMy0U{gpkeV9tbpz*N| z(VSpr=M3&soZ}7VCW4=!IhiUi-EYZcrAa6>qX6nF`!accppqWpHu|s> zDHx-f=7H@m`Fz0S2&0dc&tzuW0iF)J9C6!>BYB<4V#vp0X=g}p9(E*H4@J`vPlq&| z6$^=2(eZgKW}CvxuQ1zUGn5xEVZB4=Q@-aq2DV-vs%%_6^Re(-XS9q^@z}F_YizA0 zuhJ?Rti6M^G^x3!C7q6tb@SKZ=mM!Tldb&}9aRdAv`!00 zR`?(2eq=IiV@*%J4lA;HkohEfjDhOT3bbw--2cNKkiQ11363vFoW3>kE=S|ICc|79 z6CqsaEThefLrNe;D&w{$n1%5$m`NleFpN10%vTf)vZb>LgP74Ngh`R<7U+UrfN;l8 z(%+(TD=|xlds4+}*aRX?-qBwP*ZK@f(BzDYl;Awt6P*CYckuW|q$P52vqWo~n?aK3 z^0X29AgOR0Em5;mR-nb0;_RGv2WCzZ80W$SU=C&)t2~_($R(MvNFZUbd z@nU|;cRs3RoZ$0R32Vf>Zu4}N4tC|9He+}ViQAnHKU z6IJye8$c{TMVW*sLVBO>rcOU(BynicwKC#4@p;xLEJQ!#LbjQtL`afId}bxl!dTff zQ15%8p7mq+MB$e|QXX0tpDTQC5mahorU-W{>ay&DLoGGRLbR`IMv|SgoBF99lD&m} zO4&Y+F8cS1XA60=(@US(tdOUVzEzjXOo_H3^Ef^<^Ss?3Sc*9v?_r3v4D7S+tM9wn zCvYMKWiath)?M^T%ag0a>VZ%xZg)mvfa7|dT)0xcWao+q%~(i z(kfAgi%FL)WBl^xHfC7}@qTlJrMcjKw*PVW{nBbpY{8greR$E6 zqAQkxmNyFo?r-V~_E&e`?r|(>ys%fhn;4$0RUY2zbiXMu5~N+5o#h-ej%tyM+0nBS z78JhIjmmkf2jv|;$Rm8yoKo}RxO~6jAsL(F%L*7p0htQE`HWQQuO4RGuue*P`sD_;8mj*qm?wV2R7#x8~BHa$W%AD z%K*}}nxong{lR_jTJ6wk?krzHwej9GbuMwLd|dpR&jpLY78>NF zx~-NHl=*dIa(4REy6W`z*i;fce?aR|18<5PgWNaD%r}KE)7O^2Ev-6li! z_#SzcoI!4K8lbi{NeQJTmM7j4T32Q|DNv9hcYIEuR(=(BTv<}31hNoVV_YNuJWxzk zjxe2{Zi-6p;_C8901ju~Fg|Q5EG3K=Hl@6z5HlT=w{(+S`tBA!b@y1^o{8)hL~OhC zx2qDX;;mbszAS1NZMSY`Z)a{zed#Y|WNMp%lQM^zCk-Y=U5vjtoGLFbj-#X~tGz0F zrTc1q`Vz%KX}wdmKG1H(qzj(aMG94AyvLR0dv=OVG2(I?zu{Ca#*}}uBJgtHqm7X5 zx7#7%)nu!8_oqoV8U3kCq=r-Vjx%Q@j~u!&_z%|Z`w58&4!kYthts#@lI_0O`3-F> zBrP04NcrS%z=+P2=FCHJrMdb&>GSDzqW)$l#NLZXy5f<>z9(cQ9N@O68Y~G@-zTl7 zuiJB0Q#W95qd!Iu92=6n^tez_Lt?L>ZKmycTj|Aw)DqjCs-!Byx~#k1TxI`M{%f|> zb#8`ZCvCOt?>zgMntbTYMR~Jg9VRO>Hx%dwtO} zbHMBJN@<#7a8+w|Zpq~9OJl@e!7cgs`g(dPKNt?J-#!9IgAYH5%HvVKs$b?49r8?Baf4 z{@|^~`927mS1C25tj;fFeq!aor)B%J>)AVq!M4|_5z-{AOBJbp;rjQ!qg`jj%VZxZ zT*24%Tzlk4!jKB;=xWO}ZinpF+#lGP zQyjuNUSoAMyOOkaV$Eh<&s!6lPR&kT ze6xI0BUQ_U4~wD8UmN@+d}oGY7dV7l+64VH1U6c?`@Sjf;+H!jpf%7b$I?cHP`0ga zI_afWhBX#TgA!pdZj+1j)Wr~2brXiL_&1!b$`dPVX_^xs7+e^3kKFE}x_o5Pl&VZ1 z>Mig9M6icB5LPP)Q&vA#&~Ica(6vk-MwFc_@r>xxpb(9!z^r|r}r z?gRo(1q$`|_m}pUlg9eGL1mPcm7!;4p|Y}4G!H3!Aco)^AcesT{|xf4I66qYi!T~S zKw~lB{kYButRF!Q0@+XW=lWSEG64NoCJg>pTeJqD`xdB-^jYYCgAvfE{{h>#{Dl4J z>t{OE{a`BAc%-j3*4rD2A*la#;;KI~{hRTxI)4Jq&;dvH(H{^Z`DyF_@TAj}OXQ(ct3jGh=-~BC+c&wM-zPmLBO;D#5_yh7c z^)HkvbpKj@yQZHr@Iy-*0(DkZ=$`|n&T2neAx|4`PXiq-s{qhquhwAze6UsXMdPPt zGURq?gi+7P3g%ft_&p)eS9TVzAO6Hu5<# diff --git a/WordPress/WordPressTest/Test Images/100x100.gif b/WordPress/WordPressTest/Test Images/100x100.gif deleted file mode 100644 index f70ac685ee572a228ea4aaa2341646ac3bb48ac5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmZ?wbhEHbOkqf2XkY+=|Ns9h{$ye00y1?#e2@$SQ*lrK%F}Q87th&pt9$dkJ-_+e z9(hcA*17D}scrALkAL#H{;l`%MdIbsvAS>1U7M>g#W|{pRgI{rtAg-1k2MaPU4J5Jnq@q;5X If`P#r0NM6XUH||9 diff --git a/WordPress/WordPressTest/Test Images/100x100.jpg b/WordPress/WordPressTest/Test Images/100x100.jpg deleted file mode 100644 index db3980bd4aa96122e1a3f5b89a91cae4658bd1ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4620 zcmcgv2UHW;`u}DoodgI01f+LCkPf0q2`v;ss$e4|frJu5Xo?M$#T8u(Dxz4?RZ$n) z0?Jwtds!75_6NF%sOw@u?D^kBiTmC;p7YNCz4PvzJKt}1?xkmN-_YVqrm|HXh6nvfItXC7hpy}R$+h$Y^WN?VfJ5raDe#|Abs^o zMN-*NW@YqONmNw;6+P*oUcM1mdkZ%b-!QU4U_6({Lo!|<7m1_=;=Dq|pWqXVWFC5u znY%l?fKL!Fnxq^LH5Il|LG5cMi*7A}eWczpdd;)9L9-MOSQB|%?v&WLm{49+h>9{R ziZ7Gp;MxGW(n5J$SdjVnlvH!-A3z5U)NMeMFDQ@&#>Pe|a}G*Xe)a9m4HYxE>ZHu2 zR{wiIM<^-~prx6J+|z{xf?R}KQSTGTPkHH3BLXo3hoR^rSr_@ff1 zt1t;MK|Tp_TmX_adU;GfU!DoTbU}LaVnHU#!;D4PMJkj?5iUb`j6lp6BHW7b@Z8)y zC4PpmJ@S_UuyhgjN&j2U%)eniKV6-l&(BojFT7ABnnbXqK$gRwrh52q-g0w_(Aue* zExI$s^6)q$4bb}C%+8BcV`=)>m_fY+u^YtVqVU8)EGXbA_o^yZ$PbPj#L}Fx%6G5Q zXGlVon^A=&g}j78EGh_zAH?#!IQ3W=a&F+Do-aq}Yiv<=qFOH&@znWC#R$qYGPaRt*P6!{=3;5sWFOmj+ z%ULj9JvX5!IAqXYBu!NFl@$i5$CBl!z7y5@h;qWz`hwzkb!;KpoP&8}g|X^2%;HBV z_oXTpq96>+ArW$*5ahszP>_HWM1`WMX#WCm^JLTHl1y=-c_8}YMCLrHzzaeU-eyL`scps=G6oE9aJb3zkSn1Tn5;i2Eb_irn8>}pxpy-=8&LBUaU%| z`Z1yZ9RsvL7YxA^tiT?ezzw`$6a+vBL_iEAKnhF-0f-?N&AbF=z#OQ6h3LPx64t;5 z*bLj@7uW~Qa0rgUDL4<8;X2%bPUwcG&M9L(B}b!5lF+%m*8Tg@3!f-NL%CUhF0I9w%@nu8W)Cw)hC# z2M@#}@dSJlo{8t#I??@C9hh$E2B8?)2lM+b+Qa))GX%T4+X*;Qjbb@r1 z)J5teeI~QW#$-pbFPTRkPtGKlkSoa5H)*PI*Y_r&6hgR7a{GHHONk7E&vytEs!FN2yn+-PE@<4VoFvjTTBvrAcXX zY1OnHv?H`Dv>w_Ax;EX0?n{rM3+dD7OXyqZE%ZzDZu$oXn_e%Xp=m>SnbvEl9*LlbxI941k zN5GlK*}^%=>DHy_+UfFivve2g{-S$Pw@*(;&qFUsZ>nCc-eJ8?eL~+>pQoRrzeInZ z{tf;2LrjJQ4iODmFr;C~l_3KLLk$89LTRW_$>25jwYlWphQHrqb7GqB^? zmD=sJyKB#~_qWftud}~4j55q;SoW}W!!A4E4xSEThgyeAj<}WF0{PLKHP;^~s(vc=_&tBz}g>s;3s z*OzV%ZUVPjw`=Y!_fYrQ?g!jocsP29Jl1>M^yGL(d(QW4^ZexH?IrhW@apxp_D=I& z>)qj_>l5p<$mh&R^2nf(vqv5p`OeqNx6pT=?_Z;wMrDuMG3xPXyV0W2TShx_aDBNmxvjxO za9D6<@Wl|Fki?KRA@@TqLo-8nhW3SdhD{G^4JU_3hA#{6;F<6Qyn0?=gm=Wuh!c^_ z$oR-Lkq@FAqU2F6(Rg%3^z!JtV{OJt$2P}63@>JR%)MB<*!M3QVyh2 zQv_rZTFOnz%gP`A5cb2)3e5_6 zMaTS6^EdoR`cd-Z#RcvQRxkKmDXcuV&}Cuu!jFpti_ZPz`cutMip3d=FD~&~vVJLL zsdVX$W&X>ySFx)~s~#?oUf#6Ad`0Do*DI&2JX`Hiyny8ustF2crUHx&5 zc+K_NpxWJQjn^(%JFrf;?#lXr^$i_J=!Vcgc4v?`hwwzHfZL?jOVc zvAff&^Ke&i*M$f15AHk^Jbd~`!5<&H%O9~GRri?n)IWB6e5g0H_wthoPr9GVo_=^% z@hAJwwa@LJ|N2+JUl;nu_jSLJzfinf^vdwnw$~o7+xlbs@4v}?^I@R!t>N44?|j~! zexLll=R?Uy>c^T-!#=fqj{JOAk*iS1_;S9g7Eqps@CISHgEZP>$dInJNE7WwXv!Bz`>Tb<0np@I(_Esxhq$% zUBA(B^VaRI2M_<~e$?~0_sNTwuU_}R8F>5dy^X8*zqEdqvH7CgH@u|70WUXCHwH!kER+Q1 diff --git a/WordPress/WordPressTest/Test Images/valid-gif-header.gif b/WordPress/WordPressTest/Test Images/valid-gif-header.gif deleted file mode 100755 index c9f3ee173650..000000000000 --- a/WordPress/WordPressTest/Test Images/valid-gif-header.gif +++ /dev/null @@ -1 +0,0 @@ -GIF89a \ No newline at end of file diff --git a/WordPress/WordPressTest/Test Images/valid-jpeg-header.jpg b/WordPress/WordPressTest/Test Images/valid-jpeg-header.jpg deleted file mode 100755 index ae4a6ef503de6dec1ff6974f42fa6bc55c5ee8c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12 Tcmex= Date: Tue, 24 Dec 2024 12:13:30 -0500 Subject: [PATCH 021/101] Update MediaItemHeaderView to use AsyncImageView instead of CachedAnimatedImageView --- .../ViewRelated/Cells/MediaItemHeaderView.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift index 6c5b67854f6a..4d19260cbacd 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift @@ -4,7 +4,7 @@ import WordPressShared import WordPressMedia final class MediaItemHeaderView: UIView { - let imageView = CachedAnimatedImageView() + let imageView = AsyncImageView() private let errorView = UIImageView() private let videoIconView = PlayIconView() private let loadingIndicator = UIActivityIndicatorView(style: .large) @@ -103,13 +103,7 @@ final class MediaItemHeaderView: UIView { Task { let image = try? await MediaImageService.shared.image(for: media, size: .large) loadingIndicator.stopAnimating() - - if let gif = image as? AnimatedImage, let data = gif.gifData { - imageView.animate(withGIFData: data) - } else { - imageView.image = image - } - + imageView.image = image errorView.isHidden = image != nil } From a3adfee150a5f57564755a151d466177bc874829 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:15:31 -0500 Subject: [PATCH 022/101] Fix code formatting in RichTextView --- .../Views/RichTextView/RichTextView.swift | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift b/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift index badf919f0774..637cebc2ddd4 100644 --- a/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift +++ b/WordPress/Classes/ViewRelated/Views/RichTextView/RichTextView.swift @@ -137,20 +137,20 @@ import UniformTypeIdentifiers // MARK: - Private Methods fileprivate func setupSubviews() { - gesturesRecognizer = UITapGestureRecognizer() + gesturesRecognizer = UITapGestureRecognizer() gesturesRecognizer.addTarget(self, action: #selector(RichTextView.handleTextViewTap(_:))) - textView = UITextView(frame: bounds) - textView.backgroundColor = backgroundColor - textView.contentInset = UIEdgeInsets.zero - textView.textContainerInset = UIEdgeInsets.zero - textView.textContainer.lineFragmentPadding = 0 - textView.layoutManager.allowsNonContiguousLayout = false - textView.isEditable = editable - textView.isScrollEnabled = false - textView.dataDetectorTypes = dataDetectorTypes - textView.delegate = self - textView.gestureRecognizers = [gesturesRecognizer] + textView = UITextView(frame: bounds) + textView.backgroundColor = backgroundColor + textView.contentInset = UIEdgeInsets.zero + textView.textContainerInset = UIEdgeInsets.zero + textView.textContainer.lineFragmentPadding = 0 + textView.layoutManager.allowsNonContiguousLayout = false + textView.isEditable = editable + textView.isScrollEnabled = false + textView.dataDetectorTypes = dataDetectorTypes + textView.delegate = self + textView.gestureRecognizers = [gesturesRecognizer] addSubview(textView) // Setup Layout @@ -183,8 +183,8 @@ import UniformTypeIdentifiers return } - let unwrappedView = attachmentView! - unwrappedView.frame.origin = self.textView.frameForTextInRange(range).integral.origin + let unwrappedView = attachmentView! + unwrappedView.frame.origin = self.textView.frameForTextInRange(range).integral.origin self.textView.addSubview(unwrappedView) self.attachmentViews.append(unwrappedView) } @@ -208,14 +208,16 @@ import UniformTypeIdentifiers // NOTE: Why do we need this? // Because this mechanism allows us to disable DataDetectors, and yet, detect taps on links. // - let textStorage = textView.textStorage - let layoutManager = textView.layoutManager - let textContainer = textView.textContainer - - let locationInTextView = recognizer.location(in: textView) - let characterIndex = layoutManager.characterIndex(for: locationInTextView, - in: textContainer, - fractionOfDistanceBetweenInsertionPoints: nil) + let textStorage = textView.textStorage + let layoutManager = textView.layoutManager + let textContainer = textView.textContainer + + let locationInTextView = recognizer.location(in: textView) + let characterIndex = layoutManager.characterIndex( + for: locationInTextView, + in: textContainer, + fractionOfDistanceBetweenInsertionPoints: nil + ) if characterIndex >= textStorage.length { return From 61871d0fd5f50715200b502d962f28d5ad3251ef Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:17:37 -0500 Subject: [PATCH 023/101] Update AnimatedGifAttachmentViewProvider to use GIFImageView directly --- .../RichTextView/AnimatedGifAttachmentViewProvider.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift b/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift index 65b20773431c..c9a36d9885c2 100644 --- a/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift +++ b/WordPress/Classes/ViewRelated/Views/RichTextView/AnimatedGifAttachmentViewProvider.swift @@ -1,4 +1,5 @@ import UIKit +import Gifu /** * This adds custom view rendering for animated Gif images in a UITextView @@ -7,11 +8,11 @@ import UIKit */ class AnimatedGifAttachmentViewProvider: NSTextAttachmentViewProvider { deinit { - guard let animatedImageView = view as? CachedAnimatedImageView else { + guard let animatedImageView = view as? GIFImageView else { return } - animatedImageView.stopAnimating() + animatedImageView.prepareForReuse() } override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { @@ -20,8 +21,8 @@ class AnimatedGifAttachmentViewProvider: NSTextAttachmentViewProvider { return } - let imageView = CachedAnimatedImageView(frame: parentView?.bounds ?? .zero) - imageView.setAnimatedImage(contents) + let imageView = GIFImageView(frame: parentView?.bounds ?? .zero) + imageView.animate(withGIFData: contents) view = imageView } From 4b3f4ef11618b373c5b9e97396457d5921ee8f6d Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:18:11 -0500 Subject: [PATCH 024/101] Remove SolidColorActivityIndicator --- .../Media/SolidColorActivityIndicator.swift | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift diff --git a/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift b/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift deleted file mode 100644 index 0a8a87876954..000000000000 --- a/WordPress/Classes/ViewRelated/Media/SolidColorActivityIndicator.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -final class SolidColorActivityIndicator: UIView, ActivityIndicatorType { - init(color: UIColor = .secondarySystemBackground) { - super.init(frame: .zero) - backgroundColor = color - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func startAnimating() { - isHidden = false - } - - func stopAnimating() { - isHidden = true - } -} From 79b3a0a813443bef52bf420ede2fa325eb0fad24 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:19:16 -0500 Subject: [PATCH 025/101] Remove CachedAnimatedImageView --- ...arProgressView+ActivityIndicatorType.swift | 2 +- .../Media/CachedAnimatedImageView.swift | 285 ------------------ 2 files changed, 1 insertion(+), 286 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift diff --git a/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift b/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift index b05174a15acc..e0cff74a813d 100644 --- a/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift +++ b/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift @@ -1,6 +1,6 @@ import UIKit -extension CircularProgressView: ActivityIndicatorType { +extension CircularProgressView { func startAnimating() { isHidden = false state = .indeterminate diff --git a/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift b/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift deleted file mode 100644 index 95e52342a294..000000000000 --- a/WordPress/Classes/ViewRelated/Media/CachedAnimatedImageView.swift +++ /dev/null @@ -1,285 +0,0 @@ -// -// Previously, we were using FLAnimatedImage to show gifs. (https://github.com/Flipboard/FLAnimatedImage) -// It's a good, battle-tested component written in Obj-c with a good solution for memory usage on big files. -// We decided to look for other alternatives and we got to Gifu. (https://github.com/kaishin/Gifu) -// - It has a similar approach to be memory efficient. Tests showed that is more memory efficient than FLAnimatedImage. -// - It's written in Swift, in a protocol oriented approach. That make it easier to implement it in a Swift code base. -// - It has extra features, like stopping and plying gifs, and a special `prepareForReuse` for table/collection views. -// - It seems to be more active, being updated few months ago, in contrast to a couple of years ago of FLAnimatedImage - -import Foundation -import Gifu - -@objc public protocol ActivityIndicatorType where Self: UIView { - func startAnimating() - func stopAnimating() -} - -extension UIActivityIndicatorView: ActivityIndicatorType { -} - -public class CachedAnimatedImageView: UIImageView, GIFAnimatable { - - public enum LoadingIndicatorStyle { - case centered(withSize: CGSize?) - case fullView - } - - // MARK: Public fields - - @objc public var gifStrategy: GIFStrategy { - get { - return gifPlaybackStrategy.gifStrategy - } - set(newGifStrategy) { - gifPlaybackStrategy = newGifStrategy.playbackStrategy - } - } - - @objc public private(set) var animatedGifData: Data? - - public lazy var animator: Gifu.Animator? = { - return Gifu.Animator(withDelegate: self) - }() - - @objc public var shouldShowLoadingIndicator: Bool = true - - // MARK: Private fields - - private var gifPlaybackStrategy: GIFPlaybackStrategy = MediumGIFPlaybackStrategy() - - @objc private var currentTask: URLSessionTask? - - private var customLoadingIndicator: ActivityIndicatorType? - - private var isImageAnimated: Bool { - animatedGifData != nil - } - - private lazy var defaultLoadingIndicator: UIActivityIndicatorView = { - let loadingIndicator = UIActivityIndicatorView(style: .medium) - layoutViewCentered(loadingIndicator, size: nil) - return loadingIndicator - }() - - private var loadingIndicator: ActivityIndicatorType { - guard let custom = customLoadingIndicator else { - return defaultLoadingIndicator - } - return custom - } - - // MARK: Initializers - - public override init(image: UIImage?, highlightedImage: UIImage?) { - super.init(image: image, highlightedImage: highlightedImage) - commonInit() - } - - public override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - public override init(image: UIImage?) { - super.init(image: image) - commonInit() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - } - - private func commonInit() { - NotificationCenter.default.addObserver(self, - selector: #selector(handleLowMemoryWarningNotification), - name: UIApplication.didReceiveMemoryWarningNotification, - object: nil) - } - - // MARK: - Public methods - - override open func display(_ layer: CALayer) { - // Fixes an unrecognized selector crash on iOS 13 and below when calling super.display(_:) directly - // This was first reported here: p5T066-1xs-p2#comment-5908 - // Investigating the issue I came across this discussion with a workaround in the Gifu repo: https://git.io/JUPxC - if UIImageView.instancesRespond(to: #selector(display(_:))) { - super.display(layer) - } - - updateImageIfNeeded() - } - - @objc public func setAnimatedImage(_ urlRequest: URLRequest, - placeholderImage: UIImage?, - success: (() -> Void)?, - failure: ((NSError?) -> Void)?) { - - currentTask?.cancel() - image = placeholderImage - - if checkCache(urlRequest, success) { - return - } - - let successBlock: (Data, UIImage?) -> Void = { [weak self] animatedImageData, staticImage in - self?.validateAndSetGifData(animatedImageData, alternateStaticImage: staticImage, success: success) - } - - currentTask = AnimatedImageCache.shared.animatedImage(urlRequest, - placeholderImage: placeholderImage, - success: successBlock, - failure: failure) - } - - @objc public func setAnimatedImage(_ animatedImageData: Data, success: (() -> Void)? = nil) { - currentTask?.cancel() - validateAndSetGifData(animatedImageData, alternateStaticImage: nil, success: success) - } - - /// Clean the image view from previous images and ongoing data tasks. - /// - @objc public func clean() { - currentTask?.cancel() - image = nil - animatedGifData = nil - } - - @objc public func prepForReuse() { - if isImageAnimated { - self.prepareForReuse() - } - } - - @objc public func startLoadingAnimation() { - guard shouldShowLoadingIndicator else { - return - } - DispatchQueue.main.async() { - self.loadingIndicator.startAnimating() - } - } - - @objc public func stopLoadingAnimation() { - DispatchQueue.main.async() { - self.loadingIndicator.stopAnimating() - } - } - - public func addLoadingIndicator(_ loadingIndicator: ActivityIndicatorType, style: LoadingIndicatorStyle) { - removeCustomLoadingIndicator() - customLoadingIndicator = loadingIndicator - addCustomLoadingIndicator(loadingIndicator, style: style) - } - - // MARK: - Private methods - - @objc private func handleLowMemoryWarningNotification(_ notification: NSNotification) { - stopAnimatingGIF() - } - - private func validateAndSetGifData(_ animatedImageData: Data, alternateStaticImage: UIImage? = nil, success: (() -> Void)? = nil) { - let didVerifyDataSize = gifPlaybackStrategy.verifyDataSize(animatedImageData) - DispatchQueue.main.async() { - if let staticImage = alternateStaticImage { - self.image = staticImage - } else { - self.image = UIImage(data: animatedImageData) - } - - DispatchQueue.global().async { - if didVerifyDataSize { - self.animate(data: animatedImageData, success: success) - } else { - self.animatedGifData = nil - success?() - } - } - } - } - - private func checkCache(_ urlRequest: URLRequest, _ success: (() -> Void)?) -> Bool { - if let cachedData = AnimatedImageCache.shared.cachedData(url: urlRequest.url) { - // Always attempt to load momentary image to show while gif is loading to avoid flashing. - if let cachedStaticImage = AnimatedImageCache.shared.cachedStaticImage(url: urlRequest.url) { - image = cachedStaticImage - } else { - animatedGifData = nil - let staticImage = UIImage(data: cachedData) - image = staticImage - AnimatedImageCache.shared.cacheStaticImage(url: urlRequest.url, image: staticImage) - } - - if gifPlaybackStrategy.verifyDataSize(cachedData) { - animate(data: cachedData, success: success) - } else { - success?() - } - - return true - } - - return false - } - - private func animate(data: Data, success: (() -> Void)?) { - animatedGifData = data - DispatchQueue.main.async() { - self.setFrameBufferCount(self.gifPlaybackStrategy.frameBufferCount) - self.animate(withGIFData: data, preparationBlock: { - success?() - }) - } - } - - // MARK: Loading indicator - - private func removeCustomLoadingIndicator() { - if let oldLoadingIndicator = customLoadingIndicator { - oldLoadingIndicator.removeFromSuperview() - } - } - - private func addCustomLoadingIndicator(_ loadingView: UIView, style: LoadingIndicatorStyle) { - switch style { - case .centered(let size): - layoutViewCentered(loadingView, size: size) - default: - layoutViewFullView(loadingView) - } - } - - // MARK: Layout - - private func prepareViewForLayout(_ view: UIView) { - if view.superview == nil { - addSubview(view) - } - view.translatesAutoresizingMaskIntoConstraints = false - } - - private func layoutViewCentered(_ view: UIView, size: CGSize?) { - prepareViewForLayout(view) - var constraints: [NSLayoutConstraint] = [ - view.centerXAnchor.constraint(equalTo: centerXAnchor), - view.centerYAnchor.constraint(equalTo: centerYAnchor) - ] - if let size { - constraints.append(view.heightAnchor.constraint(equalToConstant: size.height)) - constraints.append(view.widthAnchor.constraint(equalToConstant: size.width)) - } - NSLayoutConstraint.activate(constraints) - } - - private func layoutViewFullView(_ view: UIView) { - prepareViewForLayout(view) - NSLayoutConstraint.activate([ - view.leadingAnchor.constraint(equalTo: leadingAnchor), - view.trailingAnchor.constraint(equalTo: trailingAnchor), - view.topAnchor.constraint(equalTo: topAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - -} From c96c8449e0a8b36020321d0cdbe8c16fcbd855a9 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:19:35 -0500 Subject: [PATCH 026/101] Remove GIFPlaybackStrategy --- .../Utility/Media/GIFPlaybackStrategy.swift | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 WordPress/Classes/Utility/Media/GIFPlaybackStrategy.swift diff --git a/WordPress/Classes/Utility/Media/GIFPlaybackStrategy.swift b/WordPress/Classes/Utility/Media/GIFPlaybackStrategy.swift deleted file mode 100644 index 4a4f80a16a50..000000000000 --- a/WordPress/Classes/Utility/Media/GIFPlaybackStrategy.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation - -@objc -public enum GIFStrategy: Int { - case tinyGIFs - case smallGIFs - case mediumGIFs - case largeGIFs - - /// Returns the corresponding playback strategy instance - /// - var playbackStrategy: GIFPlaybackStrategy { - switch self { - case .tinyGIFs: - return TinyGIFPlaybackStrategy() - case .smallGIFs: - return SmallGIFPlaybackStrategy() - case .mediumGIFs: - return MediumGIFPlaybackStrategy() - case .largeGIFs: - return LargeGIFPlaybackStrategy() - } - } -} - -public protocol GIFPlaybackStrategy { - /// Maximum size GIF data can be in order to be animated. - /// - var maxSize: Int { get } - - /// The number of frames that should be buffered. A high number will result in more - /// memory usage and less CPU load, and vice versa. Default is 50. - /// - var frameBufferCount: Int { get } - - /// Returns the coresponding GIFStrategy enum value. - /// - var gifStrategy: GIFStrategy { get } - - /// Verifies the GIF data against the `maxSize` var. - /// - /// - Parameter data: object containg the GIF - /// - Returns: **true** if data is under the maximum size limit (inclusive) and **false** if over the limit - /// - func verifyDataSize(_ data: Data) -> Bool -} - -extension GIFPlaybackStrategy { - func verifyDataSize(_ data: Data) -> Bool { - guard data.count <= maxSize else { - DDLogDebug("⚠️ Maximum GIF data size exceeded \(maxSize) with \(data.count)") - return false - } - return true - } -} -// This is good for thumbnail GIFs used in a collection view -class TinyGIFPlaybackStrategy: GIFPlaybackStrategy { - var maxSize = 2_000_000 // in MB - var frameBufferCount = 5 - var gifStrategy: GIFStrategy = .tinyGIFs -} - -class SmallGIFPlaybackStrategy: GIFPlaybackStrategy { - var maxSize = 8_000_000 // in MB - var frameBufferCount = 50 - var gifStrategy: GIFStrategy = .smallGIFs -} - -class MediumGIFPlaybackStrategy: GIFPlaybackStrategy { - var maxSize = 20_000_000 // in MB - var frameBufferCount = 50 - var gifStrategy: GIFStrategy = .mediumGIFs -} - -class LargeGIFPlaybackStrategy: GIFPlaybackStrategy { - var maxSize = 50_000_000 // in MB - var frameBufferCount = 50 - var gifStrategy: GIFStrategy = .largeGIFs -} From 3f270d263beaf632dc9def1171b9d783580a2799 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:54:23 -0500 Subject: [PATCH 027/101] Update EditorMediaUtility to use ImageDownloader directly (without AuthenticatedImageDownload redirect) --- .../Gutenberg/EditorMediaUtility.swift | 29 ++++++++++--------- .../Gutenberg/GutenbergImageLoader.swift | 18 ------------ .../Utils/GutenbergMediaEditorImage.swift | 2 +- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift index c740486437b5..c6dd9ff97a38 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift @@ -137,17 +137,18 @@ class EditorMediaUtility { callbackQueue.async { failure(error) } - return EmptyImageDownloaderTask() - case let .success((requestURL, mediaHost)): - let imageDownload = AuthenticatedImageDownload( - url: requestURL, - mediaHost: mediaHost, - callbackQueue: callbackQueue, - onSuccess: success, - onFailure: failure - ) - imageDownload.start() - return imageDownload + return MeediaUtilityTask { /* do nothing */ } + case let .success((imageURL, host)): + let task = Task { @MainActor in + do { + let image = try await ImageDownloader.shared.image(from: imageURL, host: host) + success(image) + } catch { + failure(error) + + } + } + return MeediaUtilityTask { task.cancel() } } } @@ -251,8 +252,10 @@ class EditorMediaUtility { } } -private class EmptyImageDownloaderTask: ImageDownloaderTask { +private struct MeediaUtilityTask: ImageDownloaderTask { + let closure: @Sendable () -> Void + func cancel() { - // Do nothing + closure() } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergImageLoader.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergImageLoader.swift index a0ce0202c7c6..2a1b46e5f9c6 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergImageLoader.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergImageLoader.swift @@ -19,12 +19,6 @@ class GutenbergImageLoader: NSObject, RCTImageURLLoader { } func loadImage(for imageURL: URL, size: CGSize, scale: CGFloat, resizeMode: RCTResizeMode, progressHandler: RCTImageLoaderProgressBlock, partialLoadHandler: RCTImageLoaderPartialLoadBlock, completionHandler: @escaping RCTImageLoaderCompletionBlock) -> RCTImageLoaderCancellationBlock? { - let cacheKey = getCacheKey(for: imageURL, size: size) - - if let image = AnimatedImageCache.shared.cachedStaticImage(url: cacheKey) { - completionHandler(nil, image) - return {} - } var finalSize = size var finalScale = scale @@ -36,7 +30,6 @@ class GutenbergImageLoader: NSObject, RCTImageURLLoader { } let task = mediaUtility.downloadImage(from: imageURL, size: finalSize, scale: finalScale, post: post, success: { (image) in - AnimatedImageCache.shared.cacheStaticImage(url: cacheKey, image: image) completionHandler(nil, image) }, onFailure: { (error) in completionHandler(error, nil) @@ -56,17 +49,6 @@ class GutenbergImageLoader: NSObject, RCTImageURLLoader { return nil } - private func getCacheKey(for url: URL, size: CGSize) -> URL? { - guard size != CGSize.zero else { - return url - } - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) - let queryItems = components?.queryItems - let newQueryItems = (queryItems ?? []) + [URLQueryItem(name: "cachekey", value: "\(size)")] - components?.queryItems = newQueryItems - return components?.url - } - static func moduleName() -> String! { return String(describing: self) } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift index 9f4b34ae79f3..a3882df36f10 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift @@ -32,7 +32,7 @@ class GutenbergMediaEditorImage: AsyncImage { init(url: URL, post: AbstractPost) { originalURL = url self.post = post - thumb = AnimatedImageCache.shared.cachedStaticImage(url: originalURL) + thumb = ImageDownloader.shared.cachedImage(for: originalURL) } /** From dca254863715c30fad66a831e46a95c33c8b7026 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 12:57:51 -0500 Subject: [PATCH 028/101] Remove AuthenticatedImageDownload --- .../Gutenberg/EditorMediaUtility.swift | 60 ++----------------- 1 file changed, 5 insertions(+), 55 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift index c6dd9ff97a38..225583627599 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift @@ -4,60 +4,6 @@ import Gridicons import WordPressShared import WordPressMedia -final class AuthenticatedImageDownload: AsyncOperation, @unchecked Sendable { - enum DownloadError: Error { - case blogNotFound - } - - let url: URL - let mediaHost: MediaHost - private let callbackQueue: DispatchQueue - private let onSuccess: (UIImage) -> () - private let onFailure: (Error) -> () - - init(url: URL, mediaHost: MediaHost, callbackQueue: DispatchQueue, onSuccess: @escaping (UIImage) -> (), onFailure: @escaping (Error) -> ()) { - self.url = url - self.mediaHost = mediaHost - self.callbackQueue = callbackQueue - self.onSuccess = onSuccess - self.onFailure = onFailure - } - - override func main() { - let mediaRequestAuthenticator = MediaRequestAuthenticator() - mediaRequestAuthenticator.authenticatedRequest( - for: url, - from: mediaHost, - onComplete: { request in - ImageDownloader.shared.downloadImage(for: request) { (image, error) in - self.state = .isFinished - - self.callbackQueue.async { - guard let image else { - DDLogError("Unable to download image for attachment with url = \(String(describing: request.url)). Details: \(String(describing: error?.localizedDescription))") - if let error { - self.onFailure(error) - } else { - self.onFailure(NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil)) - } - - return - } - - self.onSuccess(image) - } - } - }, - onFailure: { error in - self.state = .isFinished - self.callbackQueue.async { - self.onFailure(error) - } - } - ) - } -} - class EditorMediaUtility { private static let InternalInconsistencyError = NSError(domain: NSExceptionName.internalInconsistencyException.rawValue, code: 0) @@ -65,6 +11,10 @@ class EditorMediaUtility { static let placeholderDocumentLink = URL(string: "documentUploading://")! } + enum DownloadError: Error { + case blogNotFound + } + func placeholderImage(for attachment: NSTextAttachment, size: CGSize, tintColor: UIColor?) -> UIImage { var icon: UIImage switch attachment { @@ -161,7 +111,7 @@ class EditorMediaUtility { ) throws -> (URL, MediaHost) { // This function is added to debug the issue linked below. let safeExistingObject: (NSManagedObjectID) throws -> NSManagedObject = { objectID in - var object: Result = .failure(AuthenticatedImageDownload.DownloadError.blogNotFound) + var object: Result = .failure(DownloadError.blogNotFound) do { // Catch an Objective-C `NSInvalidArgumentException` exception from `existingObject(with:)`. // See https://github.com/wordpress-mobile/WordPress-iOS/issues/20630 From f9a23154de1f914f980b9661665841da32ac6676 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 13:11:34 -0500 Subject: [PATCH 029/101] Update MediaExternalExporter to use ImageDownloader for downloading GIF data --- .../Utility/Media/MediaExternalExporter.swift | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift index 75dfc1bbbe69..07f9e5e681b4 100644 --- a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift @@ -49,21 +49,16 @@ class MediaExternalExporter: MediaExporter { /// Downloads an external GIF file, or uses one from the AnimatedImageCache. /// private func downloadGif(from url: URL, onCompletion: @escaping OnMediaExport, onError: @escaping OnExportError) -> Progress { - let request = URLRequest(url: url) - let task = AnimatedImageCache.shared.animatedImage(request, placeholderImage: nil, - success: { (data, _) in - self.gifDataDownloaded(data: data, - fromURL: url, - error: nil, - onCompletion: onCompletion, - onError: onError) - }, failure: { error in - if let error { - onError(self.exporterErrorWith(error: error)) + Task { + do { + let options = ImageRequestOptions(isMemoryCacheEnabled: false) + let data = try await ImageDownloader.shared.data(for: ImageRequest(url: url, options: options)) + self.gifDataDownloaded(data: data, fromURL: url, error: nil, onCompletion: onCompletion, onError: onError) + } catch { + onError(ExportError.downloadError(error as NSError)) } - }) - - return task?.progress ?? Progress.discreteCompletedProgress() + } + return Progress.discreteCompletedProgress() } /// Saves downloaded GIF data to the filesystem and exports it. From 8ad55b8da6ba0f6faecc045ae3300ed676c04bd1 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 13:12:04 -0500 Subject: [PATCH 030/101] Remove AnimatedImageCache --- .../Utility/Media/MediaExternalExporter.swift | 2 - .../Media/MemoryCache+Extensions.swift | 2 - .../Media/AnimatedImageCache.swift | 99 ------------------- 3 files changed, 103 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Media/AnimatedImageCache.swift diff --git a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift index 07f9e5e681b4..25809dfccba3 100644 --- a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift @@ -46,8 +46,6 @@ class MediaExternalExporter: MediaExporter { return Progress.discreteCompletedProgress() } - /// Downloads an external GIF file, or uses one from the AnimatedImageCache. - /// private func downloadGif(from url: URL, onCompletion: @escaping OnMediaExport, onError: @escaping OnExportError) -> Progress { Task { do { diff --git a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift index 48e208b35a4e..1cbada8b8a06 100644 --- a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift +++ b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift @@ -13,8 +13,6 @@ extension MemoryCache { UIImageView.af.sharedImageDownloader = AlamofireImage.ImageDownloader( imageCache: AlamofireImageCacheAdapter(cache: .shared) ) - - // WordPress.AnimatedImageCache uses WordPress.MemoryCache directly } } diff --git a/WordPress/Classes/ViewRelated/Media/AnimatedImageCache.swift b/WordPress/Classes/ViewRelated/Media/AnimatedImageCache.swift deleted file mode 100644 index 47d0f2b782f2..000000000000 --- a/WordPress/Classes/ViewRelated/Media/AnimatedImageCache.swift +++ /dev/null @@ -1,99 +0,0 @@ -import UIKit -import WordPressMedia - -/// AnimatedImageCache is an image + animated gif data cache used in -/// CachedAnimatedImageView. It should be accessed via the `shared` singleton. -/// -final class AnimatedImageCache { - - // MARK: Singleton - - static let shared: AnimatedImageCache = AnimatedImageCache() - - private init() {} - - // MARK: Private fields - - fileprivate lazy var session: URLSession = { - let sessionConfiguration = URLSessionConfiguration.default - let session = URLSession(configuration: sessionConfiguration) - return session - }() - - // MARK: Instance methods - - func cacheData(data: Data, url: URL?) { - guard let url else { return } - let key = url.absoluteString + Constants.keyDataSuffix - MemoryCache.shared.setData(data, forKey: key) - } - - func cachedData(url: URL?) -> Data? { - guard let url else { return nil } - let key = url.absoluteString + Constants.keyDataSuffix - return MemoryCache.shared.geData(forKey: key) - } - - func cacheStaticImage(url: URL?, image: UIImage?) { - guard let url, let image else { return } - let key = url.absoluteString + Constants.keyStaticImageSuffix - MemoryCache.shared.setImage(image, forKey: key) - } - - func cachedStaticImage(url: URL?) -> UIImage? { - guard let url else { return nil } - let key = url.absoluteString + Constants.keyStaticImageSuffix - return MemoryCache.shared.getImage(forKey: key) - } - - func animatedImage(_ urlRequest: URLRequest, - placeholderImage: UIImage?, - success: ((Data, UIImage?) -> Void)?, - failure: ((NSError?) -> Void)? ) -> URLSessionTask? { - - if let cachedImageData = cachedData(url: urlRequest.url) { - success?(cachedImageData, cachedStaticImage(url: urlRequest.url)) - return nil - } - - let task = session.dataTask(with: urlRequest, completionHandler: { [weak self] (data, response, error) in - //check if view is still here - guard let self else { - return - } - // check if there is an error - if let error { - let nsError = error as NSError - // task.cancel() triggers an error that we don't want to send to the error handler. - if nsError.code != NSURLErrorCancelled { - failure?(nsError) - } - return - } - // check if data is here and is animated gif - guard let data else { - failure?(nil) - return - } - - let staticImage = UIImage(data: data) - if let key = urlRequest.url { - self.cacheData(data: data, url: key) - self.cacheStaticImage(url: key, image: staticImage) - } - success?(data, staticImage) - }) - - task.resume() - return task - } -} - -// MARK: - Constants - -private extension AnimatedImageCache { - struct Constants { - static let keyDataSuffix = "_data" - static let keyStaticImageSuffix = "_static_image" - } -} From f013a313c218176e41463e9eb996b6625cdb18a4 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 13:18:31 -0500 Subject: [PATCH 031/101] Remove remaining AlamofireImage usages from the anouncement cells --- .../WhatsNew/Views/AnnouncementCell.swift | 3 ++- .../Views/DashboardCustomAnnouncementCell.swift | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/AnnouncementCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/AnnouncementCell.swift index ec80d65d9df4..7be2473e8678 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/AnnouncementCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/AnnouncementCell.swift @@ -1,3 +1,4 @@ +import UIKit class AnnouncementCell: AnnouncementTableViewCell { @@ -59,7 +60,7 @@ class AnnouncementCell: AnnouncementTableViewCell { } else if let url = URL(string: feature.iconUrl) { - announcementImageView.af.setImage(withURL: url) + announcementImageView.wp.setImage(with: url) } headingLabel.text = feature.title subHeadingLabel.text = feature.subtitle diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift index a42d26e5c286..63d1832e235d 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift @@ -65,20 +65,15 @@ class DashboardCustomAnnouncementCell: AnnouncementTableViewCell { func configure(feature: WordPressKit.Feature) { if let url = URL(string: feature.iconUrl) { - announcementImageView.af.setImage(withURL: url, completion: { [weak self] response in - - guard let self, - let width = response.value?.size.width, - let height = response.value?.size.height else { + announcementImageView.wp.setImage(with: url) { [weak self] result in + guard let self, case .success(let image) = result else { return } - - let aspectRatio = width / height - + let aspectRatio = image.size.width / image.size.height NSLayoutConstraint.activate([ self.announcementImageView.widthAnchor.constraint(equalTo: self.announcementImageView.heightAnchor, multiplier: aspectRatio) ]) - }) + } } headingLabel.text = feature.subtitle } From f27c41d1edf4bcaaa13dc97fff9ff777b6205a50 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 13:19:06 -0500 Subject: [PATCH 032/101] Remove AlamofireImageCacheAdapter --- .../Media/MemoryCache+Extensions.swift | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift index 1cbada8b8a06..dc3e2cf3c11e 100644 --- a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift +++ b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift @@ -1,6 +1,5 @@ import UIKit import WordPressMedia -import AlamofireImage import WordPressUI extension MemoryCache { @@ -8,11 +7,6 @@ extension MemoryCache { func register() { // WordPressUI WordPressUI.ImageCache.shared = WordpressUICacheAdapter(cache: .shared) - - // AlamofireImage - UIImageView.af.sharedImageDownloader = AlamofireImage.ImageDownloader( - imageCache: AlamofireImageCacheAdapter(cache: .shared) - ) } } @@ -27,45 +21,3 @@ private struct WordpressUICacheAdapter: WordPressUI.ImageCaching { cache.getImage(forKey: key) } } - -private struct AlamofireImageCacheAdapter: AlamofireImage.ImageRequestCache { - let cache: MemoryCache - - func image(for request: URLRequest, withIdentifier identifier: String?) -> AlamofireImage.Image? { - image(withIdentifier: cacheKey(for: request, identifier: identifier)) - } - - func add(_ image: AlamofireImage.Image, for request: URLRequest, withIdentifier identifier: String?) { - add(image, withIdentifier: cacheKey(for: request, identifier: identifier)) - } - - func removeImage(for request: URLRequest, withIdentifier identifier: String?) -> Bool { - removeImage(withIdentifier: cacheKey(for: request, identifier: identifier)) - } - - func image(withIdentifier identifier: String) -> AlamofireImage.Image? { - cache.getImage(forKey: identifier) - } - - func add(_ image: AlamofireImage.Image, withIdentifier identifier: String) { - cache.setImage(image, forKey: identifier) - } - - func removeImage(withIdentifier identifier: String) -> Bool { - cache.removeImage(forKey: identifier) - return true - } - - func removeAllImages() -> Bool { - // Do nothing (the app decides when to remove images) - return true - } - - private func cacheKey(for request: URLRequest, identifier: String?) -> String { - var key = request.url?.absoluteString ?? "" - if let identifier { - key += "-\(identifier)" - } - return key - } -} From 129bc3ebf6dbcaae9f6c4f306b95fc7c82118e21 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 13:20:28 -0500 Subject: [PATCH 033/101] Remove AlamofireImage --- Modules/Package.swift | 2 -- .../xcshareddata/swiftpm/Package.resolved | 11 +---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 784600c6d15e..8c475f26cfd0 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -18,7 +18,6 @@ let package = Package( dependencies: [ .package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"), .package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.1"), - .package(url: "https://github.com/Alamofire/AlamofireImage", from: "4.3.0"), .package(url: "https://github.com/AliSoftware/OHHTTPStubs", from: "9.1.0"), .package(url: "https://github.com/Automattic/Automattic-Tracks-iOS", from: "3.4.2"), .package(url: "https://github.com/Automattic/AutomatticAbout-swift", from: "1.1.4"), @@ -146,7 +145,6 @@ enum XcodeSupport { "WordPressMedia", "WordPressUI", .product(name: "Alamofire", package: "Alamofire"), - .product(name: "AlamofireImage", package: "AlamofireImage"), .product(name: "AutomatticAbout", package: "AutomatticAbout-swift"), .product(name: "AutomatticTracks", package: "Automattic-Tracks-iOS"), .product(name: "CocoaLumberjack", package: "CocoaLumberjack"), diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4888e01b33f6..e3f213f8d1e8 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e67326cf4e0b967f85976c160b6e3742a401276eabf3e18b25fce8bb219e1350", + "originHash" : "2325eaeb036deffbb1d475c9c1b62fef474fe61fdbea5d4335dd314f4bd5cab6", "pins" : [ { "identity" : "alamofire", @@ -10,15 +10,6 @@ "version" : "5.9.1" } }, - { - "identity" : "alamofireimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/AlamofireImage", - "state" : { - "revision" : "1eaf3b6c6882bed10f6e7b119665599dd2329aa1", - "version" : "4.3.0" - } - }, { "identity" : "automattic-tracks-ios", "kind" : "remoteSourceControl", From eece5e96c7ba5bb3c683c479bc49db9ca66a9b85 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 11:45:52 -0500 Subject: [PATCH 034/101] Add ImagePrefetcher --- Modules/Package.swift | 5 +- .../WordPressMedia/ImagePrefetcher.swift | 108 ++++++++++++++++++ .../Sources/WordPressMedia/ImageRequest.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 11 +- .../Media/ImageDownloader+Extensions.swift | 6 + .../Reader/Cards/ReaderPostCell.swift | 20 ++-- .../ReaderStreamViewController.swift | 26 +++++ 7 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 Modules/Sources/WordPressMedia/ImagePrefetcher.swift diff --git a/Modules/Package.swift b/Modules/Package.swift index 8c475f26cfd0..36669a34ba12 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -19,6 +19,7 @@ let package = Package( .package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"), .package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.1"), .package(url: "https://github.com/AliSoftware/OHHTTPStubs", from: "9.1.0"), + .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), .package(url: "https://github.com/Automattic/Automattic-Tracks-iOS", from: "3.4.2"), .package(url: "https://github.com/Automattic/AutomatticAbout-swift", from: "1.1.4"), .package(url: "https://github.com/Automattic/Gravatar-SDK-iOS", from: "3.1.0"), @@ -58,7 +59,9 @@ let package = Package( .product(name: "XCUITestHelpers", package: "XCUITestHelpers"), ], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]), - .target(name: "WordPressMedia"), + .target(name: "WordPressMedia", dependencies: [ + .product(name: "Collections", package: "swift-collections"), + ]), .target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressShared", dependencies: [.target(name: "WordPressSharedObjC")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressTesting", resources: [.process("Resources")]), diff --git a/Modules/Sources/WordPressMedia/ImagePrefetcher.swift b/Modules/Sources/WordPressMedia/ImagePrefetcher.swift new file mode 100644 index 000000000000..d40ace3039ce --- /dev/null +++ b/Modules/Sources/WordPressMedia/ImagePrefetcher.swift @@ -0,0 +1,108 @@ +import UIKit +import Collections + +@ImageDownloaderActor +public final class ImagePrefetcher { + private let downloader: ImageDownloader + private let maxConcurrentTasks: Int + private var queue = OrderedDictionary() + private var numberOfActiveTasks = 0 + + deinit { + let tasks = queue.values.compactMap(\.task) + for task in tasks { + task.cancel() + } + } + + public nonisolated init(downloader: ImageDownloader, maxConcurrentTasks: Int = 2) { + self.downloader = downloader + self.maxConcurrentTasks = maxConcurrentTasks + } + + public nonisolated func startPrefetching(for requests: [ImageRequest]) { + Task { @ImageDownloaderActor in + for request in requests { + startPrefetching(for: request) + } + performPendingTasks() + } + } + + private func startPrefetching(for request: ImageRequest) { + let key = PrefetchKey(request: request) + guard queue[key] == nil else { + return + } + queue[key] = PrefetchTask() + } + + private func performPendingTasks() { + var index = 0 + func nextPendingTask() -> (PrefetchKey, PrefetchTask)? { + while index < queue.count { + if queue.elements[index].value.task == nil { + return queue.elements[index] + } + index += 1 + } + return nil + } + while numberOfActiveTasks < maxConcurrentTasks, let (key, task) = nextPendingTask() { + task.task = Task { + await self.actuallyPrefetchImage(for: key.request) + } + numberOfActiveTasks += 1 + } + } + + private func actuallyPrefetchImage(for request: ImageRequest) async { + _ = try? await downloader.image(for: request) + + numberOfActiveTasks -= 1 + queue[PrefetchKey(request: request)] = nil + performPendingTasks() + } + + public nonisolated func stopPrefetching(for requests: [ImageRequest]) { + Task { @ImageDownloaderActor in + for request in requests { + stopPrefetching(for: request) + } + performPendingTasks() + } + } + + private func stopPrefetching(for request: ImageRequest) { + let key = PrefetchKey(request: request) + if let task = queue.removeValue(forKey: key) { + task.task?.cancel() + } + } + + public nonisolated func stopAll() { + Task { @ImageDownloaderActor in + for (_, value) in queue { + value.task?.cancel() + } + queue.removeAll() + } + } + + private struct PrefetchKey: Hashable, Sendable { + let request: ImageRequest + + func hash(into hasher: inout Hasher) { + request.source.url?.hash(into: &hasher) + } + + static func == (lhs: PrefetchKey, rhs: PrefetchKey) -> Bool { + let (lhs, rhs) = (lhs.request, rhs.request) + return (lhs.source.url, lhs.options) == (rhs.source.url, rhs.options) + } + } + + private final class PrefetchTask: @unchecked Sendable { + var task: Task? + } +} diff --git a/Modules/Sources/WordPressMedia/ImageRequest.swift b/Modules/Sources/WordPressMedia/ImageRequest.swift index 3c77b28fe0cb..e5c811381183 100644 --- a/Modules/Sources/WordPressMedia/ImageRequest.swift +++ b/Modules/Sources/WordPressMedia/ImageRequest.swift @@ -27,7 +27,7 @@ public final class ImageRequest: Sendable { } } -public struct ImageRequestOptions: Sendable { +public struct ImageRequestOptions: Hashable, Sendable { /// Resize the thumbnail to the given size (in pixels). By default, `nil`. public var size: CGSize? diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index e3f213f8d1e8..eb6f17315f2a 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2325eaeb036deffbb1d475c9c1b62fef474fe61fdbea5d4335dd314f4bd5cab6", + "originHash" : "e79c26721ac0bbd7fe1003896d175bc4293a42c53ed03372aca8310d5da175ed", "pins" : [ { "identity" : "alamofire", @@ -306,6 +306,15 @@ "version" : "2.3.1" } }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift index a35dadacf692..3cc28ddf4250 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift @@ -5,6 +5,12 @@ extension ImageDownloader { nonisolated static let shared = ImageDownloader(authenticator: MediaRequestAuthenticator()) } +extension ImagePrefetcher { + convenience nonisolated init() { + self.init(downloader: .shared) + } +} + // MARK: - ImageDownloader (Closures) extension ImageDownloader { diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index c1b28ec48686..844d35587976 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -50,6 +50,15 @@ final class ReaderPostCell: ReaderStreamBaseCell { contentViewConstraints = view.pinEdges(.horizontal, to: isCompact ? contentView : contentView.readableContentGuide) super.updateConstraints() } + + static func preferredCoverSize(in window: UIWindow, isCompact: Bool) -> CGSize { + var coverWidth = ReaderPostCell.regularCoverWidth + if isCompact { + coverWidth = min(window.bounds.width, window.bounds.height) - ReaderStreamBaseCell.insets.left * 2 + } + return CGSize(width: coverWidth, height: coverWidth) + .scaled(by: min(2, window.traitCollection.displayScale)) + } } private final class ReaderPostCellView: UIView { @@ -307,16 +316,7 @@ private final class ReaderPostCellView: UIView { private var preferredCoverSize: CGSize? { guard let window = window ?? UIApplication.shared.mainWindow else { return nil } - return Self.preferredCoverSize(in: window, isCompact: isCompact) - } - - static func preferredCoverSize(in window: UIWindow, isCompact: Bool) -> CGSize { - var coverWidth = ReaderPostCell.regularCoverWidth - if isCompact { - coverWidth = min(window.bounds.width, window.bounds.height) - ReaderStreamBaseCell.insets.left * 2 - } - return CGSize(width: coverWidth, height: coverWidth) - .scaled(by: min(2, window.traitCollection.displayScale)) + return ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) } private func configureToolbar(with viewModel: ReaderPostToolbarViewModel) { diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index ae6bca99e855..5b8b266697c2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -2,6 +2,7 @@ import Foundation import SVProgressHUD import WordPressShared import WordPressFlux +import WordPressMedia import UIKit import Combine import WordPressUI @@ -88,6 +89,8 @@ import AutomatticTracks /// Configuration of cells private let cellConfiguration = ReaderCellConfiguration() + private let prefetcher = ImagePrefetcher() + enum NavigationItemTag: Int { case notifications case share @@ -477,6 +480,7 @@ import AutomatticTracks tableViewController.didMove(toParent: self) tableConfiguration.setup(tableView) tableView.delegate = self + tableView.prefetchDataSource = self } @objc func configureRefreshControl() { @@ -1494,6 +1498,28 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { } } +extension ReaderStreamViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + prefetcher.startPrefetching(for: makeImageRequests(for: indexPaths)) + } + + func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { + prefetcher.stopPrefetching(for: makeImageRequests(for: indexPaths)) + + } + + private func makeImageRequests(for indexPaths: [IndexPath]) -> [ImageRequest] { + guard let window = view.window else { return [] } + let targetSize = ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) + return indexPaths.compactMap { + guard let imageURL = getPost(at: $0)?.featuredImageURLForDisplay() else { + return nil + } + return ImageRequest(url: imageURL, options: ImageRequestOptions(size: targetSize)) + } + } +} + // MARK: - SearchableActivity Conformance extension ReaderStreamViewController: SearchableActivityConvertable { From 49df350c37f7231cf8f101492808b6fe8df2ceae Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 14:01:40 -0500 Subject: [PATCH 035/101] Update releaes notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b19d2671241a..ffda07ddec97 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,7 @@ 25.7 ----- * [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] +* [*] Add prefetching to Reader streams [#23928] 25.6 ----- From 1e5f0e80b0ee072e28cade7912b42c4de6c2d9e7 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 16:01:53 -0500 Subject: [PATCH 036/101] Add ImageRequest support in AsyncImageView --- .../WordPressMedia/ImageDownloader.swift | 6 +++++ .../Utility/Media/AsyncImageView.swift | 10 ++++++--- .../Media/ImageLoadingController.swift | 22 ++++--------------- .../Media/UIImageView+ImageDownloader.swift | 13 +++++------ .../LightboxImagePageViewController.swift | 3 ++- .../Views/ReaderDetailFeaturedImageView.swift | 2 +- .../Views/WPRichText/WPRichTextImage.swift | 2 +- .../DashboardCustomAnnouncementCell.swift | 3 ++- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Modules/Sources/WordPressMedia/ImageDownloader.swift b/Modules/Sources/WordPressMedia/ImageDownloader.swift index 08e6b907bd43..384aac235b76 100644 --- a/Modules/Sources/WordPressMedia/ImageDownloader.swift +++ b/Modules/Sources/WordPressMedia/ImageDownloader.swift @@ -69,6 +69,12 @@ public final class ImageDownloader { // MARK: - Caching + /// Returns an image from the memory cache. + nonisolated public func cachedImage(for request: ImageRequest) -> UIImage? { + guard let imageURL = request.source.url else { return nil } + return cachedImage(for: imageURL, size: request.options.size) + } + /// Returns an image from the memory cache. /// /// - note: Use it to retrieve the image synchronously, which is no not possible diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index 5f071c43ae31..16b0b48df0a9 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -84,10 +84,14 @@ final class AsyncImageView: UIView { func setImage( with imageURL: URL, host: MediaHost? = nil, - size: CGSize? = nil, - completion: (@MainActor (Result) -> Void)? = nil + size: CGSize? = nil ) { - controller.setImage(with: imageURL, host: host, size: size, completion: completion) + let request = ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size)) + controller.setImage(with: request) + } + + func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { + controller.setImage(with: request, completion: completion) } private func setState(_ state: ImageLoadingController.State) { diff --git a/WordPress/Classes/Utility/Media/ImageLoadingController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift index a1e0a2c41933..1611a2c2aed1 100644 --- a/WordPress/Classes/Utility/Media/ImageLoadingController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -27,28 +27,17 @@ final class ImageLoadingController { } /// - parameter completion: Gets called on completion _after_ `onStateChanged`. - func setImage( - with imageURL: URL, - host: MediaHost? = nil, - size: CGSize? = nil, - completion: (@MainActor (Result) -> Void)? = nil - ) { + func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { task?.cancel() - if let image = downloader.cachedImage(for: imageURL, size: size) { + if let image = downloader.cachedImage(for: request) { onStateChanged(.success(image)) completion?(.success(image)) } else { onStateChanged(.loading) task = Task { @MainActor [downloader, weak self] in do { - let options = ImageRequestOptions(size: size) - let image: UIImage - if let host { - image = try await downloader.image(from: imageURL, host: host, options: options) - } else { - image = try await downloader.image(from: imageURL, options: options) - } + let image = try await downloader.image(for: request) // This line guarantees that if you cancel on the main thread, // none of the `onStateChanged` callbacks get called. guard !Task.isCancelled else { return } @@ -63,10 +52,7 @@ final class ImageLoadingController { } } - func setImage( - with media: Media, - size: MediaImageService.ImageSize - ) { + func setImage(with media: Media, size: MediaImageService.ImageSize) { task?.cancel() if let image = service.getCachedThumbnail(for: .init(media), size: size) { diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index 45ab051e78ab..bed820a60d8f 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -22,13 +22,12 @@ struct ImageViewExtensions { } } - func setImage( - with imageURL: URL, - host: MediaHost? = nil, - size: CGSize? = nil, - completion: (@MainActor (Result) -> Void)? = nil - ) { - controller.setImage(with: imageURL, host: host, size: size, completion: completion) + func setImage(with imageURL: URL, host: MediaHost? = nil, size: CGSize? = nil) { + setImage(with: ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size))) + } + + func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { + controller.setImage(with: request, completion: completion) } var controller: ImageLoadingController { diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index f18934f37aac..fdc0ae966234 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -1,5 +1,6 @@ import UIKit import WordPressUI +import WordPressMedia final class LightboxImagePageViewController: UIViewController { private(set) var scrollView = LightboxImageScrollView() @@ -51,7 +52,7 @@ final class LightboxImagePageViewController: UIViewController { case .image(let image): setState(.success(image)) case .asset(let asset): - controller.setImage(with: asset.sourceURL, host: asset.host) + controller.setImage(with: ImageRequest(url: asset.sourceURL, host: asset.host)) case .media(let media): controller.setImage(with: media, size: .original) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 063a836debc5..6c5242a6c3de 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -223,7 +223,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { completionHandler(CGSize(width: 1000, height: 1000 * ReaderPostCell.coverAspectRatio)) } - imageView.setImage(with: imageURL, host: MediaHost(post)) { [weak self] result in + imageView.setImage(with: ImageRequest(url: imageURL, host: MediaHost(post))) { [weak self] result in guard let self else { return } switch result { case .success: diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift index 3853c08f8701..dd314f896c58 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift @@ -57,7 +57,7 @@ class WPRichTextImage: UIControl, WPRichTextMediaAttachment { return } - imageView.setImage(with: contentURL, host: host) { result in + imageView.setImage(with: ImageRequest(url: contentURL, host: host)) { result in switch result { case .success: onSuccess?() case .failure(let error): onError?(error) diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift index 63d1832e235d..f3acd56f02cc 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia class DashboardCustomAnnouncementCell: AnnouncementTableViewCell { @@ -65,7 +66,7 @@ class DashboardCustomAnnouncementCell: AnnouncementTableViewCell { func configure(feature: WordPressKit.Feature) { if let url = URL(string: feature.iconUrl) { - announcementImageView.wp.setImage(with: url) { [weak self] result in + announcementImageView.wp.setImage(with: ImageRequest(url: url)) { [weak self] result in guard let self, case .success(let image) = result else { return } From 023b3cd52382cc5d209dcb6743f30c9d1d2abc7f Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 16:29:54 -0500 Subject: [PATCH 037/101] Add ImageSize --- .../Sources/WordPressMedia/ImageDecoder.swift | 2 +- .../WordPressMedia/ImageDownloader.swift | 10 ++--- .../Sources/WordPressMedia/ImageRequest.swift | 40 +++++++++++++++++-- .../ImageDownloaderTests.swift | 6 +-- .../Utility/Media/AsyncImageView.swift | 2 +- .../Media/UIImageView+ImageDownloader.swift | 2 +- .../BlazeCampaignTableViewCell.swift | 3 +- .../Blaze/Overlay/BlazePostPreviewView.swift | 5 +-- .../Blaze/DashboardBlazeCampaignView.swift | 3 +- .../ExternalMediaPickerCollectionCell.swift | 3 +- .../ExternalMediaPickerViewController.swift | 3 +- .../Views/NoteBlockHeaderTableViewCell.swift | 3 +- .../Post/Views/PostCompactCell.swift | 2 +- .../Reader/Cards/ReaderCrossPostCell.swift | 4 +- .../Reader/Cards/ReaderPostCell.swift | 11 +++-- .../Reader/Views/ReaderAvatarView.swift | 3 +- .../StatsLatestPostSummaryInsightsCell.swift | 3 +- 17 files changed, 69 insertions(+), 36 deletions(-) diff --git a/Modules/Sources/WordPressMedia/ImageDecoder.swift b/Modules/Sources/WordPressMedia/ImageDecoder.swift index 899bf7fead2a..8e7e8c0b44ef 100644 --- a/Modules/Sources/WordPressMedia/ImageDecoder.swift +++ b/Modules/Sources/WordPressMedia/ImageDecoder.swift @@ -70,7 +70,7 @@ private extension Data { } } -private extension CGSize { +extension CGSize { func scaled(by scale: CGFloat) -> CGSize { CGSize(width: width * scale, height: height * scale) } diff --git a/Modules/Sources/WordPressMedia/ImageDownloader.swift b/Modules/Sources/WordPressMedia/ImageDownloader.swift index 384aac235b76..2076816867b6 100644 --- a/Modules/Sources/WordPressMedia/ImageDownloader.swift +++ b/Modules/Sources/WordPressMedia/ImageDownloader.swift @@ -39,7 +39,7 @@ public final class ImageDownloader { return image } let data = try await data(for: request) - let image = try await ImageDecoder.makeImage(from: data, size: options.size) + let image = try await ImageDecoder.makeImage(from: data, size: options.size.map(CGSize.init)) if options.isMemoryCacheEnabled { cache[key] = image } @@ -79,20 +79,20 @@ public final class ImageDownloader { /// /// - note: Use it to retrieve the image synchronously, which is no not possible /// with the async functions. - nonisolated public func cachedImage(for imageURL: URL, size: CGSize? = nil) -> UIImage? { + nonisolated public func cachedImage(for imageURL: URL, size: ImageSize? = nil) -> UIImage? { cache[makeKey(for: imageURL, size: size)] } - nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: CGSize? = nil) { + nonisolated public func setCachedImage(_ image: UIImage?, for imageURL: URL, size: ImageSize? = nil) { cache[makeKey(for: imageURL, size: size)] = image } - private nonisolated func makeKey(for imageURL: URL?, size: CGSize?) -> String { + private nonisolated func makeKey(for imageURL: URL?, size: ImageSize?) -> String { guard let imageURL else { assertionFailure("The request.url was nil") // This should never happen return "" } - return imageURL.absoluteString + (size.map { "?size=\($0)" } ?? "") + return imageURL.absoluteString + (size.map { "?w=\($0.width),h=\($0.height)" } ?? "") } public func clearURLSessionCache() { diff --git a/Modules/Sources/WordPressMedia/ImageRequest.swift b/Modules/Sources/WordPressMedia/ImageRequest.swift index e5c811381183..0c299c489bba 100644 --- a/Modules/Sources/WordPressMedia/ImageRequest.swift +++ b/Modules/Sources/WordPressMedia/ImageRequest.swift @@ -28,8 +28,8 @@ public final class ImageRequest: Sendable { } public struct ImageRequestOptions: Hashable, Sendable { - /// Resize the thumbnail to the given size (in pixels). By default, `nil`. - public var size: CGSize? + /// Resize the thumbnail to the given size. By default, `nil`. + public var size: ImageSize? /// If enabled, uses ``MemoryCache`` for caching decompressed images. public var isMemoryCacheEnabled = true @@ -39,7 +39,7 @@ public struct ImageRequestOptions: Hashable, Sendable { public var isDiskCacheEnabled = true public init( - size: CGSize? = nil, + size: ImageSize? = nil, isMemoryCacheEnabled: Bool = true, isDiskCacheEnabled: Bool = true ) { @@ -48,3 +48,37 @@ public struct ImageRequestOptions: Hashable, Sendable { self.isDiskCacheEnabled = isDiskCacheEnabled } } + +/// Image size in **pixels**. +public struct ImageSize: Hashable, Sendable { + public let width: CGFloat + public let height: CGFloat + + public init(width: CGFloat, height: CGFloat) { + self.width = width + self.height = height + } + + public init(_ size: CGSize) { + self.width = size.width + self.height = size.height + } + + /// Initializes `ImageSize` with the given size scaled for the given view. + @MainActor + public init(scaling size: CGSize, in view: UIView) { + self.init(size.scaled(by: view.traitCollection.displayScale)) + } + + /// Initializes `ImageSize` with the given size scaled for the current trait + /// collection display scale. + public init(scaling size: CGSize) { + self.init(size.scaled(by: UITraitCollection.current.displayScale)) + } +} + +extension CGSize { + init(_ size: ImageSize) { + self.init(width: size.width, height: size.height) + } +} diff --git a/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift b/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift index 00e9f4c33f86..f661075ddfb4 100644 --- a/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift +++ b/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift @@ -27,7 +27,7 @@ import OHHTTPStubsSwift // WHEN let options = ImageRequestOptions( - size: CGSize(width: 256, height: 256), + size: ImageSize(width: 256, height: 256), isMemoryCacheEnabled: false, isDiskCacheEnabled: false ) @@ -46,7 +46,7 @@ import OHHTTPStubsSwift // WHEN let options = ImageRequestOptions( - size: CGSize(width: 256, height: 256), + size: ImageSize(width: 256, height: 256), isMemoryCacheEnabled: false, isDiskCacheEnabled: false ) @@ -72,7 +72,7 @@ import OHHTTPStubsSwift let imageURL = try #require(URL(string: "https://example.files.wordpress.com/2023/09/image.jpg")) try mockResponse(withResource: "test-image", fileExtension: "jpg") - let size = CGSize(width: 256, height: 256) + let size = ImageSize(width: 256, height: 256) let options = ImageRequestOptions( size: size, isMemoryCacheEnabled: true, diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index 16b0b48df0a9..b094ea94f3fc 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -84,7 +84,7 @@ final class AsyncImageView: UIView { func setImage( with imageURL: URL, host: MediaHost? = nil, - size: CGSize? = nil + size: ImageSize? = nil ) { let request = ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size)) controller.setImage(with: request) diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index bed820a60d8f..92d5c0619831 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -22,7 +22,7 @@ struct ImageViewExtensions { } } - func setImage(with imageURL: URL, host: MediaHost? = nil, size: CGSize? = nil) { + func setImage(with imageURL: URL, host: MediaHost? = nil, size: ImageSize? = nil) { setImage(with: ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size))) } diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift index 10d4b6b32a2b..6976b2e9ba62 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift @@ -110,8 +110,7 @@ final class BlazeCampaignTableViewCell: UITableViewCell, Reusable { let host = MediaHost(with: blog, failure: { error in WordPressAppDelegate.crashLogging?.logError(error) }) - let preferredSize = CGSize(width: Metrics.featuredImageSize, height: Metrics.featuredImageSize) - .scaled(by: UITraitCollection.current.displayScale) + let preferredSize = ImageSize(scaling: CGSize(width: Metrics.featuredImageSize, height: Metrics.featuredImageSize), in: self) featuredImageView.setImage(with: imageURL, host: host, size: preferredSize) } diff --git a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift index c84e478be3a7..3f58ab5c5c3d 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift @@ -96,9 +96,8 @@ final class BlazePostPreviewView: UIView { if let url = post.featuredImageURL { featuredImageView.isHidden = false - let preferredSize = CGSize(width: featuredImageView.frame.width, height: featuredImageView.frame.height) - .scaled(by: UITraitCollection.current.displayScale) - featuredImageView.setImage(with: url, host: MediaHost(post), size: preferredSize) + let targetSize = ImageSize(scaling: featuredImageView.frame.size, in: self) + featuredImageView.setImage(with: url, host: MediaHost(post), size: targetSize) } else { featuredImageView.isHidden = true diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift index 39c61d3e232d..0fc0c6d7163a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift @@ -64,8 +64,7 @@ final class DashboardBlazeCampaignView: UIView { let host = MediaHost(with: blog, failure: { error in WordPressAppDelegate.crashLogging?.logError(error) }) - let targetSize = Constants.imageSize - .scaled(by: UITraitCollection.current.displayScale) + let targetSize = ImageSize(scaling: Constants.imageSize, in: self) imageView.setImage(with: imageURL, host: host, size: targetSize) } diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift index 2a50aeb6b2f3..4d80e39286f1 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia final class ExternalMediaPickerCollectionCell: UICollectionViewCell { private let imageView = AsyncImageView() @@ -22,7 +23,7 @@ final class ExternalMediaPickerCollectionCell: UICollectionViewCell { imageView.prepareForReuse() } - func configure(imageURL: URL, size: CGSize) { + func configure(imageURL: URL, size: ImageSize) { imageView.setImage(with: imageURL, size: size) } diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift index 62ad231900e3..729b59ba52ca 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia protocol ExternalMediaPickerViewDelegate: AnyObject { /// If the user cancels the flow, the selection is empty. @@ -235,7 +236,7 @@ final class ExternalMediaPickerViewController: UIViewController, UICollectionVie func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Self.cellReuseID, for: indexPath) as! ExternalMediaPickerCollectionCell let item = dataSource.assets[indexPath.item] - cell.configure(imageURL: item.thumbnailURL, size: flowLayout.itemSize.scaled(by: UIScreen.main.scale)) + cell.configure(imageURL: item.thumbnailURL, size: ImageSize(scaling: flowLayout.itemSize)) return cell } diff --git a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift index acea65a38bf3..0633039f54ba 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift @@ -1,6 +1,7 @@ import Foundation import WordPressShared import WordPressUI +import WordPressMedia import Gravatar // MARK: - NoteBlockHeaderTableViewCell @@ -70,7 +71,7 @@ class NoteBlockHeaderTableViewCell: NoteBlockTableViewCell { if let gravatar = AvatarURL(url: url) { authorAvatarImageView.downloadGravatar(gravatar, placeholder: .gravatarPlaceholderImage, animate: true) } else { - authorAvatarImageView.wp.setImage(with: url, size: SiteIconViewModel.Size.regular.size) + authorAvatarImageView.wp.setImage(with: url, size: ImageSize(scaling: SiteIconViewModel.Size.regular.size)) } } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift index 0dbfe52398c5..cb84ba4496cc 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift @@ -79,7 +79,7 @@ final class PostCompactCell: UITableViewCell, Reusable { featuredImageView.isHidden = false let host = MediaHost(post) - let targetSize = Constants.imageSize.scaled(by: traitCollection.displayScale) + let targetSize = ImageSize(scaling: Constants.imageSize, in: self) featuredImageView.setImage(with: url, host: host, size: targetSize) } else { featuredImageView.isHidden = true diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift index 1f3fa9f6e941..ed69ee523d0c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift @@ -2,6 +2,7 @@ import Foundation import AutomatticTracks import WordPressShared import WordPressUI +import WordPressMedia final class ReaderCrossPostCell: ReaderStreamBaseCell { private let view = ReaderCrossPostView() @@ -132,8 +133,7 @@ private final class ReaderCrossPostView: UIView { avatarView.setPlaceholder(UIImage(named: "post-blavatar-placeholder")) if let avatarURL = post.avatarURLForDisplay() { - let avatarSize = CGSize(width: avatarSize, height: avatarSize) - .scaled(by: UITraitCollection.current.displayScale) + let avatarSize = ImageSize(scaling: CGSize(width: avatarSize, height: avatarSize)) avatarView.setImage(with: avatarURL, size: avatarSize) } } diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 844d35587976..29693a544e8d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -2,6 +2,7 @@ import SwiftUI import UIKit import Combine import WordPressShared +import WordPressMedia final class ReaderPostCell: ReaderStreamBaseCell { private let view = ReaderPostCellView() @@ -51,13 +52,12 @@ final class ReaderPostCell: ReaderStreamBaseCell { super.updateConstraints() } - static func preferredCoverSize(in window: UIWindow, isCompact: Bool) -> CGSize { + static func preferredCoverSize(in window: UIWindow, isCompact: Bool) -> ImageSize { var coverWidth = ReaderPostCell.regularCoverWidth if isCompact { coverWidth = min(window.bounds.width, window.bounds.height) - ReaderStreamBaseCell.insets.left * 2 } - return CGSize(width: coverWidth, height: coverWidth) - .scaled(by: min(2, window.traitCollection.displayScale)) + return ImageSize(scaling: CGSize(width: coverWidth, height: coverWidth), in: window) } } @@ -314,7 +314,7 @@ private final class ReaderPostCellView: UIView { } } - private var preferredCoverSize: CGSize? { + private var preferredCoverSize: ImageSize? { guard let window = window ?? UIApplication.shared.mainWindow else { return nil } return ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) } @@ -345,8 +345,7 @@ private final class ReaderPostCellView: UIView { private func setAvatar(with viewModel: ReaderPostCellViewModel) { avatarView.setPlaceholder(UIImage(named: "post-blavatar-placeholder")) - let avatarSize = CGSize(width: ReaderPostCell.avatarSize, height: ReaderPostCell.avatarSize) - .scaled(by: UITraitCollection.current.displayScale) + let avatarSize = ImageSize(scaling: CGSize(width: ReaderPostCell.avatarSize, height: ReaderPostCell.avatarSize)) if let avatarURL = viewModel.avatarURL { avatarView.setImage(with: avatarURL, size: avatarSize) } else { diff --git a/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift b/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift index 7c76771caa4e..87913fe3306a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia final class ReaderAvatarView: UIView { private let asyncImageView = AsyncImageView() @@ -42,7 +43,7 @@ final class ReaderAvatarView: UIView { asyncImageView.image = image } - func setImage(with imageURL: URL, size: CGSize? = nil) { + func setImage(with imageURL: URL, size: ImageSize? = nil) { asyncImageView.setImage(with: imageURL, size: size) } } diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift index dfff7c03f493..f07e4b7cad3a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift @@ -235,8 +235,7 @@ class StatsLatestPostSummaryInsightsCell: StatsBaseCell, LatestPostSummaryConfig DDLogError("Failed to create media host: \(error.localizedDescription)") }) let targetSize = CGSize(width: Metrics.thumbnailSize, height: Metrics.thumbnailSize) - .scaled(by: traitCollection.displayScale) - postImageView.setImage(with: url, host: host, size: targetSize) + postImageView.setImage(with: url, host: host, size: ImageSize(scaling: targetSize, in: self)) } else { postImageView.isHidden = true } From f3522254651fb163c73caa242d057baef0c0a1c1 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 17:53:21 -0500 Subject: [PATCH 038/101] Fix an issue with blogging reminders flow not being shown after publishing a new post --- RELEASE-NOTES.txt | 1 + .../AztecPostViewController.swift | 2 +- ...gingRemindersFlowIntroViewController.swift | 34 ++++--------------- .../Gutenberg/GutenbergViewController.swift | 2 +- .../NewGutenbergViewController.swift | 4 +-- .../Controllers/EditPageViewController.swift | 2 +- .../Post/EditPostViewController.swift | 21 ++++-------- .../ViewRelated/Post/PostEditor+Publish.swift | 18 +++++----- .../Classes/ViewRelated/Post/PostEditor.swift | 2 +- .../Post/PostListEditorPresenter.swift | 2 +- 10 files changed, 30 insertions(+), 58 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ffda07ddec97..14842c841edb 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,6 +2,7 @@ ----- * [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] * [*] Add prefetching to Reader streams [#23928] +* [*] Fix an issue with blogging reminders prompt not being shown after publishing a new post [#23930] 25.6 ----- diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 08b1d51bb244..8b88ea4f9ef2 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -23,7 +23,7 @@ class AztecPostViewController: UIViewController, PostEditor { /// Closure to be executed when the editor gets closed. /// - var onClose: ((_ changesSaved: Bool) -> ())? + var onClose: (() -> ())? /// Verification Prompt Helper /// diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift index 76430c083a7f..36666edd5eb8 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift @@ -29,7 +29,7 @@ class BloggingRemindersFlowIntroViewController: UIViewController { label.font = WPStyleGuide.serifFontForTextStyle(.title1, fontWeight: .semibold) label.numberOfLines = 2 label.textAlignment = .center - label.text = TextContent.introTitle + label.text = Strings.introTitle return label }() @@ -46,7 +46,7 @@ class BloggingRemindersFlowIntroViewController: UIViewController { private lazy var getStartedButton: UIButton = { let button = FancyButton() button.isPrimary = true - button.setTitle(TextContent.introButtonTitle, for: .normal) + button.setTitle(Strings.introButtonTitle, for: .normal) button.addTarget(self, action: #selector(getStartedTapped), for: .touchUpInside) return button }() @@ -58,18 +58,6 @@ class BloggingRemindersFlowIntroViewController: UIViewController { private let source: BloggingRemindersTracker.FlowStartSource private weak var delegate: BloggingRemindersFlowDelegate? - private var introDescription: String { - switch source { - case .publishFlow: - return TextContent.postPublishingintroDescription - case .blogSettings, - .notificationSettings, - .statsInsights, - .bloggingPromptsFeatureIntroduction: - return TextContent.siteSettingsIntroDescription - } - } - init(for blog: Blog, tracker: BloggingRemindersTracker, source: BloggingRemindersTracker.FlowStartSource, @@ -98,7 +86,7 @@ class BloggingRemindersFlowIntroViewController: UIViewController { configureStackView() configureConstraints() - promptLabel.text = introDescription + promptLabel.text = Strings.introDescription } override func viewDidAppear(_ animated: Bool) { @@ -197,18 +185,10 @@ extension BloggingRemindersFlowIntroViewController: ChildDrawerPositionable { // MARK: - Constants -private enum TextContent { - static let introTitle = NSLocalizedString("Set your blogging reminders", - comment: "Title of the Blogging Reminders Settings screen.") - - static let postPublishingintroDescription = NSLocalizedString("Your post is publishing... in the meantime, set up your blogging reminders on days you want to post.", - comment: "Description on the first screen of the Blogging Reminders Settings flow called aftet post publishing.") - - static let siteSettingsIntroDescription = NSLocalizedString("Set up your blogging reminders on days you want to post.", - comment: "Description on the first screen of the Blogging Reminders Settings flow called from site settings.") - - static let introButtonTitle = NSLocalizedString("Set reminders", - comment: "Title of the set goals button in the Blogging Reminders Settings flow.") +private enum Strings { + static let introTitle = NSLocalizedString("bloggingRemindersPrompt.intro.title", value: "Blogging Reminders", comment: "Title of the Blogging Reminders Settings screen.") + static let introDescription = NSLocalizedString("bloggingRemindersPrompt.intro.details", value: "Set up your blogging reminders on days you want to post.", comment: "Description on the first screen of the Blogging Reminders Settings flow called aftet post publishing.") + static let introButtonTitle = NSLocalizedString("bloggingRemindersPrompt.intro.continueButton", value: "Set reminders", comment: "Title of the set goals button in the Blogging Reminders Settings flow.") } private enum Images { diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 758bf6f4abff..1589fc3e3ade 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -91,7 +91,7 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega var editorSession: PostEditorAnalyticsSession - var onClose: ((Bool) -> Void)? + var onClose: (() -> Void)? var postIsReblogged: Bool = false diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 8b8516e62d7c..f3fd5637ea5a 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -34,7 +34,7 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor var analyticsEditorSource: String { Analytics.editorSource } var editorSession: PostEditorAnalyticsSession - var onClose: ((Bool) -> Void)? + var onClose: (() -> Void)? // MARK: - Set content @@ -321,7 +321,7 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { - onClose?(false) + onClose?() } func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { diff --git a/WordPress/Classes/ViewRelated/Pages/Controllers/EditPageViewController.swift b/WordPress/Classes/ViewRelated/Pages/Controllers/EditPageViewController.swift index ee4b37ab70da..e74acbe58500 100644 --- a/WordPress/Classes/ViewRelated/Pages/Controllers/EditPageViewController.swift +++ b/WordPress/Classes/ViewRelated/Pages/Controllers/EditPageViewController.swift @@ -67,7 +67,7 @@ class EditPageViewController: UIViewController { private func show(_ editor: EditorViewController) { editor.entryPoint = entryPoint - editor.onClose = { [weak self] _ in + editor.onClose = { [weak self] in // Dismiss navigation controller self?.dismiss(animated: true) { // Dismiss self diff --git a/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift b/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift index 1998e29e5702..524fda7ec2de 100644 --- a/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/EditPostViewController.swift @@ -22,7 +22,7 @@ class EditPostViewController: UIViewController { fileprivate var editingExistingPost = false fileprivate let blog: Blog - @objc var onClose: ((_ changesSaved: Bool) -> ())? + @objc var onClose: (() -> ())? @objc var afterDismiss: (() -> Void)? override var modalPresentationStyle: UIModalPresentationStyle { @@ -126,19 +126,12 @@ class EditPostViewController: UIViewController { } private func showEditor(_ editor: EditorViewController) { - editor.onClose = { [weak self, weak editor] changesSaved in - guard let strongSelf = self else { + editor.onClose = { [weak self, weak editor] in + guard let self else { editor?.dismiss(animated: true) {} return } - - // NOTE: - // We need to grab the latest Post Reference, since it may have changed (ie. revision / user picked a - // new blog). - if changesSaved { - strongSelf.post = editor?.post as? Post - } - strongSelf.closeEditor(changesSaved) + self.closeEditor() } let navController = AztecNavigationController(rootViewController: editor) @@ -166,8 +159,8 @@ class EditPostViewController: UIViewController { } } - @objc func closeEditor(_ changesSaved: Bool = true, from presentingViewController: UIViewController? = nil) { - onClose?(changesSaved) + @objc func closeEditor(from presentingViewController: UIViewController? = nil) { + onClose?() dismiss(animated: true) { self.closeEditor(animated: false) } @@ -182,7 +175,7 @@ class EditPostViewController: UIViewController { return } self.afterDismiss?() - guard let post = self.post, + guard let post = self.post?.original(), post.isPublished(), !self.editingExistingPost, let controller = presentingController else { diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift index 9f6d7777905d..410d3df38e61 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift @@ -26,7 +26,7 @@ protocol PublishingEditor where Self: UIViewController { var alertBarButtonItem: UIBarButtonItem? { get } /// Closure to be executed when the editor gets closed. - var onClose: ((_ changesSaved: Bool) -> Void)? { get set } + var onClose: (() -> Void)? { get set } /// Return the current html in the editor func getHTML() -> String @@ -204,19 +204,18 @@ extension PublishingEditor { } func discardUnsavedChangesAndUpdateGUI() { - let postDeleted = discardChanges() - dismissOrPopView(didSave: !postDeleted) + discardChanges() + dismissOrPopView() } - @discardableResult - func discardChanges() -> Bool { + func discardChanges() { guard post.status != .trash else { - return true // No revision is created for trashed posts + return // No revision is created for trashed posts } guard let context = post.managedObjectContext else { wpAssertionFailure("Missing managedObjectContext") - return true + return } WPAppAnalytics.track(.editorDiscardedChanges, withProperties: [WPAppAnalyticsKeyEditorSource: analyticsEditorSource], with: post) @@ -233,7 +232,6 @@ extension PublishingEditor { AbstractPost.deleteLatestRevision(post, in: context) ContextManager.shared.saveContextAndWait(context) - return true } private func showCloseDraftConfirmationAlert() { @@ -276,7 +274,7 @@ extension PublishingEditor { // MARK: - Publishing extension PublishingEditor { - func dismissOrPopView(didSave: Bool = true, presentBloggingReminders: Bool = false) { + func dismissOrPopView(presentBloggingReminders: Bool = false) { stopEditing() WPAppAnalytics.track(.editorClosed, withProperties: [WPAppAnalyticsKeyEditorSource: analyticsEditorSource], with: post) @@ -284,7 +282,7 @@ extension PublishingEditor { if let onClose { // if this closure exists, the presentation of the Blogging Reminders flow (if needed) // needs to happen in the closure. - onClose(didSave) + onClose() } else if isModal(), let controller = presentingViewController { controller.dismiss(animated: true) { if presentBloggingReminders { diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor.swift b/WordPress/Classes/ViewRelated/Post/PostEditor.swift index bbe24267d949..a9bc7231d967 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor.swift @@ -267,7 +267,7 @@ extension PostEditor where Self: UIViewController { let deletedObjects = ((userInfo[NSDeletedObjectsKey] as? Set) ?? []) if deletedObjects.contains(where: { $0.objectID == originalPostID }) { - onClose?(false) + onClose?() } } } diff --git a/WordPress/Classes/ViewRelated/Post/PostListEditorPresenter.swift b/WordPress/Classes/ViewRelated/Post/PostListEditorPresenter.swift index f66d2aa4fccd..d2736f1c8f0d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListEditorPresenter.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListEditorPresenter.swift @@ -53,7 +53,7 @@ struct PostListEditorPresenter { let editor = EditPostViewController(post: post) editor.modalPresentationStyle = .fullScreen editor.entryPoint = entryPoint - editor.onClose = { _ in + editor.onClose = { NotificationCenter.default.post(name: .postListEditorPresenterDidHideEditor, object: nil) } postListViewController.present(editor, animated: false) From afdfd054658da1e82fa13780d440bf3403f66443 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 18:13:24 -0500 Subject: [PATCH 039/101] Remove unused LightNavigationController --- WordPress/Classes/Utility/Spotlight/SearchManager.swift | 2 +- .../Blog/Blog Details/BlogDetailsViewController.m | 2 +- .../BloggingRemindersNavigationController.swift | 2 +- .../Blog/My Site/Header/HomeSiteHeaderViewController.swift | 2 +- .../ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m | 2 +- .../JetpackRestoreCompleteViewController.swift | 2 +- .../Notifications/ReplyTextView/ReplyTextView.swift | 2 +- .../People/Controllers/PeopleViewController.swift | 2 +- .../Post/Controllers/AbstractPostListViewController.swift | 2 +- .../Classes/ViewRelated/Post/PostEditor+MoreOptions.swift | 2 +- .../Post/Scheduling/LightNavigationController.swift | 7 ------- .../Post/Utils/PostNoticeNavigationCoordinator.swift | 2 +- .../Post/Utils/PostNoticePublishSuccessView.swift | 2 +- .../ReaderPostActions/ReaderVisitSiteAction.swift | 2 +- .../Reader/Detail/ReaderDetailCoordinator.swift | 2 +- WordPress/WordPress.xcodeproj/project.pbxproj | 4 ---- .../FullScreenCommentReplyViewControllerTests.swift | 2 +- 17 files changed, 15 insertions(+), 26 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Post/Scheduling/LightNavigationController.swift diff --git a/WordPress/Classes/Utility/Spotlight/SearchManager.swift b/WordPress/Classes/Utility/Spotlight/SearchManager.swift index eeb41704e412..52f20c19f3be 100644 --- a/WordPress/Classes/Utility/Spotlight/SearchManager.swift +++ b/WordPress/Classes/Utility/Spotlight/SearchManager.swift @@ -458,7 +458,7 @@ fileprivate extension SearchManager { let controller = PreviewWebKitViewController(post: apost, source: "spotlight_preview_post") controller.trackOpenEvent() - let navWrapper = LightNavigationController(rootViewController: controller) + let navWrapper = UINavigationController(rootViewController: controller) let rootViewController = RootViewCoordinator.sharedPresenter.rootViewController if rootViewController.traitCollection.userInterfaceIdiom == .pad { navWrapper.modalPresentationStyle = .fullScreen diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index ab41fbca01b8..33a2f1bca520 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -1939,7 +1939,7 @@ - (void)showViewSiteFromSource:(BlogDetailsNavigationSource)source source:@"my_site_view_site" withDeviceModes:true onClose:nil]; - LightNavigationController *navController = [[LightNavigationController alloc] initWithRootViewController:webViewController]; + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; if (self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad) { navController.modalPresentationStyle = UIModalPresentationFullScreen; } diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift index df093f371e63..33d9562ac586 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift @@ -5,7 +5,7 @@ protocol ChildDrawerPositionable { var preferredDrawerPosition: DrawerPosition { get } } -class BloggingRemindersNavigationController: LightNavigationController { +class BloggingRemindersNavigationController: UINavigationController { typealias DismissClosure = () -> Void diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController.swift index 2d9150326fbb..ff200d4d21a0 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController.swift @@ -255,7 +255,7 @@ extension HomeSiteHeaderViewController { onClose: nil ) - let navigationController = LightNavigationController(rootViewController: webViewController) + let navigationController = UINavigationController(rootViewController: webViewController) if traitCollection.userInterfaceIdiom == .pad { navigationController.modalPresentationStyle = .fullScreen diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m index 30436b269ca2..0b64b9e8aef6 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m @@ -91,7 +91,7 @@ - (void)authorizeWithConnectionURL:(NSURL *)connectionURL { SharingAuthorizationWebViewController *webViewController = [[SharingAuthorizationWebViewController alloc] initWith:self.publicizeService url:connectionURL for:self.blog delegate:self]; - self.navController = [[LightNavigationController alloc] initWithRootViewController:webViewController]; + self.navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; self.navController.modalPresentationStyle = UIModalPresentationFormSheet; [self.viewController presentViewController:self.navController animated:YES completion:nil]; } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/JetpackRestoreCompleteViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/JetpackRestoreCompleteViewController.swift index afb44f0f80a1..7075dbfcd052 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/JetpackRestoreCompleteViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/JetpackRestoreCompleteViewController.swift @@ -54,7 +54,7 @@ class JetpackRestoreCompleteViewController: BaseRestoreCompleteViewController { } let webVC = WebViewControllerFactory.controller(url: homeURL, source: "jetpack_restore_complete") - let navigationVC = LightNavigationController(rootViewController: webVC) + let navigationVC = UINavigationController(rootViewController: webVC) self.present(navigationVC, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift index aada63071618..003e65a7ff59 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift @@ -216,7 +216,7 @@ import Gridicons self.resignFirstResponder() - let navController = LightNavigationController(rootViewController: editViewController) + let navController = UINavigationController(rootViewController: editViewController) rootViewController.present(navController, animated: true) } diff --git a/WordPress/Classes/ViewRelated/People/Controllers/PeopleViewController.swift b/WordPress/Classes/ViewRelated/People/Controllers/PeopleViewController.swift index f3d6ccca3ab7..a2f689990ebb 100644 --- a/WordPress/Classes/ViewRelated/People/Controllers/PeopleViewController.swift +++ b/WordPress/Classes/ViewRelated/People/Controllers/PeopleViewController.swift @@ -188,7 +188,7 @@ class PeopleViewController: UITableViewController { self?.refreshPeople() } let viewController = WebKitViewController(configuration: configuration) - let navWrapper = LightNavigationController(rootViewController: viewController) + let navWrapper = UINavigationController(rootViewController: viewController) navigationController?.present(navWrapper, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift index 020e21f3b4e3..8fa79f8f1282 100644 --- a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift @@ -664,7 +664,7 @@ class AbstractPostListViewController: UIViewController, // NOTE: We'll set the title to match the title of the View action button. // If the button title changes we should also update the title here. controller.navigationItem.title = NSLocalizedString("View", comment: "Verb. The screen title shown when viewing a post inside the app.") - let navWrapper = LightNavigationController(rootViewController: controller) + let navWrapper = UINavigationController(rootViewController: controller) if navigationController?.traitCollection.userInterfaceIdiom == .pad { navWrapper.modalPresentationStyle = .fullScreen } diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift index 76000b4302af..9bae8164b833 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift @@ -87,7 +87,7 @@ extension PostEditor { previewController = PreviewWebKitViewController(post: self.post, source: "edit_post_more_preview") } previewController.trackOpenEvent() - let navWrapper = LightNavigationController(rootViewController: previewController) + let navWrapper = UINavigationController(rootViewController: previewController) if self.navigationController?.traitCollection.userInterfaceIdiom == .pad { navWrapper.modalPresentationStyle = .fullScreen } diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/LightNavigationController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/LightNavigationController.swift deleted file mode 100644 index 6a8937bf1c91..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/LightNavigationController.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -// TODO: remove - -// A Navigation Controller with a light navigation bar style -class LightNavigationController: UINavigationController { -} diff --git a/WordPress/Classes/ViewRelated/Post/Utils/PostNoticeNavigationCoordinator.swift b/WordPress/Classes/ViewRelated/Post/Utils/PostNoticeNavigationCoordinator.swift index ad536f6b5194..8168ed23bf60 100644 --- a/WordPress/Classes/ViewRelated/Post/Utils/PostNoticeNavigationCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Post/Utils/PostNoticeNavigationCoordinator.swift @@ -28,7 +28,7 @@ class PostNoticeNavigationCoordinator { controller.trackOpenEvent() controller.navigationItem.title = NSLocalizedString("View", comment: "Verb. The screen title shown when viewing a post inside the app.") - let navigationController = LightNavigationController(rootViewController: controller) + let navigationController = UINavigationController(rootViewController: controller) if presenter.traitCollection.userInterfaceIdiom == .pad { navigationController.modalPresentationStyle = .fullScreen } diff --git a/WordPress/Classes/ViewRelated/Post/Utils/PostNoticePublishSuccessView.swift b/WordPress/Classes/ViewRelated/Post/Utils/PostNoticePublishSuccessView.swift index 6b7e22e75f8f..0b3c259ed034 100644 --- a/WordPress/Classes/ViewRelated/Post/Utils/PostNoticePublishSuccessView.swift +++ b/WordPress/Classes/ViewRelated/Post/Utils/PostNoticePublishSuccessView.swift @@ -117,7 +117,7 @@ struct PostNoticePublishSuccessView: View { WPAnalytics.track(.postEpilogueView) let controller = PreviewWebKitViewController(post: post, source: "edit_post_preview") controller.trackOpenEvent() - let navWrapper = LightNavigationController(rootViewController: controller) + let navWrapper = UINavigationController(rootViewController: controller) presenter.present(navWrapper, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderVisitSiteAction.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderVisitSiteAction.swift index 0e2cb91c8c29..ddad644dfeb6 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderVisitSiteAction.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderVisitSiteAction.swift @@ -14,7 +14,7 @@ final class ReaderVisitSiteAction { configuration.authenticate(account: account) } let controller = WebViewControllerFactory.controller(configuration: configuration, source: "reader_visit_site") - let navController = LightNavigationController(rootViewController: controller) + let navController = UINavigationController(rootViewController: controller) origin.present(navController, animated: true) WPAnalytics.trackReader(.readerArticleVisited) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index c037dfef9f3a..607ff17c6c8a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -573,7 +573,7 @@ class ReaderDetailCoordinator { configuration.authenticateWithDefaultAccount() configuration.addsWPComReferrer = true let controller = WebViewControllerFactory.controller(configuration: configuration, source: "reader_detail") - let navController = LightNavigationController(rootViewController: controller) + let navController = UINavigationController(rootViewController: controller) viewController?.present(navController, animated: true) } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 2d1bfab4901d..927371beb713 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3414,7 +3414,6 @@ "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", ViewRelated/Aztec/Processors/ImgUploadProcessor.swift, ViewRelated/Cells/WPReusableTableViewCells.swift, - ViewRelated/Post/Scheduling/LightNavigationController.swift, ViewRelated/Tools/TableViewKeyboardObserver.swift, ViewRelated/Views/NoResults.storyboard, ViewRelated/Views/NoResultsViewController.swift, @@ -3454,7 +3453,6 @@ "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", ViewRelated/Aztec/Processors/ImgUploadProcessor.swift, ViewRelated/Cells/WPReusableTableViewCells.swift, - ViewRelated/Post/Scheduling/LightNavigationController.swift, ViewRelated/Tools/TableViewKeyboardObserver.swift, ViewRelated/Views/NoResults.storyboard, ViewRelated/Views/NoResultsViewController.swift, @@ -3585,7 +3583,6 @@ "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", ViewRelated/Aztec/Processors/ImgUploadProcessor.swift, ViewRelated/Cells/WPReusableTableViewCells.swift, - ViewRelated/Post/Scheduling/LightNavigationController.swift, ViewRelated/Tools/TableViewKeyboardObserver.swift, ViewRelated/Views/NoResults.storyboard, ViewRelated/Views/NoResultsViewController.swift, @@ -3623,7 +3620,6 @@ "ViewRelated/Aztec/Extensions/TextList+WordPress.swift", ViewRelated/Aztec/Processors/ImgUploadProcessor.swift, ViewRelated/Cells/WPReusableTableViewCells.swift, - ViewRelated/Post/Scheduling/LightNavigationController.swift, ViewRelated/Tools/TableViewKeyboardObserver.swift, ViewRelated/Views/NoResults.storyboard, ViewRelated/Views/NoResultsViewController.swift, diff --git a/WordPress/WordPressTest/Comments/Controllers/FullScreenCommentReplyViewControllerTests.swift b/WordPress/WordPressTest/Comments/Controllers/FullScreenCommentReplyViewControllerTests.swift index 1e839c8ef615..7f04a1ecec65 100644 --- a/WordPress/WordPressTest/Comments/Controllers/FullScreenCommentReplyViewControllerTests.swift +++ b/WordPress/WordPressTest/Comments/Controllers/FullScreenCommentReplyViewControllerTests.swift @@ -136,7 +136,7 @@ class FullScreenCommentReplyViewControllerTests: CoreDataTestCase { /// - inWindow: The window instance you want to load it in private func load(_ controller: UIViewController, inWindow: UIWindow) { inWindow.addSubview(controller.view) - inWindow.rootViewController = LightNavigationController(rootViewController: controller) + inWindow.rootViewController = UINavigationController(rootViewController: controller) inWindow.makeKeyAndVisible() controller.beginAppearanceTransition(true, animated: false) RunLoop.current.run(until: Date()) From af4ba9c7fc1ab80d6ff147277915f9e52be99078 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 18:37:11 -0500 Subject: [PATCH 040/101] Remove BottomSheetViewController usage from BloggingReminders flow --- .../Extensions/UIButton+Extensions.swift | 13 ++ .../BloggingRemindersScheduler.swift | 2 +- .../BloggingRemindersAnimator.swift | 67 ---------- .../BloggingRemindersFlow.swift | 87 ------------- ...loggingRemindersNavigationController.swift | 123 ------------------ .../BloggingRemindersActions.swift | 2 + .../BloggingRemindersFlow.swift | 78 +++++++++++ ...emindersFlowCompletionViewController.swift | 24 ---- ...gingRemindersFlowIntroViewController.swift | 77 +++-------- ...gRemindersFlowSettingsViewController.swift | 21 +-- ...loggingRemindersNavigationController.swift | 32 +++++ ...ingRemindersPushPromptViewController.swift | 14 -- .../BloggingRemindersTracker.swift | 0 .../CalendarDayToggleButton.swift | 0 ...loggingRemindersTimeSelectionButton.swift} | 2 +- .../BloggingRemindersTimeSelectionView.swift} | 6 +- ...emindersTimeSelectionViewController.swift} | 32 +---- .../JetpackOverlayViewController.swift | 6 - 18 files changed, 154 insertions(+), 432 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersAnimator.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersActions.swift (97%) create mode 100644 WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersFlowCompletionViewController.swift (92%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersFlowIntroViewController.swift (67%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersFlowSettingsViewController.swift (97%) create mode 100644 WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersPushPromptViewController.swift (96%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/BloggingRemindersTracker.swift (100%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders => BloggingReminders}/CalendarDayToggleButton.swift (100%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders/Time Selector/TimeSelectionButton.swift => BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionButton.swift} (97%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders/Time Selector/TimeSelectionView.swift => BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift} (91%) rename WordPress/Classes/ViewRelated/Blog/{Blogging Reminders/Time Selector/TimeSelectionViewController.swift => BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift} (65%) diff --git a/WordPress/Classes/Extensions/UIButton+Extensions.swift b/WordPress/Classes/Extensions/UIButton+Extensions.swift index 68b46e1ee49e..2194627639e6 100644 --- a/WordPress/Classes/Extensions/UIButton+Extensions.swift +++ b/WordPress/Classes/Extensions/UIButton+Extensions.swift @@ -31,3 +31,16 @@ extension UIButton { }()) } } + +extension UIButton.Configuration { + static func primary() -> UIButton.Configuration { + var configuration = UIButton.Configuration.borderedProminent() + configuration.titleTextAttributesTransformer = .init { attributes in + var attributes = attributes + attributes.font = UIFont.preferredFont(forTextStyle: .headline) + return attributes + } + configuration.buttonSize = .large + return configuration + } +} diff --git a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift index d083a0e054e6..63c31026a227 100644 --- a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift +++ b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift @@ -17,7 +17,7 @@ extension InteractiveNotificationsManager: PushNotificationAuthorizer { /// Main interface for scheduling blogging reminders /// -class BloggingRemindersScheduler { +final class BloggingRemindersScheduler { // MARK: - Convenience Typealiases diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersAnimator.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersAnimator.swift deleted file mode 100644 index 978538eef3dc..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersAnimator.swift +++ /dev/null @@ -1,67 +0,0 @@ -/// A transition animator that moves in the pushed view controller horizontally. -/// Does not handle the pop animation since the BloggingReminders setup flow does not allow to navigate back. -class BloggingRemindersAnimator: NSObject, UIViewControllerAnimatedTransitioning { - - var popStyle = false - - private static let animationDuration: TimeInterval = 0.2 - private static let sourceEndFrameOffset: CGFloat = -60.0 - - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { - return Self.animationDuration - } - - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - - guard !popStyle else { - animatePop(using: transitionContext) - return - } - - guard let sourceViewController = - transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), - let destinationViewController = - transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { - return - } - // final position of the destination view - let destinationEndFrame = transitionContext.finalFrame(for: destinationViewController) - // final position of the source view - let sourceEndFrame = transitionContext.initialFrame(for: sourceViewController).offsetBy(dx: Self.sourceEndFrameOffset, dy: .zero) - - // initial position of the destination view - let destinationStartFrame = destinationEndFrame.offsetBy(dx: destinationEndFrame.width, dy: .zero) - destinationViewController.view.frame = destinationStartFrame - - transitionContext.containerView.insertSubview(destinationViewController.view, aboveSubview: sourceViewController.view) - - UIView.animate(withDuration: transitionDuration(using: transitionContext), - animations: { - destinationViewController.view.frame = destinationEndFrame - sourceViewController.view.frame = sourceEndFrame - }, completion: {_ in - transitionContext.completeTransition(true) - }) - } - - func animatePop(using transitionContext: UIViewControllerContextTransitioning) { - guard let sourceViewController = - transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), - let destinationViewController = - transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { - return - } - let destinationEndFrame = transitionContext.finalFrame(for: destinationViewController) - let destinationStartFrame = destinationEndFrame.offsetBy(dx: Self.sourceEndFrameOffset, dy: .zero) - destinationViewController.view.frame = destinationStartFrame - transitionContext.containerView.insertSubview(destinationViewController.view, belowSubview: sourceViewController.view) - - UIView.animate(withDuration: transitionDuration(using: transitionContext), - animations: { - destinationViewController.view.frame = destinationEndFrame - sourceViewController.view.transform = sourceViewController.view.transform.translatedBy(x: sourceViewController.view.frame.width, y: 0) - }, completion: {_ in - transitionContext.completeTransition(true) - }) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift deleted file mode 100644 index 582380d5db83..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlow.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import WordPressUI - -class BloggingRemindersFlow { - - typealias DismissClosure = () -> Void - - static func present(from viewController: UIViewController, - for blog: Blog, - source: BloggingRemindersTracker.FlowStartSource, - alwaysShow: Bool = true, - delegate: BloggingRemindersFlowDelegate? = nil, - onDismiss: DismissClosure? = nil) { - - guard blog.areBloggingRemindersAllowed() else { - return - } - - guard alwaysShow || !hasShownWeeklyRemindersFlow(for: blog) else { - return - } - - let blogType: BloggingRemindersTracker.BlogType = blog.isHostedAtWPcom ? .wpcom : .selfHosted - - let tracker = BloggingRemindersTracker(blogType: blogType) - tracker.flowStarted(source: source) - - let flowStartViewController = makeStartViewController(for: blog, - tracker: tracker, - source: source, - delegate: delegate) - let navigationController = BloggingRemindersNavigationController( - rootViewController: flowStartViewController, - onDismiss: { - NoticesDispatch.unlock() - onDismiss?() - }) - - let bottomSheet = BottomSheetViewController(childViewController: navigationController, - customHeaderSpacing: 0) - - NoticesDispatch.lock() - bottomSheet.show(from: viewController) - setHasShownWeeklyRemindersFlow(for: blog) - } - - /// if the flow has never been seen, it starts with the intro. Otherwise it starts with the calendar settings - private static func makeStartViewController(for blog: Blog, - tracker: BloggingRemindersTracker, - source: BloggingRemindersTracker.FlowStartSource, - delegate: BloggingRemindersFlowDelegate? = nil) -> UIViewController { - - guard hasShownWeeklyRemindersFlow(for: blog) else { - return BloggingRemindersFlowIntroViewController(for: blog, - tracker: tracker, - source: source, - delegate: delegate) - } - - return (try? BloggingRemindersFlowSettingsViewController(for: blog, tracker: tracker, delegate: delegate)) ?? - BloggingRemindersFlowIntroViewController(for: blog, tracker: tracker, source: source, delegate: delegate) - } - - // MARK: - Weekly reminders flow presentation status - // - // stores a key for each blog in UserDefaults to determine if - // the flow was presented for the given blog. - private static func hasShownWeeklyRemindersFlow(for blog: Blog) -> Bool { - UserPersistentStoreFactory.instance().bool(forKey: weeklyRemindersKey(for: blog)) - } - - static func setHasShownWeeklyRemindersFlow(for blog: Blog) { - UserPersistentStoreFactory.instance().set(true, forKey: weeklyRemindersKey(for: blog)) - } - - private static func weeklyRemindersKey(for blog: Blog) -> String { - // weekly reminders key prefix - let prefix = "blogging-reminder-weekly-" - return prefix + blog.objectID.uriRepresentation().absoluteString - } - - /// By making this private we ensure this can't be instantiated. - /// - private init() { - assertionFailure() - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift deleted file mode 100644 index 33d9562ac586..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersNavigationController.swift +++ /dev/null @@ -1,123 +0,0 @@ -import UIKit -import WordPressUI - -protocol ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { get } -} - -class BloggingRemindersNavigationController: UINavigationController { - - typealias DismissClosure = () -> Void - - private let onDismiss: DismissClosure? - - required init(rootViewController: UIViewController, onDismiss: DismissClosure? = nil) { - self.onDismiss = onDismiss - - super.init(rootViewController: rootViewController) - - delegate = self - setNavigationBarHidden(true, animated: false) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - if isBeingDismissedDirectlyOrByAncestor() { - onDismiss?() - } - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait - } - - override public var preferredContentSize: CGSize { - set { - viewControllers.last?.preferredContentSize = newValue - super.preferredContentSize = newValue - } - get { - guard let visibleViewController = viewControllers.last else { - return .zero - } - - return visibleViewController.preferredContentSize - } - } - - override func pushViewController(_ viewController: UIViewController, animated: Bool) { - super.pushViewController(viewController, animated: animated) - - updateDrawerPosition() - } - - override func popViewController(animated: Bool) -> UIViewController? { - let viewController = super.popViewController(animated: animated) - - updateDrawerPosition() - - return viewController - } - - private func updateDrawerPosition() { - if let bottomSheet = self.parent as? BottomSheetViewController, - let presentedVC = bottomSheet.presentedVC, - let currentVC = topViewController as? ChildDrawerPositionable { - presentedVC.transition(to: currentVC.preferredDrawerPosition) - } - } -} - -// MARK: - DrawerPresentable - -extension BloggingRemindersNavigationController: DrawerPresentable { - var allowsUserTransition: Bool { - return false - } - - var allowsDragToDismiss: Bool { - return true - } - - var allowsTapToDismiss: Bool { - return true - } - - var expandedHeight: DrawerHeight { - return .maxHeight - } - - var collapsedHeight: DrawerHeight { - if let viewController = viewControllers.last as? DrawerPresentable { - return viewController.collapsedHeight - } - - return .intrinsicHeight - } - - func handleDismiss() { - (children.last as? DrawerPresentable)?.handleDismiss() - } -} - -// MARK: - NavigationControllerDelegate - -extension BloggingRemindersNavigationController: UINavigationControllerDelegate { - - /// This implementation uses the custom `BloggingRemindersAnimator` to improve screen transitions - /// in the blogging reminders setup flow. - func navigationController(_ navigationController: UINavigationController, - animationControllerFor operation: UINavigationController.Operation, - from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { - - let animator = BloggingRemindersAnimator() - animator.popStyle = (operation == .pop) - - return animator - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersActions.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersActions.swift similarity index 97% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersActions.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersActions.swift index b1105e99422a..22b0ac2d858b 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersActions.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersActions.swift @@ -1,3 +1,5 @@ +import UIKit + /// Conform to this protocol to implement common actions for the blogging reminders flow protocol BloggingRemindersActions: UIViewController { func dismiss(from button: BloggingRemindersTracker.Button, diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift new file mode 100644 index 000000000000..622047076dc7 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift @@ -0,0 +1,78 @@ +import UIKit +import WordPressUI + +final class BloggingRemindersFlow { + static func present( + from presentingViewController: UIViewController, + for blog: Blog, + source: BloggingRemindersTracker.FlowStartSource, + alwaysShow: Bool = true, + delegate: BloggingRemindersFlowDelegate? = nil, + onDismiss: (() -> Void)? = nil + ) { + guard blog.areBloggingRemindersAllowed() else { + return + } + + guard alwaysShow || !hasShownWeeklyRemindersFlow(for: blog) else { + return + } + + let blogType: BloggingRemindersTracker.BlogType = blog.isHostedAtWPcom ? .wpcom : .selfHosted + + let tracker = BloggingRemindersTracker(blogType: blogType) + tracker.flowStarted(source: source) + + let showSettings = { [weak presentingViewController] in + do { + let settingsVC = try BloggingRemindersFlowSettingsViewController(for: blog, tracker: tracker, delegate: delegate) + let navigationController = BloggingRemindersNavigationController(rootViewController: settingsVC, onDismiss: { + NoticesDispatch.unlock() + onDismiss?() + }) + NoticesDispatch.lock() + presentingViewController?.present(navigationController, animated: true) + } catch { + wpAssertionFailure("Could not instantiate the blogging reminders settings VC", userInfo: ["error": "\(error)"]) + } + } + + if hasShownWeeklyRemindersFlow(for: blog) { + showSettings() + } else { + let introVC = BloggingRemindersFlowIntroViewController(for: blog, tracker: tracker, source: source) { [weak presentingViewController] in + presentingViewController?.dismiss(animated: true) { + showSettings() + } + } + introVC.sheetPresentationController?.detents = [.medium()] + presentingViewController.present(introVC, animated: true) + } + + setHasShownWeeklyRemindersFlow(for: blog) + } + + // MARK: - Weekly reminders flow presentation status + // + // stores a key for each blog in UserDefaults to determine if + // the flow was presented for the given blog. + private static func hasShownWeeklyRemindersFlow(for blog: Blog) -> Bool { + UserPersistentStoreFactory.instance().bool(forKey: weeklyRemindersKey(for: blog)) + } + + static func setHasShownWeeklyRemindersFlow(for blog: Blog) { + UserPersistentStoreFactory.instance().set(true, forKey: weeklyRemindersKey(for: blog)) + } + + private static func weeklyRemindersKey(for blog: Blog) -> String { + // weekly reminders key prefix + let prefix = "blogging-reminder-weekly-" + return prefix + blog.objectID.uriRepresentation().absoluteString + } + + /// By making this private we ensure this can't be instantiated. + /// + private init() { + assertionFailure() + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowCompletionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift similarity index 92% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowCompletionViewController.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift index 63a439c3f8ab..c71158b053ff 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowCompletionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift @@ -121,22 +121,12 @@ class BloggingRemindersFlowCompletionViewController: UIViewController { } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - calculatePreferredContentSize() - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) hintLabel.isHidden = traitCollection.preferredContentSizeCategory.isAccessibilityCategory } - func calculatePreferredContentSize() { - let size = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) - preferredContentSize = view.systemLayoutSizeFitting(size) - } - // MARK: - View Configuration private func configureStackView() { @@ -223,20 +213,6 @@ extension BloggingRemindersFlowCompletionViewController: BloggingRemindersAction } } -// MARK: - DrawerPresentable - -extension BloggingRemindersFlowCompletionViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .intrinsicHeight - } -} - -extension BloggingRemindersFlowCompletionViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - return .collapsed - } -} - // MARK: - Constants private enum TextContent { diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift similarity index 67% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift index 36666edd5eb8..31447713322e 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowIntroViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift @@ -1,7 +1,7 @@ import UIKit import WordPressUI -class BloggingRemindersFlowIntroViewController: UIViewController { +final class BloggingRemindersFlowIntroViewController: UIViewController { // MARK: - Subviews @@ -43,37 +43,33 @@ class BloggingRemindersFlowIntroViewController: UIViewController { return label }() - private lazy var getStartedButton: UIButton = { - let button = FancyButton() - button.isPrimary = true - button.setTitle(Strings.introButtonTitle, for: .normal) - button.addTarget(self, action: #selector(getStartedTapped), for: .touchUpInside) - return button - }() + private lazy var buttonNext: UIButton = { + var configuration = UIButton.Configuration.primary() + configuration.title = Strings.introButtonTitle - // MARK: - Initializers + return UIButton(configuration: configuration, primaryAction: .init { [weak self] _ in + self?.buttonGetStartedTapped() + }) + }() private let blog: Blog private let tracker: BloggingRemindersTracker private let source: BloggingRemindersTracker.FlowStartSource - private weak var delegate: BloggingRemindersFlowDelegate? + private let onNextTapped: () -> Void init(for blog: Blog, tracker: BloggingRemindersTracker, source: BloggingRemindersTracker.FlowStartSource, - delegate: BloggingRemindersFlowDelegate? = nil) { + onNextTapped: @escaping () -> Void) { self.blog = blog self.tracker = tracker self.source = source - self.delegate = delegate + self.onNextTapped = onNextTapped super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { - // This VC is designed to be instantiated programmatically. If we ever need to initialize this VC - // from a coder, we can implement support for it - but I don't think it's necessary right now. - // - diegoreymendez fatalError("Use init(tracker:) instead") } @@ -105,22 +101,6 @@ class BloggingRemindersFlowIntroViewController: UIViewController { } } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - calculatePreferredContentSize() - } - - private func calculatePreferredContentSize() { - let size = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) - preferredContentSize = view.systemLayoutSizeFitting(size) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - view.setNeedsLayout() - } - // MARK: - View Configuration private func configureStackView() { @@ -129,7 +109,7 @@ class BloggingRemindersFlowIntroViewController: UIViewController { imageView, titleLabel, promptLabel, - getStartedButton + buttonNext ]) stackView.setCustomSpacing(Metrics.afterPromptSpacing, after: promptLabel) } @@ -141,48 +121,23 @@ class BloggingRemindersFlowIntroViewController: UIViewController { stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Metrics.edgeMargins.top), stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeBottomAnchor, constant: -Metrics.edgeMargins.bottom), - getStartedButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.getStartedButtonHeight), - getStartedButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), + buttonNext.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.getStartedButtonHeight), + buttonNext.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) } - @objc private func getStartedTapped() { + private func buttonGetStartedTapped() { tracker.buttonPressed(button: .continue, screen: .main) - - do { - let flowSettingsViewController = try BloggingRemindersFlowSettingsViewController(for: blog, tracker: tracker, delegate: delegate) - - navigationController?.pushViewController(flowSettingsViewController, animated: true) - } catch { - DDLogError("Could not instantiate the blogging reminders settings VC: \(error.localizedDescription)") - dismiss(animated: true, completion: nil) - } + onNextTapped() } } extension BloggingRemindersFlowIntroViewController: BloggingRemindersActions { - @objc private func dismissTapped() { dismiss(from: .dismiss, screen: .main, tracker: tracker) } } -// MARK: - DrawerPresentable - -extension BloggingRemindersFlowIntroViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .intrinsicHeight - } -} - -// MARK: - ChildDrawerPositionable - -extension BloggingRemindersFlowIntroViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - return .collapsed - } -} - // MARK: - Constants private enum Strings { diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowSettingsViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift similarity index 97% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowSettingsViewController.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift index 17af112e0157..22f605d939b9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersFlowSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift @@ -6,7 +6,7 @@ protocol BloggingRemindersFlowDelegate: AnyObject { func didSetUpBloggingReminders() } -class BloggingRemindersFlowSettingsViewController: UIViewController { +final class BloggingRemindersFlowSettingsViewController: UIViewController { // MARK: - Subviews @@ -110,8 +110,8 @@ class BloggingRemindersFlowSettingsViewController: UIViewController { makeDivider() }() - private lazy var timeSelectionButton: TimeSelectionButton = { - let button = TimeSelectionButton(selectedTime: scheduledTime.toLocalTime()) + private lazy var timeSelectionButton: BloggingRemindersTimeSelectionButton = { + let button = BloggingRemindersTimeSelectionButton(selectedTime: scheduledTime.toLocalTime()) button.isUserInteractionEnabled = true button.translatesAutoresizingMaskIntoConstraints = false button.addTarget(self, action: #selector(navigateToTimePicker), for: .touchUpInside) @@ -408,8 +408,7 @@ class BloggingRemindersFlowSettingsViewController: UIViewController { private extension BloggingRemindersFlowSettingsViewController { func pushTimeSelectionViewController() { - let viewController = TimeSelectionViewController(scheduledTime: scheduler.scheduledTime(for: blog), - tracker: tracker) { [weak self] date in + let viewController = BloggingRemindersTimeSelectionViewController(scheduledTime: scheduler.scheduledTime(for: blog), tracker: tracker) { [weak self] date in self?.scheduledTime = date self?.timeSelectionButton.setSelectedTime(date.toLocalTime()) self?.refreshNextButton() @@ -653,18 +652,6 @@ extension BloggingRemindersFlowSettingsViewController: BloggingRemindersActions } } -extension BloggingRemindersFlowSettingsViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .maxHeight - } -} - -extension BloggingRemindersFlowSettingsViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - return .expanded - } -} - // MARK: - Blogging Prompts Helpers private extension BloggingRemindersFlowSettingsViewController { diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift new file mode 100644 index 000000000000..9b943350c866 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift @@ -0,0 +1,32 @@ +import UIKit +import WordPressUI + +final class BloggingRemindersNavigationController: UINavigationController { + private let onDismiss: (() -> Void)? + + required init(rootViewController: UIViewController, onDismiss: (() -> Void)? = nil) { + self.onDismiss = onDismiss + + super.init(rootViewController: rootViewController) + + setNavigationBarHidden(true, animated: false) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if isBeingDismissedDirectlyOrByAncestor() { + onDismiss?() + } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersPushPromptViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift similarity index 96% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersPushPromptViewController.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift index 5c694e8b4118..19d429f150b1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersPushPromptViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift @@ -230,20 +230,6 @@ extension BloggingRemindersPushPromptViewController: BloggingRemindersActions { } } -// MARK: - DrawerPresentable - -extension BloggingRemindersPushPromptViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .maxHeight - } -} - -extension BloggingRemindersPushPromptViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - return .expanded - } -} - // MARK: - Constants private enum TextContent { diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersTracker.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersTracker.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/BloggingRemindersTracker.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersTracker.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/CalendarDayToggleButton.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/CalendarDayToggleButton.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/CalendarDayToggleButton.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/CalendarDayToggleButton.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionButton.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionButton.swift similarity index 97% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionButton.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionButton.swift index 2dd59d12c661..ab64a5d8ef73 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionButton.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionButton.swift @@ -1,6 +1,6 @@ import UIKit -class TimeSelectionButton: UIButton { +final class BloggingRemindersTimeSelectionButton: UIButton { private(set) var selectedTime: String { didSet { diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift similarity index 91% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift index 973353ba2781..40aa9bd75d51 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionView.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift @@ -1,7 +1,7 @@ import UIKit /// A view that contains a time picker and a title reporting the selected time -class TimeSelectionView: UIView { +final class BloggingRemindersTimeSelectionView: UIView { private var selectedTime: Date @@ -19,8 +19,8 @@ class TimeSelectionView: UIView { titleBar.setSelectedTime(timePicker.date.toLocalTime()) } - private lazy var titleBar: TimeSelectionButton = { - let button = TimeSelectionButton(selectedTime: selectedTime.toLocalTime(), insets: Self.titleInsets) + private lazy var titleBar: BloggingRemindersTimeSelectionButton = { + let button = BloggingRemindersTimeSelectionButton(selectedTime: selectedTime.toLocalTime(), insets: Self.titleInsets) button.translatesAutoresizingMaskIntoConstraints = false button.isUserInteractionEnabled = false button.isChevronHidden = true diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift similarity index 65% rename from WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionViewController.swift rename to WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift index 16305668ca88..b9865ac7d24a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Reminders/Time Selector/TimeSelectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift @@ -1,7 +1,7 @@ import UIKit import WordPressUI -class TimeSelectionViewController: UIViewController { +final class BloggingRemindersTimeSelectionViewController: UIViewController { var preferredWidth: CGFloat? @@ -11,8 +11,8 @@ class TimeSelectionViewController: UIViewController { private var onDismiss: ((Date) -> Void)? - private lazy var timeSelectionView: TimeSelectionView = { - let view = TimeSelectionView(selectedTime: scheduledTime) + private lazy var timeSelectionView: BloggingRemindersTimeSelectionView = { + let view = BloggingRemindersTimeSelectionView(selectedTime: scheduledTime) view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -36,18 +36,6 @@ class TimeSelectionViewController: UIViewController { self.view = mainView } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - calculatePreferredSize() - } - - private func calculatePreferredSize() { - let targetSize = CGSize(width: view.bounds.width, - height: UIView.layoutFittingCompressedSize.height) - preferredContentSize = view.systemLayoutSizeFitting(targetSize) - navigationController?.preferredContentSize = preferredContentSize - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: false) @@ -55,6 +43,7 @@ class TimeSelectionViewController: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + navigationController?.setNavigationBarHidden(true, animated: false) if isMovingFromParent { onDismiss?(timeSelectionView.getDate()) @@ -71,16 +60,3 @@ class TimeSelectionViewController: UIViewController { } } } - -// MARK: - DrawerPresentable -extension TimeSelectionViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .intrinsicHeight - } -} - -extension TimeSelectionViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - return .collapsed - } -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift index 3f17f95d9a15..ff9c853a6cc4 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift @@ -51,9 +51,3 @@ extension JetpackOverlayViewController: DrawerPresentable { .maxWidth } } - -extension JetpackOverlayViewController: ChildDrawerPositionable { - var preferredDrawerPosition: DrawerPosition { - .collapsed - } -} From c9dca5f8dafdebe6abe48edd071dd8dd4fe21dc2 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 30 Dec 2024 23:07:08 -0500 Subject: [PATCH 041/101] Simplify BloggingRemindersFlowIntroViewController --- .../BloggingRemindersFlow.swift | 2 +- ...gingRemindersFlowIntroViewController.swift | 32 ++++--------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift index 622047076dc7..3f1b3472c70e 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift @@ -40,7 +40,7 @@ final class BloggingRemindersFlow { if hasShownWeeklyRemindersFlow(for: blog) { showSettings() } else { - let introVC = BloggingRemindersFlowIntroViewController(for: blog, tracker: tracker, source: source) { [weak presentingViewController] in + let introVC = BloggingRemindersFlowIntroViewController(tracker: tracker) { [weak presentingViewController] in presentingViewController?.dismiss(animated: true) { showSettings() } diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift index 31447713322e..a9bb1eadee46 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift @@ -8,7 +8,7 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Metrics.stackSpacing + stackView.spacing = 20 stackView.axis = .vertical stackView.alignment = .center stackView.distribution = .equalSpacing @@ -52,18 +52,11 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { }) }() - private let blog: Blog private let tracker: BloggingRemindersTracker - private let source: BloggingRemindersTracker.FlowStartSource private let onNextTapped: () -> Void - init(for blog: Blog, - tracker: BloggingRemindersTracker, - source: BloggingRemindersTracker.FlowStartSource, - onNextTapped: @escaping () -> Void) { - self.blog = blog + init(tracker: BloggingRemindersTracker, onNextTapped: @escaping () -> Void) { self.tracker = tracker - self.source = source self.onNextTapped = onNextTapped super.init(nibName: nil, bundle: nil) @@ -105,23 +98,21 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { private func configureStackView() { view.addSubview(stackView) + let spacer = UIView() stackView.addArrangedSubviews([ imageView, titleLabel, promptLabel, + spacer, buttonNext ]) - stackView.setCustomSpacing(Metrics.afterPromptSpacing, after: promptLabel) + stackView.setCustomSpacing(8, after: titleLabel) + stackView.setCustomSpacing(24, after: promptLabel) } private func configureConstraints() { + stackView.pinEdges(to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(.all, 24)) NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.edgeMargins.left), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.edgeMargins.right), - stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Metrics.edgeMargins.top), - stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeBottomAnchor, constant: -Metrics.edgeMargins.bottom), - - buttonNext.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.getStartedButtonHeight), buttonNext.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) } @@ -138,8 +129,6 @@ extension BloggingRemindersFlowIntroViewController: BloggingRemindersActions { } } -// MARK: - Constants - private enum Strings { static let introTitle = NSLocalizedString("bloggingRemindersPrompt.intro.title", value: "Blogging Reminders", comment: "Title of the Blogging Reminders Settings screen.") static let introDescription = NSLocalizedString("bloggingRemindersPrompt.intro.details", value: "Set up your blogging reminders on days you want to post.", comment: "Description on the first screen of the Blogging Reminders Settings flow called aftet post publishing.") @@ -149,10 +138,3 @@ private enum Strings { private enum Images { static let celebrationImageName = "reminders-celebration" } - -private enum Metrics { - static let edgeMargins = UIEdgeInsets(top: 46, left: 20, bottom: 20, right: 20) - static let stackSpacing: CGFloat = 20.0 - static let afterPromptSpacing: CGFloat = 24.0 - static let getStartedButtonHeight: CGFloat = 44.0 -} From 956a3f0cc22f9062a0ff9a84c5b791ab260b600f Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 11:20:26 -0500 Subject: [PATCH 042/101] Add SpacerView --- .../WordPressUI/Views/SpacerView.swift | 54 +++++++++++++++++++ ...gingRemindersFlowIntroViewController.swift | 40 +++++--------- 2 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 Modules/Sources/WordPressUI/Views/SpacerView.swift diff --git a/Modules/Sources/WordPressUI/Views/SpacerView.swift b/Modules/Sources/WordPressUI/Views/SpacerView.swift new file mode 100644 index 000000000000..2dd3e6bd46d6 --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/SpacerView.swift @@ -0,0 +1,54 @@ +import UIKit + +public final class SpacerView: UIView { + public convenience init(minWidth: CGFloat) { + self.init() + + widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true + } + + public convenience init(minHeight: CGFloat) { + self.init() + + heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight).isActive = true + } + + public convenience init(width: CGFloat) { + self.init() + + widthAnchor.constraint(equalToConstant: width).isActive = true + } + + public convenience init(height: CGFloat) { + self.init() + + heightAnchor.constraint(equalToConstant: height).isActive = true + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + + // Make sure it compresses or expands before any other views if needed. + setContentCompressionResistancePriority(.init(10), for: .horizontal) + setContentCompressionResistancePriority(.init(10), for: .vertical) + setContentHuggingPriority(.init(10), for: .horizontal) + setContentHuggingPriority(.init(10), for: .vertical) + } + + public override var intrinsicContentSize: CGSize { + CGSizeMake(0, 0) // Avoid ambiguous layout + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override public class var layerClass: AnyClass { + CATransformLayer.self // Draws nothing + } + + override public var backgroundColor: UIColor? { + get { return nil } + set { /* Do nothing */ } + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift index a9bb1eadee46..13666ba0dc1c 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift @@ -3,20 +3,8 @@ import WordPressUI final class BloggingRemindersFlowIntroViewController: UIViewController { - // MARK: - Subviews - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = 20 - stackView.axis = .vertical - stackView.alignment = .center - stackView.distribution = .equalSpacing - return stackView - }() - private let imageView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: Images.celebrationImageName)) + let imageView = UIImageView(image: UIImage(named: "reminders-celebration")) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .systemYellow return imageView @@ -73,8 +61,7 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { view.backgroundColor = .systemBackground - configureStackView() - configureConstraints() + setupView() promptLabel.text = Strings.introDescription } @@ -96,22 +83,23 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { // MARK: - View Configuration - private func configureStackView() { - view.addSubview(stackView) - let spacer = UIView() - stackView.addArrangedSubviews([ + private func setupView() { + let stackView = UIStackView(axis: .vertical, alignment: .center, spacing: 20, [ imageView, titleLabel, promptLabel, - spacer, + SpacerView(minHeight: 8), buttonNext ]) stackView.setCustomSpacing(8, after: titleLabel) stackView.setCustomSpacing(24, after: promptLabel) - } - private func configureConstraints() { - stackView.pinEdges(to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(.all, 24)) + view.addSubview(stackView) + + var insets = UIEdgeInsets(.all, 24) + insets.top = 48 + + stackView.pinEdges(to: view.safeAreaLayoutGuide, insets: insets) NSLayoutConstraint.activate([ buttonNext.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) @@ -132,9 +120,5 @@ extension BloggingRemindersFlowIntroViewController: BloggingRemindersActions { private enum Strings { static let introTitle = NSLocalizedString("bloggingRemindersPrompt.intro.title", value: "Blogging Reminders", comment: "Title of the Blogging Reminders Settings screen.") static let introDescription = NSLocalizedString("bloggingRemindersPrompt.intro.details", value: "Set up your blogging reminders on days you want to post.", comment: "Description on the first screen of the Blogging Reminders Settings flow called aftet post publishing.") - static let introButtonTitle = NSLocalizedString("bloggingRemindersPrompt.intro.continueButton", value: "Set reminders", comment: "Title of the set goals button in the Blogging Reminders Settings flow.") -} - -private enum Images { - static let celebrationImageName = "reminders-celebration" + static let introButtonTitle = NSLocalizedString("bloggingRemindersPrompt.intro.continueButton", value: "Set Reminders", comment: "Title of the set goals button in the Blogging Reminders Settings flow.") } From a49d006eb073af388e25cdc15091bc1ad72a9be9 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:17:46 -0500 Subject: [PATCH 043/101] Add BottomToolbarView --- .../Extensions/UIButton+Extensions.swift | 14 +++ .../WordPressUI/Views/BottomToolbarView.swift | 86 +++++++++++++++++++ .../WordPressUI/Views/SeparatorView.swift | 25 ++++++ .../ExperimentalFeaturesList.swift | 0 .../ExperimentalFeaturesViewModel.swift | 0 .../Feature.swift | 0 .../Extensions/UIButton+Extensions.swift | 13 --- .../Views}/RestApiUpgradePrompt.swift | 8 +- .../BloggingRemindersFlow.swift | 10 ++- ...gingRemindersFlowIntroViewController.swift | 66 ++++++++------ 10 files changed, 175 insertions(+), 47 deletions(-) create mode 100644 Modules/Sources/WordPressUI/Extensions/UIButton+Extensions.swift create mode 100644 Modules/Sources/WordPressUI/Views/BottomToolbarView.swift create mode 100644 Modules/Sources/WordPressUI/Views/SeparatorView.swift rename Modules/Sources/WordPressUI/Views/Settings/{Experimental Features => ExperimentalFeatures}/ExperimentalFeaturesList.swift (100%) rename Modules/Sources/WordPressUI/Views/Settings/{Experimental Features => ExperimentalFeatures}/ExperimentalFeaturesViewModel.swift (100%) rename Modules/Sources/WordPressUI/Views/Settings/{Experimental Features => ExperimentalFeatures}/Feature.swift (100%) rename {Modules/Sources/WordPressUI/Components => WordPress/Classes/ViewRelated/Blog/Blog Details/Views}/RestApiUpgradePrompt.swift (93%) diff --git a/Modules/Sources/WordPressUI/Extensions/UIButton+Extensions.swift b/Modules/Sources/WordPressUI/Extensions/UIButton+Extensions.swift new file mode 100644 index 000000000000..4c7b6581fb84 --- /dev/null +++ b/Modules/Sources/WordPressUI/Extensions/UIButton+Extensions.swift @@ -0,0 +1,14 @@ +import UIKit + +extension UIButton.Configuration { + public static func primary() -> UIButton.Configuration { + var configuration = UIButton.Configuration.borderedProminent() + configuration.titleTextAttributesTransformer = .init { attributes in + var attributes = attributes + attributes.font = UIFont.preferredFont(forTextStyle: .headline) + return attributes + } + configuration.buttonSize = .large + return configuration + } +} diff --git a/Modules/Sources/WordPressUI/Views/BottomToolbarView.swift b/Modules/Sources/WordPressUI/Views/BottomToolbarView.swift new file mode 100644 index 000000000000..14a7bb96c38e --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/BottomToolbarView.swift @@ -0,0 +1,86 @@ +import UIKit +import Combine + +/// A custom bottom toolbar implementation that, unlike the native toolbar, +/// can accommodate larger buttons but shares a lot of its behavior including +/// edge appearance. +public class BottomToolbarView: UIView { + private let separator = SeparatorView.horizontal() + private let effectView = UIVisualEffectView() + private var isEdgeAppearanceEnabled = false + private weak var scrollView: UIScrollView? + private var cancellable: AnyCancellable? + + public let contentView = UIView() + + public override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(effectView) + addSubview(separator) + + separator.pinEdges([.top, .horizontal]) + effectView.pinEdges() + + effectView.contentView.addSubview(contentView) + + contentView.pinEdges(to: effectView.contentView.safeAreaLayoutGuide, insets: UIEdgeInsets(.all, 20)) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { + super.layoutSubviews() + + updateScrollViewContentInsets() + } + + public override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + + updateScrollViewContentInsets() + } + + /// - warning: If you use this view, you'll typically need to take over the + /// scroll view content inset adjustment. + public func configure(in viewController: UIViewController, scrollView: UIScrollView) { + viewController.view.addSubview(self) + pinEdges([.horizontal, .bottom]) + self.scrollView = scrollView + + cancellable = scrollView.publisher(for: \.contentOffset, options: [.new]).sink { [weak self] offset in + self?.updateEdgeAppearance(animated: true) + } + updateScrollViewContentInsets() + updateEdgeAppearance(animated: false) + } + + private func updateEdgeAppearance(animated: Bool) { + guard let scrollView, let superview else { return } + + let isContentOverlapping = superview.convert(scrollView.contentLayoutGuide.layoutFrame, from: scrollView).maxY > (frame.minY + 16) + setEdgeAppearanceEnabled(!isContentOverlapping, animated: animated) + } + + private func setEdgeAppearanceEnabled(_ isEnabled: Bool, animated: Bool) { + guard isEdgeAppearanceEnabled != isEnabled else { return } + isEdgeAppearanceEnabled = isEnabled + + UIView.animate(withDuration: animated ? 0 : 0.33, delay: 0.0, options: [.allowUserInteraction, .beginFromCurrentState]) { + self.effectView.effect = isEnabled ? nil : UIBlurEffect(style: .extraLight) + self.separator.alpha = isEnabled ? 0 : 1 + } + } + + // The toolbar does no extend the safe area because it itself depends on it, + // so it resorts to changing `contentInset` instead. + private func updateScrollViewContentInsets() { + guard let scrollView else { return } + let bottomInset = bounds.height - safeAreaInsets.bottom + if scrollView.contentInset.bottom != bottomInset { + scrollView.contentInset.bottom = bottomInset + } + } +} diff --git a/Modules/Sources/WordPressUI/Views/SeparatorView.swift b/Modules/Sources/WordPressUI/Views/SeparatorView.swift new file mode 100644 index 000000000000..8d0d8b888ddb --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/SeparatorView.swift @@ -0,0 +1,25 @@ +import UIKit + +public final class SeparatorView: UIView { + public static func horizontal() -> SeparatorView { + let view = SeparatorView() + view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true + return view + } + + public static func vertical() -> SeparatorView { + let view = SeparatorView() + view.widthAnchor.constraint(equalToConstant: 0.5).isActive = true + return view + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .separator + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Modules/Sources/WordPressUI/Views/Settings/Experimental Features/ExperimentalFeaturesList.swift b/Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/ExperimentalFeaturesList.swift similarity index 100% rename from Modules/Sources/WordPressUI/Views/Settings/Experimental Features/ExperimentalFeaturesList.swift rename to Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/ExperimentalFeaturesList.swift diff --git a/Modules/Sources/WordPressUI/Views/Settings/Experimental Features/ExperimentalFeaturesViewModel.swift b/Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/ExperimentalFeaturesViewModel.swift similarity index 100% rename from Modules/Sources/WordPressUI/Views/Settings/Experimental Features/ExperimentalFeaturesViewModel.swift rename to Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/ExperimentalFeaturesViewModel.swift diff --git a/Modules/Sources/WordPressUI/Views/Settings/Experimental Features/Feature.swift b/Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/Feature.swift similarity index 100% rename from Modules/Sources/WordPressUI/Views/Settings/Experimental Features/Feature.swift rename to Modules/Sources/WordPressUI/Views/Settings/ExperimentalFeatures/Feature.swift diff --git a/WordPress/Classes/Extensions/UIButton+Extensions.swift b/WordPress/Classes/Extensions/UIButton+Extensions.swift index 2194627639e6..68b46e1ee49e 100644 --- a/WordPress/Classes/Extensions/UIButton+Extensions.swift +++ b/WordPress/Classes/Extensions/UIButton+Extensions.swift @@ -31,16 +31,3 @@ extension UIButton { }()) } } - -extension UIButton.Configuration { - static func primary() -> UIButton.Configuration { - var configuration = UIButton.Configuration.borderedProminent() - configuration.titleTextAttributesTransformer = .init { attributes in - var attributes = attributes - attributes.font = UIFont.preferredFont(forTextStyle: .headline) - return attributes - } - configuration.buttonSize = .large - return configuration - } -} diff --git a/Modules/Sources/WordPressUI/Components/RestApiUpgradePrompt.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Views/RestApiUpgradePrompt.swift similarity index 93% rename from Modules/Sources/WordPressUI/Components/RestApiUpgradePrompt.swift rename to WordPress/Classes/ViewRelated/Blog/Blog Details/Views/RestApiUpgradePrompt.swift index 7b6160afff0c..a276f9a5fd18 100644 --- a/Modules/Sources/WordPressUI/Components/RestApiUpgradePrompt.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Views/RestApiUpgradePrompt.swift @@ -1,3 +1,5 @@ +import UIKit +import WordPressUI import SwiftUI public struct RestApiUpgradePrompt: View { @@ -40,9 +42,3 @@ public struct RestApiUpgradePrompt: View { } } } - -#Preview { - RestApiUpgradePrompt(localizedFeatureName: "User Management") { - debugPrint("Tapped Get Started") - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift index 3f1b3472c70e..18aa466c0054 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift @@ -45,8 +45,14 @@ final class BloggingRemindersFlow { showSettings() } } - introVC.sheetPresentationController?.detents = [.medium()] - presentingViewController.present(introVC, animated: true) + let navigationVC = UINavigationController(rootViewController: introVC) + if presentingViewController.traitCollection.horizontalSizeClass == .regular { + navigationVC.preferredContentSize = CGSize(width: 375, height: 420) + } else { + navigationVC.sheetPresentationController?.detents = [.medium()] + navigationVC.sheetPresentationController?.preferredCornerRadius = 16 + } + presentingViewController.present(navigationVC, animated: true) } setHasShownWeeklyRemindersFlow(for: blog) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift index 13666ba0dc1c..8d62ab6b8cd4 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowIntroViewController.swift @@ -1,11 +1,11 @@ import UIKit -import WordPressUI + import WordPressUI final class BloggingRemindersFlowIntroViewController: UIViewController { + private let scrollView = UIScrollView() private let imageView: UIImageView = { let imageView = UIImageView(image: UIImage(named: "reminders-celebration")) - imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .systemYellow return imageView }() @@ -13,8 +13,7 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { private let titleLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true - label.adjustsFontSizeToFitWidth = true - label.font = WPStyleGuide.serifFontForTextStyle(.title1, fontWeight: .semibold) + label.font = .preferredFont(forTextStyle: .title1).withWeight(.semibold) label.numberOfLines = 2 label.textAlignment = .center label.text = Strings.introTitle @@ -24,7 +23,6 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { private let promptLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true - label.adjustsFontSizeToFitWidth = true label.font = .preferredFont(forTextStyle: .body) label.numberOfLines = 5 label.textAlignment = .center @@ -35,12 +33,17 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { var configuration = UIButton.Configuration.primary() configuration.title = Strings.introButtonTitle - return UIButton(configuration: configuration, primaryAction: .init { [weak self] _ in - self?.buttonGetStartedTapped() + let button = UIButton(configuration: configuration, primaryAction: .init { [weak self] _ in + self?.buttonContinueTapped() }) + button.titleLabel?.adjustsFontForContentSizeCategory = true + return button }() + private let bottomBarView = BottomToolbarView() + private let tracker: BloggingRemindersTracker + private var isOnNextTapped = false private let onNextTapped: () -> Void init(tracker: BloggingRemindersTracker, onNextTapped: @escaping () -> Void) { @@ -62,7 +65,13 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { view.backgroundColor = .systemBackground setupView() + setupBottomBar() + promptLabel.text = Strings.introDescription + + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: .init(handler: { [weak self] _ in + self?.buttonCloseTapped() + })) } override func viewDidAppear(_ animated: Bool) { @@ -74,9 +83,7 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - // If a parent VC is being dismissed, and this is the last view shown in its navigation controller, we'll assume - // the flow was interrupted. - if isBeingDismissedDirectlyOrByAncestor() && navigationController?.viewControllers.last == self { + if !isOnNextTapped { tracker.flowDismissed(source: .main) } } @@ -87,33 +94,40 @@ final class BloggingRemindersFlowIntroViewController: UIViewController { let stackView = UIStackView(axis: .vertical, alignment: .center, spacing: 20, [ imageView, titleLabel, - promptLabel, - SpacerView(minHeight: 8), - buttonNext + promptLabel ]) stackView.setCustomSpacing(8, after: titleLabel) - stackView.setCustomSpacing(24, after: promptLabel) - view.addSubview(stackView) + scrollView.showsVerticalScrollIndicator = false + scrollView.alwaysBounceVertical = false - var insets = UIEdgeInsets(.all, 24) - insets.top = 48 + scrollView.addSubview(stackView) + view.addSubview(scrollView) - stackView.pinEdges(to: view.safeAreaLayoutGuide, insets: insets) - NSLayoutConstraint.activate([ - buttonNext.widthAnchor.constraint(equalTo: stackView.widthAnchor), - ]) + stackView.pinEdges(insets: UIEdgeInsets(.all, 20)) + stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40).isActive = true + + scrollView.pinEdges() } - private func buttonGetStartedTapped() { + private func setupBottomBar() { + bottomBarView.contentView.addSubview(buttonNext) + buttonNext.pinEdges() + + bottomBarView.configure(in: self, scrollView: scrollView) + } + + // MARK: Actions + + private func buttonContinueTapped() { tracker.buttonPressed(button: .continue, screen: .main) + isOnNextTapped = true onNextTapped() } -} -extension BloggingRemindersFlowIntroViewController: BloggingRemindersActions { - @objc private func dismissTapped() { - dismiss(from: .dismiss, screen: .main, tracker: tracker) + private func buttonCloseTapped() { + tracker.buttonPressed(button: .dismiss, screen: .main) + presentingViewController?.dismiss(animated: true, completion: nil) } } From 6431474fe0428dcf7abe348655507b1b5de19627 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:33:55 -0500 Subject: [PATCH 044/101] Fix notice covering the blogging reminders fow --- .../Blog/BloggingReminders/BloggingRemindersFlow.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift index 18aa466c0054..361b0d81fb55 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressFlux import WordPressUI final class BloggingRemindersFlow { @@ -27,10 +28,8 @@ final class BloggingRemindersFlow { do { let settingsVC = try BloggingRemindersFlowSettingsViewController(for: blog, tracker: tracker, delegate: delegate) let navigationController = BloggingRemindersNavigationController(rootViewController: settingsVC, onDismiss: { - NoticesDispatch.unlock() onDismiss?() }) - NoticesDispatch.lock() presentingViewController?.present(navigationController, animated: true) } catch { wpAssertionFailure("Could not instantiate the blogging reminders settings VC", userInfo: ["error": "\(error)"]) @@ -56,6 +55,7 @@ final class BloggingRemindersFlow { } setHasShownWeeklyRemindersFlow(for: blog) + ActionDispatcher.dispatch(NoticeAction.dismiss) } // MARK: - Weekly reminders flow presentation status From 79334a26e77f848f91b2ba3f168f089dc18bab17 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:37:22 -0500 Subject: [PATCH 045/101] Replace FancyButton --- .../BloggingRemindersFlowSettingsViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift index 22f605d939b9..e374c584e285 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift @@ -54,8 +54,8 @@ final class BloggingRemindersFlowSettingsViewController: UIViewController { }() private lazy var button: UIButton = { - let button = FancyButton() - button.isPrimary = true + var configuration = UIButton.Configuration.primary() + let button = UIButton(configuration: configuration, primaryAction: nil) button.addTarget(self, action: #selector(notifyMeButtonTapped), for: .touchUpInside) return button }() From 9c0bed9112a0f1ec64c8dfd499abd775504ff275 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:41:26 -0500 Subject: [PATCH 046/101] Add close button to BloggingRemindersFlowSettingsViewController --- ...emindersFlowCompletionViewController.swift | 2 -- ...gRemindersFlowSettingsViewController.swift | 21 +++++++------------ ...loggingRemindersNavigationController.swift | 2 -- ...ingRemindersPushPromptViewController.swift | 2 -- ...RemindersTimeSelectionViewController.swift | 2 -- 5 files changed, 7 insertions(+), 22 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift index c71158b053ff..df0176ce46c6 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift @@ -100,8 +100,6 @@ class BloggingRemindersFlowCompletionViewController: UIViewController { configureConstraints() configurePromptLabel() configureTitleLabel() - - navigationController?.setNavigationBarHidden(true, animated: false) } override func viewDidAppear(_ animated: Bool) { diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift index e374c584e285..d052ea4bdf36 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift @@ -279,13 +279,16 @@ final class BloggingRemindersFlowSettingsViewController: UIViewController { refreshFrequencyLabel() showFullUI(shouldShowFullUI) + + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: .init(handler: { [weak self] _ in + self?.presentingViewController?.dismiss(animated: true) + })) } override func viewDidAppear(_ animated: Bool) { - tracker.screenShown(.dayPicker) - super.viewDidAppear(animated) - calculatePreferredContentSize() + + tracker.screenShown(.dayPicker) } override func viewDidDisappear(_ animated: Bool) { @@ -298,11 +301,6 @@ final class BloggingRemindersFlowSettingsViewController: UIViewController { } } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - calculatePreferredContentSize() - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -311,8 +309,8 @@ final class BloggingRemindersFlowSettingsViewController: UIViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) + showFullUI(shouldShowFullUI) - calculatePreferredContentSize() } // MARK: - Actions @@ -498,11 +496,6 @@ private extension BloggingRemindersFlowSettingsViewController { frequencyLabel.sizeToFit() } - func calculatePreferredContentSize() { - let size = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) - preferredContentSize = view.systemLayoutSizeFitting(size) - } - func configureStackView() { view.addSubview(stackView) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift index 9b943350c866..ae119f8e8ad3 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersNavigationController.swift @@ -8,8 +8,6 @@ final class BloggingRemindersNavigationController: UINavigationController { self.onDismiss = onDismiss super.init(rootViewController: rootViewController) - - setNavigationBarHidden(true, animated: false) } required init?(coder aDecoder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift index 19d429f150b1..27c5a739f310 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift @@ -124,8 +124,6 @@ class BloggingRemindersPushPromptViewController: UIViewController { view.addSubview(turnOnNotificationsButton) configureConstraints() - - navigationController?.setNavigationBarHidden(true, animated: false) } override func viewDidAppear(_ animated: Bool) { diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift index b9865ac7d24a..c8af9acd0fca 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift @@ -38,13 +38,11 @@ final class BloggingRemindersTimeSelectionViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(false, animated: false) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - navigationController?.setNavigationBarHidden(true, animated: false) if isMovingFromParent { onDismiss?(timeSelectionView.getDate()) } From 882df62cc94913abe1ea074d06b24e195ab63e2f Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:48:28 -0500 Subject: [PATCH 047/101] Fix BloggingRemindersTimeSelectionViewController presentation --- ...gRemindersFlowSettingsViewController.swift | 1 - .../BloggingRemindersTimeSelectionView.swift | 5 ++-- ...RemindersTimeSelectionViewController.swift | 24 ++++++------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift index d052ea4bdf36..99f93f2931e4 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowSettingsViewController.swift @@ -412,7 +412,6 @@ private extension BloggingRemindersFlowSettingsViewController { self?.refreshNextButton() self?.refreshFrequencyLabel() } - viewController.preferredWidth = self.view.frame.width navigationController?.pushViewController(viewController, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift index 40aa9bd75d51..0160e57fc9c7 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionView.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressUI /// A view that contains a time picker and a title reporting the selected time final class BloggingRemindersTimeSelectionView: UIView { @@ -65,9 +66,9 @@ final class BloggingRemindersTimeSelectionView: UIView { self.selectedTime = selectedTime super.init(frame: .zero) - backgroundColor = .systemBackground addSubview(verticalStackView) - pinSubviewToSafeArea(verticalStackView) + verticalStackView.pinEdges(to: safeAreaLayoutGuide) + NSLayoutConstraint.activate([ timePicker.centerXAnchor.constraint(equalTo: centerXAnchor), titleBar.widthAnchor.constraint(equalTo: widthAnchor), diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift index c8af9acd0fca..99b75e03bda1 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/TimeSelector/BloggingRemindersTimeSelectionViewController.swift @@ -2,20 +2,13 @@ import UIKit import WordPressUI final class BloggingRemindersTimeSelectionViewController: UIViewController { - - var preferredWidth: CGFloat? - private let scheduledTime: Date private let tracker: BloggingRemindersTracker private var onDismiss: ((Date) -> Void)? - private lazy var timeSelectionView: BloggingRemindersTimeSelectionView = { - let view = BloggingRemindersTimeSelectionView(selectedTime: scheduledTime) - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() + private lazy var timeSelectionView = BloggingRemindersTimeSelectionView(selectedTime: scheduledTime) init(scheduledTime: Date, tracker: BloggingRemindersTracker, onDismiss: ((Date) -> Void)? = nil) { self.scheduledTime = scheduledTime @@ -28,16 +21,13 @@ final class BloggingRemindersTimeSelectionViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { - let mainView = timeSelectionView - if let width = preferredWidth { - mainView.widthAnchor.constraint(equalToConstant: width).isActive = true - } - self.view = mainView - } + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + view.addSubview(timeSelectionView) + timeSelectionView.pinEdges([.top, .horizontal]) } override func viewWillDisappear(_ animated: Bool) { From 581a10a341802115167942d6ecf2c40f9d2a1034 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:52:48 -0500 Subject: [PATCH 048/101] Remove FancyButton from BloggingRemindersPushPromptViewController --- .../BloggingRemindersPushPromptViewController.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift index 27c5a739f310..1462d66e1072 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift @@ -1,7 +1,7 @@ import UIKit import WordPressUI -class BloggingRemindersPushPromptViewController: UIViewController { +final class BloggingRemindersPushPromptViewController: UIViewController { // MARK: - Subviews @@ -58,12 +58,12 @@ class BloggingRemindersPushPromptViewController: UIViewController { }() private lazy var turnOnNotificationsButton: UIButton = { - let button = FancyButton() + var configuration = UIButton.Configuration.primary() + configuration.title = TextContent.turnOnButtonTitle + + let button = UIButton(configuration: configuration, primaryAction: nil) button.translatesAutoresizingMaskIntoConstraints = false - button.isPrimary = true - button.setTitle(TextContent.turnOnButtonTitle, for: .normal) button.addTarget(self, action: #selector(turnOnButtonTapped), for: .touchUpInside) - button.titleLabel?.adjustsFontSizeToFitWidth = true return button }() From 9f81f7f6cb6ca15c029a1c7ca4ddbf589b0bd73e Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:53:32 -0500 Subject: [PATCH 049/101] Remove dismiss button (it now shows back) --- ...BloggingRemindersPushPromptViewController.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift index 1462d66e1072..a035ddcb23db 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift @@ -67,15 +67,6 @@ final class BloggingRemindersPushPromptViewController: UIViewController { return button }() - private lazy var dismissButton: UIButton = { - let button = UIButton(type: .custom) - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(.gridicon(.cross), for: .normal) - button.tintColor = .secondaryLabel - button.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside) - return button - }() - // MARK: - Properties /// Indicates whether push notifications have been disabled or not. @@ -118,7 +109,6 @@ final class BloggingRemindersPushPromptViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground - view.addSubview(dismissButton) configureStackView() @@ -188,9 +178,6 @@ final class BloggingRemindersPushPromptViewController: UIViewController { turnOnNotificationsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.edgeMargins.left), turnOnNotificationsButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.edgeMargins.right), turnOnNotificationsButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -Metrics.edgeMargins.bottom), - - dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.dismissButtonMargin), - dismissButton.topAnchor.constraint(equalTo: view.topAnchor, constant: Metrics.dismissButtonMargin) ]) } @@ -247,7 +234,6 @@ private enum Images { } private enum Metrics { - static let dismissButtonMargin: CGFloat = 20.0 static let edgeMargins = UIEdgeInsets(top: 80, left: 28, bottom: 80, right: 28) static let stackSpacing: CGFloat = 20.0 static let turnOnButtonHeight: CGFloat = 44.0 From 1d56a16432e490c7a6a769e0e6a7b34b4b63eb78 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 14:57:05 -0500 Subject: [PATCH 050/101] Update BloggingRemindersPushPromptViewController layout --- ...oggingRemindersPushPromptViewController.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift index a035ddcb23db..51e05e817800 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersPushPromptViewController.swift @@ -133,22 +133,12 @@ final class BloggingRemindersPushPromptViewController: UIViewController { } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - calculatePreferredContentSize() - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) hintLabel.isHidden = traitCollection.preferredContentSizeCategory.isAccessibilityCategory } - private func calculatePreferredContentSize() { - let size = CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height) - preferredContentSize = view.systemLayoutSizeFitting(size) - } - @objc private func applicationBecameActive() { refreshPushAuthorizationStatus() @@ -174,10 +164,9 @@ final class BloggingRemindersPushPromptViewController: UIViewController { stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Metrics.edgeMargins.top), turnOnNotificationsButton.topAnchor.constraint(greaterThanOrEqualTo: stackView.bottomAnchor, constant: Metrics.edgeMargins.bottom), - turnOnNotificationsButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.turnOnButtonHeight), turnOnNotificationsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.edgeMargins.left), turnOnNotificationsButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.edgeMargins.right), - turnOnNotificationsButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -Metrics.edgeMargins.bottom), + turnOnNotificationsButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Metrics.edgeMargins.bottom), ]) } @@ -234,7 +223,6 @@ private enum Images { } private enum Metrics { - static let edgeMargins = UIEdgeInsets(top: 80, left: 28, bottom: 80, right: 28) + static let edgeMargins = UIEdgeInsets(top: 80, left: 20, bottom: 20, right: 20) static let stackSpacing: CGFloat = 20.0 - static let turnOnButtonHeight: CGFloat = 44.0 } From a5d4fc23552e55fccdc9817451bbdc71be07242b Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 15:04:54 -0500 Subject: [PATCH 051/101] Remove FancyButton from BloggingRemindersFlowCompletionViewController --- .../BloggingRemindersFlowCompletionViewController.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift index df0176ce46c6..f528a9f93e43 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift @@ -1,7 +1,7 @@ import UIKit import WordPressUI -class BloggingRemindersFlowCompletionViewController: UIViewController { +final class BloggingRemindersFlowCompletionViewController: UIViewController { // MARK: - Subviews @@ -57,8 +57,10 @@ class BloggingRemindersFlowCompletionViewController: UIViewController { }() private lazy var doneButton: UIButton = { - let button = FancyButton() - button.isPrimary = true + var configuration = UIButton.Configuration.primary() + configuration.title = TextContent.doneButtonTitle + + let button = UIButton(configuration: configuration, primaryAction: nil) button.setTitle(TextContent.doneButtonTitle, for: .normal) button.addTarget(self, action: #selector(doneButtonTapped), for: .touchUpInside) return button From 94dd45ffc303bd22ed6e3b22f61b87951d8138ee Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 15:12:35 -0500 Subject: [PATCH 052/101] Update BloggingRemindersFlowCompletionViewController layout --- ...emindersFlowCompletionViewController.swift | 82 ++++++++----------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift index f528a9f93e43..86d16817cfb4 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlowCompletionViewController.swift @@ -5,18 +5,10 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { // MARK: - Subviews - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Metrics.stackSpacing - stackView.axis = .vertical - stackView.alignment = .center - stackView.distribution = .equalSpacing - return stackView - }() + private let scrollView = UIScrollView() private let imageView: UIImageView = { - let imageView = UIImageView(image: UIImage(named: Images.bellImageName)) + let imageView = UIImageView(image: UIImage(named: "reminders-bell")) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .systemYellow return imageView @@ -25,7 +17,6 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { private let titleLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true - label.adjustsFontSizeToFitWidth = true label.font = WPStyleGuide.serifFontForTextStyle(.title1, fontWeight: .semibold) label.numberOfLines = 2 label.textAlignment = .center @@ -36,7 +27,6 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { private let promptLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true - label.adjustsFontSizeToFitWidth = true label.font = .preferredFont(forTextStyle: .body) label.numberOfLines = 6 label.textAlignment = .center @@ -47,7 +37,6 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { private let hintLabel: UILabel = { let label = UILabel() label.adjustsFontForContentSizeCategory = true - label.adjustsFontSizeToFitWidth = true label.font = .preferredFont(forTextStyle: .footnote) label.text = TextContent.completionUpdateHint label.numberOfLines = 3 @@ -66,6 +55,8 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { return button }() + private let bottomBarView = BottomToolbarView() + // MARK: - Initializers let blog: Blog @@ -96,18 +87,22 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground - configureStackView() - configureConstraints() + setupView() + setupBottomBar() + configurePromptLabel() configureTitleLabel() + + navigationController?.setNavigationBarHidden(true, animated: false) } override func viewDidAppear(_ animated: Bool) { - tracker.screenShown(.allSet) - super.viewDidAppear(animated) + + tracker.screenShown(.allSet) } override func viewDidDisappear(_ animated: Bool) { @@ -118,40 +113,39 @@ final class BloggingRemindersFlowCompletionViewController: UIViewController { if isBeingDismissedDirectlyOrByAncestor() && navigationController?.viewControllers.last == self { tracker.flowCompleted() } - - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - hintLabel.isHidden = traitCollection.preferredContentSizeCategory.isAccessibilityCategory } // MARK: - View Configuration - private func configureStackView() { - view.addSubview(stackView) - - stackView.addArrangedSubviews([ + private func setupView() { + let stackView = UIStackView(axis: .vertical, alignment: .center, spacing: 8, [ imageView, titleLabel, promptLabel, - hintLabel, - doneButton + hintLabel ]) - stackView.setCustomSpacing(Metrics.afterHintSpacing, after: hintLabel) + stackView.setCustomSpacing(16, after: titleLabel) + + scrollView.showsVerticalScrollIndicator = false + scrollView.alwaysBounceVertical = false + + scrollView.addSubview(stackView) + view.addSubview(scrollView) + + var insets = UIEdgeInsets(.all, 20) + insets.top = 48 + + stackView.pinEdges(insets: insets) + stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40).isActive = true + + scrollView.pinEdges() } - private func configureConstraints() { - NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Metrics.edgeMargins.left), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Metrics.edgeMargins.right), - stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: Metrics.edgeMargins.top), - stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeBottomAnchor, constant: -Metrics.edgeMargins.bottom), + private func setupBottomBar() { + bottomBarView.contentView.addSubview(doneButton) + doneButton.pinEdges() - doneButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Metrics.doneButtonHeight), - doneButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), - ]) + bottomBarView.configure(in: self, scrollView: scrollView) } // Populates the prompt label with formatted text detailing the reminders set by the user. @@ -226,14 +220,6 @@ private enum TextContent { static let doneButtonTitle = NSLocalizedString("Done", comment: "Title for a Done button.") } -private enum Images { - static let bellImageName = "reminders-bell" -} - private enum Metrics { - static let edgeMargins = UIEdgeInsets(top: 46, left: 20, bottom: 20, right: 20) - static let stackSpacing: CGFloat = 20.0 - static let doneButtonHeight: CGFloat = 44.0 - static let afterHintSpacing: CGFloat = 24.0 static let promptTextLineSpacing: CGFloat = 1.5 } From 7bbd838d6f368c6302012e4a75ef9d1182f4c4f8 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 15:20:16 -0500 Subject: [PATCH 053/101] Update releaes notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 14842c841edb..e66b9081a057 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ * [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] * [*] Add prefetching to Reader streams [#23928] * [*] Fix an issue with blogging reminders prompt not being shown after publishing a new post [#23930] +* [*] Fix transitions in Blogging Reminders flow, improve accessibiliy, add close buttons [#23931] 25.6 ----- From c1c70e12014f20fe501f9b698f29376fd5777fdb Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 15:23:07 -0500 Subject: [PATCH 054/101] Fix typo in release notes --- RELEASE-NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e66b9081a057..a115294c325e 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,7 +3,7 @@ * [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] * [*] Add prefetching to Reader streams [#23928] * [*] Fix an issue with blogging reminders prompt not being shown after publishing a new post [#23930] -* [*] Fix transitions in Blogging Reminders flow, improve accessibiliy, add close buttons [#23931] +* [*] Fix transitions in Blogging Reminders flow, improve accessibility, add close buttons [#23931] 25.6 ----- From 60a9e23ccdcc344e81c75e6703e94491a79780d5 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 16:36:56 -0500 Subject: [PATCH 055/101] Fix compliance popover accessibility settings --- .../Foundation/CGFloat+DesignSystem.swift | 4 - .../DesignSystem/Gallery/LengthGallery.swift | 2 +- .../EEUUSCompliance/CompliancePopover.swift | 42 +++++----- .../CompliancePopoverCoordinator.swift | 8 +- .../CompliancePopoverViewController.swift | 81 +------------------ .../CompliancePopoverViewModel.swift | 2 +- 6 files changed, 30 insertions(+), 109 deletions(-) diff --git a/Modules/Sources/DesignSystem/Foundation/CGFloat+DesignSystem.swift b/Modules/Sources/DesignSystem/Foundation/CGFloat+DesignSystem.swift index b3ae3590a5e6..15a2b96b68d7 100644 --- a/Modules/Sources/DesignSystem/Foundation/CGFloat+DesignSystem.swift +++ b/Modules/Sources/DesignSystem/Foundation/CGFloat+DesignSystem.swift @@ -12,10 +12,6 @@ public extension CGFloat { public static let max: CGFloat = 48 } - public enum Hitbox { - public static let minTappableLength: CGFloat = 44 - } - public enum Radius { public static let small: CGFloat = 5 public static let medium: CGFloat = 10 diff --git a/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift b/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift index 5e7749a55446..783a1452cffe 100644 --- a/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift +++ b/Modules/Sources/DesignSystem/Gallery/LengthGallery.swift @@ -30,7 +30,7 @@ struct LengthGallery: View { ZStack { RoundedRectangle(cornerRadius: .DS.Radius.small) .fill(.background) - .frame(height: .DS.Hitbox.minTappableLength) + .frame(height: 44) HStack { Text(name) .offset(x: .DS.Padding.double) diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift index 658d4bab2d85..f9d5a224b3a8 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift @@ -1,21 +1,28 @@ import SwiftUI import JetpackStatsWidgetsCore -import DesignSystem struct CompliancePopover: View { @StateObject var viewModel: CompliancePopoverViewModel var body: some View { - VStack(alignment: .leading, spacing: .DS.Padding.double) { - titleText - subtitleText - analyticsToggle - footnote - buttonsHStack + ScrollView(.vertical) { + VStack(alignment: .leading, spacing: 8) { + titleText.padding(.top, 16) + subtitleText + analyticsToggle.padding(.top, 8) + footnote + } + .padding(20) + } + .safeAreaInset(edge: .bottom) { + HStack(spacing: 8) { + settingsButton + saveButton + } + .padding(20) + .background(Color(.systemBackground)) } - .padding(.DS.Padding.medium) - .fixedSize(horizontal: false, vertical: true) } private var titleText: some View { @@ -33,7 +40,7 @@ struct CompliancePopover: View { Toggle(Strings.toggleTitle, isOn: $viewModel.isAnalyticsEnabled) .foregroundStyle(Color(.label)) .toggleStyle(UIAppColor.switchStyle) - .padding(.vertical, .DS.Padding.single) + .padding(.vertical, 8) } private var footnote: some View { @@ -42,26 +49,19 @@ struct CompliancePopover: View { .foregroundColor(.secondary) } - private var buttonsHStack: some View { - HStack(spacing: .DS.Padding.single) { - settingsButton - saveButton - }.padding(.top, .DS.Padding.medium) - } - private var settingsButton: some View { Button(action: { self.viewModel.didTapSettings() }) { ZStack { - RoundedRectangle(cornerRadius: .DS.Padding.single) - .stroke(.gray, lineWidth: .DS.Border.thin) + RoundedRectangle(cornerRadius: 8) + .stroke(.gray, lineWidth: 0.5) Text(Strings.settingsButtonTitle) .font(.body) } } .foregroundColor(AppColor.brand) - .frame(height: .DS.Hitbox.minTappableLength) + .frame(height: 44) } private var saveButton: some View { @@ -76,7 +76,7 @@ struct CompliancePopover: View { } } .foregroundColor(.white) - .frame(height: .DS.Hitbox.minTappableLength) + .frame(height: 44) } } diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift index 5d839056ee74..69aa565667fb 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverCoordinator.swift @@ -101,10 +101,10 @@ final class CompliancePopoverCoordinator: CompliancePopoverCoordinatorProtocol { contextManager: ContextManager.shared ) complianceViewModel.coordinator = self - let complianceViewController = CompliancePopoverViewController(viewModel: complianceViewModel) - let bottomSheetViewController = BottomSheetViewController(childViewController: complianceViewController, customHeaderSpacing: 0) - - bottomSheetViewController.show(from: presentingViewController) + let complianceVC = CompliancePopoverViewController(viewModel: complianceViewModel) + complianceVC.sheetPresentationController?.detents = [.medium(), .large()] + complianceVC.isModalInPresentation = true + presentingViewController.present(complianceVC, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewController.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewController.swift index eaf6676ecc6f..151de0f31f6f 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewController.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewController.swift @@ -2,34 +2,12 @@ import UIKit import SwiftUI import WordPressUI -final class CompliancePopoverViewController: UIViewController { - - // MARK: - Dependencies - +final class CompliancePopoverViewController: UIHostingController { private let viewModel: CompliancePopoverViewModel - // MARK: - Views - - private let scrollView: UIScrollView = { - let view = UIScrollView() - view.showsVerticalScrollIndicator = false - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private let hostingController: UIHostingController - - private var contentView: UIView { - return hostingController.view - } - - // MARK: - Init - init(viewModel: CompliancePopoverViewModel) { self.viewModel = viewModel - let content = CompliancePopover(viewModel: viewModel) - self.hostingController = UIHostingController(rootView: content) - super.init(nibName: nil, bundle: nil) + super.init(rootView: CompliancePopover(viewModel: viewModel)) } required dynamic init?(coder aDecoder: NSCoder) { @@ -40,60 +18,7 @@ final class CompliancePopoverViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - self.addContentView() - self.viewModel.didDisplayPopover() - } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - // Calculate the size needed for the view to fit its content - let targetSize = CGSize(width: view.bounds.width, height: 0) - self.contentView.frame = CGRect(origin: .zero, size: targetSize) - let contentViewSize = contentView.systemLayoutSizeFitting(targetSize) - self.contentView.frame.size = contentViewSize - - // Set the scrollView's content size to match the contentView's size - // - // Scroll is enabled / disabled automatically depending on whether the `contentSize` is bigger than the its size. - self.scrollView.contentSize = contentViewSize - - // Set the preferred content size for the view controller to match the contentView's size - // - // This property should be updated when `DrawerPresentable.collapsedHeight` is `intrinsicHeight`. - // Because under the hood the `BottomSheetViewController` reads this property to layout its subviews. - self.preferredContentSize = contentViewSize - } - - private func addContentView() { - self.view.addSubview(scrollView) - self.view.pinSubviewToAllEdges(scrollView) - self.hostingController.willMove(toParent: self) - self.addChild(hostingController) - self.contentView.translatesAutoresizingMaskIntoConstraints = true - self.scrollView.addSubview(contentView) - self.hostingController.didMove(toParent: self) - } -} - -// MARK: - DrawerPresentable - -extension CompliancePopoverViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - if traitCollection.verticalSizeClass == .compact { - return .maxHeight - } - return .intrinsicHeight - } - - var allowsUserTransition: Bool { - return false - } - - var allowsDragToDismiss: Bool { - false - } - - var allowsTapToDismiss: Bool { - return false + self.viewModel.didDisplayPopover() } } diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift index 9599596f042d..ef00df80069f 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import WordPressUI -class CompliancePopoverViewModel: ObservableObject { +final class CompliancePopoverViewModel: ObservableObject { @Published var isAnalyticsEnabled: Bool = !WPAppAnalytics.userHasOptedOut() From a1786a452f54987b06f0cb0360fc94094c2eba71 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 16:42:37 -0500 Subject: [PATCH 056/101] Fix an issue with compliance popover not dismissing --- .../EEUUSCompliance/CompliancePopoverViewModel.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift index ef00df80069f..621b79190a11 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift @@ -45,13 +45,10 @@ final class CompliancePopoverViewModel: ObservableObject { let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) return (account?.userID, account?.wordPressComRestApi) } - - guard let accountID, let restAPI else { - return + if let accountID, let restAPI { + let change = AccountSettingsChange.tracksOptOut(!isAnalyticsEnabled) + AccountSettingsService(userID: accountID.intValue, api: restAPI).saveChange(change) } - - let change = AccountSettingsChange.tracksOptOut(!isAnalyticsEnabled) - AccountSettingsService(userID: accountID.intValue, api: restAPI).saveChange(change) coordinator?.dismiss() defaults.didShowCompliancePopup = true analyticsTracker.trackPrivacyChoicesBannerSaveButtonTapped(analyticsEnabled: isAnalyticsEnabled) From 195e8e8941be19feb33fe129612aa0371828dd34 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 16:46:26 -0500 Subject: [PATCH 057/101] Update release notes --- RELEASE-NOTES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index a115294c325e..c8f88ab3e1e1 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -4,6 +4,8 @@ * [*] Add prefetching to Reader streams [#23928] * [*] Fix an issue with blogging reminders prompt not being shown after publishing a new post [#23930] * [*] Fix transitions in Blogging Reminders flow, improve accessibility, add close buttons [#23931] +* [*] Fix an issue with compliance popover not dismissing for self-hosted site [#23932] +* [*] Fix dynamic type support in the compliance popover [#23932] 25.6 ----- From f91c0d749f370115daa60a902a5fa8cbee898578 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 31 Dec 2024 16:49:51 -0500 Subject: [PATCH 058/101] Remove unused CircularProgressView extensions --- ...CircularProgressView+ActivityIndicatorType.swift | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift diff --git a/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift b/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift deleted file mode 100644 index e0cff74a813d..000000000000 --- a/WordPress/Classes/Extensions/Media/CircularProgressView+ActivityIndicatorType.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit - -extension CircularProgressView { - func startAnimating() { - isHidden = false - state = .indeterminate - } - - func stopAnimating() { - isHidden = true - state = .stopped - } -} From ae33151f423476dc225ef97f7139c78860f1adf6 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:08:30 -0500 Subject: [PATCH 059/101] Remove BottomSheetViewController usage from JetpackBrandingCoordinator --- .../JetpackBrandingCoordinator.swift | 8 +-- .../Branding/Overlay/JetpackOverlayView.swift | 58 ++++++++----------- .../JetpackOverlayViewController.swift | 14 ----- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift index dd59b175b96c..1227814ac5c9 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackBrandingCoordinator.swift @@ -4,7 +4,7 @@ import WordPressUI /// A class containing convenience methods for the the Jetpack branding experience class JetpackBrandingCoordinator { - static func presentOverlay(from viewController: UIViewController, redirectAction: (() -> Void)? = nil) { + static func presentOverlay(from presentingViewController: UIViewController, redirectAction: (() -> Void)? = nil) { let action = redirectAction ?? { // Try to export WordPress data to a shared location before redirecting the user. @@ -13,9 +13,9 @@ class JetpackBrandingCoordinator { } } - let jetpackOverlayViewController = JetpackOverlayViewController(viewFactory: makeJetpackOverlayView, redirectAction: action) - let bottomSheet = BottomSheetViewController(childViewController: jetpackOverlayViewController, customHeaderSpacing: 0) - bottomSheet.show(from: viewController) + let jetpackOverlayVC = JetpackOverlayViewController(viewFactory: makeJetpackOverlayView, redirectAction: action) + jetpackOverlayVC.sheetPresentationController?.detents = [.medium()] + presentingViewController.present(jetpackOverlayVC, animated: true) } static func makeJetpackOverlayView(redirectAction: (() -> Void)? = nil) -> UIView { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift index 7a44c82bc646..5729457435c5 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayView.swift @@ -1,7 +1,8 @@ import Lottie import UIKit +import WordPressUI -class JetpackOverlayView: UIView { +final class JetpackOverlayView: UIView { private var buttonAction: (() -> Void)? @@ -38,7 +39,7 @@ class JetpackOverlayView: UIView { }() private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [animationContainerView, titleLabel, descriptionLabel, getJetpackButton]) + let stackView = UIStackView(arrangedSubviews: [animationContainerView, titleLabel, descriptionLabel, SpacerView(minHeight: 8), getJetpackButton]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.alignment = .leading @@ -85,13 +86,11 @@ class JetpackOverlayView: UIView { }() private lazy var getJetpackButton: UIButton = { - let button = UIButton() - button.backgroundColor = UIAppColor.jetpackGreen(.shade40) - button.setTitle(TextContent.buttonTitle, for: .normal) - button.titleLabel?.adjustsFontSizeToFitWidth = true + var configuration = UIButton.Configuration.primary() + configuration.title = TextContent.buttonTitle + + let button = UIButton(configuration: configuration, primaryAction: nil) button.titleLabel?.adjustsFontForContentSizeCategory = true - button.layer.cornerRadius = Metrics.tryJetpackButtonCornerRadius - button.layer.cornerCurve = .continuous return button }() @@ -137,24 +136,16 @@ class JetpackOverlayView: UIView { private func configureConstraints() { animationContainerView.pinSubviewToAllEdges(animationView) - let stackViewTrailingConstraint = stackView.trailingAnchor.constraint(equalTo: trailingAnchor, - constant: -Metrics.edgeMargins.right) - stackViewTrailingConstraint.priority = Metrics.veryHighPriority - let stackViewBottomConstraint = stackView.bottomAnchor.constraint(lessThanOrEqualTo: safeBottomAnchor, - constant: -Metrics.edgeMargins.bottom) - stackViewBottomConstraint.priority = Metrics.veryHighPriority - NSLayoutConstraint.activate([ dismissButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.dismissButtonTrailingPadding), dismissButton.topAnchor.constraint(equalTo: topAnchor, constant: Metrics.dismissButtonTopPadding), dismissButton.heightAnchor.constraint(equalToConstant: Metrics.dismissButtonSize), dismissButton.widthAnchor.constraint(equalToConstant: Metrics.dismissButtonSize), stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.edgeMargins.left), - stackViewTrailingConstraint, + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.edgeMargins.right).withPriority(999), stackView.topAnchor.constraint(equalTo: dismissButton.bottomAnchor), - stackViewBottomConstraint, + stackView.bottomAnchor.constraint(lessThanOrEqualTo: safeBottomAnchor, constant: -Metrics.edgeMargins.bottom).withPriority(999), - getJetpackButton.heightAnchor.constraint(equalToConstant: Metrics.tryJetpackButtonHeight), getJetpackButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) } @@ -174,7 +165,7 @@ private extension JetpackOverlayView { static let imageToTitleSpacing: CGFloat = 24 static let titleToDescriptionSpacing: CGFloat = 10 static let descriptionToButtonSpacing: CGFloat = 40 - static let edgeMargins = UIEdgeInsets(top: 46, left: 30, bottom: 20, right: 30) + static let edgeMargins = UIEdgeInsets(top: 46, left: 20, bottom: 10, right: 20) // dismiss button static let dismissButtonTopPadding: CGFloat = 10 // takes into account the gripper static let dismissButtonTrailingPadding: CGFloat = 20 @@ -202,24 +193,25 @@ private extension JetpackOverlayView { let font = UIFont(descriptor: fontDescriptor, size: min(fontDescriptor.pointSize, maximumFontSize)) return UIFontMetrics.default.scaledFont(for: font, maximumPointSize: maximumFontSize) } - // "Try Jetpack" button - static let tryJetpackButtonHeight: CGFloat = 44 - static let tryJetpackButtonCornerRadius: CGFloat = 6 - // constraints - static let veryHighPriority = UILayoutPriority(rawValue: 999) } enum TextContent { - static let title = NSLocalizedString("jetpack.branding.overlay.title", - value: "WordPress is better with Jetpack", - comment: "Title of the Jetpack powered overlay.") + static let title = NSLocalizedString( + "jetpack.branding.overlay.title", + value: "WordPress is better with Jetpack", + comment: "Title of the Jetpack powered overlay." + ) - static let description = NSLocalizedString("jetpack.branding.overlay.description", - value: "The new Jetpack app has Stats, Reader, Notifications, and more that make your WordPress better.", - comment: "Description of the Jetpack powered overlay.") + static let description = NSLocalizedString( + "jetpack.branding.overlay.description", + value: "The new Jetpack app has Stats, Reader, Notifications, and more that make your WordPress better.", + comment: "Description of the Jetpack powered overlay." + ) - static let buttonTitle = NSLocalizedString("jetpack.branding.overlay.button.title", - value: "Try the new Jetpack app", - comment: "Button title of the Jetpack powered overlay.") + static let buttonTitle = NSLocalizedString( + "jetpack.branding.overlay.button.title", + value: "Try the new Jetpack app", + comment: "Button title of the Jetpack powered overlay." + ) } } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift index ff9c853a6cc4..e61f94333462 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Branding/Overlay/JetpackOverlayViewController.swift @@ -37,17 +37,3 @@ class JetpackOverlayViewController: UIViewController { view.setNeedsLayout() } } - -extension JetpackOverlayViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - .intrinsicHeight - } - - var allowsUserTransition: Bool { - false - } - - var compactWidth: DrawerWidth { - .maxWidth - } -} From c97bbddbfb4bdc2593edaed52f820ff047e43ec8 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:08:47 -0500 Subject: [PATCH 060/101] Remove ottomSheetViewControllerTests --- .../BottomSheetViewControllerTests.swift | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift diff --git a/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift b/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift deleted file mode 100644 index 3cfc8457fc1f..000000000000 --- a/Modules/Tests/WordPressUITests/BottomSheet/BottomSheetViewControllerTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -import XCTest - -@testable import WordPressUI - -class BottomSheetViewControllerTests: XCTestCase { - - /// - Add the given ViewController as a child View Controller - /// - func testAddTheGivenViewControllerAsAChildViewController() { - let viewController = BottomSheetPresentableViewController() - let bottomSheet = BottomSheetViewController(childViewController: viewController) - - bottomSheet.viewDidLoad() - - XCTAssertTrue(bottomSheet.children.contains(viewController)) - } - - /// - Add the given ViewController view to the subviews of the Bottom Sheet - /// - func testAddGivenVCViewToTheBottomSheetSubviews() { - let viewController = BottomSheetPresentableViewController() - let bottomSheet = BottomSheetViewController(childViewController: viewController) - - bottomSheet.viewDidLoad() - - XCTAssertTrue(bottomSheet.view.subviews.flatMap { $0.subviews }.contains(viewController.view)) - } -} - -private class BottomSheetPresentableViewController: UIViewController, DrawerPresentable { - var initialHeight: CGFloat = 0 -} From 927616aa20004ebe11d458314bb7c3e8840f2de5 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:10:41 -0500 Subject: [PATCH 061/101] Remove BottomSheetViewController --- .../BottomSheetViewController.swift | 276 ------------------ 1 file changed, 276 deletions(-) delete mode 100644 Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift diff --git a/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift b/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift deleted file mode 100644 index 4d5d1a000a70..000000000000 --- a/Modules/Sources/WordPressUI/BottomSheet/BottomSheetViewController.swift +++ /dev/null @@ -1,276 +0,0 @@ -import UIKit - -public class BottomSheetViewController: UIViewController { - public enum Constants { - static let gripHeight: CGFloat = 5 - static let cornerRadius: CGFloat = 8 - static let buttonSpacing: CGFloat = 8 - static let minimumWidth: CGFloat = 300 - - /// The height of the space above the bottom sheet content, including the grip view and space around it. - /// - public static let additionalContentTopMargin: CGFloat = BottomSheetViewController.Constants.gripHeight - + BottomSheetViewController.Constants.Header.spacing - + BottomSheetViewController.Constants.Stack.insets.top - - enum Header { - static let spacing: CGFloat = 16 - static let insets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18) - } - - enum Button { - static let height: CGFloat = 54 - static let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 35) - static let titleInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0) - } - - enum Stack { - static let insets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0) - } - } - - private var customHeaderSpacing: CGFloat? - - public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return childViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations - } - - /// Additional safe are insets for regular horizontal size class - public var additionalSafeAreaInsetsRegular: UIEdgeInsets = .zero - - private weak var childViewController: DrawerPresentableViewController? - - public init(childViewController: DrawerPresentableViewController, - customHeaderSpacing: CGFloat? = nil) { - self.childViewController = childViewController - self.customHeaderSpacing = customHeaderSpacing - super.init(nibName: nil, bundle: nil) - } - - /// Presents the bottom sheet given an optional anchor and arrow directions for the popover on iPad. - /// If no anchors are provided, on iPad it will present a form sheet. - /// - Parameters: - /// - presenting: the view controller that presents the bottom sheet. - /// - sourceView: optional anchor view for the popover on iPad. - /// - sourceBarButtonItem: optional anchor bar button item for the popover on iPad. If non-nil, `sourceView` and `arrowDirections` are not used. - /// - arrowDirections: optional arrow directions for the popover on iPad. - public func show(from presenting: UIViewController, - sourceView: UIView? = nil, - sourceBarButtonItem: UIBarButtonItem? = nil, - arrowDirections: UIPopoverArrowDirection = .any) { - if UIDevice.isPad() { - - // If the anchor views are not set, or the user is using a larger text option - // we'll display the content in a sheet - if (sourceBarButtonItem == nil && sourceView == nil) || - traitCollection.preferredContentSizeCategory.isAccessibilityCategory { - modalPresentationStyle = .formSheet - } else { - modalPresentationStyle = .popover - - if let sourceBarButtonItem { - popoverPresentationController?.barButtonItem = sourceBarButtonItem - } else { - popoverPresentationController?.permittedArrowDirections = arrowDirections - popoverPresentationController?.sourceView = sourceView - popoverPresentationController?.sourceRect = sourceView?.bounds ?? .zero - } - - popoverPresentationController?.delegate = self - popoverPresentationController?.backgroundColor = view.backgroundColor - } - - } else { - transitioningDelegate = self - modalPresentationStyle = .custom - } - presenting.present(self, animated: true) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private lazy var gripButton: UIButton = { - let button = GripButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget( - self, - action: #selector(buttonPressed), - for: .touchUpInside - ) - button.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for button to dismiss a bottom sheet") - return button - }() - - private var stackView: UIStackView! - - private var defaultBrackgroundColor: UIColor { - return .systemBackground - } - - @objc func buttonPressed() { - dismiss(animated: true, completion: nil) - } - - override public func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) - - view.clipsToBounds = true - view.layer.cornerRadius = Constants.cornerRadius - view.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] - view.backgroundColor = childViewController?.view.backgroundColor ?? defaultBrackgroundColor - - NSLayoutConstraint.activate([ - gripButton.heightAnchor.constraint(equalToConstant: Constants.gripHeight) - ]) - - guard let childViewController else { - return - } - - addChild(childViewController) - - stackView = UIStackView(arrangedSubviews: [ - gripButton, - childViewController.view - ]) - - stackView.setCustomSpacing(customHeaderSpacing ?? Constants.Header.spacing, after: gripButton) - - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - - refreshForTraits() - - view.addSubview(stackView) - view.pinSubviewToSafeArea(stackView, insets: Constants.Stack.insets) - - childViewController.didMove(toParent: self) - } - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - refreshForTraits() - } - - override public var preferredContentSize: CGSize { - set { - childViewController?.view.layoutIfNeeded() - - childViewController?.preferredContentSize = newValue - // Continue to make the assignment via super so preferredContentSizeDidChange is called on iPad popovers, resizing them as needed. - super.preferredContentSize = computePreferredContentSize() - } - get { - return computePreferredContentSize() - } - } - - func computePreferredContentSize() -> CGSize { - return (childViewController?.preferredContentSize ?? super.preferredContentSize) - } - - public override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - super.preferredContentSizeDidChange(forChildContentContainer: container) - // Update our preferred size in response to a child updating theres. - // While this leads to a recursive call, the sizes are the same preventing a loop. - // The assignment is needed in order for iPad popovers to correctly resize. - preferredContentSize = container.preferredContentSize - } - - override public func accessibilityPerformEscape() -> Bool { - dismiss(animated: true, completion: nil) - return true - } - - private func refreshForTraits() { - if presentingViewController?.traitCollection.horizontalSizeClass == .regular && presentingViewController?.traitCollection.verticalSizeClass != .compact { - gripButton.isHidden = true - additionalSafeAreaInsets = additionalSafeAreaInsetsRegular - } else { - gripButton.isHidden = false - additionalSafeAreaInsets = .zero - } - } - - @objc func keyboardWillShow(_ notification: NSNotification) { - guard childViewController?.presentedViewController == nil else { - return - } - - self.presentedVC?.transition(to: .expanded) - } - - @objc func keyboardWillHide(_ notification: NSNotification) { - guard childViewController?.presentedViewController == nil else { - return - } - - self.presentedVC?.transition(to: .collapsed) - } -} - -extension BottomSheetViewController: UIViewControllerTransitioningDelegate { - public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - return BottomSheetAnimationController(transitionType: .presenting) - } - - public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - - handleDismiss() - - return BottomSheetAnimationController(transitionType: .dismissing) - } - - public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - return DrawerPresentationController(presentedViewController: presented, presenting: presenting) - } -} - -// MARK: - DrawerDelegate -extension BottomSheetViewController: DrawerPresentable { - public var allowsUserTransition: Bool { - return childViewController?.allowsUserTransition ?? true - } - - public var allowsTapToDismiss: Bool { - childViewController?.allowsTapToDismiss ?? true - } - - public var allowsDragToDismiss: Bool { - childViewController?.allowsDragToDismiss ?? true - } - - public var compactWidth: DrawerWidth { - childViewController?.compactWidth ?? .percentage(0.66) - } - - public var expandedHeight: DrawerHeight { - return childViewController?.expandedHeight ?? .maxHeight - } - - public var collapsedHeight: DrawerHeight { - return childViewController?.collapsedHeight ?? .contentHeight(200) - } - - public var scrollableView: UIScrollView? { - return childViewController?.scrollableView - } - - public func handleDismiss() { - if let childViewController { - childViewController.handleDismiss() - } - } -} - -extension BottomSheetViewController: UIPopoverPresentationControllerDelegate { - public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - handleDismiss() - } -} From 2aa3bff42e75c997e72b5453e3ba3785c44c5acc Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:13:23 -0500 Subject: [PATCH 062/101] Remove DrawerPresentationController --- .../DrawerPresentationController.swift | 656 ------------------ .../Post/PostTagPickerViewController.swift | 20 - ...blishingSocialAccountsViewController.swift | 22 - 3 files changed, 698 deletions(-) delete mode 100644 Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift diff --git a/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift b/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift deleted file mode 100644 index 00af0a747449..000000000000 --- a/Modules/Sources/WordPressUI/BottomSheet/DrawerPresentationController.swift +++ /dev/null @@ -1,656 +0,0 @@ -import UIKit - -public enum DrawerPosition { - case expanded - case collapsed - case closed - case hidden -} - -public enum DrawerHeight { - // The maximum height for the screen - case maxHeight - - // Height is based on the specified margin from the top of the screen - case topMargin(CGFloat) - - // Height will be equal to the the content height value. A height of 0 will use the calculated height. - case contentHeight(CGFloat) - - // Height in the hidden state will be equal the screens height - case hidden - - // Calculate the intrisinc content based on the View Controller - case intrinsicHeight -} - -public enum DrawerWidth { - // Fills the whole screen width - case maxWidth - - // When in compact mode, fills a percentage of the screen - case percentage(CGFloat) - - // Width will be equal to the the content height value - case contentWidth(CGFloat) -} - -public protocol DrawerPresentable: AnyObject { - /// The height of the drawer when it's in the expanded position - var expandedHeight: DrawerHeight { get } - - /// The height of the drawer when it's in the collapsed position - var collapsedHeight: DrawerHeight { get } - - /// The width of the Drawer in compact screen - var compactWidth: DrawerWidth { get } - - /// Whether or not the user is allowed to swipe to switch between the expanded and collapsed position - var allowsUserTransition: Bool { get } - - /// Whether or not the user is allowed to drag to dismiss the drawer - var allowsDragToDismiss: Bool { get } - - /// Whether or not the user is allowed to tap outside the view to dismiss the drawer - var allowsTapToDismiss: Bool { get } - - /// A scroll view that should have its insets adjusted when the drawer is expanded/collapsed - var scrollableView: UIScrollView? { get } - - func handleDismiss() -} - -private enum Constants { - static let transitionDuration: TimeInterval = 0.5 - - static let flickVelocity: CGFloat = 300 - static let bounceAmount: CGFloat = 0.01 - - enum Defaults { - static let expandedHeight: DrawerHeight = .topMargin(20) - static let collapsedHeight: DrawerHeight = .contentHeight(0) - static let compactWidth: DrawerWidth = .percentage(0.66) - - static let allowsUserTransition: Bool = true - static let allowsTapToDismiss: Bool = true - static let allowsDragToDismiss: Bool = true - } -} - -public typealias DrawerPresentableViewController = DrawerPresentable & UIViewController - -public extension DrawerPresentable where Self: UIViewController { - // Default values - var allowsUserTransition: Bool { - return Constants.Defaults.allowsUserTransition - } - - var expandedHeight: DrawerHeight { - return Constants.Defaults.expandedHeight - } - - var collapsedHeight: DrawerHeight { - return Constants.Defaults.collapsedHeight - } - - var compactWidth: DrawerWidth { - return Constants.Defaults.compactWidth - } - - var scrollableView: UIScrollView? { - return nil - } - - var allowsDragToDismiss: Bool { - return Constants.Defaults.allowsDragToDismiss - } - - var allowsTapToDismiss: Bool { - return Constants.Defaults.allowsTapToDismiss - } - - // Helpers - - /// Try to determine the correct DrawerPresentationController to use - - /// Returns the `DrawerPresentationController` for a view controller if there is one - /// This tries to determine the correct one to use in the following order: - /// - The view controller - /// - The navController - /// - The navController parentViewController - /// - The views parentViewController - var presentedVC: DrawerPresentationController? { - let presentationController = self.presentationController as? DrawerPresentationController - let navigationPresentationController = navigationController?.presentationController as? DrawerPresentationController - let navParentPresetationController = navigationController?.parent?.presentationController as? DrawerPresentationController - let parentPresentationController = parent?.presentationController as? DrawerPresentationController - - return presentationController ?? navigationPresentationController ?? navParentPresetationController ?? parentPresentationController - } - - func handleDismiss() { } -} - -public class DrawerPresentationController: FancyAlertPresentationController { - override public var frameOfPresentedViewInContainerView: CGRect { - guard let containerView = self.containerView else { - return .zero - } - - var frame = containerView.frame - let y = collapsedYPosition - var width: CGFloat = containerView.bounds.width - (containerView.safeAreaInsets.left + containerView.safeAreaInsets.right) - - frame.origin.y = y - - /// If we're in a compact vertical size class, constrain the width a bit more so it doesn't get overly wide. - if let widthForCompactSizeClass = presentableViewController?.compactWidth, - traitCollection.verticalSizeClass == .compact { - - switch widthForCompactSizeClass { - case .percentage(let percentage): - width = width * percentage - case .contentWidth(let givenWidth): - width = givenWidth - case .maxWidth: - break - } - } - frame.size.width = width - - /// If we constrain the width, this centers the view by applying the appropriate insets based on width - frame.origin.x = ((containerView.bounds.width - width) / 2) - - return frame - } - - override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - coordinator.animate(alongsideTransition: { _ in - self.presentedView?.frame = self.frameOfPresentedViewInContainerView - self.transition(to: self.currentPosition) - }, completion: nil) - super.viewWillTransition(to: size, with: coordinator) - } - - /// Returns the current position of the drawer - public var currentPosition: DrawerPosition = .collapsed - - /// Returns the Y position of the drawer - public var yPosition: CGFloat? { - return presentedView?.frame.origin.y - } - - /// Animates between the drawer positions - /// - Parameter position: The position to animate to - public func transition(to position: DrawerPosition) { - currentPosition = position - - if position == .closed { - dismiss() - return - } - - var margin: CGFloat = 0 - - switch position { - case .expanded: - margin = expandedYPosition - - case .collapsed: - margin = collapsedYPosition - - case .hidden: - margin = hiddenYPosition - - default: - margin = 0 - } - - setTopMargin(margin) - } - - @objc func dismiss() { - presentedViewController.dismiss(animated: true, completion: nil) - } - - public override func presentationTransitionWillBegin() { - super.presentationTransitionWillBegin() - - configureScrollViewInsets() - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - transition(to: currentPosition) - } - - public override func presentationTransitionDidEnd(_ completed: Bool) { - super.presentationTransitionDidEnd(completed) - - configureScrollViewInsets() - } - - // MARK: - Internal Positions - // Helpers to calculate the Y positions for the drawer positions - - private var closedPosition: CGFloat { - guard let presentedView = self.presentedView else { - return 0 - } - - return presentedView.bounds.height - } - - private var collapsedYPosition: CGFloat { - let height = presentableViewController?.collapsedHeight ?? Constants.Defaults.collapsedHeight - - return topMargin(with: height) - } - - private var expandedYPosition: CGFloat { - let height = presentableViewController?.expandedHeight ?? Constants.Defaults.expandedHeight - - return topMargin(with: height) - } - - private var hiddenYPosition: CGFloat { - return topMargin(with: .hidden) - } - - /// Calculates the Y position for the view based on a DrawerHeight enum - /// - Parameter drawerHeight: The drawer height to calculate - private func topMargin(with drawerHeight: DrawerHeight) -> CGFloat { - var topMargin: CGFloat - - switch drawerHeight { - case .contentHeight(let height): - topMargin = calculatedTopMargin(for: height) - - case .topMargin(let margin): - topMargin = safeAreaInsets.top + margin - - case .maxHeight: - topMargin = safeAreaInsets.top - - case .intrinsicHeight: - // Force a layout to make sure we get the correct size from the views - presentedViewController.view.layoutIfNeeded() - - let height = presentedViewController.preferredContentSize.height - topMargin = calculatedTopMargin(for: height) - - case .hidden: - topMargin = UIScreen.main.bounds.height - } - - return topMargin - } - - // MARK: - Gestures - private lazy var tapGestureRecognizer: UITapGestureRecognizer = { - let gesture = UITapGestureRecognizer(target: self, action: #selector(self.dismiss(_:))) - gesture.delegate = self - return gesture - }() - - private lazy var panGestureRecognizer: UIPanGestureRecognizer = { - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.pan(_:))) - panGesture.delegate = self - return panGesture - }() - - override public func containerViewWillLayoutSubviews() { - super.containerViewWillLayoutSubviews() - - addGestures() - observe(scrollView: presentableViewController?.scrollableView) - } - - /// Represents whether the view is animating to a new position - private var isPresentedViewAnimating = false - - /// Whether or not the presented view is anchored to the top of the screen - private var isPresentedViewAnchored: Bool { - if !isPresentedViewAnimating - && (presentedView?.frame.origin.y.rounded() ?? 0) <= expandedYPosition.rounded() { - return true - } - - return false - } - - private var dragStartPoint: CGPoint? - - /// Stores the current `contentOffset.y` for `presentableViewController.scrollableView` - /// See `haltScrolling` and `trackScrolling` for more information. - private var scrollViewYOffset: CGFloat = 0.0 - - /// An observer of the content offset for `presentableViewController.scrollableView` - private var scrollObserver: NSKeyValueObservation? - - deinit { - scrollObserver?.invalidate() - } -} - -// MARK: - Dragging -private extension DrawerPresentationController { - - private func addGestures() { - guard - let presentedView = self.presentedView, - let containerView = self.containerView - else { return } - - presentedView.addGestureRecognizer(panGestureRecognizer) - containerView.addGestureRecognizer(tapGestureRecognizer) - } - - /// Dismiss action for the tap gesture - /// Will prevent dismissal if the `allowsTapToDismiss` is false - /// - Parameter gesture: The tap gesture - @objc func dismiss(_ gesture: UIPanGestureRecognizer) { - let canDismiss = presentableViewController?.allowsTapToDismiss ?? Constants.Defaults.allowsTapToDismiss - - guard canDismiss else { - return - } - - dismiss() - } - - @objc func pan(_ gesture: UIPanGestureRecognizer) { - guard let presentedView = self.presentedView else { return } - - let isScrolling = presentableViewController?.scrollableView?.isScrolling == true - - guard (presentableViewController?.scrollableView?.contentOffset.y ?? 0) <= 0 || isScrolling == false else { return } - - /// Ignore the animation once panning begins so we can immediately interact - isPresentedViewAnimating = false - - let translation = gesture.translation(in: presentedView) - let allowsUserTransition = presentableViewController?.allowsUserTransition ?? Constants.Defaults.allowsUserTransition - let allowDragToDismiss = presentableViewController?.allowsDragToDismiss ?? Constants.Defaults.allowsDragToDismiss - - switch gesture.state { - case .began: - dragStartPoint = presentedView.frame.origin - - case .changed: - let startY = dragStartPoint?.y ?? 0 - var yTranslation = translation.y - - /// Slows the deceleration rate - if isScrolling && presentedView.frame.origin.y < expandedYPosition { - yTranslation /= 2.0 - } - - if !allowsUserTransition || !allowDragToDismiss { - let maxBounce: CGFloat = (startY * Constants.bounceAmount) - - if yTranslation < 0 { - yTranslation = max(yTranslation, maxBounce * -1) - } else { - if !allowDragToDismiss { - yTranslation = min(yTranslation, maxBounce) - } - } - } - - let maxY = topMargin(with: .maxHeight) - var yPosition = startY + yTranslation - if isScrolling { - /// During scrolling, ensure yPosition doesn't extend past the expanded position - yPosition = max(yPosition, expandedYPosition) - } - - let newMargin = max(yPosition, maxY) - setTopMargin(newMargin, animated: false) - - case .ended: - /// Helper closure to prevent user transition/dismiss - let transition: (DrawerPosition) -> Void = { pos in - if allowsUserTransition || pos == .closed && allowDragToDismiss { - self.transition(to: pos) - } else { - // Reset to the original position - self.transition(to: self.currentPosition) - } - } - - let velocity = gesture.velocity(in: presentedView).y - let startY = dragStartPoint?.y ?? 0 - - let currentPosition = (startY + translation.y) - let position = closestPosition(for: currentPosition) - - // Determine how to handle flicking of the view - if (abs(velocity) - Constants.flickVelocity) > 0 { - // Flick up - if velocity < 0 { - transition(.expanded) - } else { - if position == .expanded { - transition(.collapsed) - } else { - transition(.closed) - } - } - - return - } - - transition(position) - - dragStartPoint = nil - - default: - return - } - } -} - -// MARK: - Scrolling -private extension DrawerPresentationController { - - /// Adds an observer for the scroll view's content offset. - /// Track scrolling without overriding the `scrollView` delegate - /// - Parameter scrollView: The scroll view whose content offset will be tracked. - func observe(scrollView: UIScrollView?) { - scrollObserver?.invalidate() - scrollObserver = scrollView?.observe(\.contentOffset, options: .old) { [weak self] scrollView, change in - - /// In case there are two containerViews in the same presentation - guard self?.containerView != nil - else { return } - - self?.didPan(on: scrollView, change: change) - } - } - - /// Handles scroll view content offset changes - /// - Parameters: - /// - scrollView: The scroll view whose content offset is changing. - /// - change: The change representing the old and new content offsets. - func didPan(on scrollView: UIScrollView, change: NSKeyValueObservedChange) { - - guard - !presentedViewController.isBeingDismissed, - !presentedViewController.isBeingPresented - else { return } - - if !isPresentedViewAnchored && scrollView.contentOffset.y > 0 { - - /// Halts scrolling when scrolling down from expanded or up from compact - haltScrolling(scrollView) - - } else if scrollView.isScrolling { - - if isPresentedViewAnchored { - /// Allow normal scrolling (with tracking) - trackScrolling(scrollView) - } else { - /// Halts scrolling when panning down from expanded - haltScrolling(scrollView) - } - - } else { - /// Allow normal scrolling (with tracking) - trackScrolling(scrollView) - } - } - - /// Stops scrolling behavior on `scrollView` and anchors to `scrollViewYOffset`. - /// - Parameter scrollView: The scroll view to stop and anchor anchor - private func haltScrolling(_ scrollView: UIScrollView) { - // Only halt the scrolling if we haven't halted it before - guard scrollView.showsVerticalScrollIndicator else { - return - } - - scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewYOffset), animated: false) - scrollView.showsVerticalScrollIndicator = false - } - - /// Tracks and saves the y offset of `scrollView` in `scrollViewYOffset`. - /// Used later by `haltScrolling` to adjust the scroll view to `scrollViewYOffset` to give the appearance of the sticking position. - /// - Parameter scrollView: The scroll view to track. - private func trackScrolling(_ scrollView: UIScrollView) { - scrollViewYOffset = max(scrollView.contentOffset.y, 0) - scrollView.showsVerticalScrollIndicator = true - } -} - -private extension UIScrollView { - /// A flag to determine if a scroll view is scrolling - var isScrolling: Bool { - return isDragging && !isDecelerating || isTracking - } -} - -extension DrawerPresentationController: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - - guard tapGestureRecognizer == gestureRecognizer else { return true } - - /// Shouldn't happen; should always have container & presented view when tapped - guard - let containerView, - let presentedView, - currentPosition != .hidden - else { - return false - } - - let touchPoint = touch.location(in: containerView) - let isInPresentedView = presentedView.frame.contains(touchPoint) - - /// Do not accept the touch if inside of the presented view - return (gestureRecognizer == tapGestureRecognizer) && isInPresentedView == false - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return otherGestureRecognizer.view == presentableViewController?.scrollableView - } -} - -// MARK: - Private: Helpers -private extension DrawerPresentationController { - - private func configureScrollViewInsets() { - guard - let scrollView = presentableViewController?.scrollableView, - let presentedView = self.presentedView, - let presentingView = presentingViewController.view - else { return } - - let bottom = presentingView.safeAreaInsets.bottom - let margin = presentedView.frame.origin.y + bottom - - scrollView.contentInset.bottom = margin - } - - private var presentableViewController: DrawerPresentable? { - return presentedViewController as? DrawerPresentable - } - - private func calculatedTopMargin(for height: CGFloat) -> CGFloat { - guard let containerView = self.containerView else { - return 0 - } - - let bounds = containerView.bounds - let margin = bounds.maxY - (safeAreaInsets.bottom + ((height > 0) ? height : (bounds.height * 0.5))) - - // Limit the max height - return max(margin, safeAreaInsets.top) - } - - private func setTopMargin(_ margin: CGFloat, animated: Bool = true) { - guard let presentedView = self.presentedView else { - return - } - - var frame = presentedView.frame - frame.origin.y = margin - - let animations = { - presentedView.frame = frame - - self.configureScrollViewInsets() - } - - if animated { - animate(animations) - } else { - animations() - } - } - - private var safeAreaInsets: UIEdgeInsets { - guard let rootViewController = self.rootViewController else { - return .zero - } - - return rootViewController.view.safeAreaInsets - } - - func closestPosition(for yPosition: CGFloat) -> DrawerPosition { - let positions = [closedPosition, collapsedYPosition, expandedYPosition] - let closestVal = positions.min(by: { abs(yPosition - $0) < abs(yPosition - $1) }) ?? yPosition - - var returnPosition: DrawerPosition = .closed - - if closestVal == expandedYPosition { - returnPosition = .expanded - } else if closestVal == collapsedYPosition { - returnPosition = .collapsed - } - - return returnPosition - } - - private func animate(_ animations: @escaping () -> Void) { - isPresentedViewAnimating = true - UIView.animate(withDuration: Constants.transitionDuration, - delay: 0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0, - options: [.curveEaseInOut, .allowUserInteraction], - animations: animations) { [weak self] _ in - self?.isPresentedViewAnimating = false - } - } - - private var rootViewController: UIViewController? { - guard let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication - else { return nil } - - return application.keyWindow?.rootViewController - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift index 4a7910392a37..b28e4b320d24 100644 --- a/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/PostTagPickerViewController.swift @@ -120,8 +120,6 @@ class PostTagPickerViewController: UIViewController { loadTags() tableView.contentInset.bottom += descriptionLabel.frame.height + 20 - - updateTableViewBottomInset() } override func viewWillDisappear(_ animated: Bool) { @@ -145,14 +143,6 @@ class PostTagPickerViewController: UIViewController { fileprivate func reloadTableData() { tableView.reloadData() } - - fileprivate func updateTableViewBottomInset() { - guard !UIDevice.isPad() else { - return - } - - tableView.contentInset.bottom += presentedVC?.yPosition ?? 0 - } } // MARK: - Tags Loading @@ -449,13 +439,3 @@ extension WPStyleGuide { cell.backgroundColor = .secondarySystemGroupedBackground } } - -extension PostTagPickerViewController: DrawerPresentable { - var collapsedHeight: DrawerHeight { - return .contentHeight(300) - } - - var scrollableView: UIScrollView? { - return tableView - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift index b0d233fc80ea..a0e22c05034e 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingSocialAccountsViewController.swift @@ -114,17 +114,6 @@ class PrepublishingSocialAccountsViewController: UITableViewController { tableView.tableHeaderView = UIView(frame: .init(x: 0, y: 0, width: 0, height: Constants.tableTopPadding)) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // when the vertical size class changes, ensure that we are displaying the max drawer height on compact size - // or revert to collapsed mode otherwise. - if let previousVerticalSizeClass = previousTraitCollection?.verticalSizeClass, - previousVerticalSizeClass != traitCollection.verticalSizeClass { - presentedVC?.transition(to: traitCollection.verticalSizeClass == .compact ? .expanded : .collapsed) - } - } - deinit { // only call the delegate method if the user has made some changes. if hasChanges { @@ -383,17 +372,6 @@ private extension PrepublishingSocialAccountsViewController { } -extension PrepublishingSocialAccountsViewController: DrawerPresentable { - - var collapsedHeight: DrawerHeight { - .intrinsicHeight - } - - var scrollableView: UIScrollView? { - tableView - } -} - private extension PrepublishingAutoSharingModel { var enabledConnectionsCount: Int { services.flatMap { $0.connections }.filter { $0.enabled }.count From 8b886a33b3bad903aa293abc0eba229251032680 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 11:26:40 -0500 Subject: [PATCH 063/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index c8f88ab3e1e1..99da70923bec 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ * [*] Fix transitions in Blogging Reminders flow, improve accessibility, add close buttons [#23931] * [*] Fix an issue with compliance popover not dismissing for self-hosted site [#23932] * [*] Fix dynamic type support in the compliance popover [#23932] +* [*] Improve transisions and interactive dismiss gestures for sheets [#23933] 25.6 ----- From 1782da16608d4f59432e6965900d8638be2d48fd Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 13:19:19 -0500 Subject: [PATCH 064/101] Add Share action to the site link on dashboard --- .../Detail Header/BlogDetailHeaderView.swift | 30 +++++++++++-------- ...SiteHeaderViewController+SiteActions.swift | 7 ++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift index ebb460f60e46..6f4422c73487 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift @@ -10,6 +10,7 @@ import SwiftUI func siteIconShouldAllowDroppedImages() -> Bool func siteTitleTapped() func siteSwitcherTapped(sourceView: UIView) + func buttonShareSiteTapped() func visitSiteTapped() } @@ -116,6 +117,7 @@ class BlogDetailHeaderView: UIView { self?.delegate?.siteIconReceivedDroppedImage(images.first) } + titleView.subtitleButton.menu = makeSiteLinkMenu() titleView.subtitleButton.addTarget(self, action: #selector(subtitleButtonTapped), for: .touchUpInside) titleView.titleButton.addTarget(self, action: #selector(titleButtonTapped), for: .touchUpInside) @@ -126,6 +128,20 @@ class BlogDetailHeaderView: UIView { setupConstraintsForChildViews() } + private func makeSiteLinkMenu() -> UIMenu { + UIMenu(children: [ + UIAction(title: Strings.visitSite, image: UIImage(systemName: "safari"), handler: { [weak self] _ in + self?.delegate?.visitSiteTapped() + }), + UIAction(title: SharedStrings.Button.copyLink, image: UIImage(systemName: "doc.on.doc"), handler: { [weak self] _ in + UIPasteboard.general.url = URL(string: (self?.blog?.displayURL ?? "") as String) + }), + UIAction(title: SharedStrings.Button.share + "…", image: UIImage(systemName: "square.and.arrow.up"), handler: { [weak self] _ in + self?.delegate?.buttonShareSiteTapped() + }) + ]) + } + // MARK: - Constraints private func setupConstraintsForChildViews() { @@ -203,16 +219,6 @@ extension BlogDetailHeaderView { configuration.contentInsets = isSidebarModeEnabled ? NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 2, trailing: 0) : NSDirectionalEdgeInsets(top: 2, leading: 0, bottom: 1, trailing: 0) configuration.titleLineBreakMode = .byTruncatingTail button.configuration = configuration - - button.menu = UIMenu(children: [ - UIAction(title: Strings.visitSite, image: UIImage(systemName: "safari"), handler: { [weak button] _ in - button?.sendActions(for: .touchUpInside) - }), - UIAction(title: Strings.actionCopyURL, image: UIImage(systemName: "doc.on.doc"), handler: { [weak button] _ in - UIPasteboard.general.url = URL(string: button?.titleLabel?.text ?? "") - }) - ]) - button.accessibilityHint = NSLocalizedString("Tap to view your site", comment: "Accessibility hint for button used to view the user's site") button.translatesAutoresizingMaskIntoConstraints = false return button @@ -354,7 +360,5 @@ private extension String { } private enum Strings { - static let visitSite = NSLocalizedString("blogHeader.actionVisitSite", value: "Visit site", comment: "Context menu button title") - static let actionCopyURL = NSLocalizedString("blogHeader.actionCopyURL", value: "Copy URL", comment: "Context menu button title") - + static let visitSite = NSLocalizedString("blogHeader.actionVisitSite", value: "Visit Site", comment: "Context menu button title") } diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteActions.swift b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteActions.swift index e5403159061a..0f8966bb6ffa 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteActions.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteActions.swift @@ -22,7 +22,7 @@ extension HomeSiteHeaderViewController { private func makePrimarySection() -> UIMenu { let menuItems = [ - MenuItem.visitSite({ [weak self] in self?.visitSiteTapped() }), + MenuItem.visitSite { [weak self] in self?.visitSiteTapped() }, MenuItem.shareSite { [weak self] in self?.buttonShareSiteTapped() }, ] return UIMenu(options: .displayInline, children: menuItems.map { $0.toAction }) @@ -54,7 +54,7 @@ extension HomeSiteHeaderViewController { // MARK: - Actions - private func buttonShareSiteTapped() { + func buttonShareSiteTapped() { guard let urlString = blog.homeURL as String?, let url = URL(string: urlString) else { assertionFailure("Site has no URL") @@ -108,7 +108,7 @@ private enum MenuItem { var title: String { switch self { case .visitSite: return Strings.visitSite - case .shareSite: return Strings.shareSite + case .shareSite: return SharedStrings.Button.share + "…" case .siteTitle: return Strings.siteTitle case .personalizeHome: return Strings.personalizeHome } @@ -136,7 +136,6 @@ private enum MenuItem { private enum Strings { static let visitSite = NSLocalizedString("mySite.siteActions.visitSite", value: "Visit site", comment: "Menu title for the visit site option") - static let shareSite = NSLocalizedString("mySite.siteActions.shareSite", value: "Share site", comment: "Menu title for the share site option") static let siteTitle = NSLocalizedString("mySite.siteActions.siteTitle", value: "Change site title", comment: "Menu title for the change site title option") static let siteIcon = NSLocalizedString("mySite.siteActions.siteIcon", value: "Change site icon", comment: "Menu title for the change site icon option") static let personalizeHome = NSLocalizedString("mySite.siteActions.personalizeHome", value: "Personalize home", comment: "Menu title for the personalize home option") From 92d446fbc3426764edfa6381ca6f3096548e726b Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 13:23:12 -0500 Subject: [PATCH 065/101] Remove duplicated Share actions --- .../Utility/WebViewController/WebKitViewController.swift | 2 +- .../Cards/Prompts/DashboardPromptsCardCell.swift | 3 +-- .../ViewRelated/Blog/Site Monitoring/PHPLogsView.swift | 4 ++-- .../Site Monitoring/SiteMonitoringEntryDetailsView.swift | 2 +- .../Blog/Site Monitoring/WebServerLogsView.swift | 4 ++-- .../ViewRelated/Media/MediaItemViewController.swift | 2 +- .../Media/SiteMedia/SiteMediaViewController.swift | 3 +-- .../NotificationTableViewCell.swift | 8 +------- .../Post/Views/AbstractPostHelper+Actions.swift | 3 +-- .../ViewRelated/Post/Views/AbstractPostMenuHelper.swift | 3 +-- .../Reader/Comments/ReaderCommentsViewController.swift | 2 +- .../Controllers/ReaderPostActions/ReaderPostMenu.swift | 6 ++---- .../Controllers/ReaderStreamViewController+Sharing.swift | 2 +- .../Reader/Detail/ReaderDetailViewController.swift | 8 ++------ .../System/Action Sheet/BloggingPromptsHeaderView.swift | 3 +-- 15 files changed, 19 insertions(+), 36 deletions(-) diff --git a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift index 8474a358a790..ef059aa7c96e 100644 --- a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift +++ b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift @@ -59,7 +59,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { style: .plain, target: self, action: #selector(share)) - button.title = NSLocalizedString("Share", comment: "Button label to share a web page") + button.title = NSLocalizedString(SharedStrings.Button.share, comment: "Button label to share a web page") return button }() @objc lazy var safariButton: UIBarButtonItem = { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index a744376c28e6..e7f06e08bf67 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -271,7 +271,7 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable { private lazy var shareButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(Strings.shareButtonTitle, for: .normal) + button.setTitle(SharedStrings.Button.share, for: .normal) button.setTitleColor(WPStyleGuide.BloggingPrompts.buttonTitleColor, for: .normal) button.titleLabel?.font = WPStyleGuide.BloggingPrompts.buttonTitleFont button.titleLabel?.adjustsFontForContentSizeCategory = true @@ -552,7 +552,6 @@ private extension DashboardPromptsCardCell { static let cardFrameTitle = NSLocalizedString("Prompts", comment: "Title label for the Prompts card in My Sites tab.") static let answerButtonTitle = NSLocalizedString("Answer Prompt", comment: "Title for a call-to-action button on the prompts card.") static let answeredLabelTitle = NSLocalizedString("✓ Answered", comment: "Title label that indicates the prompt has been answered.") - static let shareButtonTitle = NSLocalizedString("Share", comment: "Title for a button that allows the user to share their answer to the prompt.") static let answerInfoSingularFormat = NSLocalizedString("%1$d answer", comment: "Singular format string for displaying the number of users " + "that answered the blogging prompt.") static let answerInfoPluralFormat = NSLocalizedString("%1$d answers", comment: "Plural format string for displaying the number of users " diff --git a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/PHPLogsView.swift b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/PHPLogsView.swift index 4111ecd1b4d3..01336a5885aa 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/PHPLogsView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/PHPLogsView.swift @@ -110,13 +110,13 @@ struct PHPLogsView: View { PHPLogsEntryRowView(entry: entry) .swipeActions(edge: .trailing) { ShareLink(item: attributedDescription.string) { - Label("Share", systemImage: "square.and.arrow.up") + Label(SharedStrings.Button.share, systemImage: "square.and.arrow.up") } .tint(Color.blue) } .contextMenu { ShareLink(item: attributedDescription.string) { - Label("Share", systemImage: "square.and.arrow.up") + Label(SharedStrings.Button.share, systemImage: "square.and.arrow.up") } } preview: { Text(AttributedString(attributedDescription)) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/SiteMonitoringEntryDetailsView.swift b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/SiteMonitoringEntryDetailsView.swift index e359a9f758c5..b1ce914441a9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/SiteMonitoringEntryDetailsView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/SiteMonitoringEntryDetailsView.swift @@ -9,7 +9,7 @@ struct SiteMonitoringEntryDetailsView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ShareLink(item: text.string) { - Label("Share", systemImage: "square.and.arrow.up") + Label(SharedStrings.Button.share, systemImage: "square.and.arrow.up") } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/WebServerLogsView.swift b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/WebServerLogsView.swift index 65393a47c543..3370310de483 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Monitoring/WebServerLogsView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Monitoring/WebServerLogsView.swift @@ -123,13 +123,13 @@ struct WebServerLogsView: View { WebServerLogsRowView(entry: entry, width: width) .swipeActions(edge: .trailing) { ShareLink(item: attributedDescription.string) { - Label("Share", systemImage: "square.and.arrow.up") + Label(SharedStrings.Button.share, systemImage: "square.and.arrow.up") } .tint(Color.blue) } .contextMenu { ShareLink(item: attributedDescription.string) { - Label("Share", systemImage: "square.and.arrow.up") + Label(SharedStrings.Button.share, systemImage: "square.and.arrow.up") } } preview: { Text(AttributedString(attributedDescription)) diff --git a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift index 79cff2797f91..59f28acbb564 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift @@ -150,7 +150,7 @@ final class MediaItemViewController: UITableViewController { style: .plain, target: self, action: #selector(shareTapped)) - shareItem.accessibilityLabel = NSLocalizedString("Share", comment: "Accessibility label for share buttons in nav bars") + shareItem.accessibilityLabel = SharedStrings.Button.share let trashItem = UIBarButtonItem(image: UIImage(systemName: "trash"), style: .plain, diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift index d6dab88f2dc8..611e566097d2 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaViewController.swift @@ -285,7 +285,7 @@ final class SiteMediaViewController: UIViewController, SiteMediaCollectionViewCo func siteMediaViewController(_ viewController: SiteMediaCollectionViewController, contextMenuFor media: Media, sourceView: UIView) -> UIMenu? { var actions: [UIAction] = [] - actions.append(UIAction(title: Strings.buttonShare, image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in + actions.append(UIAction(title: SharedStrings.Button.share, image: UIImage(systemName: "square.and.arrow.up")) { [weak self] _ in self?.shareSelectedMedia([media], sourceView: sourceView) }) if blog.supports(.mediaDeletion) { @@ -314,7 +314,6 @@ private enum Strings { static let deletionSuccessMessage = NSLocalizedString("mediaLibrary.deletionSuccessMessage", value: "Deleted!", comment: "Text displayed in HUD after successfully deleting a media item") static let deletionFailureMessage = NSLocalizedString("mediaLibrary.deletionFailureMessage", value: "Unable to delete all media items.", comment: "Text displayed in HUD if there was an error attempting to delete a group of media items.") static let sharingFailureMessage = NSLocalizedString("mediaLibrary.sharingFailureMessage", value: "Unable to share the selected items.", comment: "Text displayed in HUD if there was an error attempting to share a group of media items.") - static let buttonShare = NSLocalizedString("mediaLibrary.buttonShare", value: "Share", comment: "Context menu button") static let buttonDelete = NSLocalizedString("mediaLibrary.buttonDelete", value: "Delete", comment: "Context menu button") static let aspectRatioGrid = NSLocalizedString("mediaLibrary.aspectRatioGrid", value: "Aspect Ratio Grid", comment: "Button name in the more menu") static let squareGrid = NSLocalizedString("mediaLibrary.squareGrid", value: "Square Grid", comment: "Button name in the more menu") diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift index 5cdc7c8ddcf8..4eb00960487e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift @@ -66,7 +66,7 @@ final class NotificationTableViewCell: HostingTableViewCell UIBarButtonItem? { let button = barButtonItem(with: .gridicon(.shareiOS), action: #selector(didTapShareButton(_:))) - button.accessibilityLabel = Strings.shareButtonAccessibilityLabel + button.accessibilityLabel = SharedStrings.Button.share button.isEnabled = enabled return button @@ -1186,11 +1186,7 @@ extension ReaderDetailViewController { value: "Open in Safari", comment: "Spoken accessibility label" ) - static let shareButtonAccessibilityLabel = NSLocalizedString( - "readerDetail.shareButton.accessibilityLabel", - value: "Share", - comment: "Spoken accessibility label" - ) + static let moreButtonAccessibilityLabel = NSLocalizedString( "readerDetail.moreButton.accessibilityLabel", value: "More", diff --git a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift index 74c8c66b7978..3b8858a80a65 100644 --- a/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift +++ b/WordPress/Classes/ViewRelated/System/Action Sheet/BloggingPromptsHeaderView.swift @@ -61,7 +61,7 @@ private extension BloggingPromptsHeaderView { infoButton.accessibilityLabel = Strings.infoButtonAccessibilityLabel answerPromptButton.setTitle(Strings.answerButtonTitle, for: .normal) answeredLabel.text = Strings.answeredLabelTitle - shareButton.titleLabel?.text = Strings.shareButtonTitle + shareButton.titleLabel?.text = SharedStrings.Button.share } func configureStyles() { @@ -145,7 +145,6 @@ private extension BloggingPromptsHeaderView { static let title = NSLocalizedString("Prompts", comment: "Title label for blogging prompts in the create new bottom action sheet.") static let answerButtonTitle = NSLocalizedString("Answer Prompt", comment: "Title for a call-to-action button in the create new bottom action sheet.") static let answeredLabelTitle = NSLocalizedString("✓ Answered", comment: "Title label that indicates the prompt has been answered.") - static let shareButtonTitle = NSLocalizedString("Share", comment: "Title for a button that allows the user to share their answer to the prompt.") static let infoButtonAccessibilityLabel = NSLocalizedString("Learn more about prompts", comment: "Accessibility label for the blogging prompts info button on the prompts header view.") } From 892178342c7abe406a9425bc473dd04077e6a625 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 13:33:23 -0500 Subject: [PATCH 066/101] Remove duplicated Strings.ok --- .../Classes/Services/PostCoordinator.swift | 15 ++---- .../Classes/Users/Views/UserDetailsView.swift | 9 +--- .../SubmitFeedbackViewController.swift | 6 +-- .../PushAuthenticationManager.swift | 13 +++-- WordPress/Classes/Utility/ZendeskUtils.swift | 10 ++-- .../AztecPostViewController.swift | 2 +- .../Blog/Sharing/KeyringAccountHelper.swift | 2 +- .../SharingButtonsViewController.swift | 2 +- .../DeleteSiteViewController.swift | 53 +++++++++---------- ...ettingsViewController+SiteManagement.swift | 4 +- .../SiteTagsViewController.swift | 2 +- .../StartOverViewController.swift | 7 +-- .../RegisterDomainDetailsViewController.swift | 6 +-- ...tpackScanThreatDetailsViewController.swift | 10 ++-- .../Media/MediaPicker/MediaPickerMenu.swift | 3 +- .../NotificationsViewController.swift | 13 +---- .../InvitePersonViewController.swift | 3 +- .../ViewRelated/Post/PostEditor+Publish.swift | 3 +- .../PostSettingsViewController+Swift.swift | 2 +- .../ReaderStreamViewController.swift | 6 +-- .../Manage/ReaderTagsTableViewModel.swift | 4 +- .../FancyAlerts+VerificationPrompt.swift | 9 ++-- .../Themes/ThemeBrowserViewController.swift | 6 +-- .../Voice/VoiceToContentView.swift | 3 +- 24 files changed, 72 insertions(+), 121 deletions(-) diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 00953e877eb0..61d633c4abde 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -18,7 +18,7 @@ class PostCoordinator: NSObject { case maximumRetryTimeIntervalReached var errorDescription: String? { - Strings.genericErrorTitle + SharedStrings.Error.generic } var errorUserInfo: [String: Any] { @@ -177,20 +177,20 @@ class PostCoordinator: NSObject { wpAssertionFailure("Failed to show an error alert") return } - let alert = UIAlertController(title: Strings.genericErrorTitle, message: error.localizedDescription, preferredStyle: .alert) + let alert = UIAlertController(title: SharedStrings.Error.generic, message: error.localizedDescription, preferredStyle: .alert) if let error = error as? PostRepository.PostSaveError { switch error { case .conflict(let latest): - alert.addDefaultActionWithTitle(Strings.buttonOK) { [weak self] _ in + alert.addDefaultActionWithTitle(SharedStrings.Button.ok) { [weak self] _ in self?.showResolveConflictView(post: post, remoteRevision: latest, source: .editor) } case .deleted: - alert.addDefaultActionWithTitle(Strings.buttonOK) { [weak self] _ in + alert.addDefaultActionWithTitle(SharedStrings.Button.ok) { [weak self] _ in self?.handlePermanentlyDeleted(post) } } } else { - alert.addDefaultActionWithTitle(Strings.buttonOK, handler: nil) + alert.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) } topViewController.present(alert, animated: true) } @@ -945,8 +945,3 @@ private extension NSManagedObjectID { .trimmingCharacters(in: CharacterSet(charactersIn: "/>")) } } - -private enum Strings { - static let genericErrorTitle = NSLocalizedString("postNotice.errorTitle", value: "An error occured", comment: "A generic error message title") - static let buttonOK = NSLocalizedString("postNotice.ok", value: "OK", comment: "Button OK") -} diff --git a/WordPress/Classes/Users/Views/UserDetailsView.swift b/WordPress/Classes/Users/Views/UserDetailsView.swift index a1bc81e2113e..2785de5fd482 100644 --- a/WordPress/Classes/Users/Views/UserDetailsView.swift +++ b/WordPress/Classes/Users/Views/UserDetailsView.swift @@ -240,13 +240,6 @@ struct UserDetailsView: View { value: "There was an error deleting the user.", comment: "The message in the alert that appears when deleting a user" ) - - static let deleteUserErrorAlertOkButton = NSLocalizedString( - "userDetails.alert.deleteUserErrorAlertOkButton", - value: "OK", - comment: "The title of the OK button in the alert that appears when deleting a user" - ) - } } @@ -293,7 +286,7 @@ private extension View { isPresented: view.$presentDeleteUserError, presenting: view.deleteUserViewModel.error, actions: { _ in - Button(Strings.deleteUserErrorAlertOkButton) { + Button(SharedStrings.Button.ok) { view.presentDeleteUserError = false } }, diff --git a/WordPress/Classes/Utility/In-App Feedback/SubmitFeedbackViewController.swift b/WordPress/Classes/Utility/In-App Feedback/SubmitFeedbackViewController.swift index 6eccce8e7a2e..0129e0cf7c38 100644 --- a/WordPress/Classes/Utility/In-App Feedback/SubmitFeedbackViewController.swift +++ b/WordPress/Classes/Utility/In-App Feedback/SubmitFeedbackViewController.swift @@ -56,7 +56,7 @@ private struct SubmitFeedbackView: View { .listStyle(.plain) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button(Strings.cancel) { + Button(SharedStrings.Button.cancel) { if isInputEmpty { dismiss() } else { @@ -87,7 +87,7 @@ private struct SubmitFeedbackView: View { } } .alert(Strings.attachmentsStillUploadingAlertTitle, isPresented: $isShowingAttachmentsUploadingAlert) { - Button(Strings.ok) {} + Button(SharedStrings.Button.ok) {} } .onChange(of: isInputEmpty) { presentingViewController?.isModalInPresentation = !$0 @@ -179,8 +179,6 @@ private struct SubmitFeedbackView: View { } private enum Strings { - static let ok = NSLocalizedString("submit.feedback.buttonOK", value: "OK", comment: "The button title for the Cancel button in the In-App Feedback screen") - static let cancel = NSLocalizedString("submit.feedback.buttonCancel", value: "Cancel", comment: "The button title for the Cancel button in the In-App Feedback screen") static let submit = NSLocalizedString("submit.feedback.submit.button", value: "Submit", comment: "The button title for the Submit button in the In-App Feedback screen") static let title = NSLocalizedString("submit.feedback.title", value: "Feedback", comment: "The title for the the In-App Feedback screen") static let details = NSLocalizedString("submit.feedback.detailsPlaceholder", value: "Details", comment: "The section title and or placeholder") diff --git a/WordPress/Classes/Utility/Notifications/PushAuthenticationManager.swift b/WordPress/Classes/Utility/Notifications/PushAuthenticationManager.swift index 8c03452cf608..f31f09ae2245 100644 --- a/WordPress/Classes/Utility/Notifications/PushAuthenticationManager.swift +++ b/WordPress/Classes/Utility/Notifications/PushAuthenticationManager.swift @@ -128,10 +128,9 @@ private extension PushAuthenticationManager { /// Displays an AlertView indicating that a Login Request has expired. /// func showLoginExpiredAlert() { - let title = NSLocalizedString("Login Request Expired", comment: "Login Request Expired") - let message = NSLocalizedString("The login request has expired. Log in to WordPress.com to try again.", - comment: "WordPress.com Push Authentication Expired message") - let acceptButtonTitle = NSLocalizedString("OK", comment: "OK") + let title = NSLocalizedString("Login Request Expired", comment: "Login Request Expired") + let message = NSLocalizedString("The login request has expired. Log in to WordPress.com to try again.", comment: "WordPress.com Push Authentication Expired message") + let acceptButtonTitle = SharedStrings.Button.ok alertControllerProxy.show(withTitle: title, message: message, @@ -147,9 +146,9 @@ private extension PushAuthenticationManager { /// - completion: A closure that receives a parameter, indicating whether the login attempt was confirmed or not. /// func showLoginVerificationAlert(_ message: String, completion: @escaping ((_ approved: Bool) -> ())) { - let title = NSLocalizedString("Verify Log In", comment: "Push Authentication Alert Title") - let cancelButtonTitle = NSLocalizedString("Ignore", comment: "Ignore action. Verb") - let acceptButtonTitle = NSLocalizedString("Approve", comment: "Approve action. Verb") + let title = NSLocalizedString("Verify Log In", comment: "Push Authentication Alert Title") + let cancelButtonTitle = NSLocalizedString("Ignore", comment: "Ignore action. Verb") + let acceptButtonTitle = NSLocalizedString("Approve", comment: "Approve action. Verb") alertControllerProxy.show(withTitle: title, message: message, diff --git a/WordPress/Classes/Utility/ZendeskUtils.swift b/WordPress/Classes/Utility/ZendeskUtils.swift index 546a0a37cd68..2dcbbd89a9f7 100644 --- a/WordPress/Classes/Utility/ZendeskUtils.swift +++ b/WordPress/Classes/Utility/ZendeskUtils.swift @@ -1141,8 +1141,6 @@ private extension ZendeskUtils { struct LocalizedText { static let alertMessageWithName = NSLocalizedString("To continue please enter your email address and name.", comment: "Instructions for alert asking for email and name.") static let alertMessage = NSLocalizedString("Please enter your email address.", comment: "Instructions for alert asking for email.") - static let alertSubmit = NSLocalizedString("OK", comment: "Submit button on prompt for user information.") - static let alertCancel = NSLocalizedString("Cancel", comment: "Cancel prompt for user information.") static let emailPlaceholder = NSLocalizedString("Email", comment: "Email address text field placeholder") static let emailAccessibilityLabel = NSLocalizedString("Email", comment: "Accessibility label for the Email text field.") static let namePlaceholder = NSLocalizedString("Name", comment: "Name text field placeholder") @@ -1192,8 +1190,8 @@ extension ZendeskUtils { optionalIdentity: false, includesName: true, message: LocalizedText.alertMessageWithName, - submit: LocalizedText.alertSubmit, - cancel: LocalizedText.alertCancel + submit: SharedStrings.Button.ok, + cancel: SharedStrings.Button.cancel ) } @@ -1202,8 +1200,8 @@ extension ZendeskUtils { optionalIdentity: false, includesName: false, message: LocalizedText.alertMessage, - submit: LocalizedText.alertSubmit, - cancel: LocalizedText.alertCancel + submit: SharedStrings.Button.ok, + cancel: SharedStrings.Button.cancel ) } } diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 8b88ea4f9ef2..03c86785f764 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -2775,7 +2775,7 @@ extension AztecPostViewController { func displayUnableToPlayVideoAlert() { let alertController = UIAlertController(title: MediaUnableToPlayVideoAlert.title, message: MediaUnableToPlayVideoAlert.message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"), style: .`default`, handler: nil)) + alertController.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default, handler: nil)) present(alertController, animated: true) return } diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift index c549d73a78d3..a68d0bdf80b7 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift @@ -86,7 +86,7 @@ private extension KeyringAccountHelper { let alertBodyMessage = NSLocalizedString("The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages.", comment: "Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages.") let continueActionTitle = NSLocalizedString("Learn more", comment: "A button title.") - let cancelActionTitle = NSLocalizedString("OK", comment: "A button title for closing the dialog.") + let cancelActionTitle = SharedStrings.Button.ok return ValidationError(header: alertHeaderMessage, body: alertBodyMessage, diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingButtonsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingButtonsViewController.swift index e2eb28ad0cf2..833e17ee896d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingButtonsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingButtonsViewController.swift @@ -675,7 +675,7 @@ import WordPressShared message.append(error.localizedDescription) } let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) - controller.addCancelActionWithTitle(NSLocalizedString("OK", comment: "A button title."), handler: nil) + controller.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) controller.presentFromRootViewController() } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift index 5e62d654abed..92abdc23ef10 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift @@ -215,34 +215,31 @@ open class DeleteSiteViewController: UITableViewController { let trackedBlog = blog WPAppAnalytics.track(.siteSettingsDeleteSiteRequested, with: trackedBlog) let service = SiteManagementService(coreDataStack: ContextManager.sharedInstance()) - service.deleteSiteForBlog(blog, - success: { [weak self] in - WPAppAnalytics.track(.siteSettingsDeleteSiteResponseOK, with: trackedBlog) - let status = NSLocalizedString("Site deleted", comment: "Overlay message displayed when site successfully deleted") - SVProgressHUD.showDismissibleSuccess(withStatus: status) - - self?.updateNavigationStackAfterSiteDeletion() - - let context = ContextManager.shared.mainContext - let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) - if let account { - AccountService(coreDataStack: ContextManager.sharedInstance()).updateUserDetails(for: account, - success: {}, - failure: { _ in }) - } - }, - failure: { error in - DDLogError("Error deleting site: \(error.localizedDescription)") - WPAppAnalytics.track(.siteSettingsDeleteSiteResponseError, with: trackedBlog) - SVProgressHUD.dismiss() - - let errorTitle = NSLocalizedString("Delete Site Error", comment: "Title of alert when site deletion fails") - let alertController = UIAlertController(title: errorTitle, message: error.localizedDescription, preferredStyle: .alert) - - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") - alertController.addDefaultActionWithTitle(okTitle, handler: nil) - - alertController.presentFromRootViewController() + service.deleteSiteForBlog(blog, success: { [weak self] in + WPAppAnalytics.track(.siteSettingsDeleteSiteResponseOK, with: trackedBlog) + let status = NSLocalizedString("Site deleted", comment: "Overlay message displayed when site successfully deleted") + SVProgressHUD.showDismissibleSuccess(withStatus: status) + + self?.updateNavigationStackAfterSiteDeletion() + + let context = ContextManager.shared.mainContext + let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) + if let account { + AccountService(coreDataStack: ContextManager.sharedInstance()).updateUserDetails(for: account, + success: {}, + failure: { _ in }) + } + }, failure: { error in + DDLogError("Error deleting site: \(error.localizedDescription)") + WPAppAnalytics.track(.siteSettingsDeleteSiteResponseError, with: trackedBlog) + SVProgressHUD.dismiss() + + let errorTitle = NSLocalizedString("Delete Site Error", comment: "Title of alert when site deletion fails") + let alertController = UIAlertController(title: errorTitle, message: error.localizedDescription, preferredStyle: .alert) + + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) + + alertController.presentFromRootViewController() }) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift index 061cff44b7f4..bbf63ae4bdac 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift @@ -62,7 +62,7 @@ public extension SiteSettingsViewController { let errorTitle = NSLocalizedString("Export Content Error", comment: "Title of alert when export content fails") let alertController = UIAlertController(title: errorTitle, message: error.localizedDescription, preferredStyle: .alert) - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") + let okTitle = SharedStrings.Button.ok _ = alertController.addDefaultActionWithTitle(okTitle, handler: nil) alertController.presentFromRootViewController() @@ -101,7 +101,7 @@ public extension SiteSettingsViewController { let errorTitle = NSLocalizedString("Check Purchases Error", comment: "Title of alert when getting purchases fails") let alertController = UIAlertController(title: errorTitle, message: error.localizedDescription, preferredStyle: .alert) - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") + let okTitle = SharedStrings.Button.ok alertController.addDefaultActionWithTitle(okTitle, handler: nil) alertController.presentFromRootViewController() diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift index b65590deea58..e0dd2ecc49d8 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteTagsViewController.swift @@ -314,7 +314,7 @@ extension SiteTagsViewController { comment: "Message of the alert indicating that a tag with that name already exists. The placeholder is the name of the tag"), tagName) - let acceptTitle = NSLocalizedString("OK", comment: "Alert dismissal title") + let acceptTitle = SharedStrings.Button.ok let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addDefaultActionWithTitle(acceptTitle) present(alertController, animated: true) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/StartOverViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/StartOverViewController.swift index 359e182588c6..e1e089fa7861 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/StartOverViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/StartOverViewController.swift @@ -145,11 +145,8 @@ open class StartOverViewController: UITableViewController, MFMailComposeViewCont let title = String(format: NSLocalizedString("Contact us at %@", comment: "Alert title for contact us alert, placeholder for help email address, inserted at run time."), mailRecipient) let message = NSLocalizedString("\nPlease send us an email to have your content cleared out.", comment: "Message to ask the user to send us an email to clear their content.") - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("OK", - comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addCancelActionWithTitle(SharedStrings.Button.ok) alertController.presentFromRootViewController() } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift index c0c3d5b69f8d..25fc91de5f33 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewController/RegisterDomainDetailsViewController.swift @@ -117,16 +117,12 @@ class RegisterDomainDetailsViewController: UITableViewController { } private func showAlert(title: String? = nil, message: String) { - let alertCancel = NSLocalizedString( - "OK", - comment: "Title of an OK button. Pressing the button acknowledges and dismisses a prompt." - ) let alertController = UIAlertController( title: title, message: message, preferredStyle: .alert ) - alertController.addCancelActionWithTitle(alertCancel, handler: nil) + alertController.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) present(alertController, animated: true, completion: nil) } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanThreatDetailsViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanThreatDetailsViewController.swift index 543ee7952759..5b694b1cf27f 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanThreatDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanThreatDetailsViewController.swift @@ -82,8 +82,8 @@ class JetpackScanThreatDetailsViewController: UIViewController { message: viewModel.fixDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: Strings.cancel, style: .cancel)) - alert.addAction(UIAlertAction(title: Strings.ok, style: .default, handler: { [weak self] _ in + alert.addAction(UIAlertAction(title: SharedStrings.Button.cancel, style: .cancel)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default, handler: { [weak self] _ in guard let self else { return } @@ -105,8 +105,8 @@ class JetpackScanThreatDetailsViewController: UIViewController { message: String(format: viewModel.ignoreActionMessage, blogName), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: Strings.cancel, style: .cancel)) - alert.addAction(UIAlertAction(title: Strings.ok, style: .default, handler: { [weak self] _ in + alert.addAction(UIAlertAction(title: SharedStrings.Button.cancel, style: .cancel)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default, handler: { [weak self] _ in guard let self else { return } @@ -274,8 +274,6 @@ extension JetpackScanThreatDetailsViewController { private enum Strings { static let title = NSLocalizedString("Threat details", comment: "Title for the Jetpack Scan Threat Details screen") - static let ok = NSLocalizedString("OK", comment: "OK button for alert") - static let cancel = NSLocalizedString("Cancel", comment: "Cancel button for alert") static let jetpackSettingsNotice = NSLocalizedString("Unable to visit Jetpack settings for site", comment: "Message displayed when visiting the Jetpack settings page fails.") } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift index 472b38236fed..a0204f0b1beb 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift @@ -142,7 +142,7 @@ extension MediaPickerMenu { private func showAccessRestrictedAlert() { let alert = UIAlertController(title: Strings.noCameraAccessTitle, message: Strings.noCameraAccessMessage, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: Strings.buttonOK, style: .cancel)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .cancel)) alert.addAction(UIAlertAction(title: Strings.noCameraOpenSettings, style: .default) { _ in guard let url = URL(string: UIApplication.openSettingsURLString) else { return assertionFailure("Failed to create Open Settigns URL") @@ -300,5 +300,4 @@ private enum Strings { static let noCameraAccessTitle = NSLocalizedString("mediaPicker.noCameraAccessTitle", value: "Media Capture", comment: "Title for alert when access to camera is not granted") static let noCameraAccessMessage = NSLocalizedString("mediaPicker.noCameraAccessMessage", value: "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this.", comment: "Message for alert when access to camera is not granted") static let noCameraOpenSettings = NSLocalizedString("mediaPicker.openSettings", value: "Open Settings", comment: "Button that opens the Settings app") - static let buttonOK = NSLocalizedString("OK", value: "OK", comment: "OK") } diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift index a2eb3fd09fd4..ac5d2d6970be 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationsViewController.swift @@ -1107,15 +1107,6 @@ private extension NotificationsViewController { ) } - let cancelTitle = NSLocalizedString( - "Cancel", - comment: "Cancels the mark all as read action." - ) - let markAllTitle = NSLocalizedString( - "OK", - comment: "Marks all notifications as read." - ) - let alertController = UIAlertController( title: String.localizedStringWithFormat(title, filter.confirmationMessageTitle), message: nil, @@ -1123,9 +1114,9 @@ private extension NotificationsViewController { ) alertController.view.accessibilityIdentifier = "mark-all-as-read-alert" - alertController.addCancelActionWithTitle(cancelTitle) + alertController.addCancelActionWithTitle(SharedStrings.Button.cancel) - alertController.addActionWithTitle(markAllTitle, style: .default) { [weak self] _ in + alertController.addActionWithTitle(SharedStrings.Button.ok, style: .default) { [weak self] _ in self?.markAllAsRead() } diff --git a/WordPress/Classes/ViewRelated/People/Controllers/InvitePersonViewController.swift b/WordPress/Classes/ViewRelated/People/Controllers/InvitePersonViewController.swift index 4131d7a8e2c3..5cfae7c98479 100644 --- a/WordPress/Classes/ViewRelated/People/Controllers/InvitePersonViewController.swift +++ b/WordPress/Classes/ViewRelated/People/Controllers/InvitePersonViewController.swift @@ -545,10 +545,9 @@ private extension InvitePersonViewController { let message = messageMap[error] ?? messageMap[.unknownError]! let title = NSLocalizedString("Sorry!", comment: "Invite Validation Alert") - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addDefaultActionWithTitle(okTitle) + alert.addDefaultActionWithTitle(SharedStrings.Button.ok) present(alert, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift index 410d3df38e61..fc616c29c925 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift @@ -147,7 +147,7 @@ extension PublishingEditor { func displayMediaIsUploadingAlert() { let alertController = UIAlertController(title: MediaUploadingAlert.title, message: MediaUploadingAlert.message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(MediaUploadingAlert.acceptTitle) + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok) present(alertController, animated: true, completion: nil) } @@ -359,5 +359,4 @@ private enum Strings { private struct MediaUploadingAlert { static let title = NSLocalizedString("Uploading media", comment: "Title for alert when trying to save/exit a post before media upload process is complete.") static let message = NSLocalizedString("You are currently uploading media. Please wait until this completes.", comment: "This is a notification the user receives if they are trying to save a post (or exit) before the media upload process is complete.") - static let acceptTitle = NSLocalizedString("OK", comment: "Accept Action") } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 6cd78ab87e76..c8def4b258e0 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -177,7 +177,7 @@ extension PostSettingsViewController { private func showWarningPostWillBePublishedAlert() { let alert = UIAlertController(title: nil, message: Strings.warningPostWillBePublishedAlertMessage, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("postSettings.ok", value: "OK", comment: "Button OK"), style: .default)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.ok, style: .default)) present(alert, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 5b8b266697c2..612dfec0326a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -886,11 +886,10 @@ import AutomatticTracks if !canSync() { let alertTitle = NSLocalizedString("Unable to Load Posts", comment: "Title of a prompt saying the app needs an internet connection before it can load posts") let alertMessage = NSLocalizedString("Please check your internet connection and try again.", comment: "Politely asks the user to check their internet connection before trying again. ") - let cancelTitle = NSLocalizedString("OK", comment: "Title of a button that dismisses a prompt") let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) - alertController.addCancelActionWithTitle(cancelTitle, handler: nil) + alertController.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() return @@ -898,11 +897,10 @@ import AutomatticTracks if let syncHelper, syncHelper.isSyncing { let alertTitle = NSLocalizedString("Busy", comment: "Title of a prompt letting the user know that they must wait until the current aciton completes.") let alertMessage = NSLocalizedString("Please wait until the current fetch completes.", comment: "Asks the user to wait until the currently running fetch request completes.") - let cancelTitle = NSLocalizedString("OK", comment: "Title of a button that dismisses a prompt") let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) - alertController.addCancelActionWithTitle(cancelTitle, handler: nil) + alertController.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() return diff --git a/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift index d89f3c859b18..2cb097abeb7e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Manage/ReaderTagsTableViewModel.swift @@ -198,7 +198,7 @@ extension ReaderTagsTableViewModel { let title = NSLocalizedString("Could Not Follow Topic", comment: "Title of a prompt informing the user there was a probem unsubscribing from a topic in the reader.") let message = error?.localizedDescription let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addCancelActionWithTitle(NSLocalizedString("OK", comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + alert.addCancelActionWithTitle(SharedStrings.Button.ok) alert.presentFromRootViewController() }, source: "manage") } @@ -215,7 +215,7 @@ extension ReaderTagsTableViewModel { let title = NSLocalizedString("Could Not Remove Topic", comment: "Title of a prompt informing the user there was a probem unsubscribing from a topic in the reader.") let message = error?.localizedDescription let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addCancelActionWithTitle(NSLocalizedString("OK", comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + alert.addCancelActionWithTitle(SharedStrings.Button.ok) alert.presentFromRootViewController() } } diff --git a/WordPress/Classes/ViewRelated/System/Fancy Alerts/FancyAlerts+VerificationPrompt.swift b/WordPress/Classes/ViewRelated/System/Fancy Alerts/FancyAlerts+VerificationPrompt.swift index d13ca4b330bc..b287ecb264e6 100644 --- a/WordPress/Classes/ViewRelated/System/Fancy Alerts/FancyAlerts+VerificationPrompt.swift +++ b/WordPress/Classes/ViewRelated/System/Fancy Alerts/FancyAlerts+VerificationPrompt.swift @@ -36,7 +36,7 @@ extension FancyAlertViewController { }) } - let defaultButton = FancyAlertViewController.Config.ButtonConfig(Strings.ok) { controller, _ in + let defaultButton = FancyAlertViewController.Config.ButtonConfig(SharedStrings.Button.ok) { controller, _ in completion?() controller.dismiss(animated: true) } @@ -55,7 +55,7 @@ extension FancyAlertViewController { } private static func successfullySentVerificationEmailConfig() -> FancyAlertViewController.Config { - let okButton = FancyAlertViewController.Config.ButtonConfig(Strings.ok) { controller, _ in + let okButton = FancyAlertViewController.Config.ButtonConfig(SharedStrings.Button.ok) { controller, _ in controller.dismiss(animated: true) } @@ -71,7 +71,7 @@ extension FancyAlertViewController { } private static func failureSendingVerificationEmailConfig(with error: VerificationFailureError) -> FancyAlertViewController.Config { - let okButton = FancyAlertViewController.Config.ButtonConfig(Strings.ok) { controller, _ in + let okButton = FancyAlertViewController.Config.ButtonConfig(SharedStrings.Button.ok) { controller, _ in controller.dismiss(animated: true) } @@ -103,9 +103,6 @@ extension FancyAlertViewController { static let resendEmail = NSLocalizedString("Resend", comment: "Title of secondary button on alert prompting verify their accounts while attempting to publish") - static let ok = NSLocalizedString("OK", - comment: "Title of primary button on alert prompting verify their accounts while attempting to publish") - static let emailSentSuccesfully = NSLocalizedString("Verification email sent, check your inbox.", comment: "Message shown when a verification email was re-sent succesfully") diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 65ad2b50374f..7d37c1187616 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -782,7 +782,6 @@ public protocol ThemePresenter: AnyObject { let successFormat = NSLocalizedString("Thanks for choosing %@ by %@", 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") - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") self?.updateActivateButton(isLoading: false) @@ -794,14 +793,13 @@ public protocol ThemePresenter: AnyObject { handler: { [weak self] (action: UIAlertAction) in _ = self?.navigationController?.popViewController(animated: true) }) - alertController.addDefaultActionWithTitle(okTitle, handler: nil) + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() }, failure: { [weak self] (error) in DDLogError("Error activating theme \(String(describing: theme.themeId)): \(String(describing: error?.localizedDescription))") let errorTitle = NSLocalizedString("Activation Error", comment: "Title of alert when theme activation fails") - let okTitle = NSLocalizedString("OK", comment: "Alert dismissal title") self?.activityIndicator.stopAnimating() self?.activateButton?.customView = nil @@ -809,7 +807,7 @@ public protocol ThemePresenter: AnyObject { let alertController = UIAlertController(title: errorTitle, message: error?.localizedDescription, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(okTitle, handler: nil) + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() }) } diff --git a/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift b/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift index 66445dd8931c..74b5cbfa3ecd 100644 --- a/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift +++ b/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift @@ -10,7 +10,7 @@ struct VoiceToContentView: View { .onAppear(perform: viewModel.onViewAppeared) .tint(Color(uiColor: UIAppColor.brand)) .alert(viewModel.errorAlertMessage ?? "", isPresented: $viewModel.isShowingErrorAlert, actions: { - Button(Strings.ok, action: buttonCancelTapped) + Button(SharedStrings.Button.ok, action: buttonCancelTapped) }) } @@ -189,6 +189,5 @@ private enum Strings { static let retry = NSLocalizedString("postFromAudio.retry", value: "Retry", comment: "Button title") static let notEnoughRequests = NSLocalizedString("postFromAudio.notEnoughRequestsMessage", value: "You don't have enough requests available to create a post from audio.", comment: "Message for 'not eligible' state view") static let upgrade = NSLocalizedString("postFromAudio.buttonUpgrade", value: "Upgrade for more requests", comment: "Button title") - static let ok = NSLocalizedString("postFromAudio.ok", value: "OK", comment: "Button title") static let close = NSLocalizedString("postFromAudio.close", value: "Close", comment: "Button close title (only used as an accessibility identifier)") } From 57dc1070f134480623284084b64a8014b916e041 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 13:36:41 -0500 Subject: [PATCH 067/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 99da70923bec..9789b2bf087a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -7,6 +7,7 @@ * [*] Fix an issue with compliance popover not dismissing for self-hosted site [#23932] * [*] Fix dynamic type support in the compliance popover [#23932] * [*] Improve transisions and interactive dismiss gestures for sheets [#23933] +* [*] Add "Share" action to site link context menu on dashboard [#23935] 25.6 ----- From 86ee87ba5b246dbc533079dd04d1b6f1c2158b49 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 14:58:11 -0500 Subject: [PATCH 068/101] Fix layout issues in Privacy Settings --- .../WordPressUI/Extensions/UIImage+Color.swift | 13 ++++++++----- .../PrivacySettingsViewController.swift | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift b/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift index 4437aacdbe07..714fe82de028 100644 --- a/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift +++ b/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift @@ -4,13 +4,16 @@ public extension UIImage { /// Create an image of the given `size` that's made of a single `color`. /// - /// Size is in points. + /// - parameter size: Size in points. convenience init(color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0)) { - let image = UIGraphicsImageRenderer(size: size).image { rendererContext in + let image = UIGraphicsImageRenderer(size: size).image { context in color.setFill() - rendererContext.fill(CGRect(origin: .zero, size: size)) + context.fill(CGRect(origin: .zero, size: size)) + } + if let cgImage = image.cgImage { + self.init(cgImage: cgImage, scale: image.scale, orientation: .up) + } else { + self.init() } - - self.init(cgImage: image.cgImage!) // Force because there's no reason that the `cgImage` should be nil } } diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/Privacy Settings/PrivacySettingsViewController.swift b/WordPress/Classes/ViewRelated/Me/App Settings/Privacy Settings/PrivacySettingsViewController.swift index 7f295f430f68..50be801ad3d7 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/Privacy Settings/PrivacySettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/Privacy Settings/PrivacySettingsViewController.swift @@ -38,7 +38,7 @@ class PrivacySettingsViewController: UITableViewController { PaddedInfoRow.self, SwitchRow.self, PaddedLinkRow.self - ], tableView: self.tableView) + ], tableView: self.tableView) handler = ImmuTableViewHandler(takeOver: self) reloadViewModel() From 9038395251a729647fc65dbeade873fd36b1a17c Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 15:00:02 -0500 Subject: [PATCH 069/101] Add assertion --- Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift b/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift index 714fe82de028..73f507749adb 100644 --- a/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift +++ b/Modules/Sources/WordPressUI/Extensions/UIImage+Color.swift @@ -13,6 +13,7 @@ public extension UIImage { if let cgImage = image.cgImage { self.init(cgImage: cgImage, scale: image.scale, orientation: .up) } else { + assertionFailure("faield to render image with color") self.init() } } From df6f93897d24d3cc70e44a58f4ebd64251d50252 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 15:00:21 -0500 Subject: [PATCH 070/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 9789b2bf087a..6475505c9181 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -8,6 +8,7 @@ * [*] Fix dynamic type support in the compliance popover [#23932] * [*] Improve transisions and interactive dismiss gestures for sheets [#23933] * [*] Add "Share" action to site link context menu on dashboard [#23935] +* [*] Fix layout issues in Privacy Settings section of App Settings [#23936] 25.6 ----- From b9de1da6e536fc185b542d7e8abb359cb8dcb2a8 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 16:55:37 -0500 Subject: [PATCH 071/101] Rename WordPressMedia to AsyncImageKit --- Modules/Package.swift | 10 +++++----- .../AnimagedImage.swift | 0 .../FaviconService.swift | 0 .../ImageDecoder.swift | 0 .../ImageDownloader.swift | 0 .../ImagePrefetcher.swift | 0 .../ImageRequest.swift | 0 .../{WordPressMedia => AsyncImageKit}/MediaHost.swift | 0 .../MemoryCache.swift | 0 .../ImageDownloaderTests.swift | 2 +- .../MediaHostTests.swift | 0 .../Extensions/NSAttributedString+Helpers.swift | 2 +- .../Classes/Networking/MediaHost+Extensions.swift | 2 +- .../Classes/Networking/MediaRequestAuthenticator.swift | 2 +- WordPress/Classes/Services/MediaHelper.swift | 2 +- WordPress/Classes/Services/MediaImageService.swift | 2 +- WordPress/Classes/System/WordPressAppDelegate.swift | 2 +- WordPress/Classes/Utility/Media/AsyncImageView.swift | 2 +- WordPress/Classes/Utility/Media/CachedAsyncImage.swift | 2 +- .../Utility/Media/ImageDownloader+Extensions.swift | 2 +- .../Utility/Media/ImageDownloader+Gravatar.swift | 6 +++--- .../Classes/Utility/Media/ImageLoadingController.swift | 2 +- .../Classes/Utility/Media/MediaExternalExporter.swift | 2 +- .../Classes/Utility/Media/MemoryCache+Extensions.swift | 2 +- .../Utility/Media/UIImageView+ImageDownloader.swift | 2 +- .../ViewControllers/AztecPostViewController.swift | 2 +- .../Blaze Campaigns/BlazeCampaignTableViewCell.swift | 2 +- .../Blaze/Overlay/BlazePostPreviewView.swift | 2 +- .../Cards/Blaze/DashboardBlazeCampaignView.swift | 2 +- .../Blog Details/BlogDetailsViewController+Me.swift | 4 ++-- .../Blog/Site Picker/BlogList/SiteIconViewModel.swift | 2 +- .../ViewRelated/Cells/MediaItemHeaderView.swift | 2 +- .../ViewRelated/Cells/PostFeaturedImageCell.swift | 2 +- .../ContentRenderer/RichCommentContentRenderer.swift | 2 +- .../Gutenberg/AztecAttachmentDelegate.swift | 2 +- .../ViewRelated/Gutenberg/EditorMediaUtility.swift | 2 +- .../Gutenberg/GutenbergViewController.swift | 2 +- .../Gutenberg/Utils/GutenbergMediaEditorImage.swift | 2 +- .../Gravatar/GravatarQuickEditorPresenter.swift | 2 +- .../External/ExternalMediaPickerCollectionCell.swift | 2 +- .../External/ExternalMediaPickerViewController.swift | 2 +- .../Lightbox/LightboxImagePageViewController.swift | 2 +- .../ViewRelated/Media/Lightbox/LightboxItem.swift | 2 +- .../Media/Lightbox/LightboxViewController.swift | 2 +- .../SiteMedia/Views/SiteMediaCollectionCell.swift | 2 +- .../NewGutenberg/NewGutenbergViewController.swift | 2 +- .../Tools/NotificationMediaDownloader.swift | 2 +- .../Views/NoteBlockHeaderTableViewCell.swift | 2 +- .../Classes/ViewRelated/Pages/Views/PageListCell.swift | 2 +- .../Preview/RevisionPreviewTextViewManager.swift | 2 +- .../ViewRelated/Post/Views/PostCompactCell.swift | 2 +- .../Classes/ViewRelated/Post/Views/PostListCell.swift | 2 +- .../ViewRelated/Reader/Cards/ReaderCrossPostCell.swift | 2 +- .../ViewRelated/Reader/Cards/ReaderPostCell.swift | 2 +- .../Reader/Cards/ReaderPostCellViewModel.swift | 2 +- .../Controllers/ReaderStreamViewController.swift | 2 +- .../Reader/Detail/ReaderDetailCoordinator.swift | 2 +- .../Detail/Views/ReaderDetailFeaturedImageView.swift | 2 +- .../ViewRelated/Reader/Views/ReaderAvatarView.swift | 2 +- .../ViewRelated/Reader/Views/ReaderSiteIconView.swift | 2 +- .../Insights/StatsLatestPostSummaryInsightsCell.swift | 2 +- .../ViewRelated/Stats/Shared Views/StatsTotalRow.swift | 2 +- .../ViewRelated/System/WPTabBarController+MeTab.swift | 2 +- .../Views/WPRichText/WPRichContentView.swift | 2 +- .../ViewRelated/Views/WPRichText/WPRichTextImage.swift | 2 +- .../Views/DashboardCustomAnnouncementCell.swift | 2 +- WordPress/WordPressTest/MediaImageServiceTests.swift | 2 +- 67 files changed, 65 insertions(+), 65 deletions(-) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/AnimagedImage.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/FaviconService.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/ImageDecoder.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/ImageDownloader.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/ImagePrefetcher.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/ImageRequest.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/MediaHost.swift (100%) rename Modules/Sources/{WordPressMedia => AsyncImageKit}/MemoryCache.swift (100%) rename Modules/Tests/{WordPressMediaTests => AsyncImageKitTests}/ImageDownloaderTests.swift (99%) rename Modules/Tests/{WordPressMediaTests => AsyncImageKitTests}/MediaHostTests.swift (100%) diff --git a/Modules/Package.swift b/Modules/Package.swift index 36669a34ba12..513237db4e23 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]), .library(name: "DesignSystem", targets: ["DesignSystem"]), .library(name: "WordPressFlux", targets: ["WordPressFlux"]), - .library(name: "WordPressMedia", targets: ["WordPressMedia"]), + .library(name: "AsyncImageKit", targets: ["AsyncImageKit"]), .library(name: "WordPressShared", targets: ["WordPressShared"]), .library(name: "WordPressUI", targets: ["WordPressUI"]), ], @@ -59,7 +59,7 @@ let package = Package( .product(name: "XCUITestHelpers", package: "XCUITestHelpers"), ], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]), - .target(name: "WordPressMedia", dependencies: [ + .target(name: "AsyncImageKit", dependencies: [ .product(name: "Collections", package: "swift-collections"), ]), .target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), @@ -69,8 +69,8 @@ let package = Package( .testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]), .testTarget(name: "WordPressFluxTests", dependencies: ["WordPressFlux"], swiftSettings: [.swiftLanguageMode(.v5)]), - .testTarget(name: "WordPressMediaTests", dependencies: [ - .target(name: "WordPressMedia"), + .testTarget(name: "AsyncImageKitTests", dependencies: [ + .target(name: "AsyncImageKit"), .target(name: "WordPressTesting"), .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs") ]), @@ -145,7 +145,7 @@ enum XcodeSupport { "JetpackStatsWidgetsCore", "WordPressFlux", "WordPressShared", - "WordPressMedia", + "AsyncImageKit", "WordPressUI", .product(name: "Alamofire", package: "Alamofire"), .product(name: "AutomatticAbout", package: "AutomatticAbout-swift"), diff --git a/Modules/Sources/WordPressMedia/AnimagedImage.swift b/Modules/Sources/AsyncImageKit/AnimagedImage.swift similarity index 100% rename from Modules/Sources/WordPressMedia/AnimagedImage.swift rename to Modules/Sources/AsyncImageKit/AnimagedImage.swift diff --git a/Modules/Sources/WordPressMedia/FaviconService.swift b/Modules/Sources/AsyncImageKit/FaviconService.swift similarity index 100% rename from Modules/Sources/WordPressMedia/FaviconService.swift rename to Modules/Sources/AsyncImageKit/FaviconService.swift diff --git a/Modules/Sources/WordPressMedia/ImageDecoder.swift b/Modules/Sources/AsyncImageKit/ImageDecoder.swift similarity index 100% rename from Modules/Sources/WordPressMedia/ImageDecoder.swift rename to Modules/Sources/AsyncImageKit/ImageDecoder.swift diff --git a/Modules/Sources/WordPressMedia/ImageDownloader.swift b/Modules/Sources/AsyncImageKit/ImageDownloader.swift similarity index 100% rename from Modules/Sources/WordPressMedia/ImageDownloader.swift rename to Modules/Sources/AsyncImageKit/ImageDownloader.swift diff --git a/Modules/Sources/WordPressMedia/ImagePrefetcher.swift b/Modules/Sources/AsyncImageKit/ImagePrefetcher.swift similarity index 100% rename from Modules/Sources/WordPressMedia/ImagePrefetcher.swift rename to Modules/Sources/AsyncImageKit/ImagePrefetcher.swift diff --git a/Modules/Sources/WordPressMedia/ImageRequest.swift b/Modules/Sources/AsyncImageKit/ImageRequest.swift similarity index 100% rename from Modules/Sources/WordPressMedia/ImageRequest.swift rename to Modules/Sources/AsyncImageKit/ImageRequest.swift diff --git a/Modules/Sources/WordPressMedia/MediaHost.swift b/Modules/Sources/AsyncImageKit/MediaHost.swift similarity index 100% rename from Modules/Sources/WordPressMedia/MediaHost.swift rename to Modules/Sources/AsyncImageKit/MediaHost.swift diff --git a/Modules/Sources/WordPressMedia/MemoryCache.swift b/Modules/Sources/AsyncImageKit/MemoryCache.swift similarity index 100% rename from Modules/Sources/WordPressMedia/MemoryCache.swift rename to Modules/Sources/AsyncImageKit/MemoryCache.swift diff --git a/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift b/Modules/Tests/AsyncImageKitTests/ImageDownloaderTests.swift similarity index 99% rename from Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift rename to Modules/Tests/AsyncImageKitTests/ImageDownloaderTests.swift index f661075ddfb4..95e967a166c2 100644 --- a/Modules/Tests/WordPressMediaTests/ImageDownloaderTests.swift +++ b/Modules/Tests/AsyncImageKitTests/ImageDownloaderTests.swift @@ -1,6 +1,6 @@ import UIKit import Testing -import WordPressMedia +import AsyncImageKit import WordPressTesting import OHHTTPStubs import OHHTTPStubsSwift diff --git a/Modules/Tests/WordPressMediaTests/MediaHostTests.swift b/Modules/Tests/AsyncImageKitTests/MediaHostTests.swift similarity index 100% rename from Modules/Tests/WordPressMediaTests/MediaHostTests.swift rename to Modules/Tests/AsyncImageKitTests/MediaHostTests.swift diff --git a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift index 1bb9ef503f36..9cc05988f5c4 100644 --- a/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift +++ b/WordPress/Classes/Extensions/NSAttributedString+Helpers.swift @@ -1,7 +1,7 @@ import UIKit import MobileCoreServices import UniformTypeIdentifiers -import WordPressMedia +import AsyncImageKit @objc public extension NSAttributedString { diff --git a/WordPress/Classes/Networking/MediaHost+Extensions.swift b/WordPress/Classes/Networking/MediaHost+Extensions.swift index ef6a44815f0d..9525bf47677e 100644 --- a/WordPress/Classes/Networking/MediaHost+Extensions.swift +++ b/WordPress/Classes/Networking/MediaHost+Extensions.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit /// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily /// initialize it from a given `AbstractPost`. diff --git a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift index f616d7c4a9a6..e6af940a29c1 100644 --- a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift +++ b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit fileprivate let photonHost = "i0.wp.com" fileprivate let secureHttpScheme = "https" diff --git a/WordPress/Classes/Services/MediaHelper.swift b/WordPress/Classes/Services/MediaHelper.swift index 1c6ba9132cad..be1d0170a007 100644 --- a/WordPress/Classes/Services/MediaHelper.swift +++ b/WordPress/Classes/Services/MediaHelper.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit class MediaHelper: NSObject { diff --git a/WordPress/Classes/Services/MediaImageService.swift b/WordPress/Classes/Services/MediaImageService.swift index e80679b40bd0..67768d200317 100644 --- a/WordPress/Classes/Services/MediaImageService.swift +++ b/WordPress/Classes/Services/MediaImageService.swift @@ -1,7 +1,7 @@ import UIKit import CoreData import WordPressShared -import WordPressMedia +import AsyncImageKit /// A service for retrieval and caching of thumbnails for ``Media`` objects. final class MediaImageService { diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index fd78c601c94d..aba96ac78543 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -5,7 +5,7 @@ import AutomatticTracks import AutomatticEncryptedLogs import WordPressAuthenticator import WordPressShared -import WordPressMedia +import AsyncImageKit import AutomatticAbout import UIDeviceIdentifier import WordPressUI diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index b094ea94f3fc..32dd51be6c8a 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -1,6 +1,6 @@ import UIKit import Gifu -import WordPressMedia +import AsyncImageKit /// A simple image view that supports rendering both static and animated images /// (see ``AnimatedImage``). diff --git a/WordPress/Classes/Utility/Media/CachedAsyncImage.swift b/WordPress/Classes/Utility/Media/CachedAsyncImage.swift index a93fa0c2b663..8841e311fca4 100644 --- a/WordPress/Classes/Utility/Media/CachedAsyncImage.swift +++ b/WordPress/Classes/Utility/Media/CachedAsyncImage.swift @@ -1,6 +1,6 @@ import SwiftUI import DesignSystem -import WordPressMedia +import AsyncImageKit /// Asynchronous Image View that replicates the public API of `SwiftUI.AsyncImage`. /// It uses `ImageDownloader` to fetch and cache the images. diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift index 3cc28ddf4250..e2ab2f650ebd 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit extension ImageDownloader { nonisolated static let shared = ImageDownloader(authenticator: MediaRequestAuthenticator()) diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift index af43bc1eb757..57c6ed5991a2 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift @@ -1,9 +1,9 @@ -import Foundation +import UIKit import WordPressUI import Gravatar -import WordPressMedia +import AsyncImageKit -extension WordPressMedia.ImageDownloader { +extension AsyncImageKit.ImageDownloader { nonisolated func downloadGravatarImage(with email: String, forceRefresh: Bool = false, completion: @escaping (UIImage?) -> Void) { diff --git a/WordPress/Classes/Utility/Media/ImageLoadingController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift index 1611a2c2aed1..102a33d415a4 100644 --- a/WordPress/Classes/Utility/Media/ImageLoadingController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -1,6 +1,6 @@ import Foundation import UIKit -import WordPressMedia +import AsyncImageKit /// A convenience class for managing image downloads for individual views. @MainActor diff --git a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift index 25809dfccba3..62bf11469178 100644 --- a/WordPress/Classes/Utility/Media/MediaExternalExporter.swift +++ b/WordPress/Classes/Utility/Media/MediaExternalExporter.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit /// Media export handling assets from external sources i.e.: Stock Photos /// diff --git a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift index dc3e2cf3c11e..4123729b19f3 100644 --- a/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift +++ b/WordPress/Classes/Utility/Media/MemoryCache+Extensions.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit import WordPressUI extension MemoryCache { diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index 92d5c0619831..1ea7a3647b0f 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -1,7 +1,7 @@ import Foundation import UIKit import Gifu -import WordPressMedia +import AsyncImageKit extension UIImageView { @MainActor diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 03c86785f764..44bd9b2656e6 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -6,7 +6,7 @@ import Gridicons import WordPressShared import MobileCoreServices import WordPressEditor -import WordPressMedia +import AsyncImageKit import AVKit import AutomatticTracks import MediaEditor diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift index 6976b2e9ba62..b29d2351bdb2 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit final class BlazeCampaignTableViewCell: UITableViewCell, Reusable { diff --git a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift index 3f58ab5c5c3d..f88760928a50 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit final class BlazePostPreviewView: UIView { diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift index 0fc0c6d7163a..e510fcb26ee6 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift @@ -1,7 +1,7 @@ import Foundation import UIKit import WordPressKit -import WordPressMedia +import AsyncImageKit final class DashboardBlazeCampaignView: UIView { private let statusView = BlazeCampaignStatusView() diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift index e5355eeebacf..af86d97d5858 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift @@ -1,6 +1,6 @@ -import Foundation +import UIKit import WordPressUI -import WordPressMedia +import AsyncImageKit import Gravatar extension BlogDetailsViewController { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift index a6a98aeffeb5..903d86bb25bb 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI import WordPressShared import WordPressKit -import WordPressMedia +import AsyncImageKit struct SiteIconViewModel { var imageURL: URL? diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift index 4d19260cbacd..3d54aabbda1c 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift @@ -1,7 +1,7 @@ import UIKit import Gridicons import WordPressShared -import WordPressMedia +import AsyncImageKit final class MediaItemHeaderView: UIView { let imageView = AsyncImageView() diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift index 461d23fd669f..41cedf53d84f 100644 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift @@ -1,6 +1,6 @@ import UIKit import WordPressUI -import WordPressMedia +import AsyncImageKit final class PostFeaturedImageCell: UITableViewCell { let featuredImageView = AsyncImageView() diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift index 51f042e12672..8c8f796324e6 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit /// Renders the comment body through `WPRichContentView`. /// diff --git a/WordPress/Classes/ViewRelated/Gutenberg/AztecAttachmentDelegate.swift b/WordPress/Classes/ViewRelated/Gutenberg/AztecAttachmentDelegate.swift index 995bd75deea4..37a03f3edd8e 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/AztecAttachmentDelegate.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/AztecAttachmentDelegate.swift @@ -1,5 +1,5 @@ import Aztec -import WordPressMedia +import AsyncImageKit class AztecAttachmentDelegate: TextViewAttachmentDelegate { private let post: AbstractPost diff --git a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift index 225583627599..dca6a14bbac6 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift @@ -2,7 +2,7 @@ import AutomatticTracks import Aztec import Gridicons import WordPressShared -import WordPressMedia +import AsyncImageKit class EditorMediaUtility { private static let InternalInconsistencyError = NSError(domain: NSExceptionName.internalInconsistencyException.rawValue, code: 0) diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 1589fc3e3ade..8fb10799e061 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit import Gutenberg import Aztec import WordPressFlux diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift index a3882df36f10..53558805f90a 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergMediaEditorImage.swift @@ -1,6 +1,6 @@ import Foundation import MediaEditor -import WordPressMedia +import AsyncImageKit /** This is a struct to be given to MediaEditor that represent the image. diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift index a1ad28d733d6..3e66dc5d3e38 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift @@ -2,7 +2,7 @@ import Foundation import GravatarUI import WordPressShared import WordPressAuthenticator -import WordPressMedia +import AsyncImageKit @MainActor struct GravatarQuickEditorPresenter { diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift index 4d80e39286f1..7ee79240e2f8 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerCollectionCell.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit final class ExternalMediaPickerCollectionCell: UICollectionViewCell { private let imageView = AsyncImageView() diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift index 729b59ba52ca..b0ad72b0d00a 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit protocol ExternalMediaPickerViewDelegate: AnyObject { /// If the user cancels the flow, the selection is empty. diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index fdc0ae966234..075015b84353 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -1,6 +1,6 @@ import UIKit import WordPressUI -import WordPressMedia +import AsyncImageKit final class LightboxImagePageViewController: UIViewController { private(set) var scrollView = LightboxImageScrollView() diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift index 69e37075929e..254f4aa49da2 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit enum LightboxItem { case image(UIImage) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index ec1058a8fe75..0f9f8debc036 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit import WordPressUI import UniformTypeIdentifiers diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift index 54d1b8d7f510..e4c9e8b7fe67 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaCollectionCell.swift @@ -1,7 +1,7 @@ import UIKit import Combine import Gifu -import WordPressMedia +import AsyncImageKit final class SiteMediaCollectionCell: UICollectionViewCell, Reusable { private let imageContainerView = UIView() diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index f3fd5637ea5a..c7a4721ea5bd 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit import AutomatticTracks import GutenbergKit import SafariServices diff --git a/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift b/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift index 861e06c50c3a..557f9f398413 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Tools/NotificationMediaDownloader.swift @@ -1,6 +1,6 @@ import Foundation import UIKit -import WordPressMedia +import AsyncImageKit /// The purpose of this class is to provide a simple API to download assets from the web. /// Assets are downloaded, and resized to fit a maximumWidth, specified in the initial download call. diff --git a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift index 0633039f54ba..bba43f80f4f2 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Views/NoteBlockHeaderTableViewCell.swift @@ -1,7 +1,7 @@ import Foundation import WordPressShared import WordPressUI -import WordPressMedia +import AsyncImageKit import Gravatar // MARK: - NoteBlockHeaderTableViewCell diff --git a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift index 7d6534431d1c..266debf1e90f 100644 --- a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift +++ b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift @@ -1,7 +1,7 @@ import Foundation import UIKit import Combine -import WordPressMedia +import AsyncImageKit final class PageListCell: UITableViewCell, AbstractPostListCell, PostSearchResultCell, Reusable { diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift index 3f6c036029a0..3b9317740842 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Preview/RevisionPreviewTextViewManager.swift @@ -1,6 +1,6 @@ import Aztec import Gridicons -import WordPressMedia +import AsyncImageKit class RevisionPreviewTextViewManager: NSObject { var post: AbstractPost? diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift index cb84ba4496cc..6d4d2e5c5949 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift @@ -2,7 +2,7 @@ import AutomatticTracks import UIKit import WordPressShared import WordPressUI -import WordPressMedia +import AsyncImageKit final class PostCompactCell: UITableViewCell, Reusable { private let titleLabel = UILabel() diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift index 8ec4cb3c89dd..431b482a3d9f 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift @@ -1,6 +1,6 @@ import Foundation import UIKit -import WordPressMedia +import AsyncImageKit protocol AbstractPostListCell { /// A post displayed by the cell. diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift index ed69ee523d0c..923c06598f19 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderCrossPostCell.swift @@ -2,7 +2,7 @@ import Foundation import AutomatticTracks import WordPressShared import WordPressUI -import WordPressMedia +import AsyncImageKit final class ReaderCrossPostCell: ReaderStreamBaseCell { private let view = ReaderCrossPostView() diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 29693a544e8d..b822ab27de3d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -2,7 +2,7 @@ import SwiftUI import UIKit import Combine import WordPressShared -import WordPressMedia +import AsyncImageKit final class ReaderPostCell: ReaderStreamBaseCell { private let view = ReaderPostCellView() diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift index 0600cf4b61e4..91b6f8d48c85 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift @@ -1,5 +1,5 @@ import Foundation -import WordPressMedia +import AsyncImageKit final class ReaderPostCellViewModel { // Header diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 612dfec0326a..21c787b39fb5 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -2,7 +2,7 @@ import Foundation import SVProgressHUD import WordPressShared import WordPressFlux -import WordPressMedia +import AsyncImageKit import UIKit import Combine import WordPressUI diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 607ff17c6c8a..4ea86b982473 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -1,6 +1,6 @@ import Foundation import WordPressShared -import WordPressMedia +import AsyncImageKit import Combine class ReaderDetailCoordinator { diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 6c5242a6c3de..d86c3f31387a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit protocol ReaderDetailFeaturedImageViewDelegate: AnyObject { func didTapFeaturedImage(_ sender: AsyncImageView) diff --git a/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift b/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift index 87913fe3306a..34c78ee41187 100644 --- a/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Views/ReaderAvatarView.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit final class ReaderAvatarView: UIView { private let asyncImageView = AsyncImageView() diff --git a/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift b/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift index 27363ecacf74..c1eca881cc4d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift @@ -1,5 +1,5 @@ import SwiftUI -import WordPressMedia +import AsyncImageKit struct ReaderSiteIconView: View, Hashable { let site: ReaderSiteTopic diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift index f07e4b7cad3a..7d7f1e0c1fd8 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift @@ -1,7 +1,7 @@ import UIKit import Gridicons import DesignSystem -import WordPressMedia +import AsyncImageKit protocol LatestPostSummaryConfigurable { func configure(withInsightData lastPostInsight: StatsLastPostInsight?, andDelegate delegate: SiteStatsInsightsDelegate?) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift index 1d0dcfe2a23c..d95c1e0f49d2 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit struct StatsTotalRowData: Equatable { var id: UUID? diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift index 897eb3fccd3f..8429125f5e49 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift @@ -1,7 +1,7 @@ import Foundation import WordPressUI import Gravatar -import WordPressMedia +import AsyncImageKit extension WPTabBarController { diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift index 223f26f637bc..f52ca6f856d8 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift @@ -1,7 +1,7 @@ import Foundation import UIKit import WordPressShared -import WordPressMedia +import AsyncImageKit @objc protocol WPRichContentViewDelegate: UITextViewDelegate { func richContentView(_ richContentView: WPRichContentView, didReceiveImageAction image: WPRichTextImage) diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift index dd314f896c58..b6f00c328715 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit import Gifu class WPRichTextImage: UIControl, WPRichTextMediaAttachment { diff --git a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift index f3acd56f02cc..3ad260d672f3 100644 --- a/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift +++ b/WordPress/Classes/ViewRelated/WhatsNew/Views/DashboardCustomAnnouncementCell.swift @@ -1,5 +1,5 @@ import UIKit -import WordPressMedia +import AsyncImageKit class DashboardCustomAnnouncementCell: AnnouncementTableViewCell { diff --git a/WordPress/WordPressTest/MediaImageServiceTests.swift b/WordPress/WordPressTest/MediaImageServiceTests.swift index c2e35546f2a8..7fbe89e78ae5 100644 --- a/WordPress/WordPressTest/MediaImageServiceTests.swift +++ b/WordPress/WordPressTest/MediaImageServiceTests.swift @@ -1,7 +1,7 @@ import XCTest import OHHTTPStubs import OHHTTPStubsSwift -import WordPressMedia +import AsyncImageKit @testable import WordPress class MediaImageServiceTests: CoreDataTestCase { From c7b0a49eeea40caec7285ae44ccc3b35a28b3cbd Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 17:22:53 -0500 Subject: [PATCH 072/101] Remove MediaHost from AsyncImageKit --- .../Sources/AsyncImageKit/ImageDownloader.swift | 15 ++++++--------- Modules/Sources/AsyncImageKit/ImageRequest.swift | 4 ++-- .../Classes/Networking}/MediaHost.swift | 9 ++++++++- .../Networking/MediaRequestAuthenticator.swift | 5 ++--- .../Classes/Utility/Media/AsyncImageView.swift | 2 +- .../Media/ImageDownloader+Extensions.swift | 2 +- .../Media/UIImageView+ImageDownloader.swift | 2 +- WordPress/WordPress.xcodeproj/project.pbxproj | 4 ++++ .../WordPressTest}/MediaHostTests.swift | 0 9 files changed, 25 insertions(+), 18 deletions(-) rename {Modules/Sources/AsyncImageKit => WordPress/Classes/Networking}/MediaHost.swift (91%) rename {Modules/Tests/AsyncImageKitTests => WordPress/WordPressTest}/MediaHostTests.swift (100%) diff --git a/Modules/Sources/AsyncImageKit/ImageDownloader.swift b/Modules/Sources/AsyncImageKit/ImageDownloader.swift index 2076816867b6..8c2c56106864 100644 --- a/Modules/Sources/AsyncImageKit/ImageDownloader.swift +++ b/Modules/Sources/AsyncImageKit/ImageDownloader.swift @@ -4,7 +4,6 @@ import UIKit @ImageDownloaderActor public final class ImageDownloader { private nonisolated let cache: MemoryCacheProtocol - private let authenticator: MediaRequestAuthenticatorProtocol? private let urlSession = URLSession { $0.urlCache = nil @@ -21,14 +20,12 @@ public final class ImageDownloader { private var tasks: [String: ImageDataTask] = [:] public nonisolated init( - cache: MemoryCacheProtocol = MemoryCache.shared, - authenticator: MediaRequestAuthenticatorProtocol? + cache: MemoryCacheProtocol = MemoryCache.shared ) { self.cache = cache - self.authenticator = authenticator } - public func image(from url: URL, host: MediaHost? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage { + public func image(from url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) async throws -> UIImage { try await image(for: ImageRequest(url: url, host: host, options: options)) } @@ -55,8 +52,8 @@ public final class ImageDownloader { switch request.source { case .url(let url, let host): var request: URLRequest - if let host, let authenticator { - request = try await authenticator.authenticatedRequest(for: url, host: host) + if let host { + request = try await host.authenticatedRequest(for: url) } else { request = URLRequest(url: url) } @@ -195,6 +192,6 @@ private extension URLSession { } } -public protocol MediaRequestAuthenticatorProtocol: Sendable { - @MainActor func authenticatedRequest(for url: URL, host: MediaHost) async throws -> URLRequest +public protocol MediaHostProtocol: Sendable { + @MainActor func authenticatedRequest(for url: URL) async throws -> URLRequest } diff --git a/Modules/Sources/AsyncImageKit/ImageRequest.swift b/Modules/Sources/AsyncImageKit/ImageRequest.swift index 0c299c489bba..5a4ada4df736 100644 --- a/Modules/Sources/AsyncImageKit/ImageRequest.swift +++ b/Modules/Sources/AsyncImageKit/ImageRequest.swift @@ -2,7 +2,7 @@ import UIKit public final class ImageRequest: Sendable { public enum Source: Sendable { - case url(URL, MediaHost?) + case url(URL, MediaHostProtocol?) case urlRequest(URLRequest) var url: URL? { @@ -16,7 +16,7 @@ public final class ImageRequest: Sendable { let source: Source let options: ImageRequestOptions - public init(url: URL, host: MediaHost? = nil, options: ImageRequestOptions = .init()) { + public init(url: URL, host: MediaHostProtocol? = nil, options: ImageRequestOptions = .init()) { self.source = .url(url, host) self.options = options } diff --git a/Modules/Sources/AsyncImageKit/MediaHost.swift b/WordPress/Classes/Networking/MediaHost.swift similarity index 91% rename from Modules/Sources/AsyncImageKit/MediaHost.swift rename to WordPress/Classes/Networking/MediaHost.swift index 66f8afce31f0..3aad15939825 100644 --- a/Modules/Sources/AsyncImageKit/MediaHost.swift +++ b/WordPress/Classes/Networking/MediaHost.swift @@ -1,8 +1,9 @@ import Foundation +import AsyncImageKit /// Defines a media host for request authentication purposes. /// -public enum MediaHost: Equatable, Sendable { +public enum MediaHost: Equatable, Sendable, MediaHostProtocol { case publicSite case publicWPComSite case privateSelfHostedSite @@ -90,4 +91,10 @@ public enum MediaHost: Equatable, Sendable { self = .privateAtomicWPComSite(siteID: siteID, username: username, authToken: authToken) } + + // MARK: - MediaHostProtocol + + public func authenticatedRequest(for url: URL) async throws -> URLRequest { + try await MediaRequestAuthenticator().authenticatedRequest(for: url, host: self) + } } diff --git a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift index e6af940a29c1..f0a8a646719e 100644 --- a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift +++ b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift @@ -18,8 +18,7 @@ extension URL { /// /// This also includes regular and photon URLs. /// -struct MediaRequestAuthenticator: MediaRequestAuthenticatorProtocol { - +struct MediaRequestAuthenticator { /// Errors conditions that this class can find. /// enum Error: Swift.Error { @@ -56,7 +55,7 @@ struct MediaRequestAuthenticator: MediaRequestAuthenticatorProtocol { /// authentication. /// - fail: the closure that will be called upon finding an error condition. /// - func authenticatedRequest( + private func authenticatedRequest( for url: URL, from host: MediaHost, onComplete provide: @escaping (URLRequest) -> (), diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index 32dd51be6c8a..bcb0f0ef658b 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -83,7 +83,7 @@ final class AsyncImageView: UIView { /// - parameter size: Target image size in pixels. func setImage( with imageURL: URL, - host: MediaHost? = nil, + host: MediaHostProtocol? = nil, size: ImageSize? = nil ) { let request = ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size)) diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift index e2ab2f650ebd..625688020d28 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift @@ -2,7 +2,7 @@ import Foundation import AsyncImageKit extension ImageDownloader { - nonisolated static let shared = ImageDownloader(authenticator: MediaRequestAuthenticator()) + nonisolated static let shared = ImageDownloader() } extension ImagePrefetcher { diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index 1ea7a3647b0f..05aeb851ddb3 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -22,7 +22,7 @@ struct ImageViewExtensions { } } - func setImage(with imageURL: URL, host: MediaHost? = nil, size: ImageSize? = nil) { + func setImage(with imageURL: URL, host: MediaHostProtocol? = nil, size: ImageSize? = nil) { setImage(with: ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size))) } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 927371beb713..416831a84ab3 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ 0C0DF8942C2DF14600011B7D /* LoginFacadeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C0DF8932C2DF12A00011B7D /* LoginFacadeTests.m */; }; 0C2155A62C39A24D00EFE2C0 /* XcodeTarget_UITests in Frameworks */ = {isa = PBXBuildFile; productRef = 0C2155A52C39A24D00EFE2C0 /* XcodeTarget_UITests */; }; 0C2155A82C39A25400EFE2C0 /* XcodeTarget_UITests in Frameworks */ = {isa = PBXBuildFile; productRef = 0C2155A72C39A25400EFE2C0 /* XcodeTarget_UITests */; }; + 0C22EE0B2D2749A40058F329 /* MediaHostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C22EE0A2D2749A40058F329 /* MediaHostTests.swift */; }; 0C235BD22C3862D400D0E163 /* XcodeTarget_WordPressTests in Frameworks */ = {isa = PBXBuildFile; productRef = 0C235BD12C3862D400D0E163 /* XcodeTarget_WordPressTests */; }; 0C2518AE2ABE1F2800381D31 /* iphone-photo.heic in Resources */ = {isa = PBXBuildFile; fileRef = 0C2518AD2ABE1EA000381D31 /* iphone-photo.heic */; }; 0C35FFF429CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */; }; @@ -2015,6 +2016,7 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; 0C0DF8932C2DF12A00011B7D /* LoginFacadeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginFacadeTests.m; sourceTree = ""; }; + 0C22EE0A2D2749A40058F329 /* MediaHostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaHostTests.swift; sourceTree = ""; }; 0C2518AD2ABE1EA000381D31 /* iphone-photo.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iphone-photo.heic"; sourceTree = ""; }; 0C35FFF329CBA6DA00D224EB /* BlogDashboardPersonalizationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModelTests.swift; sourceTree = ""; }; 0C38581F2CA74DC7004880ED /* AppSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsTests.swift; sourceTree = ""; }; @@ -6032,6 +6034,7 @@ isa = PBXGroup; children = ( F1450CF82437EEBB00A28BFE /* MediaRequestAuthenticatorTests.swift */, + 0C22EE0A2D2749A40058F329 /* MediaHostTests.swift */, 4688E6CB26AB571D00A5D894 /* RequestAuthenticatorTests.swift */, E15027641E03E54100B847E3 /* PinghubTests.swift */, 4AB6A35F2B7C3EB500769115 /* PinghubWebSocketTests.swift */, @@ -9966,6 +9969,7 @@ 572FB401223A806000933C76 /* NoticeStoreTests.swift in Sources */, 748437EE1F1D4A7300E8DDAF /* RichContentFormatterTests.swift in Sources */, FE9438B22A050251006C40EC /* BlockEditorSettings_GutenbergEditorSettingsTests.swift in Sources */, + 0C22EE0B2D2749A40058F329 /* MediaHostTests.swift in Sources */, C81CCD6A243AEE1100A83E27 /* TenorAPIResponseTests.swift in Sources */, 8BE7C84123466927006EDE70 /* I18n.swift in Sources */, C396C80B280F2401006FE7AC /* SiteDesignTests.swift in Sources */, diff --git a/Modules/Tests/AsyncImageKitTests/MediaHostTests.swift b/WordPress/WordPressTest/MediaHostTests.swift similarity index 100% rename from Modules/Tests/AsyncImageKitTests/MediaHostTests.swift rename to WordPress/WordPressTest/MediaHostTests.swift From 4d8d512744684efbd5aacc343d50d7e85323f1f3 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 17:49:07 -0500 Subject: [PATCH 073/101] Move ImageDownloader.shared to AsyncImageKit --- Modules/Sources/AsyncImageKit/ImageDownloader.swift | 2 ++ Modules/Sources/AsyncImageKit/ImagePrefetcher.swift | 5 ++++- .../Utility/Media/ImageDownloader+Extensions.swift | 10 ---------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Modules/Sources/AsyncImageKit/ImageDownloader.swift b/Modules/Sources/AsyncImageKit/ImageDownloader.swift index 8c2c56106864..d634bd537c54 100644 --- a/Modules/Sources/AsyncImageKit/ImageDownloader.swift +++ b/Modules/Sources/AsyncImageKit/ImageDownloader.swift @@ -3,6 +3,8 @@ import UIKit /// The system that downloads and caches images, and prepares them for display. @ImageDownloaderActor public final class ImageDownloader { + public nonisolated static let shared = ImageDownloader() + private nonisolated let cache: MemoryCacheProtocol private let urlSession = URLSession { diff --git a/Modules/Sources/AsyncImageKit/ImagePrefetcher.swift b/Modules/Sources/AsyncImageKit/ImagePrefetcher.swift index d40ace3039ce..623d60ac2aa2 100644 --- a/Modules/Sources/AsyncImageKit/ImagePrefetcher.swift +++ b/Modules/Sources/AsyncImageKit/ImagePrefetcher.swift @@ -15,7 +15,10 @@ public final class ImagePrefetcher { } } - public nonisolated init(downloader: ImageDownloader, maxConcurrentTasks: Int = 2) { + public nonisolated init( + downloader: ImageDownloader = .shared, + maxConcurrentTasks: Int = 2 + ) { self.downloader = downloader self.maxConcurrentTasks = maxConcurrentTasks } diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift index 625688020d28..c592ed942089 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Extensions.swift @@ -1,16 +1,6 @@ import Foundation import AsyncImageKit -extension ImageDownloader { - nonisolated static let shared = ImageDownloader() -} - -extension ImagePrefetcher { - convenience nonisolated init() { - self.init(downloader: .shared) - } -} - // MARK: - ImageDownloader (Closures) extension ImageDownloader { From 7f45fec04d98e577616b6421d1b336f7dc1874ab Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 18:02:20 -0500 Subject: [PATCH 074/101] Move AsyncImageView and other related types to AsyncImageKit --- Modules/Package.swift | 13 ++-- .../{ => Helpers}/AnimagedImage.swift | 0 .../{ => Helpers}/FaviconService.swift | 0 .../{ => Helpers}/ImageDecoder.swift | 0 .../{ => Helpers}/MemoryCache.swift | 0 .../AsyncImageKit/Views}/AsyncImageView.swift | 54 ++++++++----- .../Views}/CachedAsyncImage.swift | 21 ++--- .../Views/ImageLoadingController.swift | 53 +++++++++++++ .../Views}/UIImageView+ImageDownloader.swift | 14 ++-- .../Media/ImageLoadingController.swift | 76 ------------------- .../Site Picker/BlogList/SiteIconView.swift | 1 + .../Gravatar/UIImageView+Gravatar.swift | 11 ++- .../LightboxImagePageViewController.swift | 7 +- .../SiteMediaImageLoadingController.swift | 44 +++++++++++ .../Views/ReaderDetailFeaturedImageView.xib | 6 +- .../Detail/Views/ReaderDetailHeaderView.swift | 1 + .../List/NotificationsList/AvatarView.swift | 1 + 17 files changed, 174 insertions(+), 128 deletions(-) rename Modules/Sources/AsyncImageKit/{ => Helpers}/AnimagedImage.swift (100%) rename Modules/Sources/AsyncImageKit/{ => Helpers}/FaviconService.swift (100%) rename Modules/Sources/AsyncImageKit/{ => Helpers}/ImageDecoder.swift (100%) rename Modules/Sources/AsyncImageKit/{ => Helpers}/MemoryCache.swift (100%) rename {WordPress/Classes/Utility/Media => Modules/Sources/AsyncImageKit/Views}/AsyncImageView.swift (74%) rename {WordPress/Classes/Utility/Media => Modules/Sources/AsyncImageKit/Views}/CachedAsyncImage.swift (78%) create mode 100644 Modules/Sources/AsyncImageKit/Views/ImageLoadingController.swift rename {WordPress/Classes/Utility/Media => Modules/Sources/AsyncImageKit/Views}/UIImageView+ImageDownloader.swift (78%) delete mode 100644 WordPress/Classes/Utility/Media/ImageLoadingController.swift create mode 100644 WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaImageLoadingController.swift diff --git a/Modules/Package.swift b/Modules/Package.swift index 513237db4e23..85e1bb172ba3 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -8,10 +8,10 @@ let package = Package( .iOS(.v16), ], products: XcodeSupport.products + [ - .library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]), + .library(name: "AsyncImageKit", targets: ["AsyncImageKit"]), .library(name: "DesignSystem", targets: ["DesignSystem"]), + .library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]), .library(name: "WordPressFlux", targets: ["WordPressFlux"]), - .library(name: "AsyncImageKit", targets: ["AsyncImageKit"]), .library(name: "WordPressShared", targets: ["WordPressShared"]), .library(name: "WordPressUI", targets: ["WordPressUI"]), ], @@ -52,16 +52,17 @@ let package = Package( .package(url: "https://github.com/Automattic/color-studio", branch: "trunk"), ], targets: XcodeSupport.targets + [ - .target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]), + .target(name: "AsyncImageKit", dependencies: [ + .product(name: "Collections", package: "swift-collections"), + .product(name: "Gifu", package: "Gifu"), + ]), .target(name: "DesignSystem", swiftSettings: [.swiftLanguageMode(.v5)]), + .target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "UITestsFoundation", dependencies: [ .product(name: "ScreenObject", package: "ScreenObject"), .product(name: "XCUITestHelpers", package: "XCUITestHelpers"), ], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]), - .target(name: "AsyncImageKit", dependencies: [ - .product(name: "Collections", package: "swift-collections"), - ]), .target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressShared", dependencies: [.target(name: "WordPressSharedObjC")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), .target(name: "WordPressTesting", resources: [.process("Resources")]), diff --git a/Modules/Sources/AsyncImageKit/AnimagedImage.swift b/Modules/Sources/AsyncImageKit/Helpers/AnimagedImage.swift similarity index 100% rename from Modules/Sources/AsyncImageKit/AnimagedImage.swift rename to Modules/Sources/AsyncImageKit/Helpers/AnimagedImage.swift diff --git a/Modules/Sources/AsyncImageKit/FaviconService.swift b/Modules/Sources/AsyncImageKit/Helpers/FaviconService.swift similarity index 100% rename from Modules/Sources/AsyncImageKit/FaviconService.swift rename to Modules/Sources/AsyncImageKit/Helpers/FaviconService.swift diff --git a/Modules/Sources/AsyncImageKit/ImageDecoder.swift b/Modules/Sources/AsyncImageKit/Helpers/ImageDecoder.swift similarity index 100% rename from Modules/Sources/AsyncImageKit/ImageDecoder.swift rename to Modules/Sources/AsyncImageKit/Helpers/ImageDecoder.swift diff --git a/Modules/Sources/AsyncImageKit/MemoryCache.swift b/Modules/Sources/AsyncImageKit/Helpers/MemoryCache.swift similarity index 100% rename from Modules/Sources/AsyncImageKit/MemoryCache.swift rename to Modules/Sources/AsyncImageKit/Helpers/MemoryCache.swift diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift similarity index 74% rename from WordPress/Classes/Utility/Media/AsyncImageView.swift rename to Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift index bcb0f0ef658b..4a6409b49b7c 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift @@ -1,46 +1,47 @@ import UIKit import Gifu -import AsyncImageKit /// A simple image view that supports rendering both static and animated images /// (see ``AnimatedImage``). @MainActor -final class AsyncImageView: UIView { +public final class AsyncImageView: UIView { private let imageView = GIFImageView() private var errorView: UIImageView? private var spinner: UIActivityIndicatorView? private let controller = ImageLoadingController() - enum LoadingStyle { + public enum LoadingStyle { /// Shows a secondary background color during the download. case background /// Shows a spinner during the download. case spinner } - struct Configuration { + public struct Configuration { /// Image tint color. - var tintColor: UIColor? + public var tintColor: UIColor? /// Image view content mode. - var contentMode: UIView.ContentMode? + public var contentMode: UIView.ContentMode? /// Enabled by default and shows an error icon on failures. - var isErrorViewEnabled = true + public var isErrorViewEnabled = true /// By default, `background`. - var loadingStyle = LoadingStyle.background + public var loadingStyle = LoadingStyle.background - var passTouchesToSuperview = false + public var passTouchesToSuperview = false + + public init() {} } - var configuration = Configuration() { + public var configuration = Configuration() { didSet { didUpdateConfiguration(configuration) } } /// The currently displayed image. If the image is animated, returns an /// instance of ``AnimatedImage``. - var image: UIImage? { + public var image: UIImage? { didSet { if let image { imageView.configure(image: image) @@ -50,12 +51,12 @@ final class AsyncImageView: UIView { } } - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) setupView() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) setupView() } @@ -65,7 +66,12 @@ final class AsyncImageView: UIView { addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false - pinSubviewToAllEdges(imageView) + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor), + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + ]) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill @@ -75,13 +81,13 @@ final class AsyncImageView: UIView { } /// Removes the current image and stops the outstanding downloads. - func prepareForReuse() { + public func prepareForReuse() { controller.prepareForReuse() image = nil } /// - parameter size: Target image size in pixels. - func setImage( + public func setImage( with imageURL: URL, host: MediaHostProtocol? = nil, size: ImageSize? = nil @@ -90,7 +96,7 @@ final class AsyncImageView: UIView { controller.setImage(with: request) } - func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { + public func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { controller.setImage(with: request, completion: completion) } @@ -134,7 +140,10 @@ final class AsyncImageView: UIView { let spinner = UIActivityIndicatorView() addSubview(spinner) spinner.translatesAutoresizingMaskIntoConstraints = false - pinSubviewAtCenter(spinner) + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) self.spinner = spinner return spinner } @@ -147,12 +156,15 @@ final class AsyncImageView: UIView { errorView.tintColor = .separator addSubview(errorView) errorView.translatesAutoresizingMaskIntoConstraints = false - pinSubviewAtCenter(errorView) + NSLayoutConstraint.activate([ + errorView.centerXAnchor.constraint(equalTo: centerXAnchor), + errorView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) self.errorView = errorView return errorView } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if configuration.passTouchesToSuperview && self.bounds.contains(point) { // Pass the touch to the superview return nil @@ -164,7 +176,7 @@ final class AsyncImageView: UIView { extension GIFImageView { /// If the image is an instance of `AnimatedImage` type, plays it as an /// animated image. - func configure(image: UIImage) { + public func configure(image: UIImage) { if let gif = image as? AnimatedImage, let data = gif.gifData { self.animate(withGIFData: data) } else { diff --git a/WordPress/Classes/Utility/Media/CachedAsyncImage.swift b/Modules/Sources/AsyncImageKit/Views/CachedAsyncImage.swift similarity index 78% rename from WordPress/Classes/Utility/Media/CachedAsyncImage.swift rename to Modules/Sources/AsyncImageKit/Views/CachedAsyncImage.swift index 8841e311fca4..d6ef77690540 100644 --- a/WordPress/Classes/Utility/Media/CachedAsyncImage.swift +++ b/Modules/Sources/AsyncImageKit/Views/CachedAsyncImage.swift @@ -1,15 +1,13 @@ import SwiftUI -import DesignSystem -import AsyncImageKit /// Asynchronous Image View that replicates the public API of `SwiftUI.AsyncImage`. /// It uses `ImageDownloader` to fetch and cache the images. -struct CachedAsyncImage: View where Content: View { +public struct CachedAsyncImage: View where Content: View { @State private var phase: AsyncImagePhase = .empty private let url: URL? private let content: (AsyncImagePhase) -> Content private let imageDownloader: ImageDownloader - private let host: MediaHost? + private let host: MediaHostProtocol? public var body: some View { content(phase) @@ -20,19 +18,24 @@ struct CachedAsyncImage: View where Content: View { /// Initializes an image without any customization. /// Provides a plain color as placeholder - init(url: URL?) where Content == _ConditionalContent { + public init(url: URL?) where Content == _ConditionalContent { self.init(url: url) { phase in if let image = phase.image { image } else { - Color(uiColor: UIAppColor.gray(.shade40)) + Color(uiColor: .secondarySystemBackground) } } } /// Allows content customization and providing a placeholder that will be shown /// until the image download is finalized. - init(url: URL?, host: MediaHost? = nil, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent, I: View, P: View { + public init( + url: URL?, + host: MediaHostProtocol? = nil, + @ViewBuilder content: @escaping (Image) -> I, + @ViewBuilder placeholder: @escaping () -> P + ) where Content == _ConditionalContent, I: View, P: View { self.init(url: url, host: host) { phase in if let image = phase.image { content(image) @@ -42,9 +45,9 @@ struct CachedAsyncImage: View where Content: View { } } - init( + public init( url: URL?, - host: MediaHost? = nil, + host: MediaHostProtocol? = nil, imageDownloader: ImageDownloader = .shared, @ViewBuilder content: @escaping (AsyncImagePhase) -> Content ) { diff --git a/Modules/Sources/AsyncImageKit/Views/ImageLoadingController.swift b/Modules/Sources/AsyncImageKit/Views/ImageLoadingController.swift new file mode 100644 index 000000000000..064dfae9bd48 --- /dev/null +++ b/Modules/Sources/AsyncImageKit/Views/ImageLoadingController.swift @@ -0,0 +1,53 @@ +import UIKit + +/// A convenience class for managing image downloads for individual views. +@MainActor +public final class ImageLoadingController { + public var downloader: ImageDownloader = .shared + public var onStateChanged: (State) -> Void = { _ in } + + public private(set) var task: Task? + + public enum State { + case loading + case success(UIImage) + case failure(Error) + } + + deinit { + task?.cancel() + } + + public init() {} + + public func prepareForReuse() { + task?.cancel() + task = nil + } + + /// - parameter completion: Gets called on completion _after_ `onStateChanged`. + public func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { + task?.cancel() + + if let image = downloader.cachedImage(for: request) { + onStateChanged(.success(image)) + completion?(.success(image)) + } else { + onStateChanged(.loading) + task = Task { @MainActor [downloader, weak self] in + do { + let image = try await downloader.image(for: request) + // This line guarantees that if you cancel on the main thread, + // none of the `onStateChanged` callbacks get called. + guard !Task.isCancelled else { return } + self?.onStateChanged(.success(image)) + completion?(.success(image)) + } catch { + guard !Task.isCancelled else { return } + self?.onStateChanged(.failure(error)) + completion?(.failure(error)) + } + } + } + } +} diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/Modules/Sources/AsyncImageKit/Views/UIImageView+ImageDownloader.swift similarity index 78% rename from WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift rename to Modules/Sources/AsyncImageKit/Views/UIImageView+ImageDownloader.swift index 05aeb851ddb3..422a61af70d5 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/Modules/Sources/AsyncImageKit/Views/UIImageView+ImageDownloader.swift @@ -1,18 +1,16 @@ -import Foundation import UIKit import Gifu -import AsyncImageKit extension UIImageView { @MainActor - var wp: ImageViewExtensions { ImageViewExtensions(imageView: self) } + public var wp: ImageViewExtensions { ImageViewExtensions(imageView: self) } } @MainActor -struct ImageViewExtensions { +public struct ImageViewExtensions { var imageView: UIImageView - func prepareForReuse() { + public func prepareForReuse() { controller.prepareForReuse() if let gifView = imageView as? GIFImageView, gifView.isAnimatingGIF { @@ -22,15 +20,15 @@ struct ImageViewExtensions { } } - func setImage(with imageURL: URL, host: MediaHostProtocol? = nil, size: ImageSize? = nil) { + public func setImage(with imageURL: URL, host: MediaHostProtocol? = nil, size: ImageSize? = nil) { setImage(with: ImageRequest(url: imageURL, host: host, options: ImageRequestOptions(size: size))) } - func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { + public func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { controller.setImage(with: request, completion: completion) } - var controller: ImageLoadingController { + public var controller: ImageLoadingController { if let controller = objc_getAssociatedObject(imageView, ImageViewExtensions.controllerKey) as? ImageLoadingController { return controller } diff --git a/WordPress/Classes/Utility/Media/ImageLoadingController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift deleted file mode 100644 index 102a33d415a4..000000000000 --- a/WordPress/Classes/Utility/Media/ImageLoadingController.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import UIKit -import AsyncImageKit - -/// A convenience class for managing image downloads for individual views. -@MainActor -final class ImageLoadingController { - var downloader: ImageDownloader = .shared - var service: MediaImageService = .shared - var onStateChanged: (State) -> Void = { _ in } - - private(set) var task: Task? - - enum State { - case loading - case success(UIImage) - case failure(Error) - } - - deinit { - task?.cancel() - } - - func prepareForReuse() { - task?.cancel() - task = nil - } - - /// - parameter completion: Gets called on completion _after_ `onStateChanged`. - func setImage(with request: ImageRequest, completion: (@MainActor (Result) -> Void)? = nil) { - task?.cancel() - - if let image = downloader.cachedImage(for: request) { - onStateChanged(.success(image)) - completion?(.success(image)) - } else { - onStateChanged(.loading) - task = Task { @MainActor [downloader, weak self] in - do { - let image = try await downloader.image(for: request) - // This line guarantees that if you cancel on the main thread, - // none of the `onStateChanged` callbacks get called. - guard !Task.isCancelled else { return } - self?.onStateChanged(.success(image)) - completion?(.success(image)) - } catch { - guard !Task.isCancelled else { return } - self?.onStateChanged(.failure(error)) - completion?(.failure(error)) - } - } - } - } - - func setImage(with media: Media, size: MediaImageService.ImageSize) { - task?.cancel() - - if let image = service.getCachedThumbnail(for: .init(media), size: size) { - onStateChanged(.success(image)) - } else { - onStateChanged(.loading) - task = Task { @MainActor [service, weak self] in - do { - let image = try await service.image(for: media, size: size) - // This line guarantees that if you cancel on the main thread, - // none of the `onStateChanged` callbacks get called. - guard !Task.isCancelled else { return } - self?.onStateChanged(.success(image)) - } catch { - guard !Task.isCancelled else { return } - self?.onStateChanged(.failure(error)) - } - } - } - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift index ae4bd41274e0..65a761cdc047 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift @@ -1,5 +1,6 @@ import UIKit import SwiftUI +import AsyncImageKit import DesignSystem import WordPressShared diff --git a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift index 9c61bdf2b8d0..694c530778b7 100644 --- a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift +++ b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift @@ -1,4 +1,5 @@ import Foundation +import AsyncImageKit import GravatarUI import WordPressUI @@ -114,10 +115,12 @@ fileprivate struct GravatarDefaults { extension AvatarURL { - public static func url(for email: String, - preferredSize: ImageSize? = nil, - gravatarRating: Rating? = nil, - defaultAvatarOption: DefaultAvatarOption? = .status404) -> URL? { + public static func url( + for email: String, + preferredSize: Gravatar.ImageSize? = nil, + gravatarRating: Rating? = nil, + defaultAvatarOption: DefaultAvatarOption? = .status404 + ) -> URL? { AvatarURL( with: .email(email), // Passing GravatarDefaults.imageSize to keep the previous default. diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index 075015b84353..a71ace71fbde 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -5,6 +5,7 @@ import AsyncImageKit final class LightboxImagePageViewController: UIViewController { private(set) var scrollView = LightboxImageScrollView() private let controller = ImageLoadingController() + private let siteMediaImageLoadingController = SiteMediaImageLoadingController() private let item: LightboxItem private let activityIndicator = UIActivityIndicatorView() private var errorView: UIImageView? @@ -35,6 +36,10 @@ final class LightboxImagePageViewController: UIViewController { self?.setState($0) } + siteMediaImageLoadingController.onStateChanged = { [weak self] in + self?.setState($0) + } + startFetching() } @@ -54,7 +59,7 @@ final class LightboxImagePageViewController: UIViewController { case .asset(let asset): controller.setImage(with: ImageRequest(url: asset.sourceURL, host: asset.host)) case .media(let media): - controller.setImage(with: media, size: .original) + siteMediaImageLoadingController.setImage(with: media, size: .original) } } diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaImageLoadingController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaImageLoadingController.swift new file mode 100644 index 000000000000..cc6cf8cac6b3 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Helpers/SiteMediaImageLoadingController.swift @@ -0,0 +1,44 @@ +import UIKit +import AsyncImageKit + +/// A convenience class for managing image downloads for individual views. +@MainActor +final class SiteMediaImageLoadingController { + var service: MediaImageService = .shared + var onStateChanged: (State) -> Void = { _ in } + + private(set) var task: Task? + + typealias State = ImageLoadingController.State + + deinit { + task?.cancel() + } + + func prepareForReuse() { + task?.cancel() + task = nil + } + + func setImage(with media: Media, size: MediaImageService.ImageSize) { + task?.cancel() + + if let image = service.getCachedThumbnail(for: .init(media), size: size) { + onStateChanged(.success(image)) + } else { + onStateChanged(.loading) + task = Task { @MainActor [service, weak self] in + do { + let image = try await service.image(for: media, size: size) + // This line guarantees that if you cancel on the main thread, + // none of the `onStateChanged` callbacks get called. + guard !Task.isCancelled else { return } + self?.onStateChanged(.success(image)) + } catch { + guard !Task.isCancelled else { return } + self?.onStateChanged(.failure(error)) + } + } + } + } +} diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib index 76877ca18fdc..f400684b4880 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib @@ -1,9 +1,9 @@ - + - + @@ -12,7 +12,7 @@ - + diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift index dbe0f4e51d47..bfc1b4b8fc3e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AsyncImageKit import WordPressUI protocol ReaderDetailHeaderViewDelegate: AnyObject { diff --git a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarView.swift b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarView.swift index 277d8aa1283a..5046e3b9e77b 100644 --- a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarView.swift +++ b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/AvatarView.swift @@ -1,5 +1,6 @@ import SwiftUI import Gravatar +import AsyncImageKit import DesignSystem import WordPressUI From 90b1166f3d1920690a7300dcc381b7b98964e75b Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 18:22:17 -0500 Subject: [PATCH 075/101] Fix unit tests --- .../Utility/Blogging Reminders/BloggingRemindersScheduler.swift | 2 +- .../EEUUSCompliance/CompliancePopoverViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift index 63c31026a227..d083a0e054e6 100644 --- a/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift +++ b/WordPress/Classes/Utility/Blogging Reminders/BloggingRemindersScheduler.swift @@ -17,7 +17,7 @@ extension InteractiveNotificationsManager: PushNotificationAuthorizer { /// Main interface for scheduling blogging reminders /// -final class BloggingRemindersScheduler { +class BloggingRemindersScheduler { // MARK: - Convenience Typealiases diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift index 621b79190a11..c7b15044b923 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopoverViewModel.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import WordPressUI -final class CompliancePopoverViewModel: ObservableObject { +class CompliancePopoverViewModel: ObservableObject { @Published var isAnalyticsEnabled: Bool = !WPAppAnalytics.userHasOptedOut() From 330ebaf66afb9d7f1e34082887c04d66729fdcbc Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 18:34:58 -0500 Subject: [PATCH 076/101] Cleanup MediaHost initializers --- .../Networking/MediaHost+Extensions.swift | 46 ++++--------------- WordPress/Classes/Services/MediaHelper.swift | 2 +- .../Classes/Services/MediaImageService.swift | 4 +- .../BlazeCampaignTableViewCell.swift | 4 +- .../Blaze/DashboardBlazeCampaignView.swift | 4 +- .../BlogList/SiteIconViewModel.swift | 2 +- .../RichCommentContentRenderer.swift | 5 +- .../Gutenberg/EditorMediaUtility.swift | 2 +- .../StatsLatestPostSummaryInsightsCell.swift | 4 +- 9 files changed, 18 insertions(+), 55 deletions(-) diff --git a/WordPress/Classes/Networking/MediaHost+Extensions.swift b/WordPress/Classes/Networking/MediaHost+Extensions.swift index 9525bf47677e..7e8cc6f8ebe5 100644 --- a/WordPress/Classes/Networking/MediaHost+Extensions.swift +++ b/WordPress/Classes/Networking/MediaHost+Extensions.swift @@ -1,58 +1,30 @@ import Foundation -import AsyncImageKit -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `AbstractPost`. -/// extension MediaHost { + // MARK: - MediaHost (AbstractPost) + init(_ post: AbstractPost) { - self.init(with: post.blog, failure: { error in - // We just associate a post with the underlying error for simpler debugging. - WordPressAppDelegate.crashLogging?.logError(error) - }) + self.init(post.blog) } -} - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { - enum BlogError: Swift.Error { - case baseInitializerError(error: Error) - } - - init(with blog: Blog) { - self.init(with: blog) { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - } - } - init(with blog: Blog, failure: (BlogError) -> ()) { - let isAtomic = blog.isAtomic() - self.init(with: blog, isAtomic: isAtomic, failure: failure) - } + // MARK: - MediaHost (Blog) - init(with blog: Blog, isAtomic: Bool, failure: (BlogError) -> ()) { + init(_ blog: Blog) { self.init( isAccessibleThroughWPCom: blog.isAccessibleThroughWPCom(), isPrivate: blog.isPrivate(), - isAtomic: isAtomic, + isAtomic: blog.isAtomic(), siteID: blog.dotComID?.intValue, username: blog.usernameForSite, authToken: blog.authToken, failure: { error in - // We just associate a blog with the underlying error for simpler debugging. - failure(BlogError.baseInitializerError(error: error)) + WordPressAppDelegate.crashLogging?.logError(error) } ) } -} -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { + // MARK: - MediaHost (ReaderPost) + init(_ post: ReaderPost) { let isAccessibleThroughWPCom = post.isWPCom || post.isJetpack diff --git a/WordPress/Classes/Services/MediaHelper.swift b/WordPress/Classes/Services/MediaHelper.swift index be1d0170a007..3285fddab168 100644 --- a/WordPress/Classes/Services/MediaHelper.swift +++ b/WordPress/Classes/Services/MediaHelper.swift @@ -82,7 +82,7 @@ extension Media { return configuration }()) let authenticator = MediaRequestAuthenticator() - let host = MediaHost(with: blog) + let host = MediaHost(blog) let temporaryDirectory = Media.remoteDataTemporaryDirectoryURL var output: [URL] = [] diff --git a/WordPress/Classes/Services/MediaImageService.swift b/WordPress/Classes/Services/MediaImageService.swift index 67768d200317..292ede942201 100644 --- a/WordPress/Classes/Services/MediaImageService.swift +++ b/WordPress/Classes/Services/MediaImageService.swift @@ -110,7 +110,7 @@ final class MediaImageService { } return try? await coreDataStack.performQuery { context in let blog = try context.existingObject(with: media.blogID) - return RemoteImageInfo(imageURL: remoteURL, host: MediaHost(with: blog)) + return RemoteImageInfo(imageURL: remoteURL, host: MediaHost(blog)) } } @@ -266,7 +266,7 @@ final class MediaImageService { return try? await coreDataStack.performQuery { context in let blog = try context.existingObject(with: media.blogID) guard let imageURL = media.getRemoteThumbnailURL(targetSize: targetSize, blog: blog) else { return nil } - return RemoteImageInfo(imageURL: imageURL, host: MediaHost(with: blog)) + return RemoteImageInfo(imageURL: imageURL, host: MediaHost(blog)) } } diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift index b29d2351bdb2..7f4b06eb16ca 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift @@ -107,9 +107,7 @@ final class BlazeCampaignTableViewCell: UITableViewCell, Reusable { featuredImageView.prepareForReuse() featuredImageView.isHidden = viewModel.imageURL == nil if let imageURL = viewModel.imageURL { - let host = MediaHost(with: blog, failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - }) + let host = MediaHost(blog) let preferredSize = ImageSize(scaling: CGSize(width: Metrics.featuredImageSize, height: Metrics.featuredImageSize), in: self) featuredImageView.setImage(with: imageURL, host: host, size: preferredSize) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift index e510fcb26ee6..6ee6f36d99c2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Blaze/DashboardBlazeCampaignView.swift @@ -61,9 +61,7 @@ final class DashboardBlazeCampaignView: UIView { imageView.prepareForReuse() imageView.isHidden = viewModel.imageURL == nil if let imageURL = viewModel.imageURL { - let host = MediaHost(with: blog, failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - }) + let host = MediaHost(blog) let targetSize = ImageSize(scaling: Constants.imageSize, in: self) imageView.setImage(with: imageURL, host: host, size: targetSize) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift index 903d86bb25bb..8daaa23b1785 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift @@ -34,7 +34,7 @@ struct SiteIconViewModel { if blog.hasIcon, let icon = blog.icon { self.imageURL = SiteIconViewModel.optimizedURL(for: icon, imageSize: size.size, isP2: blog.isAutomatticP2) - self.host = MediaHost(with: blog) + self.host = MediaHost(blog) } } diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift index 8c8f796324e6..1fb14ad14e7e 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift @@ -74,10 +74,7 @@ private extension RichCommentContentRenderer { var mediaHost: MediaHost { if let blog = comment.blog { - return MediaHost(with: blog, failure: { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - }) + return MediaHost(blog) } else if let post = comment.post as? ReaderPost, post.isBlogPrivate { return MediaHost(post) } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift index dca6a14bbac6..db8140d79a04 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/EditorMediaUtility.swift @@ -150,7 +150,7 @@ class EditorMediaUtility { requestURL = PhotonImageURLHelper.photonURL(with: size, forImageURL: url) } - return (requestURL, MediaHost(with: post.blog)) + return (requestURL, MediaHost(post.blog)) } static func fetchRemoteVideoURL(for media: Media, in post: AbstractPost, withToken: Bool = false, completion: @escaping ( Result<(URL), Error> ) -> Void) { diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift index 7d7f1e0c1fd8..1d60c04610df 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift @@ -231,9 +231,7 @@ class StatsLatestPostSummaryInsightsCell: StatsBaseCell, LatestPostSummaryConfig let blog = try? Blog.lookup(withID: siteID, in: ContextManager.shared.mainContext) { postImageView.isHidden = false - let host = MediaHost(with: blog, failure: { error in - DDLogError("Failed to create media host: \(error.localizedDescription)") - }) + let host = MediaHost(blog) let targetSize = CGSize(width: Metrics.thumbnailSize, height: Metrics.thumbnailSize) postImageView.setImage(with: url, host: host, size: ImageSize(scaling: targetSize, in: self)) } else { From 58ec973d8d46877445853f06a5b1ca28db04e055 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 18:35:48 -0500 Subject: [PATCH 077/101] Optimize account lookup --- WordPress/Classes/Models/WPAccount+Lookup.swift | 3 +++ WordPress/Classes/Networking/MediaHost+Extensions.swift | 1 + 2 files changed, 4 insertions(+) diff --git a/WordPress/Classes/Models/WPAccount+Lookup.swift b/WordPress/Classes/Models/WPAccount+Lookup.swift index 1ba1629f8e73..f64cf6832acd 100644 --- a/WordPress/Classes/Models/WPAccount+Lookup.swift +++ b/WordPress/Classes/Models/WPAccount+Lookup.swift @@ -47,6 +47,7 @@ public extension WPAccount { /// static func lookup(withUUIDString uuidString: String, in context: NSManagedObjectContext) throws -> WPAccount? { let fetchRequest = NSFetchRequest(entityName: WPAccount.entityName()) + fetchRequest.fetchLimit = 1 fetchRequest.predicate = NSPredicate(format: "uuid = %@", uuidString) guard let defaultAccount = try context.fetch(fetchRequest).first else { @@ -70,6 +71,7 @@ public extension WPAccount { /// static func lookup(withUsername username: String, in context: NSManagedObjectContext) throws -> WPAccount? { let fetchRequest = NSFetchRequest(entityName: WPAccount.entityName()) + fetchRequest.fetchLimit = 1 fetchRequest.predicate = NSPredicate(format: "username = [c] %@ || email = [c] %@", username, username) guard let account = try context.fetch(fetchRequest).first else { @@ -88,6 +90,7 @@ public extension WPAccount { /// static func lookup(withUserID userID: Int64, in context: NSManagedObjectContext) throws -> WPAccount? { let fetchRequest = NSFetchRequest(entityName: WPAccount.entityName()) + fetchRequest.fetchLimit = 1 fetchRequest.predicate = NSPredicate(format: "userID = %ld", userID) guard let account = try context.fetch(fetchRequest).first else { diff --git a/WordPress/Classes/Networking/MediaHost+Extensions.swift b/WordPress/Classes/Networking/MediaHost+Extensions.swift index 7e8cc6f8ebe5..197fdf34d15d 100644 --- a/WordPress/Classes/Networking/MediaHost+Extensions.swift +++ b/WordPress/Classes/Networking/MediaHost+Extensions.swift @@ -1,6 +1,7 @@ import Foundation extension MediaHost { + // MARK: - MediaHost (AbstractPost) init(_ post: AbstractPost) { From ebbeb8b36c79c71a1ac66a53a561f9abf4a1746e Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 18:47:46 -0500 Subject: [PATCH 078/101] Fix MediaHostTests --- WordPress/WordPressTest/MediaHostTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/WordPressTest/MediaHostTests.swift b/WordPress/WordPressTest/MediaHostTests.swift index 67c0b8a3a51d..bc8086638133 100644 --- a/WordPress/WordPressTest/MediaHostTests.swift +++ b/WordPress/WordPressTest/MediaHostTests.swift @@ -1,5 +1,5 @@ import Testing -import WordPressMedia +@testable import WordPress struct MediaHostTests { @Test func initializationWithPublicSite() { From 42aea778d02de08888f2ece6afeb2d1cda1b7828 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 2 Jan 2025 21:49:36 -0500 Subject: [PATCH 079/101] Fix crash in ReaderDetailFeaturedImageView --- .../AsyncImageKit/Views/AsyncImageView.swift | 4 +- .../Detail/ReaderDetailViewController.swift | 2 +- .../Views/ReaderDetailFeaturedImageView.swift | 58 ++++++++-------- .../Views/ReaderDetailFeaturedImageView.xib | 67 ------------------- .../Views/LinearGradientView.swift | 2 +- 5 files changed, 33 insertions(+), 100 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib diff --git a/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift index 4a6409b49b7c..9878957f0314 100644 --- a/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift +++ b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift @@ -53,12 +53,12 @@ public final class AsyncImageView: UIView { public override init(frame: CGRect) { super.init(frame: frame) + setupView() } public required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() + fatalError("init(coder:) has not been implemented") } private func setupView() { diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index 93fad0d4a8af..e6c907c7eb9a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift @@ -67,7 +67,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { private let activityIndicator = UIActivityIndicatorView(style: .medium) /// The actual header - private let featuredImage: ReaderDetailFeaturedImageView = .loadFromNib() + private let featuredImage = ReaderDetailFeaturedImageView() /// The actual header private lazy var header: ReaderDetailHeaderHostingView = { diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index d86c3f31387a..32b92b9207ff 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -1,5 +1,6 @@ import UIKit import AsyncImageKit +import WordPressUI protocol ReaderDetailFeaturedImageViewDelegate: AnyObject { func didTapFeaturedImage(_ sender: AsyncImageView) @@ -9,12 +10,12 @@ protocol UpdatableStatusBarStyle: UIViewController { func updateStatusBarStyle(to style: UIStatusBarStyle) } -class ReaderDetailFeaturedImageView: UIView, NibLoadable { +final class ReaderDetailFeaturedImageView: UIView { // MARK: - Constants struct Constants { - struct multipliers { + struct Multipliers { static let maxPortaitHeight: CGFloat = 0.70 static let maxPadPortaitHeight: CGFloat = 0.50 static let maxLandscapeHeight: CGFloat = 0.30 @@ -37,10 +38,9 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { // MARK: - Private: IBOutlets - @IBOutlet private weak var imageView: AsyncImageView! - @IBOutlet private weak var gradientView: UIView! - @IBOutlet private weak var heightConstraint: NSLayoutConstraint! - @IBOutlet private weak var loadingView: UIView! + private let imageView = AsyncImageView() + private let gradientView = LinearGradientView() + private lazy var heightConstraint = heightAnchor.constraint(equalToConstant: 230) // MARK: - Public: Properties @@ -127,15 +127,32 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { scrollViewObserver?.invalidate() } - override func awakeFromNib() { - super.awakeFromNib() + override init(frame: CGRect) { + super.init(frame: frame) + + translatesAutoresizingMaskIntoConstraints = false + heightConstraint.isActive = true + + gradientView.backgroundColor = UIColor.clear + gradientView.startColor = UIColor.black.withAlphaComponent(0.66) + gradientView.endColor = UIColor.clear + + addSubview(imageView) + imageView.pinEdges() + + addSubview(gradientView) + gradientView.heightAnchor.constraint(equalToConstant: 120).isActive = true + gradientView.pinEdges([.top, .horizontal]) - loadingView.backgroundColor = .placeholderText isUserInteractionEnabled = false reset() } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func viewWillDisappear() { scrollViewObserver?.invalidate() scrollViewObserver = nil @@ -192,8 +209,6 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { return } - loadingView.isHidden = false - isLoading = true isLoaded = true @@ -235,7 +250,6 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { completionHandler(size) } } - self.hideLoading() case .failure: failureHandler() } @@ -281,7 +295,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { return } - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped(_:))) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped)) tapGesture.cancelsTouchesInView = false tapGesture.delegate = self scrollView.addGestureRecognizer(tapGesture) @@ -315,15 +329,6 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { updateNavigationBar(in: scrollView) } - private func hideLoading() { - UIView.animate(withDuration: 0.3, animations: { - self.loadingView.alpha = 0.0 - }) { (_) in - self.loadingView.isHidden = true - self.loadingView.alpha = 1.0 - } - } - private func scrollViewDidScroll() { self.updateIfNotLoading() } @@ -396,8 +401,6 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { resetStatusBarStyle() heightConstraint.constant = 0 isHidden = true - - loadingView.isHidden = true } private func resetStatusBarStyle() { @@ -418,10 +421,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { // MARK: - Private: Calculations private func featuredImageHeight() -> CGFloat { - guard - let imageSize = self.imageSize, - let superview = self.superview - else { + guard let imageSize, let superview else { return 0 } @@ -429,7 +429,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { let height = bounds.width / aspectRatio let isLandscape = UIDevice.current.orientation.isLandscape - let maxHeightMultiplier: CGFloat = isLandscape ? Constants.multipliers.maxLandscapeHeight : UIDevice.isPad() ? Constants.multipliers.maxPadPortaitHeight : Constants.multipliers.maxPortaitHeight + let maxHeightMultiplier: CGFloat = isLandscape ? Constants.Multipliers.maxLandscapeHeight : UIDevice.isPad() ? Constants.Multipliers.maxPadPortaitHeight : Constants.Multipliers.maxPortaitHeight let result = min(height, superview.bounds.height * maxHeightMultiplier) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib deleted file mode 100644 index f400684b4880..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift b/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift index fa9eee077c17..b04f00867cf6 100644 --- a/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift +++ b/WordPress/Classes/ViewRelated/Views/LinearGradientView.swift @@ -1,6 +1,6 @@ import UIKit -class LinearGradientView: UIView { +final class LinearGradientView: UIView { @IBInspectable var startColor: UIColor? = nil @IBInspectable var endColor: UIColor? = nil From 14b5c56d6a2c203a83253a69a7923b66748082e5 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 07:30:59 -0500 Subject: [PATCH 080/101] Fix RTL support in WebKitViewController --- .../WebKitViewController.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift index ef059aa7c96e..977f338193ce 100644 --- a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift +++ b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift @@ -1,5 +1,3 @@ -import Foundation -import Gridicons import UIKit @preconcurrency import WebKit import WordPressShared @@ -39,7 +37,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { let analyticsSource: String? @objc lazy var backButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: UIImage.gridicon(.chevronLeft).imageFlippedForRightToLeftLayoutDirection(), + let button = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(goBack)) @@ -47,7 +45,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { return button }() @objc lazy var forwardButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: .gridicon(.chevronRight), + let button = UIBarButtonItem(image: UIImage(systemName: "chevron.forward"), style: .plain, target: self, action: #selector(goForward)) @@ -55,7 +53,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { return button }() @objc lazy var shareButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: .gridicon(.shareiOS), + let button = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(share)) @@ -63,7 +61,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { return button }() @objc lazy var safariButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: .gridicon(.globe), + let button = UIBarButtonItem(image: UIImage(systemName: "safari"), style: .plain, target: self, action: #selector(openInSafari)) @@ -72,12 +70,12 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { return button }() @objc lazy var refreshButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: .gridicon(.refresh), style: .plain, target: self, action: #selector(WebKitViewController.refresh)) + let button = UIBarButtonItem(image: UIImage(systemName: "arrow.clockwise"), style: .plain, target: self, action: #selector(WebKitViewController.refresh)) button.title = NSLocalizedString("Refresh", comment: "Button label to refres a web page") return button }() @objc lazy var closeButton: UIBarButtonItem = { - let button = UIBarButtonItem(image: .gridicon(.cross), style: .plain, target: self, action: #selector(WebKitViewController.close)) + let button = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(WebKitViewController.close)) button.title = NSLocalizedString("webKit.button.dismiss", value: "Dismiss", comment: "Verb. Dismiss the web view screen.") return button }() @@ -178,7 +176,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { let stackView = UIStackView(arrangedSubviews: [ progressView, webView - ]) + ]) stackView.axis = .vertical stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) @@ -329,6 +327,9 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { space, safariButton ] + for item in items { + item.tintColor = UIAppColor.tint + } setToolbarItems(items, animated: false) } From eb7bed63e7d383f676aa47c9ff1b5d73d327ba0d Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 07:34:49 -0500 Subject: [PATCH 081/101] Use semantic back/forward chevrons in other places --- .../Blaze Campaigns/BlazeCampaignTableViewCell.swift | 2 +- .../Classes/ViewRelated/Blog/My Site/NoSitesView.swift | 2 +- .../ViewRelated/Domains/Views/SiteDomainsView.swift | 2 +- .../Me/App Settings/DebugMenuViewController.swift | 2 +- .../ViewRelated/Post/Prepublishing/PublishButton.swift | 2 +- .../Controllers/ReaderPostActions/ReaderPostMenu.swift | 2 +- .../Reader/Sidebar/ReaderSidebarViewController.swift | 7 ++----- 7 files changed, 8 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift index 7f4b06eb16ca..d32641df8d26 100644 --- a/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Blaze Campaigns/BlazeCampaignTableViewCell.swift @@ -77,7 +77,7 @@ final class BlazeCampaignTableViewCell: UITableViewCell, Reusable { }() private lazy var chevronView: UIImageView = { - let image = UIImage(systemName: "chevron.right")?.imageFlippedForRightToLeftLayoutDirection() + let image = UIImage(systemName: "chevron.forward") let imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .separator diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift b/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift index 8e405a0e1eb8..cec681d6fcdc 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/NoSitesView.swift @@ -59,7 +59,7 @@ struct NoSitesView: View { makeGravatarIcon(size: 40) accountAndSettingsStackView Spacer() - Image(systemName: "chevron.right") + Image(systemName: "chevron.forward") .tint(.secondary) } .padding(.horizontal, 16) diff --git a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift index f4b5fea218f1..21456796e2d7 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift @@ -89,7 +89,7 @@ struct SiteDomainsView: View { Button(action: { showDetails(for: navigation) }) { HStack(alignment: .center) { AllDomainsListCardView(viewModel: row.viewModel, padding: 0) - Image(systemName: "chevron.right") + Image(systemName: "chevron.forward") .font(.subheadline.weight(.medium)) .foregroundColor(.secondary.opacity(0.5)) } diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift b/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift index 0478d76a9bc5..99cbeb3076c7 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/DebugMenuViewController.swift @@ -86,7 +86,7 @@ struct DebugMenuView: View { HStack { Text(Strings.encryptedLogging) Spacer() - Image(systemName: "chevron.right") + Image(systemName: "chevron.forward") .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary.opacity(0.5)) } diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift index e9cb2e1dfa15..dbcdbbcef882 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift @@ -87,7 +87,7 @@ struct PublishButton: View { } private var chevronUpView: some View { - Image(systemName: "chevron.right") + Image(systemName: "chevron.forward") .font(.subheadline.weight(.semibold)) .tint(Color.secondary) } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift index 4c7daee89be3..2d05fc714563 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift @@ -79,7 +79,7 @@ struct ReaderPostMenu { } private var goToBlog: UIAction { - UIAction(Strings.goToBlog, systemImage: "chevron.right") { + UIAction(Strings.goToBlog, systemImage: "chevron.forward") { guard let viewController else { return } ReaderHeaderAction().execute(post: post, origin: viewController) track(.goToBlog) diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift index 8a0cfd2d3714..f44e3b7dd6a7 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift @@ -74,7 +74,6 @@ private struct ReaderSidebarView: View { @State private var searchText = "" - @Environment(\.layoutDirection) var layoutDirection @Environment(\.editMode) var editMode var isEditing: Bool { editMode?.wrappedValue.isEditing == true } @@ -163,7 +162,7 @@ private struct ReaderSidebarView: View { .lineLimit(1) if viewModel.isCompact { Spacer() - Image(systemName: layoutDirection == .rightToLeft ? "chevron.left" : "chevron.right") + Image(systemName: "chevron.forward") .font(.system(size: 14).weight(.medium)) .foregroundStyle(.secondary.opacity(0.8)) } @@ -195,8 +194,6 @@ private struct ReaderSidebarSection: View { var isCompact: Bool @ViewBuilder var content: () -> Content - @Environment(\.layoutDirection) var layoutDirection - var body: some View { if isCompact { Button { @@ -207,7 +204,7 @@ private struct ReaderSidebarSection: View { .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) Spacer() - Image(systemName: isExpanded ? "chevron.down" : (layoutDirection == .rightToLeft ? "chevron.left" : "chevron.right")) + Image(systemName: isExpanded ? "chevron.down" : "chevron.forward") .font(.system(size: 14).weight(.semibold)) .foregroundStyle(AppColor.brand) .frame(width: 14) From 2b4ff0543bdf1aba65f2d5516eab0628eb6142e3 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 08:22:14 -0500 Subject: [PATCH 082/101] Update StatsBaseCell --- .../Stats/Insights/StatsBaseCell.swift | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift index 0d8bc1dbc408..fb69f8bacf1f 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift @@ -1,5 +1,4 @@ import UIKit -import DesignSystem class StatsBaseCell: UITableViewCell { @@ -15,23 +14,16 @@ class StatsBaseCell: UITableViewCell { }() private lazy var showDetailsButton: UIButton = { - let button = UIButton() + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: "chevron.forward") + configuration.buttonSize = .small + configuration.imagePadding = 4 + configuration.baseForegroundColor = .secondaryLabel + configuration.imagePlacement = .trailing + + let button = UIButton(configuration: configuration) button.translatesAutoresizingMaskIntoConstraints = true button.addTarget(self, action: #selector(detailsButtonTapped), for: .touchUpInside) - button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .callout) - button.titleLabel?.adjustsFontSizeToFitWidth = true - button.tintColor = .secondaryLabel - button.setTitleColor(.secondaryLabel, for: .normal) - button.setImage(UIImage.gridicon(.chevronRight).withTintColor(UIColor(color: WPStyleGuide.greyLighten20())), for: .normal) - - if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { - button.semanticContentAttribute = .forceLeftToRight - button.titleEdgeInsets = Metrics.rtlButtonTitleInsets - } else { - button.semanticContentAttribute = .forceRightToLeft - button.titleEdgeInsets = Metrics.buttonTitleInsets - } - button.accessibilityHint = LocalizedText.buttonAccessibilityHint return button }() @@ -39,7 +31,7 @@ class StatsBaseCell: UITableViewCell { private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Metrics.stackSpacing + stackView.spacing = 8 stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .equalSpacing @@ -79,7 +71,7 @@ class StatsBaseCell: UITableViewCell { NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Metrics.padding), - stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Metrics.padding), + stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0), stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Metrics.padding) ]) @@ -113,11 +105,11 @@ class StatsBaseCell: UITableViewCell { switch statSection { case .insightsViewsVisitors: - showDetailsButton.setTitle(LocalizedText.buttonTitleThisWeek, for: .normal) + showDetailsButton.configuration?.title = LocalizedText.buttonTitleThisWeek case .insightsFollowerTotals, .insightsCommentsTotals, .insightsLikesTotals: - showDetailsButton.setTitle(LocalizedText.buttonTitleViewMore, for: .normal) + showDetailsButton.configuration?.title = LocalizedText.buttonTitleViewMore default: - showDetailsButton.setTitle("", for: .normal) + showDetailsButton.configuration?.title = nil } headingWidthConstraint?.isActive = true @@ -179,11 +171,8 @@ class StatsBaseCell: UITableViewCell { } enum Metrics { - static let padding: CGFloat = .DS.Padding.double - static let bottomSpacing: CGFloat = .DS.Padding.split - static let stackSpacing: CGFloat = .DS.Padding.single - static let buttonTitleInsets = UIEdgeInsets(top: 0, left: -.DS.Padding.single, bottom: 0, right: .DS.Padding.single) - static let rtlButtonTitleInsets = UIEdgeInsets(top: 0, left: .DS.Padding.single, bottom: 0, right: -.DS.Padding.single) + static let padding: CGFloat = 16 + static let bottomSpacing: CGFloat = 12 } private enum LocalizedText { From c227ab48fc2108f0a79904a0fa174cef3f04c443 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 08:48:29 -0500 Subject: [PATCH 083/101] Update SiteStatsTableHeaderView --- .../Stats/Insights/StatsBaseCell.swift | 1 + .../SiteStatsTableHeaderView.swift | 20 ++--- .../Date Chooser/SiteStatsTableHeaderView.xib | 73 +++---------------- .../System/Notices/NoticeView.swift | 2 +- 4 files changed, 25 insertions(+), 71 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift b/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift index fb69f8bacf1f..d4511ad1e02c 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/StatsBaseCell.swift @@ -20,6 +20,7 @@ class StatsBaseCell: UITableViewCell { configuration.imagePadding = 4 configuration.baseForegroundColor = .secondaryLabel configuration.imagePlacement = .trailing + configuration.titleLineBreakMode = .byTruncatingTail let button = UIButton(configuration: configuration) button.translatesAutoresizingMaskIntoConstraints = true diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.swift index 28eba0b8f5b3..f7f22165f566 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.swift @@ -16,8 +16,6 @@ class SiteStatsTableHeaderView: UIView, NibLoadable, Accessible { @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var timezoneLabel: UILabel! - @IBOutlet weak var backArrow: UIImageView! - @IBOutlet weak var forwardArrow: UIImageView! @IBOutlet weak var bottomSeparatorLine: UIView! { didSet { bottomSeparatorLine.isGhostableDisabled = true @@ -134,6 +132,9 @@ private extension SiteStatsTableHeaderView { func applyStyles() { backgroundColor = .secondarySystemGroupedBackground + backButton.configuration = makeNavigationButtonConfiguraiton(systemImage: "chevron.backward") + forwardButton.configuration = makeNavigationButtonConfiguraiton(systemImage: "chevron.forward") + Style.configureLabelAsCellRowTitle(dateLabel) dateLabel.font = Metrics.dateLabelFont dateLabel.adjustsFontForContentSizeCategory = true @@ -151,6 +152,14 @@ private extension SiteStatsTableHeaderView { bottomSeparatorLine.backgroundColor = .separator } + private func makeNavigationButtonConfiguraiton(systemImage: String) -> UIButton.Configuration { + var configuration = UIButton.Configuration.plain() + configuration.buttonSize = .small + configuration.baseForegroundColor = .label + configuration.image = UIImage(systemName: systemImage) + return configuration + } + func displayDate() -> String? { guard let components = displayDateComponents() else { return nil @@ -261,22 +270,15 @@ private extension SiteStatsTableHeaderView { guard let date, let period else { forwardButton.isEnabled = false backButton.isEnabled = false - updateArrowStates() return } let helper = StatsPeriodHelper() forwardButton.isEnabled = helper.dateAvailableAfterDate(date, period: period, mostRecentDate: mostRecentDate) backButton.isEnabled = helper.dateAvailableBeforeDate(date, period: period, backLimit: backLimit, mostRecentDate: mostRecentDate) - updateArrowStates() prepareForVoiceOver() } - func updateArrowStates() { - forwardArrow.image = Style.imageForGridiconType(.chevronRight, withTint: (forwardButton.isEnabled ? .darkGrey : .grey)) - backArrow.image = Style.imageForGridiconType(.chevronLeft, withTint: (backButton.isEnabled ? .darkGrey : .grey)) - } - func postAccessibilityPeriodLabel() { UIAccessibility.post(notification: .screenChanged, argument: dateLabel) } diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.xib b/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.xib index d61c8fe00698..568893056aeb 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.xib +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Date Chooser/SiteStatsTableHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -16,78 +16,38 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + - - - - - - - - + + + + + + + + + + - + - - - - - - - - - - + + + - - - - - - - + - - - + + - + + + - @@ -158,7 +171,7 @@ - + @@ -190,6 +203,7 @@ Nulla sodales mauris ullamcorper massa tincidunt, eu pretium erat fringilla. Sed + @@ -197,7 +211,6 @@ Nulla sodales mauris ullamcorper massa tincidunt, eu pretium erat fringilla. Sed - @@ -216,8 +229,8 @@ Nulla sodales mauris ullamcorper massa tincidunt, eu pretium erat fringilla. Sed - + @@ -231,8 +244,8 @@ Nulla sodales mauris ullamcorper massa tincidunt, eu pretium erat fringilla. Sed - + @@ -250,7 +263,13 @@ Nulla sodales mauris ullamcorper massa tincidunt, eu pretium erat fringilla. Sed - + + + + + + + From 56ef3a78444932b9e28a0ea0ffa8b5c8402cb660 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 09:55:48 -0500 Subject: [PATCH 092/101] Modernize menus and stuff --- .../RevisionDiffsBrowserViewController.swift | 51 ++++++++----------- .../Revisions/Browser/Revisions.storyboard | 14 ++--- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/RevisionDiffsBrowserViewController.swift b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/RevisionDiffsBrowserViewController.swift index c250ed35bdac..f7f4ecc8f573 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/RevisionDiffsBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/RevisionDiffsBrowserViewController.swift @@ -18,28 +18,7 @@ class RevisionDiffsBrowserViewController: UIViewController { @IBOutlet private var previousButton: UIButton! @IBOutlet private var nextButton: UIButton! - private lazy var doneBarButtonItem: UIBarButtonItem = { - let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: nil) - doneItem.title = NSLocalizedString("Done", comment: "Label on button to dismiss revisions view") - doneItem.on() { [weak self] _ in - WPAnalytics.track(.postRevisionsDetailCancelled) - self?.dismiss(animated: true) - } - return doneItem - }() - - private lazy var moreBarButtonItem: UIBarButtonItem = { - let image = UIImage(systemName: "ellipsis") - let button = UIButton(type: .system) - button.setImage(image, for: .normal) - button.frame = CGRect(origin: .zero, size: image?.size ?? .zero) - button.accessibilityLabel = NSLocalizedString("More", comment: "Action button to display more available options") - button.on(.touchUpInside) { [weak self] _ in - self?.moreWasPressed() - } - button.setContentHuggingPriority(.required, for: .horizontal) - return UIBarButtonItem(customView: button) - }() + private lazy var moreBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: makeMoreMenu()) private lazy var loadBarButtonItem: UIBarButtonItem = { let title = NSLocalizedString("Load", comment: "Title of the screen that load selected the revisions.") @@ -111,6 +90,13 @@ class RevisionDiffsBrowserViewController: UIViewController { } } } + + // MARK: - Actions + + @objc private func buttonCloseTapped() { + WPAnalytics.track(.postRevisionsDetailCancelled) + dismiss(animated: true) + } } private extension RevisionDiffsBrowserViewController { @@ -141,7 +127,7 @@ private extension RevisionDiffsBrowserViewController { } private func setupNavbarItems() { - navigationItem.leftBarButtonItems = [doneBarButtonItem] + navigationItem.leftBarButtonItem = UIBarButtonItem(title: SharedStrings.Button.close, style: .plain, target: self, action: #selector(buttonCloseTapped)) navigationItem.rightBarButtonItems = [moreBarButtonItem, loadBarButtonItem] navigationItem.title = NSLocalizedString("Revision", comment: "Title of the screen that shows the revisions.") strokeView.backgroundColor = .separator @@ -234,14 +220,19 @@ private extension RevisionDiffsBrowserViewController { }) } - private func moreWasPressed() { - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alert.addDefaultActionWithTitle(contentPreviewState.toggle().title) { [unowned self] _ in - self.triggerPreviewState() + private func makeMoreMenu() -> UIMenu { + UIMenu(options: .displayInline, children: [ + UIDeferredMenuElement.uncached { [weak self] in + $0(self?.makeMoreMenuActions() ?? []) + } + ]) + } + + private func makeMoreMenuActions() -> [UIAction] { + let toggleMode = UIAction(title: contentPreviewState.toggle().title) { [weak self] _ in + self?.triggerPreviewState() } - alert.addCancelActionWithTitle(NSLocalizedString("Not Now", comment: "Nicer dialog answer for \"No\".")) - alert.popoverPresentationController?.barButtonItem = moreBarButtonItem - present(alert, animated: true) + return [toggleMode] } private func trackRevisionsDetailViewed(with source: ShowRevisionSource) { diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Revisions.storyboard b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Revisions.storyboard index 5648d954471a..e1df51970ded 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Revisions.storyboard +++ b/WordPress/Classes/ViewRelated/Post/Revisions/Browser/Revisions.storyboard @@ -35,14 +35,14 @@ - + - + @@ -53,6 +53,7 @@ + @@ -69,7 +71,7 @@ - + - + @@ -92,7 +94,7 @@ - + @@ -132,7 +134,7 @@ - + From 8b02a8e3048a14d4a4f022a8c104199a1e2c0d13 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 10:05:01 -0500 Subject: [PATCH 093/101] Fix MediaRequestAuthenticatorTests --- WordPress/Classes/Networking/MediaRequestAuthenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift index f0a8a646719e..c05ca6b2ab93 100644 --- a/WordPress/Classes/Networking/MediaRequestAuthenticator.swift +++ b/WordPress/Classes/Networking/MediaRequestAuthenticator.swift @@ -55,7 +55,7 @@ struct MediaRequestAuthenticator { /// authentication. /// - fail: the closure that will be called upon finding an error condition. /// - private func authenticatedRequest( + func authenticatedRequest( for url: URL, from host: MediaHost, onComplete provide: @escaping (URLRequest) -> (), From c74a857dbc29ec6b268d5d7b7dafba3ec8f76b64 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 10:54:37 -0500 Subject: [PATCH 094/101] Remove preflight connection check when sending replies (can be lagging behind) --- .../Notifications/ReplyTextView/ReplyTextView.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift index 003e65a7ff59..3659cc60322e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift @@ -147,17 +147,6 @@ import Gridicons return } - // We can't reply without an internet connection - let appDelegate = WordPressAppDelegate.shared - guard appDelegate!.connectionAvailable else { - let title = NSLocalizedString("No Connection", comment: "Title of error prompt when no internet connection is available.") - let message = NSLocalizedString("The Internet connection appears to be offline.", - comment: "Message of error prompt shown when a user tries to perform an action without an internet connection.") - WPError.showAlert(withTitle: title, message: message) - textView.resignFirstResponder() - return - } - // Load the new text let newText = textView.text textView.resignFirstResponder() From b1709b74ee89def9673597b7331b5fe12ce50631 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 11:19:11 -0500 Subject: [PATCH 095/101] Fix an issue with comments disppearing if request fails --- .../ReplyTextView/ReplyTextView.swift | 27 ++++++++++++------- .../ReplyTextView/ReplyTextView.xib | 5 ++-- .../Comments/ReaderCommentsViewController.m | 10 ++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift index 3659cc60322e..924ddcb1c32a 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift @@ -143,19 +143,14 @@ import Gridicons // MARK: - IBActions @IBAction fileprivate func btnReplyPressed() { - guard let handler = onReply else { - return - } + guard let onReply else { return } // Load the new text let newText = textView.text textView.resignFirstResponder() - // Cleanup + Shrink - text = String() - // Hit the handler - handler(newText!) + onReply(newText ?? "") } @IBAction fileprivate func btnEnterFullscreenPressed(_ sender: Any) { @@ -275,8 +270,12 @@ import Gridicons comment: "Accessibility Label for the enter full screen button on the comment reply text view") // Reply button - replyButton.setTitleColor(UIAppColor.brand, for: .normal) - replyButton.titleLabel?.text = NSLocalizedString("Reply", comment: "Reply to a comment.") + replyButton.configuration = { + var configuration = UIButton.Configuration.plain() + configuration.baseForegroundColor = UIAppColor.brand + configuration.title = NSLocalizedString("Reply", comment: "Reply to a comment.") + return configuration + }() replyButton.accessibilityIdentifier = "reply-button" replyButton.accessibilityLabel = NSLocalizedString("Reply", comment: "Accessibility label for the reply button") refreshReplyButton() @@ -297,6 +296,16 @@ import Gridicons frame.size.height = minimumHeight } + @objc func setShowingLoadingIndicator(_ isLoading: Bool) { + isUserInteractionEnabled = !isLoading + + textView.alpha = isLoading ? 0.33 : 1.0 + + replyButton.isEnabled = !isLoading + replyButton.configuration?.title = isLoading ? nil : NSLocalizedString("Reply", comment: "Reply to a comment.") + replyButton.configuration?.showsActivityIndicator = isLoading + } + // MARK: - Refresh Helpers fileprivate func refreshInterface() { refreshPlaceholder() diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib index bff11aee058d..873787813b75 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib @@ -1,9 +1,9 @@ - + - + @@ -102,7 +102,6 @@ - diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m index 2caa6da68a6d..e0610eaedf52 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m @@ -916,6 +916,9 @@ - (void)sendReplyWithNewContent:(NSString *)content NSString *successMessage = NSLocalizedString(@"Reply Sent!", @"The app successfully sent a comment"); [weakSelf displayNoticeWithTitle:successMessage message:nil]; + [weakSelf.replyTextView setShowingLoadingIndicator:NO]; + weakSelf.replyTextView.text = @""; + [weakSelf trackReplyTo:replyToComment]; [weakSelf.tableView deselectSelectedRowWithAnimation:YES]; [weakSelf refreshReplyTextViewPlaceholder]; @@ -931,11 +934,16 @@ - (void)sendReplyWithNewContent:(NSString *)content DDLogError(@"Error sending reply: %@", error); [generator notificationOccurred:UINotificationFeedbackTypeError]; NSString *message = NSLocalizedString(@"There has been an unexpected error while sending your reply", "Reply Failure Message"); - [weakSelf displayNoticeWithTitle:message message:nil]; + [weakSelf.replyTextView setShowingLoadingIndicator:NO]; + [weakSelf displayNoticeWithTitle:message message:[error localizedDescription]]; + + [weakSelf.replyTextView becomeFirstResponder]; [weakSelf refreshTableViewAndNoResultsView:NO]; }; + [self.replyTextView setShowingLoadingIndicator:YES]; + CommentService *service = [[CommentService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; if (replyToComment) { From d847e8a0d2b424a3f26d5096da5248e81b0d504d Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 11:32:21 -0500 Subject: [PATCH 096/101] Update other screens using TextView --- .../CommentDetailViewController.swift | 23 ++++++++++++++----- .../NotificationDetailsViewController.swift | 17 ++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift index 7b0a4c918050..679cfb7551e6 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentDetailViewController.swift @@ -1068,13 +1068,15 @@ private extension CommentDetailViewController { return } + replyTextView?.setShowingLoadingIndicator(true) + commentService.createReply(for: comment, content: content) { reply in self.commentService.uploadComment(reply, success: { [weak self] in - self?.displayReplyNotice(success: true) + self?.didSendReply(success: true) self?.refreshCommentReplyIfNeeded() }, failure: { [weak self] error in DDLogError("Failed uploading comment reply: \(String(describing: error))") - self?.displayReplyNotice(success: false) + self?.didSendReply(success: false, error: error) }) } } @@ -1084,21 +1086,30 @@ private extension CommentDetailViewController { return } + replyTextView?.setShowingLoadingIndicator(true) + commentService.replyToHierarchicalComment(withID: NSNumber(value: comment.commentID), post: post, content: content, success: { [weak self] in - self?.displayReplyNotice(success: true) + self?.didSendReply(success: true) self?.refreshCommentReplyIfNeeded() }, failure: { [weak self] error in DDLogError("Failed creating post comment reply: \(String(describing: error))") - self?.displayReplyNotice(success: false) + self?.didSendReply(success: false, error: error) }) } - func displayReplyNotice(success: Bool) { + func didSendReply(success: Bool, error: Error? = nil) { + replyTextView?.setShowingLoadingIndicator(false) + if success { + replyTextView?.text = "" + } else { + replyTextView?.becomeFirstResponder() + } + let message = success ? ReplyMessages.successMessage : ReplyMessages.failureMessage - displayNotice(title: message) + displayNotice(title: message, message: error?.localizedDescription) } func configureSuggestionsView() { diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift index 27a99751b05b..006525b144ff 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift @@ -417,12 +417,15 @@ extension NotificationDetailsViewController { replyTextView.accessibilityIdentifier = .replyTextViewAccessibilityId replyTextView.accessibilityLabel = NSLocalizedString("Reply Text", comment: "Notifications Reply Accessibility Identifier") replyTextView.delegate = self - replyTextView.onReply = { [weak self] content in - let group = self?.note.contentGroup(ofKind: .comment) + replyTextView.onReply = { [weak self, weak replyTextView] content in + guard let self, let replyTextView else { + return + } + let group = self.note.contentGroup(ofKind: .comment) guard let block: FormattableCommentContent = group?.blockOfKind(.comment) else { return } - self?.replyCommentWithBlock(block, content: content) + self.replyCommentWithBlock(block, content: content, textView: replyTextView) } replyTextView.setContentCompressionResistancePriority(.required, for: .vertical) @@ -1085,26 +1088,30 @@ private extension NotificationDetailsViewController { _ = navigationController?.popToRootViewController(animated: true) } - func replyCommentWithBlock(_ block: FormattableCommentContent, content: String) { + func replyCommentWithBlock(_ block: FormattableCommentContent, content: String, textView: ReplyTextView) { guard let replyAction = block.action(id: ReplyToCommentAction.actionIdentifier()) else { return } let generator = UINotificationFeedbackGenerator() generator.prepare() - generator.notificationOccurred(.success) let actionContext = ActionContext(block: block, content: content) { [weak self] (request, success) in + textView.setShowingLoadingIndicator(false) if success { + generator.notificationOccurred(.success) WPAppAnalytics.track(.notificationsCommentRepliedTo) + textView.text = "" let message = NSLocalizedString("Reply Sent!", comment: "The app successfully sent a comment") self?.displayNotice(title: message) } else { generator.notificationOccurred(.error) + textView.becomeFirstResponder() self?.displayReplyError(with: block, content: content) } } + textView.setShowingLoadingIndicator(true) replyAction.execute(context: actionContext) } From cb47b04e2310678152c90998be1b725071bc70e0 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 12:05:16 -0500 Subject: [PATCH 097/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0be968bb772f..d099e643848f 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -11,6 +11,7 @@ * [*] Fix layout issues in Privacy Settings section of App Settings [#23936] * [*] Fix incorrect chevron icons direction in RTL languages [#23940] * [*] Fix an issue with clear navigation bar background in revision browser [#23941] +* [*] Fix an issue with comments being lost on request failure [#23942] 25.6 ----- From ccbdbfbd082ef564b487650c4ff602d7d46cc422 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 12:19:22 -0500 Subject: [PATCH 098/101] Fix formatting --- .../SiteStatsInsightsDetailsViewModel.swift | 123 ++++++++++++------ 1 file changed, 80 insertions(+), 43 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift index 17cb531c2a2c..5a50c435f68a 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift @@ -274,12 +274,16 @@ class SiteStatsInsightsDetailsViewModel: Observable { // Views Visitors let weekEnd = futureEndOfWeekDate(for: periodSummary) - rows.append(contentsOf: SiteStatsImmuTableRows.viewVisitorsImmuTableRows(periodSummary, - selectedSegment: selectedViewsVisitorsSegment, - periodDate: selectedDate!, - periodEndDate: weekEnd, - siteStatsInsightsDelegate: nil, - viewsAndVisitorsDelegate: viewsAndVisitorsDelegate)) + rows.append( + contentsOf: SiteStatsImmuTableRows.viewVisitorsImmuTableRows( + periodSummary, + selectedSegment: selectedViewsVisitorsSegment, + periodDate: selectedDate!, + periodEndDate: weekEnd, + siteStatsInsightsDelegate: nil, + viewsAndVisitorsDelegate: viewsAndVisitorsDelegate + ) + ) // Referrers if let referrers = viewsAndVisitorsData.topReferrers { @@ -287,13 +291,16 @@ class SiteStatsInsightsDetailsViewModel: Observable { let chartViewModel = StatsReferrersChartViewModel(referrers: referrers) let chartView: UIView? = referrers.totalReferrerViewsCount > 0 ? chartViewModel.makeReferrersChartView() : nil - var referrersRow = TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodReferrers.itemSubtitle, - dataSubtitle: StatSection.periodReferrers.dataSubtitle, - dataRows: referrersData, - statSection: StatSection.periodReferrers, - siteStatsPeriodDelegate: nil, //TODO - look at if I need to be not null - siteStatsReferrerDelegate: nil, - siteStatsInsightsDetailsDelegate: insightsDetailsDelegate) + var referrersRow = TopTotalsPeriodStatsRow( + itemSubtitle: StatSection.periodReferrers.itemSubtitle, + dataSubtitle: StatSection.periodReferrers.dataSubtitle, + dataRows: referrersData, + statSection: StatSection.periodReferrers, + siteStatsPeriodDelegate: nil, + //TODO - look at if I need to be not null + siteStatsReferrerDelegate: nil, + siteStatsInsightsDetailsDelegate: insightsDetailsDelegate + ) referrersRow.topAccessoryView = chartView rows.append(referrersRow) } @@ -304,12 +311,18 @@ class SiteStatsInsightsDetailsViewModel: Observable { if isMapShown { rows.append(CountriesMapRow(countriesMap: map, statSection: .periodCountries)) } - rows.append(CountriesStatsRow(itemSubtitle: StatSection.periodCountries.itemSubtitle, - dataSubtitle: StatSection.periodCountries.dataSubtitle, - statSection: isMapShown ? nil : .periodCountries, - dataRows: countriesRowData(topCountries: viewsAndVisitorsData.topCountries), - siteStatsPeriodDelegate: nil, - siteStatsInsightsDetailsDelegate: insightsDetailsDelegate)) + rows.append( + CountriesStatsRow( + itemSubtitle: StatSection.periodCountries.itemSubtitle, + dataSubtitle: StatSection.periodCountries.dataSubtitle, + statSection: isMapShown ? nil : .periodCountries, + dataRows: countriesRowData( + topCountries: viewsAndVisitorsData.topCountries + ), + siteStatsPeriodDelegate: nil, + siteStatsInsightsDetailsDelegate: insightsDetailsDelegate + ) + ) return rows } @@ -326,29 +339,42 @@ class SiteStatsInsightsDetailsViewModel: Observable { let emailFollowersCount = insightsStore.getEmailFollowers()?.emailFollowersCount ?? 0 if dotComFollowersCount > 0 || emailFollowersCount > 0 { - let chartViewModel = StatsFollowersChartViewModel(dotComFollowersCount: dotComFollowersCount, - emailFollowersCount: emailFollowersCount) + let chartViewModel = StatsFollowersChartViewModel( + dotComFollowersCount: dotComFollowersCount, + emailFollowersCount: emailFollowersCount + ) let chartView: UIView = chartViewModel.makeFollowersChartView() - var chartRow = TopTotalsPeriodStatsRow(itemSubtitle: "", - dataSubtitle: "", - dataRows: followersRowData(dotComFollowersCount: dotComFollowersCount, - emailFollowersCount: emailFollowersCount, - totalCount: dotComFollowersCount + emailFollowersCount), - statSection: StatSection.insightsFollowersWordPress, - siteStatsPeriodDelegate: nil, //TODO - look at if I need to be not null - siteStatsReferrerDelegate: nil) + var chartRow = TopTotalsPeriodStatsRow( + itemSubtitle: "", + dataSubtitle: "", + dataRows: followersRowData( + dotComFollowersCount: dotComFollowersCount, + emailFollowersCount: emailFollowersCount, + totalCount: dotComFollowersCount + emailFollowersCount + ), + statSection: StatSection.insightsFollowersWordPress, + siteStatsPeriodDelegate: nil, + //TODO - look at if I need to be not null + siteStatsReferrerDelegate: nil + ) chartRow.topAccessoryView = chartView rows.append(chartRow) } - rows.append(TabbedTotalsStatsRow(tabsData: [tabDataForFollowerType(.insightsFollowersWordPress), - tabDataForFollowerType(.insightsFollowersEmail)], + rows.append( + TabbedTotalsStatsRow( + tabsData: [ + tabDataForFollowerType(.insightsFollowersWordPress), + tabDataForFollowerType(.insightsFollowersEmail) + ], statSection: .insightsFollowersWordPress, siteStatsInsightsDelegate: insightsDetailsDelegate, siteStatsDetailsDelegate: detailsDelegate, - showTotalCount: false)) + showTotalCount: false + ) + ) return rows } case .insightsLikesTotals: @@ -358,21 +384,32 @@ class SiteStatsInsightsDetailsViewModel: Observable { let likesTotalsData = revampStore.getLikesTotalsData() if let summary = likesTotalsData.summary { - rows.append(TotalInsightStatsRow(dataRow: createLikesTotalInsightsRow(periodSummary: summary), - statSection: statSection, - siteStatsInsightsDelegate: nil) + rows.append( + TotalInsightStatsRow( + dataRow: createLikesTotalInsightsRow( + periodSummary: summary + ), + statSection: statSection, + siteStatsInsightsDelegate: nil + ) ) } if let topPostsAndPages = likesTotalsData.topPostsAndPages { - rows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodPostsAndPages.itemSubtitle, - dataSubtitle: StatSection.periodPostsAndPages.dataSubtitle, - dataRows: postsAndPagesRowData(topPostsAndPages: topPostsAndPages), - statSection: StatSection.periodPostsAndPages, - siteStatsPeriodDelegate: nil, - siteStatsReferrerDelegate: nil, - siteStatsInsightsDetailsDelegate: insightsDetailsDelegate, - siteStatsDetailsDelegate: detailsDelegate)) + rows.append( + TopTotalsPeriodStatsRow( + itemSubtitle: StatSection.periodPostsAndPages.itemSubtitle, + dataSubtitle: StatSection.periodPostsAndPages.dataSubtitle, + dataRows: postsAndPagesRowData( + topPostsAndPages: topPostsAndPages + ), + statSection: StatSection.periodPostsAndPages, + siteStatsPeriodDelegate: nil, + siteStatsReferrerDelegate: nil, + siteStatsInsightsDetailsDelegate: insightsDetailsDelegate, + siteStatsDetailsDelegate: detailsDelegate + ) + ) } return rows From 13bcd399e8205e4e7d91a5b2ac269ee07e310a59 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 12:35:49 -0500 Subject: [PATCH 099/101] Fix an issue with referrers showing invalid icons --- .../SiteStatsPeriodViewModel.swift | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index 11d1e4eb2812..4f84a6d10334 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -502,27 +502,19 @@ private extension SiteStatsPeriodViewModel { let referrers = store.getTopReferrers()?.referrers.prefix(10) ?? [] func rowDataFromReferrer(referrer: StatsReferrer) -> StatsTotalRowData { - var icon: UIImage? = nil - var iconURL: URL? = nil - - switch referrer.iconURL?.lastPathComponent { - case "search-engine.png": - icon = Style.imageForGridiconType(.search) - case nil: - icon = Style.imageForGridiconType(.globe) - default: - iconURL = referrer.iconURL - } - - return StatsTotalRowData(name: referrer.title, - data: referrer.viewsCount.abbreviatedString(), - icon: icon, - socialIconURL: iconURL, - showDisclosure: true, - disclosureURL: referrer.url, - childRows: referrer.children.map { rowDataFromReferrer(referrer: $0) }, - statSection: .periodReferrers, - isReferrerSpam: referrer.isSpam) + return StatsTotalRowData( + name: referrer.title, + data: referrer.viewsCount.abbreviatedString(), + icon: nil, + socialIconURL: nil, + showDisclosure: true, + disclosureURL: referrer.url, + childRows: referrer.children.map { + rowDataFromReferrer(referrer: $0) + }, + statSection: .periodReferrers, + isReferrerSpam: referrer.isSpam + ) } return referrers.map { rowDataFromReferrer(referrer: $0) } From 2f22b2f6beb54703b3fdce06f7ea1e0f280c6b9d Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 12:37:27 -0500 Subject: [PATCH 100/101] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d099e643848f..0fb781f035fd 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -12,6 +12,7 @@ * [*] Fix incorrect chevron icons direction in RTL languages [#23940] * [*] Fix an issue with clear navigation bar background in revision browser [#23941] * [*] Fix an issue with comments being lost on request failure [#23942] +* [*] Fix an issue with Referrers in Stats showing invalid icons [#23943] 25.6 ----- From 4c2f229391fa232f0c981c0709a10c821cd11c69 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 3 Jan 2025 19:20:32 -0500 Subject: [PATCH 101/101] Remove some of the scenarios where isInternetConnected used --- WordPress/Classes/Utility/WPError.m | 14 ++++++-------- .../Blog/Blog Details/BlogDetailsViewController.m | 5 ----- .../Comments/Controllers/CommentsViewController.m | 6 ------ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/WordPress/Classes/Utility/WPError.m b/WordPress/Classes/Utility/WPError.m index 881591ff5f06..3d2d3e11fccd 100644 --- a/WordPress/Classes/Utility/WPError.m +++ b/WordPress/Classes/Utility/WPError.m @@ -131,15 +131,13 @@ + (void)showAlertWithTitle:(NSString *)title message:(NSString *)message withSup [alertController addAction:action]; // Add the 'Need help' button only if internet is accessible (i.e. if the user can actually get help). - if (showSupport && ReachabilityUtils.isInternetReachable) { + if (showSupport) { NSString *supportText = NSLocalizedString(@"Need Help?", @"'Need help?' button label, links off to the WP for iOS FAQ."); - UIAlertAction *action = [UIAlertAction actionWithTitle:supportText - style:UIAlertActionStyleCancel - handler:^(UIAlertAction * _Nonnull __unused action) { - SupportTableViewController *supportVC = [[SupportTableViewController alloc] init]; - [supportVC showFromTabBar]; - [WPError internalInstance].alertShowing = NO; - }]; + UIAlertAction *action = [UIAlertAction actionWithTitle:supportText style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull __unused action) { + SupportTableViewController *supportVC = [[SupportTableViewController alloc] init]; + [supportVC showFromTabBar]; + [WPError internalInstance].alertShowing = NO; + }]; [alertController addAction:action]; } [alertController presentFromRootViewController]; diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index 33a2f1bca520..33b8dc9d72e0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -1951,11 +1951,6 @@ - (void)showViewSiteFromSource:(BlogDetailsNavigationSource)source - (void)showViewAdmin { - if (![ReachabilityUtils isInternetReachable]) { - [ReachabilityUtils showAlertNoInternetConnection]; - return; - } - [WPAppAnalytics track:WPAnalyticsStatOpenedViewAdmin withBlog:self.blog]; NSString *dashboardUrl; diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentsViewController.m b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentsViewController.m index fd7bca9f6dad..b28450406825 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/CommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/CommentsViewController.m @@ -552,12 +552,6 @@ - (BOOL)contentIsEmpty - (void)refreshAndSyncWithInteraction { - if (!ReachabilityUtils.isInternetReachable) { - [self refreshPullToRefresh]; - [self refreshNoConnectionView]; - return; - } - [self.syncHelper syncContentWithUserInteraction]; }