// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import GRDB 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 @objc public var useFallbackPicture = false @objc public var publicKey: String! @objc public var additionalPublicKey: String? @objc public var openGroupProfilePicture: UIImage? // Constraints private var imageViewWidthConstraint: NSLayoutConstraint! private var imageViewHeightConstraint: NSLayoutConstraint! private var additionalImageViewWidthConstraint: NSLayoutConstraint! private var additionalImageViewHeightConstraint: NSLayoutConstraint! // MARK: Components private lazy var imageView = getImageView() private lazy var additionalImageView = getImageView() // 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() { // 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 } // MARK: Updating @objc(updateForContact:) public func update(for publicKey: String) { // TODO: Confirm this is still used GRDBStorage.shared.read { db in update(db, publicKey: publicKey) } } public func update(_ db: Database, publicKey: String) { openGroupProfilePicture = nil self.publicKey = publicKey additionalPublicKey = nil useFallbackPicture = false update(db) } public func update(_ db: Database, thread: SessionThread) { openGroupProfilePicture = nil switch thread.variant { case .contact: update(db, publicKey: thread.id) case .closedGroup: let userPublicKey: String = getUserHexEncodedPublicKey(db) var randomUsers: [String] = (try? thread.closedGroup .fetchOne(db)? .members .fetchAll(db) .map { $0.profileId } .filter { $0 != userPublicKey } .sorted()) // Sort to provide a level of stability .defaulting(to: []) if randomUsers.count == 1 { // Ensure the current user is at the back visually randomUsers.insert(userPublicKey, at: 0) } publicKey = (randomUsers.first ?? "") additionalPublicKey = (randomUsers.count >= 2 ? randomUsers[1] : "") useFallbackPicture = false update(db) case .openGroup: openGroupProfilePicture = (try? thread.openGroup .fetchOne(db)? .imageData) .map { UIImage(data: $0) } publicKey = "" useFallbackPicture = (openGroupProfilePicture == nil) hasTappableProfilePicture = (openGroupProfilePicture != nil) update(db) } } @objc public func update() { // TODO: Confirm this is still used GRDBStorage.shared.read { db in update(db) } } public func update(_ db: Database) { AssertIsOnMainThread() func getProfilePicture(of size: CGFloat, for publicKey: String) -> UIImage? { guard !publicKey.isEmpty else { return nil } if let profilePicture: UIImage = ProfileManager.profileAvatar(db, id: publicKey) { hasTappableProfilePicture = true return profilePicture } hasTappableProfilePicture = false // TODO: Pass in context? let displayName: String = Profile.displayName(db, id: publicKey) return Identicon.generatePlaceholderIcon(seed: publicKey, text: displayName, size: size) } let size: CGFloat if let additionalPublicKey = additionalPublicKey, !useFallbackPicture, openGroupProfilePicture == nil { if self.size == 40 { size = 32 } else if self.size == Values.largeProfilePictureSize { size = 56 } else { size = Values.smallProfilePictureSize } imageViewWidthConstraint.constant = size imageViewHeightConstraint.constant = size additionalImageViewWidthConstraint.constant = size additionalImageViewHeightConstraint.constant = size additionalImageView.isHidden = false additionalImageView.image = getProfilePicture(of: size, for: additionalPublicKey) } else { size = self.size imageViewWidthConstraint.constant = size imageViewHeightConstraint.constant = size additionalImageView.isHidden = true additionalImageView.image = nil } guard publicKey != nil || openGroupProfilePicture != nil else { return } imageView.image = useFallbackPicture ? nil : (openGroupProfilePicture ?? getProfilePicture(of: size, for: publicKey)) imageView.backgroundColor = useFallbackPicture ? UIColor(rgbHex: 0x353535) : Colors.unimportant imageView.layer.cornerRadius = size / 2 additionalImageView.layer.cornerRadius = size / 2 imageView.contentMode = useFallbackPicture ? .center : .scaleAspectFit if useFallbackPicture { switch size { case Values.smallProfilePictureSize.. UIImageView { let result = UIImageView() result.layer.masksToBounds = true result.backgroundColor = Colors.unimportant result.contentMode = .scaleAspectFit return result } @objc public func getProfilePicture() -> UIImage? { return hasTappableProfilePicture ? imageView.image : nil } }