From 58127605554fce020f7fc2de670f17c684f516f8 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 09:46:33 -0500 Subject: [PATCH 001/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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/193] 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]; } From 565a34b31b0b32bb67c0aa5d3e58f515accd995c Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 10:10:24 -0500 Subject: [PATCH 102/193] Update site menu style on iPhone --- .../DashboardQuickActionsCardCell.swift | 23 ++++++------------- .../Blog Details/BlogDetailsViewController.m | 16 ++++++++----- .../Sidebar/SiteMenuViewController.swift | 6 +++-- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift index 216994cf126f..08fd952c4162 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift @@ -22,7 +22,6 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab private var items: [DashboardQuickActionItemViewModel] = [] private var viewModel: DashboardQuickActionsViewModel? private weak var parentViewController: BlogDashboardViewController? - private weak var blogDetailsViewController: BlogDetailsViewController? private var cancellables: [AnyCancellable] = [] override init(frame: CGRect) { @@ -109,13 +108,9 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab trackQuickActionsEvent(.statsAccessed, blog: blog) StatsViewController.show(for: blog, from: parentViewController) case .more: - let viewController = BlogDetailsViewController() - viewController.isScrollEnabled = true - viewController.tableView.isScrollEnabled = true - viewController.blog = blog - viewController.presentationDelegate = self - self.blogDetailsViewController = viewController - self.parentViewController?.show(viewController, sender: nil) + let viewController = SiteMenuViewController(blog: blog) + viewController.delegate = self + parentViewController.show(viewController, sender: nil) } } @@ -124,15 +119,11 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab } } -// MARK: - DashboardQuickActionsCardCell (BlogDetailsPresentationDelegate) +// MARK: - DashboardQuickActionsCardCell (SiteMenuViewControllerDelegate) -extension DashboardQuickActionsCardCell: BlogDetailsPresentationDelegate { - func showBlogDetailsSubsection(_ subsection: BlogDetailsSubsection) { - self.blogDetailsViewController?.showDetailView(for: subsection) - } - - func presentBlogDetailsViewController(_ viewController: UIViewController) { - self.blogDetailsViewController?.show(viewController, sender: nil) +extension DashboardQuickActionsCardCell: SiteMenuViewControllerDelegate { + func siteMenuViewController(_ siteMenuViewController: SiteMenuViewController, showDetailsViewController viewController: UIViewController) { + siteMenuViewController.show(viewController, sender: nil) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index 33b8dc9d72e0..6ce4f52196e1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -323,6 +323,10 @@ - (void)viewDidLoad if (@available(iOS 17.0, *)) { [self registerForTraitChanges:@[[UITraitHorizontalSizeClass self]] withAction:@selector(handleTraitChanges)]; } + + if (self.isSidebarModeEnabled && ![self isSplitViewDisplayed]) { + self.tableView.backgroundColor = [UIColor systemBackgroundColor]; + } } - (void)viewWillAppear:(BOOL)animated @@ -1038,7 +1042,7 @@ - (void)configureTableViewData } - (Boolean)isSplitViewDisplayed { - return self.isSidebarModeEnabled; + return self.splitViewController != nil; } /// This section is available on Jetpack only. @@ -1053,7 +1057,7 @@ - (BlogDetailsSection *)contentSectionViewModel [rows addObject:[self mediaRow]]; [rows addObject:[self commentsRow]]; - NSString *title = self.isSidebarModeEnabled ? nil : [BlogDetailsViewControllerStrings contentSectionTitle]; + NSString *title = [self isSplitViewDisplayed] ? nil : [BlogDetailsViewControllerStrings contentSectionTitle]; return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryContent]; } @@ -1582,7 +1586,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [WPStyleGuide configureTableViewDestructiveActionCell:cell]; } else { if (row.showsDisclosureIndicator) { - cell.accessoryType = [self isSplitViewDisplayed] ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; + cell.accessoryType = [self isSidebarModeEnabled] ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; } else { cell.accessoryType = UITableViewCellAccessoryNone; } @@ -1713,7 +1717,7 @@ - (void)showCommentsFromSource:(BlogDetailsNavigationSource)source CommentsViewController *commentsVC = [CommentsViewController controllerWithBlog:self.blog]; commentsVC.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; - if (self.isSidebarModeEnabled) { + if ([self isSplitViewDisplayed]) { commentsVC.isSidebarModeEnabled = YES; UISplitViewController *splitVC = [[UISplitViewController alloc] initWithStyle:UISplitViewControllerStyleDoubleColumn]; @@ -1795,7 +1799,7 @@ - (void)showSettingsFromSource:(BlogDetailsNavigationSource)source [self trackEvent:WPAnalyticsStatOpenedSiteSettings fromSource:source]; SiteSettingsViewController *controller = [[SiteSettingsViewController alloc] initWithBlog:self.blog]; controller.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; - if (self.isSidebarModeEnabled) { + if ([self isSplitViewDisplayed]) { UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller]; __weak BlogDetailsViewController *weakSelf = self; #pragma clang diagnostic push @@ -1863,7 +1867,7 @@ - (void)showStatsFromSource:(BlogDetailsNavigationSource)source - (void)showDashboard { - if (self.isSidebarModeEnabled) { + if ([self isSplitViewDisplayed]) { MySiteViewController *controller = [MySiteViewController makeForBlog:self.blog isSidebarModeEnabled:true]; [self.presentationDelegate presentBlogDetailsViewController:controller]; } else { diff --git a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift index 9a4c9a08b056..987b4e357a47 100644 --- a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift +++ b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift @@ -37,7 +37,9 @@ final class SiteMenuViewController: UIViewController { blogDetailsVC.view.translatesAutoresizingMaskIntoConstraints = false view.pinSubviewToAllEdges(blogDetailsVC.view) - blogDetailsVC.showInitialDetailsForBlog() + if splitViewController != nil { + blogDetailsVC.showInitialDetailsForBlog() + } navigationItem.title = blog.settings?.name ?? (blog.displayURL as String?) ?? "" @@ -65,7 +67,7 @@ final class SiteMenuViewController: UIViewController { super.viewDidAppear(animated) if #available(iOS 17, *) { - if tipObserver == nil { + if tipObserver == nil && splitViewController != nil { tipObserver = registerTipPopover(AppTips.SidebarTip(), sourceItem: getTipAnchor(), arrowDirection: [.up]) } } From c02b29bcc8a5e284eafdc852dfdce23dc13cc01d Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 10:13:47 -0500 Subject: [PATCH 103/193] Update release notes --- RELEASE-NOTES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0fb781f035fd..47b8af094eed 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -13,6 +13,8 @@ * [*] 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] +* [*] Update site menu style on iPhone [#23944] + 25.6 ----- From 26058c7f82575c99771215233006f6600a2e49a3 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 10:56:25 -0500 Subject: [PATCH 104/193] Integrate zoom transitions in Theme browser --- .../Themes/ThemeBrowserViewController.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 7d37c1187616..5228e9a640e3 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -850,7 +850,13 @@ public protocol ThemePresenter: AnyObject { presentUrlForTheme(theme, url: theme?.viewUrl(), onClose: onWebkitViewControllerClose) } - @objc open func presentUrlForTheme(_ theme: Theme?, url: String?, activeButton: Bool = true, modalStyle: UIModalPresentationStyle = .pageSheet, onClose: (() -> Void)? = nil) { + @objc open func presentUrlForTheme( + _ theme: Theme?, + url: String?, + activeButton: Bool = true, + modalStyle: UIModalPresentationStyle = .pageSheet, + onClose: (() -> Void)? = nil + ) { guard let theme, let url = url.flatMap(URL.init(string:)) else { return } @@ -870,8 +876,14 @@ public protocol ThemePresenter: AnyObject { let webViewController = WebViewControllerFactory.controller(configuration: configuration, source: "theme_browser") webViewController.navigationItem.rightBarButtonItem = activateButton + let navigation = UINavigationController(rootViewController: webViewController) navigation.modalPresentationStyle = modalStyle + if #available(iOS 18, *), let indexPath = collectionView.indexPathsForSelectedItems?.first { + navigation.preferredTransition = .zoom(sourceViewProvider: { [weak self] _ in + self?.collectionView.cellForItem(at: indexPath)?.contentView + }) + } if searchController != nil && searchController.isActive { searchController.dismiss(animated: true, completion: { From babc5299e2345b74688c8dc20b6d151974781a17 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 10:58:43 -0500 Subject: [PATCH 105/193] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 47b8af094eed..e340975dad7f 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -14,6 +14,7 @@ * [*] Fix an issue with comments being lost on request failure [#23942] * [*] Fix an issue with Referrers in Stats showing invalid icons [#23943] * [*] Update site menu style on iPhone [#23944] +* [*] Integrate zoom transitions in Themes [#23945] 25.6 From af9dd41b33182dc0bfe16a0767277d1fb9fb9cbf Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 13:00:56 -0500 Subject: [PATCH 106/193] Fix tint colors in wpios --- .../Colors and Styles/WPStyleGuide+ApplicationStyles.swift | 6 +++--- WordPress/Classes/Utility/App Configuration/AppColor.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift index 4d7bbf291541..41de9ac06460 100644 --- a/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift +++ b/WordPress/Classes/Extensions/Colors and Styles/WPStyleGuide+ApplicationStyles.swift @@ -13,8 +13,8 @@ extension WPStyleGuide { // looking the same on newer versions of iOS. UIStackView.appearance().backgroundColor = .clear - UIWindow.appearance().tintColor = UIAppColor.brand - UISwitch.appearance().onTintColor = UIAppColor.brand + UIWindow.appearance().tintColor = UIAppColor.primary + UISwitch.appearance().onTintColor = UIAppColor.primary UITableView.appearance().sectionHeaderTopPadding = 0 @@ -164,7 +164,7 @@ extension WPStyleGuide { @objc class func configureTableViewActionCell(_ cell: UITableViewCell?) { configureTableViewCell(cell) - cell?.textLabel?.textColor = UIAppColor.brand + cell?.textLabel?.textColor = UIAppColor.primary } @objc diff --git a/WordPress/Classes/Utility/App Configuration/AppColor.swift b/WordPress/Classes/Utility/App Configuration/AppColor.swift index d55f47c19ad4..49ffaedc67a6 100644 --- a/WordPress/Classes/Utility/App Configuration/AppColor.swift +++ b/WordPress/Classes/Utility/App Configuration/AppColor.swift @@ -102,7 +102,7 @@ struct UIAppColor { #endif #if IS_WORDPRESS - static let tint = brand + static let tint = primary static let brand = CSColor.WordPressBlue.base @@ -142,7 +142,7 @@ struct UIAppColor { static let prologueBackground = UIColor(light: blue(.shade0), dark: .systemBackground) - static let switchStyle: SwitchToggleStyle = SwitchToggleStyle(tint: Color(UIAppColor.brand)) + static let switchStyle: SwitchToggleStyle = SwitchToggleStyle(tint: Color(UIAppColor.primary)) } struct AppColor { From ff839118964e622e1daf8d5fb8aeed6256dfc746 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 13:25:27 -0500 Subject: [PATCH 107/193] Remove UIAppColor.brand --- .../Utility/App Configuration/AppColor.swift | 18 +++--------------- .../ZendeskAttachmentsSection.swift | 2 +- .../TwitterDeprecationTableFooterView.swift | 2 +- .../Detail/CommentContentTableViewCell.swift | 2 +- .../Domains/Views/SiteDomainsView.swift | 2 +- .../EEUUSCompliance/CompliancePopover.swift | 4 ++-- .../View/JetpackRemoteInstallStateView.swift | 4 ++-- .../Views/DomainPurchaseChoicesView.swift | 6 +++--- .../BooleanUserDefaultsDebugView.swift | 2 +- .../NotificationTableViewCell.swift | 2 +- .../ReplyTextView/ReplyTextView.swift | 2 +- .../PublishDatePickerViewController.swift | 2 +- .../Post/Views/PostMediaUploadsView.swift | 2 +- .../Reader/Cards/ReaderPostCell.swift | 2 +- .../Cards/ReaderRecommendedSitesCell.swift | 4 ++-- .../ReaderSearchSuggestionsView.swift | 2 +- .../Sidebar/ReaderSidebarTagsSection.swift | 4 ++-- .../Sidebar/ReaderSidebarViewController.swift | 4 ++-- .../Style/WPStyleGuide+ReaderComments.swift | 2 +- .../Subscriptions/ReaderSubscriptionCell.swift | 4 ++-- .../Countries/Map/CountriesMapView.swift | 4 ++-- .../System/Sidebar/SidebarViewController.swift | 4 ++-- .../NotificationsTableViewCellContent.swift | 2 +- .../Voice/AudioRecorderVisualizerView.swift | 2 +- .../ViewRelated/Voice/VoiceToContentView.swift | 4 ++-- 25 files changed, 38 insertions(+), 50 deletions(-) diff --git a/WordPress/Classes/Utility/App Configuration/AppColor.swift b/WordPress/Classes/Utility/App Configuration/AppColor.swift index 49ffaedc67a6..5a4a1d3679ec 100644 --- a/WordPress/Classes/Utility/App Configuration/AppColor.swift +++ b/WordPress/Classes/Utility/App Configuration/AppColor.swift @@ -88,13 +88,7 @@ struct UIAppColor { #if IS_JETPACK static let tint = UIColor.label - static let brand = UIColor(light: CSColor.JetpackGreen.shade(.shade40), dark: CSColor.JetpackGreen.shade(.shade30)) - - static func brand(_ shade: ColorStudioShade) -> UIColor { - CSColor.JetpackGreen.shade(shade) - } - - static let primary = CSColor.JetpackGreen.base + static let primary = UIColor(light: CSColor.JetpackGreen.shade(.shade40), dark: CSColor.JetpackGreen.shade(.shade30)) static func primary(_ shade: ColorStudioShade) -> UIColor { CSColor.JetpackGreen.shade(shade) @@ -104,13 +98,7 @@ struct UIAppColor { #if IS_WORDPRESS static let tint = primary - static let brand = CSColor.WordPressBlue.base - - static func brand(_ shade: ColorStudioShade) -> UIColor { - CSColor.WordPressBlue.shade(shade) - } - - static let primary = CSColor.Blue.base + static let primary = UIColor(light: CSColor.Blue.base, dark: primary(.shade40)) static func primary(_ shade: ColorStudioShade) -> UIColor { CSColor.Blue.shade(shade) @@ -147,5 +135,5 @@ struct UIAppColor { struct AppColor { static let tint = Color(UIAppColor.tint) - static let brand = Color(UIAppColor.brand) + static let primary = Color(UIAppColor.primary) } diff --git a/WordPress/Classes/Utility/In-App Feedback/ZendeskAttachmentsSection.swift b/WordPress/Classes/Utility/In-App Feedback/ZendeskAttachmentsSection.swift index b23a936fbf67..d01b8d658a23 100644 --- a/WordPress/Classes/Utility/In-App Feedback/ZendeskAttachmentsSection.swift +++ b/WordPress/Classes/Utility/In-App Feedback/ZendeskAttachmentsSection.swift @@ -62,7 +62,7 @@ struct ZendeskAttachmentsSection: View { Image(systemName: "paperclip") Text(Strings.addAttachment) } - .foregroundStyle(Color(uiColor: UIAppColor.brand)) + .foregroundStyle(Color(uiColor: UIAppColor.primary)) } .onChange(of: selection, perform: viewModel.process) } diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift index 22236f729dc8..304258e36e66 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift @@ -27,7 +27,7 @@ let hyperlinkText = NSAttributedString(string: Constants.hyperlinkText, attributes: [ .paragraphStyle: paragraphStyle, .attachment: attachmentURL, - .foregroundColor: UIAppColor.brand + .foregroundColor: UIAppColor.primary ]) attributedString.append(hyperlinkText) } diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift index 50e721c35868..89acdcacf975 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/CommentContentTableViewCell.swift @@ -81,7 +81,7 @@ class CommentContentTableViewCell: UITableViewCell, NibReusable { @objc var isReplyHighlighted: Bool = false { didSet { - replyButton.tintColor = isReplyHighlighted ? UIAppColor.brand : .label + replyButton.tintColor = isReplyHighlighted ? UIAppColor.primary : .label replyButton.configuration?.image = UIImage(systemName: isReplyHighlighted ? "arrowshape.turn.up.left.fill" : "arrowshape.turn.up.left") } } diff --git a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift index 21456796e2d7..6ba2c30724e2 100644 --- a/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift +++ b/WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift @@ -130,7 +130,7 @@ struct SiteDomainsView: View { } label: { Text(TextContent.additionalDomainTitle(blog.canRegisterDomainWithPaidPlan)) .style(TextStyle.bodyMedium(.regular)) - .foregroundColor(AppColor.brand) + .foregroundColor(AppColor.primary) } } } diff --git a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift index f9d5a224b3a8..0eb19455c14a 100644 --- a/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift +++ b/WordPress/Classes/ViewRelated/EEUUSCompliance/CompliancePopover.swift @@ -60,7 +60,7 @@ struct CompliancePopover: View { .font(.body) } } - .foregroundColor(AppColor.brand) + .foregroundColor(AppColor.primary) .frame(height: 44) } @@ -70,7 +70,7 @@ struct CompliancePopover: View { }) { ZStack { RoundedRectangle(cornerRadius: 8) - .fill(AppColor.brand) + .fill(AppColor.primary) Text(Strings.saveButtonTitle) .font(.body) } diff --git a/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallStateView.swift b/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallStateView.swift index d1c67eb7cff5..73256c2b594f 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallStateView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Install/View/JetpackRemoteInstallStateView.swift @@ -128,7 +128,7 @@ private extension JetpackRemoteInstallStateView { } struct MainButton { - static let normalBackground = UIImage.renderBackgroundImage(fill: UIAppColor.brand) + static let normalBackground = UIImage.renderBackgroundImage(fill: UIAppColor.primary) static let loadingBackground = UIImage.renderBackgroundImage(fill: UIAppColor.jetpackGreen(.shade70)) static let titleColor = UIColor.white static let font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .semibold) @@ -136,7 +136,7 @@ private extension JetpackRemoteInstallStateView { } struct SupportButton { - static let color = UIAppColor.brand + static let color = UIAppColor.primary static let font = WPStyleGuide.fontForTextStyle(.body) static let text = NSLocalizedString("Contact Support", comment: "Contact Support button title") } diff --git a/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift b/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift index 066a82beaa50..9f446d6fe50c 100644 --- a/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift +++ b/WordPress/Classes/ViewRelated/Me/All Domains/Views/DomainPurchaseChoicesView.swift @@ -85,7 +85,7 @@ struct DomainPurchaseChoicesView: View { Image(imageName) .renderingMode(.template) .resizable() - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) .frame(width: Constants.imageLength, height: Constants.imageLength) .padding(.top, .DS.Padding.double) VStack(alignment: .leading, spacing: .DS.Padding.single) { @@ -95,7 +95,7 @@ struct DomainPurchaseChoicesView: View { .foregroundStyle(.secondary) if let footer { Text(footer) - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) .font(.body.bold()) } } @@ -122,7 +122,7 @@ struct DomainPurchaseChoicesView: View { Text(Strings.chooseSiteSubtitle) .foregroundStyle(Color(.secondaryLabel)) Text(Strings.chooseSiteFooter) - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) } } diff --git a/WordPress/Classes/ViewRelated/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugView.swift b/WordPress/Classes/ViewRelated/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugView.swift index 1195f1f73deb..966ae873f066 100644 --- a/WordPress/Classes/ViewRelated/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugView.swift +++ b/WordPress/Classes/ViewRelated/Me/App Settings/Boolean User Defaults/BooleanUserDefaultsDebugView.swift @@ -32,7 +32,7 @@ struct BooleanUserDefaultsDebugView: View { .onAppear { viewModel.load() } - .tint(AppColor.brand) + .tint(AppColor.primary) } } diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift index 4eb00960487e..f80c7c82b88e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController/NotificationTableViewCell.swift @@ -131,7 +131,7 @@ final class NotificationTableViewCell: HostingTableViewCell (image: Image, color: Color?) { let image: Image = Image.DS.icon(named: filled ? .starFill : .starOutline) - let color: Color? = filled ? AppColor.brand: nil + let color: Color? = filled ? AppColor.primary: nil return (image: image, color: color) } diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift index 924ddcb1c32a..fb34703c357e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.swift @@ -272,7 +272,7 @@ import Gridicons // Reply button replyButton.configuration = { var configuration = UIButton.Configuration.plain() - configuration.baseForegroundColor = UIAppColor.brand + configuration.baseForegroundColor = UIAppColor.primary configuration.title = NSLocalizedString("Reply", comment: "Reply to a comment.") return configuration }() diff --git a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift index 72c8ce100f3d..e2b6844ae583 100644 --- a/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Scheduling/PublishDatePickerViewController.swift @@ -73,7 +73,7 @@ struct PublishDatePickerView: View { .environment(\.defaultMinListHeaderHeight, 0) .navigationTitle(Strings.title) .navigationBarTitleDisplayMode(.inline) - .tint(Color(uiColor: UIAppColor.brand)) + .tint(Color(uiColor: UIAppColor.primary)) } private var dateRow: some View { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift index 500a0e0ca2bc..1268760cf984 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift @@ -133,7 +133,7 @@ struct MediaUploadProgressView: View { .stroke(Color.secondary.opacity(0.25), lineWidth: 2) Circle() .trim(from: 0, to: progress) - .stroke(Color(uiColor: UIAppColor.brand), style: StrokeStyle(lineWidth: 2, lineCap: .round)) + .stroke(Color(uiColor: UIAppColor.primary), style: StrokeStyle(lineWidth: 2, lineCap: .round)) .rotationEffect(.degrees(-90)) .animation(.easeOut, value: progress) } diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index b822ab27de3d..d5b45aa84d1c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -323,7 +323,7 @@ private final class ReaderPostCellView: UIView { buttons.bookmark.configuration = { var configuration = buttons.bookmark.configuration ?? .plain() configuration.image = UIImage(systemName: viewModel.isBookmarked ? "bookmark.fill" : "bookmark") - configuration.baseForegroundColor = viewModel.isBookmarked ? UIAppColor.brand : .secondaryLabel + configuration.baseForegroundColor = viewModel.isBookmarked ? UIAppColor.primary : .secondaryLabel return configuration }() diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderRecommendedSitesCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderRecommendedSitesCell.swift index 0d74bee0019d..462773d33323 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderRecommendedSitesCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderRecommendedSitesCell.swift @@ -72,7 +72,7 @@ private final class ReaderRecommendedSitesCellView: UIView { let buttonSubscribe = UIButton(configuration: { var configuration = UIButton.Configuration.plain() configuration.image = UIImage(systemName: "plus.circle") - configuration.baseForegroundColor = UIAppColor.brand + configuration.baseForegroundColor = UIAppColor.primary configuration.contentInsets = .zero return configuration }()) @@ -160,7 +160,7 @@ private final class ReaderRecommendedSitesCellView: UIView { buttonSubscribe.configuration?.baseForegroundColor = .secondaryLabel ReaderSubscriptionHelper().toggleFollowingForSite(site) { [weak self] _ in self?.buttonSubscribe.configuration?.showsActivityIndicator = false - self?.buttonSubscribe.configuration?.baseForegroundColor = UIAppColor.brand + self?.buttonSubscribe.configuration?.baseForegroundColor = UIAppColor.primary } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderSearchSuggestionsView.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderSearchSuggestionsView.swift index a7a6bb56dba5..56edb2440c93 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderSearchSuggestionsView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderSearchSuggestionsView.swift @@ -21,7 +21,7 @@ struct ReaderSearchSuggestionsView: View { viewModel.buttonClearSearchHistoryTapped() } label: { Text(Strings.clearHistory) - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarTagsSection.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarTagsSection.swift index 860942c50ec6..5383da62daae 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarTagsSection.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarTagsSection.swift @@ -37,14 +37,14 @@ struct ReaderSidebarTagsSection: View { } label: { Label(Strings.addTag, systemImage: "plus.circle") } - .listItemTint(AppColor.brand) + .listItemTint(AppColor.primary) Button { viewModel.navigate(.discoverTags) } label: { Label(Strings.discoverTags, systemImage: "sparkle.magnifyingglass") } - .listItemTint(AppColor.brand) + .listItemTint(AppColor.primary) } func delete(at offsets: IndexSet) { diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift index f44e3b7dd6a7..a99b77c29eed 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift @@ -142,7 +142,7 @@ private struct ReaderSidebarView: View { makeSection(Strings.subscriptions, isExpanded: $isSectionSubscriptionsExpanded) { Label(Strings.subscriptions, systemImage: "checkmark.rectangle.stack") .tag(ReaderSidebarItem.allSubscriptions) - .listItemTint(AppColor.brand) + .listItemTint(AppColor.primary) .withDisabledSelection(isEditing) ReaderSidebarSubscriptionsSection(viewModel: viewModel) @@ -206,7 +206,7 @@ private struct ReaderSidebarSection: View { Spacer() Image(systemName: isExpanded ? "chevron.down" : "chevron.forward") .font(.system(size: 14).weight(.semibold)) - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) .frame(width: 14) } .contentShape(Rectangle()) diff --git a/WordPress/Classes/ViewRelated/Reader/Style/WPStyleGuide+ReaderComments.swift b/WordPress/Classes/ViewRelated/Reader/Style/WPStyleGuide+ReaderComments.swift index 23934bf4db7f..19c8fb98b262 100644 --- a/WordPress/Classes/ViewRelated/Reader/Style/WPStyleGuide+ReaderComments.swift +++ b/WordPress/Classes/ViewRelated/Reader/Style/WPStyleGuide+ReaderComments.swift @@ -29,6 +29,6 @@ extension WPStyleGuide { static let buttonTitleLabelFont = WPStyleGuide.fontForTextStyle(.body, fontWeight: .semibold) static let buttonBorderColor = UIColor.systemGray3 static let switchOnTintColor = UIColor.systemGreen - static let switchInProgressTintColor = UIAppColor.brand + static let switchInProgressTintColor = UIAppColor.primary } } diff --git a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift index 7820f3d9a327..bf8589559671 100644 --- a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift @@ -52,10 +52,10 @@ struct ReaderSubscriptionCell: View { switch status { case .all: Image(systemName: "bell.and.waves.left.and.right") - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) case .personalized: Image(systemName: "bell") - .foregroundStyle(AppColor.brand) + .foregroundStyle(AppColor.primary) case .none: Image(systemName: "bell.slash") .foregroundStyle(.secondary) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/Map/CountriesMapView.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/Map/CountriesMapView.swift index 89528ca69e71..e583a8ee9c70 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/Map/CountriesMapView.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/Map/CountriesMapView.swift @@ -69,9 +69,9 @@ private extension CountriesMapView { func mapColors() -> [UIColor] { if traitCollection.userInterfaceStyle == .dark { - return [WPStyleGuide.Stats.mapBackground, UIAppColor.brand] + return [WPStyleGuide.Stats.mapBackground, UIAppColor.primary] } else { - return [WPStyleGuide.Stats.mapBackground, UIAppColor.brand] + return [WPStyleGuide.Stats.mapBackground, UIAppColor.primary] } } diff --git a/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift b/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift index bfe633678c1e..b2fe93f3c2ad 100644 --- a/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift +++ b/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift @@ -203,7 +203,7 @@ private struct SidebarProfileContainerView: View { } } } - .tint(Color(UIAppColor.brand)) + .tint(Color(UIAppColor.primary)) } Spacer() @@ -247,7 +247,7 @@ struct SidebarAddButtonLabel: View { Text(title) } icon: { Image(systemName: "plus.square.fill") - .foregroundStyle(AppColor.brand, Color(.secondarySystemFill)) + .foregroundStyle(AppColor.primary, Color(.secondarySystemFill)) .font(.title2) } } diff --git a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift index 116471027f26..275cd2c0df08 100644 --- a/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift +++ b/WordPress/Classes/ViewRelated/Views/List/NotificationsList/NotificationsTableViewCellContent.swift @@ -111,7 +111,7 @@ fileprivate extension NotificationsTableViewCellContent { private var indicator: some View { Circle() - .fill(AppColor.brand) + .fill(AppColor.primary) .frame(width: .DS.Padding.single) } diff --git a/WordPress/Classes/ViewRelated/Voice/AudioRecorderVisualizerView.swift b/WordPress/Classes/ViewRelated/Voice/AudioRecorderVisualizerView.swift index f7d7e317a700..c65e884e14c7 100644 --- a/WordPress/Classes/ViewRelated/Voice/AudioRecorderVisualizerView.swift +++ b/WordPress/Classes/ViewRelated/Voice/AudioRecorderVisualizerView.swift @@ -22,7 +22,7 @@ private struct AudiowaveView: View { ForEach(indices, id: \.self) { index in let height = max(10, 80 * normalizePowerLevel(samples[index])) Capsule(style: .continuous) - .fill(Color(uiColor: UIAppColor.brand)) + .fill(Color(uiColor: UIAppColor.primary)) .frame(width: 10, height: CGFloat(height)) .animation(.spring(duration: 0.1), value: height) } diff --git a/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift b/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift index 74b5cbfa3ecd..0a7d4dd52bd4 100644 --- a/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift +++ b/WordPress/Classes/ViewRelated/Voice/VoiceToContentView.swift @@ -8,7 +8,7 @@ struct VoiceToContentView: View { var body: some View { contents .onAppear(perform: viewModel.onViewAppeared) - .tint(Color(uiColor: UIAppColor.brand)) + .tint(Color(uiColor: UIAppColor.primary)) .alert(viewModel.errorAlertMessage ?? "", isPresented: $viewModel.isShowingErrorAlert, actions: { Button(SharedStrings.Button.ok, action: buttonCancelTapped) }) @@ -150,7 +150,7 @@ private struct RecordButton: View { private var backgroundColor: Color { if !isRecording { - return viewModel.isButtonRecordEnabled ? Color(uiColor: UIAppColor.brand) : Color.secondary.opacity(0.5) + return viewModel.isButtonRecordEnabled ? Color(uiColor: UIAppColor.primary) : Color.secondary.opacity(0.5) } return .black } From 8cdd43e360da4dc621b8628f3f2db2078be71f7a Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 14:30:26 -0500 Subject: [PATCH 108/193] Enable zoom transitions in Reader (iPad) --- .../Reader/Cards/ReaderPostCell.swift | 4 ++++ .../Controllers/ReaderStreamViewController.swift | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index d5b45aa84d1c..0d98025c8c78 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -59,6 +59,10 @@ final class ReaderPostCell: ReaderStreamBaseCell { } return ImageSize(scaling: CGSize(width: coverWidth, height: coverWidth), in: window) } + + func getViewForZoomTransition() -> UIView { + view + } } private final class ReaderPostCellView: UIView { diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 21c787b39fb5..bdb482407384 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -1379,6 +1379,10 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { // Check to see if we need to load more. syncMoreContentIfNeeded(for: tableView, indexPathForVisibleRow: indexPath) + if traitCollection.horizontalSizeClass == .regular, #available(iOS 18, *) { + cell.selectionStyle = .none + } + guard cell.isKind(of: ReaderCrossPostCell.self) else { return } @@ -1471,6 +1475,18 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { WPAnalytics.trackReader(.readerPostCardTapped, properties: topicPropertyForStats() ?? [:]) } + if traitCollection.horizontalSizeClass == .regular, #available(iOS 18, *) { + controller.preferredTransition = .zoom { [weak self] context in + guard let self, let cell = self.tableView.cellForRow(at: indexPath) else { + return nil + } + if let cell = (cell as? ReaderPostCell) { + return cell.getViewForZoomTransition() + } + return cell.contentView + } + } + navigationController?.pushViewController(controller, animated: true) tableView.deselectRow(at: indexPath, animated: false) From 7584f56738c99ebb9abaee55ca63968f4ae52eef Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 14:32:22 -0500 Subject: [PATCH 109/193] Update release notes --- RELEASE-NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e340975dad7f..9dc4a9881152 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -14,7 +14,7 @@ * [*] Fix an issue with comments being lost on request failure [#23942] * [*] Fix an issue with Referrers in Stats showing invalid icons [#23943] * [*] Update site menu style on iPhone [#23944] -* [*] Integrate zoom transitions in Themes [#23945] +* [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] 25.6 From d86c34286ec09073193f44d4770fbe4c7e11e2d4 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 14:33:15 -0500 Subject: [PATCH 110/193] Remove unused isVisibleInScrollView --- .../Reader/Detail/ReaderDetailViewController.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index e0041e3fd502..3626c9e62c01 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift @@ -1143,17 +1143,6 @@ private extension ReaderDetailViewController { let image = image.withRenderingMode(.alwaysTemplate) return UIBarButtonItem(image: image, style: .plain, target: self, action: action) } - - /// Checks if the view is visible in the viewport. - func isVisibleInScrollView(_ view: UIView) -> Bool { - guard view.superview != nil, !view.isHidden else { - return false - } - - let scrollViewFrame = CGRect(origin: scrollView.contentOffset, size: scrollView.frame.size) - let convertedViewFrame = scrollView.convert(view.bounds, from: view) - return scrollViewFrame.intersects(convertedViewFrame) - } } // MARK: - NoResultsViewControllerDelegate From 40f7142358e69a27213ce3be65eef6509a689324 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 14:36:27 -0500 Subject: [PATCH 111/193] Enable toolbar hiding on iPad --- .../ViewRelated/Reader/Detail/ReaderDetailViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index 3626c9e62c01..3f43801c8d31 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift @@ -812,8 +812,6 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { extension ReaderDetailViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard traitCollection.horizontalSizeClass == .compact else { return } - let currentOffset = scrollView.contentOffset.y // Using `safeAreaLayoutGuide.layoutFrame.height` because it doesn't // change when we extend the scroll view size by hiding the toolbar From fe68ae3cd18a2e09bc5ea69c7164cda56d03351f Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 14:44:36 -0500 Subject: [PATCH 112/193] Fix ReaderDetailFeaturedImageView gradienet showing up when no image is present --- .../Detail/ReaderDetailViewController.swift | 38 +++++++++---------- .../Views/ReaderDetailFeaturedImageView.swift | 5 ++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index 3f43801c8d31..bf431ce55fde 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() + private let featuredImageView = ReaderDetailFeaturedImageView() /// The actual header private lazy var header: ReaderDetailHeaderHostingView = { @@ -202,7 +202,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { return } - featuredImage.viewWillDisappear() + featuredImageView.viewWillDisappear() toolbar.viewWillDisappear() } @@ -210,7 +210,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { _ in - self.featuredImage.deviceDidRotate() + self.featuredImageView.deviceDidRotate() }) } @@ -222,7 +222,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { func render(_ post: ReaderPost) { configureDiscoverAttribution(post) - featuredImage.configure(for: post, with: self) + featuredImageView.configure(for: post, with: self) toolbar.configure(for: post, in: self) header.configure(for: post) fetchLikes() @@ -245,12 +245,12 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { self?.webView.loadHTMLString(post.contentForDisplay()) } - guard !featuredImage.isLoaded else { + guard !featuredImageView.isLoaded else { return } // Load the image - featuredImage.load { [weak self] in + featuredImageView.load { [weak self] in self?.hideLoading() } @@ -301,7 +301,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { } func hideLoading() { - guard !featuredImage.isLoading, !isLoadingWebView else { + guard !featuredImageView.isLoading, !isLoadingWebView else { return } @@ -448,7 +448,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { } // Featured image view - featuredImage.displaySetting = displaySetting + featuredImageView.displaySetting = displaySetting // Update Reader Post web view if let contentForDisplay = post?.contentForDisplay() { @@ -507,18 +507,18 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { private func setupFeaturedImage() { configureFeaturedImage() - featuredImage.configure( + featuredImageView.configure( scrollView: scrollView, navigationBar: navigationController?.navigationBar, navigationItem: navigationItem ) - guard !featuredImage.isLoaded else { + guard !featuredImageView.isLoaded else { return } // Load the image - featuredImage.load { [weak self] in + featuredImageView.load { [weak self] in guard let self else { return } @@ -527,24 +527,24 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { } private func configureFeaturedImage() { - guard featuredImage.superview == nil else { + guard featuredImageView.superview == nil else { return } if ReaderDisplaySetting.customizationEnabled { - featuredImage.displaySetting = displaySetting + featuredImageView.displaySetting = displaySetting } - featuredImage.useCompatibilityMode = useCompatibilityMode + featuredImageView.useCompatibilityMode = useCompatibilityMode - featuredImage.delegate = coordinator + featuredImageView.delegate = coordinator - view.insertSubview(featuredImage, belowSubview: webView) + view.insertSubview(featuredImageView, belowSubview: webView) NSLayoutConstraint.activate([ - featuredImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), - featuredImage.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), - featuredImage.topAnchor.constraint(equalTo: view.topAnchor, constant: 0) + featuredImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + featuredImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + featuredImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0) ]) headerContainerView.translatesAutoresizingMaskIntoConstraints = false diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 32b92b9207ff..4d9b516a27e4 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -141,8 +141,11 @@ final class ReaderDetailFeaturedImageView: UIView { imageView.pinEdges() addSubview(gradientView) - gradientView.heightAnchor.constraint(equalToConstant: 120).isActive = true gradientView.pinEdges([.top, .horizontal]) + NSLayoutConstraint.activate([ + gradientView.heightAnchor.constraint(equalToConstant: 120).withPriority(999), + gradientView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) // Make sure it collapses + ]) isUserInteractionEnabled = false From 99b0ba2677660de3008e7a0b825d4eefa6ee5079 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 15:06:59 -0500 Subject: [PATCH 113/193] Fix an issue with Publisize options appearing in the prepublishing sheet for XMLRPC sites --- .../Models/Blog/Blog+Capabilities.swift | 30 +++++++++---------- WordPress/Classes/Models/Blog/Blog.h | 20 ++++++------- ...blishingViewController+JetpackSocial.swift | 14 ++++----- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/WordPress/Classes/Models/Blog/Blog+Capabilities.swift b/WordPress/Classes/Models/Blog/Blog+Capabilities.swift index ae00fa1621e7..7ff568add020 100644 --- a/WordPress/Classes/Models/Blog/Blog+Capabilities.swift +++ b/WordPress/Classes/Models/Blog/Blog+Capabilities.swift @@ -6,21 +6,21 @@ extension Blog { /// Enumeration that contains all of the Blog's available capabilities. /// public enum Capability: String { - case DeleteOthersPosts = "delete_others_posts" - case DeletePosts = "delete_posts" - case EditOthersPages = "edit_others_pages" - case EditOthersPosts = "edit_others_posts" - case EditPages = "edit_pages" - case EditPosts = "edit_posts" - case EditThemeOptions = "edit_theme_options" - case EditUsers = "edit_users" - case ListUsers = "list_users" - case ManageCategories = "manage_categories" - case ManageOptions = "manage_options" - case PromoteUsers = "promote_users" - case PublishPosts = "publish_posts" - case UploadFiles = "upload_files" - case ViewStats = "view_stats" + case DeleteOthersPosts = "delete_others_posts" + case DeletePosts = "delete_posts" + case EditOthersPages = "edit_others_pages" + case EditOthersPosts = "edit_others_posts" + case EditPages = "edit_pages" + case EditPosts = "edit_posts" + case EditThemeOptions = "edit_theme_options" + case EditUsers = "edit_users" + case ListUsers = "list_users" + case ManageCategories = "manage_categories" + case ManageOptions = "manage_options" + case PromoteUsers = "promote_users" + case PublishPosts = "publish_posts" + case UploadFiles = "upload_files" + case ViewStats = "view_stats" } /// Returns true if a given capability is enabled. False otherwise diff --git a/WordPress/Classes/Models/Blog/Blog.h b/WordPress/Classes/Models/Blog/Blog.h index 9cecad79e951..a82a4225b10c 100644 --- a/WordPress/Classes/Models/Blog/Blog.h +++ b/WordPress/Classes/Models/Blog/Blog.h @@ -194,20 +194,20 @@ typedef NS_ENUM(NSInteger, SiteVisibility) { * * @warn For WordPress.com or Jetpack Managed sites this will be nil. Use usernameForSite instead */ -@property (nonatomic, strong, readwrite, nullable) NSString *username; -@property (nonatomic, strong, readwrite, nullable) NSString *password; +@property (nonatomic, strong, readwrite, nullable) NSString *username; +@property (nonatomic, strong, readwrite, nullable) NSString *password; // Readonly Properties -@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormatNames; -@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormats; -@property (nonatomic, weak, readonly, nullable) NSArray *sortedConnections; +@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormatNames; +@property (nonatomic, weak, readonly, nullable) NSArray *sortedPostFormats; +@property (nonatomic, weak, readonly, nullable) NSArray *sortedConnections; @property (nonatomic, readonly, nullable) NSArray *sortedRoles; -@property (nonatomic, strong, readonly, nullable) WordPressOrgXMLRPCApi *xmlrpcApi; -@property (nonatomic, strong, readonly, nullable) WordPressOrgRestApi *selfHostedSiteRestApi; -@property (nonatomic, weak, readonly, nullable) NSString *version; -@property (nonatomic, strong, readonly, nullable) NSString *authToken; -@property (nonatomic, strong, readonly, nullable) NSSet *allowedFileTypes; +@property (nonatomic, strong, readonly, nullable) WordPressOrgXMLRPCApi *xmlrpcApi; +@property (nonatomic, strong, readonly, nullable) WordPressOrgRestApi *selfHostedSiteRestApi; +@property (nonatomic, weak, readonly, nullable) NSString *version; +@property (nonatomic, strong, readonly, nullable) NSString *authToken; +@property (nonatomic, strong, readonly, nullable) NSSet *allowedFileTypes; @property (nonatomic, copy, readonly, nullable) NSString *usernameForSite; @property (nonatomic, assign, readonly) BOOL canBlaze; diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift index 0dc845b52e85..f914613acda5 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController+JetpackSocial.swift @@ -8,7 +8,12 @@ extension PrepublishingViewController { /// Determines whether the account and the post's blog is eligible to see the Jetpack Social row. func canDisplaySocialRow(isJetpack: Bool = AppConfiguration.isJetpack, isFeatureEnabled: Bool = RemoteFeatureFlag.jetpackSocialImprovements.enabled()) -> Bool { - guard isJetpack && isFeatureEnabled && !isPostPrivate && hasPublicizeServices else { + guard isJetpack && + isFeatureEnabled && + !isPostPrivate && + hasPublicizeServices && + post.blog.supportsPublicize() + else { return false } @@ -17,12 +22,7 @@ extension PrepublishingViewController { return !isNoConnectionDismissed } - let blogSupportsPublicize = coreDataStack.performQuery { [postObjectID = post.objectID] context in - let post = (try? context.existingObject(with: postObjectID)) as? Post - return post?.blog.supportsPublicize() ?? false - } - - return blogSupportsPublicize + return true } func configureSocialCell(_ cell: UITableViewCell) { From 600ad8ff17a893a688e0ab9025a8a18e6e6b2621 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 16:03:19 -0500 Subject: [PATCH 114/193] Fix code formatting and remove unused imports --- .../Site Picker/BlogList/SiteIconView.swift | 1 - .../ShareModularViewController.swift | 26 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift index 65a761cdc047..908b728a6fb2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconView.swift @@ -2,7 +2,6 @@ import UIKit import SwiftUI import AsyncImageKit import DesignSystem -import WordPressShared struct SiteIconView: View { let viewModel: SiteIconViewModel diff --git a/WordPress/WordPressShareExtension/ShareModularViewController.swift b/WordPress/WordPressShareExtension/ShareModularViewController.swift index e4f9b7e5403a..a4b099e7ba1e 100644 --- a/WordPress/WordPressShareExtension/ShareModularViewController.swift +++ b/WordPress/WordPressShareExtension/ShareModularViewController.swift @@ -981,24 +981,24 @@ fileprivate extension ShareModularViewController { fileprivate extension ShareModularViewController { struct Constants { - static let sitesReuseIdentifier = String(describing: ShareSitesTableViewCell.self) - static let modulesReuseIdentifier = String(describing: ShareModularViewController.self) - static let siteRowHeight = CGFloat(74.0) - static let defaultRowHeight = CGFloat(44.0) - static let flashAnimationLength = 0.2 - static let unknownDefaultCategoryID = NSNumber(value: -1) - static let unknownDefaultCategoryName = AppLocalizedString("Default", comment: "Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post.") + static let sitesReuseIdentifier = String(describing: ShareSitesTableViewCell.self) + static let modulesReuseIdentifier = String(describing: ShareModularViewController.self) + static let siteRowHeight = CGFloat(74.0) + static let defaultRowHeight = CGFloat(44.0) + static let flashAnimationLength = 0.2 + static let unknownDefaultCategoryID = NSNumber(value: -1) + static let unknownDefaultCategoryName = AppLocalizedString("Default", comment: "Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post.") } struct SummaryText { - static let summaryPostPublishing = AppLocalizedString("Publish post on:", comment: "Text displayed in the share extension's summary view. It describes the publish post action.") - static let summaryDraftPostDefault = AppLocalizedString("Save draft post on:", comment: "Text displayed in the share extension's summary view that describes the save draft post action.") + static let summaryPostPublishing = AppLocalizedString("Publish post on:", comment: "Text displayed in the share extension's summary view. It describes the publish post action.") + static let summaryDraftPostDefault = AppLocalizedString("Save draft post on:", comment: "Text displayed in the share extension's summary view that describes the save draft post action.") static let summaryDraftPostSingular = AppLocalizedString("Save 1 photo as a draft post on:", comment: "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft post.") - static let summaryDraftPostPlural = AppLocalizedString("Save %ld photos as a draft post on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft post.") - static let summaryPagePublishing = AppLocalizedString("Publish page on:", comment: "Text displayed in the share extension's summary view. It describes the publish page action.") - static let summaryDraftPageDefault = AppLocalizedString("Save draft page on:", comment: "Text displayed in the share extension's summary view that describes the save draft page action.") + static let summaryDraftPostPlural = AppLocalizedString("Save %ld photos as a draft post on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft post.") + static let summaryPagePublishing = AppLocalizedString("Publish page on:", comment: "Text displayed in the share extension's summary view. It describes the publish page action.") + static let summaryDraftPageDefault = AppLocalizedString("Save draft page on:", comment: "Text displayed in the share extension's summary view that describes the save draft page action.") static let summaryDraftPageSingular = AppLocalizedString("Save 1 photo as a draft page on:", comment: "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft page.") - static let summaryDraftPagePlural = AppLocalizedString("Save %ld photos as a draft page on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft page.") + static let summaryDraftPagePlural = AppLocalizedString("Save %ld photos as a draft page on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft page.") } struct StatusText { From c966dc61b1b1e93d7cc71c82811ebec9e797edce Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 16:30:44 -0500 Subject: [PATCH 115/193] Move SiteIconView to WordPressUI --- Modules/Package.swift | 10 ++- .../Resources/Assets.xcassets/Contents.json | 6 +- .../vector.imageset/Contents.json | 16 +++++ .../vector.imageset/Vector.pdf | Bin 0 -> 1883 bytes .../WordPressUI/Views}/SiteIconView.swift | 57 +++++++++++++++--- .../Classes/Models/ReaderPost+Swift.swift | 1 + .../Detail Header/BlogDetailHeaderView.swift | 1 + .../SiteDetailsSiteIconView.swift | 1 + .../BlogList/BlogListSiteView.swift | 1 + ...ift => SiteIconViewModel+Extensions.swift} | 32 ++-------- .../Gutenberg/GutenbergViewController.swift | 1 + .../Views/NotificationSettingsSiteView.swift | 1 + .../PrepublishingHeaderView.swift | 1 + .../Reader/Cards/ReaderPostCell.swift | 1 + .../Reader/Controllers/ReaderFeedCell.swift | 3 +- .../ReaderSubscriptionCell.swift | 1 + .../Reader/Views/ReaderSiteIconView.swift | 1 + 17 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Contents.json create mode 100644 Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Vector.pdf rename {WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList => Modules/Sources/WordPressUI/Views}/SiteIconView.swift (69%) rename WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/{SiteIconViewModel.swift => SiteIconViewModel+Extensions.swift} (89%) diff --git a/Modules/Package.swift b/Modules/Package.swift index 85e1bb172ba3..fafa8e6cd200 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -66,7 +66,15 @@ let package = Package( .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")]), - .target(name: "WordPressUI", dependencies: [.target(name: "WordPressShared")], resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]), + .target( + name: "WordPressUI", + dependencies: [ + "AsyncImageKit", + .target(name: "WordPressShared") + ], + resources: [.process("Resources")], + swiftSettings: [.swiftLanguageMode(.v5)] + ), .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)]), diff --git a/Modules/Sources/WordPressUI/Resources/Assets.xcassets/Contents.json b/Modules/Sources/WordPressUI/Resources/Assets.xcassets/Contents.json index da4a164c9186..73c00596a7fc 100644 --- a/Modules/Sources/WordPressUI/Resources/Assets.xcassets/Contents.json +++ b/Modules/Sources/WordPressUI/Resources/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Contents.json b/Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Contents.json new file mode 100644 index 000000000000..4bdc4947b421 --- /dev/null +++ b/Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Vector.pdf b/Modules/Sources/WordPressUI/Resources/Assets.xcassets/vector.imageset/Vector.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d4c9dab9b0a3c604cbf432efb232f52c313ee618 GIT binary patch literal 1883 zcmZuyO;6iE5WVlOmI)D`= zlQ%o>&4)ccxm{hKt0)&jfrReoF9C3I0rC2Iytx~z(YSwV`l&0BQ7Y{PmzVA%OuMyV zA~}lxY}(!U2q}UaJSc{-H`PJJEAw|>o6XG?Ebr$3>OuSw@mj%d5GtA^B#L?CoUOM$ zcGU+Vqa@WyxxmO^WJ$izFx0ImiBA<|9mImn05YXwCqv9pqnb~TPqD!+KF&xj-eC>e zOc5aFz)(&+kjyOha!3fM#pA32S)`}r;UH8RDQ7E2XVyDjX2*m$q=-t#(g>xAv5az0 zokoeZ6et;GjFGUUbI6!DqUmH_+6xe)7#S5lI-y%JFxFV%!nq47un`%}iL+^hCYq8A z9cd@#PbtvERgyYpM~UX-#B#}NCKLebAblMAM;J$LRL#u6>QgTcFrXBw*ZP)->kA5PHCK0HGSL0o+39J%J*B+NGRLA2tVu=~v~FWPi= zsku$55}EJ<#H}-1pdZ?{8wa@mfeRI{`1`-#2IAXlXFTx5Z1>ew_YC)8&vBc=V;d}D z+L)nx>T5HAUD+E?x9r-{w5VwJFoCyKj~HkV;U1fGf z)vq9pB_2aMDPBXF41r@`?Hkhz`}+I1!gibv-NBr}>3)0Wni6liZUkHK)ZqAT|KDKt U)9bD3$Eh5ZWlEf!tiN8r0H UIMenu? diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteDetailsSiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteDetailsSiteIconView.swift index b69f148e58c1..d4a4e419c004 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteDetailsSiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteDetailsSiteIconView.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressUI final class SiteDetailsSiteIconView: UIView { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListSiteView.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListSiteView.swift index d701c2809db8..8ee2a7bd9525 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListSiteView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListSiteView.swift @@ -1,4 +1,5 @@ import SwiftUI +import WordPressUI import WordPressShared struct BlogListSiteView: View { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel+Extensions.swift similarity index 89% rename from WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift rename to WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel+Extensions.swift index 8daaa23b1785..aa49a5f98592 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/SiteIconViewModel+Extensions.swift @@ -3,33 +3,12 @@ import SwiftUI import WordPressShared import WordPressKit import AsyncImageKit +import WordPressUI -struct SiteIconViewModel { - var imageURL: URL? - var firstLetter: Character? - var size: Size - var host: MediaHost? - - enum Size { - case small - case regular - case large - - var width: CGFloat { - switch self { - case .small: 28 - case .regular: 40 - case .large: 72 - } - } - - var size: CGSize { - CGSize(width: width, height: width) - } - } - +extension SiteIconViewModel { init(blog: Blog, size: Size = .regular) { - self.size = size + self.init(size: size) + self.firstLetter = blog.title?.first if blog.hasIcon, let icon = blog.icon { @@ -39,7 +18,8 @@ struct SiteIconViewModel { } init(readerSiteTopic: ReaderSiteTopic, size: Size = .regular) { - self.size = size + self.init(size: size) + self.firstLetter = readerSiteTopic.title.first self.imageURL = SiteIconViewModel.makeReaderSiteIconURL( iconURL: readerSiteTopic.siteBlavatar, diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 8fb10799e061..bf503b28f924 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -4,6 +4,7 @@ import Gutenberg import Aztec import WordPressFlux import WordPressShared +import WordPressUI import React import AutomatticTracks import Combine diff --git a/WordPress/Classes/ViewRelated/Notifications/Views/NotificationSettingsSiteView.swift b/WordPress/Classes/ViewRelated/Notifications/Views/NotificationSettingsSiteView.swift index 299f952c75f2..973979ebc43e 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Views/NotificationSettingsSiteView.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Views/NotificationSettingsSiteView.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import WordPressUI struct NotificationSettingsSiteView: View { let viewModel: NotificationSettingsSiteViewModel diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift index 2233a5113d1a..c60b6c400548 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing Nudge/PrepublishingHeaderView.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressUI final class PrepublishingHeaderView: UIView { private let blogImageView = SiteIconHostingView() diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 0d98025c8c78..27922ffe48ee 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -1,6 +1,7 @@ import SwiftUI import UIKit import Combine +import WordPressUI import WordPressShared import AsyncImageKit diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderFeedCell.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderFeedCell.swift index 68be211e6f0b..6b90262ebb00 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderFeedCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderFeedCell.swift @@ -1,4 +1,5 @@ import SwiftUI +import WordPressUI import WordPressKit struct ReaderFeedCell: View { @@ -25,7 +26,7 @@ struct ReaderFeedCell: View { extension SiteIconViewModel { init(feed: ReaderFeed, size: Size = .regular) { - self.size = size + self.init(size: size) if let iconURL = feed.blavatarURL { self.imageURL = SiteIconViewModel.optimizedURL(for: iconURL.absoluteString, imageSize: size.size) } diff --git a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift index bf8589559671..49575ab2542f 100644 --- a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift @@ -1,4 +1,5 @@ import SwiftUI +import WordPressUI struct ReaderSubscriptionCell: View { let site: ReaderSiteTopic diff --git a/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift b/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift index c1eca881cc4d..339720cdaa06 100644 --- a/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Views/ReaderSiteIconView.swift @@ -1,5 +1,6 @@ import SwiftUI import AsyncImageKit +import WordPressUI struct ReaderSiteIconView: View, Hashable { let site: ReaderSiteTopic From a6775eb22babebb5fe3b80c759770b98c21e23af Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 17:04:36 -0500 Subject: [PATCH 116/193] Update Share extension to use SiteIconView --- Modules/Package.swift | 1 + .../NotificationSettingsViewController.swift | 14 ++-- .../ShareModularViewController.swift | 71 +++++++++---------- .../WPStyleGuide+Share.swift | 21 ------ 4 files changed, 43 insertions(+), 64 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index fafa8e6cd200..1f999d2b80ce 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -201,6 +201,7 @@ enum XcodeSupport { .xcodeTarget("XcodeTarget_StatsWidget", dependencies: [ "JetpackStatsWidgetsCore", "WordPressShared", + "WordPressUI", .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), .product(name: "WordPressAPI", package: "wordpress-rs"), .product(name: "ColorStudio", package: "color-studio"), diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift index 181ee7c1a0d1..57c6905b1ca6 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationSettingsViewController.swift @@ -331,11 +331,15 @@ private extension NotificationSettingsViewController { labelView.translatesAutoresizingMaskIntoConstraints = false let textProvider = JetpackBrandingTextProvider(screen: JetpackBadgeScreen.notificationsSettings) - let badgeView = JetpackButton.makeBadgeView(title: textProvider.brandingText(), - topPadding: FooterMetrics.jetpackBadgeTopPadding, - bottomPadding: FooterMetrics.jetpackBadgeBottomPatting, - target: self, - selector: #selector(jetpackButtonTapped)) + let badgeView = JetpackButton.makeBadgeView( + title: textProvider.brandingText(), + topPadding: FooterMetrics.jetpackBadgeTopPadding, + bottomPadding: FooterMetrics.jetpackBadgeBottomPatting, + target: self, + selector: #selector( + jetpackButtonTapped + ) + ) badgeView.translatesAutoresizingMaskIntoConstraints = false let stackView = UIStackView(arrangedSubviews: [labelView, badgeView]) diff --git a/WordPress/WordPressShareExtension/ShareModularViewController.swift b/WordPress/WordPressShareExtension/ShareModularViewController.swift index a4b099e7ba1e..7d1dd3566ac7 100644 --- a/WordPress/WordPressShareExtension/ShareModularViewController.swift +++ b/WordPress/WordPressShareExtension/ShareModularViewController.swift @@ -1,6 +1,8 @@ import UIKit +import SwiftUI import WordPressKit import WordPressShared +import WordPressUI class ShareModularViewController: ShareExtensionAbstractViewController { @@ -185,7 +187,7 @@ class ShareModularViewController: ShareExtensionAbstractViewController { fileprivate func setupSitesTableView() { // Register the cells - sitesTableView.register(ShareSitesTableViewCell.self, forCellReuseIdentifier: Constants.sitesReuseIdentifier) + sitesTableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.sitesReuseIdentifier) sitesTableView.estimatedRowHeight = Constants.siteRowHeight // Hide the separators, whenever the table is empty @@ -564,34 +566,17 @@ fileprivate extension ShareModularViewController { return } - // Site's Details - let displayURL = URL(string: site.url)?.host ?? "" - if let name = site.name.nonEmptyString() { - cell.textLabel?.text = name - cell.detailTextLabel?.isEnabled = true - cell.detailTextLabel?.text = displayURL - } else { - cell.textLabel?.text = displayURL - cell.detailTextLabel?.isEnabled = false - cell.detailTextLabel?.text = nil - } + cell.selectionStyle = .none - // Site's Blavatar - cell.imageView?.image = WPStyleGuide.Share.blavatarPlaceholderImage - if let siteIconPath = site.icon, - let siteIconUrl = URL(string: siteIconPath) { - cell.imageView?.downloadBlavatar(from: siteIconUrl) - } else { - cell.imageView?.image = WPStyleGuide.Share.blavatarPlaceholderImage - } + cell.contentConfiguration = UIHostingConfiguration { + ShareSiteCellView(site: site) + }.margins(.vertical, 12) if site.blogID.intValue == shareData.selectedSiteID { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } - - WPStyleGuide.Share.configureTableViewSiteCell(cell) } var rowCountForSites: Int { @@ -599,10 +584,7 @@ fileprivate extension ShareModularViewController { } func selectedSitesTableRowAt(_ indexPath: IndexPath) { - sitesTableView.flashRowAtIndexPath(indexPath, - scrollPosition: .none, - flashLength: Constants.flashAnimationLength, - completion: nil) + sitesTableView.flashRowAtIndexPath(indexPath, scrollPosition: .none, flashLength: Constants.flashAnimationLength, completion: nil) guard let cell = sitesTableView.cellForRow(at: indexPath), let site = siteForRowAtIndexPath(indexPath), @@ -981,7 +963,7 @@ fileprivate extension ShareModularViewController { fileprivate extension ShareModularViewController { struct Constants { - static let sitesReuseIdentifier = String(describing: ShareSitesTableViewCell.self) + static let sitesReuseIdentifier = "sitesReuseIdentifier" static let modulesReuseIdentifier = String(describing: ShareModularViewController.self) static let siteRowHeight = CGFloat(74.0) static let defaultRowHeight = CGFloat(44.0) @@ -1020,18 +1002,31 @@ private enum Strings { // MARK: - UITableView Cells -class ShareSitesTableViewCell: WPTableViewCell { - - // MARK: - Initializers - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - public required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) +private struct ShareSiteCellView: View { + let site: RemoteBlog + let size: SiteIconViewModel.Size = .regular + + var body: some View { + HStack(alignment: .center, spacing: 16) { + SiteIconView(viewModel: SiteIconViewModel(site: site)) + .frame(width: size.width, height: size.width) + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(site.name) + .font(.callout.weight(.medium)) + } + Text(URL(string: site.url)?.host ?? "") + .font(.footnote) + .foregroundStyle(.secondary) + } + .lineLimit(1) + } } +} - public convenience init() { - self.init(style: .subtitle, reuseIdentifier: nil) +private extension SiteIconViewModel { + init(site: RemoteBlog) { + self.init(size: .regular) + self.imageURL = site.icon.flatMap(URL.init) } } diff --git a/WordPress/WordPressShareExtension/WPStyleGuide+Share.swift b/WordPress/WordPressShareExtension/WPStyleGuide+Share.swift index 737e312a3b6c..6dca1692df24 100644 --- a/WordPress/WordPressShareExtension/WPStyleGuide+Share.swift +++ b/WordPress/WordPressShareExtension/WPStyleGuide+Share.swift @@ -5,8 +5,6 @@ extension WPStyleGuide { // MARK: - Styles Used by the WordPress Share Extension // class Share { - static let blavatarPlaceholderImage = UIImage(named: "blavatar-default") - static func configureModuleCell(_ cell: UITableViewCell) { cell.textLabel?.font = tableviewTextFont() cell.textLabel?.sizeToFit() @@ -68,24 +66,5 @@ extension WPStyleGuide { cell.backgroundColor = UIColor.clear cell.separatorInset = UIEdgeInsets.zero } - - static func configureTableViewSiteCell(_ cell: UITableViewCell) { - cell.textLabel?.font = tableviewTextFont() - cell.textLabel?.sizeToFit() - cell.textLabel?.textColor = .label - cell.textLabel?.numberOfLines = 0 - - cell.detailTextLabel?.font = subtitleFont() - cell.detailTextLabel?.sizeToFit() - cell.detailTextLabel?.textColor = .secondaryLabel - cell.detailTextLabel?.numberOfLines = 0 - - cell.imageView?.layer.borderColor = UIColor.white.cgColor - cell.imageView?.layer.borderWidth = 1 - cell.imageView?.tintColor = UIAppColor.neutral(.shade30) - - cell.backgroundColor = .secondarySystemGroupedBackground - cell.tintColor = UIAppColor.primary - } } } From 7c33ae7d5c892294a623dfbb7e79dda2d54a9495 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 17:05:40 -0500 Subject: [PATCH 117/193] Remove UIImageView+Blavatar --- .../Extensions/UIImageView+Blavatar.swift | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 Modules/Sources/WordPressUI/Extensions/UIImageView+Blavatar.swift diff --git a/Modules/Sources/WordPressUI/Extensions/UIImageView+Blavatar.swift b/Modules/Sources/WordPressUI/Extensions/UIImageView+Blavatar.swift deleted file mode 100644 index bc4b0414a71f..000000000000 --- a/Modules/Sources/WordPressUI/Extensions/UIImageView+Blavatar.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import UIKit - -public extension UIImageView { - - /// Downloads a resized Blavatar, meant to perfectly fit the UIImageView's Dimensions - /// - /// - Parameter url: The URL of the target blavatar - /// - func downloadBlavatar(from url: URL) { - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) - components?.query = String(format: Downloader.blavatarResizeFormat, blavatarSize) - - guard let updatedURL = components?.url else { - assertionFailure() - return - } - - let size = CGSize(width: blavatarSizeInPoints, height: blavatarSizeInPoints) - downloadResizedImage(from: updatedURL, pointSize: size) - } - - /// Returns the desired Blavatar Side-Size, in pixels - /// - private var blavatarSize: Int { - return blavatarSizeInPoints * Int(mainScreenScale) - } - - /// Returns the desired Blavatar Side-Size, in points - /// - private var blavatarSizeInPoints: Int { - var size = Downloader.defaultImageSize - - if !bounds.size.equalTo(.zero) { - size = max(bounds.width, bounds.height) - } - - return Int(size) - } - - /// Returns the Main Screen Scale - /// - private var mainScreenScale: CGFloat { - return UIScreen.main.scale - } - - /// Private helper structure - /// - private struct Downloader { - /// Default Blavatar Image Size - /// - static let defaultImageSize = CGFloat(40) - - /// Blavatar Resize Query FormatString - /// - static let blavatarResizeFormat = "d=404&s=%d" - } -} From 119f4ccff993fa3d177b60f354c717a2933c41b0 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 17:07:35 -0500 Subject: [PATCH 118/193] Use firstLetter --- .../WordPressShareExtension/ShareModularViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/WordPressShareExtension/ShareModularViewController.swift b/WordPress/WordPressShareExtension/ShareModularViewController.swift index 7d1dd3566ac7..9093b6de4e66 100644 --- a/WordPress/WordPressShareExtension/ShareModularViewController.swift +++ b/WordPress/WordPressShareExtension/ShareModularViewController.swift @@ -1027,6 +1027,7 @@ private struct ShareSiteCellView: View { private extension SiteIconViewModel { init(site: RemoteBlog) { self.init(size: .regular) + self.firstLetter = blog.name.first self.imageURL = site.icon.flatMap(URL.init) } } From 0862ef18c8399bf37129bb81f8a8d45248281238 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 6 Jan 2025 17:08:56 -0500 Subject: [PATCH 119/193] Update release notes --- RELEASE-NOTES.txt | 2 +- .../WordPressShareExtension/ShareModularViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 9dc4a9881152..5c15bbe78c7c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -15,7 +15,7 @@ * [*] Fix an issue with Referrers in Stats showing invalid icons [#23943] * [*] Update site menu style on iPhone [#23944] * [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] - +* [*] Fix an issue with site icons cropped in share extensions [#23950] 25.6 ----- diff --git a/WordPress/WordPressShareExtension/ShareModularViewController.swift b/WordPress/WordPressShareExtension/ShareModularViewController.swift index 9093b6de4e66..f8afd9189d2c 100644 --- a/WordPress/WordPressShareExtension/ShareModularViewController.swift +++ b/WordPress/WordPressShareExtension/ShareModularViewController.swift @@ -1027,7 +1027,7 @@ private struct ShareSiteCellView: View { private extension SiteIconViewModel { init(site: RemoteBlog) { self.init(size: .regular) - self.firstLetter = blog.name.first + self.firstLetter = site.name.first self.imageURL = site.icon.flatMap(URL.init) } } From f95fe7c740810cfdc4af809ce86d1f0b4a323a7e Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 10:48:28 -0500 Subject: [PATCH 120/193] Disable universal links support for QR code login --- .../Coordinators/QRLoginCoordinator.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginCoordinator.swift b/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginCoordinator.swift index e3cdb57c6bb9..c21a7c2a8b28 100644 --- a/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginCoordinator.swift +++ b/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginCoordinator.swift @@ -18,13 +18,13 @@ struct QRLoginCoordinator: QRLoginParentCoordinator { static func didHandle(url: URL) -> Bool { guard - let token = QRLoginURLParser(urlString: url.absoluteString).parse(), + let _ = QRLoginURLParser(urlString: url.absoluteString).parse(), let source = UIApplication.shared.leafViewController else { return false } - - self.init(origin: .deepLink).showVerifyAuthorization(token: token, from: source) + self.init(origin: .deepLink).showCameraScanningView(from: source) + Notice(title: Strings.scanFromApp).post() return true } @@ -34,10 +34,7 @@ struct QRLoginCoordinator: QRLoginParentCoordinator { func showVerifyAuthorization(token: QRLoginToken, from source: UIViewController? = nil) { let controller = QRLoginVerifyAuthorizationViewController() - controller.coordinator = QRLoginVerifyCoordinator(token: token, - view: controller, - parentCoordinator: self) - + controller.coordinator = QRLoginVerifyCoordinator(token: token, view: controller, parentCoordinator: self) pushOrPresent(controller, from: source) } } @@ -115,3 +112,7 @@ extension QRLoginCoordinator { QRLoginCoordinator(origin: origin).showVerifyAuthorization(token: token, from: source) } } + +private enum Strings { + static let scanFromApp = NSLocalizedString("qrLogin.codeHasToBeScannedFromTheAppNotice.title", value: "Please scan the code using the app", comment: "Informational notice title. Showed when you scan a code using a camera app outside of the app, which is not allowed.") +} From 4dc172d49277886d09d06dcc0cebd9e65b31e681 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 10:48:38 -0500 Subject: [PATCH 121/193] Fix an issue with the confirmation screen shown more than once --- .../QR Login/Coordinators/QRLoginScanningCoordinator.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginScanningCoordinator.swift b/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginScanningCoordinator.swift index c1efd3c2ec44..2a13ded14863 100644 --- a/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginScanningCoordinator.swift +++ b/WordPress/Classes/ViewRelated/QR Login/Coordinators/QRLoginScanningCoordinator.swift @@ -4,6 +4,7 @@ class QRLoginScanningCoordinator: NSObject { let parentCoordinator: QRLoginParentCoordinator let view: QRLoginScanningView var cameraSession: QRCodeScanningSession + private var didHandleToken = false init(view: QRLoginScanningView, parentCoordinator: QRLoginParentCoordinator, cameraSession: QRCodeScanningSession = QRLoginCameraSession()) { self.view = view @@ -48,6 +49,9 @@ extension QRLoginScanningCoordinator { } func didScanToken(_ token: QRLoginToken) { + guard !didHandleToken else { return } + didHandleToken = true // Prevents the subsequent captures. + parentCoordinator.track(.qrLoginScannerScannedCode) // Give the user a tap to let them know they've successfully scanned the code From f9738eeea989bf557c23f9fa2ca4c1885fe098c5 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 10:51:39 -0500 Subject: [PATCH 122/193] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5c15bbe78c7c..e6a4c5d0f534 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -16,6 +16,7 @@ * [*] Update site menu style on iPhone [#23944] * [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] * [*] Fix an issue with site icons cropped in share extensions [#23950] +* [*] Disable universal links support for QR code login. You can only scan the codes using the app now. [#23953] 25.6 ----- From 406da34ea86ebcd065fb263e7440336e224f282c Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 11:06:34 -0500 Subject: [PATCH 123/193] Enable fast deceleration for filters on the Discover tab --- .../ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift index eca49f1b5ff6..df3394a10f02 100644 --- a/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Headers/ReaderDiscoverHeaderView.swift @@ -22,6 +22,8 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView, UITextViewDelegate { scrollView.addSubview(channelsStackView) scrollView.showsHorizontalScrollIndicator = false scrollView.clipsToBounds = false + scrollView.decelerationRate = .fast + channelsStackView.pinEdges() scrollView.heightAnchor.constraint(equalTo: channelsStackView.heightAnchor).isActive = true @@ -65,7 +67,7 @@ final class ReaderDiscoverHeaderView: ReaderBaseHeaderView, UITextViewDelegate { private func updateScrollViewInsets() { scrollView.contentInset.left = contentView.frame.minX - (isCompact ? 0 : 10) - scrollView.contentInset.right = frame.maxX - contentView.frame.maxX + scrollView.contentInset.right = frame.maxX - contentView.frame.maxX + 10 scrollView.contentOffset = CGPoint(x: -scrollView.contentInset.left, y: 0) } From 9879df095ae6815bb0897511cbff19c35cf453b4 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 11:08:02 -0500 Subject: [PATCH 124/193] Update release notes --- RELEASE-NOTES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5c15bbe78c7c..88f3ef15b7ef 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -16,6 +16,8 @@ * [*] Update site menu style on iPhone [#23944] * [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] * [*] Fix an issue with site icons cropped in share extensions [#23950] +* [*] Enable fast deceleration for filters on the Discover tab [#23954] + 25.6 ----- From 09f7e55925e57435958264c381545ea0f40a5b83 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 13:05:04 -0500 Subject: [PATCH 125/193] Show selected filter in the Discover navigation bar --- .../ReaderDiscoverViewController.swift | 5 ++++- .../ReaderStreamViewController.swift | 6 +++--- .../ReaderNavigationCustomTitleView.swift | 21 ++++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift index 29027b99b8f2..dad7bd1eb645 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderDiscoverViewController.swift @@ -127,7 +127,10 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe streamVC.view.pinEdges() streamVC.didMove(toParent: self) - navigationItem.titleView = streamVC.navigationItem.titleView // important + streamVC.titleView.detailsLabel.text = selectedChannel.localizedTitle + streamVC.titleView.detailsLabel.isHidden = false + + navigationItem.titleView = streamVC.titleView // important } /// TODO: (tech-debt) the app currently stores the responses from the `/discover` diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index bdb482407384..3d0ebcca28e1 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -65,8 +65,9 @@ import AutomatticTracks return refreshControl }() - private let loadMoreThreashold = 4 + let titleView = ReaderNavigationCustomTitleView() + private let loadMoreThreashold = 5 private let refreshInterval = 300 private var cleanupAndRefreshAfterScrolling = false private let recentlyBlockedSitePostObjectIDs = NSMutableArray() @@ -77,7 +78,6 @@ import AutomatticTracks private var indexPathForGapMarker: IndexPath? private var didSetupView = false private var didBumpStats = false - @Lazy private var titleView = ReaderNavigationCustomTitleView() internal let scrollViewTranslationPublisher = PassthroughSubject() private let notificationsButtonViewModel = NotificationsButtonViewModel() private var notificationsButtonCancellable: AnyCancellable? @@ -1660,7 +1660,7 @@ extension ReaderStreamViewController: UITableViewDelegate, JPScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { layoutEmptyStateView() processJetpackBannerVisibility(scrollView) - $titleView.value?.updateAlpha(in: scrollView) + titleView.updateAlpha(in: scrollView) } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderNavigationCustomTitleView.swift b/WordPress/Classes/ViewRelated/Reader/ReaderNavigationCustomTitleView.swift index a940e26ff144..bcabed72ade3 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderNavigationCustomTitleView.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderNavigationCustomTitleView.swift @@ -4,31 +4,36 @@ import WordPressUI /// A custom replacement for a navigation bar title view. final class ReaderNavigationCustomTitleView: UIView { let textLabel = UILabel() + let detailsLabel = UILabel() + private lazy var stackView = UIStackView(axis: .vertical, alignment: .center, [textLabel, detailsLabel]) override init(frame: CGRect) { super.init(frame: frame) textLabel.font = WPStyleGuide.navigationBarStandardFont - textLabel.alpha = 0 - // The label has to be a subview of the title view because - // navigation bar doesn't seem to allow you to change the alpha - // of `navigationItem.titleView` itself. - addSubview(textLabel) - textLabel.pinEdges() + detailsLabel.font = .preferredFont(forTextStyle: .footnote) + detailsLabel.textColor = .secondaryLabel + detailsLabel.isHidden = true + + addSubview(stackView) + stackView.pinEdges() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + // The label has to be a subview of the title view because + // navigation bar doesn't seem to allow you to change the alpha + // of `navigationItem.titleView` itself. func updateAlpha(in scrollView: UIScrollView) { let offsetY = scrollView.contentOffset.y if offsetY < 16 { - textLabel.alpha = 0 + stackView.alpha = 0 } else { let alpha = (offsetY - 16) / 24 - textLabel.alpha = max(0, min(1, alpha)) + stackView.alpha = max(0, min(1, alpha)) } } } From 5fd1a436a0bf381ac771972b8857b382d74ec948 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 13:07:36 -0500 Subject: [PATCH 126/193] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 88f3ef15b7ef..f4ee4035594c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -16,6 +16,7 @@ * [*] Update site menu style on iPhone [#23944] * [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] * [*] Fix an issue with site icons cropped in share extensions [#23950] +* [*] Show selected filter in the Discover navigation bar [#23956] * [*] Enable fast deceleration for filters on the Discover tab [#23954] From 9e705816cd6083e1dafb4b479d5ac1f7429222b1 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 13:37:12 -0500 Subject: [PATCH 127/193] Remove unused makeCreateButtonAnnouncementAlertController --- .../Utility/Analytics/WPAnalyticsEvent.swift | 3 - ...wController+CreateButtonAnnouncement.swift | 62 ------------------ .../Contents.json | 12 ---- .../wp-illustration-ia-announcement.pdf | Bin 13735 -> 0 bytes 4 files changed, 77 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/System/Floating Create Button/FancyAlertViewController+CreateButtonAnnouncement.swift delete mode 100644 WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/wp-illustration-ia-announcement.pdf diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index 5fece8a12885..379dfc2654d0 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -5,7 +5,6 @@ import Foundation case createSheetShown case createSheetActionTapped - case createAnnouncementModalShown // Media Editor case mediaEditorShown @@ -625,8 +624,6 @@ import Foundation return "create_sheet_shown" case .createSheetActionTapped: return "create_sheet_action_tapped" - case .createAnnouncementModalShown: - return "create_announcement_modal_shown" // Media Editor case .mediaEditorShown: return "media_editor_shown" diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/FancyAlertViewController+CreateButtonAnnouncement.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/FancyAlertViewController+CreateButtonAnnouncement.swift deleted file mode 100644 index 361ebdebc599..000000000000 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/FancyAlertViewController+CreateButtonAnnouncement.swift +++ /dev/null @@ -1,62 +0,0 @@ -import WordPressUI - -extension FancyAlertViewController { - private struct Strings { - static let titleText = NSLocalizedString("Streamlined navigation", comment: "Title of alert announcing new Create Button feature.") - static let bodyText = NSLocalizedString("Now there are fewer and better-organized tabs, posting shortcuts, and more, so you can find what you need fast.", comment: "Body text of alert announcing new Create Button feature.") - static let okayButtonText = NSLocalizedString("Got it!", comment: "Okay button title shown in alert announcing new Create Button feature.") - static let readMoreButtonText = NSLocalizedString("Learn more", comment: "Read more button title shown in alert announcing new Create Button feature.") - } - - private struct Analytics { - static let locationKey = "location" - static let alertKey = "alert" - } - - /// Create the fancy alert controller for the notification primer - /// - /// - Parameter approveAction: block to call when approve is tapped - /// - Returns: FancyAlertViewController of the primer - @objc static func makeCreateButtonAnnouncementAlertController(readMoreAction: @escaping ((_ controller: FancyAlertViewController) -> Void)) -> FancyAlertViewController { - - let okayButton = ButtonConfig(Strings.okayButtonText) { controller, _ in - controller.dismiss(animated: true) - } - - let readMoreButton = ButtonConfig(Strings.readMoreButtonText) { controller, _ in - readMoreAction(controller) - } - - let image = UIImage(named: "wp-illustration-ia-announcement") - - let config = FancyAlertViewController.Config(titleText: Strings.titleText, - bodyText: Strings.bodyText, - headerImage: image, - dividerPosition: .bottom, - defaultButton: readMoreButton, - cancelButton: okayButton, - appearAction: { - WPAnalytics.track(WPAnalyticsEvent.createAnnouncementModalShown, properties: [Analytics.locationKey: Analytics.alertKey]) - }, - dismissAction: {}) - - let controller = FancyAlertViewController.controllerWithConfiguration(configuration: config) - return controller - } -} - -@objc -extension UserDefaults { - private enum Keys: String { - case createButtonAlertWasDisplayed = "CreateButtonAlertWasDisplayed" - } - - var createButtonAlertWasDisplayed: Bool { - get { - return bool(forKey: Keys.createButtonAlertWasDisplayed.rawValue) - } - set { - set(newValue, forKey: Keys.createButtonAlertWasDisplayed.rawValue) - } - } -} diff --git a/WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/Contents.json deleted file mode 100644 index 45756b8638b6..000000000000 --- a/WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wp-illustration-ia-announcement.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/wp-illustration-ia-announcement.pdf b/WordPress/Resources/AppImages.xcassets/Illustrations/wp-illustration-ia-announcement.imageset/wp-illustration-ia-announcement.pdf deleted file mode 100644 index 24964d3bdb3de4aa04f16763c3c0fc316717eac1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13735 zcmch;1ymee(=Lhzmjw4AWN;hY3GPk^8r(IwOA_26xCM6$?hrhfurS|Tc=c%p&$_tCqf#{hLfgKws8+*B@pN2cS5Lp0>0Bb!nL>?XhgS4TQ zvAqd^6)aK&Fo>F3*c;k{j~2T2hQfyW)&_%ZqFZ)>fuWM~i205>Wu3eW;Dh&bDeDcOVP26&vim>mcJV*J$rfI(GG&&*KY z{>LRzOAvtlchi5g{wVt0`Y+3I{LuPW%Q5_%(C>B`lpOTze@s!-+SU?mWbo&s1|kT+ zASh_<4A6KS3y2f^$HWRA>>rcn{Mmwt?X=c!B*0D0Wb(D0YIQf zEkp(hfW55)*n(03BV7x-$MyTm5{AZ0ln~Wm`E6AphC$bUX~r{##dCqgVKZ3f^v=A4 zzPfLJUbr|`g^utp=m99V6zNsx)zFg6-*iEGrqbv{ihYrbS=HQEJ9WXvb>6E82j z7DTSXo3+cWmS(Yw>p|*j&D(AF>wSo{BRA^A zE#eVw6?83iO9Szwvyt1+lmXnI-4NHTbVUaS;@)1KUJJ&5_hqznh4sjx^>|jD?QTP- zFA`gIQ8uv@81>an0sUi(Tr3@7kWa7(lA>oZAISa)uiBzM4x(M3TW{~4tUYZ{wOdI{ zgN|s8BXD+nLEKn^<4i7YZAPg;LVgIy@_HA~>_BD^z{83De5t!H9n&rPj>+OR%0=CB zcS2x*%sh(ZWG*gCl)MTn$h<-^sNrb zPoGDO^V`cu&Cg!>A5PV+u?@c{DcUzKX4U%ygba{`5LFXg4EU&8sL`AB#F1TSK7?Mb zab6DN^ise3J^#%&T^-kE$Ds2!F{Oz8@v-hgrg#N(M5&~Vr4u7DF>blVd}0XY_~JuR z?yuu9p9Xe3R~^c)-=$-jeJW^cc@l7&Tn6DsYSGvj40rIbryLN`h_Lq70Fbk8;)!5N zNLs=rScYzG;RTTQqCM8gwwM>jd`~ono7S$EJ=hb4d5bRzB(8Dy_7P?@8g z8QI~$6DuM=*m+|%wA%a}BAXt8L>!=()|bDQ^ERPvAtV-O(YNQAedFW-YV6C(M$Yw!RWQwYfLzmtI+djzR%PnEIHp>I28#>Ot-}uk&C*5p5Xw)Gu@* zD7#n`yz{<&!I%APB6HQxH`_c5MaP_F>nB|^+^hE;;zKRNR}r~g43hjbAhzrVa}E=y zQEd%z%tLdH=(G`3=@<~_-8Ye&BmIc4EJ#(g?@$Y(*`xg7qm-x0-xmWVt%!nQ@V_&Y zc|}5P)7oL-bKy2lxC)q^;?krWu4YB3NOCL3cvudYc(9(rWk1)!>vYLM#u*Z|NSnp8 z$>>3;uhhg*883^|8gVKc?95VCbg{Do zRim{U80oN7b&p3_#NO`AGr{$>8n0OL&)gA7T1s8NFi zz$I7bY`j5QeCB+-pLluL;_z3(T_fXRvd?6Zac0KKIkafIBnvSk)Ul-P&-n*@Tm__c z`7xg;>{Lpo1+hkiG79G~;m52kVg<|)<}L28#_GA`yXYyN_=5bTEz7&ZKr^hh!R0Ad zN^q4X&>+-Hs)9ZF_F4FJNw*@d8qjq>yu6=V#{Ft-YWF^7PPtjFX_1n4v3J)}2Uj=S zMgb6mW!kSg_PMVXYyrq=mkgPeRzeOw3EwZC&#%=dFb8=*SBsT8N4$2uhpz071CJRMo!zE1i{ZHN9i|l9oG2nJozLFmnI&R)2FS(;`FoDuC2aEvw}=^G zY^W5#zJK@Bvv?#~WI$6FS1f0R0TnYXWW}mi*`1HIV%{x-8&&CxyhK9nZ_ohyRBN02oa{i!~(Ilty-(a#cD)MLgv^9yw3q_i4e9&fy zsa!uxx1P%Dp$7?NrYt-d#QCcB$mgSb2n_FU`?u#^7|HuLrm*pwkhw@}*PTVk6M4rD zZ=O*Cs8yxU?UDO7<9kr3WK>jmUCMZwYhcMCvAlqrl~kK3j!^GPaO{wtu(ryt5E% z5H)3)EC^(2bdv10=9M6kOo=Kgx?WZaSu{EW(NP@f*vp-pj8GE)CI(xE@+AI|!BG0Q z=JNQn(3z>A`1V{~YM~3m;h! zJocDS%|`nqO>DOZxQW_U3V2&NdV7Yg2bFFvBZ0?J5@ub53nOeQ~{t0uoL}(Ucc~E{Ln^ zG1g9ia&2SQCqNv7D?R-vN8K^La~9m#s?FU~&qFx=VO3QYf>NcSi8dL_ugA+*sB1oQ z+_7is9_0%+#-IHUYNuwT*5oXwc5tqRMikDb1AW`Z_AmKAm#_%_6I(w5(I1GKmGKAi zems8!j=%BhKhdy|wS~26k0A0FX8Z-IL4U^7KOwD*uAMo60gOVW z9V|_)tnEzg!5CBw0Qw2n|G>*X1iuIH3owIzLiYcIh?#@)7h;ytwYN0|A_dS7yYctHodRm*AZbL`WK#s{c|yY80pb)>`cFz&5yzVw&mXzlR?4~ z%$fwj!+kso{W?lKQl8)J-Y5F1i-SqEe`bBL1Cl2$YCji51Qv)!4 z1c8@;LEO;P*u?&EDZo8}>6xvK^&^c${7FatwAnzv_>9tzZN&7GfiVc0+S|z++6q}) z+E`mXazBoL<)esy=A#G0AV>%Zh%cY+^Nmr^0$%F2dP71#LB?g&`M-G5y3h)Nntp=v zFRS+Z83Zx0|6aBq)BBrknSM|;Fo*lW27j;6AN=tD+qQpL67jEF^$(l>qJaNDP5Kvp zcpy(BPo{ug#3h}9VfO^t1iix>0vaC%)Rqhi^L}n%u{z;%eeHBSYY&t*xK1 ztu67(ryTbhC;WV#4HgYMs;let5#NCRq7sjeDWjckS=k|>-4DXhbQ#L zCYpp+$M-i44L1!p5S7k4Va6WO+)0}Fp56{6b2fMER`ZMVioWdo+bf;CrbLeRH8ciQ zsND6XMU|PFRBm}6X_O7ATbKhA+%UWLT6R0WPq4n~+2d`jr6`_in8W(4UV0&a<&>y! zqCktq`!NVu@ch-ZS~}-GU4zpWT$Y>}ox)jt9R)DiX9u9kx4HV5}^25z@RRSH)8 z2)7m~G|Kh%ElrB5mRdHV%ioOZnx{Ev5X;=BjE5FKlCYl8hCgueHMFZ`nmZOdF3)w! zeB;~Q>7Bl=r%!(DlO44`6snzwtiglM*LjLF9UE|dIw=aF}?^<&4^6j}| zWEHM>MOs5p>1DQG?Tw-=rAfbt3ViWu8iUTeyM+6&UI>o3;jnfOmB@L&o&tk;m{}Vv_5y1NGUw%52KcZX=KV9sPZ}2}y z$(a5ecKhY*fr(VV@A4TEE| z6LeWZP4-j`Qq@=s*_DYJ(Us=-pJyhp1uCMW4f^+A3`&n8c~HMw4K8e!5NK?ogMT;b z+BOMyqnIzR;3BHspJML1K7@H z*PC}TKcfG&mFsGU6hBJ=pEgg0H)>$;nWrF4$z6*Ee}o+4Yl=gQqu17T1hlqNz>@{I zW|hiu;4+4=Plee+W#ALPHaU`-J*Z$pKbaTsw;8P)Y<{7BICGb*V$nmLpE!|d3u6SI zI>S1rMBM#C3b8BX6i!CqJSpe`(qoyK-P_<{6`8mB_ zIY&*h>x#ecz9b(k@!JHeq^rR<)taNIIo#KB)JBru^p2|P5Bn)?kuC>BWyix*eR|&| zh(om-1NuT;M&x{wDwk={bMfi)UiE&GQKS@;2wGLAz=(Zjn)QCA%$AYRNo(Lk#ZU&d zb7*4Nz?Vdaj|VAQ#e#-qPqdsWsXGNp6rfY7&)}wm_LS0b5Am13KCI=Y^o53{;S=cU zR^vR}Ow-N{<;);c?NgnzaJR1N7*F#}IbpliV+Fcf0VL;!`T}~07NE>p0`M)Vb?#t; z@YPEEn@ecJ*}8c8@J?Z6nohcwc$-GcDt4*Kuen^;-|(W{V%HL@cGeS5+jvm=q(Ko} zYKU1ks+*-YjTydG#Fwp{%$;+^)_MgKvpYBlv=Xw2OVd_xi^>vi=B$1*0x?S6MS zdkm=WGOjOzb6J?6fyc#sSiVBi?&bHo{m7Re`GLNP&V$+mm=ebqM<4xMD}r89TWU#? zL`u8%x?v!~?feB3jvep>du*b(tY#`+m6O^!jwE*N1ocY6W%3o(NdONpcU`N@!f9wA z$vV5n_H2VP!{NqmOLbLkL{p9fdg(46Vjy^$9s0CKHFNEoX~vS?A~F|(Z zrj&KUp(*H8YpG3`kvJsc**sA)f-Ec=!n{Z9`v#q)w#4lLl+zXJYw8U)e$r+N%)$w| zMD;p78n-Vi6fy989-0yayX&=z!w#JwwV+-)D>j$6Q5{Z7`(*VAcFq3HmalmRJ^f4` zw4E2|?!(VR}XsLfxa&VEcx z;2dmnI-s(bcA-%`fVS-Qe*4`$$)_~;SO4PX9zD^cO9L^obNFOLU1%+LP$tZ z*Ur!Y@WYQO0^^pIw>p7VEYd>u{RArdkz4e;kzOLM!ChY zU=#u+yyc<%K9D;K!-`@)rBX3aZiN){f37criS+)R-W0+Kx_nogg0RTOLWnuSQ8DZ3 zV&Yx5+S1Hc&D4zXk;ha$g#7J0m@oFKklrlms+fm+ZJN(8`Z_M4aM2-ARUunfjErE! zE`_1SZ<}}%67s~mD$Wn-y$u@XXj|mT?Is_*BA>Ghy?6qFC^Esu<4KOQ0rgR-q7#ql z0p{yj3>A|fhzUC#A*1N|T1TnHdG01nLvMT>`Fc9O+8|ydOKM1i{9+<-B{KvjT{9lI zY@F#1XV|1HkqEJIK{+;dnJf*#h?$_>zeS#I5y&KhiFX)y0tEs8v4gYG7B%>a(KCh~{Iv%u0Z>^w3!)au_3ycd2!(2qaL=GQlMB zpqTLBKy7l<(6iP%%=jv;NxR$9a*^cX*@1 zorA;gLneX6Y$&pklba(Y+~1Fk8aHHccY*$}*2-x*(A!R%oYsnS?7RI+Vf!P(hd`4C zUmhRT2zmp=4_*^1Ubt)&aCn|6=9nf9+7KSTobt62a6Q(%5JF_oFRb`op*;39&_Rej z&5b6-k)?LwOs@A^*Ca@qK~^i$0?(B;&ym9oY=IfHc$rHdEZU%+MZnpALS*rwXN02F zK_T>Jwt-#*Kz#5PM~9AurWc1YeF^hffNvTCQ2;az>G;3g7xM2XxlRHR{4^QKv#&l7pA*#J+%D4&P#lG|{3p-;a z!{M~ftw`2l^P$#!!C4{r20`nq{UsXyWe+^`h^Q(&!LhJlgz5{(r=;eQ&$FQNMG_-W zvk2-!#UtOYLqG9P*Nw-;?!sI1R|{FuQ_-E&lVqHT*N<0`#@S>uLTC25dY7#?QJ$kp zQN-_v!1gpLl&(WXCtOFPEUp}ST6dqD5VgA%Z)q``Zy9c6|5 z*twB<5%cIBTASTBJ$KS(L@z=w;-zqst`>?21a%nmcW9)sfEN}ol+p3wx8UzSJpIC= z;FBk3|J)DVr9)GXsyu93pizR5Ts9h0!tVu+;tQ&TN*`qjv*%jmyyQ)!(Y;&-Vz#(y zFW<t-x^r}K=XpI4!3oGDu|h@lllufZ-*qgkWmBq%4v zpvs_Ks&JIfr;t{hmenHGqT%(mbr>+K6D*L_mX{us-lChTo4KM-RuEY@LY=9e$=$`F zU;Q@dm~Tykyeu$9B1$5(S4L@id{J4aeyDN?*?iN4oTZSpp*y2&UQMk?bn+8tJ5P^$ zrc2Tl4H9p#YH$kn0=72o0j(0<9xaI)l^U$7W#w+!b^B~bqPdp7N4KBZntsJ#&cbF@ zaX#IWW6OaDccNC=0(OIJ!_YbE`STm{8xS0?zq5Z6>czCZBW1$}kBO&y+bgw$$02KQ z6mAsdV-mSbDQbgMgD8WUNXR(faK`CyZAad&)Dqf8(i_l+Xgk}akH4CWSySJ;?w6Sg z-E!=(>{yuQow=*Mp+S>Eixdm){+JuRg+GM!CLpf#b>!>Pny3rCBeSDiJ}$mMzED1l zX2<424~9$KJE%Ly>xI*)Z(G+>H*zo>FhvO4h*>bAFe-3-NK6RNU>#uE+aTK--jOhj zYQNAN$A%KT6>{|DL10DJ!n(G@rKe>nBHwqZedX-w>;yxnCn7A=(I6h)Guk5-9EvT4 z?IhYO)+sU}CL`h|5+@=Rr%J0%Za(m%un}0F7AmzWdzlykaNYAVp zTd_ZDTeXqjH&ha#-KO@InwWruXbJrx5aUb!hIGl9cAe7(%v5xP|4r0Z7tL1avDT&G zZ6r+GhtD5IgyTB20`(5o>f~VD?0D~Y+s1lmj#}(HS)w^tC+nouR=@JSc`w`& zU@1QHtxvQs)R17wDrj+B;gdpHvyMr{(Po!mouI->6WyQ&szs*-mc_w>QAM%YT-@_F zBj5G_1LCS3R+*HewrPml_ zD0C+rA2B9jqgT&`ym_&JWvA}^N>TgJUdC>Hi?Nrsm+*o3apaJguUK~XmvDy(?(r%1 zJob-lUqYk0%Gcg@Ti-2z7jnZW+>vL_th?Y<;AN=4?GaS8G$*rB*fJFgyA z4^mH`oSzJ=&SEollXhF$@g7Sd8YH6QPkL}a3pr0ntoUjyH556Bm7K(x=Hz)I*yVd1 z9Eju3_1d1VzH)surKGH6vCk*2^v!XdZT?)-&F!nWLgNxnEpCq#kER2^yU}A{YpolG#q)ZPjj)Fl0_fl;*N61YZVzwfd3QV(??&MA z(cbX)yK>x_-0APHWl$i>QKuI07vD`>P8sDlo$Y^`(iy4-^Ri5;wNy+d_3;eGxNKnC9tp(`2mI+wskOWG{hVaxt%& z^Lp~!a?4Egrb z&A))!-?=YLY@k14y1#k&UqJ280Qn!F_RsP6-(>w~6qi939OeEeS^pWj{%>qO`!$?u z^b|hdgZLN8X(iF=9;Q$GcnOdOXadT5&%|*_pS<&~rR+qUq(dddq6<>2RDViIKfe0O zN`9|aUJ5oFyRf#*bqL|p@Oj+@-_XDV;r)fXvHKqHk{85Ft)3Y1&u<}KhH+@)KkJiC zO-TNdwE&Iy5+@Hz?99^508U}*hGOS4yTCRg#Kg#xv`0K0)+P$EQ-M*A%J*G4S`6VX`YT7{{g-ODnFnb zInp*bo^(E$kbZ1tBms|ZKnBO~;r)ck$ir!0Z!BrU$Pf%$Ok+{4&86dq^SI+WANU3# z_xNsE2&OfO3^dX7+Og~x0PxpR*0;4agiMe67O6qqVkUKiB%CdwFF`C2x zkBlD~y`xsQ(({Cw@`1h-fZDskhJyqhks^;X6Vr^F_Lit}zCorUC32o$j{mdUG| z_Kb2cOxX2~wB*@(@13uQpx#0Sk*;-Yr6C_s$v z1I76jLNfxI07G2|O#?m|=qH0{yzWCwIpke?vBt5RiDeQB6Sn@&o`ad7gdmy;lB9%0 z&Huc|7v=kKv&V^tOXf9UdNagYsSk1(QX2#zjvx!PeIL+wxS4Po&07(Vu+Q7M5!<+j z%ov@n#-Za0OIYPuxLL0+&84jt*f|C)GaPS1KBWUZ` zsFD?62-j_`>nx=AD25`f??|c!{j0++Jlz|+^xW~gXh;r|QFt-KyC~4qPf=%6SnIEq zST^vdaC0UOC39BU21g$CEAD7MJ^H%a_7G7eH-kTR$Vh6fu;xN)jQC*P=v_}a zMfI8rjglP_quDFnUhx`GFI5MZ#!Ye%NZbvF>DEE`^p@rwUl>T|gBnjA%*Jy9K1v3! znp34g(etZ5R9w=jV2-}nqj$Km&;(zLUZzf==cOT?M#5#%z2^~`xA6|aOKW&awOXF$@oKeZ;8LIv0~G)c+n~)WKuZrT*$k3Ge0rh_o^^)7o|}7 z5V_wYN7SCY((wsofk%(b2CX0er8{8-Et0c68Yl%N7gcQWE zyUQ2j=P#mWkq~|dLQ6O)cXzo#IFSV66qr0%++9YKesWbT&KFQNzgo0C%JUhQH)GUM zOo>S5^2L8r4wo3iK>5K54Fbv*D59v~$C%dZS3?LHO}5E7%bcPn`)$wfP}&3)Vn~18 ziBvlGJw-HjNRRkg1_aPKZ4~PITcH_h%OvVB4f`@|OM<{RRCL%?0j7B9JM=HolDY;` ztxWQ$)NS}>Pc2}%+lb}i@LP3lkSpJfEyLL$Xu_r})8D3^y`#`UZ-i;b^&4U2$_d=j zCyxgj`E%+Y#xr1l5YZ`DWs3DP)X|gni_uBbjhW`Mps7YGY;W#NEFYVO-N$!?+iSZn z=P#?_@F`qYuYGFeE7!L0mW?>9d#&Oa*`4oANZd-{w*hCAH{tB7xyMq@6pNw!ZT|2t zDKWS{P_4YI#efvXj1pFmG|28{H>t?0 zFe=!nz^jNWpwoP$R;Jp2&5>@SwG#QIEF&=^J0n-`<8$qD@wZj0#GTlkG(k;4?UK8a zlahRr$C3$>eIMCqSSeGK>T}OD)zoIBXM{LK)v_9uT`EpUH%`CuezjI+QMPymrL6eM zfVxomydYVzF7qT=HdkZhZinj3{hHd9=nnY~@{Uy~dZlNX@iGJ$VkSzq#F-6R&Exv`*eb)0NO0bOvMb+OtY%fY{G27tY+MAoOL2)JagP`{B^<6>y}T8xdLNl zqwu54<{wS>Cpj{Y<)epLrjUo%O+9n!#7>iqO0i${sRw7qmomlfRPY;@P1PnjfBIoU}<_v1&(c zCneL*L?;Uz!OmnBh3?|&y6;NpA_$Sgy+|@iyi5#`W{Xc|ap@m7`kXM+KhR&%FQ6ne zCa;vRLqFD+({40rgkY3y1UGP)M3c~vl$&JDXw6Jg$ygq*tDyI`b(KO&M9N7jeS^Hu zvG29Yq>6+}UvZe)ceOn=yQ1J?G}Wbw^tsEK{+j&qI?e>H84ugZ^@Hfr+0|l){uAtA+(ux1c8frs?Lc``GtE&~xdnE8arknPK`L^z+w1-MLD%Ia$(b+}R#q z-C~hw2s{@KxlB2HIb*o0IPO*~_WJEg9Gf{~_*k8Hqw-~x(qt-<$m6T^XDxAL;_&GE zv}QH$?H%l5>e*_~8`NsE>n!aWHcB>#51L2s8sR$i>${(YtAw+smZV;#`ty2S*nazZ zGJ7b-boo$MvhVde?LspS~ zC-sQaYwcFRw6>BBtuHBM>SHPFL)(7DOb=h&cg#y{Wt_%05w*+W>8|OuWtlzICL$x5 ztIMmUI8jJ0NOO|XQn4{-w0aD)Rk#{F)^F#YodOM|T;u#`7iy$U)h0iaNU3G-Nou>t zo~cPCD9nt0_D}P7_xzNsKu_0FHE(`3$k4+etgZP5@-tp(w5cK0rCOi5biMZAP&@WFd5J1j zcG08cGdYI?Pm_|WRV~yiHWpM?CI@$SCg5uDtTf6GCLHh2_qcDJSoT?F)#^?UUT2hA z9U;z4sOrB`s!FPF(77medv;Ojb#U*X1@;z(2Wf7dZVeZmhuj)lDz9snY#w%@FCk}q z?ytwM3zuL#$Hte3%l1AEwtW>^j@TbwvmR?c;3D*_JqWmo^gE($nR8S=H4!13R2}DV zmUb>`psL$Zw^Oy#2!7o+azxa!G-o!e?xA^exr-r5@m1EHL(ScG`{Vj}>rTiR+T3RK z0x$Dv|3C%Em>~^`-|60enPjlCVoomgf^*5W?NsnoGw ztHxEw?45${iCFW+DOt^n2G48JZt|CN zR~n>NfX%DTvv&n>erRU*tXJA_4O$K17cl4cw}cm`3;vD%a-O|68(P*grDxiO#0fkF zdv=HB2WW>a*Upn`ixg25?a&)7`480x`2%A|o)-^I`Zk+7N6b4FXkJC`B@gaL!KMte zei{D6jr}g1w*{+J_j?}J^GlU2GOjl!&TZVY4Fg`X_Z1r+mp5)CwtViF>~vb+2>*r1 z{VoDvW@h_OmdB5jmVdmR_*ty*A0qerwZ>yA&d)cskF^eV00tRD15@2UDggeI&m?5+ z0M3H}Gdw9%13PdT!_VX_tv_Y%F#XOr`J+uCU3*;%YvZ4V6n4KWC{zq>?M$t$01O~{ zmOtJ9n27Q??8n=|IYkBz`rz^pN=sdRIVHgFTR@ETAXa)1K$B9?!PLS4T%jPy!J$b7 zP;k(-wRZ)8ODldsJWf$I>!g(CLgeuN(re<+2ym>8KD8Ce;@l$V7G#G%f}NCEx@ z>&aLfJgNZxXO%ya$ee8rjSv|DOq_^}|M>y1v9dC=0*nAZWgsRFX0UZ1e*jj$WQ>d; zHn73|A_Fn8aex=)FEa2ykFSybCIhj6S@_>%;5qz52HpgJmoYQ4{zH$Mk^LVsRz^;+ z`TyDn8#4=dd;L|$33|*S`m2l?Y}S9sSRd2v{-(#u{EvQ_K`j61mznMH_5a`Q#_e`54`06BKsGcvN1FL!+vbc9RILACnM)S`sD<%{@qsK@jX5fe;FS* z7mMvr_uAX)npzmz{@C$KrmlwIeFZMHv9`7c{CJkYMNkq}M%F))2OrOc!H*q}p}r9t zJ0~j>Ckv}Sh>?|z I Date: Tue, 7 Jan 2025 17:25:54 -0500 Subject: [PATCH 128/193] Add scroll-to-top button to Reader --- .../Reader/Cards/ReaderPostCell.swift | 2 +- .../Reader/Cards/ReaderStreamBaseCell.swift | 2 +- .../ReaderStreamViewController.swift | 14 ++++++-- .../Reader/ReaderButtonScrollToTop.swift | 33 +++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 27922ffe48ee..811b48aab76a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -168,7 +168,7 @@ private final class ReaderPostCellView: UIView { avatarView.widthAnchor.constraint(equalToConstant: ReaderPostCell.avatarSize), avatarView.heightAnchor.constraint(equalToConstant: ReaderPostCell.avatarSize), avatarView.centerYAnchor.constraint(equalTo: timeLabel.centerYAnchor), - avatarView.trailingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: -8), + avatarView.trailingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: -9), headerView.topAnchor.constraint(equalTo: topAnchor, constant: 6), headerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left), diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderStreamBaseCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderStreamBaseCell.swift index 931aa1a20032..149264301cbd 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderStreamBaseCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderStreamBaseCell.swift @@ -1,7 +1,7 @@ import UIKit class ReaderStreamBaseCell: UITableViewCell { - static let insets = UIEdgeInsets(top: 0, left: 44, bottom: 0, right: 16) + static let insets = UIEdgeInsets(top: 0, left: 46, bottom: 0, right: 16) var isCompact: Bool = true { didSet { diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 3d0ebcca28e1..c914ab4c9adc 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -65,13 +65,16 @@ import AutomatticTracks return refreshControl }() + private lazy var buttonScrollToTop = ReaderButtonScrollToTop.make { [weak self] in + self?.tableView.scrollToTop(animated: true) + } + let titleView = ReaderNavigationCustomTitleView() private let loadMoreThreashold = 5 private let refreshInterval = 300 private var cleanupAndRefreshAfterScrolling = false private let recentlyBlockedSitePostObjectIDs = NSMutableArray() - private let heightForFooterView = CGFloat(44) private let estimatedHeightsCache = NSCache() private var isFeed = false private var syncIsFillingGap = false @@ -303,6 +306,7 @@ import AutomatticTracks setupTableView() setupFooterView() setupContentHandler() + setupButtonScrollToTop() observeNetworkStatus() @@ -493,9 +497,14 @@ import AutomatticTracks content.initializeContent(tableView: tableView, delegate: self) } + private func setupButtonScrollToTop() { + view.addSubview(buttonScrollToTop) + buttonScrollToTop.pinEdges([.leading, .bottom], to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(horizontal: 8, vertical: 16)) + } + private func setupFooterView() { var frame = footerView.frame - frame.size.height = heightForFooterView + frame.size.height = 44 footerView.frame = frame tableView.tableFooterView = footerView footerView.isHidden = true @@ -1661,6 +1670,7 @@ extension ReaderStreamViewController: UITableViewDelegate, JPScrollViewDelegate layoutEmptyStateView() processJetpackBannerVisibility(scrollView) titleView.updateAlpha(in: scrollView) + buttonScrollToTop.setButtonHidden(scrollView.contentOffset.y < 100, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift b/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift new file mode 100644 index 000000000000..328228e37fca --- /dev/null +++ b/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift @@ -0,0 +1,33 @@ +import UIKit + +final class ReaderButtonScrollToTop: UIButton { + private var isButtonHidden = false + + static func make(closure: @escaping () -> Void) -> ReaderButtonScrollToTop { + var configuration = UIButton.Configuration.bordered() + configuration.image = UIImage(systemName: "arrow.up")? + .withConfiguration(UIImage.SymbolConfiguration(pointSize: 12, weight: .regular)) + configuration.cornerStyle = .capsule + configuration.baseBackgroundColor = .secondarySystemBackground + configuration.baseForegroundColor = .label + configuration.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10) + + return ReaderButtonScrollToTop(configuration: configuration, primaryAction: .init { _ in + closure() + }) + } + + func setButtonHidden(_ isHidden: Bool, animated: Bool) { + guard isButtonHidden != isHidden else { return } + isButtonHidden = isHidden + + UIView.animate(withDuration: animated ? 0.33 : 0.0) { + self.alpha = isHidden ? 0 : 1 + self.isUserInteractionEnabled = !isHidden + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + bounds.insetBy(dx: -8, dy: -10).contains(point) + } +} From 321c8d2cc858c964a6ae123022b0ff31681c1da7 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 17:28:38 -0500 Subject: [PATCH 129/193] Cleanup --- .../Reader/Controllers/ReaderStreamViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index c914ab4c9adc..0aa00fa06590 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -1670,7 +1670,7 @@ extension ReaderStreamViewController: UITableViewDelegate, JPScrollViewDelegate layoutEmptyStateView() processJetpackBannerVisibility(scrollView) titleView.updateAlpha(in: scrollView) - buttonScrollToTop.setButtonHidden(scrollView.contentOffset.y < 100, animated: true) + buttonScrollToTop.setButtonHidden(scrollView.contentOffset.y < bounds.height / 3, animated: true) } } From 8e0026c72b9cd6c7646b55db4b1fcbbc236d3474 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 17:35:20 -0500 Subject: [PATCH 130/193] Update design for iPad --- .../Reader/Controllers/ReaderStreamViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 0aa00fa06590..28cdbc295e50 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -499,7 +499,7 @@ import AutomatticTracks private func setupButtonScrollToTop() { view.addSubview(buttonScrollToTop) - buttonScrollToTop.pinEdges([.leading, .bottom], to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(horizontal: 8, vertical: 16)) + buttonScrollToTop.pinEdges([.leading, .bottom], to: view.safeAreaLayoutGuide, insets: isCompact ? UIEdgeInsets(horizontal: 8, vertical: 16) : UIEdgeInsets(.all, 20)) } private func setupFooterView() { From 6ead7b3d3fba8767d2ff92977d9cce1cb608871f Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 17:37:30 -0500 Subject: [PATCH 131/193] Add analytics --- WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift | 3 +++ .../Reader/Controllers/ReaderStreamViewController.swift | 2 +- .../Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index 379dfc2654d0..0e1f26a7c723 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -116,6 +116,7 @@ import Foundation case readerCommentTextCopied case readerPostContextMenuButtonTapped case readerAddSiteToFavoritesTapped + case readerButtonScrollToTopTapped // Stats - Empty Stats nudges case statsPublicizeNudgeShown @@ -816,6 +817,8 @@ import Foundation return "reader_post_context_menu_button_tapped" case .readerAddSiteToFavoritesTapped: return "reader_add_site_to_favorites_tapped" + case .readerButtonScrollToTopTapped: + return "reader_button_scroll_to_top_tapped" // Stats - Empty Stats nudges case .statsPublicizeNudgeShown: diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 28cdbc295e50..295854a3c0c1 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -1670,7 +1670,7 @@ extension ReaderStreamViewController: UITableViewDelegate, JPScrollViewDelegate layoutEmptyStateView() processJetpackBannerVisibility(scrollView) titleView.updateAlpha(in: scrollView) - buttonScrollToTop.setButtonHidden(scrollView.contentOffset.y < bounds.height / 3, animated: true) + buttonScrollToTop.setButtonHidden(scrollView.contentOffset.y < view.bounds.height / 3, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift b/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift index 328228e37fca..3ad73d91e0b7 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderButtonScrollToTop.swift @@ -14,6 +14,7 @@ final class ReaderButtonScrollToTop: UIButton { return ReaderButtonScrollToTop(configuration: configuration, primaryAction: .init { _ in closure() + WPAnalytics.track(.readerButtonScrollToTopTapped) }) } From 2069c49bb0a79df981c753431297ecf2236d2e84 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 7 Jan 2025 17:38:11 -0500 Subject: [PATCH 132/193] Update releaes notes --- RELEASE-NOTES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index a682de3d6fad..6b176a366550 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -19,6 +19,8 @@ * [*] Show selected filter in the Discover navigation bar [#23956] * [*] Enable fast deceleration for filters on the Discover tab [#23954] * [*] Disable universal links support for QR code login. You can only scan the codes using the app now. [#23953] +* [*] Add scroll-to-top button to Reader streams [#23957] + 25.6 ----- From 34775f3b4a6c4744105b404893809870534edea6 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 8 Jan 2025 10:46:45 +0100 Subject: [PATCH 133/193] Flatten a nested localized string to avoid `genstrings` failure --- .../Utility/WebViewController/WebKitViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift index 977f338193ce..5604f8c42a36 100644 --- a/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift +++ b/WordPress/Classes/Utility/WebViewController/WebKitViewController.swift @@ -57,7 +57,7 @@ class WebKitViewController: UIViewController, WebKitAuthenticatable { style: .plain, target: self, action: #selector(share)) - button.title = NSLocalizedString(SharedStrings.Button.share, comment: "Button label to share a web page") + button.title = NSLocalizedString("webKit.button.share", value: "Share", comment: "Button label to share a web page") return button }() @objc lazy var safariButton: UIBarButtonItem = { From b49185818bb2e6b44e21b00a1781ab130d8bd1c1 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 8 Jan 2025 11:02:12 +0100 Subject: [PATCH 134/193] Import `WordPressUI` in `SiteIconViewModelTests` Otherwise, it won't build --- WordPress/WordPressTest/SiteIconViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/WordPressTest/SiteIconViewModelTests.swift b/WordPress/WordPressTest/SiteIconViewModelTests.swift index c9bfb2fc67f8..433f565e8116 100644 --- a/WordPress/WordPressTest/SiteIconViewModelTests.swift +++ b/WordPress/WordPressTest/SiteIconViewModelTests.swift @@ -1,6 +1,6 @@ import UIKit import XCTest - +import WordPressUI @testable import WordPress final class SiteIconViewModelTests: XCTestCase { From ab4716965d780c842a191058d93c4145b6c2820c Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 09:19:41 -0500 Subject: [PATCH 135/193] Add initial MediaPicker implementation --- .../Extensions/SwiftUI+Extensions.swift | 20 +++++ .../Media/MediaPicker/MediaPicker.swift | 80 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift diff --git a/Modules/Sources/WordPressUI/Extensions/SwiftUI+Extensions.swift b/Modules/Sources/WordPressUI/Extensions/SwiftUI+Extensions.swift index 91d904394483..5e39a6ad8f8a 100644 --- a/Modules/Sources/WordPressUI/Extensions/SwiftUI+Extensions.swift +++ b/Modules/Sources/WordPressUI/Extensions/SwiftUI+Extensions.swift @@ -1,5 +1,25 @@ +import UIKit import SwiftUI public extension EdgeInsets { static let zero = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) } + +private struct PresentingViewControllerKey: EnvironmentKey { + static let defaultValue = WeakEnvironmentValueWrapper() +} + +extension EnvironmentValues { + public var presentingViewController: UIViewController? { + get { + self[PresentingViewControllerKey.self].value ?? UIViewController.topViewController + } + set { + self[PresentingViewControllerKey.self].value = newValue + } + } +} + +private final class WeakEnvironmentValueWrapper { + weak var value: T? +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift new file mode 100644 index 000000000000..8924c3197bb9 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -0,0 +1,80 @@ +import SwiftUI +import Photos +import PhotosUI +import WordPressUI + +/// A media picker menu. +/// +/// - note: Use `.environment(\.presentingViewController, <#vc#>)` to pass the +/// presenting view controller. If not provided, the current top view controller +/// is used. +struct MediaPicker: View { + var filter: MediaPickerMenu.MediaFilter? + var isMultipleSelectionEnabled: Bool = false + var initialSelection: [Media] = [] + + @Environment(\.presentingViewController) var presentingViewController + + var body: some View { + Menu { + actions + } label: { + // TODO: make customizable + Text("Set Image") + } + } + + @ViewBuilder + private var actions: some View { + let menu = MediaPickerMenu(viewController: presentingViewController ?? UIViewController(), filter: filter) + let delegate = MediaPickerMenuDelegate() + let actions: [UIAction] = [ + menu.makePhotosAction(delegate: delegate), + // TODO: implement +// menu.makeCameraAction(delegate: delegate), +// menu.makeImagePlaygroundAction(delegate: delegate), + // menu.makeSiteMediaAction(blog: self.apost.blog, delegate: delegate) + ] + ForEach(actions, id: \.self) { action in + Button.init { + action.performWithSender(nil, target: nil) + } label: { + Label { + Text(action.title) + } icon: { + action.image.map(Image.init) + } + + } + } + } +} + +enum MediaPickerSource { + /// Apple Photos app. + case photos +} + +private final class MediaPickerMenuDelegate: NSObject {} + +extension MediaPickerMenuDelegate: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + // TODO: + } + + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + // TODO: + } +} + +extension MediaPickerMenuDelegate: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + // TODO: + } +} + +extension MediaPickerMenuDelegate: ImagePlaygroundPickerDelegate { + func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) { + // TODO: + } +} From 6ddf9638e7877f36b4b4d1836d46dd92df6a357c Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 09:27:02 -0500 Subject: [PATCH 136/193] Add initial PostSettingsFeaturedImageCell implementation --- .../PostSettingsViewController+Swift.swift | 10 ++++ .../Post/PostSettingsViewController.m | 59 ++++++++----------- .../Views/PostSettingsFeaturedImageCell.swift | 27 +++++++++ 3 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index c8def4b258e0..3e005adc573d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -254,6 +254,16 @@ extension PostSettingsViewController { // MARK: - PostSettingsViewController (Featued Image) extension PostSettingsViewController { + @objc func makeFeaturedImageCell() -> UITableViewCell { + // TODO: reuse cell? + let cell = UITableViewCell(style: .default, reuseIdentifier: nil) + cell.contentConfiguration = UIHostingConfiguration { + PostSettingsFeaturedImageCell() + .environment(\.presentingViewController, self) + } + return cell + } + @objc func showFeaturedImageSelector() { guard let featuredImage = apost.featuredImage else { return wpAssertionFailure("featured image missing") diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 711d82dbce99..7b2cd493ca88 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -38,8 +38,6 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { PostSettingsRowParentPage }; -static CGFloat CellHeight = 44.0f; - static NSString *const PostSettingsAnalyticsTrackingSource = @"post_settings"; static NSString *const TableViewActivityCellIdentifier = @"TableViewActivityCellIdentifier"; static NSString *const TableViewProgressCellIdentifier = @"TableViewProgressCellIdentifier"; @@ -436,14 +434,6 @@ - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSIntege - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - NSInteger sectionId = [[self.sections objectAtIndex:indexPath.section] integerValue]; - - if (sectionId == PostSettingsSectionFeaturedImage) { - if ([self isUploadingMedia]) { - return CellHeight; - } - } - return UITableViewAutomaticDimension; } @@ -644,29 +634,32 @@ - (UITableViewCell *)configurePostFormatCellForIndexPath:(NSIndexPath *)indexPat - (UITableViewCell *)configureFeaturedImageCellForIndexPath:(NSIndexPath *)indexPath { - if (!self.apost.featuredImage && !self.isUploadingMedia) { - return [self cellForSetFeaturedImage]; - - } else if (self.isUploadingMedia || self.apost.featuredImage.remoteStatus == MediaRemoteStatusPushing) { - // Is featured Image set on the post and it's being pushed to the server? - if (!self.isUploadingMedia) { - self.isUploadingMedia = YES; - [self setupObservingOfMedia:self.apost.featuredImage]; - } - self.featuredImage = nil; - return [self cellForFeaturedImageUploadProgressAtIndexPath:indexPath]; - - } else if (self.apost.featuredImage && self.apost.featuredImage.remoteStatus == MediaRemoteStatusFailed) { - // Do we have an feature image set and for some reason the upload failed? - return [self cellForFeaturedImageError]; - } else { - NSURL *featuredURL = [self urlForFeaturedImage]; - if (!featuredURL) { - return [self cellForSetFeaturedImage]; - } - - return [self cellForFeaturedImageWithURL:featuredURL atIndexPath:indexPath]; - } + return [self makeFeaturedImageCell]; + + // TODO: remove unused code +// if (!self.apost.featuredImage && !self.isUploadingMedia) { +// return [self cellForSetFeaturedImage]; +// +// } else if (self.isUploadingMedia || self.apost.featuredImage.remoteStatus == MediaRemoteStatusPushing) { +// // Is featured Image set on the post and it's being pushed to the server? +// if (!self.isUploadingMedia) { +// self.isUploadingMedia = YES; +// [self setupObservingOfMedia:self.apost.featuredImage]; +// } +// self.featuredImage = nil; +// return [self cellForFeaturedImageUploadProgressAtIndexPath:indexPath]; +// +// } else if (self.apost.featuredImage && self.apost.featuredImage.remoteStatus == MediaRemoteStatusFailed) { +// // Do we have an feature image set and for some reason the upload failed? +// return [self cellForFeaturedImageError]; +// } else { +// NSURL *featuredURL = [self urlForFeaturedImage]; +// if (!featuredURL) { +// return [self cellForSetFeaturedImage]; +// } +// +// return [self cellForFeaturedImageWithURL:featuredURL atIndexPath:indexPath]; +// } } - (UITableViewCell *)configureStickyPostCellForIndexPath:(NSIndexPath *)indexPath diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift new file mode 100644 index 000000000000..f2795f6bc8be --- /dev/null +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -0,0 +1,27 @@ +import SwiftUI +import WordPressUI + +struct PostSettingsFeaturedImageCell: View { +// @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel +// weak var presentingViewController: UIViewController? + + var body: some View { + MediaPicker(filter: .images) + } +} + +final class PostSettingsFeaturedImageViewModel: ObservableObject { + @Published var isUploading = false + + @Published private var state: State = .empty + + enum State { + case empty + // TODO: show PostMediaUploadsView + case uploading + } +} + +private enum Strings { + static let buttonSetFeaturedImage = NSLocalizedString("postSettings.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") +} From 3338325e59e78174d706a047bb46b48d7cf980cb Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 10:03:34 -0500 Subject: [PATCH 137/193] Add configurable MediaPicker content --- .../Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift | 5 +++-- .../Post/Views/PostSettingsFeaturedImageCell.swift | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 8924c3197bb9..72a1dc235ebc 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -13,14 +13,15 @@ struct MediaPicker: View { var isMultipleSelectionEnabled: Bool = false var initialSelection: [Media] = [] + @ViewBuilder var content: () -> Content + @Environment(\.presentingViewController) var presentingViewController var body: some View { Menu { actions } label: { - // TODO: make customizable - Text("Set Image") + content() } } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index f2795f6bc8be..76a02a1eb4c1 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -6,7 +6,11 @@ struct PostSettingsFeaturedImageCell: View { // weak var presentingViewController: UIViewController? var body: some View { - MediaPicker(filter: .images) + MediaPicker(filter: .images) { + Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) // Make the whole cell tappable + } } } From bcd27383cf366942c51ad1e2a700dae62bca925c Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 10:10:51 -0500 Subject: [PATCH 138/193] Add ViewModel to PostSettingsFeaturedImageCell --- .../Post/PostSettingsViewController+Swift.swift | 4 ++-- .../ViewRelated/Post/PostSettingsViewController.m | 5 ++++- .../Post/Views/PostSettingsFeaturedImageCell.swift | 13 ++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 3e005adc573d..e3ca89fa28f6 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -254,11 +254,11 @@ extension PostSettingsViewController { // MARK: - PostSettingsViewController (Featued Image) extension PostSettingsViewController { - @objc func makeFeaturedImageCell() -> UITableViewCell { + @objc func makeFeaturedImageCell(viewModel: PostSettingsFeaturedImageViewModel) -> UITableViewCell { // TODO: reuse cell? let cell = UITableViewCell(style: .default, reuseIdentifier: nil) cell.contentConfiguration = UIHostingConfiguration { - PostSettingsFeaturedImageCell() + PostSettingsFeaturedImageCell(viewModel: viewModel) .environment(\.presentingViewController, self) } return cell diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 7b2cd493ca88..07edc56eda2c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -63,6 +63,8 @@ @interface PostSettingsViewController () Date: Wed, 8 Jan 2025 10:16:26 -0500 Subject: [PATCH 139/193] Add reuseIdentifier for featured image cells --- .../PostSettingsViewController+Swift.swift | 5 +---- .../Post/PostSettingsViewController.m | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index e3ca89fa28f6..5f8ce5ce606b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -254,14 +254,11 @@ extension PostSettingsViewController { // MARK: - PostSettingsViewController (Featued Image) extension PostSettingsViewController { - @objc func makeFeaturedImageCell(viewModel: PostSettingsFeaturedImageViewModel) -> UITableViewCell { - // TODO: reuse cell? - let cell = UITableViewCell(style: .default, reuseIdentifier: nil) + @objc func configureFeaturedImageCell(cell: UITableViewCell, viewModel: PostSettingsFeaturedImageViewModel) { cell.contentConfiguration = UIHostingConfiguration { PostSettingsFeaturedImageCell(viewModel: viewModel) .environment(\.presentingViewController, self) } - return cell } @objc func showFeaturedImageSelector() { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 07edc56eda2c..53143db6e8dc 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -121,7 +121,7 @@ - (void)viewDidLoad [self.tableView registerNib:[UINib nibWithNibName:@"WPTableViewActivityCell" bundle:nil] forCellReuseIdentifier:TableViewActivityCellIdentifier]; [self.tableView registerClass:[WPProgressTableViewCell class] forCellReuseIdentifier:TableViewProgressCellIdentifier]; - [self.tableView registerClass:[PostFeaturedImageCell class] forCellReuseIdentifier:TableViewFeaturedImageCellIdentifier]; + [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TableViewFeaturedImageCellIdentifier]; [self.tableView registerClass:[SwitchTableViewCell class] forCellReuseIdentifier:TableViewToggleCellIdentifier]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TableViewGenericCellIdentifier]; @@ -451,7 +451,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } else if (sec == PostSettingsSectionMeta) { cell = [self configureMetaPostMetaCellForIndexPath:indexPath]; } else if (sec == PostSettingsSectionFeaturedImage) { - cell = [self configureFeaturedImageCellForIndexPath:indexPath]; + cell = [self makeFeaturedImageCellForIndexPath:indexPath]; } else if (sec == PostSettingsSectionStickyPost) { cell = [self configureStickyPostCellForIndexPath:indexPath]; } else if (sec == PostSettingsSectionShare || sec == PostSettingsSectionDisabledTwitter) { @@ -635,9 +635,12 @@ - (UITableViewCell *)configurePostFormatCellForIndexPath:(NSIndexPath *)indexPat return cell; } -- (UITableViewCell *)configureFeaturedImageCellForIndexPath:(NSIndexPath *)indexPath +- (UITableViewCell *)makeFeaturedImageCellForIndexPath:(NSIndexPath *)indexPath { - return [self makeFeaturedImageCellWithViewModel:self.featuredImageViewModel]; + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; + [self configureFeaturedImageCellWithCell:cell viewModel:self.featuredImageViewModel]; + cell.tag = PostSettingsRowFeaturedImage; + return cell; // TODO: remove unused code // if (!self.apost.featuredImage && !self.isUploadingMedia) { @@ -705,10 +708,13 @@ - (UITableViewCell *)cellForFeaturedImageUploadProgressAtIndexPath:(NSIndexPath - (UITableViewCell *)cellForFeaturedImageWithURL:(nonnull NSURL *)featuredURL atIndexPath:(NSIndexPath *)indexPath { - PostFeaturedImageCell *featuredImageCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; - [featuredImageCell setImageWithURL:featuredURL post:self.apost]; - featuredImageCell.tag = PostSettingsRowFeaturedImage; - return featuredImageCell; + // TODO: remove + return [UITableViewCell new]; + +// PostFeaturedImageCell *featuredImageCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; +// [featuredImageCell setImageWithURL:featuredURL post:self.apost]; +// featuredImageCell.tag = PostSettingsRowFeaturedImage; +// return featuredImageCell; } - (nullable NSURL *)urlForFeaturedImage { From dbfb9dbc286c5921f70bdb37bfef21043e591eab Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 10:35:02 -0500 Subject: [PATCH 140/193] Pass selection from MediaPicker to PostSettingsFeaturedImageViewModel --- .../Media/MediaPicker/MediaPicker.swift | 44 ++++++++----------- .../MediaPickerMenuController.swift | 28 ++++++++++++ .../Views/PostSettingsFeaturedImageCell.swift | 9 +++- 3 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 72a1dc235ebc..a72950ce35c3 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -1,7 +1,7 @@ import SwiftUI +import WordPressUI import Photos import PhotosUI -import WordPressUI /// A media picker menu. /// @@ -12,9 +12,12 @@ struct MediaPicker: View { var filter: MediaPickerMenu.MediaFilter? var isMultipleSelectionEnabled: Bool = false var initialSelection: [Media] = [] + var onSelection: (([MediaPickerSelection]) -> Void)? @ViewBuilder var content: () -> Content + @StateObject private var viewModel = MediaPickerViewModel() + @Environment(\.presentingViewController) var presentingViewController var body: some View { @@ -28,9 +31,9 @@ struct MediaPicker: View { @ViewBuilder private var actions: some View { let menu = MediaPickerMenu(viewController: presentingViewController ?? UIViewController(), filter: filter) - let delegate = MediaPickerMenuDelegate() + let controller = makeMediaPickerMenuController() let actions: [UIAction] = [ - menu.makePhotosAction(delegate: delegate), + menu.makePhotosAction(delegate: controller), // TODO: implement // menu.makeCameraAction(delegate: delegate), // menu.makeImagePlaygroundAction(delegate: delegate), @@ -49,33 +52,24 @@ struct MediaPicker: View { } } } -} - -enum MediaPickerSource { - /// Apple Photos app. - case photos -} -private final class MediaPickerMenuDelegate: NSObject {} - -extension MediaPickerMenuDelegate: PHPickerViewControllerDelegate { - public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - // TODO: + private func makeMediaPickerMenuController() -> MediaPickerMenuController { + let controller = MediaPickerMenuController() + controller.onSelection = onSelection + viewModel.controller = controller // Needs to be retained + return controller } +} - func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - // TODO: - } +private final class MediaPickerViewModel: ObservableObject { + var controller: MediaPickerMenuController? } -extension MediaPickerMenuDelegate: SiteMediaPickerViewControllerDelegate { - func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { - // TODO: - } +enum MediaPickerSource { + /// Apple Photos app. + case applePhotos } -extension MediaPickerMenuDelegate: ImagePlaygroundPickerDelegate { - func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) { - // TODO: - } +enum MediaPickerSelection { + case phPickerResult(PHPickerResult) } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift new file mode 100644 index 000000000000..04dc0b46e24e --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift @@ -0,0 +1,28 @@ +import Photos +import PhotosUI + +final class MediaPickerMenuController: NSObject { + var onSelection: (([MediaPickerSelection]) -> Void)? +} + +extension MediaPickerMenuController: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.presentingViewController?.dismiss(animated: true) { + if !results.isEmpty { + self.onSelection?(results.map { MediaPickerSelection.phPickerResult($0) }) + } + } + } +} + +extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate { + func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { + // TODO: + } +} + +extension MediaPickerMenuController: ImagePlaygroundPickerDelegate { + func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) { + // TODO: + } +} diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 68a768913759..1017aef5a38b 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -5,7 +5,7 @@ struct PostSettingsFeaturedImageCell: View { @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel var body: some View { - MediaPicker(filter: .images) { + MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) // Make the whole cell tappable @@ -27,6 +27,13 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { // TODO: show PostMediaUploadsView case uploading } + + func setFeaturedImage(from items: [MediaPickerSelection]) { + guard let item = items.first else { + return wpAssertionFailure("selection is empty") + } + + } } private enum Strings { From d23bf0e239a94e5e7f29c940eb2a77a04d33a7b1 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 11:05:55 -0500 Subject: [PATCH 141/193] Show upload status using PostMediaUploadItemView --- .../Media/MediaPicker/MediaPicker.swift | 7 ++++ .../PostSettingsViewController+Swift.swift | 2 +- .../Post/PostSettingsViewController.m | 2 +- .../Post/Views/PostMediaUploadsView.swift | 2 +- .../Views/PostMediaUploadsViewModel.swift | 17 ++++++++-- .../Views/PostSettingsFeaturedImageCell.swift | 32 ++++++++++++------- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index a72950ce35c3..92afc4026ad9 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -72,4 +72,11 @@ enum MediaPickerSource { enum MediaPickerSelection { case phPickerResult(PHPickerResult) + + var exportableAsset: ExportableAsset { + switch self { + case .phPickerResult(let result): + return result.itemProvider + } + } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 5f8ce5ce606b..2972b02c1492 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -263,7 +263,7 @@ extension PostSettingsViewController { @objc func showFeaturedImageSelector() { guard let featuredImage = apost.featuredImage else { - return wpAssertionFailure("featured image missing") + return } let lightboxVC = LightboxViewController(media: featuredImage) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 53143db6e8dc..86bb34211c8c 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -94,7 +94,7 @@ - (instancetype)initWithPost:(AbstractPost *)aPost self.apost = aPost; self.unsupportedConnections = @[]; self.enabledConnections = [NSMutableArray array]; - self.featuredImageViewModel = [[PostSettingsFeaturedImageViewModel alloc] initWithBlog:aPost.blog]; + self.featuredImageViewModel = [[PostSettingsFeaturedImageViewModel alloc] initWithPost:aPost]; } return self; } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift index 1268760cf984..f03e3c46c96e 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift @@ -51,7 +51,7 @@ struct PostMediaUploadsView: View { } } -private struct PostMediaUploadItemView: View { +struct PostMediaUploadItemView: View { @ObservedObject var viewModel: PostMediaUploadItemViewModel var body: some View { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift index 2a76e119ddfc..af99fc9be729 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift @@ -27,7 +27,7 @@ final class PostMediaUploadsViewModel: ObservableObject { self.uploads = Array(post.media).filter(\.isUploadNeeded).sorted { ($0.creationDate ?? .now) < ($1.creationDate ?? .now) }.map { - PostMediaUploadItemViewModel(media: $0, coordinator: coordinator) + PostMediaUploadItemViewModel(media: $0, coordinator: coordinator, isAutoUpdateEnabled: false) } coordinator.uploadMedia(for: post) @@ -115,6 +115,8 @@ final class PostMediaUploadItemViewModel: ObservableObject, Identifiable { return nil } + private weak var updateTimer: Timer? + enum State { case uploading case failed(Error) @@ -122,10 +124,15 @@ final class PostMediaUploadItemViewModel: ObservableObject, Identifiable { } deinit { + updateTimer?.invalidate() retryTimer?.invalidate() } - init(media: Media, coordinator: MediaCoordinator) { + init( + media: Media, + coordinator: MediaCoordinator, + isAutoUpdateEnabled: Bool = true + ) { self.media = media self.coordinator = coordinator @@ -133,6 +140,12 @@ final class PostMediaUploadItemViewModel: ObservableObject, Identifiable { update() NotificationCenter.default.addObserver(self, selector: #selector(didUpdateReachability), name: .reachabilityChanged, object: nil) + + if isAutoUpdateEnabled { + updateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in + self?.update() + } + } } fileprivate func update() { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 1017aef5a38b..22279dd6be87 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -5,34 +5,44 @@ struct PostSettingsFeaturedImageCell: View { @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel var body: some View { - MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { - Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) // Make the whole cell tappable + switch viewModel.state { + case .empty: + MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { + Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) // Make the whole cell tappable + } + case .uploading(let viewModel): + PostMediaUploadItemView(viewModel: viewModel) } } } final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { - @Published private var state: State = .empty + @Published private(set) var state: State = .empty - let blog: Blog + let post: AbstractPost - @objc init(blog: Blog) { - self.blog = blog + private let coordinator = MediaCoordinator.shared + + @objc init(post: AbstractPost) { + self.post = post } enum State { case empty - // TODO: show PostMediaUploadsView - case uploading + case uploading(PostMediaUploadItemViewModel) } func setFeaturedImage(from items: [MediaPickerSelection]) { guard let item = items.first else { return wpAssertionFailure("selection is empty") } - + guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { + return wpAssertionFailure("failed to add media to post") + } + let viewModel = PostMediaUploadItemViewModel(media: media, coordinator: coordinator) + self.state = .uploading(viewModel) } } From 6c9f7e75facd3da2ba800c7802797841e281b82b Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 11:06:23 -0500 Subject: [PATCH 142/193] Rename MediaUploadItemViewModel --- .../ViewRelated/Post/Views/PostMediaUploadsView.swift | 2 +- .../ViewRelated/Post/Views/PostMediaUploadsViewModel.swift | 6 +++--- .../Post/Views/PostSettingsFeaturedImageCell.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift index f03e3c46c96e..6e223ceda051 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift @@ -52,7 +52,7 @@ struct PostMediaUploadsView: View { } struct PostMediaUploadItemView: View { - @ObservedObject var viewModel: PostMediaUploadItemViewModel + @ObservedObject var viewModel: MediaUploadItemViewModel var body: some View { HStack(alignment: .center, spacing: 0) { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift index af99fc9be729..99424b26ebbc 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift @@ -4,7 +4,7 @@ import Combine /// Manages media upload for the given revision of the post. final class PostMediaUploadsViewModel: ObservableObject { - private(set) var uploads: [PostMediaUploadItemViewModel] + private(set) var uploads: [MediaUploadItemViewModel] @Published private(set) var totalFileSize: Int64 = 0 @Published private(set) var fractionCompleted = 0.0 @@ -27,7 +27,7 @@ final class PostMediaUploadsViewModel: ObservableObject { self.uploads = Array(post.media).filter(\.isUploadNeeded).sorted { ($0.creationDate ?? .now) < ($1.creationDate ?? .now) }.map { - PostMediaUploadItemViewModel(media: $0, coordinator: coordinator, isAutoUpdateEnabled: false) + MediaUploadItemViewModel(media: $0, coordinator: coordinator, isAutoUpdateEnabled: false) } coordinator.uploadMedia(for: post) @@ -68,7 +68,7 @@ final class PostMediaUploadsViewModel: ObservableObject { } /// Manages individual media upload. -final class PostMediaUploadItemViewModel: ObservableObject, Identifiable { +final class MediaUploadItemViewModel: ObservableObject, Identifiable { @Published private(set) var state: State = .uploading let media: Media diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 22279dd6be87..ef4ca2648aff 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -31,7 +31,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { enum State { case empty - case uploading(PostMediaUploadItemViewModel) + case uploading(MediaUploadItemViewModel) } func setFeaturedImage(from items: [MediaPickerSelection]) { @@ -41,7 +41,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { return wpAssertionFailure("failed to add media to post") } - let viewModel = PostMediaUploadItemViewModel(media: media, coordinator: coordinator) + let viewModel = MediaUploadItemViewModel(media: media, coordinator: coordinator) self.state = .uploading(viewModel) } } From b90111e175c8886ddfdef9feb597d9e3d9dcccdb Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 11:33:36 -0500 Subject: [PATCH 143/193] Add PostSettingsFeaturedImageUploadView to show upload progress --- .../PostSettingsViewController+Swift.swift | 1 + .../Views/PostSettingsFeaturedImageCell.swift | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 2972b02c1492..b845c728ed23 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -259,6 +259,7 @@ extension PostSettingsViewController { PostSettingsFeaturedImageCell(viewModel: viewModel) .environment(\.presentingViewController, self) } + cell.selectionStyle = .none } @objc func showFeaturedImageSelector() { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index ef4ca2648aff..2290fe07cb4c 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -13,7 +13,67 @@ struct PostSettingsFeaturedImageCell: View { .contentShape(Rectangle()) // Make the whole cell tappable } case .uploading(let viewModel): - PostMediaUploadItemView(viewModel: viewModel) + PostSettingsFeaturedImageUploadView(viewModel: viewModel, onCancelTapped: { + self.viewModel.didCancelUpload() + viewModel.buttonCancelTapped() + }) + } + } +} + +private struct PostSettingsFeaturedImageUploadView: View { + @ObservedObject var viewModel: MediaUploadItemViewModel + + var onCancelTapped: () -> Void + + var body: some View { + HStack(alignment: .center, spacing: 0) { + ProgressView() + .padding(.trailing, 12) + + VStack(alignment: .leading) { + Text(Strings.uploading) + .font(.subheadline) + .lineLimit(1) + Text(viewModel.details) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(3) + } + + Spacer(minLength: 8) + + HStack(alignment: .center, spacing: 8) { + switch viewModel.state { + case .uploading: + MediaUploadProgressView(progress: viewModel.fractionCompleted) + .padding(.trailing, 4) // To align with the exlamation mark + menu + case .failed: + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.red) + menu + case .uploaded: + EmptyView() // processing + } + } + } + } + + private var menu: some View { + Menu { + if viewModel.error != nil { + Button(action: viewModel.buttonRetryTapped) { + Label(Strings.retryUpload, systemImage: "arrow.clockwise") + } + } + Button(role: .destructive, action: onCancelTapped) { + Label(Strings.cancelUpload, systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis") + .font(.subheadline) + .tint(.secondary) } } } @@ -44,8 +104,16 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { let viewModel = MediaUploadItemViewModel(media: media, coordinator: coordinator) self.state = .uploading(viewModel) } + + func didCancelUpload() { + // TODO: restore to the previous state + state = .empty + } } private enum Strings { static let buttonSetFeaturedImage = NSLocalizedString("postSettings.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") + static let uploading = NSLocalizedString("postSettings.featuredImage.uploading", value: "Uploading…", comment: "Post Settings") + static let retryUpload = NSLocalizedString("postSettings.featuredImage.retryUpload", value: "Retry Upload", comment: "Retry (single) upload button in Post Settings / Featuerd Image cell") + static let cancelUpload = NSLocalizedString("postSettings.featuredImage.cancelUpload", value: "Cancel Upload", comment: "Cancel (single) upload button in Post Settings / Featuerd Image cell") } From 20cae40e73a7cef5de78fb0fdf98f44055b14ae5 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 11:45:00 -0500 Subject: [PATCH 144/193] Simlify how the app shows media upload status --- .../Post/Views/PostMediaUploadsView.swift | 4 +- .../Views/PostMediaUploadsViewModel.swift | 21 +---- .../Views/PostSettingsFeaturedImageCell.swift | 88 ++++++------------- 3 files changed, 33 insertions(+), 80 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift index 6e223ceda051..1268760cf984 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsView.swift @@ -51,8 +51,8 @@ struct PostMediaUploadsView: View { } } -struct PostMediaUploadItemView: View { - @ObservedObject var viewModel: MediaUploadItemViewModel +private struct PostMediaUploadItemView: View { + @ObservedObject var viewModel: PostMediaUploadItemViewModel var body: some View { HStack(alignment: .center, spacing: 0) { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift index 99424b26ebbc..2a76e119ddfc 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostMediaUploadsViewModel.swift @@ -4,7 +4,7 @@ import Combine /// Manages media upload for the given revision of the post. final class PostMediaUploadsViewModel: ObservableObject { - private(set) var uploads: [MediaUploadItemViewModel] + private(set) var uploads: [PostMediaUploadItemViewModel] @Published private(set) var totalFileSize: Int64 = 0 @Published private(set) var fractionCompleted = 0.0 @@ -27,7 +27,7 @@ final class PostMediaUploadsViewModel: ObservableObject { self.uploads = Array(post.media).filter(\.isUploadNeeded).sorted { ($0.creationDate ?? .now) < ($1.creationDate ?? .now) }.map { - MediaUploadItemViewModel(media: $0, coordinator: coordinator, isAutoUpdateEnabled: false) + PostMediaUploadItemViewModel(media: $0, coordinator: coordinator) } coordinator.uploadMedia(for: post) @@ -68,7 +68,7 @@ final class PostMediaUploadsViewModel: ObservableObject { } /// Manages individual media upload. -final class MediaUploadItemViewModel: ObservableObject, Identifiable { +final class PostMediaUploadItemViewModel: ObservableObject, Identifiable { @Published private(set) var state: State = .uploading let media: Media @@ -115,8 +115,6 @@ final class MediaUploadItemViewModel: ObservableObject, Identifiable { return nil } - private weak var updateTimer: Timer? - enum State { case uploading case failed(Error) @@ -124,15 +122,10 @@ final class MediaUploadItemViewModel: ObservableObject, Identifiable { } deinit { - updateTimer?.invalidate() retryTimer?.invalidate() } - init( - media: Media, - coordinator: MediaCoordinator, - isAutoUpdateEnabled: Bool = true - ) { + init(media: Media, coordinator: MediaCoordinator) { self.media = media self.coordinator = coordinator @@ -140,12 +133,6 @@ final class MediaUploadItemViewModel: ObservableObject, Identifiable { update() NotificationCenter.default.addObserver(self, selector: #selector(didUpdateReachability), name: .reachabilityChanged, object: nil) - - if isAutoUpdateEnabled { - updateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in - self?.update() - } - } } fileprivate func update() { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 2290fe07cb4c..a70896cfbbe2 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -6,76 +6,40 @@ struct PostSettingsFeaturedImageCell: View { var body: some View { switch viewModel.state { - case .empty: - MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { - Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) // Make the whole cell tappable - } - case .uploading(let viewModel): - PostSettingsFeaturedImageUploadView(viewModel: viewModel, onCancelTapped: { - self.viewModel.didCancelUpload() - viewModel.buttonCancelTapped() - }) + case .empty: empty + case .uploading: uploading } } -} -private struct PostSettingsFeaturedImageUploadView: View { - @ObservedObject var viewModel: MediaUploadItemViewModel - - var onCancelTapped: () -> Void + private var empty: some View { + MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { + Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) // Make the whole cell tappable + } + } - var body: some View { + private var uploading: some View { HStack(alignment: .center, spacing: 0) { ProgressView() .padding(.trailing, 12) - VStack(alignment: .leading) { - Text(Strings.uploading) - .font(.subheadline) - .lineLimit(1) - Text(viewModel.details) - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(3) - } + Text(Strings.uploading) + .lineLimit(1) Spacer(minLength: 8) - HStack(alignment: .center, spacing: 8) { - switch viewModel.state { - case .uploading: - MediaUploadProgressView(progress: viewModel.fractionCompleted) - .padding(.trailing, 4) // To align with the exlamation mark - menu - case .failed: - Image(systemName: "exclamationmark.circle.fill") - .foregroundStyle(.red) - menu - case .uploaded: - EmptyView() // processing + Menu { + Button(role: .destructive, action: viewModel.onCancelTapped) { + Label(Strings.cancelUpload, systemImage: "trash") } + } label: { + Image(systemName: "ellipsis") + .font(.subheadline) + .tint(.secondary) } } } - - private var menu: some View { - Menu { - if viewModel.error != nil { - Button(action: viewModel.buttonRetryTapped) { - Label(Strings.retryUpload, systemImage: "arrow.clockwise") - } - } - Button(role: .destructive, action: onCancelTapped) { - Label(Strings.cancelUpload, systemImage: "trash") - } - } label: { - Image(systemName: "ellipsis") - .font(.subheadline) - .tint(.secondary) - } - } } final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { @@ -91,7 +55,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { enum State { case empty - case uploading(MediaUploadItemViewModel) + case uploading(Media) } func setFeaturedImage(from items: [MediaPickerSelection]) { @@ -101,13 +65,15 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { return wpAssertionFailure("failed to add media to post") } - let viewModel = MediaUploadItemViewModel(media: media, coordinator: coordinator) - self.state = .uploading(viewModel) + self.state = .uploading(media) } - func didCancelUpload() { - // TODO: restore to the previous state - state = .empty + func onCancelTapped() { + guard case .uploading(let media) = state else { + return + } + coordinator.cancelUploadAndDeleteMedia(media) + state = .empty // TODO: restore previous state } } From ff308551709adb65abeefa8732fe1f54afba97b5 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 11:59:55 -0500 Subject: [PATCH 145/193] Handle upload failure --- .../Views/PostSettingsFeaturedImageCell.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index a70896cfbbe2..9c2ee277a986 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -25,6 +25,7 @@ struct PostSettingsFeaturedImageCell: View { .padding(.trailing, 12) Text(Strings.uploading) + .foregroundStyle(.secondary) .lineLimit(1) Spacer(minLength: 8) @@ -47,6 +48,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { let post: AbstractPost + private var receipt: UUID? private let coordinator = MediaCoordinator.shared @objc init(post: AbstractPost) { @@ -65,14 +67,34 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { return wpAssertionFailure("failed to add media to post") } + self.receipt = coordinator.addObserver({ [weak self] _, state in + self?.didUpdateUploadState(state) + }, for: media) self.state = .uploading(media) } + private func didUpdateUploadState(_ state: MediaCoordinator.MediaState) { + switch state { + case .ended: + // TODO: upload media + break + case .failed(let error): + Notice(title: Strings.uploadFailed, message: error.localizedDescription).post() + reset() + default: + break + } + } + func onCancelTapped() { guard case .uploading(let media) = state else { return } coordinator.cancelUploadAndDeleteMedia(media) + reset() + } + + private func reset() { state = .empty // TODO: restore previous state } } @@ -80,6 +102,6 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { private enum Strings { static let buttonSetFeaturedImage = NSLocalizedString("postSettings.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") static let uploading = NSLocalizedString("postSettings.featuredImage.uploading", value: "Uploading…", comment: "Post Settings") - static let retryUpload = NSLocalizedString("postSettings.featuredImage.retryUpload", value: "Retry Upload", comment: "Retry (single) upload button in Post Settings / Featuerd Image cell") static let cancelUpload = NSLocalizedString("postSettings.featuredImage.cancelUpload", value: "Cancel Upload", comment: "Cancel (single) upload button in Post Settings / Featuerd Image cell") + static let uploadFailed = NSLocalizedString("postSettings.featuredImage.uploadFailed", value: "Failed to upload new featured image", comment: "Snackbar title") } From 495d9211db7433546b3b6439446836ca5445ceba Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 12:02:38 -0500 Subject: [PATCH 146/193] Implement featured image save --- .../Post/Views/PostSettingsFeaturedImageCell.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 9c2ee277a986..6a488445009f 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -67,17 +67,16 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { return wpAssertionFailure("failed to add media to post") } - self.receipt = coordinator.addObserver({ [weak self] _, state in - self?.didUpdateUploadState(state) + self.receipt = coordinator.addObserver({ [weak self] media, state in + self?.didUpdateUploadState(state, media: media) }, for: media) self.state = .uploading(media) } - private func didUpdateUploadState(_ state: MediaCoordinator.MediaState) { + private func didUpdateUploadState(_ state: MediaCoordinator.MediaState, media: Media) { switch state { case .ended: - // TODO: upload media - break + post.featuredImage = media case .failed(let error): Notice(title: Strings.uploadFailed, message: error.localizedDescription).post() reset() From c83378bf89d7e5ea4e4ecd912091ba575de5ac34 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 12:48:34 -0500 Subject: [PATCH 147/193] Add support for showing a selected featured image --- .../PostSettingsViewController+Swift.swift | 8 ++- .../Views/PostSettingsFeaturedImageCell.swift | 64 +++++++++++-------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index b845c728ed23..8ab193aca8b8 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -255,10 +255,14 @@ extension PostSettingsViewController { extension PostSettingsViewController { @objc func configureFeaturedImageCell(cell: UITableViewCell, viewModel: PostSettingsFeaturedImageViewModel) { - cell.contentConfiguration = UIHostingConfiguration { - PostSettingsFeaturedImageCell(viewModel: viewModel) + var configuration = UIHostingConfiguration { + PostSettingsFeaturedImageCell(post: apost, viewModel: viewModel) .environment(\.presentingViewController, self) } + if viewModel.featuredImageURL != nil { + configuration = configuration.margins(.all, 0) + } + cell.contentConfiguration = configuration cell.selectionStyle = .none } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 6a488445009f..7ad9d356f1a8 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -1,21 +1,23 @@ import SwiftUI +import AsyncImageKit import WordPressUI struct PostSettingsFeaturedImageCell: View { + @ObservedObject var post: AbstractPost @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel var body: some View { - switch viewModel.state { - case .empty: empty - case .uploading: uploading - } - } - - private var empty: some View { - MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { - Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) // Make the whole cell tappable + if let imageURL = viewModel.featuredImageURL { + FeaturedImageView(imageURL: imageURL, post: viewModel.post) + .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) + } else if viewModel.upload != nil { + uploading + } else { + MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { + Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) // Make the whole cell tappable + } } } @@ -43,8 +45,24 @@ struct PostSettingsFeaturedImageCell: View { } } +private struct FeaturedImageView: UIViewRepresentable { + let imageURL: URL + let post: AbstractPost + + func makeUIView(context: Context) -> AsyncImageView { + let imageView = AsyncImageView() + imageView.configuration.loadingStyle = .spinner + imageView.setImage(with: imageURL, host: MediaHost(post)) + return imageView + } + + func updateUIView(_ view: AsyncImageView, context: Context) { + // Do nothing + } +} + final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { - @Published private(set) var state: State = .empty + @Published private(set) var upload: Media? let post: AbstractPost @@ -55,11 +73,6 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { self.post = post } - enum State { - case empty - case uploading(Media) - } - func setFeaturedImage(from items: [MediaPickerSelection]) { guard let item = items.first else { return wpAssertionFailure("selection is empty") @@ -70,31 +83,30 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { self.receipt = coordinator.addObserver({ [weak self] media, state in self?.didUpdateUploadState(state, media: media) }, for: media) - self.state = .uploading(media) + self.upload = media } private func didUpdateUploadState(_ state: MediaCoordinator.MediaState, media: Media) { switch state { case .ended: + wpAssert(media.remoteURL != nil) post.featuredImage = media case .failed(let error): Notice(title: Strings.uploadFailed, message: error.localizedDescription).post() - reset() + upload = nil default: break } } func onCancelTapped() { - guard case .uploading(let media) = state else { - return - } - coordinator.cancelUploadAndDeleteMedia(media) - reset() + guard let upload else { return } + coordinator.cancelUploadAndDeleteMedia(upload) + self.upload = nil } - private func reset() { - state = .empty // TODO: restore previous state + var featuredImageURL: URL? { + post.featuredImageURL ?? post.featuredImage?.remoteURL.flatMap(URL.init) } } From 8cdce43f8a0e16b490ee5cc46daf3fcead0e3435 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 13:10:56 -0500 Subject: [PATCH 148/193] Add support for removing featured image --- .../Post/PostSettingsViewController.m | 2 ++ .../Views/PostSettingsFeaturedImageCell.swift | 34 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 86bb34211c8c..f88121a4b6a9 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -133,6 +133,8 @@ - (void)viewDidLoad self.tableView.accessibilityIdentifier = @"SettingsTable"; self.isUploadingMedia = NO; + self.featuredImageViewModel.tableView = self.tableView; + _blogService = [[BlogService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; [self setupPostDateFormatter]; diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 7ad9d356f1a8..de2e3d42a389 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -10,6 +10,16 @@ struct PostSettingsFeaturedImageCell: View { if let imageURL = viewModel.featuredImageURL { FeaturedImageView(imageURL: imageURL, post: viewModel.post) .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) + .overlay(alignment: .topTrailing) { + Menu { + Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) + } label: { + Image(systemName: "ellipsis.circle.fill") + .foregroundStyle(Color(.label), Color(.secondarySystemBackground)) + .font(.title) + .padding(8) + } + } } else if viewModel.upload != nil { uploading } else { @@ -33,7 +43,7 @@ struct PostSettingsFeaturedImageCell: View { Spacer(minLength: 8) Menu { - Button(role: .destructive, action: viewModel.onCancelTapped) { + Button(role: .destructive, action: viewModel.buttonCancelTapped) { Label(Strings.cancelUpload, systemImage: "trash") } } label: { @@ -66,9 +76,15 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { let post: AbstractPost + var featuredImageURL: URL? { + post.featuredImage?.remoteURL.flatMap(URL.init) + } + private var receipt: UUID? private let coordinator = MediaCoordinator.shared + @objc weak var tableView: UITableView? + @objc init(post: AbstractPost) { self.post = post } @@ -90,7 +106,10 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { switch state { case .ended: wpAssert(media.remoteURL != nil) - post.featuredImage = media + UIView.performWithoutAnimation { + post.featuredImage = media + tableView?.reloadData() + } case .failed(let error): Notice(title: Strings.uploadFailed, message: error.localizedDescription).post() upload = nil @@ -99,14 +118,19 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { } } - func onCancelTapped() { + func buttonCancelTapped() { guard let upload else { return } coordinator.cancelUploadAndDeleteMedia(upload) self.upload = nil } - var featuredImageURL: URL? { - post.featuredImageURL ?? post.featuredImage?.remoteURL.flatMap(URL.init) + func buttonRemoveTapped() { + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: [ + "via": "settings", "action": "removed" + ]) + + post.featuredImage = nil + tableView?.reloadData() } } From 477227ad9253dee5e96bb38f0f1ff8dcff2b66b5 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 13:33:48 -0500 Subject: [PATCH 149/193] Simplify lightbox --- .../PostSettingsViewController+Swift.swift | 29 ++----------------- .../Post/PostSettingsViewController.m | 11 ------- .../PostSettingsViewController_Internal.h | 2 -- .../Views/PostSettingsFeaturedImageCell.swift | 4 +++ 4 files changed, 6 insertions(+), 40 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 8ab193aca8b8..1b4b534c2dca 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -272,36 +272,11 @@ extension PostSettingsViewController { } 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) + lightboxVC.configureZoomTransition() + present(lightboxVC, 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 f88121a4b6a9..c1141bf7c2e7 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1166,15 +1166,4 @@ - (void)postCategoriesViewController:(PostCategoriesViewController *)controller } } -#pragma mark - Featured Image - -- (void)removeFeaturedImage { - [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; - self.featuredImage = 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 25cd07f874ba..d6ed3545b9cc 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -28,6 +28,4 @@ typedef enum { @property (nullable, nonatomic, strong) WPProgressTableViewCell *progressCell; -- (void)removeFeaturedImage; - @end diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index de2e3d42a389..f4da5dff25e8 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -59,10 +59,13 @@ private struct FeaturedImageView: UIViewRepresentable { let imageURL: URL let post: AbstractPost + @Environment(\.presentingViewController) var presentingViewController + func makeUIView(context: Context) -> AsyncImageView { let imageView = AsyncImageView() imageView.configuration.loadingStyle = .spinner imageView.setImage(with: imageURL, host: MediaHost(post)) + return imageView } @@ -107,6 +110,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { case .ended: wpAssert(media.remoteURL != nil) UIView.performWithoutAnimation { + upload = nil post.featuredImage = media tableView?.reloadData() } From 2ab52b7bec88202310e403d4b48a9cbee0b958ee Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 13:53:56 -0500 Subject: [PATCH 150/193] Add support for camera as a source --- .../Media/MediaPicker/MediaPicker.swift | 65 ++++++++++++------- .../MediaPickerMenuController.swift | 19 +++++- ...gsViewController+FeaturedImageUpload.swift | 2 + .../Post/PostSettingsViewController.m | 3 +- .../Views/PostSettingsFeaturedImageCell.swift | 10 +-- 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 92afc4026ad9..973bb3a7398c 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -9,10 +9,10 @@ import PhotosUI /// presenting view controller. If not provided, the current top view controller /// is used. struct MediaPicker: View { + var sources: [MediaPickerSource] = [.photos, .camera] var filter: MediaPickerMenu.MediaFilter? var isMultipleSelectionEnabled: Bool = false - var initialSelection: [Media] = [] - var onSelection: (([MediaPickerSelection]) -> Void)? + var onSelection: ((MediaPickerSelection) -> Void)? @ViewBuilder var content: () -> Content @@ -22,24 +22,15 @@ struct MediaPicker: View { var body: some View { Menu { - actions + menu } label: { content() } } @ViewBuilder - private var actions: some View { - let menu = MediaPickerMenu(viewController: presentingViewController ?? UIViewController(), filter: filter) - let controller = makeMediaPickerMenuController() - let actions: [UIAction] = [ - menu.makePhotosAction(delegate: controller), - // TODO: implement -// menu.makeCameraAction(delegate: delegate), -// menu.makeImagePlaygroundAction(delegate: delegate), - // menu.makeSiteMediaAction(blog: self.apost.blog, delegate: delegate) - ] - ForEach(actions, id: \.self) { action in + private var menu: some View { + ForEach(makeActions(), id: \.self) { action in Button.init { action.performWithSender(nil, target: nil) } label: { @@ -48,16 +39,31 @@ struct MediaPicker: View { } icon: { action.image.map(Image.init) } - } } } - private func makeMediaPickerMenuController() -> MediaPickerMenuController { + private func makeActions() -> [UIAction] { + let menu = MediaPickerMenu(viewController: presentingViewController ?? UIViewController(), filter: filter) + let controller = MediaPickerMenuController() controller.onSelection = onSelection viewModel.controller = controller // Needs to be retained - return controller + + return sources.map { source in + switch source { + case .photos: menu.makePhotosAction(delegate: controller) + case .camera: menu.makeCameraAction(delegate: controller) + } + } +// let actions: [UIAction] = [ +// menu.makePhotosAction(delegate: controller), +// menu.makeCameraAction(delegate: controller), +// // TODO: implement +// // +// // menu.makeImagePlaygroundAction(delegate: delegate), +// // menu.makeSiteMediaAction(blog: self.apost.blog, delegate: delegate) +// ] } } @@ -66,17 +72,32 @@ private final class MediaPickerViewModel: ObservableObject { } enum MediaPickerSource { - /// Apple Photos app. - case applePhotos + case photos + case camera + + var analyticsValue: String { + switch self { + case .photos: "apple_photos" + case .camera: "camera" + } + } +} + +struct MediaPickerSelection { + var items: [MediaPickerItem] + var source: MediaPickerSource } -enum MediaPickerSelection { - case phPickerResult(PHPickerResult) +enum MediaPickerItem { + case pickerResult(PHPickerResult) + case image(UIImage) var exportableAsset: ExportableAsset { switch self { - case .phPickerResult(let result): + case .pickerResult(let result): return result.itemProvider + case .image(let image): + return image } } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift index 04dc0b46e24e..943c1aed0fc6 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift @@ -2,14 +2,29 @@ import Photos import PhotosUI final class MediaPickerMenuController: NSObject { - var onSelection: (([MediaPickerSelection]) -> Void)? + var onSelection: ((MediaPickerSelection) -> Void)? + + fileprivate func didSelect(_ items: [MediaPickerItem], source: MediaPickerSource) { + let selection = MediaPickerSelection(items: items, source: source) + onSelection?(selection) + } } extension MediaPickerMenuController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.presentingViewController?.dismiss(animated: true) { if !results.isEmpty { - self.onSelection?(results.map { MediaPickerSelection.phPickerResult($0) }) + self.didSelect(results.map { MediaPickerItem.pickerResult($0) }, source: .photos) + } + } + } +} + +extension MediaPickerMenuController: ImagePickerControllerDelegate { + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + picker.presentingViewController?.dismiss(animated: true) { + if let image = info[.originalImage] as? UIImage { + self.didSelect([.image(image)], source: .camera) } } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift index c714bfac5748..1afa0a8f9193 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift @@ -46,6 +46,8 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke } } + // MARK: ImagePickerControllerDelegate + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { self.dismiss(animated: true) { if let image = info[.originalImage] as? UIImage { diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index c1141bf7c2e7..24494ed5bc86 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1132,8 +1132,7 @@ - (void)reloadSocialSectionComparingValue:(NSUInteger)value - (void)reloadFeaturedImageCell { NSIndexPath *featureImageCellPath = [NSIndexPath indexPathForRow:0 inSection:[self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]]; - [self.tableView reloadRowsAtIndexPaths:@[featureImageCellPath] - withRowAnimation:UITableViewRowAnimationFade]; + [self.tableView reloadRowsAtIndexPaths:@[featureImageCellPath] withRowAnimation:UITableViewRowAnimationFade]; } // MARK: - Page Attributes diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index f4da5dff25e8..54635708f13d 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -92,8 +92,10 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { self.post = post } - func setFeaturedImage(from items: [MediaPickerSelection]) { - guard let item = items.first else { + func setFeaturedImage(selection: MediaPickerSelection) { + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added", "source": selection.source.analyticsValue]) + + guard let item = selection.items.first else { return wpAssertionFailure("selection is empty") } guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { @@ -129,9 +131,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { } func buttonRemoveTapped() { - WPAnalytics.track(.editorPostFeaturedImageChanged, properties: [ - "via": "settings", "action": "removed" - ]) + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "removed"]) post.featuredImage = nil tableView?.reloadData() From a800eae15b9c3fcf3e2623d5011a82e04cb7ab9d Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 14:16:31 -0500 Subject: [PATCH 151/193] Add .siteMedia(blog:) source --- .../MediaPickerMenuController.swift | 23 +- .../Media/MediaPicker/MediaPicker.swift | 47 ++- .../Media/MediaPicker/MediaPickerMenu.swift | 303 ------------------ .../Menu/MediaPickerMenu+Camera.swift | 109 +++++++ .../Menu/MediaPickerMenu+External.swift | 73 +++++ .../MediaPickerMenu+ImagePlayground.swift | 0 .../Menu/MediaPickerMenu+Photos.swift | 40 +++ .../Menu/MediaPickerMenu+SiteMedia.swift | 30 ++ .../MediaPicker/Menu/MediaPickerMenu.swift | 49 +++ .../SiteMediaPickerViewController.swift | 3 +- .../Views/PostSettingsFeaturedImageCell.swift | 38 ++- 11 files changed, 375 insertions(+), 340 deletions(-) rename WordPress/Classes/ViewRelated/Media/MediaPicker/{ => Helpers}/MediaPickerMenuController.swift (69%) delete mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Camera.swift create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+External.swift rename WordPress/Classes/ViewRelated/Media/MediaPicker/{ => Menu}/MediaPickerMenu+ImagePlayground.swift (100%) create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Photos.swift create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+SiteMedia.swift create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift similarity index 69% rename from WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift rename to WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift index 943c1aed0fc6..c9f0d959ad46 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift @@ -6,33 +6,36 @@ final class MediaPickerMenuController: NSObject { fileprivate func didSelect(_ items: [MediaPickerItem], source: MediaPickerSource) { let selection = MediaPickerSelection(items: items, source: source) - onSelection?(selection) + DispatchQueue.main.async { + self.onSelection?(selection) + } } } extension MediaPickerMenuController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.presentingViewController?.dismiss(animated: true) { - if !results.isEmpty { - self.didSelect(results.map { MediaPickerItem.pickerResult($0) }, source: .photos) - } + picker.presentingViewController?.dismiss(animated: true) + if !results.isEmpty { + self.didSelect(results.map(MediaPickerItem.pickerResult), source: .photos) } } } extension MediaPickerMenuController: ImagePickerControllerDelegate { func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - picker.presentingViewController?.dismiss(animated: true) { - if let image = info[.originalImage] as? UIImage { - self.didSelect([.image(image)], source: .camera) - } + picker.presentingViewController?.dismiss(animated: true) + if let image = info[.originalImage] as? UIImage { + self.didSelect([.image(image)], source: .camera) } } } extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate { func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { - // TODO: + viewController.presentingViewController?.dismiss(animated: true) + if !selection.isEmpty { + self.didSelect(selection.map(MediaPickerItem.media), source: .siteMedia(blog: viewController.blog)) + } } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 973bb3a7398c..3ab7d6ca0c9e 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -9,9 +9,7 @@ import PhotosUI /// presenting view controller. If not provided, the current top view controller /// is used. struct MediaPicker: View { - var sources: [MediaPickerSource] = [.photos, .camera] - var filter: MediaPickerMenu.MediaFilter? - var isMultipleSelectionEnabled: Bool = false + var configuration = MediaPickerConfiguration() var onSelection: ((MediaPickerSelection) -> Void)? @ViewBuilder var content: () -> Content @@ -44,29 +42,40 @@ struct MediaPicker: View { } private func makeActions() -> [UIAction] { - let menu = MediaPickerMenu(viewController: presentingViewController ?? UIViewController(), filter: filter) + let menu = MediaPickerMenu( + viewController: presentingViewController ?? UIViewController(), + filter: configuration.filter, + isMultipleSelectionEnabled: configuration.isMultipleSelectionEnabled + ) let controller = MediaPickerMenuController() controller.onSelection = onSelection viewModel.controller = controller // Needs to be retained - return sources.map { source in + return configuration.sources.compactMap { source in switch source { - case .photos: menu.makePhotosAction(delegate: controller) - case .camera: menu.makeCameraAction(delegate: controller) + case .photos: + return menu.makePhotosAction(delegate: controller) + case .camera: + return menu.makeCameraAction(delegate: controller) + case .siteMedia(let blog): + return menu.makeSiteMediaAction(blog: blog, delegate: controller) } } // let actions: [UIAction] = [ -// menu.makePhotosAction(delegate: controller), -// menu.makeCameraAction(delegate: controller), // // TODO: implement // // // // menu.makeImagePlaygroundAction(delegate: delegate), -// // menu.makeSiteMediaAction(blog: self.apost.blog, delegate: delegate) // ] } } +struct MediaPickerConfiguration { + var sources: [MediaPickerSource] = [.photos, .camera] + var filter: MediaPickerMenu.MediaFilter? + var isMultipleSelectionEnabled = false +} + private final class MediaPickerViewModel: ObservableObject { var controller: MediaPickerMenuController? } @@ -74,11 +83,13 @@ private final class MediaPickerViewModel: ObservableObject { enum MediaPickerSource { case photos case camera + case siteMedia(blog: Blog) var analyticsValue: String { switch self { case .photos: "apple_photos" case .camera: "camera" + case .siteMedia: "site_media" } } } @@ -91,13 +102,23 @@ struct MediaPickerSelection { enum MediaPickerItem { case pickerResult(PHPickerResult) case image(UIImage) + case media(Media) - var exportableAsset: ExportableAsset { + /// Prepares the item for export and upload to your site media. If the item + /// is already uploaded, returns `Media`. + func exported() -> Exportable { switch self { case .pickerResult(let result): - return result.itemProvider + return .asset(result.itemProvider) case .image(let image): - return image + return .asset(image) + case .media(let media): + return .media(media) } } + + enum Exportable { + case asset(ExportableAsset) + case media(Media) + } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift deleted file mode 100644 index a0204f0b1beb..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift +++ /dev/null @@ -1,303 +0,0 @@ -import UIKit -import PhotosUI -import UniformTypeIdentifiers -import AVFoundation - -/// A convenience API for creating actions for picking media from different -/// source supported by the app: Photos library, Camera, Media library. -struct MediaPickerMenu { - weak var presentingViewController: UIViewController? - var filter: MediaFilter? - var isMultipleSelectionEnabled: Bool - var initialSelection: [Media] - - enum MediaFilter { - case images - case videos - } - - /// Initializes the options. - /// - /// - parameters: - /// - viewController: The view controller to use for presentation. - /// - filter: By default, `nil` – allow all content types. - /// - isMultipleSelectionEnabled: By default, `false`. - /// - initialSelection: By default, `[]`. - init(viewController: UIViewController, - filter: MediaFilter? = nil, - isMultipleSelectionEnabled: Bool = false, - initialSelection: [Media] = []) { - self.presentingViewController = viewController - self.filter = filter - self.isMultipleSelectionEnabled = isMultipleSelectionEnabled - self.initialSelection = initialSelection - } -} - -// MARK: - MediaPickerMenu (Photos) - -extension MediaPickerMenu { - /// Returns an action for picking photos from the device's Photos library. - /// - /// - note: Use `PHPickerResult.loadImage(for:)` to retrieve an image from the result. - func makePhotosAction(delegate: PHPickerViewControllerDelegate) -> UIAction { - UIAction( - title: Strings.pickFromPhotosLibrary, - image: UIImage(systemName: "photo.on.rectangle.angled"), - attributes: [], - handler: { _ in showPhotosPicker(delegate: delegate) } - ) - } - - func showPhotosPicker(delegate: PHPickerViewControllerDelegate) { - var configuration = PHPickerConfiguration() - configuration.preferredAssetRepresentationMode = .current - if let filter { - switch filter { - case .images: - configuration.filter = .images - case .videos: - configuration.filter = .videos - } - } - if isMultipleSelectionEnabled { - configuration.selectionLimit = 0 - configuration.selection = .ordered - } - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = delegate - presentingViewController?.present(picker, animated: true) - } -} - -// MARK: - MediaPickerMenu (Camera) - -protocol ImagePickerControllerDelegate: AnyObject { - // Hides `NSObject` and `UINavigationControllerDelegate` conformances that - // the original `UIImagePickerControllerDelegate` has. - - /// - parameter info: If the info is empty, nothing was selected. - func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) -} - -extension MediaPickerMenu { - /// Returns an action from capturing media using the device's camera. - /// - /// - parameters: - /// - camera: The camera to use. By default, `.rear`. - /// - delegate: The delegate. - func makeCameraAction( - camera: UIImagePickerController.CameraDevice = .rear, - delegate: ImagePickerControllerDelegate - ) -> UIAction { - UIAction( - title: cameraActionTitle, - image: UIImage(systemName: "camera"), - attributes: [], - handler: { _ in showCamera(camera: camera, delegate: delegate) } - ) - } - - private var cameraActionTitle: String { - guard let filter else { - return Strings.takePhotoOrVideo - } - switch filter { - case .images: return Strings.takePhoto - case .videos: return Strings.takeVideo - } - } - - func showCamera(camera: UIImagePickerController.CameraDevice = .rear, delegate: ImagePickerControllerDelegate) { - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized, .notDetermined: - actuallyShowCamera(camera: camera, delegate: delegate) - case .restricted, .denied: - showAccessRestrictedAlert() - @unknown default: - showAccessRestrictedAlert() - } - } - - private func actuallyShowCamera(camera: UIImagePickerController.CameraDevice, delegate: ImagePickerControllerDelegate) { - let picker = UIImagePickerController() - picker.sourceType = .camera - picker.cameraDevice = camera - picker.videoQuality = .typeHigh - if let filter { - switch filter { - case .images: picker.mediaTypes = [UTType.image.identifier] - case .videos: picker.mediaTypes = [UTType.movie.identifier] - } - } else { - picker.mediaTypes = [UTType.image.identifier, UTType.movie.identifier] - } - - let delegate = ImagePickerDelegate(delegate: delegate) - picker.delegate = delegate - objc_setAssociatedObject(picker, &MediaPickerMenu.strongDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - presentingViewController?.present(picker, animated: true) - } - - private func showAccessRestrictedAlert() { - let alert = UIAlertController(title: Strings.noCameraAccessTitle, message: Strings.noCameraAccessMessage, preferredStyle: .alert) - 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") - } - UIApplication.shared.open(url) - }) - presentingViewController?.present(alert, animated: true) - } - - private final class ImagePickerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - weak var delegate: ImagePickerControllerDelegate? - - init(delegate: ImagePickerControllerDelegate) { - self.delegate = delegate - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: info) - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: [:]) - } - } - - private static var strongDelegateKey: UInt8 = 0 -} - -// MARK: - MediaPickerMenu (Site Media) - -extension MediaPickerMenu { - /// Returns an action for selecting media from the media uploaded by the user - /// to their site. - func makeSiteMediaAction(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) -> UIAction { - UIAction( - title: Strings.pickFromMedia, - image: UIImage(systemName: "photo.stack"), - attributes: [], - handler: { _ in showSiteMediaPicker(blog: blog, delegate: delegate) } - ) - } - - func showSiteMediaPicker(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) { - let viewController = SiteMediaPickerViewController( - blog: blog, - filter: filter.map { [$0.mediaType] }, - allowsMultipleSelection: isMultipleSelectionEnabled, - initialSelection: initialSelection - ) - viewController.delegate = delegate - let navigation = UINavigationController(rootViewController: viewController) - presentingViewController?.present(navigation, animated: true) - } -} - -// MARK: - MediaPickerMenu (Stock Photo) - -extension MediaPickerMenu { - func makeStockPhotos(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction? { - guard blog.supports(.stockPhotos) else { - return nil - } - return UIAction( - title: Strings.pickFromStockPhotos, - image: UIImage(systemName: "photo.on.rectangle"), - attributes: [], - handler: { _ in showStockPhotosPicker(blog: blog, delegate: delegate) } - ) - } - - func showStockPhotosPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { - guard let presentingViewController, - let api = blog.wordPressComRestApi() else { - return - } - - let picker = ExternalMediaPickerViewController( - dataSource: StockPhotosDataSource(service: DefaultStockPhotosService(api: api)), - source: .stockPhotos, - allowsMultipleSelection: isMultipleSelectionEnabled - ) - picker.title = Strings.pickFromStockPhotos - picker.welcomeView = StockPhotosWelcomeView() - picker.delegate = delegate - - let navigation = UINavigationController(rootViewController: picker) - presentingViewController.present(navigation, animated: true) - } -} - -// MARK: - MediaPickerMenu (Free GIF, Tenor) - -extension MediaPickerMenu { - func makeFreeGIFAction(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction? { - guard blog.supports(.tenor) else { - return nil - } - return UIAction( - title: Strings.pickFromTenor, - image: UIImage(systemName: "play.square.stack"), - attributes: [], - handler: { _ in showFreeGIFPicker(blog: blog, delegate: delegate) } - ) - } - - func showFreeGIFPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { - guard let presentingViewController else { return } - - let picker = ExternalMediaPickerViewController( - dataSource: TenorDataSource(service: TenorService()), - source: .tenor, - allowsMultipleSelection: isMultipleSelectionEnabled - ) - picker.title = Strings.pickFromTenor - picker.welcomeView = TenorWelcomeView() - picker.delegate = delegate - - let navigation = UINavigationController(rootViewController: picker) - presentingViewController.present(navigation, animated: true) - } -} - -// MARK: - Helpers - -extension MediaPickerMenu.MediaFilter { - init?(_ mediaType: WPMediaType) { - switch mediaType { - case .image: self = .images - case .video: self = .videos - default: return nil - } - } - - var mediaType: MediaType { - switch self { - case .images: return .image - case .videos: return .video - } - } -} - -private enum Strings { - // MARK: Actions - - static let pickFromPhotosLibrary = NSLocalizedString("mediaPicker.pickFromPhotosLibrary", value: "Choose from Device", comment: "The name of the action in the context menu") - static let takePhoto = NSLocalizedString("mediaPicker.takePhoto", value: "Take Photo", comment: "The name of the action in the context menu") - static let takeVideo = NSLocalizedString("mediaPicker.takeVideo", value: "Take Video", comment: "The name of the action in the context menu") - static let takePhotoOrVideo = NSLocalizedString("mediaPicker.takePhotoOrVideo", value: "Take Photo or Video", comment: "The name of the action in the context menu") - static let pickFromMedia = NSLocalizedString("mediaPicker.pickFromMediaLibrary", value: "Choose from Media", comment: "The name of the action in the context menu (user's WordPress Media Library") - static let pickFromStockPhotos = NSLocalizedString("mediaPicker.pickFromStockPhotos", value: "Free Photo Library", comment: "The name of the action in the context menu for selecting photos from free stock photos") - static let pickFromTenor = NSLocalizedString("mediaPicker.pickFromFreeGIFLibrary", value: "Free GIF Library", comment: "The name of the action in the context menu for selecting photos from Tenor (free GIF library)") - - // MARK: Misc - - 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") -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Camera.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Camera.swift new file mode 100644 index 000000000000..2a3648df7fcf --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Camera.swift @@ -0,0 +1,109 @@ +import UIKit + +protocol ImagePickerControllerDelegate: AnyObject { + // Hides `NSObject` and `UINavigationControllerDelegate` conformances that + // the original `UIImagePickerControllerDelegate` has. + + /// - parameter info: If the info is empty, nothing was selected. + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) +} + +extension MediaPickerMenu { + /// Returns an action from capturing media using the device's camera. + /// + /// - parameters: + /// - camera: The camera to use. By default, `.rear`. + /// - delegate: The delegate. + func makeCameraAction( + camera: UIImagePickerController.CameraDevice = .rear, + delegate: ImagePickerControllerDelegate + ) -> UIAction { + UIAction( + title: cameraActionTitle, + image: UIImage(systemName: "camera"), + attributes: [], + handler: { _ in showCamera(camera: camera, delegate: delegate) } + ) + } + + private var cameraActionTitle: String { + guard let filter else { + return Strings.takePhotoOrVideo + } + switch filter { + case .images: return Strings.takePhoto + case .videos: return Strings.takeVideo + } + } + + func showCamera(camera: UIImagePickerController.CameraDevice = .rear, delegate: ImagePickerControllerDelegate) { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized, .notDetermined: + actuallyShowCamera(camera: camera, delegate: delegate) + case .restricted, .denied: + showAccessRestrictedAlert() + @unknown default: + showAccessRestrictedAlert() + } + } + + private func actuallyShowCamera(camera: UIImagePickerController.CameraDevice, delegate: ImagePickerControllerDelegate) { + let picker = UIImagePickerController() + picker.sourceType = .camera + picker.cameraDevice = camera + picker.videoQuality = .typeHigh + if let filter { + switch filter { + case .images: picker.mediaTypes = [UTType.image.identifier] + case .videos: picker.mediaTypes = [UTType.movie.identifier] + } + } else { + picker.mediaTypes = [UTType.image.identifier, UTType.movie.identifier] + } + + let delegate = ImagePickerDelegate(delegate: delegate) + picker.delegate = delegate + objc_setAssociatedObject(picker, &MediaPickerMenu.strongDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + presentingViewController?.present(picker, animated: true) + } + + private func showAccessRestrictedAlert() { + let alert = UIAlertController(title: Strings.noCameraAccessTitle, message: Strings.noCameraAccessMessage, preferredStyle: .alert) + 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") + } + UIApplication.shared.open(url) + }) + presentingViewController?.present(alert, animated: true) + } + + private final class ImagePickerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + weak var delegate: ImagePickerControllerDelegate? + + init(delegate: ImagePickerControllerDelegate) { + self.delegate = delegate + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: info) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: [:]) + } + } + + private static var strongDelegateKey: UInt8 = 0 +} + +private enum Strings { + static let takePhoto = NSLocalizedString("mediaPicker.takePhoto", value: "Take Photo", comment: "The name of the action in the context menu") + static let takeVideo = NSLocalizedString("mediaPicker.takeVideo", value: "Take Video", comment: "The name of the action in the context menu") + static let takePhotoOrVideo = NSLocalizedString("mediaPicker.takePhotoOrVideo", value: "Take Photo or Video", comment: "The name of the action in the context menu") + 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") +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+External.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+External.swift new file mode 100644 index 000000000000..98b0d17c1223 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+External.swift @@ -0,0 +1,73 @@ +import UIKit + +// MARK: - MediaPickerMenu (Stock Photo) + +extension MediaPickerMenu { + func makeStockPhotos(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction? { + guard blog.supports(.stockPhotos) else { + return nil + } + return UIAction( + title: Strings.pickFromStockPhotos, + image: UIImage(systemName: "photo.on.rectangle"), + attributes: [], + handler: { _ in showStockPhotosPicker(blog: blog, delegate: delegate) } + ) + } + + func showStockPhotosPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { + guard let presentingViewController, + let api = blog.wordPressComRestApi() else { + return + } + + let picker = ExternalMediaPickerViewController( + dataSource: StockPhotosDataSource(service: DefaultStockPhotosService(api: api)), + source: .stockPhotos, + allowsMultipleSelection: isMultipleSelectionEnabled + ) + picker.title = Strings.pickFromStockPhotos + picker.welcomeView = StockPhotosWelcomeView() + picker.delegate = delegate + + let navigation = UINavigationController(rootViewController: picker) + presentingViewController.present(navigation, animated: true) + } +} + +// MARK: - MediaPickerMenu (Free GIF, Tenor) + +extension MediaPickerMenu { + func makeFreeGIFAction(blog: Blog, delegate: ExternalMediaPickerViewDelegate) -> UIAction? { + guard blog.supports(.tenor) else { + return nil + } + return UIAction( + title: Strings.pickFromTenor, + image: UIImage(systemName: "play.square.stack"), + attributes: [], + handler: { _ in showFreeGIFPicker(blog: blog, delegate: delegate) } + ) + } + + func showFreeGIFPicker(blog: Blog, delegate: ExternalMediaPickerViewDelegate) { + guard let presentingViewController else { return } + + let picker = ExternalMediaPickerViewController( + dataSource: TenorDataSource(service: TenorService()), + source: .tenor, + allowsMultipleSelection: isMultipleSelectionEnabled + ) + picker.title = Strings.pickFromTenor + picker.welcomeView = TenorWelcomeView() + picker.delegate = delegate + + let navigation = UINavigationController(rootViewController: picker) + presentingViewController.present(navigation, animated: true) + } +} + +private enum Strings { + static let pickFromStockPhotos = NSLocalizedString("mediaPicker.pickFromStockPhotos", value: "Free Photo Library", comment: "The name of the action in the context menu for selecting photos from free stock photos") + static let pickFromTenor = NSLocalizedString("mediaPicker.pickFromFreeGIFLibrary", value: "Free GIF Library", comment: "The name of the action in the context menu for selecting photos from Tenor (free GIF library)") +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu+ImagePlayground.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu+ImagePlayground.swift rename to WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Photos.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Photos.swift new file mode 100644 index 000000000000..243851fcd756 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+Photos.swift @@ -0,0 +1,40 @@ +import UIKit +import PhotosUI + +extension MediaPickerMenu { + /// Returns an action for picking photos from the device's Photos library. + /// + /// - note: Use `PHPickerResult.loadImage(for:)` to retrieve an image from the result. + func makePhotosAction(delegate: PHPickerViewControllerDelegate) -> UIAction { + UIAction( + title: Strings.pickFromPhotosLibrary, + image: UIImage(systemName: "photo.on.rectangle.angled"), + attributes: [], + handler: { _ in showPhotosPicker(delegate: delegate) } + ) + } + + func showPhotosPicker(delegate: PHPickerViewControllerDelegate) { + var configuration = PHPickerConfiguration() + configuration.preferredAssetRepresentationMode = .current + if let filter { + switch filter { + case .images: + configuration.filter = .images + case .videos: + configuration.filter = .videos + } + } + if isMultipleSelectionEnabled { + configuration.selectionLimit = 0 + configuration.selection = .ordered + } + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = delegate + presentingViewController?.present(picker, animated: true) + } +} + +private enum Strings { + static let pickFromPhotosLibrary = NSLocalizedString("mediaPicker.pickFromPhotosLibrary", value: "Choose from Device", comment: "The name of the action in the context menu") +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+SiteMedia.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+SiteMedia.swift new file mode 100644 index 000000000000..2b56cfd7b5bf --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+SiteMedia.swift @@ -0,0 +1,30 @@ +import UIKit + +extension MediaPickerMenu { + /// Returns an action for selecting media from the media uploaded by the user + /// to their site. + func makeSiteMediaAction(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) -> UIAction { + UIAction( + title: Strings.pickFromMedia, + image: UIImage(systemName: "photo.stack"), + attributes: [], + handler: { _ in showSiteMediaPicker(blog: blog, delegate: delegate) } + ) + } + + func showSiteMediaPicker(blog: Blog, delegate: SiteMediaPickerViewControllerDelegate) { + let viewController = SiteMediaPickerViewController( + blog: blog, + filter: filter.map { [$0.mediaType] }, + allowsMultipleSelection: isMultipleSelectionEnabled, + initialSelection: initialSelection + ) + viewController.delegate = delegate + let navigation = UINavigationController(rootViewController: viewController) + presentingViewController?.present(navigation, animated: true) + } +} + +private enum Strings { + static let pickFromMedia = NSLocalizedString("mediaPicker.pickFromMediaLibrary", value: "Choose from Media", comment: "The name of the action in the context menu (user's WordPress Media Library") +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift new file mode 100644 index 000000000000..53730bb8d795 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift @@ -0,0 +1,49 @@ +import UIKit + +/// A convenience API for creating actions for picking media from different +/// source supported by the app: Photos library, Camera, Media library. +struct MediaPickerMenu { + weak var presentingViewController: UIViewController? + var filter: MediaFilter? + var isMultipleSelectionEnabled: Bool + var initialSelection: [Media] + + enum MediaFilter { + case images + case videos + } + + /// Initializes the options. + /// + /// - parameters: + /// - viewController: The view controller to use for presentation. + /// - filter: By default, `nil` – allow all content types. + /// - isMultipleSelectionEnabled: By default, `false`. + /// - initialSelection: By default, `[]`. + init(viewController: UIViewController, + filter: MediaFilter? = nil, + isMultipleSelectionEnabled: Bool = false, + initialSelection: [Media] = []) { + self.presentingViewController = viewController + self.filter = filter + self.isMultipleSelectionEnabled = isMultipleSelectionEnabled + self.initialSelection = initialSelection + } +} + +extension MediaPickerMenu.MediaFilter { + init?(_ mediaType: WPMediaType) { + switch mediaType { + case .image: self = .images + case .video: self = .videos + default: return nil + } + } + + var mediaType: MediaType { + switch self { + case .images: return .image + case .videos: return .video + } + } +} diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift index 169dd9f0b599..ae19c9ef2ffd 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift @@ -7,7 +7,8 @@ protocol SiteMediaPickerViewControllerDelegate: AnyObject { /// The media picker for your site media. final class SiteMediaPickerViewController: UIViewController, SiteMediaCollectionViewControllerDelegate { - private let blog: Blog + let blog: Blog + private let allowsMultipleSelection: Bool private let initialSelection: [Media] diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 54635708f13d..1cf1bc474e27 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -23,7 +23,11 @@ struct PostSettingsFeaturedImageCell: View { } else if viewModel.upload != nil { uploading } else { - MediaPicker(filter: .images, onSelection: viewModel.setFeaturedImage) { + let configuration = MediaPickerConfiguration( + sources: [.photos, .camera, .siteMedia(blog: post.blog)], + filter: .images + ) + MediaPicker(configuration: configuration, onSelection: viewModel.setFeaturedImage) { Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") .frame(maxWidth: .infinity) .contentShape(Rectangle()) // Make the whole cell tappable @@ -98,24 +102,24 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { guard let item = selection.items.first else { return wpAssertionFailure("selection is empty") } - guard let media = coordinator.addMedia(from: item.exportableAsset, to: post) else { - return wpAssertionFailure("failed to add media to post") + switch item.exported() { + case .asset(let exportableAsset): + guard let media = coordinator.addMedia(from: exportableAsset, to: post) else { + return wpAssertionFailure("failed to add media to post") + } + self.receipt = coordinator.addObserver({ [weak self] media, state in + self?.didUpdateUploadState(state, media: media) + }, for: media) + self.upload = media + case .media(let media): + didProcessMedia(media) } - self.receipt = coordinator.addObserver({ [weak self] media, state in - self?.didUpdateUploadState(state, media: media) - }, for: media) - self.upload = media } private func didUpdateUploadState(_ state: MediaCoordinator.MediaState, media: Media) { switch state { case .ended: - wpAssert(media.remoteURL != nil) - UIView.performWithoutAnimation { - upload = nil - post.featuredImage = media - tableView?.reloadData() - } + didProcessMedia(media) case .failed(let error): Notice(title: Strings.uploadFailed, message: error.localizedDescription).post() upload = nil @@ -124,6 +128,14 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { } } + private func didProcessMedia(_ media: Media) { + wpAssert(media.remoteURL != nil) + UIView.performWithoutAnimation { + upload = nil + post.featuredImage = media + tableView?.reloadData() + } + } func buttonCancelTapped() { guard let upload else { return } coordinator.cancelUploadAndDeleteMedia(upload) From 842dcfe6cdb758572cfc7c2e4ce90be1742071da Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 14:36:03 -0500 Subject: [PATCH 152/193] Add ImagePlayground source support --- .../Helpers/MediaPickerMenuController.swift | 8 +++++++- .../ViewRelated/Media/MediaPicker/MediaPicker.swift | 11 +++++------ .../Post/Views/PostSettingsFeaturedImageCell.swift | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift index c9f0d959ad46..ae30b4c92f42 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift @@ -41,6 +41,12 @@ extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate { extension MediaPickerMenuController: ImagePlaygroundPickerDelegate { func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) { - // TODO: + + viewController.presentingViewController?.dismiss(animated: true) + if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) { + self.didSelect([.image(image)], source: .playground) + } else { + wpAssertionFailure("failed to read the image created by ImagePlayground") + } } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 3ab7d6ca0c9e..dc66bc6e030f 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -29,7 +29,7 @@ struct MediaPicker: View { @ViewBuilder private var menu: some View { ForEach(makeActions(), id: \.self) { action in - Button.init { + Button { action.performWithSender(nil, target: nil) } label: { Label { @@ -60,13 +60,10 @@ struct MediaPicker: View { return menu.makeCameraAction(delegate: controller) case .siteMedia(let blog): return menu.makeSiteMediaAction(blog: blog, delegate: controller) + case .playground: + return menu.makeImagePlaygroundAction(delegate: controller) } } -// let actions: [UIAction] = [ -// // TODO: implement -// // -// // menu.makeImagePlaygroundAction(delegate: delegate), -// ] } } @@ -84,12 +81,14 @@ enum MediaPickerSource { case photos case camera case siteMedia(blog: Blog) + case playground var analyticsValue: String { switch self { case .photos: "apple_photos" case .camera: "camera" case .siteMedia: "site_media" + case .playground: "image_playground" } } } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 1cf1bc474e27..80f9af735c23 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -24,7 +24,7 @@ struct PostSettingsFeaturedImageCell: View { uploading } else { let configuration = MediaPickerConfiguration( - sources: [.photos, .camera, .siteMedia(blog: post.blog)], + sources: [.photos, .camera, .playground, .siteMedia(blog: post.blog)], filter: .images ) MediaPicker(configuration: configuration, onSelection: viewModel.setFeaturedImage) { From 0d5640d71dd357621b2cf0a4ce8b85d60a62f395 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 14:37:38 -0500 Subject: [PATCH 153/193] Add ImagePlayground support in MediaPicker --- .../Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index dc66bc6e030f..3d24daf4f53d 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -61,6 +61,9 @@ struct MediaPicker: View { case .siteMedia(let blog): return menu.makeSiteMediaAction(blog: blog, delegate: controller) case .playground: + guard MediaPickerMenu.isImagePlaygroundAvailable else { + return nil + } return menu.makeImagePlaygroundAction(delegate: controller) } } From 748349730ebe454453392358b5d0803e594740d2 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 14:50:12 -0500 Subject: [PATCH 154/193] Add free photos and GIFs support to MediaPicker --- .../Helpers/MediaPickerMenuController.swift | 20 +++++++++---- .../Media/MediaPicker/MediaPicker.swift | 28 +++++++++---------- .../MediaPickerMenu+ImagePlayground.swift | 7 +++-- .../SiteMediaPickerViewController.swift | 2 +- .../Views/PostSettingsFeaturedImageCell.swift | 2 +- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift index ae30b4c92f42..b6db786883e9 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift @@ -4,7 +4,7 @@ import PhotosUI final class MediaPickerMenuController: NSObject { var onSelection: ((MediaPickerSelection) -> Void)? - fileprivate func didSelect(_ items: [MediaPickerItem], source: MediaPickerSource) { + fileprivate func didSelect(_ items: [MediaPickerItem], source: String) { let selection = MediaPickerSelection(items: items, source: source) DispatchQueue.main.async { self.onSelection?(selection) @@ -16,7 +16,7 @@ extension MediaPickerMenuController: PHPickerViewControllerDelegate { public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.presentingViewController?.dismiss(animated: true) if !results.isEmpty { - self.didSelect(results.map(MediaPickerItem.pickerResult), source: .photos) + self.didSelect(results.map(MediaPickerItem.pickerResult), source: "apple_photos") } } } @@ -25,7 +25,7 @@ extension MediaPickerMenuController: ImagePickerControllerDelegate { func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.presentingViewController?.dismiss(animated: true) if let image = info[.originalImage] as? UIImage { - self.didSelect([.image(image)], source: .camera) + self.didSelect([.image(image)], source: "camera") } } } @@ -34,7 +34,7 @@ extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate { func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { viewController.presentingViewController?.dismiss(animated: true) if !selection.isEmpty { - self.didSelect(selection.map(MediaPickerItem.media), source: .siteMedia(blog: viewController.blog)) + self.didSelect(selection.map(MediaPickerItem.media), source: "site_media") } } } @@ -44,9 +44,19 @@ extension MediaPickerMenuController: ImagePlaygroundPickerDelegate { viewController.presentingViewController?.dismiss(animated: true) if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) { - self.didSelect([.image(image)], source: .playground) + self.didSelect([.image(image)], source: "image_playground") } else { wpAssertionFailure("failed to read the image created by ImagePlayground") } } } + +extension MediaPickerMenuController: ExternalMediaPickerViewDelegate { + func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) { + viewController.presentingViewController?.dismiss(animated: true) + if !selection.isEmpty { + let source = viewController.source == .tenor ? "free_gifs" : "free_photos" + self.didSelect(selection.map(MediaPickerItem.external), source: source) + } + } +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index 3d24daf4f53d..c2645cc0e1d2 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -61,10 +61,12 @@ struct MediaPicker: View { case .siteMedia(let blog): return menu.makeSiteMediaAction(blog: blog, delegate: controller) case .playground: - guard MediaPickerMenu.isImagePlaygroundAvailable else { - return nil - } return menu.makeImagePlaygroundAction(delegate: controller) + case .freePhotos(let blog): + return menu.makeStockPhotos(blog: blog, delegate: controller) + case .freeGIFs(let blog): + return menu.makeFreeGIFAction(blog: blog, delegate: controller) + } } } @@ -81,30 +83,24 @@ private final class MediaPickerViewModel: ObservableObject { } enum MediaPickerSource { - case photos + case photos // Apple Photos case camera case siteMedia(blog: Blog) - case playground - - var analyticsValue: String { - switch self { - case .photos: "apple_photos" - case .camera: "camera" - case .siteMedia: "site_media" - case .playground: "image_playground" - } - } + case playground // Image Playground + case freePhotos(blog: Blog) // Pexels + case freeGIFs(blog: Blog) // Tenor } struct MediaPickerSelection { var items: [MediaPickerItem] - var source: MediaPickerSource + var source: String } enum MediaPickerItem { case pickerResult(PHPickerResult) case image(UIImage) case media(Media) + case external(ExternalMediaAsset) /// Prepares the item for export and upload to your site media. If the item /// is already uploaded, returns `Media`. @@ -116,6 +112,8 @@ enum MediaPickerItem { return .asset(image) case .media(let media): return .media(media) + case .external(let asset): + return .asset(asset) } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift index 7946391bd8d2..3669e143350d 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu+ImagePlayground.swift @@ -13,8 +13,11 @@ extension MediaPickerMenu { Strings.imagePlayground } - func makeImagePlaygroundAction(delegate: ImagePlaygroundPickerDelegate) -> UIAction { - UIAction( + func makeImagePlaygroundAction(delegate: ImagePlaygroundPickerDelegate) -> UIAction? { + guard MediaPickerMenu.isImagePlaygroundAvailable else { + return nil + } + return UIAction( title: Strings.imagePlayground, image: UIImage(systemName: "apple.image.playground"), attributes: [], diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift index ae19c9ef2ffd..f5a57fa21717 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift @@ -7,7 +7,7 @@ protocol SiteMediaPickerViewControllerDelegate: AnyObject { /// The media picker for your site media. final class SiteMediaPickerViewController: UIViewController, SiteMediaCollectionViewControllerDelegate { - let blog: Blog + private let blog: Blog private let allowsMultipleSelection: Bool private let initialSelection: [Media] diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 80f9af735c23..9af4481ecfa1 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -97,7 +97,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { } func setFeaturedImage(selection: MediaPickerSelection) { - WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added", "source": selection.source.analyticsValue]) + WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added", "source": selection.source]) guard let item = selection.items.first else { return wpAssertionFailure("selection is empty") From d2c05f06085de999e00433d81cdb124799857257 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 14:59:18 -0500 Subject: [PATCH 155/193] Remove unused media upload code from PostSettingsViewController --- ...omeSiteHeaderViewController+SiteIcon.swift | 2 +- .../Cells/PostFeaturedImageCell.swift | 27 --- .../SiteMediaAddMediaMenuController.swift | 2 +- ...gsViewController+FeaturedImageUpload.swift | 226 ------------------ .../Post/PostSettingsViewController.m | 63 +---- .../PostSettingsViewController_Internal.h | 8 - .../Views/PostSettingsFeaturedImageCell.swift | 3 + 7 files changed, 7 insertions(+), 324 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift delete mode 100644 WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteIcon.swift b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteIcon.swift index fa108934eb09..7e05c14d1013 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteIcon.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteIcon.swift @@ -28,7 +28,7 @@ extension HomeSiteHeaderViewController { mediaMenu.makeCameraAction(delegate: presenter), mediaMenu.makeImagePlaygroundAction(delegate: presenter), mediaMenu.makeSiteMediaAction(blog: blog, delegate: presenter) - ] + ].compactMap { $0 } if FeatureFlag.siteIconCreator.enabled { actions.append(UIAction( title: SiteIconAlertStrings.Actions.createWithEmoji, diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift deleted file mode 100644 index 41cedf53d84f..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift +++ /dev/null @@ -1,27 +0,0 @@ -import UIKit -import WordPressUI -import AsyncImageKit - -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/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift index ebe6a25659a7..66c0434f51cd 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift @@ -21,7 +21,7 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel menu.makeCameraAction(delegate: self), menu.makeImagePlaygroundAction(delegate: self), makeDocumentPickerAction(from: viewController) - ]) + ].compactMap { $0 }) ] let freeMediaActions: [UIAction] = [ menu.makeStockPhotos(blog: blog, delegate: self), diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift deleted file mode 100644 index 1afa0a8f9193..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift +++ /dev/null @@ -1,226 +0,0 @@ -import Foundation -import Photos -import PhotosUI -import WordPressFlux - -// MARK: - PostSettingsViewController (Featured Image Menu) - -extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePickerControllerDelegate { - @objc func makeSetFeaturedImageCell() -> UITableViewCell { - let cell = UITableViewCell(style: .default, reuseIdentifier: nil) - cell.selectionStyle = .none - - let button = UIButton() - var configuration = UIButton.Configuration.plain() - configuration.title = Strings.buttonSetFeaturedImage - configuration.baseForegroundColor = UIAppColor.primary - button.configuration = configuration - button.menu = makeSetFeaturedImageMenu() - button.showsMenuAsPrimaryAction = true - - cell.contentView.addSubview(button) - button.translatesAutoresizingMaskIntoConstraints = false - cell.contentView.pinSubviewToAllEdges(button) - - cell.accessibilityIdentifier = "SetFeaturedImage" - return cell - } - - private func makeSetFeaturedImageMenu() -> UIMenu { - let menu = MediaPickerMenu(viewController: self, filter: .images) - return UIMenu(children: [ - menu.makePhotosAction(delegate: self), - menu.makeCameraAction(delegate: self), - menu.makeImagePlaygroundAction(delegate: self), - menu.makeSiteMediaAction(blog: self.apost.blog, delegate: self) - ]) - } - - // MARK: PHPickerViewControllerDelegate - - public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - self.dismiss(animated: true) { - if let result = results.first { - self.setFeaturedImage(with: result.itemProvider) - } - } - } - - // MARK: ImagePickerControllerDelegate - - func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - self.dismiss(animated: true) { - if let image = info[.originalImage] as? UIImage { - self.setFeaturedImage(with: image) - } - } - } -} - -extension PostSettingsViewController: SiteMediaPickerViewControllerDelegate { - func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) { - dismiss(animated: true) - - guard let media = selection.first else { return } - - WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "added"]) - setFeaturedImage(media: media) - reloadFeaturedImageCell() - } -} - -extension PostSettingsViewController: ImagePlaygroundPickerDelegate { - func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) { - dismiss(animated: true) - - if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) { - setFeaturedImage(with: image) - } else { - wpAssertionFailure("failed to read the image created by ImagePlayground") - } - } -} - -// MARK: - PostSettingsViewController (Featured Image Upload) - -extension PostSettingsViewController { - func setFeaturedImage(with asset: ExportableAsset) { - guard let media = MediaCoordinator.shared.addMedia(from: asset, to: apost) else { - return - } - self.apost.featuredImage = media - self.setupObservingOf(media: media) - } - - @objc func setFeaturedImage(media: Media) { - apost.featuredImage = media - if !media.hasRemote { - MediaCoordinator.shared.retryMedia(media) - setupObservingOf(media: media) - } - - if let mediaIdentifier = apost.featuredImage?.mediaID { - featuredImageDelegate?.gutenbergDidRequestFeaturedImageId(mediaIdentifier) - } - } - - @objc func removeMediaObserver() { - if let receipt = mediaObserverReceipt { - MediaCoordinator.shared.removeObserver(withUUID: receipt) - mediaObserverReceipt = nil - } - } - - @objc func setupObservingOf(media: Media) { - removeMediaObserver() - isUploadingMedia = true - mediaObserverReceipt = MediaCoordinator.shared.addObserver({ [weak self](media, state) in - self?.mediaObserver(media: media, state: state) - }) - let progress = MediaCoordinator.shared.progress(for: media) - if let url = media.absoluteThumbnailLocalURL, let data = try? Data(contentsOf: url) { - progress?.setUserInfoObject(UIImage(data: data), forKey: .WPProgressImageThumbnailKey) - } - featuredImageProgress = progress - } - - func mediaObserver(media: Media, state: MediaCoordinator.MediaState) { - switch state { - case .processing: - featuredImageProgress?.localizedDescription = NSLocalizedString("Preparing...", comment: "Label to show while converting and/or resizing media to send to server") - case .thumbnailReady: - if let url = media.absoluteThumbnailLocalURL, let data = try? Data(contentsOf: url) { - featuredImageProgress?.setUserInfoObject(UIImage(data: data), forKey: .WPProgressImageThumbnailKey) - } - case .uploading(let progress): - featuredImageProgress = progress - featuredImageProgress?.kind = .file - featuredImageProgress?.setUserInfoObject(Progress.FileOperationKind.copying, forKey: ProgressUserInfoKey.fileOperationKindKey) - featuredImageProgress?.localizedDescription = NSLocalizedString("Uploading...", comment: "Label to show while uploading media to server") - progressCell?.setProgress(progress) - tableView.reloadData() - case .ended: - isUploadingMedia = false - tableView.reloadData() - case .failed(let error): - DDLogError("Couldn't upload the featured image: \(error.localizedDescription)") - isUploadingMedia = false - tableView.reloadData() - if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled { - apost.featuredImage = nil - apost.removeMediaObject(media) - break - } - - let errorTitle = NSLocalizedString("Couldn't upload the featured image", comment: "The title for an alert that says to the user that the featured image he selected couldn't be uploaded.") - let notice = Notice(title: errorTitle, message: error.localizedDescription) - - ActionDispatcher.dispatch(NoticeAction.clearWithTag(MediaProgressCoordinatorNoticeViewModel.uploadErrorNoticeTag)) - // The Media coordinator shows its own notice about a failed upload. We have a better, more explanatory message for users here - // so we want to supress the one coming from the coordinator and show ours. - ActionDispatcher.dispatch(NoticeAction.post(notice)) - case .progress, .cancelled: - break - } - } - - @objc func showFeaturedImageRemoveOrRetryAction(atIndexPath indexPath: IndexPath) { - guard let media = apost.featuredImage else { - return - } - - let alertController = UIAlertController(title: FeaturedImageActionSheet.title, message: nil, preferredStyle: .actionSheet) - alertController.addActionWithTitle(FeaturedImageActionSheet.dismissActionTitle, - style: .cancel, - handler: nil) - - alertController.addActionWithTitle(FeaturedImageActionSheet.retryUploadActionTitle, - style: .default, - handler: { (action) in - self.setFeaturedImage(media: media) - }) - - alertController.addActionWithTitle(FeaturedImageActionSheet.removeActionTitle, - style: .destructive, - handler: { (action) in - self.apost.featuredImage = nil - self.apost.removeMediaObject(media) - }) - if let error = media.error { - alertController.message = error.localizedDescription - } - if let anchorView = self.tableView.cellForRow(at: indexPath) ?? self.view { - alertController.popoverPresentationController?.sourceView = anchorView - alertController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: anchorView.bounds.midX, y: anchorView.bounds.midY), size: CGSize(width: 1, height: 1)) - alertController.popoverPresentationController?.permittedArrowDirections = .any - } - present(alertController, animated: true) - } - - struct FeaturedImageActionSheet { - static let title = NSLocalizedString( - "postSettings.featuredImageUploadActionSheet.title", - value: "Featured Image Options", - comment: "Title for action sheet with featured media options." - ) - static let dismissActionTitle = NSLocalizedString( - "postSettings.featuredImageUploadActionSheet.dismiss", - value: "Dismiss", - comment: "User action to dismiss featured media options." - ) - static let retryUploadActionTitle = NSLocalizedString( - "postSettings.featuredImageUploadActionSheet.retryUpload", - value: "Retry", - comment: "User action to retry featured media upload." - ) - static let removeActionTitle = NSLocalizedString( - "postSettings.featuredImageUploadActionSheet.remove", - value: "Remove", - comment: "User action to remove featured media." - ) - } -} - -private enum Strings { - static let buttonSetFeaturedImage = NSLocalizedString("postSettings.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 24494ed5bc86..6845007efd77 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -26,9 +26,6 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { PostSettingsRowVisibility, PostSettingsRowFormat, PostSettingsRowFeaturedImage, - PostSettingsRowFeaturedImageAdd, - PostSettingsRowFeaturedImageRemove, - PostSettingsRowFeaturedLoading, PostSettingsRowShareConnection, PostSettingsRowShareMessage, PostSettingsRowSlug, @@ -39,8 +36,6 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { }; static NSString *const PostSettingsAnalyticsTrackingSource = @"post_settings"; -static NSString *const TableViewActivityCellIdentifier = @"TableViewActivityCellIdentifier"; -static NSString *const TableViewProgressCellIdentifier = @"TableViewProgressCellIdentifier"; static NSString *const TableViewFeaturedImageCellIdentifier = @"TableViewFeaturedImageCellIdentifier"; static NSString *const TableViewToggleCellIdentifier = @"TableViewToggleCellIdentifier"; static NSString *const TableViewGenericCellIdentifier = @"TableViewGenericCellIdentifier"; @@ -83,8 +78,6 @@ @implementation PostSettingsViewController - (void)dealloc { [self.internetReachability stopNotifier]; - - [self removeMediaObserver]; } - (instancetype)initWithPost:(AbstractPost *)aPost @@ -119,8 +112,6 @@ - (void)viewDidLoad [self setupFormatsList]; [self setupPublicizeConnections]; - [self.tableView registerNib:[UINib nibWithNibName:@"WPTableViewActivityCell" bundle:nil] forCellReuseIdentifier:TableViewActivityCellIdentifier]; - [self.tableView registerClass:[WPProgressTableViewCell class] forCellReuseIdentifier:TableViewProgressCellIdentifier]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TableViewFeaturedImageCellIdentifier]; [self.tableView registerClass:[SwitchTableViewCell class] forCellReuseIdentifier:TableViewToggleCellIdentifier]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TableViewGenericCellIdentifier]; @@ -131,7 +122,6 @@ - (void)viewDidLoad // Compensate for the first section's height of 1.0f self.tableView.contentInset = UIEdgeInsetsMake(-1.0f, 0, 0, 0); self.tableView.accessibilityIdentifier = @"SettingsTable"; - self.isUploadingMedia = NO; self.featuredImageViewModel.tableView = self.tableView; @@ -490,10 +480,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath [self showPostFormatSelector]; } else if (cell.tag == PostSettingsRowFeaturedImage) { [self showFeaturedImageSelector]; - } else if (cell.tag == PostSettingsRowFeaturedImageAdd) { - [self showFeaturedImageSelector]; - } else if (cell.tag == PostSettingsRowFeaturedImageRemove) { - [self showFeaturedImageRemoveOrRetryActionAtIndexPath:indexPath]; } else if (sec == PostSettingsSectionDisabledTwitter) { [self showShareDetailForIndexPath:indexPath]; } else if (cell.tag == PostSettingsRowShareConnection) { @@ -644,7 +630,7 @@ - (UITableViewCell *)makeFeaturedImageCellForIndexPath:(NSIndexPath *)indexPath cell.tag = PostSettingsRowFeaturedImage; return cell; - // TODO: remove unused code + // TODO: (kean) remove unused code // if (!self.apost.featuredImage && !self.isUploadingMedia) { // return [self cellForSetFeaturedImage]; // @@ -684,41 +670,7 @@ - (UITableViewCell *)configureStickyPostCellForIndexPath:(NSIndexPath *)indexPat return cell; } -- (UITableViewCell *)cellForSetFeaturedImage -{ - UITableViewCell *cell = [self makeSetFeaturedImageCell]; - cell.tag = PostSettingsRowFeaturedImageAdd; - return cell; -} - -- (UITableViewCell *)cellForFeaturedImageError -{ - WPTableViewActivityCell *activityCell = [self getWPTableViewActivityCell]; - activityCell.textLabel.text = NSLocalizedString(@"Upload failed. Tap for options.", @"Description to show on post setting for a featured image that failed to upload."); - activityCell.tag = PostSettingsRowFeaturedImageRemove; - return activityCell; -} - -- (UITableViewCell *)cellForFeaturedImageUploadProgressAtIndexPath:(NSIndexPath *)indexPath -{ - self.progressCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewProgressCellIdentifier forIndexPath:indexPath]; - [WPStyleGuide configureTableViewCell:self.progressCell]; - [self.progressCell setProgress:self.featuredImageProgress]; - self.progressCell.tag = PostSettingsRowFeaturedLoading; - return self.progressCell; -} - -- (UITableViewCell *)cellForFeaturedImageWithURL:(nonnull NSURL *)featuredURL atIndexPath:(NSIndexPath *)indexPath -{ - // TODO: remove - return [UITableViewCell new]; - -// PostFeaturedImageCell *featuredImageCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; -// [featuredImageCell setImageWithURL:featuredURL post:self.apost]; -// featuredImageCell.tag = PostSettingsRowFeaturedImage; -// return featuredImageCell; -} - +// TODO: (kean) remove - (nullable NSURL *)urlForFeaturedImage { NSURL *featuredURL = self.apost.featuredImage.absoluteLocalURL; @@ -862,17 +814,6 @@ - (WPTableViewCell *)getWPTableViewImageAndAccessoryCell return cell; } -- (WPTableViewActivityCell *)getWPTableViewActivityCell -{ - WPTableViewActivityCell *cell = [self.tableView dequeueReusableCellWithIdentifier:TableViewActivityCellIdentifier]; - cell.accessoryType = UITableViewCellAccessoryNone; - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - [WPStyleGuide configureTableViewActionCell:cell]; - - cell.tag = 0; - return cell; -} - - (void)showPostAuthorSelector { PostAuthorSelectorViewController *vc = [[PostAuthorSelectorViewController alloc] init:self.apost]; diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h index d6ed3545b9cc..b801650a2bbe 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -20,12 +20,4 @@ typedef enum { @property (nonnull, nonatomic, strong) NSArray *sections; -@property (nullable, nonatomic, strong) NSProgress *featuredImageProgress; - -@property (nonatomic, assign) BOOL isUploadingMedia; - -@property (nullable, nonatomic, strong) NSUUID *mediaObserverReceipt; - -@property (nullable, nonatomic, strong) WPProgressTableViewCell *progressCell; - @end diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 9af4481ecfa1..8fcbfc0f97f3 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -31,6 +31,7 @@ struct PostSettingsFeaturedImageCell: View { Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") .frame(maxWidth: .infinity) .contentShape(Rectangle()) // Make the whole cell tappable + .accessibilityIdentifier("SetFeaturedImage") } } } @@ -130,6 +131,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { private func didProcessMedia(_ media: Media) { wpAssert(media.remoteURL != nil) + // TODO: (kean) fix animations UIView.performWithoutAnimation { upload = nil post.featuredImage = media @@ -145,6 +147,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { func buttonRemoveTapped() { WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "removed"]) + // TODO: (kean) do we need to call removeMediaObject? post.featuredImage = nil tableView?.reloadData() } From 7fbb68ada1af54170de8ddece423fdcb6d95c909 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 15:01:28 -0500 Subject: [PATCH 156/193] Remove WPTableViewActivityCell --- .../Cells/WPTableViewActivityCell.h | 9 ----- .../Cells/WPTableViewActivityCell.m | 5 --- .../Cells/WPTableViewActivityCell.xib | 36 ------------------- .../Post/PostSettingsViewController.m | 1 - 4 files changed, 51 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.h delete mode 100644 WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.m delete mode 100644 WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.xib diff --git a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.h b/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.h deleted file mode 100644 index 5664d394f281..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.h +++ /dev/null @@ -1,9 +0,0 @@ -@import UIKit; -@import WordPressShared; - -@interface WPTableViewActivityCell : WPTableViewCell {} - -@property (nonatomic, strong) IBOutlet UIActivityIndicatorView *spinner; -@property (nonatomic, strong) IBOutlet UIView *viewForBackground; - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.m b/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.m deleted file mode 100644 index f93b933c2def..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.m +++ /dev/null @@ -1,5 +0,0 @@ -#import "WPTableViewActivityCell.h" - -@implementation WPTableViewActivityCell - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.xib b/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.xib deleted file mode 100644 index 0688316dcecf..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/WPTableViewActivityCell.xib +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 6845007efd77..c731d540caf4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -3,7 +3,6 @@ #import "Media.h" #import "SettingsSelectionViewController.h" #import "SharingDetailViewController.h" -#import "WPTableViewActivityCell.h" #import "CoreDataStack.h" #import "MediaService.h" #import "WPProgressTableViewCell.h" From 4e283d45431c3b47348b6b3074823c5de573cdeb Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 15:02:50 -0500 Subject: [PATCH 157/193] Remove WPProgressTableViewCell --- .../System/WordPress-Bridging-Header.h | 1 - .../Cells/WPProgressTableViewCell.h | 15 --- .../Cells/WPProgressTableViewCell.m | 124 ------------------ .../Post/PostSettingsViewController.m | 3 - .../PostSettingsViewController_Internal.h | 3 - .../StoppableProgressIndicatorView.swift | 117 ----------------- 6 files changed, 263 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.h delete mode 100644 WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.m delete mode 100644 WordPress/Classes/ViewRelated/Views/StoppableProgressIndicatorView.swift diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index cce86c7a3683..01a375ecb212 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -41,7 +41,6 @@ #import "PostServiceOptions.h" #import "PostSettingsViewController.h" #import "PostSettingsViewController_Internal.h" -#import "WPProgressTableViewCell.h" #import "PostTag.h" #import "PostTagService.h" diff --git a/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.h b/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.h deleted file mode 100644 index d576ff22671f..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.h +++ /dev/null @@ -1,15 +0,0 @@ -@import UIKit; -@import WordPressSharedObjC; - -@class WPTableViewCell; - -/** - The corresponding value is an UIImage instance representing the work being done - */ -extern NSProgressUserInfoKey const WPProgressImageThumbnailKey; - -@interface WPProgressTableViewCell : WPTableViewCell - -- (void) setProgress:(NSProgress *)progress; - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.m b/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.m deleted file mode 100644 index 099920c77192..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/WPProgressTableViewCell.m +++ /dev/null @@ -1,124 +0,0 @@ -#import "WPProgressTableViewCell.h" -#import "WordPress-Swift.h" - -static void *ProgressObserverContext = &ProgressObserverContext; - -NSProgressUserInfoKey const WPProgressImageThumbnailKey = @"WPProgressImageThumbnailKey"; -@interface WPProgressTableViewCell () - -@property (nonatomic, strong) StoppableProgressIndicatorView * progressView; - -@end - -@implementation WPProgressTableViewCell { - NSProgress *_progress; -} - -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]; - if (self) { - _progressView = [[StoppableProgressIndicatorView alloc] initWithFrame:CGRectMake(10.0,0.0,40.0,40.0)]; - _progressView.hidden = YES; - self.accessoryView = _progressView; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)coder -{ - NSAssert(false, @"WPProgressTableViewCell can't be created using a nib"); - return [super initWithCoder:coder]; -} - -- (void)dealloc -{ - [_progress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))]; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - [self setProgress:nil]; - self.progressView.hidden = YES; - self.progressView.hidesWhenStopped=YES; - [self.progressView stopAnimating]; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - self.imageView.frame = CGRectInset(self.imageView.frame, 0.0, 5.0); -} - -#pragma mark - Progress handling - -- (void) setProgress:(NSProgress *) progress { - if (progress == _progress){ - return; - } - [_progress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))]; - - _progress = progress; - - [_progress addObserver:self - forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) - options:NSKeyValueObservingOptionInitial - context:ProgressObserverContext]; - - if (_progress.isCancellable){ - [self.progressView.stopButton addTarget:self action:@selector(stopPressed:) forControlEvents:UIControlEventTouchUpInside]; - } - [self updateProgress]; -} - -- (void)updateProgress -{ - if (_progress.fractionCompleted < 1 && !_progress.isCancelled) { - [_progressView startAnimating]; - } else { - [_progressView stopAnimating]; - } - - _progressView.mayStop = _progress.isCancellable; - if ([_progress isCancelled]) { - self.textLabel.text = NSLocalizedString(@"Canceled", @"The action was canceled"); - self.detailTextLabel.text = @""; - } else if ((_progress.totalUnitCount == 0 && _progress.completedUnitCount == 0) || _progress.userInfo[@"mediaError"] ) { - self.textLabel.text = NSLocalizedString(@"Failed", @"The action failed"); - self.detailTextLabel.text = @""; - } else if (_progress.fractionCompleted >= 1) { - self.textLabel.text = NSLocalizedString(@"Completed", @"The action is completed"); - self.detailTextLabel.text = @""; - } else { - self.textLabel.text = [_progress localizedDescription]; - self.detailTextLabel.text = [_progress localizedAdditionalDescription]; - } - [self.imageView setImage:_progress.userInfo[WPProgressImageThumbnailKey]]; -} - -#pragma mark - KVO - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context -{ - if (context == ProgressObserverContext && object == _progress) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - [self updateProgress]; - }]; - } else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -#pragma mark - Stop Button events - -- (void) stopPressed:(id)sender -{ - [_progress cancel]; - [self updateProgress]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index c731d540caf4..7cb3b8140780 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -5,9 +5,6 @@ #import "SharingDetailViewController.h" #import "CoreDataStack.h" #import "MediaService.h" -#import "WPProgressTableViewCell.h" -#import -#import #import "WordPress-Swift.h" @import Gridicons; diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h index b801650a2bbe..7be46a2547ac 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -13,9 +13,6 @@ typedef enum { PostSettingsSectionPageAttributes } PostSettingsSection; - -@class WPProgressTableViewCell; - @interface PostSettingsViewController () @property (nonnull, nonatomic, strong) NSArray *sections; diff --git a/WordPress/Classes/ViewRelated/Views/StoppableProgressIndicatorView.swift b/WordPress/Classes/ViewRelated/Views/StoppableProgressIndicatorView.swift deleted file mode 100644 index d374bd509a3b..000000000000 --- a/WordPress/Classes/ViewRelated/Views/StoppableProgressIndicatorView.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import UIKit - -private let rotationAnimationKey = "rotation" - -@objcMembers class StoppableProgressIndicatorView: UIView { - - private let progressIndicator: CAShapeLayer - let stopButton: UIControl - - var hidesWhenStopped: Bool = true { - didSet { - if hidesWhenStopped, !isAnimating { - isHidden = true - } - } - } - - var mayStop: Bool { - get { !stopButton.isHidden } - set { stopButton.isHidden = !newValue } - } - - var isAnimating: Bool { - progressIndicator.animation(forKey: rotationAnimationKey) != nil - } - - private var resumeAnimation: Bool = true - - override init(frame: CGRect) { - stopButton = UIControl(frame: .zero) - progressIndicator = CAShapeLayer() - progressIndicator.isHidden = true - progressIndicator.lineWidth = 2 - super.init(frame: frame) - - layer.addSublayer(progressIndicator) - addSubview(stopButton) - stopButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stopButton.centerXAnchor.constraint(equalTo: centerXAnchor), - stopButton.centerYAnchor.constraint(equalTo: centerYAnchor), - stopButton.widthAnchor.constraint(equalTo: stopButton.heightAnchor), - stopButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.3), - ]) - - updateAppearance() - NotificationCenter.default.addObserver(self, selector: #selector(enterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func tintColorDidChange() { - super.tintColorDidChange() - updateAppearance() - } - - override func layoutSubviews() { - super.layoutSubviews() - progressIndicator.frame = layer.bounds - resetIndicatorShape() - } - - func startAnimating() { - if isAnimating { - return - } - - isHidden = false - progressIndicator.isHidden = false - resumeAnimation = true - - let rotation = CABasicAnimation(keyPath: "transform.rotation") - rotation.toValue = CGFloat.pi * 2 - rotation.duration = 1 - rotation.repeatCount = Float.infinity - progressIndicator.add(rotation, forKey: rotationAnimationKey) - } - - func stopAnimating() { - resumeAnimation = false - progressIndicator.removeAnimation(forKey: rotationAnimationKey) - progressIndicator.isHidden = true - - if hidesWhenStopped { - isHidden = true - } - } - - func enterForeground() { - if resumeAnimation { - startAnimating() - } - } - - private func resetIndicatorShape() { - let bounds = progressIndicator.bounds - let path = CGMutablePath() - path.addArc( - center: CGPoint(x: bounds.midX, y: bounds.midY), - radius: (bounds.width - progressIndicator.lineWidth) / 2, - startAngle: -CGFloat.pi / 2, - endAngle: -CGFloat.pi / 2 + CGFloat.pi * 1.8, - clockwise: false - ) - progressIndicator.path = path - } - - private func updateAppearance() { - stopButton.backgroundColor = tintColor - progressIndicator.strokeColor = tintColor.cgColor - progressIndicator.fillColor = nil - } - -} From 5e3dd5a7f07922b43e637ef569497516d9cccdc3 Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 15:17:15 -0500 Subject: [PATCH 158/193] Remvove unused featured image size --- .../Post/PostSettingsViewController.m | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 7cb3b8140780..d296da2f4781 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -44,10 +44,9 @@ @interface PostSettingsViewController () *unsupportedConnections; @property (nonatomic, strong) NSMutableArray *enabledConnections; @@ -1018,20 +1017,6 @@ - (void)showTagsPicker [self.navigationController pushViewController:tagsPicker animated:YES]; } -- (CGSize)featuredImageSize -{ - CGFloat width = CGRectGetWidth(self.view.frame); - CGFloat height = ceilf(width * 0.66); - return CGSizeMake(width, height); -} - -- (void)featuredImageFailedLoading:(NSIndexPath *)indexPath withError:(NSError *)error -{ - DDLogError(@"Error loading featured image: %@", error); - UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; - cell.textLabel.text = NSLocalizedString(@"Featured Image did not load", @""); -} - #pragma mark - Jetpack Social - (UITableViewCell *)configureGenericCellWith:(UIView *)view { From ec09ca84cf17a1092d43f10026f0d9b6ea8e5a1d Mon Sep 17 00:00:00 2001 From: kean Date: Wed, 8 Jan 2025 15:19:55 -0500 Subject: [PATCH 159/193] Remove more unused code --- WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h | 1 + WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h index 501d28ef6fbc..926e9d6b571e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h @@ -1,6 +1,7 @@ #import #import "AbstractPost.h" +// TODO: (kean) figure out if it's still used (presumably yet) @protocol FeaturedImageDelegate - (void)gutenbergDidRequestFeaturedImageId:(nonnull NSNumber *)mediaID; diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index d296da2f4781..bb2de297d6a1 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -44,8 +44,6 @@ @interface PostSettingsViewController () *unsupportedConnections; From e4554a9dd0b3040fe74e96f02a7611759dc60f69 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 09:31:21 -0500 Subject: [PATCH 160/193] Remove unused code --- .../ViewRelated/Post/PostSettingsViewController.m | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index bb2de297d6a1..eb6bbb3843c9 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -663,20 +663,6 @@ - (UITableViewCell *)configureStickyPostCellForIndexPath:(NSIndexPath *)indexPat return cell; } -// TODO: (kean) remove -- (nullable NSURL *)urlForFeaturedImage { - NSURL *featuredURL = self.apost.featuredImage.absoluteLocalURL; - - if (!featuredURL || ![featuredURL checkResourceIsReachableAndReturnError:nil]) { - featuredURL = [NSURL URLWithString:self.apost.featuredImage.remoteURL]; - } - - if (!featuredURL) { - featuredURL = self.apost.featuredImageURLForDisplay; - } - return featuredURL; -} - - (UITableViewCell *)configureSocialCellForIndexPath:(NSIndexPath *)indexPath connection:(PublicizeConnection *)connection canEditSharing:(BOOL)canEditSharing From e8b846b76d81858ebba6b8d3a885e6b367d738d7 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 09:49:45 -0500 Subject: [PATCH 161/193] Add SiteMediaImageView --- .../AsyncImageKit/Views/AsyncImageView.swift | 2 +- .../SiteMedia/Views/SiteMediaImageView.swift | 125 ++++++++++++++++++ .../PostSettingsViewController+Swift.swift | 7 +- .../Views/PostSettingsFeaturedImageCell.swift | 28 +--- 4 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift diff --git a/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift index 9878957f0314..464490a0bfb2 100644 --- a/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift +++ b/Modules/Sources/AsyncImageKit/Views/AsyncImageView.swift @@ -184,7 +184,7 @@ extension GIFImageView { } } - private func prepareForReuse() { + public func prepareForReuse() { if isAnimatingGIF { prepareForReuse() } else { diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift new file mode 100644 index 000000000000..d538a4c3f406 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift @@ -0,0 +1,125 @@ +import UIKit +import Gifu +import SwiftUI +import AsyncImageKit +import WordPressUI + +struct SiteMediaImage: UIViewRepresentable { + var media: Media + var size: MediaImageService.ImageSize + private var _loadingStyle = SiteMediaImageView.LoadingStyle.background + + init(media: Media, size: MediaImageService.ImageSize) { + self.media = media + self.size = size + } + + func loadingStyle(_ style: SiteMediaImageView.LoadingStyle) -> SiteMediaImage { + var copy = self + copy._loadingStyle = style + return copy + } + + func makeUIView(context: Context) -> SiteMediaImageView { + SiteMediaImageView() + } + + func updateUIView(_ view: SiteMediaImageView, context: Context) { + view.loadingStyle = _loadingStyle + view.setImage(with: media, size: size) + } +} + +@MainActor +final class SiteMediaImageView: UIView { + private let imageView = GIFImageView() + private var spinner: UIActivityIndicatorView? + private let controller = SiteMediaImageLoadingController() + + /// By default, `background`. + var loadingStyle = LoadingStyle.background + + enum LoadingStyle { + /// Shows a secondary background color during the download. + case background + /// Shows a spinner during the download. + case spinner + } + + /// The currently displayed image. If the image is animated, returns an + /// instance of ``AnimatedImage``. + var image: UIImage? { + didSet { + if let image { + imageView.configure(image: image) + } else { + imageView.prepareForReuse() + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + controller.onStateChanged = { [weak self] in self?.setState($0) } + + addSubview(imageView) + imageView.pinEdges() + + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.accessibilityIgnoresInvertColors = true + + backgroundColor = .secondarySystemBackground + } + + /// Removes the current image and stops the outstanding downloads. + func prepareForReuse() { + controller.prepareForReuse() + image = nil + } + + func setImage(with media: Media, size: MediaImageService.ImageSize) { + controller.setImage(with: media, size: size) + } + + private func setState(_ state: ImageLoadingController.State) { + imageView.isHidden = true + spinner?.stopAnimating() + + switch state { + case .loading: + switch loadingStyle { + case .background: + backgroundColor = .secondarySystemBackground + case .spinner: + makeSpinner().startAnimating() + } + case .success(let image): + self.image = image + imageView.isHidden = false + backgroundColor = .clear + case .failure: + break + } + } + + private func makeSpinner() -> UIActivityIndicatorView { + if let spinner { + return spinner + } + let spinner = UIActivityIndicatorView() + addSubview(spinner) + spinner.pinCenter() + self.spinner = spinner + return spinner + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 1b4b534c2dca..d90e23126e74 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -259,7 +259,7 @@ extension PostSettingsViewController { PostSettingsFeaturedImageCell(post: apost, viewModel: viewModel) .environment(\.presentingViewController, self) } - if viewModel.featuredImageURL != nil { + if apost.featuredImage != nil { configuration = configuration.margins(.all, 0) } cell.contentConfiguration = configuration @@ -267,10 +267,7 @@ extension PostSettingsViewController { } @objc func showFeaturedImageSelector() { - guard let featuredImage = apost.featuredImage else { - return - } - + guard let featuredImage = apost.featuredImage else { return } let lightboxVC = LightboxViewController(media: featuredImage) lightboxVC.configureZoomTransition() present(lightboxVC, animated: true) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 8fcbfc0f97f3..7ae386e548e2 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -7,8 +7,9 @@ struct PostSettingsFeaturedImageCell: View { @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel var body: some View { - if let imageURL = viewModel.featuredImageURL { - FeaturedImageView(imageURL: imageURL, post: viewModel.post) + if let image = post.featuredImage { + SiteMediaImage(media: image, size: .large) + .loadingStyle(.spinner) .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) .overlay(alignment: .topTrailing) { Menu { @@ -60,34 +61,11 @@ struct PostSettingsFeaturedImageCell: View { } } -private struct FeaturedImageView: UIViewRepresentable { - let imageURL: URL - let post: AbstractPost - - @Environment(\.presentingViewController) var presentingViewController - - func makeUIView(context: Context) -> AsyncImageView { - let imageView = AsyncImageView() - imageView.configuration.loadingStyle = .spinner - imageView.setImage(with: imageURL, host: MediaHost(post)) - - return imageView - } - - func updateUIView(_ view: AsyncImageView, context: Context) { - // Do nothing - } -} - final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { @Published private(set) var upload: Media? let post: AbstractPost - var featuredImageURL: URL? { - post.featuredImage?.remoteURL.flatMap(URL.init) - } - private var receipt: UUID? private let coordinator = MediaCoordinator.shared From 45b2fcc49a63be3328f91cb4c278646121021d60 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 09:51:43 -0500 Subject: [PATCH 162/193] Remove unused code --- .../Post/PostSettingsViewController.m | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index eb6bbb3843c9..e044a505d22b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -622,31 +622,6 @@ - (UITableViewCell *)makeFeaturedImageCellForIndexPath:(NSIndexPath *)indexPath [self configureFeaturedImageCellWithCell:cell viewModel:self.featuredImageViewModel]; cell.tag = PostSettingsRowFeaturedImage; return cell; - - // TODO: (kean) remove unused code -// if (!self.apost.featuredImage && !self.isUploadingMedia) { -// return [self cellForSetFeaturedImage]; -// -// } else if (self.isUploadingMedia || self.apost.featuredImage.remoteStatus == MediaRemoteStatusPushing) { -// // Is featured Image set on the post and it's being pushed to the server? -// if (!self.isUploadingMedia) { -// self.isUploadingMedia = YES; -// [self setupObservingOfMedia:self.apost.featuredImage]; -// } -// self.featuredImage = nil; -// return [self cellForFeaturedImageUploadProgressAtIndexPath:indexPath]; -// -// } else if (self.apost.featuredImage && self.apost.featuredImage.remoteStatus == MediaRemoteStatusFailed) { -// // Do we have an feature image set and for some reason the upload failed? -// return [self cellForFeaturedImageError]; -// } else { -// NSURL *featuredURL = [self urlForFeaturedImage]; -// if (!featuredURL) { -// return [self cellForSetFeaturedImage]; -// } -// -// return [self cellForFeaturedImageWithURL:featuredURL atIndexPath:indexPath]; -// } } - (UITableViewCell *)configureStickyPostCellForIndexPath:(NSIndexPath *)indexPath From 7f4c780ef6443eb4421ddd48304b5a969570b641 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 10:43:11 -0500 Subject: [PATCH 163/193] Integrate FeaturedImageDelegate --- .../Classes/ViewRelated/Post/PostSettingsViewController.h | 2 +- .../Classes/ViewRelated/Post/PostSettingsViewController.m | 1 + .../Post/Views/PostSettingsFeaturedImageCell.swift | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h index 926e9d6b571e..24aa49e8ae50 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h @@ -1,7 +1,7 @@ #import #import "AbstractPost.h" -// TODO: (kean) figure out if it's still used (presumably yet) +// TODO: It can be removed when the new editor is released. It only exists to support the "Featured" badge on featured images in Gutenberg mobile. @protocol FeaturedImageDelegate - (void)gutenbergDidRequestFeaturedImageId:(nonnull NSNumber *)mediaID; diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index e044a505d22b..7f8ea7915fda 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -117,6 +117,7 @@ - (void)viewDidLoad self.tableView.accessibilityIdentifier = @"SettingsTable"; self.featuredImageViewModel.tableView = self.tableView; + self.featuredImageViewModel.delegate = self.featuredImageDelegate; _blogService = [[BlogService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 7ae386e548e2..49b2d6f733fc 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -70,6 +70,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { private let coordinator = MediaCoordinator.shared @objc weak var tableView: UITableView? + @objc weak var delegate: FeaturedImageDelegate? @objc init(post: AbstractPost) { self.post = post @@ -113,6 +114,9 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { UIView.performWithoutAnimation { upload = nil post.featuredImage = media + if let mediaID = media.mediaID { + delegate?.gutenbergDidRequestFeaturedImageId(mediaID) + } tableView?.reloadData() } } @@ -125,14 +129,14 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { func buttonRemoveTapped() { WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "removed"]) - // TODO: (kean) do we need to call removeMediaObject? post.featuredImage = nil + delegate?.gutenbergDidRequestFeaturedImageId(GutenbergFeaturedImageHelper.mediaIdNoFeaturedImageSet as NSNumber) tableView?.reloadData() } } private enum Strings { - static let buttonSetFeaturedImage = NSLocalizedString("postSettings.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") + static let buttonSetFeaturedImage = NSLocalizedString("postSettings.featuredImage.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") static let uploading = NSLocalizedString("postSettings.featuredImage.uploading", value: "Uploading…", comment: "Post Settings") static let cancelUpload = NSLocalizedString("postSettings.featuredImage.cancelUpload", value: "Cancel Upload", comment: "Cancel (single) upload button in Post Settings / Featuerd Image cell") static let uploadFailed = NSLocalizedString("postSettings.featuredImage.uploadFailed", value: "Failed to upload new featured image", comment: "Snackbar title") From 3d007febebf0f771f4a3b592b8a5d091b8880f82 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 10:48:37 -0500 Subject: [PATCH 164/193] Fix SiteMediaImage background when loading with spinner --- .../ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift index d538a4c3f406..cf2e38dedefa 100644 --- a/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift +++ b/WordPress/Classes/ViewRelated/Media/SiteMedia/Views/SiteMediaImageView.swift @@ -94,6 +94,7 @@ final class SiteMediaImageView: UIView { private func setState(_ state: ImageLoadingController.State) { imageView.isHidden = true spinner?.stopAnimating() + backgroundColor = .clear switch state { case .loading: @@ -106,7 +107,6 @@ final class SiteMediaImageView: UIView { case .success(let image): self.image = image imageView.isHidden = false - backgroundColor = .clear case .failure: break } From c2523d4490d01c729a7b5705d1ca6dcb486d12b4 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 10:55:37 -0500 Subject: [PATCH 165/193] Fix animations --- .../Views/PostSettingsFeaturedImageCell.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 49b2d6f733fc..0ffcbe36abe6 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -110,15 +110,9 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { private func didProcessMedia(_ media: Media) { wpAssert(media.remoteURL != nil) - // TODO: (kean) fix animations - UIView.performWithoutAnimation { - upload = nil - post.featuredImage = media - if let mediaID = media.mediaID { - delegate?.gutenbergDidRequestFeaturedImageId(mediaID) - } - tableView?.reloadData() - } + + upload = nil + setFeaturedImage(media) } func buttonCancelTapped() { guard let upload else { return } @@ -129,9 +123,16 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { func buttonRemoveTapped() { WPAnalytics.track(.editorPostFeaturedImageChanged, properties: ["via": "settings", "action": "removed"]) - post.featuredImage = nil - delegate?.gutenbergDidRequestFeaturedImageId(GutenbergFeaturedImageHelper.mediaIdNoFeaturedImageSet as NSNumber) - tableView?.reloadData() + setFeaturedImage(nil) + } + + private func setFeaturedImage(_ media: Media?) { + upload = nil + post.featuredImage = media + delegate?.gutenbergDidRequestFeaturedImageId(media?.mediaID ?? GutenbergFeaturedImageHelper.mediaIdNoFeaturedImageSet as NSNumber) + UIView.performWithoutAnimation { + tableView?.reloadData() + } } } From 017f72f1bfed4e9ba760376f6a209d7a59c9b8db Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:04:54 -0500 Subject: [PATCH 166/193] Add zoom transition --- .../ViewRelated/Post/PostSettingsViewController+Swift.swift | 4 ++-- .../Classes/ViewRelated/Post/PostSettingsViewController.m | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index d90e23126e74..b61456416479 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -266,10 +266,10 @@ extension PostSettingsViewController { cell.selectionStyle = .none } - @objc func showFeaturedImageSelector() { + @objc func showFeaturedImageSelector(cell: UITableViewCell) { guard let featuredImage = apost.featuredImage else { return } let lightboxVC = LightboxViewController(media: featuredImage) - lightboxVC.configureZoomTransition() + lightboxVC.configureZoomTransition(sourceView: cell.contentView) present(lightboxVC, animated: true) } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 7f8ea7915fda..03e352cc8acd 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -473,7 +473,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else if (cell.tag == PostSettingsRowFormat) { [self showPostFormatSelector]; } else if (cell.tag == PostSettingsRowFeaturedImage) { - [self showFeaturedImageSelector]; + [self showFeaturedImageSelectorWithCell:cell]; } else if (sec == PostSettingsSectionDisabledTwitter) { [self showShareDetailForIndexPath:indexPath]; } else if (cell.tag == PostSettingsRowShareConnection) { From 0e100ad1da61c0699a69a906248d1d55dac87991 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:09:23 -0500 Subject: [PATCH 167/193] Add shadow to more menu --- .../ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 0ffcbe36abe6..cb1574060fcd 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -18,6 +18,7 @@ struct PostSettingsFeaturedImageCell: View { Image(systemName: "ellipsis.circle.fill") .foregroundStyle(Color(.label), Color(.secondarySystemBackground)) .font(.title) + .shadow(color: .black.opacity(0.5), radius: 10) .padding(8) } } From 7ea1cf4829bc285c2cdedc5db51420fc1316bc25 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:24:06 -0500 Subject: [PATCH 168/193] Make the entire cell tappable --- .../ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index cb1574060fcd..7a2c4d7ac299 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -11,7 +11,7 @@ struct PostSettingsFeaturedImageCell: View { SiteMediaImage(media: image, size: .large) .loadingStyle(.spinner) .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) - .overlay(alignment: .topTrailing) { + .overlay { Menu { Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) } label: { @@ -20,6 +20,7 @@ struct PostSettingsFeaturedImageCell: View { .font(.title) .shadow(color: .black.opacity(0.5), radius: 10) .padding(8) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) } } } else if viewModel.upload != nil { From 5d13e712fdaab6481614f8b3f9a703d15f9aa7fc Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:38:11 -0500 Subject: [PATCH 169/193] Add View action --- .../PostSettingsViewController+Swift.swift | 8 ++-- .../Post/PostSettingsViewController.m | 2 - .../Views/PostSettingsFeaturedImageCell.swift | 38 ++++++++++++++----- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index b61456416479..ebffcc334b5e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -256,8 +256,10 @@ extension PostSettingsViewController { extension PostSettingsViewController { @objc func configureFeaturedImageCell(cell: UITableViewCell, viewModel: PostSettingsFeaturedImageViewModel) { var configuration = UIHostingConfiguration { - PostSettingsFeaturedImageCell(post: apost, viewModel: viewModel) - .environment(\.presentingViewController, self) + PostSettingsFeaturedImageCell(post: apost, viewModel: viewModel) { [weak self] in + self?.showFeaturedImageSelector(cell: cell) + } + .environment(\.presentingViewController, self) } if apost.featuredImage != nil { configuration = configuration.margins(.all, 0) @@ -266,7 +268,7 @@ extension PostSettingsViewController { cell.selectionStyle = .none } - @objc func showFeaturedImageSelector(cell: UITableViewCell) { + private func showFeaturedImageSelector(cell: UITableViewCell) { guard let featuredImage = apost.featuredImage else { return } let lightboxVC = LightboxViewController(media: featuredImage) lightboxVC.configureZoomTransition(sourceView: cell.contentView) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 03e352cc8acd..49c816a23070 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -472,8 +472,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath [self showPostAuthorSelector]; } else if (cell.tag == PostSettingsRowFormat) { [self showPostFormatSelector]; - } else if (cell.tag == PostSettingsRowFeaturedImage) { - [self showFeaturedImageSelectorWithCell:cell]; } else if (sec == PostSettingsSectionDisabledTwitter) { [self showShareDetailForIndexPath:indexPath]; } else if (cell.tag == PostSettingsRowShareConnection) { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 7a2c4d7ac299..d4246033dba3 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -6,23 +6,22 @@ struct PostSettingsFeaturedImageCell: View { @ObservedObject var post: AbstractPost @ObservedObject var viewModel: PostSettingsFeaturedImageViewModel + var onViewTapped: () -> Void + var body: some View { if let image = post.featuredImage { SiteMediaImage(media: image, size: .large) .loadingStyle(.spinner) .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) .overlay { - Menu { - Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) - } label: { - Image(systemName: "ellipsis.circle.fill") - .foregroundStyle(Color(.label), Color(.secondarySystemBackground)) - .font(.title) - .shadow(color: .black.opacity(0.5), radius: 10) - .padding(8) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - } + menu + } + .contextMenu { + actions + } preview: { + SiteMediaImage(media: image, size: .large) } + } else if viewModel.upload != nil { uploading } else { @@ -39,6 +38,25 @@ struct PostSettingsFeaturedImageCell: View { } } + private var menu: some View { + Menu { + actions + } label: { + Image(systemName: "ellipsis.circle.fill") + .foregroundStyle(Color(.label), Color(.secondarySystemBackground)) + .font(.title) + .shadow(color: .black.opacity(0.5), radius: 10) + .padding(8) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + } + } + + @ViewBuilder + private var actions: some View { + Button(SharedStrings.Button.view, systemImage: "photo", action: onViewTapped) + Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) + } + private var uploading: some View { HStack(alignment: .center, spacing: 0) { ProgressView() From 3f27e1794f29c89f3466fd67c0bcc446814e4576 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:44:17 -0500 Subject: [PATCH 170/193] Add replace action --- .../Views/PostSettingsFeaturedImageCell.swift | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index d4246033dba3..1c90754c7914 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -18,18 +18,12 @@ struct PostSettingsFeaturedImageCell: View { } .contextMenu { actions - } preview: { - SiteMediaImage(media: image, size: .large) } } else if viewModel.upload != nil { uploading } else { - let configuration = MediaPickerConfiguration( - sources: [.photos, .camera, .playground, .siteMedia(blog: post.blog)], - filter: .images - ) - MediaPicker(configuration: configuration, onSelection: viewModel.setFeaturedImage) { + makeMediaPicker { Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") .frame(maxWidth: .infinity) .contentShape(Rectangle()) // Make the whole cell tappable @@ -53,7 +47,10 @@ struct PostSettingsFeaturedImageCell: View { @ViewBuilder private var actions: some View { - Button(SharedStrings.Button.view, systemImage: "photo", action: onViewTapped) + Button(SharedStrings.Button.view, systemImage: "plus.magnifyingglass", action: onViewTapped) + makeMediaPicker { + Button(Strings.replaceImage, systemImage: "photo.badge.plus", action: onViewTapped) + } Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) } @@ -79,6 +76,16 @@ struct PostSettingsFeaturedImageCell: View { } } } + + private func makeMediaPicker(@ViewBuilder content: @escaping () -> Content) -> some View { + let configuration = MediaPickerConfiguration( + sources: [.photos, .camera, .playground, .siteMedia(blog: post.blog)], + filter: .images + ) + return MediaPicker(configuration: configuration, onSelection: viewModel.setFeaturedImage) { + content() + } + } } final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { @@ -159,6 +166,7 @@ final class PostSettingsFeaturedImageViewModel: NSObject, ObservableObject { private enum Strings { static let buttonSetFeaturedImage = NSLocalizedString("postSettings.featuredImage.setFeaturedImageButton", value: "Set Featured Image", comment: "Button in Post Settings") static let uploading = NSLocalizedString("postSettings.featuredImage.uploading", value: "Uploading…", comment: "Post Settings") - static let cancelUpload = NSLocalizedString("postSettings.featuredImage.cancelUpload", value: "Cancel Upload", comment: "Cancel (single) upload button in Post Settings / Featuerd Image cell") + static let cancelUpload = NSLocalizedString("postSettings.featuredImage.cancelUpload", value: "Cancel Upload", comment: "Cancel upload button in Post Settings / Featured Image cell") + static let replaceImage = NSLocalizedString("postSettings.featuredImage.replaceImage", value: "Replace", comment: "Replace image upload button in Post Settings / Featured Image cell") static let uploadFailed = NSLocalizedString("postSettings.featuredImage.uploadFailed", value: "Failed to upload new featured image", comment: "Snackbar title") } From a828b2caebb3b0c8f8c4b106acf1c2240cd26e02 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:54:55 -0500 Subject: [PATCH 171/193] Show spinner when replacing an image --- .../Views/PostSettingsFeaturedImageCell.swift | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index 1c90754c7914..bcb6896984d6 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -20,14 +20,18 @@ struct PostSettingsFeaturedImageCell: View { actions } - } else if viewModel.upload != nil { - uploading } else { - makeMediaPicker { - Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) // Make the whole cell tappable - .accessibilityIdentifier("SetFeaturedImage") + if viewModel.upload != nil { + // The upload state when no image is selected. For the "Replace" + // flow, the app shows the upload differently (see `menu`). + uploading + } else { + makeMediaPicker { + Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) // Make the whole cell tappable + .accessibilityIdentifier("SetFeaturedImage") + } } } } @@ -36,22 +40,38 @@ struct PostSettingsFeaturedImageCell: View { Menu { actions } label: { - Image(systemName: "ellipsis.circle.fill") - .foregroundStyle(Color(.label), Color(.secondarySystemBackground)) - .font(.title) - .shadow(color: .black.opacity(0.5), radius: 10) - .padding(8) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + ZStack { + Circle() + .foregroundStyle(Color(.secondarySystemBackground)) + .frame(width: 30, height: 30) + if viewModel.upload != nil { + ProgressView() + .controlSize(.small) + } else { + Image(systemName: "ellipsis") + .foregroundStyle(Color(.label)) + .font(.system(size: 18)) + } + } + .shadow(color: .black.opacity(0.5), radius: 10) + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) } } @ViewBuilder private var actions: some View { - Button(SharedStrings.Button.view, systemImage: "plus.magnifyingglass", action: onViewTapped) - makeMediaPicker { - Button(Strings.replaceImage, systemImage: "photo.badge.plus", action: onViewTapped) + if viewModel.upload == nil { + Button(SharedStrings.Button.view, systemImage: "plus.magnifyingglass", action: onViewTapped) + makeMediaPicker { + Button(Strings.replaceImage, systemImage: "photo.badge.plus", action: onViewTapped) + } + Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) + } else { + Button(role: .destructive, action: viewModel.buttonCancelTapped) { + Label(Strings.cancelUpload, systemImage: "trash") + } } - Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) } private var uploading: some View { From b0f3a4be4d0b84e3e37cc81d84ea8e62997b2002 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 11:55:11 -0500 Subject: [PATCH 172/193] Remove unused reloadFeaturedImageCell --- .../Classes/ViewRelated/Post/PostSettingsViewController.h | 1 - .../Classes/ViewRelated/Post/PostSettingsViewController.m | 5 ----- 2 files changed, 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h index 24aa49e8ae50..eb34694d7b3d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.h @@ -20,6 +20,5 @@ @property (nonatomic, weak, nullable) id featuredImageDelegate; - (void)reloadData; -- (void)reloadFeaturedImageCell; @end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 49c816a23070..fc338431b403 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1010,11 +1010,6 @@ - (void)reloadSocialSectionComparingValue:(NSUInteger)value } } -- (void)reloadFeaturedImageCell { - NSIndexPath *featureImageCellPath = [NSIndexPath indexPathForRow:0 inSection:[self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]]; - [self.tableView reloadRowsAtIndexPaths:@[featureImageCellPath] withRowAnimation:UITableViewRowAnimationFade]; -} - // MARK: - Page Attributes - (UITableViewCell *)configurePageAttributesCellForIndexPath:(NSIndexPath *)indexPath From f44329d03ceb16aa03670138450621b3fac0e9cd Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 12:01:20 -0500 Subject: [PATCH 173/193] Update release notse --- RELEASE-NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 6b176a366550..06b08377834e 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -20,7 +20,7 @@ * [*] Enable fast deceleration for filters on the Discover tab [#23954] * [*] Disable universal links support for QR code login. You can only scan the codes using the app now. [#23953] * [*] Add scroll-to-top button to Reader streams [#23957] - +* [*] Add a quick way to replace a featured image for a post [#23962] 25.6 ----- From 41cd8ee8465befab79a927b12ad18d57445ce02d Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 12:07:08 -0500 Subject: [PATCH 174/193] Revert "Update site menu style on iPhone" This reverts commit 565a34b31b0b32bb67c0aa5d3e58f515accd995c. --- RELEASE-NOTES.txt | 1 - .../DashboardQuickActionsCardCell.swift | 23 +++++++++++++------ .../Blog Details/BlogDetailsViewController.m | 16 +++++-------- .../Sidebar/SiteMenuViewController.swift | 6 ++--- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 06b08377834e..5b1a5b5f41df 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -13,7 +13,6 @@ * [*] 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] -* [*] Update site menu style on iPhone [#23944] * [*] Integrate zoom transitions in Themes, Reader [#23945, #23947] * [*] Fix an issue with site icons cropped in share extensions [#23950] * [*] Show selected filter in the Discover navigation bar [#23956] diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift index 08fd952c4162..216994cf126f 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Quick Actions/DashboardQuickActionsCardCell.swift @@ -22,6 +22,7 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab private var items: [DashboardQuickActionItemViewModel] = [] private var viewModel: DashboardQuickActionsViewModel? private weak var parentViewController: BlogDashboardViewController? + private weak var blogDetailsViewController: BlogDetailsViewController? private var cancellables: [AnyCancellable] = [] override init(frame: CGRect) { @@ -108,9 +109,13 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab trackQuickActionsEvent(.statsAccessed, blog: blog) StatsViewController.show(for: blog, from: parentViewController) case .more: - let viewController = SiteMenuViewController(blog: blog) - viewController.delegate = self - parentViewController.show(viewController, sender: nil) + let viewController = BlogDetailsViewController() + viewController.isScrollEnabled = true + viewController.tableView.isScrollEnabled = true + viewController.blog = blog + viewController.presentationDelegate = self + self.blogDetailsViewController = viewController + self.parentViewController?.show(viewController, sender: nil) } } @@ -119,11 +124,15 @@ final class DashboardQuickActionsCardCell: UICollectionViewCell, Reusable, UITab } } -// MARK: - DashboardQuickActionsCardCell (SiteMenuViewControllerDelegate) +// MARK: - DashboardQuickActionsCardCell (BlogDetailsPresentationDelegate) -extension DashboardQuickActionsCardCell: SiteMenuViewControllerDelegate { - func siteMenuViewController(_ siteMenuViewController: SiteMenuViewController, showDetailsViewController viewController: UIViewController) { - siteMenuViewController.show(viewController, sender: nil) +extension DashboardQuickActionsCardCell: BlogDetailsPresentationDelegate { + func showBlogDetailsSubsection(_ subsection: BlogDetailsSubsection) { + self.blogDetailsViewController?.showDetailView(for: subsection) + } + + func presentBlogDetailsViewController(_ viewController: UIViewController) { + self.blogDetailsViewController?.show(viewController, sender: nil) } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index 6ce4f52196e1..33b8dc9d72e0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -323,10 +323,6 @@ - (void)viewDidLoad if (@available(iOS 17.0, *)) { [self registerForTraitChanges:@[[UITraitHorizontalSizeClass self]] withAction:@selector(handleTraitChanges)]; } - - if (self.isSidebarModeEnabled && ![self isSplitViewDisplayed]) { - self.tableView.backgroundColor = [UIColor systemBackgroundColor]; - } } - (void)viewWillAppear:(BOOL)animated @@ -1042,7 +1038,7 @@ - (void)configureTableViewData } - (Boolean)isSplitViewDisplayed { - return self.splitViewController != nil; + return self.isSidebarModeEnabled; } /// This section is available on Jetpack only. @@ -1057,7 +1053,7 @@ - (BlogDetailsSection *)contentSectionViewModel [rows addObject:[self mediaRow]]; [rows addObject:[self commentsRow]]; - NSString *title = [self isSplitViewDisplayed] ? nil : [BlogDetailsViewControllerStrings contentSectionTitle]; + NSString *title = self.isSidebarModeEnabled ? nil : [BlogDetailsViewControllerStrings contentSectionTitle]; return [[BlogDetailsSection alloc] initWithTitle:title andRows:rows category:BlogDetailsSectionCategoryContent]; } @@ -1586,7 +1582,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [WPStyleGuide configureTableViewDestructiveActionCell:cell]; } else { if (row.showsDisclosureIndicator) { - cell.accessoryType = [self isSidebarModeEnabled] ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; + cell.accessoryType = [self isSplitViewDisplayed] ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; } else { cell.accessoryType = UITableViewCellAccessoryNone; } @@ -1717,7 +1713,7 @@ - (void)showCommentsFromSource:(BlogDetailsNavigationSource)source CommentsViewController *commentsVC = [CommentsViewController controllerWithBlog:self.blog]; commentsVC.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; - if ([self isSplitViewDisplayed]) { + if (self.isSidebarModeEnabled) { commentsVC.isSidebarModeEnabled = YES; UISplitViewController *splitVC = [[UISplitViewController alloc] initWithStyle:UISplitViewControllerStyleDoubleColumn]; @@ -1799,7 +1795,7 @@ - (void)showSettingsFromSource:(BlogDetailsNavigationSource)source [self trackEvent:WPAnalyticsStatOpenedSiteSettings fromSource:source]; SiteSettingsViewController *controller = [[SiteSettingsViewController alloc] initWithBlog:self.blog]; controller.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; - if ([self isSplitViewDisplayed]) { + if (self.isSidebarModeEnabled) { UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller]; __weak BlogDetailsViewController *weakSelf = self; #pragma clang diagnostic push @@ -1867,7 +1863,7 @@ - (void)showStatsFromSource:(BlogDetailsNavigationSource)source - (void)showDashboard { - if ([self isSplitViewDisplayed]) { + if (self.isSidebarModeEnabled) { MySiteViewController *controller = [MySiteViewController makeForBlog:self.blog isSidebarModeEnabled:true]; [self.presentationDelegate presentBlogDetailsViewController:controller]; } else { diff --git a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift index 987b4e357a47..9a4c9a08b056 100644 --- a/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift +++ b/WordPress/Classes/ViewRelated/System/Sidebar/SiteMenuViewController.swift @@ -37,9 +37,7 @@ final class SiteMenuViewController: UIViewController { blogDetailsVC.view.translatesAutoresizingMaskIntoConstraints = false view.pinSubviewToAllEdges(blogDetailsVC.view) - if splitViewController != nil { - blogDetailsVC.showInitialDetailsForBlog() - } + blogDetailsVC.showInitialDetailsForBlog() navigationItem.title = blog.settings?.name ?? (blog.displayURL as String?) ?? "" @@ -67,7 +65,7 @@ final class SiteMenuViewController: UIViewController { super.viewDidAppear(animated) if #available(iOS 17, *) { - if tipObserver == nil && splitViewController != nil { + if tipObserver == nil { tipObserver = registerTipPopover(AppTips.SidebarTip(), sourceItem: getTipAnchor(), arrowDirection: [.up]) } } From a49ee2defe0579fea49b76a418558514a082a6a2 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 9 Jan 2025 12:14:50 -0500 Subject: [PATCH 175/193] Fix an issue with wrong cover images appearing in Reader (#23914) * Fix an issue with wrong cover images appearing in Reader * Update release notes * Update release notes --- Modules/Package.swift | 2 +- RELEASE-NOTES.txt | 1 + WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 1f999d2b80ce..3df99b5154e2 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -44,7 +44,7 @@ let package = Package( .package(url: "https://github.com/wordpress-mobile/MediaEditor-iOS", branch: "task/spm-support"), .package(url: "https://github.com/wordpress-mobile/NSObject-SafeExpectations", from: "0.0.6"), .package(url: "https://github.com/wordpress-mobile/NSURL-IDN", branch: "trunk"), - .package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "wpios-edition"), + .package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "fix/featured-images-reader-posts"), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // We can't use wordpress-rs branches nor commits here. Only tags work. .package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20241116"), diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5b1a5b5f41df..d9efb5cf487e 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -20,6 +20,7 @@ * [*] Disable universal links support for QR code login. You can only scan the codes using the app now. [#23953] * [*] Add scroll-to-top button to Reader streams [#23957] * [*] Add a quick way to replace a featured image for a post [#23962] +* [*] Fix an issue with posts in Reader sometimes showing incorrect covers [#23914] 25.6 ----- diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index eb6f17315f2a..daf8c8ba56d5 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -383,8 +383,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/WordPressKit-iOS", "state" : { - "branch" : "wpios-edition", - "revision" : "77b3f5e98da1e837e3983615c9fd1b38d66e3084" + "branch" : "fix/featured-images-reader-posts", + "revision" : "87e2a0902e84e03afdc29bb5b52b3a3d6a1e14d9" } }, { From a045bf1ab0960f02f46e2c07e6674dea360e394b Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 12:16:40 -0500 Subject: [PATCH 176/193] Point back to wpios-edition --- Modules/Package.swift | 2 +- WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 3df99b5154e2..1f999d2b80ce 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -44,7 +44,7 @@ let package = Package( .package(url: "https://github.com/wordpress-mobile/MediaEditor-iOS", branch: "task/spm-support"), .package(url: "https://github.com/wordpress-mobile/NSObject-SafeExpectations", from: "0.0.6"), .package(url: "https://github.com/wordpress-mobile/NSURL-IDN", branch: "trunk"), - .package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "fix/featured-images-reader-posts"), + .package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "wpios-edition"), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // We can't use wordpress-rs branches nor commits here. Only tags work. .package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20241116"), diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index daf8c8ba56d5..2dcd678a2f6a 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e79c26721ac0bbd7fe1003896d175bc4293a42c53ed03372aca8310d5da175ed", + "originHash" : "c6c4224bb9091cbaed87a958001b2435576248fec163dc8ce6be041f8e1da166", "pins" : [ { "identity" : "alamofire", @@ -383,8 +383,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/WordPressKit-iOS", "state" : { - "branch" : "fix/featured-images-reader-posts", - "revision" : "87e2a0902e84e03afdc29bb5b52b3a3d6a1e14d9" + "branch" : "wpios-edition", + "revision" : "908d96a6ff4eb38217e57c03996bf1f3e9cdb114" } }, { From e9b4605594b6e6b686234d60f7c4224479b51c02 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 9 Jan 2025 12:17:57 -0500 Subject: [PATCH 177/193] Fix an issue with non-stable order in Posts and Pages in stats (#23915) * Fix an issue with non-stable order in Posts and Pages in stats * Update release notes * Update release notes --- RELEASE-NOTES.txt | 1 + .../Classes/Stores/StatsPeriodStore.swift | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d9efb5cf487e..0d92999a6d5a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -21,6 +21,7 @@ * [*] Add scroll-to-top button to Reader streams [#23957] * [*] Add a quick way to replace a featured image for a post [#23962] * [*] Fix an issue with posts in Reader sometimes showing incorrect covers [#23914] +* [*] Fix non-stable order in Posts and Pages section in Stats [#23915] 25.6 ----- diff --git a/WordPress/Classes/Stores/StatsPeriodStore.swift b/WordPress/Classes/Stores/StatsPeriodStore.swift index ab7937953065..8686f4740f14 100644 --- a/WordPress/Classes/Stores/StatsPeriodStore.swift +++ b/WordPress/Classes/Stores/StatsPeriodStore.swift @@ -895,12 +895,24 @@ private extension StatsPeriodStore { } } - private func receivedPostsAndPages(_ postsAndPages: StatsTopPostsTimeIntervalData?, _ error: Error?) { + private func receivedPostsAndPages(_ data: StatsTopPostsTimeIntervalData?, _ error: Error?) { transaction { state in state.topPostsAndPagesStatus = error != nil ? .error : .success - if postsAndPages != nil { - state.topPostsAndPages = postsAndPages + if let data { + let sortedTopPosts = data.topPosts.sorted { lhs, rhs in + if lhs.viewsCount == rhs.viewsCount { + return lhs.title.localizedCaseInsensitiveCompare(rhs.title) == .orderedAscending + } + return lhs.viewsCount > rhs.viewsCount + } + state.topPostsAndPages = StatsTopPostsTimeIntervalData( + period: data.period, + periodEndDate: data.periodEndDate, + topPosts: sortedTopPosts, + totalViewsCount: data.totalViewsCount, + otherViewsCount: data.otherViewsCount + ) } } } From 67ba286a5dbbde47d70def1a8c938d31252789a7 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 9 Jan 2025 12:19:51 -0500 Subject: [PATCH 178/193] Fix an issue with a missing "Mark as Unread" button in the More menu (#23917) * Add missing toggle read/unread button * Show read status in the list * Update release notes * Update release notes --- RELEASE-NOTES.txt | 2 ++ .../Reader/Cards/ReaderPostCell.swift | 23 ++++++++++++++++++- .../Cards/ReaderPostCellViewModel.swift | 5 +++- .../ReaderPostActions/ReaderPostMenu.swift | 17 +++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0d92999a6d5a..74811df9978a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -22,6 +22,8 @@ * [*] Add a quick way to replace a featured image for a post [#23962] * [*] Fix an issue with posts in Reader sometimes showing incorrect covers [#23914] * [*] Fix non-stable order in Posts and Pages section in Stats [#23915] +* [*] (P2) Reader: Fix an issue with a missing "Mark as Read/Unread" button that was removed in the previous release [#23917] +* [*] (P2) Reader: Show "read" status for P2 posts in the feeds [#23917] 25.6 ----- diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 811b48aab76a..3bca261a58ea 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -71,6 +71,7 @@ private final class ReaderPostCellView: UIView { let avatarView = ReaderAvatarView() let buttonAuthor = makeAuthorButton() let timeLabel = UILabel() + let seenCheckmark = UIImageView() let buttonMore = makeButton(systemImage: "ellipsis", font: .systemFont(ofSize: 13)) // Content @@ -100,6 +101,7 @@ private final class ReaderPostCellView: UIView { private var toolbarViewHeightConstraint: NSLayoutConstraint? private var imageViewConstraints: [NSLayoutConstraint] = [] + private var isSeenCheckmarkConfigured = false private var cancellables: [AnyCancellable] = [] override init(frame: CGRect) { @@ -155,7 +157,8 @@ private final class ReaderPostCellView: UIView { // These seems to be an issue with `lineBreakMode` in `UIButton.Configuration` // and `.firstLineBaseline`, so reserving to `.center`. - let headerView = UIStackView(alignment: .center, [buttonAuthor, dot, timeLabel]) + let headerView = UIStackView(alignment: .center, [buttonAuthor, dot, timeLabel, seenCheckmark]) + headerView.setCustomSpacing(4, after: timeLabel) for view in [avatarView, headerView, postPreview, buttonMore, toolbarView] { addSubview(view) @@ -308,6 +311,13 @@ private final class ReaderPostCellView: UIView { imageView.setImage(with: imageURL, size: preferredCoverSize) } + if viewModel.isSeen == true { + configureSeenCheckmarkIfNeeded() + seenCheckmark.isHidden = false + } else { + seenCheckmark.isHidden = true + } + if !viewModel.isToolbarHidden { configureToolbar(with: viewModel.toolbar) configureToolbarAccessibility(with: viewModel.toolbar) @@ -360,6 +370,17 @@ private final class ReaderPostCellView: UIView { } } + private func configureSeenCheckmarkIfNeeded() { + guard !isSeenCheckmarkConfigured else { return } + isSeenCheckmarkConfigured = true + + seenCheckmark.image = UIImage( + systemName: "checkmark", + withConfiguration: UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .caption1).withWeight(.medium)) + ) + seenCheckmark.tintColor = .secondaryLabel + } + private static let authorAttributes = AttributeContainer([ .font: WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .medium), .foregroundColor: UIColor.label diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift index 91b6f8d48c85..8f6c24e1e6d6 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCellViewModel.swift @@ -10,6 +10,7 @@ final class ReaderPostCellViewModel { // Content let title: String let details: String + let isSeen: Bool? let imageURL: URL? // Footer (Buttons) @@ -34,9 +35,10 @@ final class ReaderPostCellViewModel { } else { self.author = post.blogNameForDisplay() ?? "" } - self.time = post.dateForDisplay()?.toShortString() ?? "–" + self.time = post.dateForDisplay()?.toShortString() ?? "" self.title = post.titleForDisplay() ?? "" self.details = post.contentPreviewForDisplay() ?? "" + self.isSeen = post.isSeenSupported ? post.isSeen : nil self.imageURL = post.featuredImageURLForDisplay() self.toolbar = ReaderPostToolbarViewModel.make(post: post) @@ -64,6 +66,7 @@ final class ReaderPostCellViewModel { self.avatarURL = URL(string: "https://picsum.photos/120/120.jpg") self.author = "WordPress Mobile Apps" self.time = "9d ago" + self.isSeen = nil self.title = "Discovering the Wonders of the Wild" self.details = "Lorem ipsum dolor sit amet. Non omnis quia et natus voluptatum et eligendi voluptate vel iusto fuga sit repellendus molestiae aut voluptatem blanditiis ad neque sapiente. Id galisum distinctio quo enim aperiam non veritatis vitae et ducimus rerum." self.imageURL = URL(string: "https://picsum.photos/1260/630.jpg") diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift index 2d05fc714563..42fa44f212a2 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift @@ -22,7 +22,7 @@ struct ReaderPostMenu { share, copyPostLink, viewPostInBrowser, - + post.isSeenSupported ? toggleSeen : nil ].compactMap { $0 }) } @@ -116,6 +116,17 @@ struct ReaderPostMenu { } } + private var toggleSeen: UIAction { + UIAction(post.isSeen ? Strings.markUnread : Strings.markRead, systemImage: post.isSeen ? "circle" : "checkmark.circle") { + track(post.isSeen ? .markUnread : .markRead) + ReaderSeenAction().execute(with: post, context: context, completion: { + NotificationCenter.default.post(name: .ReaderPostSeenToggled, object: nil, userInfo: [ReaderNotificationKeys.post: post]) + }, failure: { _ in + UINotificationFeedbackGenerator().notificationOccurred(.error) + }) + } + } + // MARK: Block and Report private func makeBlockOrReportActions() -> UIMenu { @@ -195,6 +206,8 @@ private enum ReaderPostMenuAnalyticsButton: String { case blockUser = "block_user" case reportPost = "report_post" case reportUser = "report_user" + case markRead = "mark_read" + case markUnread = "mark_unread" } private enum Strings { @@ -209,4 +222,6 @@ private enum Strings { static let blockUser = NSLocalizedString("reader.postContextMenu.blockUser", value: "Block User", comment: "Context menu action") static let reportPost = NSLocalizedString("reader.postContextMenu.reportPost", value: "Report Post", comment: "Context menu action") static let reportUser = NSLocalizedString("reader.postContextMenu.reportUser", value: "Report User", comment: "Context menu action") + static let markRead = NSLocalizedString("reader.postContextMenu.markRead", value: "Mark as Read", comment: "Context menu action") + static let markUnread = NSLocalizedString("reader.postContextMenu.markUnread", value: "Mark as Unread", comment: "Context menu action") } From 846199d094ac748b895eab2d70c4113e1c33e45e Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 9 Jan 2025 12:23:10 -0500 Subject: [PATCH 179/193] Add missing social sharing icons (#23918) * Add missing social sharing icons * Update release notes * Update release notes --- RELEASE-NOTES.txt | 1 + .../Classes/Models/PublicizeService.swift | 1 + .../Blog/Sharing/PublicizeServiceCell.swift | 90 ++++++++++++++++++ .../Blog/Sharing/SharingViewController.m | 74 +++++--------- .../JetpackSocialNoConnectionView.swift | 2 +- ...04761532104096_2740029438399114184_n-2.png | Bin 0 -> 7689 bytes .../icon-threads.imageset/Contents.json | 12 +++ 7 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift create mode 100644 WordPress/Jetpack/AppImages.xcassets/Social/icon-threads.imageset/361591942_304761532104096_2740029438399114184_n-2.png create mode 100644 WordPress/Jetpack/AppImages.xcassets/Social/icon-threads.imageset/Contents.json diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 74811df9978a..0c972659445f 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -24,6 +24,7 @@ * [*] Fix non-stable order in Posts and Pages section in Stats [#23915] * [*] (P2) Reader: Fix an issue with a missing "Mark as Read/Unread" button that was removed in the previous release [#23917] * [*] (P2) Reader: Show "read" status for P2 posts in the feeds [#23917] +* [*] Fix some missing or invalid social sharing icons [#23918] 25.6 ----- diff --git a/WordPress/Classes/Models/PublicizeService.swift b/WordPress/Classes/Models/PublicizeService.swift index e056aec957f6..d4842e0506a2 100644 --- a/WordPress/Classes/Models/PublicizeService.swift +++ b/WordPress/Classes/Models/PublicizeService.swift @@ -37,6 +37,7 @@ extension PublicizeService { case linkedin case instagram = "instagram-business" case mastodon + case threads case unknown /// Returns the local image for the icon representing the social network. diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift new file mode 100644 index 000000000000..ebeaa18c8c4b --- /dev/null +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift @@ -0,0 +1,90 @@ +import UIKit +import WordPressUI + +final class PublicizeServiceCell: UITableViewCell { + let iconView = AsyncImageView() + let titleLabel = UILabel() + let detailsLabel = UILabel() + + @objc class var cellId: String { "PublicizeServiceCell" } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + detailsLabel.font = .preferredFont(forTextStyle: .footnote) + detailsLabel.textColor = .secondaryLabel + + let stackView = UIStackView(alignment: .center, spacing: 12, [ + iconView, + UIStackView(axis: .vertical, alignment: .leading, spacing: 2, [titleLabel, detailsLabel]) + ]) + contentView.addSubview(stackView) + stackView.pinEdges(to: contentView.layoutMarginsGuide) + + NSLayoutConstraint.activate([ + iconView.widthAnchor.constraint(equalToConstant: 28), + iconView.heightAnchor.constraint(equalToConstant: 28), + ]) + iconView.layer.cornerRadius = 8 + iconView.layer.masksToBounds = true + iconView.backgroundColor = UIColor.white + + iconView.contentMode = .scaleAspectFit + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + + accessoryView = .none + iconView.prepareForReuse() + } + + @objc func configure(with service: PublicizeService, connections: [PublicizeConnection]) { + let name = service.name + if name != .unknown && !name.hasModernRemoteLogo { + iconView.image = name.localIconImage + } else if let imageURL = URL(string: service.icon) { + iconView.setImage(with: imageURL) + } else { + iconView.image = UIImage(named: "social-default") + } + + titleLabel.text = service.label + + detailsLabel.isHidden = connections.isEmpty + if connections.count > 2 { + detailsLabel.text = String(format: Strings.numberOfAccounts, connections.count) + } else { + detailsLabel.text = connections + .map(\.externalDisplay) + .joined(separator: ", ") + } + + if service.isSupported { + if connections.contains(where: { $0.requiresUserAction() }) { + accessoryView = WPStyleGuide.sharingCellWarningAccessoryImageView() + } + } else { + accessoryView = WPStyleGuide.sharingCellErrorAccessoryImageView() + } + } +} + +private extension PublicizeService.ServiceName { + /// We no longer need to provide local overrides for these on this screen + /// as the remote images are good. + var hasModernRemoteLogo: Bool { + [ + PublicizeService.ServiceName.instagram, + PublicizeService.ServiceName.mastodon + ].contains(self) + } +} + +private enum Strings { + static let numberOfAccounts = NSLocalizedString("socialSharing.connectionDetails.nAccount", value: "%d accounts", comment: "The number of connected accounts on a third party sharing service connected to the user's blog. The '%d' is a placeholder for the number of accounts.") +} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m index 077c4846c677..0ddf6bc3eda4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m @@ -66,6 +66,7 @@ - (void)viewDidLoad action:@selector(doneButtonTapped)]; } + [self.tableView registerClass:[PublicizeServiceCell class] forCellReuseIdentifier:PublicizeServiceCell.cellId]; self.tableView.cellLayoutMarginsFollowReadableWidth = YES; [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; [self.publicizeServicesState addInitialConnections:[self allConnections]]; @@ -237,32 +238,16 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - WPTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (!cell) { - cell = [[WPTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; - } - - [WPStyleGuide configureTableViewCell:cell]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; switch (sectionType) { case SharingSectionAvailableServices: // fallthrough case SharingSectionUnsupported: - [self configurePublicizeCell:cell atIndexPath:indexPath]; - break; - + return [self makePublicizeCellAtIndexPath:indexPath]; case SharingSectionSharingButtons: - cell.textLabel.text = NSLocalizedString(@"Manage", @"Verb. Text label. Tapping displays a screen where the user can configure 'share' buttons for third-party services."); - cell.detailTextLabel.text = nil; - cell.imageView.image = nil; - break; - + return [self makeManageButtonCell]; default: return [UITableViewCell new]; } - - return cell; } - (PublicizeService *)publicizeServiceForIndexPath:(NSIndexPath *)indexPath @@ -278,51 +263,38 @@ - (PublicizeService *)publicizeServiceForIndexPath:(NSIndexPath *)indexPath } } -- (void)configurePublicizeCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath +- (UITableViewCell *)makePublicizeCellAtIndexPath:(NSIndexPath *)indexPath { + PublicizeServiceCell *cell = [self.tableView dequeueReusableCellWithIdentifier:PublicizeServiceCell.cellId]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + PublicizeService *publicizer = [self publicizeServiceForIndexPath:indexPath]; NSArray *connections = [self connectionsForService:publicizer]; // TODO: Remove? if ([publicizer.serviceID isEqualToString:PublicizeService.googlePlusServiceID] && [connections count] == 0) { // Temporarily hiding Google+ cell.hidden = YES; - return; - } - - // Configure the image - UIImage *image = [WPStyleGuide socialIconFor:publicizer.serviceID]; - [cell.imageView setImage:image]; - - // Configure the text - cell.textLabel.text = publicizer.label; - - // Show the name(s) or number of connections. - NSString *str = @""; - if ([connections count] > 2) { - NSString *format = NSLocalizedString(@"%d accounts", @"The number of connected accounts on a third party sharing service connected to the user's blog. The '%d' is a placeholder for the number of accounts."); - str = [NSString stringWithFormat:format, [connections count]]; - } else { - NSMutableArray *names = [NSMutableArray array]; - for (PublicizeConnection *pubConn in connections) { - [names addObject:pubConn.externalDisplay]; - } - str = [names componentsJoinedByString:@", "]; + return cell; } - cell.detailTextLabel.text = str; + [cell configureWith:publicizer connections:connections]; + return cell; +} - if (![publicizer isSupported]) { - cell.accessoryView = [WPStyleGuide sharingCellErrorAccessoryImageView]; - return; +- (UITableViewCell *)makeManageButtonCell +{ + WPTableViewCell *cell = [self. + tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (!cell) { + cell = [[WPTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; } - // Check if any of the connections are broken. - for (PublicizeConnection *pubConn in connections) { - if ([pubConn requiresUserAction]) { - cell.accessoryView = [WPStyleGuide sharingCellWarningAccessoryImageView]; - break; - } - } + [WPStyleGuide configureTableViewCell:cell]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + cell.textLabel.text = NSLocalizedString(@"Manage", @"Verb. Text label. Tapping displays a screen where the user can configure 'share' buttons for third-party services."); + cell.detailTextLabel.text = nil; + cell.imageView.image = nil; + return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift index f1d7556e0c5a..a958a0469153 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift @@ -40,7 +40,7 @@ struct JetpackSocialNoConnectionView: View { } func iconImage(image: UIImage, url: URL?) -> some View { - AsyncImage(url: url) { image in + CachedAsyncImage(url: url) { image in image .icon(backgroundColor: viewModel.preferredBackgroundColor) } placeholder: { diff --git a/WordPress/Jetpack/AppImages.xcassets/Social/icon-threads.imageset/361591942_304761532104096_2740029438399114184_n-2.png b/WordPress/Jetpack/AppImages.xcassets/Social/icon-threads.imageset/361591942_304761532104096_2740029438399114184_n-2.png new file mode 100644 index 0000000000000000000000000000000000000000..e75640c24a0248163fd3c590ecfdd594c74c7071 GIT binary patch literal 7689 zcmd6MgdJ3?c;IPkw3;b{Ls$H3A5OaB#<`bhM>xJ>B_j>^z^_^TXV|{z?HLFew!2ZtrUYg1NhS_(;KI z9{md;g`)qm!H+=yg7~`1JTlYK2Pt{Jum_3p3-Aj(lEnjoK#&)94pN57s{e?i?qnW0 z`TBZEfx!U*0sH~N{GKly!Ge;Kl3)QLu#gZR3c=?S=;3PvzA}#<{WbKT>)-A4b$0l#B@dr}+(J1B{`&+h$S(l?k8YGGYqzt( z>tz82*{h*Rh^73_bzdRrc}q*ni~P*}{k3{UoNPwF3icmlH0_y6e38azq98-(*@Yr3 zWx^;d+}Q{x617kx^y07b(HJiBBCkriDuD*Ja0L{MuHRsNHljjs5)?^CLbzGs=^{T% z5>V7SmBd_{*&NLB%y0wvv+VJW|f z{2$HGUWIF5*uD{c^4KGjvR2lKQn&*xm`fsIJMTm1t-`?vnl1pk@4vld;YC|MJm_w0MOIqO0bo1~ch49L=(e&>r@a-~MMpuUh@iiE0 zBOU9R@K&O*!~FV5@aYP#5YvETGw>~*C}uB997Ag7<+3ZG%k08B5#W`PzX-|LYAHYi zYK{<%>XiM^m(}E3$RC2T~txCV&fM?oRq@w7D@r>9NL<{aalo zyLvg0F;lBRc6u?ltaF$9ci(O$!d8`Z)J1_T0$GGCPkQ2*!(f*`-$7MFcQT5oD-|!Q zceR6VF&LhS;}@G_Rfu%@DL0}#nT?beB@mFP(l+r>hQDyEvx z`yc9lWzTV08>~$;*r6SM{dpK%84%zNr>i!XE)0z`< zHXN!fG(ce#L+g;$qb88I5q*V+#h|D3paSWp-x%gBBpBsyuZ9XAGD^NE9pntnP*QFX zG|zT5(sbxkK_F_{kK&mGUr8+}WGG`K1D;h;!Am~6O&^exbp;+S^h*s6!AXD(; zMt>{y2rHkZpK|tndTW>Xb?nqXH;@ z0qvvwDGCe=nR5v2&iJ|BgO~&KH_hlCL>05%paS@IFk>F0627K6qdNMu9#$U?+F&uaoYAguFoSKx zmVi_o;h0POmIGSS`DTxC(HhnllW)oOZWl)?Fp@OJV&=`g6 zsnhdYTM6=C3KGN}#ZLvfZCoCY`}*^gBI)p%krn&j-FfU*9x;yd9(SB4%`oRPt75>^ zJ~oa^jbq;vFEJS>HJm+X+hV*Vivko_3D0QGC_>&ejqLjE1|on8J$8Ff!)|^GS50eS z`aj$t+Ux9^T*@#2tOASQoE&r#XXc2ZT$)ow2&n@}$AP(a=I}eg_U%-yJZjiG(I#3s zfSRt*&iP(xybJEj<)Ij%C-BT&B2Z~-ajlx-Z&ViEyo*$QG}c8Hxsz_Bd-{QyP`%C2 zEqvkzxxdn@7ABH<#sRO$qqg*Y>qsbrnL*A>e4r%3HSNIvexc{!2%tE(g_=D#&fa>p zPF4! z8%PlpA`tH&TZjw|%14u*(BQRS3;X?RJlBOirwS7OfgR(C3pLvP%aB%7BOsk4#Z7@Z zjIGBI5L;~pjONgkvBiQ45jt)aksR_HV zlr6be?CFZqq?P(^mMyH5`zFafzz_y_V58!-La$!jlvh13+m=xC`LWT}celnjGeU)g za?}~kN)uA}I5B1Z7pM{Jw^6E?iG~~)8ZzQ=Yu<9LX>%ys!7eUNn-&{PPvo$_m@@@a zN|``JXHbDh=KLyCq%!zrnju;DoZmzB!$dr9%`O1$rpDl{-11FmRHW44JXLRf|TU?KkZ4vF5z_JIGUm@46n}uD=LOh zz?$LtqI%wwP7hoX%qng=DPN~N@0c#C=@ju6U`K`5uL*n#3z~Fz$#5f~*dm;`+pqw& zKD$@v#B?YSy8@uyMi7I_9_Uq0+mY2i5%7q!U^CCo3!$%5 zJ^dVV^cXbz45W-};t1S~rMB_{l2dOq^9tbRfWh~-XF3YBfOrZP_R%4BCL_@Li%~vO zl3!;Q>86wgej$A%Vke-i)M7+E?o{Dk94gNrRNSzo;6iJ23JA?)1s$;XpugNlkQ%%l z1?NE7YPl`!+*fA5Ht)EY0);l%!^T1w-IhC9k~Wu5mcZ`F%>(ssh@PAI-&Rrq1SDLH zEUYjzd}|yCImmb3f#GuC>SR0O`v(a|Ibz+WZG0C+rZgVmh>^LIb%kNGe6m*26e zPH|ulyqc7MhA4C#RF_jdwXv#v!Eszz(q2A`);a`g>U$q(SM|SV3 zca;(`UYx*BS3R-uqlu_|0jrc1>R)KHoVWZ{DyhefN>)^t4ilK=$A(%NHK9l8Xt~I% zE4t?ykN|r~0g806iRsQiba1W47-sbLZW6I&r;-RTMi<_^9Pa$qRcLvIjFZ?IP~WML z+Xe3+J%|FR0N--!vki|6$5uGLD~k)e)wD^;dbdd`0Awd&d-kQCc^TTJtr+f#!wr1D zm?tmbS~??`@Vsu%H6Wgp@|EzlygtiEizM5Vj_`=|D0_VlOYSFoqvK=}{#cfc zIhsCnld6>?thKX$g212iynEJBD<@U~9)V!mK<+f?Q6K^k`S$gu2yViahP_gCxv&|v2(wwIRxoe8<#W;dviK~B#9mXmO^5_Nfp`EAUPVlL@|!+VAOAu8Ju%|R)%5S6U|BOnQAia$r#P=&=m@% zy_RznUJh~$R#2+z=-&UCgJz%)!(A;YEZcZbX)9BnlP%3{Pu`Nz99hR`v~J+Z#WCvJYpHQUR%@OKse8r4+5SsW3zYV3 zx}!a0)m+{Qha*5dRE7-X7{W&y?>B;Hz{i3oaMnacm=3~(vpHqANiB1wcQN*i-V??# z9uSCgJq;|a@%^+GR3$*7FpPfsHL3l>M~q$n>s{l#KZSTrD^ir&0tJ-xF{Ij(r*1FK zgIx#jvsE#|j800d!d;#`5t(YXzj+vnpjMOIp5ft=IZ(pgiQ7A8+7#5O-Sa#Dinnu3 z?(uoA4A~4-N^TI&@0!t-z7clq+H9A_{fvZfMaT8?{QO07D zSc0-ShH`g{p5)kGT7tP36ImcAM5 zuX{8gW2Y*IPRR0+o}wmxWu-SqZv}h&CJ9s|J3|}&^kQ5p_CZs`S9c3ho3WN7UU=Iw zFeIy_37NB)qL~;c)}bCtbA>Zgg6_v}wTm>ymg*o!jOF*)IB)otc2{ zf$oa}$)7*W{r=ov{Z?3u&$W7r{qCuFyv_9o&0Q`KQpEsvV0=qG(2Vgl7!4|#o3S}% zCl+zN23=0xw6$bgTxV^~Fc{g+b~2D*_W35*Wr{w|hZNVZb|NuVn5YXu1C-T`UH+g_ zT|3oQK)SE>GuIPiiee}Ln*BJfqEmF6WTNf?67mWQSH}r?SS&V`>$c8I4?hGhtzM^CW^~AnwadXv9QON9W|Q(BOdWtlYjs; z6HNt1AzXY-UdNn7^2Tecvf};|Z*qHxI8r1ul#f?h5a%Bjx5=vlVQ@wc0aDc;p$7V* zp)1~lHIdee+RpbowvPHM zILM##CD8bjaYC`kD$gIIIYR=?-yQ-i2-xNmemR{XzUQ{o&an&R`xoMO-yJrkgEP~WRFQ~0xMUH7_C9anUj(nFi$QzH9 zKi{<)R-`FU>{|RGStj6zu_BGJ#s%saD4Qx%RH70ps`_R>UEreiDbdElZKIOw+e{Hj zh%q9v%9o_TIfBGgH!$|PHbHK52q&eUW9UVoRM3RvPjKkk*g*{-s=oc}dJCmj;Tb-I z5S9V^R>$&V>)qm!L9YIx2>6a762@VDQga9@h>B1W?x`5O%p^EuaMVKYTzb1n3B8h{ zHaDQ8h7u@_6t4LltWe79n|(dR`0YahMUQ;JMuVFMKABV^_T!{#{!~!$13TsPPmY=%4tBn79{ekD!$DiV*mkXzHe&n&+a6&RUwNY{@nPp&9;$2 zu~!%aCV3u(^vkl>^oM;In}JU1<|22-^fklW&BJAI#RuiIqKeIBI)Zha819P@f%m_0 zVnTdR9y8NexD3m^a2DNXA?g^St@xI`2*^8ymh^)U;s{!pGqf~ox7>&CsZTnCBCI#&XtxiPd$U`Gq!5qU>ry=pM6Tvdju2sc{9TOA@GT^Ag_knOazIv@8_~0 z2L--)nJ4;F5E1-IF-#2Mh#LjJ+Pb692QS}eT8x$G=jjz|COUQG^rn3NTVU>%s-P`YLNZMXSbwVjRd z-m@Bx!NHnWJJtJlPOQXpSoR@h$CatW*u)hL*Ea|PKMmF@7fmp;c;~xd2KqPKakJ?f zKP6G$g?ZFsR$4QrKRye`gLx~S4SP`)(Xm%p{IozTWrn|L!zgZeP&T)M$-N~uxg`;{ z^sz>t6dfsrMlYG`U^XoS(Y+-~t;IxGDKZ8)el<`&G3$v0kq6LDSi;s&TMLseT1!uJ zKqS|1?$s+=>o(3`r6AcasVSmatTXyJCh|TBk4q?3i>Isnv>uec)^E!Q6M)eZ=B%P) zeAA?PVN$ZCmSUoxnjvswOGYV_uQb{CkT)zXbDJi{Y^p;Wev%r{Tta{*$$xX!^(6be z>2t`srF_K+oq_1p?-ezW%l;T?R!#qi=vX8*ThwQ9hB8B*f!8;B-ajxj_ z)0)-5KfGDk^=_!`8%*PMcy)9(Ugq4^BCUFy*G&DLH5%)CoyPYp9Kxk{C0-*XD3#sv z%o_3))B3{LHTwXx-;o>*hfpv4*j&84kLZi<3^994=%Lt&MnPhejs&_~M)_*H=+>g% zhF#t8vM9vaFbNk{&L?28DWUxC=uAa)uQM?&?o&lwg&t9SreNc^{3Y-q%rifICuuf2(Hg00g9;u*PG#h!QxCsxPSYgC6M$gvG`b_fT3UM6o8{Y%`h1Qj0!;{>u#KE^ySvfqv4>ThdU6< zZR^SMl*6hQ+oU4o1;{c#m{4t(9eI;AbZH*VfYXS$M==ELmI_M$`ME zpxR4rIK>7SkkY_}awKhcBnK9Mgqi9Wo{4sc&tkxIETg2{f?o-EtZGTfW}+5JCuFe@ ziSl{Nt1KbNn~R5wp|Kv=&D<4tm&I6A%dprZx}h}WaeBBTKTfs)ru=n5mA7r?jF{GEPp5P_c_ZVw#q zq$kwBXOua?a(Ik1-LIZu@w5P}StcBE-Kk=6^=9n?!-Ds^QcDQPt)KSXQgzJ|*Scc< zHkuXi-JR^t(Pi*3<9L?@p{V`hScMz%nu(R1!D*4vG?EmTs5Q~4`ej94a_O}jVx=)6 z<%eW0<&;bzjb_74Cz|wZ?s{afIy#fEfTX+`c%(!OR$+e_g-XxY>u>+@rKCPDq6Nj6Fn4sW1kwb9|BU35;<3p?;6QjsH_k}}5831q! zAyjFax#|p)QAI0W$e7l7_#=83kd`2IJo5LL$==w2P}N1=Scy1eqxgT Date: Thu, 9 Jan 2025 12:25:36 -0500 Subject: [PATCH 180/193] Update release notes --- RELEASE-NOTES.txt | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0c972659445f..6739064a8200 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,7 +1,18 @@ 25.7 ----- +* [**] Add Image Playground support (part of Apple Intelligence suite) for adding images to your posts, generated featured image, site icons, and more [#23688] * [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] +* [**] Enhance the Gravatar Quick Editor by adding features that allow users to delete and share their avatars. [#23868] +* [**] Add the capability to create an avatar using Apple Image Playground through the Gravatar Quick Editor. [#23868] +* [**] Various bug fixes and improvements in the new experimental editor [#23919] +* [*] Fix sorting in Stats Subscriber Emails (latest first) [#23913] +* [*] Reader: Fix a couple of rare crashes in Reader [#23907] * [*] Add prefetching to Reader streams [#23928] +* [*] Reader: The post cover now uses the standard aspect ratio for covers, so there is no jumping. There are also a few minor improvements to the layout and animations of the cover and the header [#23897, #23909] +* [*] Reader: Move the "Reading Preferences" button to the "More" menu [#23897] +* [*] Reader: Fix an issue with empty state views being non-scrollable in streams [#23908] +* [*] Reader: Hide post toolbar when reading an article and fix like button animations [#23909] +* [*] Reader: Fix off-by-one error in post details like counter when post is liked by you [#23912] * [*] 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] @@ -28,25 +39,15 @@ 25.6 ----- -* [**] Add Image Playground support (part of Apple Intelligence suite) for adding images to your posts, generated featured image, site icons, and more [#23688] -* [**] Enhance the Gravatar Quick Editor by adding features that allow users to delete and share their avatars. [#23868] -* [**] Add the capability to create an avatar using Apple Image Playground through the Gravatar Quick Editor. [#23868] -* [*] Various bug fixes and improvements in the new experimental editor [#23919] * [*] [internal] Update Gravatar SDK to 3.0.0 [#23701] +* [*] Use the Gravatar Quick Editor to update the avatar [#23729] * [*] (Hidden under a feature flag) User Management for self-hosted sites. [#23768] -* [*] Fix sorting in Stats Subscriber Emails (latest first) [#23913] * [*] Add URL and ID to the Media details screen, add IDs for posts [#23887] -* [*] Reader: Enable quick access to notifications on iPad [#23882] +* [*] Enable quick access to notifications from Reader on iPad [#23882] * [*] Add support for restricted posts in Reader [#23853] * [*] Fix minor appearance issues in the Blaze campaign list [#23891] * [*] Improve the sidebar animations and layout on some iPad models [#23886] -* [*] Reader: Fix a couple of rare crashes in Reader [#23907] -* [*] Reader: Fix an issue with posts shown embedded in the notifications popover on iPad [#23889] -* [*] Reader: The post cover now uses the standard aspect ratio for covers, so there is no jumping. There are also a few minor improvements to the layout and animations of the cover and the header [#23897, #23909] -* [*] Reader: Move the "Reading Preferences" button to the "More" menu [#23897] -* [*] Reader: Fix an issue with empty state views being non-scrollable in streams [#23908] -* [*] Reader: Hide post toolbar when reading an article and fix like button animations [#23909] -* [*] Reader: Fix off-by-one error in post details like counter when post is liked by you [#23912] +* [*] Fix an issue with posts shown embedded in the notifications popover on iPad [#23889] 25.5 ----- From a07bfb3bb3ca8c0e963d4e31e65f9f71d56e78f5 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 12:31:12 -0500 Subject: [PATCH 181/193] Fix build --- .../Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift | 1 + .../Jetpack/Social/JetpackSocialNoConnectionView.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift index ebeaa18c8c4b..f4e91cd1b705 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift @@ -1,5 +1,6 @@ import UIKit import WordPressUI +import AsyncImageKit final class PublicizeServiceCell: UITableViewCell { let iconView = AsyncImageView() diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift index a958a0469153..81db3538dde0 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AsyncImageKit struct JetpackSocialNoConnectionView: View { From 4cd2be47799ee730efbb32b368b9ba52fef567c7 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 13:18:10 -0500 Subject: [PATCH 182/193] Update UI tests --- .../Screens/Editor/EditorPostSettings.swift | 31 +++++-------------- .../Screens/Editor/FeaturedImageScreen.swift | 29 ----------------- .../BloggingRemindersFlow.swift | 3 ++ .../PostSettingsViewController+Swift.swift | 1 + .../Views/PostSettingsFeaturedImageCell.swift | 6 ++-- 5 files changed, 16 insertions(+), 54 deletions(-) delete mode 100644 Modules/Sources/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift diff --git a/Modules/Sources/UITestsFoundation/Screens/Editor/EditorPostSettings.swift b/Modules/Sources/UITestsFoundation/Screens/Editor/EditorPostSettings.swift index e96795157d64..161a58d3bb12 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Editor/EditorPostSettings.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Editor/EditorPostSettings.swift @@ -11,22 +11,10 @@ public class EditorPostSettings: ScreenObject { $0.cells["Categories"] } - private let chooseFromMediaButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.buttons["Choose from Media"] - } - private let tagsSectionGetter: (XCUIApplication) -> XCUIElement = { $0.cells["Tags"] } - private let featuredImageButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.cells["SetFeaturedImage"] - } - - private let currentFeaturedImageGetter: (XCUIApplication) -> XCUIElement = { - $0.cells["CurrentFeaturedImage"] - } - private let publishDateButtonGetter: (XCUIApplication) -> XCUIElement = { $0.staticTexts["Publish Date"] } @@ -56,11 +44,11 @@ public class EditorPostSettings: ScreenObject { } var categoriesSection: XCUIElement { categoriesSectionGetter(app) } - var chooseFromMediaButton: XCUIElement { chooseFromMediaButtonGetter(app) } - var currentFeaturedImage: XCUIElement { currentFeaturedImageGetter(app) } + var chooseFromMediaButton: XCUIElement { app.buttons["Choose from Media"].firstMatch } var closeButton: XCUIElement { closeButtonGetter(app) } var backButton: XCUIElement? { backButtonGetter(app) } - var featuredImageButton: XCUIElement { featuredImageButtonGetter(app) } + var featuredImageCell: XCUIElement { app.cells["post_settings_featured_image_cell"].firstMatch } + var selectedFeaturedImage: XCUIElement { app.otherElements["featured_image_current_image"].firstMatch } var firstCalendarDayButton: XCUIElement { firstCalendarDayButtonGetter(app) } var monthLabel: XCUIElement { monthLabelGetter(app) } var nextMonthButton: XCUIElement { nextMonthButtonGetter(app) } @@ -100,16 +88,13 @@ public class EditorPostSettings: ScreenObject { } public func removeFeatureImage() throws -> EditorPostSettings { - currentFeaturedImage.tap() - - try FeaturedImageScreen() - .tapRemoveFeaturedImageButton() - + featuredImageCell.tap() + app.buttons["featured_image_button_remove"].firstMatch.tap() return try EditorPostSettings() } public func setFeaturedImage() throws -> EditorPostSettings { - featuredImageButton.tap() + featuredImageCell.tap() chooseFromMediaButton.tap() try MediaPickerAlbumScreen() .selectImage(atIndex: 0) // Select latest uploaded image @@ -125,9 +110,9 @@ public class EditorPostSettings: ScreenObject { XCTAssertTrue(tagsSection.staticTexts[postTag].exists, "Tag \(postTag) not set") } if hasImage { - XCTAssertTrue(currentFeaturedImage.exists, "Featured image not set") + XCTAssertTrue(selectedFeaturedImage.exists, "Featured image not set") } else { - XCTAssertFalse(currentFeaturedImage.exists, "Featured image is set but should not be") + XCTAssertFalse(selectedFeaturedImage.exists, "Featured image is set but should not be") } return try EditorPostSettings() diff --git a/Modules/Sources/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift deleted file mode 100644 index ded79b3ef42e..000000000000 --- a/Modules/Sources/UITestsFoundation/Screens/Editor/FeaturedImageScreen.swift +++ /dev/null @@ -1,29 +0,0 @@ -import ScreenObject -import XCTest - -public class FeaturedImageScreen: ScreenObject { - - private let removeFeaturedImageButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.navigationBars.buttons["Remove Featured Image"] - } - - private let removeButtonGetter: (XCUIApplication) -> XCUIElement = { - $0.buttons["Remove"] - } - - var removeButton: XCUIElement { removeButtonGetter(app) } - var removeFeaturedImageButton: XCUIElement { removeFeaturedImageButtonGetter(app) } - - init(app: XCUIApplication = XCUIApplication()) throws { - try super.init( - expectedElementGetters: [ removeFeaturedImageButtonGetter ], - app: app - ) - } - - public func tapRemoveFeaturedImageButton() { - removeFeaturedImageButton.tap() - removeButton.tap() - } - -} diff --git a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift index 361b0d81fb55..e466effcb2b8 100644 --- a/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift +++ b/WordPress/Classes/ViewRelated/Blog/BloggingReminders/BloggingRemindersFlow.swift @@ -11,6 +11,9 @@ final class BloggingRemindersFlow { delegate: BloggingRemindersFlowDelegate? = nil, onDismiss: (() -> Void)? = nil ) { + guard !UITestConfigurator.isEnabled(.disablePrompts) else { + return + } guard blog.areBloggingRemindersAllowed() else { return } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index ebffcc334b5e..af73d4cf7884 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -266,6 +266,7 @@ extension PostSettingsViewController { } cell.contentConfiguration = configuration cell.selectionStyle = .none + cell.accessibilityIdentifier = "post_settings_featured_image_cell" } private func showFeaturedImageSelector(cell: UITableViewCell) { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift index bcb6896984d6..fb905a64c741 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostSettingsFeaturedImageCell.swift @@ -12,6 +12,7 @@ struct PostSettingsFeaturedImageCell: View { if let image = post.featuredImage { SiteMediaImage(media: image, size: .large) .loadingStyle(.spinner) + .accessibilityIdentifier("featured_image_current_image") .aspectRatio(1.0 / ReaderPostCell.coverAspectRatio, contentMode: .fit) .overlay { menu @@ -19,7 +20,6 @@ struct PostSettingsFeaturedImageCell: View { .contextMenu { actions } - } else { if viewModel.upload != nil { // The upload state when no image is selected. For the "Replace" @@ -30,7 +30,6 @@ struct PostSettingsFeaturedImageCell: View { Label(Strings.buttonSetFeaturedImage, systemImage: "photo.badge.plus") .frame(maxWidth: .infinity) .contentShape(Rectangle()) // Make the whole cell tappable - .accessibilityIdentifier("SetFeaturedImage") } } } @@ -63,10 +62,13 @@ struct PostSettingsFeaturedImageCell: View { private var actions: some View { if viewModel.upload == nil { Button(SharedStrings.Button.view, systemImage: "plus.magnifyingglass", action: onViewTapped) + .accessibilityIdentifier("featured_image_button_view") makeMediaPicker { Button(Strings.replaceImage, systemImage: "photo.badge.plus", action: onViewTapped) + .accessibilityIdentifier("featured_image_button_replace") } Button(SharedStrings.Button.remove, systemImage: "trash", role: .destructive, action: viewModel.buttonRemoveTapped) + .accessibilityIdentifier("featured_image_button_remove") } else { Button(role: .destructive, action: viewModel.buttonCancelTapped) { Label(Strings.cancelUpload, systemImage: "trash") From d33d5402ffea44ec7c70d26cf8b8f6943c710034 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 14:03:31 -0500 Subject: [PATCH 183/193] Use medium font for main navigation area in Reader to align with Home --- .../Reader/Sidebar/ReaderSidebarViewController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift index a99b77c29eed..03d4f59a971c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift @@ -158,7 +158,13 @@ private struct ReaderSidebarView: View { private func makePrimaryNavigationItem(_ title: String, systemImage: String) -> some View { HStack { - Label(title, systemImage: systemImage) + Label { + Text(title) + .font(.headline).fontWeight(.medium) + } icon: { + Image(systemName: systemImage) + } +// Label(title, systemImage: systemImage) .lineLimit(1) if viewModel.isCompact { Spacer() From 39fa791c80640df7f6f021b54209197287e78a4e Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 14:11:37 -0500 Subject: [PATCH 184/193] Remove commented-out code --- .../Reader/Sidebar/ReaderSidebarViewController.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift index 03d4f59a971c..482d07fdad15 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarViewController.swift @@ -164,8 +164,7 @@ private struct ReaderSidebarView: View { } icon: { Image(systemName: systemImage) } -// Label(title, systemImage: systemImage) - .lineLimit(1) + .lineLimit(1) if viewModel.isCompact { Spacer() Image(systemName: "chevron.forward") From 3a4c67149fb656cbd98b32def0793c2a9b9854f7 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Thu, 9 Jan 2025 15:54:41 -0500 Subject: [PATCH 185/193] Add context menus and previews for sites in Reader (#23964) * Fix l10n typo * Add Unsubscribe context menu to Reader sidebar sites * Extract ReaderSiteFavoriteButton * Move actions to ReaderSidebarSubscriptionCell * Extract ReaderSubscriptionContextMenu and add Share * Add Notification Settings and Copy Link buttons * Add context menu for sites in Subscriptions view * Add previews * Fix notification settings sometimes being clipped on iPad * Fix layout in ReaderSubscriptionCel actions --- WordPress/Classes/Utility/SharedStrings.swift | 5 + .../ReaderPostActions/ReaderPostMenu.swift | 6 +- .../ReaderSidebarSubscriptionsSection.swift | 93 ++++++++++++++++--- .../ReaderSubscriptionCell.swift | 40 ++++---- ...SubscriptionNotificationSettingsView.swift | 20 ++-- 5 files changed, 110 insertions(+), 54 deletions(-) diff --git a/WordPress/Classes/Utility/SharedStrings.swift b/WordPress/Classes/Utility/SharedStrings.swift index 628c684947b1..32c4c32adcc8 100644 --- a/WordPress/Classes/Utility/SharedStrings.swift +++ b/WordPress/Classes/Utility/SharedStrings.swift @@ -29,6 +29,11 @@ enum SharedStrings { /// - warning: This is the legacy value. It's not compliant with the new format but has the correct translation for different languages. static let title = NSLocalizedString("Reader", comment: "The accessibility value of the Reader tab.") static let unfollow = NSLocalizedString("reader.button.unfollow", value: "Unfollow", comment: "Reader sidebar button title") + static let subscribe = NSLocalizedString("reader.button.subscribe", value: "Subscribe", comment: "A shared button title for Reader") + static let unsubscribe = NSLocalizedString("reader.button.unsubscribe", value: "Unsubscribe", comment: "A shared button title for Reader") + static let addToFavorites = NSLocalizedString("reader.button.addToFavorites", value: "Add to Favorites", comment: "A shared button title for Reader") + static let notificationSettings = NSLocalizedString("reader.button.notificationSettings", value: "Notification Settings", comment: "A shared button title for Reader") + static let removeFromFavorites = NSLocalizedString("reader.button.removeFromFavorites", value: "Remove from Favorites", comment: "A shared button title for Reader") static let recent = NSLocalizedString("reader.recent.title", value: "Recent", comment: "Used in multiple contexts, usually as a screen title") static let discover = NSLocalizedString("reader.discover.title", value: "Discover", comment: "Used in multiple contexts, usually as a screen title") static let saved = NSLocalizedString("reader.saved.title", value: "Saved", comment: "Used in multiple contexts, usually as a screen title") diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift index 42fa44f212a2..6bd3e5d084fe 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderPostActions/ReaderPostMenu.swift @@ -87,7 +87,7 @@ struct ReaderPostMenu { } private var subscribe: UIAction { - UIAction(Strings.subscribe, systemImage: "plus.circle") { + UIAction(SharedStrings.Reader.subscribe, systemImage: "plus.circle") { ReaderSubscriptionHelper().toggleSiteSubscription(forPost: post) track(.subscribe) } @@ -102,7 +102,7 @@ struct ReaderPostMenu { } private var ubsubscribe: UIAction { - UIAction(Strings.unsubscribe, systemImage: "minus.circle", attributes: [.destructive]) { + UIAction(SharedStrings.Reader.unsubscribe, systemImage: "minus.circle", attributes: [.destructive]) { ReaderSubscriptionHelper().toggleSiteSubscription(forPost: post) track(.unsubscribe) } @@ -214,8 +214,6 @@ private enum Strings { static let viewInBrowser = NSLocalizedString("reader.postContextMenu.viewInBrowser", value: "View in Browser", comment: "Context menu action") static let blockOrReport = NSLocalizedString("reader.postContextMenu.blockOrReportMenu", value: "Block or Report", comment: "Context menu action") static let goToBlog = NSLocalizedString("reader.postContextMenu.showBlog", value: "Go to Blog", comment: "Context menu action") - static let subscribe = NSLocalizedString("reader.postContextMenu.subscribeT", value: "Subscribe", comment: "Context menu action") - static let unsubscribe = NSLocalizedString("reader.postContextMenu.unsubscribe", value: "Unsubscribe", comment: "Context menu action") static let manageNotifications = NSLocalizedString("reader.postContextMenu.manageNotifications", value: "Manage Notifications", comment: "Context menu action") static let blogDetails = NSLocalizedString("reader.postContextMenu.blogDetails", value: "Blog Details", comment: "Context menu action (placeholder value when blog name not available – should never happen)") static let blockSite = NSLocalizedString("reader.postContextMenu.blockSite", value: "Block Site", comment: "Context menu action") diff --git a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarSubscriptionsSection.swift b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarSubscriptionsSection.swift index 40a6100cd2ac..b435a270b9da 100644 --- a/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarSubscriptionsSection.swift +++ b/WordPress/Classes/ViewRelated/Reader/Sidebar/ReaderSidebarSubscriptionsSection.swift @@ -30,6 +30,7 @@ struct ReaderSidebarSubscriptionsSection: View { struct ReaderSidebarSubscriptionCell: View { @ObservedObject var site: ReaderSiteTopic @Environment(\.editMode) var editMode + @State private var isShowingSettings = false var body: some View { HStack { @@ -40,28 +41,92 @@ struct ReaderSidebarSubscriptionCell: View { } if editMode?.wrappedValue.isEditing == true { Spacer() - Button { - if !site.showInMenu { - WPAnalytics.track(.readerAddSiteToFavoritesTapped) - } - - let siteObjectID = TaggedManagedObjectID(site) - ContextManager.shared.performAndSave({ managedObjectContext in - let site = try managedObjectContext.existingObject(with: siteObjectID) - site.showInMenu.toggle() - }, completion: nil, on: DispatchQueue.main) - } label: { - Image(systemName: site.showInMenu ? "star.fill" : "star") - .foregroundStyle(site.showInMenu ? .pink : .secondary) - }.buttonStyle(.plain) + ReaderSiteToggleFavoriteButton(site: site, source: "edit_mode") + .labelStyle(.iconOnly) } } .lineLimit(1) .tag(ReaderSidebarItem.subscription(TaggedManagedObjectID(site))) + .swipeActions(edge: .leading) { + if let siteURL = URL(string: site.siteURL) { + ShareLink(item: siteURL).tint(.blue) + } + } .swipeActions(edge: .trailing) { Button(SharedStrings.Reader.unfollow, role: .destructive) { ReaderSubscriptionHelper().unfollow(site) }.tint(.red) } + .contextMenu(menuItems: { + ReaderSubscriptionContextMenu(site: site, isShowingSettings: $isShowingSettings) + }, preview: { + ReaderTopicPreviewView(topic: site) + }) + .sheet(isPresented: $isShowingSettings) { + ReaderSubscriptionNotificationSettingsView(siteID: site.siteID.intValue) + .presentationDetents([.medium, .large]) + .edgesIgnoringSafeArea(.bottom) + } + } +} + +struct ReaderSubscriptionContextMenu: View { + let site: ReaderSiteTopic + + @Binding var isShowingSettings: Bool + + var body: some View { + if let siteURL = URL(string: site.siteURL) { + ShareLink(item: siteURL) + Button(SharedStrings.Button.copyLink, systemImage: "doc.on.doc") { + UIPasteboard.general.string = siteURL.absoluteString + } + } + if site.following { + ReaderSiteToggleFavoriteButton(site: site, source: "context_menu") + Button(SharedStrings.Reader.notificationSettings, systemImage: "bell") { + isShowingSettings = true + } + Button(SharedStrings.Reader.unsubscribe, systemImage: "minus.circle", role: .destructive) { + ReaderSubscriptionHelper().unfollow(site) + } + } else { + Button(SharedStrings.Reader.subscribe, systemImage: "plus.circle") { + ReaderSubscriptionHelper().toggleFollowingForSite(site) + } + } + } +} + +struct ReaderTopicPreviewView: UIViewControllerRepresentable { + let topic: ReaderAbstractTopic + + func makeUIViewController(context: Context) -> ReaderStreamViewController { + ReaderStreamViewController.controllerWithTopic(topic) + } + + func updateUIViewController(_ vc: ReaderStreamViewController, context: Context) { + // Do nothing + } +} + +struct ReaderSiteToggleFavoriteButton: View { + let site: ReaderSiteTopic + let source: String + + var body: some View { + Button { + if !site.showInMenu { + WPAnalytics.track(.readerAddSiteToFavoritesTapped, properties: ["via": source]) + } + let siteObjectID = TaggedManagedObjectID(site) + ContextManager.shared.performAndSave({ managedObjectContext in + let site = try managedObjectContext.existingObject(with: siteObjectID) + site.showInMenu.toggle() + }, completion: nil, on: DispatchQueue.main) + } label: { + Label(site.showInMenu ? SharedStrings.Reader.removeFromFavorites : SharedStrings.Reader.addToFavorites, systemImage: site.showInMenu ? "star.fill" : "star") + .foregroundStyle(site.showInMenu ? .pink : .secondary) + }.buttonStyle(.plain) } } diff --git a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift index 49575ab2542f..c976a7d1298e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionCell.swift @@ -38,11 +38,19 @@ struct ReaderSubscriptionCell: View { Spacer() - if let status = ReaderSubscriptionNotificationsStatus(site: site) { - makeButtonNotificationSettings(with: status) + HStack(spacing: 0) { + if let status = ReaderSubscriptionNotificationsStatus(site: site) { + makeButtonNotificationSettings(with: status) + } + buttonMore } - buttonMore + .padding(.trailing, -16) } + .contextMenu(menuItems: { + ReaderSubscriptionContextMenu(site: site, isShowingSettings: $isShowingSettings) + }, preview: { + ReaderTopicPreviewView(topic: site) + }) } private func makeButtonNotificationSettings(with status: ReaderSubscriptionNotificationsStatus) -> some View { @@ -65,36 +73,24 @@ struct ReaderSubscriptionCell: View { } .font(.subheadline) .frame(width: 34, alignment: .center) - .padding(.trailing, 6) + .contentShape(Rectangle()) } .buttonStyle(.plain) - .popover(isPresented: $isShowingSettings) { settings } - } - - @ViewBuilder - private var settings: some View { - if horizontalSizeClass == .compact { - ReaderSubscriptionNotificationSettingsView(siteID: site.siteID.intValue, isCompact: true) - .presentationDetents([.medium, .large]) - .edgesIgnoringSafeArea(.all) - } else { + .sheet(isPresented: $isShowingSettings) { ReaderSubscriptionNotificationSettingsView(siteID: site.siteID.intValue) + .presentationDetents([.medium, .large]) + .edgesIgnoringSafeArea(.bottom) } } private var buttonMore: some View { Menu { - if let siteURL = URL(string: site.siteURL) { - ShareLink(item: siteURL) - } - Button(role: .destructive) { - onDelete(site) - } label: { - Label(SharedStrings.Reader.unfollow, systemImage: "trash") - } + ReaderSubscriptionContextMenu(site: site, isShowingSettings: $isShowingSettings) } label: { Image(systemName: "ellipsis") .foregroundStyle(.secondary) + .frame(width: 40, height: 40) + .contentShape(Rectangle()) } .buttonStyle(.plain) } diff --git a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionNotificationSettingsView.swift b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionNotificationSettingsView.swift index 9242977fe0e1..350679a1d34f 100644 --- a/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionNotificationSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Subscriptions/ReaderSubscriptionNotificationSettingsView.swift @@ -3,30 +3,22 @@ import UIKit struct ReaderSubscriptionNotificationSettingsView: UIViewControllerRepresentable { let siteID: Int - var isCompact = false @Environment(\.dismiss) var dismiss func makeUIViewController(context: Context) -> UIViewController { let vc = NotificationSiteSubscriptionViewController(siteId: siteID) - if isCompact { - vc.navigationItem.rightBarButtonItem = UIBarButtonItem(title: SharedStrings.Button.done, primaryAction: .init { _ in - dismiss() - }) - // - warning: UIKit is used to prevent the modifiers from the - // containing list to affect this screen/ - return UINavigationController(rootViewController: vc) - } - return vc + vc.navigationItem.rightBarButtonItem = UIBarButtonItem(title: SharedStrings.Button.done, primaryAction: .init { _ in + dismiss() + }) + // - warning: UIKit is used to prevent the modifiers from the + // containing list to affect this screen/ + return UINavigationController(rootViewController: vc) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { // Do nothing } - - func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIViewController, context: Context) -> CGSize? { - isCompact ? nil : CGSize(width: 320, height: 434) - } } extension NotificationSiteSubscriptionViewController { From 0ea86d84c78cce818c2072a16194b51ba4b63f8f Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 9 Jan 2025 15:55:35 -0500 Subject: [PATCH 186/193] Update release notes --- RELEASE-NOTES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 6739064a8200..7ca6344db2a7 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -36,6 +36,9 @@ * [*] (P2) Reader: Fix an issue with a missing "Mark as Read/Unread" button that was removed in the previous release [#23917] * [*] (P2) Reader: Show "read" status for P2 posts in the feeds [#23917] * [*] Fix some missing or invalid social sharing icons [#23918] +* [*] Fix small tap area for “More” button in the Subscriptions screen in Reader [#23964] +* [*] Fix “Notification Settings” for individual posts sometimes being clipped [#23964] +* [*] Add context menu (long-press) and previews for subscriptions with quick access to “Share”, “Copy Link”, “Add to Favorites”, “Notification Settings”, and “Unsubscribe” buttons in both the Subscriptions view and the Sidebar in Reader [#23964] 25.6 ----- From f26b3e31e5f68794f95ba30f236c4785783bfe3d Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 10 Jan 2025 08:02:39 -0500 Subject: [PATCH 187/193] Fix more button color in dark mode --- WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 3bca261a58ea..1c616f3cbb70 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -146,7 +146,7 @@ private final class ReaderPostCellView: UIView { imageView.layer.masksToBounds = true imageView.contentMode = .scaleAspectFill - buttonMore.configuration?.baseForegroundColor = UIColor.opaqueSeparator + buttonMore.configuration?.baseForegroundColor = UIColor.secondaryLabel.withAlphaComponent(0.5) buttonMore.configuration?.contentInsets = .init(top: 12, leading: 8, bottom: 12, trailing: 20) } From daaa5ad01cd609dd9f325c3bcbc0c3ab0dcb9a38 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Fri, 10 Jan 2025 08:23:52 -0500 Subject: [PATCH 188/193] Fix an issue with fullscreen button in reply view clipped by the notch (#23965) * Fix an issue with fullscreen button in reply view clipped by the notch * Update release notes --- RELEASE-NOTES.txt | 1 + .../ReplyTextView/ReplyTextView.xib | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 7ca6344db2a7..718ed2c51cec 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -39,6 +39,7 @@ * [*] Fix small tap area for “More” button in the Subscriptions screen in Reader [#23964] * [*] Fix “Notification Settings” for individual posts sometimes being clipped [#23964] * [*] Add context menu (long-press) and previews for subscriptions with quick access to “Share”, “Copy Link”, “Add to Favorites”, “Notification Settings”, and “Unsubscribe” buttons in both the Subscriptions view and the Sidebar in Reader [#23964] +* [*] Fix an issue with fullscreen button in reply view clipped by the notch [#23965] 25.6 ----- diff --git a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib index 873787813b75..b64ae72ebdee 100644 --- a/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib +++ b/WordPress/Classes/ViewRelated/Notifications/ReplyTextView/ReplyTextView.xib @@ -23,7 +23,7 @@ - + @@ -35,7 +35,7 @@ - + @@ -63,16 +63,16 @@ - + - + - +