From 06b0b764f894effc9edb831612da42ba335fc2e1 Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 27 Jun 2024 10:36:22 -0400 Subject: [PATCH] Revert "Remove unused Gravatar" This reverts commit 5f04d80aa2d7c925dc42dc1f3ba96acef3d4606d. --- .../Extensions/Gravatar/Gravatar.swift | 148 +++++++++++++ .../Extensions/UIImageView+Gravatar.swift | 194 ++++++++++++++++++ .../Extensions/UIImageView+Networking.swift | 4 + .../Extensions/NSString+Gravatar.h | 7 + .../Extensions/NSString+Gravatar.m | 21 ++ .../include/NSString+Gravatar.h | 1 + 6 files changed, 375 insertions(+) create mode 100644 Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift create mode 100644 Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift create mode 100644 Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h create mode 100644 Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m create mode 120000 Sources/WordPressUIObjC/include/NSString+Gravatar.h diff --git a/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift b/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift new file mode 100644 index 0000000..c266524 --- /dev/null +++ b/Sources/WordPressUI/Extensions/Gravatar/Gravatar.swift @@ -0,0 +1,148 @@ +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/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift b/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift new file mode 100644 index 0000000..8fc7beb --- /dev/null +++ b/Sources/WordPressUI/Extensions/UIImageView+Gravatar.swift @@ -0,0 +1,194 @@ +import Foundation +import UIKit + +#if SWIFT_PACKAGE +import WordPressUIObjC +#endif + +/// Wrapper class used to ensure removeObserver is called +private class GravatarNotificationWrapper { + let observer: NSObjectProtocol + + init(observer: NSObjectProtocol) { + self.observer = observer + } + + deinit { + NotificationCenter.default.removeObserver(observer) + } +} + +/// 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 { + NotificationCenter.default.removeObserver(currentObersver) + gravatarWrapper = nil + } + + let observer = NotificationCenter.default.addObserver(forName: .GravatarImageUpdateNotification, object: nil, queue: nil) { [weak self] (notification) in + guard let userInfo = notification.userInfo, + let email = userInfo[Defaults.emailKey] as? String, + email == trackedEmail, + let image = userInfo[Defaults.imageKey] as? UIImage else { + return + } + + self?.image = image + } + gravatarWrapper = GravatarNotificationWrapper(observer: observer) + } + + /// Stores the gravatar observer + /// + fileprivate var gravatarWrapper: GravatarNotificationWrapper? { + get { + return objc_getAssociatedObject(self, &Defaults.gravatarWrapperKey) as? GravatarNotificationWrapper + } + set { + objc_setAssociatedObject(self, &Defaults.gravatarWrapperKey, newValue as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + /// 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: + /// - image: the new UIImage + /// - email: associated email of the new gravatar + @objc public func updateGravatar(image: UIImage, email: String?) { + self.image = image + guard let email = email else { + return + } + NotificationCenter.default.post(name: .GravatarImageUpdateNotification, object: self, userInfo: [Defaults.emailKey: email, Defaults.imageKey: image]) + } + + // MARK: - Private Helpers + + /// Returns the required gravatar size. If the current view's size is zero, falls back to the default size. + /// + private func gravatarDefaultSize() -> Int { + guard bounds.size.equalTo(.zero) == false else { + return Defaults.imageSize + } + + let targetSize = max(bounds.width, bounds.height) * UIScreen.main.scale + return Int(targetSize) + } + + /// Private helper structure: contains the default Gravatar parameters + /// + private struct Defaults { + static let imageSize = 80 + static var gravatarWrapperKey = 0x1000 + static let emailKey = "email" + static let imageKey = "image" + } +} + +public extension NSNotification.Name { + static let GravatarImageUpdateNotification = NSNotification.Name(rawValue: "GravatarImageUpdateNotification") +} diff --git a/Sources/WordPressUI/Extensions/UIImageView+Networking.swift b/Sources/WordPressUI/Extensions/UIImageView+Networking.swift index c04955e..010c4d6 100644 --- a/Sources/WordPressUI/Extensions/UIImageView+Networking.swift +++ b/Sources/WordPressUI/Extensions/UIImageView+Networking.swift @@ -1,6 +1,10 @@ import Foundation import UIKit +#if SWIFT_PACKAGE +import WordPressUIObjC +#endif + public extension UIImageView { enum ImageDownloadError: Error { case noURLSpecifiedInRequest diff --git a/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h b/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h new file mode 100644 index 0000000..14b2cbb --- /dev/null +++ b/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.h @@ -0,0 +1,7 @@ +#import + +@interface NSString (Gravatar) + +- (NSString *)sha256Hash; + +@end diff --git a/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m b/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m new file mode 100644 index 0000000..0687ccc --- /dev/null +++ b/Sources/WordPressUIObjC/Extensions/NSString+Gravatar.m @@ -0,0 +1,21 @@ +#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/Sources/WordPressUIObjC/include/NSString+Gravatar.h b/Sources/WordPressUIObjC/include/NSString+Gravatar.h new file mode 120000 index 0000000..4518122 --- /dev/null +++ b/Sources/WordPressUIObjC/include/NSString+Gravatar.h @@ -0,0 +1 @@ +../Extensions/NSString+Gravatar.h \ No newline at end of file