From 280d66fcf2c9e761b7780f003028cb0684fde77c Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 11:33:53 +0300 Subject: [PATCH 01/24] Add remote FF gravatar_quick_editor --- .../Utility/BuildInformation/RemoteFeatureFlag.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index fbb2123628d2..d45dbf1a1e4e 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -32,6 +32,7 @@ enum RemoteFeatureFlag: Int, CaseIterable { case inAppUpdates case readerTagsFeed case readerFloatingButton + case gravatarQuickEditor var defaultValue: Bool { switch self { @@ -95,6 +96,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return true case .readerFloatingButton: return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest] + case .gravatarQuickEditor: + return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest, .a8cPrereleaseTesting] } } @@ -161,6 +164,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "reader_tags_feed" case .readerFloatingButton: return "reader_floating_button" + case .gravatarQuickEditor: + return "gravatar_quick_editor" } } @@ -226,6 +231,8 @@ enum RemoteFeatureFlag: Int, CaseIterable { return "Reader Tags Feed" case .readerFloatingButton: return "Reader Floating Button" + case .gravatarQuickEditor: + return "Gravatar Quick Editor" } } From 2c5b3ae926c8cbea17e9d1951f0f9a4a8b5b2071 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 14:31:14 +0300 Subject: [PATCH 02/24] Add forceRefresh option for image download methods --- .../Classes/Extensions/URL+Helpers.swift | 8 ++++++ .../Media/ImageDownloader+Gravatar.swift | 11 +++++--- .../Gravatar/UIImageView+Gravatar.swift | 25 +++++++++++++------ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/Extensions/URL+Helpers.swift b/WordPress/Classes/Extensions/URL+Helpers.swift index 22aff7d8bd81..ff54e10c10c3 100644 --- a/WordPress/Classes/Extensions/URL+Helpers.swift +++ b/WordPress/Classes/Extensions/URL+Helpers.swift @@ -151,4 +151,12 @@ extension URL { components?.queryItems = queryItems return components?.url ?? self } + + /// Gravatar doesn't support "Cache-Control: none" header. So we add a random query parameter to + /// bypass the backend cache and get the latest image. + func appendingGravatarCacheBusterParam() -> URL { + var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) + urlComponents?.queryItems?.append(.init(name: "_", value: "\(NSDate().timeIntervalSince1970)")) + return urlComponents?.url ?? self + } } diff --git a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift index a4eb3ef71596..e90a8b8fc8df 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader+Gravatar.swift @@ -4,19 +4,22 @@ import Gravatar extension ImageDownloader { - nonisolated func downloadGravatarImage(with email: String, completion: @escaping (UIImage?) -> Void) { + nonisolated func downloadGravatarImage(with email: String, forceRefresh: Bool = false, completion: @escaping (UIImage?) -> Void) { guard let url = AvatarURL.url(for: email) else { completion(nil) return } - if let cachedImage = ImageCache.shared.getImage(forKey: url.absoluteString) { + if !forceRefresh, let cachedImage = ImageCache.shared.getImage(forKey: url.absoluteString) { completion(cachedImage) return } - - downloadImage(at: url) { image, _ in + var urlToDownload = url + if forceRefresh { + urlToDownload = url.appendingGravatarCacheBusterParam() + } + downloadImage(at: urlToDownload) { image, _ in DispatchQueue.main.async { guard let image else { diff --git a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift index 47c9cad67b5a..5ff31c9fc8bc 100644 --- a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift +++ b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift @@ -34,16 +34,18 @@ extension UIImageView { /// - email: The user's email /// - gravatarRating: Expected image rating /// - placeholderImage: Image to be used as Placeholder + /// - forceRefresh: Skip the cache and fetch the latest version of the avatar. public func downloadGravatar( for email: String, gravatarRating: Rating = .general, - placeholderImage: UIImage = .gravatarPlaceholderImage + placeholderImage: UIImage = .gravatarPlaceholderImage, + forceRefresh: Bool = false ) { let avatarURL = AvatarURL.url(for: email, preferredSize: .pixels(gravatarDefaultSize()), gravatarRating: gravatarRating) - downloadGravatar(fullURL: avatarURL, placeholder: placeholderImage, animate: false) + downloadGravatar(fullURL: avatarURL, placeholder: placeholderImage, animate: false, forceRefresh: forceRefresh) } - public func downloadGravatar(_ gravatar: AvatarURL?, placeholder: UIImage, animate: Bool) { + public func downloadGravatar(_ gravatar: AvatarURL?, placeholder: UIImage, animate: Bool, forceRefresh: Bool = false) { guard let gravatar = gravatar else { self.image = placeholder return @@ -56,14 +58,23 @@ extension UIImageView { layoutIfNeeded() let size = Int(ceil(frame.width * min(2, UIScreen.main.scale))) - let url = gravatar.replacing(options: .init(preferredSize: .pixels(size)))?.url - downloadGravatar(fullURL: url, placeholder: placeholder, animate: animate) + guard let url = gravatar.replacing(options: .init(preferredSize: .pixels(size)))?.url else { return } + downloadGravatar(fullURL: url, placeholder: placeholder, animate: animate, forceRefresh: forceRefresh) } - private func downloadGravatar(fullURL: URL?, placeholder: UIImage, animate: Bool) { + private func downloadGravatar(fullURL: URL?, placeholder: UIImage, animate: Bool, forceRefresh: Bool = false) { wp.prepareForReuse() if let fullURL { - wp.setImage(with: fullURL) + var urlToDownload = fullURL + if forceRefresh { + urlToDownload = fullURL.appendingGravatarCacheBusterParam() + } + + wp.setImage(with: urlToDownload) + if forceRefresh { + // If this is a `forceRefresh`, the cache for the original url should be updated too. + ImageDownloader.shared.setCachedImage(image, for: fullURL) + } if image == nil { // If image wasn't found synchronously in memory cache image = placeholder } From d6dee63f4a6a21f32e194ffb6b5d2df6e8cdd308 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 15:09:38 +0300 Subject: [PATCH 03/24] Force refresh on gravatar update notification --- .../BlogDetailsViewController+Me.swift | 28 +++++++++++-------- .../System/WPTabBarController+MeTab.swift | 24 ++++++++++------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift index 40b98d2cc756..15664fd0dbcf 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift @@ -4,12 +4,12 @@ import Gravatar extension BlogDetailsViewController { - @objc func downloadGravatarImage(for row: BlogDetailsRow) { + @objc func downloadGravatarImage(for row: BlogDetailsRow, forceRefresh: Bool = false) { guard let email = blog.account?.email else { return } - ImageDownloader.shared.downloadGravatarImage(with: email) { [weak self] image in + ImageDownloader.shared.downloadGravatarImage(with: email, forceRefresh: forceRefresh) { [weak self] image in guard let image, let gravatarIcon = image.gravatarIcon(size: Metrics.iconSize) else { return @@ -25,17 +25,23 @@ extension BlogDetailsViewController { } @objc private func updateGravatarImage(_ notification: Foundation.Notification) { - guard let userInfo = notification.userInfo, - let email = userInfo["email"] as? String, - let image = userInfo["image"] as? UIImage, - let url = AvatarURL.url(for: email), - let gravatarIcon = image.gravatarIcon(size: Metrics.iconSize) else { - return + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + guard let meRow else { return } + downloadGravatarImage(for: meRow, forceRefresh: true) } + else { + guard let userInfo = notification.userInfo, + let email = userInfo["email"] as? String, + let image = userInfo["image"] as? UIImage, + let url = AvatarURL.url(for: email), + let gravatarIcon = image.gravatarIcon(size: Metrics.iconSize) else { + return + } - ImageCache.shared.setImage(image, forKey: url.absoluteString) - meRow?.image = gravatarIcon - reloadMeRow() + ImageCache.shared.setImage(image, forKey: url.absoluteString) + meRow?.image = gravatarIcon + reloadMeRow() + } } private func reloadMeRow() { diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift index ab2b8e2761f5..6ab198ddef95 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift @@ -18,13 +18,16 @@ extension WPTabBarController { @objc func configureMeTabImage(placeholderImage: UIImage?) { meNavigationController.tabBarItem.image = placeholderImage + downloadImage() + } + func downloadImage(forceRefresh: Bool = false) { guard let account = defaultAccount(), let email = account.email else { return } - ImageDownloader.shared.downloadGravatarImage(with: email) { [weak self] image in + ImageDownloader.shared.downloadGravatarImage(with: email, forceRefresh: forceRefresh) { [weak self] image in guard let image else { return } @@ -34,15 +37,20 @@ extension WPTabBarController { } @objc private func updateGravatarImage(_ notification: Foundation.Notification) { - guard let userInfo = notification.userInfo, - let email = userInfo["email"] as? String, - let image = userInfo["image"] as? UIImage, - let url = AvatarURL.url(for: email) else { - return + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + downloadImage(forceRefresh: true) } + else { + guard let userInfo = notification.userInfo, + let email = userInfo["email"] as? String, + let image = userInfo["image"] as? UIImage, + let url = AvatarURL.url(for: email) else { + return + } - ImageCache.shared.setImage(image, forKey: url.absoluteString) - meNavigationController.tabBarItem.configureGravatarImage(image) + ImageCache.shared.setImage(image, forKey: url.absoluteString) + meNavigationController.tabBarItem.configureGravatarImage(image) + } } @objc private func accountDidChange() { From d25747d55f1736798f46c1c17af85e5cd59cbef8 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:13:10 +0300 Subject: [PATCH 04/24] Add `GravatarQuickEditorPresenter` --- .../GravatarQuickEditorPresenter.swift | 38 +++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 6 +++ 2 files changed, 44 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift new file mode 100644 index 000000000000..044933cbb5ed --- /dev/null +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift @@ -0,0 +1,38 @@ +import Foundation +import GravatarUI +import WordPressAuthenticator + +@MainActor +struct GravatarQuickEditorPresenter { + let email: String + let authToken: String + + init?(email: String) { + let context = ContextManager.sharedInstance().mainContext + guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) else { + return nil + } + self.email = email + self.authToken = account.authToken + } + + func presentQuickEditor(on presentingViewController: UIViewController) { + let presenter = QuickEditorPresenter( + email: Email(email), + scope: .avatarPicker(AvatarPickerConfiguration(contentLayout: .horizontal())), + configuration: .init( + interfaceStyle: presentingViewController.traitCollection.userInterfaceStyle + ), + token: authToken + ) + presenter.present( + in: presentingViewController, + onAvatarUpdated: { + AuthenticatorAnalyticsTracker.shared.track(click: .selectAvatar) + NotificationCenter.default.post(name: .GravatarImageUpdateNotification, object: self, userInfo: ["email": email]) + }, onDismiss: { + // No op. + } + ) + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index c1871b030117..3517b9872fe0 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2649,6 +2649,8 @@ 8F2289EDA1886BF77687D72D /* TimeZoneSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2283367263B37B0681F988 /* TimeZoneSelectorViewController.swift */; }; 8F228B22E190FF92D05E53DB /* TimeZoneSearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F228848D5DEACE6798CE7E2 /* TimeZoneSearchHeaderView.swift */; }; 8F228F2923045666AE456D2C /* TimeZoneSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2283367263B37B0681F988 /* TimeZoneSelectorViewController.swift */; }; + 9109D5C72CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9109D5C62CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift */; }; + 9109D5C82CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9109D5C62CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift */; }; 91138455228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91138454228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift */; }; 912347762216E27200BD9F97 /* GutenbergViewController+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */; }; 91B0535D2B726F810073455C /* GravatarInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B0535C2B726F810073455C /* GravatarInfoRow.swift */; }; @@ -8176,6 +8178,7 @@ 8F2283367263B37B0681F988 /* TimeZoneSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorViewController.swift; sourceTree = ""; }; 8F228848D5DEACE6798CE7E2 /* TimeZoneSearchHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZoneSearchHeaderView.swift; sourceTree = ""; }; 8F228AE62B771552F0F971BE /* TimeZoneSearchHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimeZoneSearchHeaderView.xib; sourceTree = ""; }; + 9109D5C62CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GravatarQuickEditorPresenter.swift; sourceTree = ""; }; 91138454228373EB00FB02B7 /* GutenbergVideoUploadProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergVideoUploadProcessor.swift; sourceTree = ""; }; 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GutenbergViewController+Localization.swift"; sourceTree = ""; }; 9149D34BF5182F360C84EDB9 /* Pods-JetpackDraftActionExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackDraftActionExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackDraftActionExtension/Pods-JetpackDraftActionExtension.debug.xcconfig"; sourceTree = ""; }; @@ -11831,6 +11834,7 @@ 43FF64EE20DAA0840060A69A /* GravatarUploader.swift */, 0CA1C8C02A940EE300F691EE /* AvatarMenuController.swift */, 91B0535C2B726F810073455C /* GravatarInfoRow.swift */, + 9109D5C62CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift */, ); path = Gravatar; sourceTree = ""; @@ -22881,6 +22885,7 @@ F5D0A64923C8FA1500B20D27 /* LinkBehavior.swift in Sources */, 08E6E07B2A4C3E3A00B807B0 /* CompliancePopoverViewController.swift in Sources */, 0C75E26E2A9F63CB00B784E5 /* MediaImageService.swift in Sources */, + 9109D5C82CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift in Sources */, 80C523A429959DE000B1C14B /* BlazeWebViewController.swift in Sources */, E6DE44671B90D251000FA7EF /* ReaderHelpers.swift in Sources */, C7BB601C2863B3D600748FD9 /* QRLoginCameraPermissionsHandler.swift in Sources */, @@ -26152,6 +26157,7 @@ FABB25AB2602FC2C00C8785C /* FancyAlertViewController+NotificationPrimer.swift in Sources */, 24DB7C172C5AFA7200A0FE92 /* WordPressClient.swift in Sources */, FABB25AC2602FC2C00C8785C /* Charts+Support.swift in Sources */, + 9109D5C72CD25A4300C7F1B1 /* GravatarQuickEditorPresenter.swift in Sources */, FABB25AD2602FC2C00C8785C /* SharingAuthorizationWebViewController.swift in Sources */, 80DB57932AF8B59B00C728FF /* RegisterDomainCoordinator.swift in Sources */, 3FB1929326C6C57A000F5AA3 /* TimeSelectionView.swift in Sources */, From 973797138255f8ba018318c5311911a5183621fe Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:14:17 +0300 Subject: [PATCH 05/24] Display QuickEditor instead of the avatar upload menu --- .../Me/My Profile/MyProfileHeaderView.swift | 29 ++++++++++++++++-- .../My Profile/MyProfileViewController.swift | 22 +++++++------- .../NUX/EpilogueUserInfoCell.swift | 30 +++++++++++++------ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift index 249d9283bab0..f34a39efc764 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift @@ -5,7 +5,7 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { // MARK: - Public Properties and Outlets @IBOutlet var gravatarImageView: CircularImageView! @IBOutlet var gravatarButton: UIButton! - + weak var presentingViewController: UIViewController? // A fake button displayed on top of gravatarImageView. let imageViewButton = UIButton(type: .system) @@ -25,7 +25,7 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { var gravatarEmail: String? = nil { didSet { if let email = gravatarEmail { - gravatarImageView.downloadGravatar(for: email, gravatarRating: .x) + downloadAvatar() } } } @@ -51,10 +51,21 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { configureGravatarButton() } + private func downloadAvatar(forceRefresh: Bool = false) { + if let email = gravatarEmail { + gravatarImageView.downloadGravatar(for: email, gravatarRating: .x, forceRefresh: forceRefresh) + } + } + + @objc private func refreshAvatar() { + downloadAvatar(forceRefresh: true) + } + /// Overrides the current Gravatar Image (set via Email) with a given image reference. /// Plus, the internal image cache is updated, to prevent undesired glitches upon refresh. /// func overrideGravatarImage(_ image: UIImage) { + guard !RemoteFeatureFlag.gravatarQuickEditor.enabled() else { return } gravatarImageView.image = image // Note: @@ -81,9 +92,23 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { gravatarImageView.addSubview(imageViewButton) imageViewButton.translatesAutoresizingMaskIntoConstraints = false imageViewButton.pinSubviewToAllEdges(gravatarImageView) + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + imageViewButton.addTarget(self, action: #selector(gravatarButtonTapped), for: .touchUpInside) + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + } } private func configureGravatarButton() { gravatarButton.tintColor = UIAppColor.primary + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + gravatarButton.addTarget(self, action: #selector(gravatarButtonTapped), for: .touchUpInside) + } + } + + @objc private func gravatarButtonTapped() { + guard let email = gravatarEmail, + let presenter = GravatarQuickEditorPresenter(email: email), + let presentingViewController else { return } + presenter.presentQuickEditor(on: presentingViewController) } } diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileViewController.swift b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileViewController.swift index 8b50453ad97e..8c010ce5d3a4 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileViewController.swift @@ -15,17 +15,19 @@ func MyProfileViewController(account: WPAccount, service: AccountSettingsService let controller = MyProfileController(account: account, service: service, headerView: headerView) let viewController = ImmuTableViewController(controller: controller, style: .insetGrouped) controller.tableView = viewController.tableView + headerView.presentingViewController = viewController + if !RemoteFeatureFlag.gravatarQuickEditor.enabled() { + let menuController = AvatarMenuController(viewController: viewController) + menuController.onAvatarSelected = { [weak controller, weak viewController] image in + guard let controller, let viewController else { return } + controller.uploadGravatarImage(image, presenter: viewController) + } + objc_setAssociatedObject(viewController, &associateObjectKey, menuController, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - let menuController = AvatarMenuController(viewController: viewController) - menuController.onAvatarSelected = { [weak controller, weak viewController] image in - guard let controller, let viewController else { return } - controller.uploadGravatarImage(image, presenter: viewController) - } - objc_setAssociatedObject(viewController, &associateObjectKey, menuController, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - for button in [headerView.imageViewButton, headerView.gravatarButton] as [UIButton] { - button.menu = menuController.makeMenu() - button.showsMenuAsPrimaryAction = true + for button in [headerView.imageViewButton, headerView.gravatarButton] as [UIButton] { + button.menu = menuController.makeMenu() + button.showsMenuAsPrimaryAction = true + } } viewController.tableView.tableHeaderView = headerView return viewController diff --git a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift index e5f69122fd23..58c0534739d0 100644 --- a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift +++ b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift @@ -66,16 +66,28 @@ class EpilogueUserInfoCell: UITableViewCell { } private func setupGravatarButton(viewController: UIViewController) { - let menuController = AvatarMenuController(viewController: viewController) - menuController.onAvatarSelected = { [weak self] in - self?.uploadGravatarImage($0) + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + gravatarButton.addTarget(self, action: #selector(gravatarButtonTapped), for: .touchUpInside) } - self.avatarMenuController = menuController // Just retaining it - gravatarButton.menu = menuController.makeMenu() - gravatarButton.showsMenuAsPrimaryAction = true - gravatarButton.addAction(UIAction { _ in - AuthenticatorAnalyticsTracker.shared.track(click: .selectAvatar) - }, for: .menuActionTriggered) + else { + let menuController = AvatarMenuController(viewController: viewController) + menuController.onAvatarSelected = { [weak self] in + self?.uploadGravatarImage($0) + } + self.avatarMenuController = menuController // Just retaining it + gravatarButton.menu = menuController.makeMenu() + gravatarButton.showsMenuAsPrimaryAction = true + gravatarButton.addAction(UIAction { _ in + AuthenticatorAnalyticsTracker.shared.track(click: .selectAvatar) + }, for: .menuActionTriggered) + } + } + + @objc private func gravatarButtonTapped() { + guard let email, + let presenter = GravatarQuickEditorPresenter(email: email), + let viewController else { return } + presenter.presentQuickEditor(on: viewController) } /// Starts the Activity Indicator Animation, and hides the Username + Fullname labels. From 9fd8df8a76e59934940536e565e7b8abd042f758 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:15:41 +0300 Subject: [PATCH 06/24] Move listenForGravatarChanges to upper level Add forceRefresh option --- .../LoginLinkRequestViewController.swift | 14 +++++++++++- .../Signin/UIImageView+Additions.swift | 12 ++++++---- .../GravatarEmailTableViewCell.swift | 22 +++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift index df6bc5f6f30d..f43b6e892789 100644 --- a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift +++ b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift @@ -38,11 +38,12 @@ class LoginLinkRequestViewController: LoginViewController { let email = loginFields.username if email.isValidEmail() { Task { - try await gravatarView?.setGravatarImage(with: email, rating: .x) + try await downloadAvatar() } } else { gravatarView?.isHidden = true } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) } override func viewDidAppear(_ animated: Bool) { @@ -50,6 +51,17 @@ class LoginLinkRequestViewController: LoginViewController { WordPressAuthenticator.track(.loginMagicLinkRequestFormViewed) } + private func downloadAvatar(forceRefresh: Bool = false) async throws { + let email = loginFields.username + try await gravatarView?.setGravatarImage(with: email, rating: .x, forceRefresh: forceRefresh) + } + + @objc private func refreshAvatar() { + Task { + try await downloadAvatar(forceRefresh: true) + } + } + // MARK: - Configuration /// Assigns localized strings to various UIControl defined in the storyboard. diff --git a/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift index 807740611632..f9d8a5d7e1a2 100644 --- a/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift +++ b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift @@ -3,12 +3,16 @@ import WordPressUI import GravatarUI extension UIImageView { - func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil) async throws { - listenForGravatarChanges(forEmail: email) + func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil, forceRefresh: Bool = false) async throws { + var options: [ImageSettingOption] = [] + if forceRefresh { + options.append(.forceRefresh) + } try await gravatar.setImage(avatarID: .email(email), placeholder: placeholder, - rating: .x, + rating: rating, preferredSize: preferredSize, - defaultAvatarOption: .status404) + defaultAvatarOption: .status404, + options: options) } } diff --git a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift index 179ce951b07f..ccc0882342c2 100644 --- a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift +++ b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift @@ -20,10 +20,14 @@ class GravatarEmailTableViewCell: UITableViewCell { /// public static let reuseIdentifier = "GravatarEmailTableViewCell" public var onChangeSelectionHandler: ((_ sender: UITextField) -> Void)? + private var gravatarPlaceholderImage: UIImage? = nil + private var gravatarPreferredSize: CGSize = .zero + private var email: String? /// Public Methods /// public func configure(withEmail email: String?, andPlaceholder placeholderImage: UIImage? = nil, hasBorders: Bool = false) { + self.email = email gravatarImageView?.tintColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor emailLabel?.textColor = WordPressAuthenticator.shared.unifiedStyle?.gravatarEmailTextColor ?? WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor ?? WordPressAuthenticator.shared.style.subheadlineColor emailLabel?.font = UIFont.preferredFont(forTextStyle: .body) @@ -36,9 +40,10 @@ class GravatarEmailTableViewCell: UITableViewCell { gravatarImageView?.image = gridicon return } - + self.gravatarPlaceholderImage = placeholderImage ?? gridicon + self.gravatarPreferredSize = gridicon.size Task { - try await gravatarImageView?.setGravatarImage(with: email, placeholder: placeholderImage ?? gridicon, preferredSize: gridicon.size) + try await downloadAvatar() } gravatarImageViewSizeConstraints.forEach { constraint in @@ -53,6 +58,19 @@ class GravatarEmailTableViewCell: UITableViewCell { containerView.layer.borderWidth = hasBorders ? 1 : 0 containerView.layer.cornerRadius = hasBorders ? 8 : 0 containerView.layer.borderColor = hasBorders ? UIColor.systemGray3.cgColor : UIColor.clear.cgColor + + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + } + + @objc private func refreshAvatar() { + Task { + try await downloadAvatar(forceRefresh: true) + } + } + + private func downloadAvatar(forceRefresh: Bool = false) async throws { + guard let email, let gravatarPlaceholderImage else { return } + try await gravatarImageView?.setGravatarImage(with: email, placeholder: gravatarPlaceholderImage, preferredSize: gravatarPreferredSize, forceRefresh: forceRefresh) } func updateEmailAddress(_ email: String?) { From 52a32cd73e30effa280c92f91f3b514325ce15f7 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:18:06 +0300 Subject: [PATCH 07/24] Add one more FF check --- WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift index 17579ff0cfaa..fbfab54bc952 100644 --- a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift @@ -71,6 +71,7 @@ final class MeHeaderView: UIView { } func overrideGravatarImage(_ image: UIImage) { + guard !RemoteFeatureFlag.gravatarQuickEditor.enabled() else { return } iconView.image = image // Note: From 16cc08ac802a1e63a968658b5fb255a7326d5084 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:24:00 +0300 Subject: [PATCH 08/24] Adjust the obj-c call --- .../ViewRelated/Blog/Blog Details/BlogDetailsViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m index ad5f18088db0..b67bd4d18f17 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController.m @@ -1329,7 +1329,7 @@ - (BlogDetailsSection *)configurationSectionViewModel callback:^{ [weakSelf showMe]; }]; - [self downloadGravatarImageFor:row]; + [self downloadGravatarImageFor:row forceRefresh: NO]; self.meRow = row; [rows addObject:row]; } From 269471883d5e30abb1be2e2c23ef12ae800777db Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:57:10 +0300 Subject: [PATCH 09/24] Listen to changes on the MeHeaderView --- .../ViewRelated/Me/Views/MeHeaderView.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift index fbfab54bc952..cf1927725def 100644 --- a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift @@ -43,6 +43,7 @@ final class MeHeaderView: UIView { // tableView.headerView inevitably has to break something $0.priority = UILayoutPriority(999) } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) } required init?(coder: NSCoder) { @@ -63,13 +64,23 @@ final class MeHeaderView: UIView { titleLabel.text = viewModel.displayName detailsLabel.text = viewModel.username - if let gravatarEmail = viewModel.gravatarEmail { - iconView.downloadGravatar(for: gravatarEmail, gravatarRating: .x) + if viewModel.gravatarEmail != nil { + downloadAvatar() } else { iconView.image = nil } } + private func downloadAvatar(forceRefresh: Bool = false) { + if let email = viewModel?.gravatarEmail { + iconView.downloadGravatar(for: email, gravatarRating: .x, forceRefresh: forceRefresh) + } + } + + @objc private func refreshAvatar() { + downloadAvatar(forceRefresh: true) + } + func overrideGravatarImage(_ image: UIImage) { guard !RemoteFeatureFlag.gravatarQuickEditor.enabled() else { return } iconView.image = image From 7989b149434dd71a816740ee47355d3277d49de0 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 16:58:15 +0300 Subject: [PATCH 10/24] Await the image download to update the cached image --- .../Classes/Utility/Media/ImageViewController.swift | 2 +- .../ViewRelated/Gravatar/UIImageView+Gravatar.swift | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/Utility/Media/ImageViewController.swift b/WordPress/Classes/Utility/Media/ImageViewController.swift index a749f1e6dba1..583c3335d92c 100644 --- a/WordPress/Classes/Utility/Media/ImageViewController.swift +++ b/WordPress/Classes/Utility/Media/ImageViewController.swift @@ -6,7 +6,7 @@ final class ImageViewController { var downloader: ImageDownloader = .shared var onStateChanged: (State) -> Void = { _ in } - private var task: Task? + private(set) var task: Task? enum State { case loading diff --git a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift index 5ff31c9fc8bc..cd386a8aad4e 100644 --- a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift +++ b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift @@ -71,10 +71,16 @@ extension UIImageView { } wp.setImage(with: urlToDownload) + if forceRefresh { - // If this is a `forceRefresh`, the cache for the original url should be updated too. - ImageDownloader.shared.setCachedImage(image, for: fullURL) + // If this is a `forceRefresh`, the cache for the original URL should be updated too. + // Because the cache buster parameter modifies the download URL. + Task { + await wp.controller.task?.value // Wait until setting the new image is done. + ImageDownloader.shared.setCachedImage(image, for: fullURL) // Update the cache for the original URL + } } + if image == nil { // If image wasn't found synchronously in memory cache image = placeholder } From e33654f42fa2cd1a5d47962df7d0252e5c6bdf2d Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 18:44:30 +0300 Subject: [PATCH 11/24] Listen to gravatar changes on the signup epilogue --- .../NUX/EpilogueUserInfoCell.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift index 58c0534739d0..3add9b7be8aa 100644 --- a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift +++ b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift @@ -16,16 +16,21 @@ class EpilogueUserInfoCell: UITableViewCell { private var gravatarStatus: GravatarUploaderStatus = .idle private var email: String? private var avatarMenuController: AnyObject? + private var allowGravatarUploads: Bool = false override func awakeFromNib() { super.awakeFromNib() configureImages() configureColors() + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + } } /// Configures the cell so that the LoginEpilogueUserInfo's payload is displayed /// func configure(userInfo: LoginEpilogueUserInfo, showEmail: Bool = false, allowGravatarUploads: Bool = false, viewController: UIViewController) { + self.allowGravatarUploads = allowGravatarUploads email = userInfo.email self.viewController = viewController @@ -59,8 +64,7 @@ class EpilogueUserInfoCell: UITableViewCell { if let gravatarUrl = userInfo.gravatarUrl, let url = URL(string: gravatarUrl) { gravatarView.downloadImage(from: url) } else { - let placeholder: UIImage = allowGravatarUploads ? .gravatarUploadablePlaceholderImage : .gravatarPlaceholderImage - gravatarView.downloadGravatar(for: userInfo.email, gravatarRating: .x, placeholderImage: placeholder) + downloadGravatar() } } } @@ -90,6 +94,17 @@ class EpilogueUserInfoCell: UITableViewCell { presenter.presentQuickEditor(on: viewController) } + private func downloadGravatar(forceRefresh: Bool = false) { + let placeholder: UIImage = allowGravatarUploads ? .gravatarUploadablePlaceholderImage : .gravatarPlaceholderImage + if let email { + gravatarView.downloadGravatar(for: email, gravatarRating: .x, placeholderImage: placeholder, forceRefresh: forceRefresh) + } + } + + @objc private func refreshAvatar() { + downloadGravatar(forceRefresh: true) + } + /// Starts the Activity Indicator Animation, and hides the Username + Fullname labels. /// func startSpinner() { From 54bb02f224c207e056434da6645b7914ee4ad2b9 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Wed, 30 Oct 2024 18:51:15 +0300 Subject: [PATCH 12/24] Add one more FF check --- WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift index cf1927725def..93bc6022a2c1 100644 --- a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift @@ -43,7 +43,9 @@ final class MeHeaderView: UIView { // tableView.headerView inevitably has to break something $0.priority = UILayoutPriority(999) } - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + if RemoteFeatureFlag.gravatarQuickEditor.enabled() { + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + } } required init?(coder: NSCoder) { From 9c5f2886a847296a973ceb04b8319dc9f18a3860 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Thu, 31 Oct 2024 13:59:55 +0300 Subject: [PATCH 13/24] Separate the notification name for the QE Add email check when handling the notification `GravatarQEAvatarUpdateNotification` --- .../Utility/Notification+Gravatar.swift | 20 ++++++++++++ .../BlogDetailsViewController+Me.swift | 32 ++++++++++--------- .../GravatarQuickEditorPresenter.swift | 5 ++- .../Me/My Profile/MyProfileHeaderView.swift | 6 ++-- .../ViewRelated/Me/Views/MeHeaderView.swift | 8 ++--- .../NUX/EpilogueUserInfoCell.swift | 7 ++-- .../System/WPTabBarController+MeTab.swift | 27 +++++++++------- .../LoginLinkRequestViewController.swift | 5 +-- .../Signin/UIImageView+Additions.swift | 1 + .../GravatarEmailTableViewCell.swift | 5 +-- 10 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 Modules/Sources/WordPressShared/Utility/Notification+Gravatar.swift diff --git a/Modules/Sources/WordPressShared/Utility/Notification+Gravatar.swift b/Modules/Sources/WordPressShared/Utility/Notification+Gravatar.swift new file mode 100644 index 000000000000..b59717aa7e1b --- /dev/null +++ b/Modules/Sources/WordPressShared/Utility/Notification+Gravatar.swift @@ -0,0 +1,20 @@ +import Foundation + +public enum GravatarQEAvatarUpdateNotificationKeys: String { + case email +} + +public extension NSNotification.Name { + /// Gravatar Quick Editor updated the avatar + static let GravatarQEAvatarUpdateNotification = NSNotification.Name(rawValue: "GravatarQEAvatarUpdateNotification") +} + +extension Foundation.Notification { + public func userInfoHasEmail(_ email: String) -> Bool { + guard let userInfo = userInfo, + let notificationEmail = userInfo[GravatarQEAvatarUpdateNotificationKeys.email.rawValue] as? String else { + return false + } + return email == notificationEmail + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift index 15664fd0dbcf..24b18203ca53 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Me.swift @@ -21,27 +21,29 @@ extension BlogDetailsViewController { } @objc func observeGravatarImageUpdate() { + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar(_:)), name: .GravatarQEAvatarUpdateNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateGravatarImage(_:)), name: .GravatarImageUpdateNotification, object: nil) } + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let meRow, + let email = blog.account?.email, + notification.userInfoHasEmail(email) else { return } + downloadGravatarImage(for: meRow, forceRefresh: true) + } + @objc private func updateGravatarImage(_ notification: Foundation.Notification) { - if RemoteFeatureFlag.gravatarQuickEditor.enabled() { - guard let meRow else { return } - downloadGravatarImage(for: meRow, forceRefresh: true) + guard let userInfo = notification.userInfo, + let email = userInfo["email"] as? String, + let image = userInfo["image"] as? UIImage, + let url = AvatarURL.url(for: email), + let gravatarIcon = image.gravatarIcon(size: Metrics.iconSize) else { + return } - else { - guard let userInfo = notification.userInfo, - let email = userInfo["email"] as? String, - let image = userInfo["image"] as? UIImage, - let url = AvatarURL.url(for: email), - let gravatarIcon = image.gravatarIcon(size: Metrics.iconSize) else { - return - } - ImageCache.shared.setImage(image, forKey: url.absoluteString) - meRow?.image = gravatarIcon - reloadMeRow() - } + ImageCache.shared.setImage(image, forKey: url.absoluteString) + meRow?.image = gravatarIcon + reloadMeRow() } private func reloadMeRow() { diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift index 044933cbb5ed..0a24a35c4641 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift @@ -1,5 +1,6 @@ import Foundation import GravatarUI +import WordPressShared import WordPressAuthenticator @MainActor @@ -29,7 +30,9 @@ struct GravatarQuickEditorPresenter { in: presentingViewController, onAvatarUpdated: { AuthenticatorAnalyticsTracker.shared.track(click: .selectAvatar) - NotificationCenter.default.post(name: .GravatarImageUpdateNotification, object: self, userInfo: ["email": email]) + NotificationCenter.default.post(name: .GravatarQEAvatarUpdateNotification, + object: self, + userInfo: [GravatarQEAvatarUpdateNotificationKeys.email.rawValue: email]) }, onDismiss: { // No op. } diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift index f34a39efc764..23d62166b93d 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift @@ -57,7 +57,9 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { } } - @objc private func refreshAvatar() { + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let email = gravatarEmail, + notification.userInfoHasEmail(email) else { return } downloadAvatar(forceRefresh: true) } @@ -94,7 +96,7 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { imageViewButton.pinSubviewToAllEdges(gravatarImageView) if RemoteFeatureFlag.gravatarQuickEditor.enabled() { imageViewButton.addTarget(self, action: #selector(gravatarButtonTapped), for: .touchUpInside) - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } } diff --git a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift index 93bc6022a2c1..714a1276a7ab 100644 --- a/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/Views/MeHeaderView.swift @@ -43,9 +43,7 @@ final class MeHeaderView: UIView { // tableView.headerView inevitably has to break something $0.priority = UILayoutPriority(999) } - if RemoteFeatureFlag.gravatarQuickEditor.enabled() { - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } required init?(coder: NSCoder) { @@ -79,7 +77,9 @@ final class MeHeaderView: UIView { } } - @objc private func refreshAvatar() { + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let email = viewModel?.gravatarEmail, + notification.userInfoHasEmail(email) else { return } downloadAvatar(forceRefresh: true) } diff --git a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift index 3add9b7be8aa..23e39c0810e9 100644 --- a/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift +++ b/WordPress/Classes/ViewRelated/NUX/EpilogueUserInfoCell.swift @@ -22,9 +22,7 @@ class EpilogueUserInfoCell: UITableViewCell { super.awakeFromNib() configureImages() configureColors() - if RemoteFeatureFlag.gravatarQuickEditor.enabled() { - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) - } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } /// Configures the cell so that the LoginEpilogueUserInfo's payload is displayed @@ -101,7 +99,8 @@ class EpilogueUserInfoCell: UITableViewCell { } } - @objc private func refreshAvatar() { + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let email, notification.userInfoHasEmail(email) else { return } downloadGravatar(forceRefresh: true) } diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift index 6ab198ddef95..25747e47e944 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift @@ -9,6 +9,8 @@ extension WPTabBarController { } @objc func observeGravatarImageUpdate() { + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar(_:)), name: .GravatarQEAvatarUpdateNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateGravatarImage(_:)), name: .GravatarImageUpdateNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidChange), name: .WPAccountDefaultWordPressComAccountChanged, object: nil) @@ -36,21 +38,22 @@ extension WPTabBarController { } } + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let email = defaultAccount()?.email, + notification.userInfoHasEmail(email) else { return } + downloadImage(forceRefresh: true) + } + @objc private func updateGravatarImage(_ notification: Foundation.Notification) { - if RemoteFeatureFlag.gravatarQuickEditor.enabled() { - downloadImage(forceRefresh: true) + guard let userInfo = notification.userInfo, + let email = userInfo["email"] as? String, + let image = userInfo["image"] as? UIImage, + let url = AvatarURL.url(for: email) else { + return } - else { - guard let userInfo = notification.userInfo, - let email = userInfo["email"] as? String, - let image = userInfo["image"] as? UIImage, - let url = AvatarURL.url(for: email) else { - return - } - ImageCache.shared.setImage(image, forKey: url.absoluteString) - meNavigationController.tabBarItem.configureGravatarImage(image) - } + ImageCache.shared.setImage(image, forKey: url.absoluteString) + meNavigationController.tabBarItem.configureGravatarImage(image) } @objc private func accountDidChange() { diff --git a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift index f43b6e892789..faebf5684c15 100644 --- a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift +++ b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift @@ -43,7 +43,7 @@ class LoginLinkRequestViewController: LoginViewController { } else { gravatarView?.isHidden = true } - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } override func viewDidAppear(_ animated: Bool) { @@ -56,7 +56,8 @@ class LoginLinkRequestViewController: LoginViewController { try await gravatarView?.setGravatarImage(with: email, rating: .x, forceRefresh: forceRefresh) } - @objc private func refreshAvatar() { + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard notification.userInfoHasEmail(loginFields.username) else { return } Task { try await downloadAvatar(forceRefresh: true) } diff --git a/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift index f9d8a5d7e1a2..02c99799b714 100644 --- a/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift +++ b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift @@ -4,6 +4,7 @@ import GravatarUI extension UIImageView { func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil, forceRefresh: Bool = false) async throws { + listenForGravatarChanges(forEmail: email) var options: [ImageSettingOption] = [] if forceRefresh { options.append(.forceRefresh) diff --git a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift index ccc0882342c2..45b1a458f4dd 100644 --- a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift +++ b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift @@ -59,10 +59,11 @@ class GravatarEmailTableViewCell: UITableViewCell { containerView.layer.cornerRadius = hasBorders ? 8 : 0 containerView.layer.borderColor = hasBorders ? UIColor.systemGray3.cgColor : UIColor.clear.cgColor - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarImageUpdateNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } - @objc private func refreshAvatar() { + @objc private func refreshAvatar(_ notification: Foundation.Notification) { + guard let email, notification.userInfoHasEmail(email) else { return } Task { try await downloadAvatar(forceRefresh: true) } From 74a9dd796356ca02c106c40b5c009318fd384f43 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Thu, 31 Oct 2024 14:47:31 +0300 Subject: [PATCH 14/24] Add a release note --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e46176e63df2..0639ba59b177 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,7 @@ 25.6 ----- * [*] [internal] Update Gravatar SDK to 3.0.0 [#23701] +* [*] Use the Gravatar Quick Editor to update the avatar [#23729] 25.5 ----- From 8d191d4e4bacf4753bde1878c9954f7eb8a5c468 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Thu, 31 Oct 2024 15:04:47 +0300 Subject: [PATCH 15/24] Revert whitespace change --- .../ViewRelated/System/WPTabBarController+MeTab.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift index 25747e47e944..6303daff9eaa 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController+MeTab.swift @@ -46,10 +46,10 @@ extension WPTabBarController { @objc private func updateGravatarImage(_ notification: Foundation.Notification) { guard let userInfo = notification.userInfo, - let email = userInfo["email"] as? String, - let image = userInfo["image"] as? UIImage, - let url = AvatarURL.url(for: email) else { - return + let email = userInfo["email"] as? String, + let image = userInfo["image"] as? UIImage, + let url = AvatarURL.url(for: email) else { + return } ImageCache.shared.setImage(image, forKey: url.absoluteString) From 2fcba8d0917412e1c18f781a9f11bef6d9c6a5e0 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Thu, 31 Oct 2024 16:56:49 +0300 Subject: [PATCH 16/24] Use overrideImageCache` --- .../Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift index cd386a8aad4e..4033afe03704 100644 --- a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift +++ b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift @@ -77,7 +77,9 @@ extension UIImageView { // Because the cache buster parameter modifies the download URL. Task { await wp.controller.task?.value // Wait until setting the new image is done. - ImageDownloader.shared.setCachedImage(image, for: fullURL) // Update the cache for the original URL + if let image { + overrideImageCache(for: fullURL, with: image) // Update the cache for the original URL + } } } From e85a655017bdef80c9051ba4ab342abc969e4667 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Fri, 1 Nov 2024 10:51:40 +0300 Subject: [PATCH 17/24] Fix the old avatar issue --- WordPress/Classes/Utility/Media/ImageDownloader.swift | 9 +++++++++ WordPress/Classes/Utility/Media/MemoryCache.swift | 6 ++++++ .../ViewRelated/Gravatar/UIImageView+Gravatar.swift | 2 +- .../Gravatar/GravatarQuickEditorPresenter.swift | 11 ++++++++--- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Utility/Media/ImageDownloader.swift b/WordPress/Classes/Utility/Media/ImageDownloader.swift index 58fce0d42621..f81deb20962d 100644 --- a/WordPress/Classes/Utility/Media/ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/ImageDownloader.swift @@ -102,6 +102,15 @@ actor ImageDownloader { return imageURL.absoluteString + (size.map { "?size=\($0)" } ?? "") } + func clearURLSessionCache() { + urlSessionWithCache.configuration.urlCache?.removeAllCachedResponses() + urlSession.configuration.urlCache?.removeAllCachedResponses() + } + + func clearMemoryCache() { + self.cache.removeAllObjects() + } + // MARK: - Networking private func data(for request: URLRequest, options: ImageRequestOptions) async throws -> Data { diff --git a/WordPress/Classes/Utility/Media/MemoryCache.swift b/WordPress/Classes/Utility/Media/MemoryCache.swift index f7824ad98d25..4eda923d339e 100644 --- a/WordPress/Classes/Utility/Media/MemoryCache.swift +++ b/WordPress/Classes/Utility/Media/MemoryCache.swift @@ -4,10 +4,12 @@ import WordPressUI protocol MemoryCacheProtocol: AnyObject { subscript(key: String) -> UIImage? { get set } + func removeAllObjects() } /// - note: The type is thread-safe because it uses thread-safe `NSCache`. final class MemoryCache: MemoryCacheProtocol, @unchecked Sendable { + /// A shared image cache used by the entire system. static let shared = MemoryCache() @@ -23,6 +25,10 @@ final class MemoryCache: MemoryCacheProtocol, @unchecked Sendable { cache.removeAllObjects() } + func removeAllObjects() { + cache.removeAllObjects() + } + // MARK: - UIImage subscript(key: String) -> UIImage? { diff --git a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift index 4033afe03704..43065c180f51 100644 --- a/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift +++ b/WordPress/Classes/ViewRelated/Gravatar/UIImageView+Gravatar.swift @@ -78,7 +78,7 @@ extension UIImageView { Task { await wp.controller.task?.value // Wait until setting the new image is done. if let image { - overrideImageCache(for: fullURL, with: image) // Update the cache for the original URL + ImageCache.shared.setImage(image, forKey: fullURL.absoluteString) // Update the cache for the original URL } } } diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift index 0a24a35c4641..519806648a35 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/Gravatar/GravatarQuickEditorPresenter.swift @@ -30,9 +30,14 @@ struct GravatarQuickEditorPresenter { in: presentingViewController, onAvatarUpdated: { AuthenticatorAnalyticsTracker.shared.track(click: .selectAvatar) - NotificationCenter.default.post(name: .GravatarQEAvatarUpdateNotification, - object: self, - userInfo: [GravatarQEAvatarUpdateNotificationKeys.email.rawValue: email]) + Task { + // Purge the cache otherwise the old avatars remain around. + await ImageDownloader.shared.clearURLSessionCache() + await ImageDownloader.shared.clearMemoryCache() + NotificationCenter.default.post(name: .GravatarQEAvatarUpdateNotification, + object: self, + userInfo: [GravatarQEAvatarUpdateNotificationKeys.email.rawValue: email]) + } }, onDismiss: { // No op. } From 9247b271674490446a8f43a2eb384562a749ca2c Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Fri, 1 Nov 2024 11:58:30 +0300 Subject: [PATCH 18/24] Update unit test --- WordPress/WordPressTest/ImageDownloaderTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WordPress/WordPressTest/ImageDownloaderTests.swift b/WordPress/WordPressTest/ImageDownloaderTests.swift index 1f7911b590fa..12786eeb8ec5 100644 --- a/WordPress/WordPressTest/ImageDownloaderTests.swift +++ b/WordPress/WordPressTest/ImageDownloaderTests.swift @@ -158,4 +158,8 @@ private final class MockMemoryCache: MemoryCacheProtocol { get { cache[key] } set { cache[key] = newValue } } + + func removeAllObjects() { + cache = [:] + } } From 896b7564f8fd95c0b02f05bb9428cd4f0c410ec1 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Mon, 11 Nov 2024 12:06:29 +0300 Subject: [PATCH 19/24] Move `addObserver` to viewDidLoad --- .../Sources/Signin/LoginLinkRequestViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift index faebf5684c15..2fc94a0f35e4 100644 --- a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift +++ b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift @@ -30,6 +30,7 @@ class LoginLinkRequestViewController: LoginViewController { if !email.isValidEmail() { assert(email.isValidEmail(), "The value of loginFields.username was not a valid email address.") } + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } override func viewWillAppear(_ animated: Bool) { @@ -43,7 +44,6 @@ class LoginLinkRequestViewController: LoginViewController { } else { gravatarView?.isHidden = true } - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } override func viewDidAppear(_ animated: Bool) { From 0ebbc55219b8dd56987c561a63ce665435c452aa Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Mon, 11 Nov 2024 12:22:37 +0300 Subject: [PATCH 20/24] Mode addObserver to init` --- .../Reusable Views/GravatarEmailTableViewCell.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift index 45b1a458f4dd..1b7c0ca94136 100644 --- a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift +++ b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift @@ -24,6 +24,11 @@ class GravatarEmailTableViewCell: UITableViewCell { private var gravatarPreferredSize: CGSize = .zero private var email: String? + required init?(coder: NSCoder) { + super.init(coder: coder) + NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) + } + /// Public Methods /// public func configure(withEmail email: String?, andPlaceholder placeholderImage: UIImage? = nil, hasBorders: Bool = false) { @@ -58,8 +63,6 @@ class GravatarEmailTableViewCell: UITableViewCell { containerView.layer.borderWidth = hasBorders ? 1 : 0 containerView.layer.cornerRadius = hasBorders ? 8 : 0 containerView.layer.borderColor = hasBorders ? UIColor.systemGray3.cgColor : UIColor.clear.cgColor - - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) } @objc private func refreshAvatar(_ notification: Foundation.Notification) { From 83e0a61dfc2a655f0f829c3cc360f54b81031b8a Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Mon, 11 Nov 2024 13:54:49 +0300 Subject: [PATCH 21/24] Add unit tests for `appendingGravatarCacheBusterParam` and handle the canonical URL as well --- .../Classes/Extensions/URL+Helpers.swift | 5 +++- WordPress/WordPress.xcodeproj/project.pbxproj | 6 ++++- .../Extensions/URLHelpersTests.swift | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 WordPress/WordPressTest/Extensions/URLHelpersTests.swift diff --git a/WordPress/Classes/Extensions/URL+Helpers.swift b/WordPress/Classes/Extensions/URL+Helpers.swift index ff54e10c10c3..0eb0102a8e9a 100644 --- a/WordPress/Classes/Extensions/URL+Helpers.swift +++ b/WordPress/Classes/Extensions/URL+Helpers.swift @@ -154,8 +154,11 @@ extension URL { /// Gravatar doesn't support "Cache-Control: none" header. So we add a random query parameter to /// bypass the backend cache and get the latest image. - func appendingGravatarCacheBusterParam() -> URL { + public func appendingGravatarCacheBusterParam() -> URL { var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) + if urlComponents?.queryItems == nil { + urlComponents?.queryItems = [] + } urlComponents?.queryItems?.append(.init(name: "_", value: "\(NSDate().timeIntervalSince1970)")) return urlComponents?.url ?? self } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3952f8a72a90..2ab7bd16fa93 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXAggregateTarget section */ @@ -991,6 +991,7 @@ 8BEE845A27B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json in Resources */ = {isa = PBXBuildFile; fileRef = 8BEE845927B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json */; }; 8BFE36FF230F1C850061EBA8 /* AbstractPost+fixLocalMediaURLsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFE36FE230F1C850061EBA8 /* AbstractPost+fixLocalMediaURLsTests.swift */; }; 91BE834E2C48FF0F00BB5B3B /* UIImageView+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91BE834D2C48FF0F00BB5B3B /* UIImageView+Additions.swift */; }; + 91CFB9552CE21196005CD369 /* URLHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91CFB9542CE21196005CD369 /* URLHelpersTests.swift */; }; 931215E1267DE1C0008C3B69 /* StatsTotalRowDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931215E0267DE1C0008C3B69 /* StatsTotalRowDataTests.swift */; }; 931D26F619ED7F7000114F17 /* BlogServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 930FD0A519882742000CC81D /* BlogServiceTest.m */; }; 931D26F719ED7F7500114F17 /* ReaderPostServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8A0401912D95B00B2FF59 /* ReaderPostServiceTest.m */; }; @@ -2792,6 +2793,7 @@ 8DE205D2AC15F16289E7D21A /* Pods-WordPressDraftActionExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressDraftActionExtension.release.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressDraftActionExtension/Pods-WordPressDraftActionExtension.release.xcconfig"; sourceTree = ""; }; 9149D34BF5182F360C84EDB9 /* Pods-JetpackDraftActionExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackDraftActionExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackDraftActionExtension/Pods-JetpackDraftActionExtension.debug.xcconfig"; sourceTree = ""; }; 91BE834D2C48FF0F00BB5B3B /* UIImageView+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Additions.swift"; sourceTree = ""; }; + 91CFB9542CE21196005CD369 /* URLHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHelpersTests.swift; sourceTree = ""; }; 92B40A77F0765C1E93B11727 /* Pods_WordPressDraftActionExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressDraftActionExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 930C6374182BD86400976C21 /* WordPress-Internal-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "WordPress-Internal-Info.plist"; sourceTree = ""; }; 930FD0A519882742000CC81D /* BlogServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogServiceTest.m; sourceTree = ""; }; @@ -7021,6 +7023,7 @@ 0CD6299A2B9AAA9A00325EA4 /* Foundation+Extensions.swift */, 8BFE36FE230F1C850061EBA8 /* AbstractPost+fixLocalMediaURLsTests.swift */, AE3047A9270B66D300FE9266 /* Scanner+QuotedTextTests.swift */, + 91CFB9542CE21196005CD369 /* URLHelpersTests.swift */, ); path = Extensions; sourceTree = ""; @@ -10105,6 +10108,7 @@ B532ACCF1DC3AB8E00FFFA57 /* NotificationSyncMediatorTests.swift in Sources */, 7E4A772F20F7FDF8001C706D /* ActivityLogTestData.swift in Sources */, 3F759FBC2A2DB2CF0039A845 /* TestError.swift in Sources */, + 91CFB9552CE21196005CD369 /* URLHelpersTests.swift in Sources */, B59D40A61DB522DF003D2D79 /* NSAttributedStringTests.swift in Sources */, F565190323CF6D1D003FACAF /* WKCookieJarTests.swift in Sources */, C738CB0D28623F07001BE107 /* QRLoginURLParserTests.swift in Sources */, diff --git a/WordPress/WordPressTest/Extensions/URLHelpersTests.swift b/WordPress/WordPressTest/Extensions/URLHelpersTests.swift new file mode 100644 index 000000000000..e702e74e510c --- /dev/null +++ b/WordPress/WordPressTest/Extensions/URLHelpersTests.swift @@ -0,0 +1,23 @@ +import XCTest +import WordPress + +class URLHelpersTests: XCTestCase { + + func testAddCacheBusterToExistingQueryParameters() async throws { + try await doTest("https://gravatar.com/avatar/1234?s=80") + } + + func testAddCacheBusterToCanonicalURL() async throws { + try await doTest("https://gravatar.com") + } + + func doTest(_ urlString: String) async throws { + let url = try XCTUnwrap(URL(string: urlString)) + let newURL = url.appendingGravatarCacheBusterParam() + XCTAssertNotEqual(url.absoluteString, newURL.absoluteString) + + let components = URLComponents(url: newURL, resolvingAgainstBaseURL: false) + let cacheBusterQueryItem = components?.queryItems?.filter { $0.name == "_" }.first + XCTAssertNotNil(cacheBusterQueryItem?.value) + } +} From 4b7b4fc6be21beb9affefdb46f7ad694f6f2c32a Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Fri, 15 Nov 2024 13:02:13 +0300 Subject: [PATCH 22/24] Fix "unused var" warning --- .../Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift index 23d62166b93d..a0f662e89062 100644 --- a/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Me/My Profile/MyProfileHeaderView.swift @@ -24,7 +24,7 @@ class MyProfileHeaderView: UITableViewHeaderFooterView { } var gravatarEmail: String? = nil { didSet { - if let email = gravatarEmail { + if gravatarEmail != nil { downloadAvatar() } } From f0b3eb971980ad3f918b2c28bfdf8cebfefb518b Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Fri, 15 Nov 2024 13:51:39 +0300 Subject: [PATCH 23/24] Update release notes --- RELEASE-NOTES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index a58a20e37bb4..eb4d8d0e8609 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,7 +1,10 @@ +25.7 +----- +* [*] Use the Gravatar Quick Editor to update the avatar [#23729] + 25.6 ----- * [*] [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] 25.5 From 157f7929928382a2398ffcdffcf9c09d3bdc7695 Mon Sep 17 00:00:00 2001 From: Pinar Olguc Date: Fri, 15 Nov 2024 14:25:41 +0300 Subject: [PATCH 24/24] Revert "Update release notes" This reverts commit f0b3eb971980ad3f918b2c28bfdf8cebfefb518b. --- RELEASE-NOTES.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index eb4d8d0e8609..a58a20e37bb4 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,10 +1,7 @@ -25.7 ------ -* [*] Use the Gravatar Quick Editor to update the avatar [#23729] - 25.6 ----- * [*] [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] 25.5