mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			304 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			304 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import UIKit
 | |
| import GRDB
 | |
| import YYImage
 | |
| import SessionUIKit
 | |
| import SessionMessagingKit
 | |
| 
 | |
| public final class ProfilePictureView: UIView {
 | |
|     public var size: CGFloat = 0
 | |
|     
 | |
|     // Constraints
 | |
|     private var imageViewWidthConstraint: NSLayoutConstraint!
 | |
|     private var imageViewHeightConstraint: NSLayoutConstraint!
 | |
|     private var additionalImageViewWidthConstraint: NSLayoutConstraint!
 | |
|     private var additionalImageViewHeightConstraint: NSLayoutConstraint!
 | |
|     
 | |
|     // MARK: - Components
 | |
|     
 | |
|     private lazy var imageContainerView: UIView = {
 | |
|         let result: UIView = UIView()
 | |
|         result.translatesAutoresizingMaskIntoConstraints = false
 | |
|         result.clipsToBounds = true
 | |
|         result.themeBackgroundColor = .backgroundSecondary
 | |
|         
 | |
|         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.themeBackgroundColor = .primary
 | |
|         result.themeBorderColor = .backgroundPrimary
 | |
|         result.layer.borderWidth = 1
 | |
|         result.layer.cornerRadius = (Values.smallProfilePictureSize / 2)
 | |
|         result.isHidden = true
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var additionalProfilePlaceholderImageView: UIImageView = {
 | |
|         let result: UIImageView = UIImageView(
 | |
|             image: UIImage(systemName: "person.fill")?.withRenderingMode(.alwaysTemplate)
 | |
|         )
 | |
|         result.translatesAutoresizingMaskIntoConstraints = false
 | |
|         result.contentMode = .scaleAspectFill
 | |
|         result.themeTintColor = .textPrimary
 | |
|         result.isHidden = true
 | |
|         
 | |
|         return result
 | |
|     }()
 | |
|     
 | |
|     private lazy var additionalImageView: UIImageView = {
 | |
|         let result: UIImageView = UIImageView()
 | |
|         result.translatesAutoresizingMaskIntoConstraints = false
 | |
|         result.contentMode = .scaleAspectFill
 | |
|         result.themeTintColor = .textPrimary
 | |
|         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
 | |
|     
 | |
|     public override init(frame: CGRect) {
 | |
|         super.init(frame: frame)
 | |
|         setUpViewHierarchy()
 | |
|     }
 | |
|     
 | |
|     public required init?(coder: NSCoder) {
 | |
|         super.init(coder: coder)
 | |
|         setUpViewHierarchy()
 | |
|     }
 | |
|     
 | |
|     private func setUpViewHierarchy() {
 | |
|         let imageViewSize = CGFloat(Values.mediumProfilePictureSize)
 | |
|         let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize)
 | |
|         
 | |
|         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)
 | |
|         additionalImageContainerView.addSubview(additionalProfilePlaceholderImageView)
 | |
|         
 | |
|         imageView.pin(to: imageContainerView)
 | |
|         animatedImageView.pin(to: imageContainerView)
 | |
|         additionalImageView.pin(to: additionalImageContainerView)
 | |
|         additionalAnimatedImageView.pin(to: additionalImageContainerView)
 | |
|         
 | |
|         additionalProfilePlaceholderImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 3)
 | |
|         additionalProfilePlaceholderImageView.pin(.left, to: .left, of: additionalImageContainerView)
 | |
|         additionalProfilePlaceholderImageView.pin(.right, to: .right, of: additionalImageContainerView)
 | |
|         additionalProfilePlaceholderImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 5)
 | |
|     }
 | |
|     
 | |
|     private func prepareForReuse() {
 | |
|         imageView.contentMode = .scaleAspectFill
 | |
|         imageView.isHidden = true
 | |
|         animatedImageView.contentMode = .scaleAspectFill
 | |
|         animatedImageView.isHidden = true
 | |
|         imageContainerView.themeBackgroundColor = .backgroundSecondary
 | |
|         additionalImageContainerView.isHidden = true
 | |
|         animatedImageView.image = nil
 | |
|         additionalImageView.image = nil
 | |
|         additionalAnimatedImageView.image = nil
 | |
|         additionalImageView.isHidden = true
 | |
|         additionalAnimatedImageView.isHidden = true
 | |
|         additionalProfilePlaceholderImageView.isHidden = true
 | |
|     }
 | |
|     
 | |
|     private func getProfilePicture(
 | |
|         of size: CGFloat,
 | |
|         for publicKey: String,
 | |
|         profile: Profile?,
 | |
|         threadVariant: SessionThread.Variant
 | |
|     ) -> (image: UIImage?, animatedImage: YYImage?) {
 | |
|         guard let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) else {
 | |
|             return (
 | |
|                 Identicon.generatePlaceholderIcon(
 | |
|                     seed: publicKey,
 | |
|                     text: (profile?.displayName(for: threadVariant))
 | |
|                         .defaulting(to: publicKey),
 | |
|                     size: size
 | |
|                 ),
 | |
|                 nil
 | |
|             )
 | |
|         }
 | |
|         
 | |
|         switch profileData.guessedImageFormat {
 | |
|             case .gif, .webp: return (nil, YYImage(data: profileData))
 | |
|             default: return (UIImage(data: profileData), nil)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public func update(
 | |
|         publicKey: String,
 | |
|         threadVariant: SessionThread.Variant,
 | |
|         customImageData: Data?,
 | |
|         profile: Profile?,
 | |
|         additionalProfile: Profile?
 | |
|     ) {
 | |
|         prepareForReuse()
 | |
|         
 | |
|         // If we are given 'customImageData' then only use that
 | |
|         if let customImageData: Data = customImageData {
 | |
|             switch customImageData.guessedImageFormat {
 | |
|                 case .gif, .webp:
 | |
|                     animatedImageView.image = YYImage(data: customImageData)
 | |
|                     animatedImageView.isHidden = false
 | |
|                     
 | |
|                 default:
 | |
|                     imageView.image = UIImage(data: customImageData)
 | |
|                     imageView.isHidden = false
 | |
|             }
 | |
|             
 | |
|             imageViewWidthConstraint.constant = self.size
 | |
|             imageViewHeightConstraint.constant = self.size
 | |
|             imageContainerView.layer.cornerRadius = (self.size / 2)
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         // Otherwise there are conversation-type-specific behaviours
 | |
|         switch threadVariant {
 | |
|             case .community:
 | |
|                 switch self.size {
 | |
|                     case Values.smallProfilePictureSize..<Values.mediumProfilePictureSize:
 | |
|                         imageView.image = #imageLiteral(resourceName: "SessionWhite16")
 | |
|                         
 | |
|                     case Values.mediumProfilePictureSize..<Values.largeProfilePictureSize:
 | |
|                         imageView.image = #imageLiteral(resourceName: "SessionWhite24")
 | |
|                         
 | |
|                     default: imageView.image = #imageLiteral(resourceName: "SessionWhite40")
 | |
|                 }
 | |
|                 
 | |
|                 imageView.contentMode = .center
 | |
|                 imageView.isHidden = false
 | |
|                 imageContainerView.themeBackgroundColorForced = .theme(.classicDark, color: .borderSeparator)
 | |
|                 imageViewWidthConstraint.constant = self.size
 | |
|                 imageViewHeightConstraint.constant = self.size
 | |
|                 imageContainerView.layer.cornerRadius = (self.size / 2)
 | |
|                 
 | |
|             case .legacyGroup, .group:
 | |
|                 guard !publicKey.isEmpty else { return }
 | |
|                 
 | |
|                 // If the `publicKey` we were given matches the first profile id then we have
 | |
|                 // provided a "ClosedGroupProfile" (which is essentially a profile object populated
 | |
|                 // with `ClosedGroup` data) so we don't want to add the 'additionalProfile' content
 | |
|                 let isCustomGroupImage: Bool = (publicKey == profile?.id)
 | |
|                 
 | |
|                 let targetSize: CGFloat = {
 | |
|                     guard !isCustomGroupImage else { return self.size }
 | |
|                     
 | |
|                     switch self.size {
 | |
|                         case 40: return 32
 | |
|                         case 80: return 64
 | |
|                         case Values.largeProfilePictureSize: return 56
 | |
|                         default: return Values.smallProfilePictureSize
 | |
|                     }
 | |
|                 }()
 | |
|                 
 | |
|                 // Set the content for the first `profile` object
 | |
|                 let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
 | |
|                     of: targetSize,
 | |
|                     for: publicKey,
 | |
|                     profile: profile,
 | |
|                     threadVariant: threadVariant
 | |
|                 )
 | |
|                 imageView.image = image
 | |
|                 imageView.isHidden = (animatedImage != nil)
 | |
|                 animatedImageView.image = animatedImage
 | |
|                 animatedImageView.isHidden = (animatedImage == nil)
 | |
|                 imageViewWidthConstraint.constant = targetSize
 | |
|                 imageViewHeightConstraint.constant = targetSize
 | |
|                 imageContainerView.layer.cornerRadius = (targetSize / 2)
 | |
|                 
 | |
|                 // If the `publicKey` we were given matches the first profile id then we have
 | |
|                 // provided a "ClosedGroupProfile" (which is essentially a profile object populated
 | |
|                 // with `ClosedGroup` data) so we don't want to add the 'additionalProfile' content
 | |
|                 guard !isCustomGroupImage else { return }
 | |
|                 
 | |
|                 additionalImageViewWidthConstraint.constant = targetSize
 | |
|                 additionalImageViewHeightConstraint.constant = targetSize
 | |
|                 additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
 | |
|                 additionalImageContainerView.isHidden = false
 | |
|                 
 | |
|                 if let additionalProfile: Profile = additionalProfile {
 | |
|                     let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
 | |
|                         of: targetSize,
 | |
|                         for: additionalProfile.id,
 | |
|                         profile: additionalProfile,
 | |
|                         threadVariant: threadVariant
 | |
|                     )
 | |
|                     
 | |
|                     // 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)
 | |
|                 }
 | |
|                 else {
 | |
|                     additionalProfilePlaceholderImageView.isHidden = false
 | |
|                 }
 | |
|                 
 | |
|             case .contact:
 | |
|                 guard !publicKey.isEmpty else { return }
 | |
|                 
 | |
|                 let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
 | |
|                     of: self.size,
 | |
|                     for: publicKey,
 | |
|                     profile: profile,
 | |
|                     threadVariant: threadVariant
 | |
|                 )
 | |
|                 imageView.image = image
 | |
|                 imageView.isHidden = (animatedImage != nil)
 | |
|                 animatedImageView.image = animatedImage
 | |
|                 animatedImageView.isHidden = (animatedImage == nil)
 | |
|                 imageViewWidthConstraint.constant = self.size
 | |
|                 imageViewHeightConstraint.constant = self.size
 | |
|                 imageContainerView.layer.cornerRadius = (self.size / 2)
 | |
|         }
 | |
|     }
 | |
| }
 |