diff --git a/Modules/Package.swift b/Modules/Package.swift index 0d2e05ae95b0..4610dc66cf8f 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -103,6 +103,8 @@ enum XcodeSupport { .product(name: "NSURL+IDN", package: "NSURL-IDN"), .product(name: "SVProgressHUD", package: "SVProgressHUD"), .product(name: "WordPressKit", package: "WordPressKit-iOS"), + .product(name: "Gravatar", package: "Gravatar-SDK-iOS"), + .product(name: "GravatarUI", package: "Gravatar-SDK-iOS"), ] let shareAndDraftExtensionsDependencies: [Target.Dependency] = [ diff --git a/Modules/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift b/Modules/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift deleted file mode 100644 index c2665248d51b..000000000000 --- a/Modules/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift +++ /dev/null @@ -1,148 +0,0 @@ -import Foundation - -/// Helper Enum that specifies all of the available Gravatar Image Ratings -/// TODO: Convert into a pure Swift String Enum. It's done this way to maintain ObjC Compatibility -/// -@available(*, deprecated, message: "Use `Rating` from the Gravatar iOS SDK. See: https://github.com/Automattic/Gravatar-SDK-iOS.") -@objc -public enum GravatarRatings: Int { - case g - case pg - case r - case x - case `default` - - func stringValue() -> String { - switch self { - case .default: - fallthrough - case .g: - return "g" - case .pg: - return "pg" - case .r: - return "r" - case .x: - return "x" - } - } -} - -/// Helper Enum that specifies some of the options for default images -/// To see all available options, visit : https://en.gravatar.com/site/implement/images/ -/// -@available(*, deprecated, message: "Use `DefaultAvatarOption` from the Gravatar iOS SDK. See: https://github.com/Automattic/Gravatar-SDK-iOS.") -public enum GravatarDefaultImage: String { - case fileNotFound = "404" - case mp - case identicon -} - -@available(*, deprecated, message: "Use `AvatarURL` from the Gravatar iOS SDK. See: https://github.com/Automattic/Gravatar-SDK-iOS") -public struct Gravatar { - fileprivate struct Defaults { - static let scheme = "https" - static let host = "secure.gravatar.com" - static let unknownHash = "ad516503a11cd5ca435acc9bb6523536" - static let baseURL = "https://gravatar.com/avatar" - static let imageSize = 80 - } - - public let canonicalURL: URL - - public func urlWithSize(_ size: Int, defaultImage: GravatarDefaultImage? = nil) -> URL { - var components = URLComponents(url: canonicalURL, resolvingAgainstBaseURL: false)! - components.query = "s=\(size)&d=\(defaultImage?.rawValue ?? GravatarDefaultImage.fileNotFound.rawValue)" - return components.url! - } - - public static func isGravatarURL(_ url: URL) -> Bool { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return false - } - - guard let host = components.host, host.hasSuffix(".gravatar.com") else { - return false - } - - guard url.path.hasPrefix("/avatar/") else { - return false - } - - return true - } - - /// Returns the Gravatar URL, for a given email, with the specified size + rating. - /// - /// - Parameters: - /// - email: the user's email - /// - size: required download size - /// - rating: image rating filtering - /// - /// - Returns: Gravatar's URL - /// - public static func gravatarUrl(for email: String, - defaultImage: GravatarDefaultImage? = nil, - size: Int? = nil, - rating: GravatarRatings = .default) -> URL? { - let hash = gravatarHash(of: email) - let targetURL = String(format: "%@/%@?d=%@&s=%d&r=%@", - Defaults.baseURL, - hash, - defaultImage?.rawValue ?? GravatarDefaultImage.fileNotFound.rawValue, - size ?? Defaults.imageSize, - rating.stringValue()) - return URL(string: targetURL) - } - - /// Returns the gravatar hash of an email - /// - /// - Parameter email: the email associated with the gravatar - /// - Returns: hashed email - /// - /// This really ought to be in a different place, like Gravatar.swift, but there's - /// lots of duplication around gravatars -nh - private static func gravatarHash(of email: String) -> String { - return email - .lowercased() - .trimmingCharacters(in: .whitespaces) - .sha256Hash() - } -} - -@available(*, deprecated, message: "Usage of the deprecated type: Gravatar.") -extension Gravatar: Equatable {} - -@available(*, deprecated, message: "Usage of the deprecated type: Gravatar.") -public func ==(lhs: Gravatar, rhs: Gravatar) -> Bool { - return lhs.canonicalURL == rhs.canonicalURL -} - -@available(*, deprecated, message: "Usage of the deprecated type: Gravatar.") -public extension Gravatar { - @available(*, deprecated, message: "Usage of the deprecated type: Gravatar.") - init?(_ url: URL) { - guard Gravatar.isGravatarURL(url) else { - return nil - } - - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return nil - } - - components.scheme = Defaults.scheme - components.host = Defaults.host - components.query = nil - - // Treat unknown@gravatar.com as a nil url - guard url.lastPathComponent != Defaults.unknownHash else { - return nil - } - - guard let sanitizedURL = components.url else { - return nil - } - - self.canonicalURL = sanitizedURL - } -} diff --git a/Modules/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift b/Modules/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift index 8fc7bebe06f7..4a069c5c8116 100644 --- a/Modules/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift +++ b/Modules/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift @@ -21,37 +21,6 @@ private class GravatarNotificationWrapper { /// UIImageView Helper Methods that allow us to download a Gravatar, given the User's Email /// extension UIImageView { - - /// Downloads and sets the User's Gravatar, given his email. - /// TODO: This is a convenience method. Please, remove once all of the code has been migrated over to Swift. - /// - /// - Parameters: - /// - email: the user's email - /// - rating: expected image rating - /// - /// This method uses deprecated types. Please check the deprecation warning in `GravatarRatings`. Also check out the UIImageView extension from the Gravatar iOS SDK as an alternative to download images. See: https://github.com/Automattic/Gravatar-SDK-iOS. - @available(*, deprecated, message: "Usage of the deprecated type: GravatarRatings.") - @objc - public func downloadGravatarWithEmail(_ email: String, rating: GravatarRatings) { - downloadGravatarWithEmail(email, rating: rating, placeholderImage: .gravatarPlaceholderImage) - } - - /// Downloads and sets the User's Gravatar, given his email. - /// - /// - Parameters: - /// - email: the user's email - /// - rating: expected image rating - /// - placeholderImage: Image to be used as Placeholder - /// This method uses deprecated types. Please check the deprecation warning in `GravatarRatings`. Also check out the UIImageView extension from the Gravatar iOS SDK as an alternative to download images. See: https://github.com/Automattic/Gravatar-SDK-iOS. - @available(*, deprecated, message: "Usage of the deprecated type: GravatarRatings.") - @objc - public func downloadGravatarWithEmail(_ email: String, rating: GravatarRatings = .default, placeholderImage: UIImage = .gravatarPlaceholderImage) { - let gravatarURL = Gravatar.gravatarUrl(for: email, size: gravatarDefaultSize(), rating: rating) - - listenForGravatarChanges(forEmail: email) - downloadImage(from: gravatarURL, placeholderImage: placeholderImage) - } - /// Configures the UIImageView to listen for changes to the gravatar it is displaying public func listenForGravatarChanges(forEmail trackedEmail: String) { if let currentObersver = gravatarWrapper?.observer { @@ -83,76 +52,6 @@ extension UIImageView { } } - /// Downloads the provided Gravatar. - /// - /// - Parameters: - /// - gravatar: the user's Gravatar - /// - placeholder: Image to be used as Placeholder - /// - animate: enable/disable fade in animation - /// - failure: Callback block to be invoked when an error occurs while fetching the Gravatar image - /// - /// This method uses deprecated types. Please check the deprecation warning in `GravatarRatings`. Also check out the UIImageView extension from the Gravatar iOS SDK as an alternative to download images. See: https://github.com/Automattic/Gravatar-SDK-iOS. - @available(*, deprecated, message: "Usage of the deprecated type: Gravatar.") - public func downloadGravatar(_ gravatar: Gravatar?, placeholder: UIImage, animate: Bool, failure: ((Error?) -> Void)? = nil) { - guard let gravatar = gravatar else { - self.image = placeholder - return - } - - // Starting with iOS 10, it seems `initWithCoder` uses a default size - // of 1000x1000, which was messing with our size calculations for gravatars - // on newly created table cells. - // Calling `layoutIfNeeded()` forces UIKit to calculate the actual size. - layoutIfNeeded() - - let size = Int(ceil(frame.width * UIScreen.main.scale)) - let url = gravatar.urlWithSize(size) - - self.downloadImage(from: url, - placeholderImage: placeholder, - success: { image in - guard image != self.image else { - return - } - - self.image = image - if animate { - self.fadeInAnimation() - } - }, failure: { error in - failure?(error) - }) - } - - /// Sets an Image Override in both, AFNetworking's Private Cache + NSURLCache - /// - /// - Parameters: - /// - image: new UIImage - /// - rating: rating for the new image. - /// - email: associated email of the new gravatar - /// - Note: You may want to use `updateGravatar(image:, email:)` instead - /// - /// *WHY* is this required?. *WHY* life has to be so complicated?, is the universe against us? - /// This has been implemented as a workaround. During Upload, we want any async calls made to the - /// `downloadGravatar` API to return the "Fresh" image. - /// - /// Note II: - /// We cannot just clear NSURLCache, since the helper that's supposed to do that, is broken since iOS 8. - /// Ref: Ref: http://blog.airsource.co.uk/2014/10/11/nsurlcache-ios8-broken/ - /// - /// P.s.: - /// Hope buddah, and the code reviewer, can forgive me for this hack. - /// - @available(*, deprecated, message: "Usage of the deprecated type: GravatarRatings.") - @objc public func overrideGravatarImageCache(_ image: UIImage, rating: GravatarRatings, email: String) { - guard let gravatarURL = Gravatar.gravatarUrl(for: email, size: gravatarDefaultSize(), rating: rating) else { - return - } - - listenForGravatarChanges(forEmail: email) - overrideImageCache(for: gravatarURL, with: image) - } - /// Updates the gravatar image for the given email, and notifies all gravatar image views /// /// - Parameters: diff --git a/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h b/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h deleted file mode 100644 index 14b2cbb948c5..000000000000 --- a/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@interface NSString (Gravatar) - -- (NSString *)sha256Hash; - -@end diff --git a/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m b/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m deleted file mode 100644 index 0687ccca3d42..000000000000 --- a/Modules/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m +++ /dev/null @@ -1,21 +0,0 @@ -#import "NSString+Gravatar.h" -#import - - -@implementation NSString (Gravatar) - -- (NSString *)sha256Hash -{ - const char *cStr = [self UTF8String]; - unsigned char result[CC_SHA256_DIGEST_LENGTH]; - - CC_SHA256(cStr, (CC_LONG)strlen(cStr), result); - - NSMutableString *hashString = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH*2]; - for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { - [hashString appendFormat:@"%02x",result[i]]; - } - return hashString; -} - -@end diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 769b1947b39a..9f931bb5bd95 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2608,6 +2608,7 @@ 912347762216E27200BD9F97 /* GutenbergViewController+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912347752216E27200BD9F97 /* GutenbergViewController+Localization.swift */; }; 91B0535D2B726F810073455C /* GravatarInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B0535C2B726F810073455C /* GravatarInfoRow.swift */; }; 91B0535E2B726F810073455C /* GravatarInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B0535C2B726F810073455C /* GravatarInfoRow.swift */; }; + 91BE834E2C48FF0F00BB5B3B /* UIImageView+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91BE834D2C48FF0F00BB5B3B /* UIImageView+Additions.swift */; }; 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */; }; 91DCE84621A6A7F50062F134 /* PostEditor+MoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91DCE84521A6A7F50062F134 /* PostEditor+MoreOptions.swift */; }; 91DCE84821A6C58C0062F134 /* PostEditor+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91DCE84721A6C58C0062F134 /* PostEditor+Publish.swift */; }; @@ -8268,6 +8269,7 @@ 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 = ""; }; 91B0535C2B726F810073455C /* GravatarInfoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GravatarInfoRow.swift; sourceTree = ""; }; + 91BE834D2C48FF0F00BB5B3B /* UIImageView+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Additions.swift"; sourceTree = ""; }; 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergMediaPickerHelper.swift; sourceTree = ""; }; 91DCE84521A6A7F50062F134 /* PostEditor+MoreOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostEditor+MoreOptions.swift"; sourceTree = ""; }; 91DCE84721A6C58C0062F134 /* PostEditor+Publish.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PostEditor+Publish.swift"; sourceTree = ""; }; @@ -13014,6 +13016,7 @@ 4AD954542C2145CB00D0EEFA /* Login2FAViewController.swift */, 4AD954552C2145CB00D0EEFA /* LoginEmailViewController.swift */, 4AD954562C2145CB00D0EEFA /* LoginLinkRequestViewController.swift */, + 91BE834D2C48FF0F00BB5B3B /* UIImageView+Additions.swift */, 4AD954572C2145CB00D0EEFA /* LoginNavigationController.swift */, 4AD954582C2145CB00D0EEFA /* LoginPrologueLoginMethodViewController.swift */, 4AD954592C2145CB00D0EEFA /* LoginProloguePageViewController.swift */, @@ -23708,6 +23711,7 @@ 4AD955172C2145CB00D0EEFA /* LoginLinkRequestViewController.swift in Sources */, 4AD954CC2C2145CB00D0EEFA /* IDToken.swift in Sources */, 4AD9550C2C2145CB00D0EEFA /* WordPressComAccountService.swift in Sources */, + 91BE834E2C48FF0F00BB5B3B /* UIImageView+Additions.swift in Sources */, 4AD9550E2C2145CB00D0EEFA /* WordPressComOAuthClientFacade.swift in Sources */, 4AD954CD2C2145CB00D0EEFA /* JSONWebToken.swift in Sources */, 4AD954EB2C2145CB00D0EEFA /* NUXKeyboardResponder.swift in Sources */, diff --git a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift index c80584929aaf..df6bc5f6f30d 100644 --- a/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift +++ b/WordPressAuthenticator/Sources/Signin/LoginLinkRequestViewController.swift @@ -1,6 +1,7 @@ import UIKit import WordPressShared import WordPressUI +import GravatarUI /// Step one in the auth link flow. This VC displays a form to request a "magic" /// authentication link be emailed to the user. Allows the user to signin via @@ -36,7 +37,9 @@ class LoginLinkRequestViewController: LoginViewController { let email = loginFields.username if email.isValidEmail() { - gravatarView?.downloadGravatarWithEmail(email, rating: .x) + Task { + try await gravatarView?.setGravatarImage(with: email, rating: .x) + } } else { gravatarView?.isHidden = true } diff --git a/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift new file mode 100644 index 000000000000..7c583309caa7 --- /dev/null +++ b/WordPressAuthenticator/Sources/Signin/UIImageView+Additions.swift @@ -0,0 +1,22 @@ +import UIKit +import WordPressUI +import GravatarUI + +extension UIImageView { + func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil) async throws { + var options: [ImageSettingOption]? + if let cache = WordPressUI.ImageCache.shared as? Gravatar.ImageCaching { + options = [.imageCache(cache)] + } + else { + assertionFailure("WordPressUI.ImageCache.shared should conform to Gravatar.ImageCaching") + } + listenForGravatarChanges(forEmail: email) + try await gravatar.setImage(avatarID: .email(email), + placeholder: placeholder, + rating: .x, + preferredSize: preferredSize, + 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 f95c71ddac51..179ce951b07f 100644 --- a/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift +++ b/WordPressAuthenticator/Sources/Unified Auth/View Related/Reusable Views/GravatarEmailTableViewCell.swift @@ -37,7 +37,9 @@ class GravatarEmailTableViewCell: UITableViewCell { return } - gravatarImageView?.downloadGravatarWithEmail(email, placeholderImage: placeholderImage ?? gridicon) + Task { + try await gravatarImageView?.setGravatarImage(with: email, placeholder: placeholderImage ?? gridicon, preferredSize: gridicon.size) + } gravatarImageViewSizeConstraints.forEach { constraint in constraint.constant = gridicon.size.width