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.
		
		
		
		
		
			
		
			
				
	
	
		
			328 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			328 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import UIKit
 | 
						|
import GRDB
 | 
						|
import YYImage
 | 
						|
import SessionUIKit
 | 
						|
import SessionMessagingKit
 | 
						|
 | 
						|
@objc(LKProfilePictureView)
 | 
						|
public final class ProfilePictureView: UIView {
 | 
						|
    private var hasTappableProfilePicture: Bool = false
 | 
						|
    @objc public var size: CGFloat = 0 // Not an implicitly unwrapped optional due to Obj-C limitations
 | 
						|
    
 | 
						|
    // 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.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 additionalProfilePlaceholderImageView: UIImageView = {
 | 
						|
        let result: UIImageView = UIImageView(
 | 
						|
            image: UIImage(systemName: "person.fill")?.withRenderingMode(.alwaysTemplate)
 | 
						|
        )
 | 
						|
        result.translatesAutoresizingMaskIntoConstraints = false
 | 
						|
        result.contentMode = .scaleAspectFill
 | 
						|
        result.tintColor = Colors.text
 | 
						|
        result.isHidden = true
 | 
						|
        
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
    
 | 
						|
    private lazy var additionalImageView: UIImageView = {
 | 
						|
        let result: UIImageView = UIImageView()
 | 
						|
        result.translatesAutoresizingMaskIntoConstraints = false
 | 
						|
        result.contentMode = .scaleAspectFill
 | 
						|
        result.tintColor = Colors.text
 | 
						|
        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: 3)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // FIXME: Remove this once we refactor the OWSConversationSettingsViewController to Swift (use the HomeViewModel approach)
 | 
						|
    @objc(updateForThreadId:)
 | 
						|
    public func update(forThreadId threadId: String?) {
 | 
						|
        guard
 | 
						|
            let threadId: String = threadId,
 | 
						|
            let viewModel: SessionThreadViewModel = Storage.shared.read({ db -> SessionThreadViewModel? in
 | 
						|
                let userPublicKey: String = getUserHexEncodedPublicKey(db)
 | 
						|
                
 | 
						|
                return try SessionThreadViewModel
 | 
						|
                    .conversationSettingsProfileQuery(threadId: threadId, userPublicKey: userPublicKey)
 | 
						|
                    .fetchOne(db)
 | 
						|
            })
 | 
						|
        else { return }
 | 
						|
        
 | 
						|
        update(
 | 
						|
            publicKey: viewModel.threadId,
 | 
						|
            profile: viewModel.profile,
 | 
						|
            additionalProfile: viewModel.additionalProfile,
 | 
						|
            threadVariant: viewModel.threadVariant,
 | 
						|
            openGroupProfilePictureData: viewModel.openGroupProfilePictureData,
 | 
						|
            useFallbackPicture: (
 | 
						|
                viewModel.threadVariant == .openGroup &&
 | 
						|
                viewModel.openGroupProfilePictureData == nil
 | 
						|
            ),
 | 
						|
            showMultiAvatarForClosedGroup: true
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    public func update(
 | 
						|
        publicKey: String = "",
 | 
						|
        profile: Profile? = nil,
 | 
						|
        additionalProfile: Profile? = nil,
 | 
						|
        threadVariant: SessionThread.Variant,
 | 
						|
        openGroupProfilePictureData: Data? = nil,
 | 
						|
        useFallbackPicture: Bool = false,
 | 
						|
        showMultiAvatarForClosedGroup: Bool = false
 | 
						|
    ) {
 | 
						|
        AssertIsOnMainThread()
 | 
						|
        guard !useFallbackPicture else {
 | 
						|
            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
 | 
						|
            animatedImageView.isHidden = true
 | 
						|
            imageContainerView.backgroundColor = UIColor(rgbHex: 0x353535)
 | 
						|
            imageContainerView.layer.cornerRadius = (self.size / 2)
 | 
						|
            imageViewWidthConstraint.constant = self.size
 | 
						|
            imageViewHeightConstraint.constant = self.size
 | 
						|
            additionalImageContainerView.isHidden = true
 | 
						|
            animatedImageView.image = nil
 | 
						|
            additionalImageView.image = nil
 | 
						|
            additionalAnimatedImageView.image = nil
 | 
						|
            additionalImageView.isHidden = true
 | 
						|
            additionalAnimatedImageView.isHidden = true
 | 
						|
            additionalProfilePlaceholderImageView.isHidden = true
 | 
						|
            return
 | 
						|
        }
 | 
						|
        guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
 | 
						|
        
 | 
						|
        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 (
 | 
						|
                Identicon.generatePlaceholderIcon(
 | 
						|
                    seed: publicKey,
 | 
						|
                    text: (profile?.displayName(for: threadVariant))
 | 
						|
                        .defaulting(to: publicKey),
 | 
						|
                    size: size
 | 
						|
                ),
 | 
						|
                nil,
 | 
						|
                false
 | 
						|
            )
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Calulate the sizes (and set the additional image content)
 | 
						|
        let targetSize: CGFloat
 | 
						|
        
 | 
						|
        switch (threadVariant, showMultiAvatarForClosedGroup) {
 | 
						|
            case (.closedGroup, true):
 | 
						|
                if self.size == 40 {
 | 
						|
                    targetSize = 32
 | 
						|
                }
 | 
						|
                else if self.size == Values.largeProfilePictureSize {
 | 
						|
                    targetSize = 56
 | 
						|
                }
 | 
						|
                else {
 | 
						|
                    targetSize = Values.smallProfilePictureSize
 | 
						|
                }
 | 
						|
                
 | 
						|
                imageViewWidthConstraint.constant = targetSize
 | 
						|
                imageViewHeightConstraint.constant = targetSize
 | 
						|
                additionalImageViewWidthConstraint.constant = targetSize
 | 
						|
                additionalImageViewHeightConstraint.constant = targetSize
 | 
						|
                additionalImageContainerView.isHidden = false
 | 
						|
                
 | 
						|
                if let additionalProfile: Profile = additionalProfile {
 | 
						|
                    let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture(
 | 
						|
                        of: targetSize,
 | 
						|
                        for: additionalProfile.id,
 | 
						|
                        profile: additionalProfile
 | 
						|
                    )
 | 
						|
                    
 | 
						|
                    // 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)
 | 
						|
                    additionalProfilePlaceholderImageView.isHidden = true
 | 
						|
                }
 | 
						|
                else {
 | 
						|
                    additionalImageView.isHidden = true
 | 
						|
                    additionalAnimatedImageView.isHidden = true
 | 
						|
                    additionalProfilePlaceholderImageView.isHidden = false
 | 
						|
                }
 | 
						|
                
 | 
						|
            default:
 | 
						|
                targetSize = self.size
 | 
						|
                imageViewWidthConstraint.constant = targetSize
 | 
						|
                imageViewHeightConstraint.constant = targetSize
 | 
						|
                additionalImageContainerView.isHidden = true
 | 
						|
                additionalImageView.image = nil
 | 
						|
                additionalImageView.isHidden = true
 | 
						|
                additionalAnimatedImageView.image = nil
 | 
						|
                additionalAnimatedImageView.isHidden = true
 | 
						|
                additionalProfilePlaceholderImageView.isHidden = true
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Set the image
 | 
						|
        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, 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
 | 
						|
        animatedImageView.contentMode = .scaleAspectFill
 | 
						|
        imageContainerView.backgroundColor = Colors.unimportant
 | 
						|
        imageContainerView.layer.cornerRadius = (targetSize / 2)
 | 
						|
        additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Convenience
 | 
						|
    
 | 
						|
    @objc public func getProfilePicture() -> UIImage? {
 | 
						|
        return (hasTappableProfilePicture ? imageView.image : nil)
 | 
						|
    }
 | 
						|
}
 |