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.
546 lines
23 KiB
Swift
546 lines
23 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 enum Size {
|
|
case navigation
|
|
case message
|
|
case list
|
|
case hero
|
|
|
|
public var viewSize: CGFloat {
|
|
switch self {
|
|
case .navigation, .message: return 26
|
|
case .list: return 46
|
|
case .hero: return 110
|
|
}
|
|
}
|
|
|
|
var imageSize: CGFloat {
|
|
switch self {
|
|
case .navigation, .message: return 26
|
|
case .list: return 46
|
|
case .hero: return 80
|
|
}
|
|
}
|
|
|
|
var multiImageSize: CGFloat {
|
|
switch self {
|
|
case .navigation, .message: return 18 // Shouldn't be used
|
|
case .list: return 32
|
|
case .hero: return 80
|
|
}
|
|
}
|
|
|
|
var iconSize: CGFloat {
|
|
switch self {
|
|
case .navigation, .message: return 8
|
|
case .list: return 16
|
|
case .hero: return 24
|
|
}
|
|
}
|
|
|
|
var iconVerticalInset: CGFloat {
|
|
switch self {
|
|
case .navigation, .message: return 1
|
|
case .list: return 3
|
|
case .hero: return 5
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum ProfileIcon {
|
|
case none
|
|
case crown
|
|
case rightPlus
|
|
}
|
|
|
|
public var size: Size {
|
|
didSet {
|
|
widthConstraint.constant = (customWidth ?? size.viewSize)
|
|
heightConstraint.constant = size.viewSize
|
|
profileIconTopConstraint.constant = size.iconVerticalInset
|
|
profileIconBottomConstraint.constant = -size.iconVerticalInset
|
|
profileIconBackgroundWidthConstraint.constant = size.iconSize
|
|
profileIconBackgroundHeightConstraint.constant = size.iconSize
|
|
additionalProfileIconTopConstraint.constant = size.iconVerticalInset
|
|
additionalProfileIconBottomConstraint.constant = -size.iconVerticalInset
|
|
additionalProfileIconBackgroundWidthConstraint.constant = size.iconSize
|
|
additionalProfileIconBackgroundHeightConstraint.constant = size.iconSize
|
|
|
|
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
|
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
|
}
|
|
}
|
|
public var customWidth: CGFloat? {
|
|
didSet {
|
|
self.widthConstraint.constant = (customWidth ?? self.size.viewSize)
|
|
}
|
|
}
|
|
private var hasTappableProfilePicture: Bool = false
|
|
|
|
// MARK: - Constraints
|
|
|
|
private var widthConstraint: NSLayoutConstraint!
|
|
private var heightConstraint: NSLayoutConstraint!
|
|
private var imageViewTopConstraint: NSLayoutConstraint!
|
|
private var imageViewLeadingConstraint: NSLayoutConstraint!
|
|
private var imageViewCenterXConstraint: NSLayoutConstraint!
|
|
private var imageViewCenterYConstraint: NSLayoutConstraint!
|
|
private var imageViewWidthConstraint: NSLayoutConstraint!
|
|
private var imageViewHeightConstraint: NSLayoutConstraint!
|
|
private var additionalImageViewWidthConstraint: NSLayoutConstraint!
|
|
private var additionalImageViewHeightConstraint: NSLayoutConstraint!
|
|
private var profileIconTopConstraint: NSLayoutConstraint!
|
|
private var profileIconBottomConstraint: NSLayoutConstraint!
|
|
private var profileIconBackgroundLeftAlignConstraint: NSLayoutConstraint!
|
|
private var profileIconBackgroundRightAlignConstraint: NSLayoutConstraint!
|
|
private var profileIconBackgroundWidthConstraint: NSLayoutConstraint!
|
|
private var profileIconBackgroundHeightConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconTopConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconBottomConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconBackgroundLeftAlignConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconBackgroundRightAlignConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconBackgroundWidthConstraint: NSLayoutConstraint!
|
|
private var additionalProfileIconBackgroundHeightConstraint: 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.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
|
|
}()
|
|
|
|
private lazy var profileIconBackgroundView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var profileIconImageView: UIImageView = {
|
|
let result: UIImageView = UIImageView()
|
|
result.contentMode = .scaleAspectFit
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var additionalProfileIconBackgroundView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var additionalProfileIconImageView: UIImageView = {
|
|
let result: UIImageView = UIImageView()
|
|
result.contentMode = .scaleAspectFit
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
public init(size: Size) {
|
|
self.size = size
|
|
|
|
super.init(frame: CGRect(x: 0, y: 0, width: size.viewSize, height: size.viewSize))
|
|
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
preconditionFailure("Use init(size:) instead.")
|
|
}
|
|
|
|
private func setUpViewHierarchy() {
|
|
addSubview(imageContainerView)
|
|
addSubview(profileIconBackgroundView)
|
|
addSubview(additionalImageContainerView)
|
|
addSubview(additionalProfileIconBackgroundView)
|
|
|
|
profileIconBackgroundView.addSubview(profileIconImageView)
|
|
additionalProfileIconBackgroundView.addSubview(additionalProfileIconImageView)
|
|
|
|
widthConstraint = self.set(.width, to: self.size.viewSize)
|
|
heightConstraint = self.set(.height, to: self.size.viewSize)
|
|
|
|
imageViewTopConstraint = imageContainerView.pin(.top, to: .top, of: self)
|
|
imageViewLeadingConstraint = imageContainerView.pin(.leading, to: .leading, of: self)
|
|
imageViewCenterXConstraint = imageContainerView.center(.horizontal, in: self)
|
|
imageViewCenterXConstraint.isActive = false
|
|
imageViewCenterYConstraint = imageContainerView.center(.vertical, in: self)
|
|
imageViewCenterYConstraint.isActive = false
|
|
imageViewWidthConstraint = imageContainerView.set(.width, to: size.imageSize)
|
|
imageViewHeightConstraint = imageContainerView.set(.height, to: size.imageSize)
|
|
additionalImageContainerView.pin(.trailing, to: .trailing, of: self)
|
|
additionalImageContainerView.pin(.bottom, to: .bottom, of: self)
|
|
additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: size.multiImageSize)
|
|
additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: size.multiImageSize)
|
|
|
|
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)
|
|
|
|
profileIconTopConstraint = profileIconImageView.pin(
|
|
.top,
|
|
to: .top,
|
|
of: profileIconBackgroundView,
|
|
withInset: size.iconVerticalInset
|
|
)
|
|
profileIconImageView.pin(.left, to: .left, of: profileIconBackgroundView)
|
|
profileIconImageView.pin(.right, to: .right, of: profileIconBackgroundView)
|
|
profileIconBottomConstraint = profileIconImageView.pin(
|
|
.bottom,
|
|
to: .bottom,
|
|
of: profileIconBackgroundView,
|
|
withInset: -size.iconVerticalInset
|
|
)
|
|
profileIconBackgroundLeftAlignConstraint = profileIconBackgroundView.pin(.leading, to: .leading, of: imageContainerView)
|
|
profileIconBackgroundRightAlignConstraint = profileIconBackgroundView.pin(.trailing, to: .trailing, of: imageContainerView)
|
|
profileIconBackgroundView.pin(.bottom, to: .bottom, of: imageContainerView)
|
|
profileIconBackgroundWidthConstraint = profileIconBackgroundView.set(.width, to: size.iconSize)
|
|
profileIconBackgroundHeightConstraint = profileIconBackgroundView.set(.height, to: size.iconSize)
|
|
profileIconBackgroundLeftAlignConstraint.isActive = false
|
|
profileIconBackgroundRightAlignConstraint.isActive = false
|
|
|
|
additionalProfileIconTopConstraint = additionalProfileIconImageView.pin(
|
|
.top,
|
|
to: .top,
|
|
of: additionalProfileIconBackgroundView,
|
|
withInset: size.iconVerticalInset
|
|
)
|
|
additionalProfileIconImageView.pin(.left, to: .left, of: additionalProfileIconBackgroundView)
|
|
additionalProfileIconImageView.pin(.right, to: .right, of: additionalProfileIconBackgroundView)
|
|
additionalProfileIconBottomConstraint = additionalProfileIconImageView.pin(
|
|
.bottom,
|
|
to: .bottom,
|
|
of: additionalProfileIconBackgroundView,
|
|
withInset: -size.iconVerticalInset
|
|
)
|
|
additionalProfileIconBackgroundLeftAlignConstraint = additionalProfileIconBackgroundView.pin(.leading, to: .leading, of: additionalImageContainerView)
|
|
additionalProfileIconBackgroundRightAlignConstraint = additionalProfileIconBackgroundView.pin(.trailing, to: .trailing, of: additionalImageContainerView)
|
|
additionalProfileIconBackgroundView.pin(.bottom, to: .bottom, of: additionalImageContainerView)
|
|
additionalProfileIconBackgroundWidthConstraint = additionalProfileIconBackgroundView.set(.width, to: size.iconSize)
|
|
additionalProfileIconBackgroundHeightConstraint = additionalProfileIconBackgroundView.set(.height, to: size.iconSize)
|
|
additionalProfileIconBackgroundLeftAlignConstraint.isActive = false
|
|
additionalProfileIconBackgroundRightAlignConstraint.isActive = false
|
|
}
|
|
|
|
// MARK: - Content
|
|
|
|
private func updateIconView(
|
|
icon: ProfileIcon,
|
|
imageView: UIImageView,
|
|
backgroundView: UIView,
|
|
leftAlignConstraint: NSLayoutConstraint,
|
|
rightAlignConstraint: NSLayoutConstraint
|
|
) {
|
|
backgroundView.isHidden = (icon == .none)
|
|
leftAlignConstraint.isActive = (
|
|
icon == .none ||
|
|
icon == .crown
|
|
)
|
|
rightAlignConstraint.isActive = (
|
|
icon == .rightPlus
|
|
)
|
|
|
|
switch icon {
|
|
case .none: imageView.image = nil
|
|
|
|
case .crown:
|
|
imageView.image = UIImage(systemName: "crown.fill")
|
|
backgroundView.themeBackgroundColor = .profileIcon_background
|
|
|
|
ThemeManager.onThemeChange(observer: imageView) { [weak imageView] _, primaryColor in
|
|
let targetColor: ThemeValue = (primaryColor == .green ?
|
|
.profileIcon_greenPrimaryColor :
|
|
.profileIcon
|
|
)
|
|
|
|
guard imageView?.themeTintColor != targetColor else { return }
|
|
|
|
imageView?.themeTintColor = targetColor
|
|
}
|
|
|
|
case .rightPlus:
|
|
imageView.image = UIImage(systemName: "plus")
|
|
imageView.themeTintColor = .black
|
|
backgroundView.themeBackgroundColor = .primary
|
|
}
|
|
}
|
|
|
|
public func update(
|
|
publicKey: String = "",
|
|
profile: Profile? = nil,
|
|
icon: ProfileIcon = .none,
|
|
additionalProfile: Profile? = nil,
|
|
additionalIcon: ProfileIcon = .none,
|
|
threadVariant: SessionThread.Variant,
|
|
openGroupProfilePictureData: Data? = nil,
|
|
useFallbackPicture: Bool = false,
|
|
showMultiAvatarForClosedGroup: Bool = false
|
|
) {
|
|
AssertIsOnMainThread()
|
|
|
|
// Sort out the profile icon first
|
|
updateIconView(
|
|
icon: icon,
|
|
imageView: profileIconImageView,
|
|
backgroundView: profileIconBackgroundView,
|
|
leftAlignConstraint: profileIconBackgroundLeftAlignConstraint,
|
|
rightAlignConstraint: profileIconBackgroundRightAlignConstraint
|
|
)
|
|
|
|
guard !useFallbackPicture else {
|
|
switch self.size {
|
|
case .navigation, .message: imageView.image = #imageLiteral(resourceName: "SessionWhite16")
|
|
case .list: imageView.image = #imageLiteral(resourceName: "SessionWhite24")
|
|
case .hero: imageView.image = #imageLiteral(resourceName: "SessionWhite40")
|
|
}
|
|
|
|
imageView.contentMode = .center
|
|
imageView.isHidden = false
|
|
animatedImageView.isHidden = true
|
|
imageContainerView.themeBackgroundColorForced = .theme(.classicDark, color: .borderSeparator)
|
|
imageContainerView.layer.cornerRadius = (self.size.imageSize / 2)
|
|
imageViewWidthConstraint.constant = self.size.imageSize
|
|
imageViewHeightConstraint.constant = self.size.imageSize
|
|
profileIconBackgroundWidthConstraint.constant = self.size.iconSize
|
|
profileIconBackgroundHeightConstraint.constant = self.size.iconSize
|
|
profileIconBackgroundView.layer.cornerRadius = (self.size.iconSize / 2)
|
|
additionalProfileIconBackgroundWidthConstraint.constant = self.size.iconSize
|
|
additionalProfileIconBackgroundHeightConstraint.constant = self.size.iconSize
|
|
additionalProfileIconBackgroundView.layer.cornerRadius = (self.size.iconSize / 2)
|
|
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):
|
|
targetSize = self.size.multiImageSize
|
|
additionalImageContainerView.isHidden = false
|
|
imageViewTopConstraint.isActive = true
|
|
imageViewLeadingConstraint.isActive = true
|
|
imageViewCenterXConstraint.isActive = false
|
|
imageViewCenterYConstraint.isActive = false
|
|
|
|
// Sort out the additinoal profile icon if needed
|
|
updateIconView(
|
|
icon: additionalIcon,
|
|
imageView: additionalProfileIconImageView,
|
|
backgroundView: additionalProfileIconBackgroundView,
|
|
leftAlignConstraint: additionalProfileIconBackgroundLeftAlignConstraint,
|
|
rightAlignConstraint: additionalProfileIconBackgroundRightAlignConstraint
|
|
)
|
|
|
|
if let additionalProfile: Profile = additionalProfile {
|
|
let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture(
|
|
of: self.size.multiImageSize,
|
|
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.imageSize
|
|
|
|
additionalImageContainerView.isHidden = true
|
|
additionalProfileIconBackgroundView.isHidden = true
|
|
additionalImageView.image = nil
|
|
additionalImageView.isHidden = true
|
|
additionalAnimatedImageView.image = nil
|
|
additionalAnimatedImageView.isHidden = true
|
|
additionalProfilePlaceholderImageView.isHidden = true
|
|
imageViewTopConstraint.isActive = false
|
|
imageViewLeadingConstraint.isActive = false
|
|
imageViewCenterXConstraint.isActive = true
|
|
imageViewCenterYConstraint.isActive = 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.themeBackgroundColor = .backgroundSecondary
|
|
imageViewWidthConstraint.constant = targetSize
|
|
imageViewHeightConstraint.constant = targetSize
|
|
imageContainerView.layer.cornerRadius = (targetSize / 2)
|
|
additionalImageViewWidthConstraint.constant = targetSize
|
|
additionalImageViewHeightConstraint.constant = targetSize
|
|
additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
|
|
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
|
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
@objc public func getProfilePicture() -> UIImage? {
|
|
return (hasTappableProfilePicture ? imageView.image : nil)
|
|
}
|
|
}
|