From 6d6d45b2833716e39c43c2a7d4054bf5baa5e14b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Aug 2022 10:35:52 +1000 Subject: [PATCH] Updated the ProfilePictureView to only use YYImage for Gif and WebP images Added support for animated OpenGroup images --- Session/Shared/FullConversationCell.swift | 6 +- .../SimplifiedConversationCell.swift | 2 +- SessionUtilitiesKit/Media/Data+Image.swift | 5 + SessionUtilitiesKit/Media/ImageFormat.swift | 1 + SessionUtilitiesKit/Media/NSData+Image.m | 2 +- .../Profile Pictures/ProfilePictureView.swift | 193 +++++++++++++----- 6 files changed, 156 insertions(+), 53 deletions(-) diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index ebf55e4c2..14faf6618 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -240,7 +240,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil) ) @@ -280,7 +280,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil) ) @@ -341,7 +341,7 @@ public final class FullConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: ( cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 9bea5687a..852379361 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -94,7 +94,7 @@ final class SimplifiedConversationCell: UITableViewCell { profile: cellViewModel.profile, additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePicture: cellViewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil), showMultiAvatarForClosedGroup: true ) diff --git a/SessionUtilitiesKit/Media/Data+Image.swift b/SessionUtilitiesKit/Media/Data+Image.swift index 57adb8a4d..429ed9d5f 100644 --- a/SessionUtilitiesKit/Media/Data+Image.swift +++ b/SessionUtilitiesKit/Media/Data+Image.swift @@ -34,6 +34,8 @@ public extension Data { case (0x42, 0x4d): return .bmp case (0x4D, 0x4D): return .tiff // Motorola byte order TIFF case (0x49, 0x49): return .tiff // Intel byte order TIFF + case (0x52, 0x49): return .webp // First two letters of WebP + default: return .unknown } } @@ -113,6 +115,9 @@ public extension Data { mimeType == OWSMimeTypeImageBmp1 || mimeType == OWSMimeTypeImageBmp2 ) + + case .webp: + return (mimeType == nil || mimeType == OWSMimeTypeImageWebp) } } diff --git a/SessionUtilitiesKit/Media/ImageFormat.swift b/SessionUtilitiesKit/Media/ImageFormat.swift index e31f408c8..1db97f1df 100644 --- a/SessionUtilitiesKit/Media/ImageFormat.swift +++ b/SessionUtilitiesKit/Media/ImageFormat.swift @@ -9,4 +9,5 @@ public enum ImageFormat { case tiff case jpeg case bmp + case webp } diff --git a/SessionUtilitiesKit/Media/NSData+Image.m b/SessionUtilitiesKit/Media/NSData+Image.m index 5af4610cd..fb13b9d20 100644 --- a/SessionUtilitiesKit/Media/NSData+Image.m +++ b/SessionUtilitiesKit/Media/NSData+Image.m @@ -328,7 +328,7 @@ typedef struct { // Intel byte order TIFF return ImageFormat_Tiff; } else if (byte0 == 0x52 && byte1 == 0x49) { - // First two letters of RIFF tag. + // First two letters of WebP tag. return ImageFormat_Webp; } diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift index 5dd308a11..bb8b477b1 100644 --- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift +++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift @@ -19,8 +19,61 @@ public final class ProfilePictureView: UIView { // MARK: - Components - private lazy var imageView = getImageView() - private lazy var additionalImageView = getImageView() + private lazy var imageContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.clipsToBounds = true + result.backgroundColor = Colors.unimportant + + return result + }() + + private lazy var imageView: UIImageView = { + let result: UIImageView = UIImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var animatedImageView: YYAnimatedImageView = { + let result: YYAnimatedImageView = YYAnimatedImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var additionalImageContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.clipsToBounds = true + result.backgroundColor = Colors.unimportant + result.layer.cornerRadius = (Values.smallProfilePictureSize / 2) + result.isHidden = true + + return result + }() + + private lazy var additionalImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() + + private lazy var additionalAnimatedImageView: YYAnimatedImageView = { + let result: YYAnimatedImageView = YYAnimatedImageView() + result.translatesAutoresizingMaskIntoConstraints = false + result.contentMode = .scaleAspectFill + result.isHidden = true + + return result + }() // MARK: - Lifecycle @@ -35,27 +88,33 @@ public final class ProfilePictureView: UIView { } private func setUpViewHierarchy() { - // Set up image view - addSubview(imageView) - imageView.pin(.leading, to: .leading, of: self) - imageView.pin(.top, to: .top, of: self) - let imageViewSize = CGFloat(Values.mediumProfilePictureSize) - imageViewWidthConstraint = imageView.set(.width, to: imageViewSize) - imageViewHeightConstraint = imageView.set(.height, to: imageViewSize) - - // Set up additional image view - addSubview(additionalImageView) - additionalImageView.pin(.trailing, to: .trailing, of: self) - additionalImageView.pin(.bottom, to: .bottom, of: self) - let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize) - additionalImageViewWidthConstraint = additionalImageView.set(.width, to: additionalImageViewSize) - additionalImageViewHeightConstraint = additionalImageView.set(.height, to: additionalImageViewSize) - additionalImageView.layer.cornerRadius = additionalImageViewSize / 2 + + addSubview(imageContainerView) + addSubview(additionalImageContainerView) + + imageContainerView.pin(.leading, to: .leading, of: self) + imageContainerView.pin(.top, to: .top, of: self) + imageViewWidthConstraint = imageContainerView.set(.width, to: imageViewSize) + imageViewHeightConstraint = imageContainerView.set(.height, to: imageViewSize) + additionalImageContainerView.pin(.trailing, to: .trailing, of: self) + additionalImageContainerView.pin(.bottom, to: .bottom, of: self) + additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: additionalImageViewSize) + additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: additionalImageViewSize) + + imageContainerView.addSubview(imageView) + imageContainerView.addSubview(animatedImageView) + additionalImageContainerView.addSubview(additionalImageView) + additionalImageContainerView.addSubview(additionalAnimatedImageView) + + imageView.pin(to: imageContainerView) + animatedImageView.pin(to: imageContainerView) + additionalImageView.pin(to: additionalImageContainerView) + additionalAnimatedImageView.pin(to: additionalImageContainerView) } - // FIXME: Remove this once we refactor the ConversationVC to Swift (use the HomeViewModel approach) + // FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach) @objc(updateForThreadId:) public func update(forThreadId threadId: String?) { guard @@ -74,7 +133,7 @@ public final class ProfilePictureView: UIView { profile: viewModel.profile, additionalProfile: viewModel.additionalProfile, threadVariant: viewModel.threadVariant, - openGroupProfilePicture: viewModel.openGroupProfilePictureData.map { UIImage(data: $0) }, + openGroupProfilePictureData: viewModel.openGroupProfilePictureData, useFallbackPicture: ( viewModel.threadVariant == .openGroup && viewModel.openGroupProfilePictureData == nil @@ -88,7 +147,7 @@ public final class ProfilePictureView: UIView { profile: Profile? = nil, additionalProfile: Profile? = nil, threadVariant: SessionThread.Variant, - openGroupProfilePicture: UIImage? = nil, + openGroupProfilePictureData: Data? = nil, useFallbackPicture: Bool = false, showMultiAvatarForClosedGroup: Bool = false ) { @@ -101,20 +160,38 @@ public final class ProfilePictureView: UIView { } imageView.contentMode = .center - imageView.backgroundColor = UIColor(rgbHex: 0x353535) - imageView.layer.cornerRadius = (self.size / 2) + imageView.isHidden = false + animatedImageView.isHidden = true + imageContainerView.backgroundColor = UIColor(rgbHex: 0x353535) + imageContainerView.layer.cornerRadius = (self.size / 2) imageViewWidthConstraint.constant = self.size imageViewHeightConstraint.constant = self.size - additionalImageView.isHidden = true + additionalImageContainerView.isHidden = true + animatedImageView.image = nil additionalImageView.image = nil - additionalImageView.layer.cornerRadius = (self.size / 2) + additionalAnimatedImageView.image = nil + additionalImageView.isHidden = true + additionalAnimatedImageView.isHidden = true return } - guard !publicKey.isEmpty || openGroupProfilePicture != nil else { return } + guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return } - func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage, isTappable: Bool) { - if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile), let image: YYImage = YYImage(data: profileData) { - return (image, true) + func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage?, animatedImage: YYImage?, isTappable: Bool) { + if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) { + let format: ImageFormat = profileData.guessedImageFormat + + let image: UIImage? = (format == .gif || format == .webp ? + nil : + UIImage(data: profileData) + ) + let animatedImage: YYImage? = (format != .gif && format != .webp ? + nil : + YYImage(data: profileData) + ) + + if image != nil || animatedImage != nil { + return (image, animatedImage, true) + } } return ( @@ -124,6 +201,7 @@ public final class ProfilePictureView: UIView { .defaulting(to: publicKey), size: size ), + nil, false ) } @@ -147,56 +225,75 @@ public final class ProfilePictureView: UIView { imageViewHeightConstraint.constant = targetSize additionalImageViewWidthConstraint.constant = targetSize additionalImageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = false + additionalImageContainerView.isHidden = false if let additionalProfile: Profile = additionalProfile { - additionalImageView.image = getProfilePicture( + let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture( of: targetSize, for: additionalProfile.id, profile: additionalProfile - ).image + ) + + // Set the images and show the appropriate imageView (non-animated should be + // visible if there is no image) + additionalImageView.image = image + additionalAnimatedImageView.image = animatedImage + additionalImageView.isHidden = (animatedImage != nil) + additionalAnimatedImageView.isHidden = (animatedImage == nil) } default: targetSize = self.size imageViewWidthConstraint.constant = targetSize imageViewHeightConstraint.constant = targetSize - additionalImageView.isHidden = true + additionalImageContainerView.isHidden = true additionalImageView.image = nil + additionalImageView.isHidden = true + additionalAnimatedImageView.image = nil + additionalAnimatedImageView.isHidden = true } // Set the image - if let openGroupProfilePicture: UIImage = openGroupProfilePicture { - imageView.image = openGroupProfilePicture + if let openGroupProfilePictureData: Data = openGroupProfilePictureData { + let format: ImageFormat = openGroupProfilePictureData.guessedImageFormat + + let image: UIImage? = (format == .gif || format == .webp ? + nil : + UIImage(data: openGroupProfilePictureData) + ) + let animatedImage: YYImage? = (format != .gif && format != .webp ? + nil : + YYImage(data: openGroupProfilePictureData) + ) + + imageView.image = image + animatedImageView.image = animatedImage + imageView.isHidden = (animatedImage != nil) + animatedImageView.isHidden = (animatedImage == nil) hasTappableProfilePicture = true } else { - let (image, isTappable): (UIImage, Bool) = getProfilePicture( + let (image, animatedImage, isTappable): (UIImage?, YYImage?, Bool) = getProfilePicture( of: targetSize, for: publicKey, profile: profile ) imageView.image = image + animatedImageView.image = animatedImage + imageView.isHidden = (animatedImage != nil) + animatedImageView.isHidden = (animatedImage == nil) hasTappableProfilePicture = isTappable } imageView.contentMode = .scaleAspectFill - imageView.backgroundColor = Colors.unimportant - imageView.layer.cornerRadius = (targetSize / 2) - additionalImageView.layer.cornerRadius = (targetSize / 2) + animatedImageView.contentMode = .scaleAspectFill + imageContainerView.backgroundColor = Colors.unimportant + imageContainerView.layer.cornerRadius = (targetSize / 2) + additionalImageContainerView.layer.cornerRadius = (targetSize / 2) } // MARK: - Convenience - private func getImageView() -> YYAnimatedImageView { - let result = YYAnimatedImageView() - result.layer.masksToBounds = true - result.backgroundColor = Colors.unimportant - result.contentMode = .scaleAspectFill - - return result - } - @objc public func getProfilePicture() -> UIImage? { return (hasTappableProfilePicture ? imageView.image : nil) }