mirror of https://github.com/oxen-io/session-ios
Merge remote-tracking branch 'origin/feature/tweak-profile-modal-ui' into feature/updated-user-config-handling
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Settings/SettingsViewModel.swift # Session/Shared/Views/SessionAvatarCell.swift # Session/Shared/Views/SessionCell+AccessoryView.swift # SessionUIKit/Components/ConfirmationModal.swift # SessionUIKit/Components/PlaceholderIcon.swift # SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swiftpull/751/head
commit
5d88db7a8a
@ -0,0 +1,115 @@
|
||||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
public extension ProfilePictureView {
|
||||
func update(
|
||||
publicKey: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
customImageData: Data?,
|
||||
profile: Profile?,
|
||||
profileIcon: ProfileIcon = .none,
|
||||
additionalProfile: Profile? = nil,
|
||||
additionalProfileIcon: ProfileIcon = .none
|
||||
) {
|
||||
// If we are given 'customImageData' then only use that
|
||||
guard customImageData == nil else { return update(Info(imageData: customImageData)) }
|
||||
|
||||
// Otherwise there are conversation-type-specific behaviours
|
||||
switch threadVariant {
|
||||
case .community:
|
||||
let placeholderImage: UIImage = {
|
||||
switch self.size {
|
||||
case .navigation, .message: return #imageLiteral(resourceName: "SessionWhite16")
|
||||
case .list: return #imageLiteral(resourceName: "SessionWhite24")
|
||||
case .hero: return #imageLiteral(resourceName: "SessionWhite40")
|
||||
}
|
||||
}()
|
||||
|
||||
update(
|
||||
Info(
|
||||
imageData: placeholderImage.pngData(),
|
||||
inset: UIEdgeInsets(
|
||||
top: 12,
|
||||
left: 12,
|
||||
bottom: 12,
|
||||
right: 12
|
||||
),
|
||||
icon: profileIcon,
|
||||
forcedBackgroundColor: .theme(.classicDark, color: .borderSeparator)
|
||||
)
|
||||
)
|
||||
|
||||
case .legacyGroup, .group:
|
||||
guard !publicKey.isEmpty else { return }
|
||||
|
||||
update(
|
||||
Info(
|
||||
imageData: (
|
||||
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
|
||||
PlaceholderIcon.generate(
|
||||
seed: publicKey,
|
||||
text: (profile?.displayName(for: threadVariant))
|
||||
.defaulting(to: publicKey),
|
||||
size: (additionalProfile != nil ?
|
||||
self.size.multiImageSize :
|
||||
self.size.viewSize
|
||||
)
|
||||
).pngData()
|
||||
),
|
||||
icon: profileIcon
|
||||
),
|
||||
additionalInfo: additionalProfile
|
||||
.map { otherProfile in
|
||||
Info(
|
||||
imageData: (
|
||||
ProfileManager.profileAvatar(profile: otherProfile) ??
|
||||
PlaceholderIcon.generate(
|
||||
seed: otherProfile.id,
|
||||
text: otherProfile.displayName(for: threadVariant),
|
||||
size: self.size.multiImageSize
|
||||
).pngData()
|
||||
),
|
||||
icon: additionalProfileIcon
|
||||
)
|
||||
}
|
||||
.defaulting(
|
||||
to: Info(
|
||||
imageData: UIImage(systemName: "person.fill")?.pngData(),
|
||||
renderingMode: .alwaysTemplate,
|
||||
themeTintColor: .white,
|
||||
inset: UIEdgeInsets(
|
||||
top: 3,
|
||||
left: 0,
|
||||
bottom: -5,
|
||||
right: 0
|
||||
),
|
||||
icon: additionalProfileIcon
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case .contact:
|
||||
guard !publicKey.isEmpty else { return }
|
||||
|
||||
update(
|
||||
Info(
|
||||
imageData: (
|
||||
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
|
||||
PlaceholderIcon.generate(
|
||||
seed: publicKey,
|
||||
text: (profile?.displayName(for: threadVariant))
|
||||
.defaulting(to: publicKey),
|
||||
size: (additionalProfile != nil ?
|
||||
self.size.multiImageSize :
|
||||
self.size.viewSize
|
||||
)
|
||||
).pngData()
|
||||
),
|
||||
icon: profileIcon
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,546 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import YYImage
|
||||
|
||||
public final class ProfilePictureView: UIView {
|
||||
public struct Info {
|
||||
let imageData: Data?
|
||||
let renderingMode: UIImage.RenderingMode
|
||||
let themeTintColor: ThemeValue?
|
||||
let inset: UIEdgeInsets
|
||||
let icon: ProfileIcon
|
||||
let backgroundColor: ThemeValue?
|
||||
let forcedBackgroundColor: ForcedThemeValue?
|
||||
|
||||
public init(
|
||||
imageData: Data?,
|
||||
renderingMode: UIImage.RenderingMode = .automatic,
|
||||
themeTintColor: ThemeValue? = nil,
|
||||
inset: UIEdgeInsets = .zero,
|
||||
icon: ProfileIcon = .none,
|
||||
backgroundColor: ThemeValue? = nil,
|
||||
forcedBackgroundColor: ForcedThemeValue? = nil
|
||||
) {
|
||||
self.imageData = imageData
|
||||
self.renderingMode = renderingMode
|
||||
self.themeTintColor = themeTintColor
|
||||
self.inset = inset
|
||||
self.icon = icon
|
||||
self.backgroundColor = backgroundColor
|
||||
self.forcedBackgroundColor = forcedBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
public var imageSize: CGFloat {
|
||||
switch self {
|
||||
case .navigation, .message: return 26
|
||||
case .list: return 46
|
||||
case .hero: return 80
|
||||
}
|
||||
}
|
||||
|
||||
public 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProfileIcon: Equatable, Hashable {
|
||||
case none
|
||||
case crown
|
||||
case rightPlus
|
||||
|
||||
func iconVerticalInset(for size: Size) -> CGFloat {
|
||||
switch (self, size) {
|
||||
case (.crown, .navigation), (.crown, .message): return 1
|
||||
case (.crown, .list): return 3
|
||||
case (.crown, .hero): return 5
|
||||
|
||||
case (.rightPlus, _): return 3
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var size: Size {
|
||||
didSet {
|
||||
widthConstraint.constant = (customWidth ?? size.viewSize)
|
||||
heightConstraint.constant = size.viewSize
|
||||
profileIconBackgroundWidthConstraint.constant = size.iconSize
|
||||
profileIconBackgroundHeightConstraint.constant = size.iconSize
|
||||
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)
|
||||
}
|
||||
}
|
||||
override public var clipsToBounds: Bool {
|
||||
didSet {
|
||||
imageContainerView.clipsToBounds = clipsToBounds
|
||||
additionalImageContainerView.clipsToBounds = clipsToBounds
|
||||
|
||||
imageContainerView.layer.cornerRadius = (clipsToBounds ?
|
||||
(additionalImageContainerView.isHidden ? (size.imageSize / 2) : (size.multiImageSize / 2)) :
|
||||
0
|
||||
)
|
||||
imageContainerView.layer.cornerRadius = (clipsToBounds ? (size.multiImageSize / 2) : 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 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!
|
||||
private lazy var imageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
|
||||
imageView.pin(.top, to: .top, of: imageContainerView, withInset: 0),
|
||||
imageView.pin(.left, to: .left, of: imageContainerView, withInset: 0),
|
||||
imageView.pin(.bottom, to: .bottom, of: imageContainerView, withInset: 0),
|
||||
imageView.pin(.right, to: .right, of: imageContainerView, withInset: 0),
|
||||
animatedImageView.pin(.top, to: .top, of: imageContainerView, withInset: 0),
|
||||
animatedImageView.pin(.left, to: .left, of: imageContainerView, withInset: 0),
|
||||
animatedImageView.pin(.bottom, to: .bottom, of: imageContainerView, withInset: 0),
|
||||
animatedImageView.pin(.right, to: .right, of: imageContainerView, withInset: 0)
|
||||
]
|
||||
private lazy var additionalImageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
|
||||
additionalImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 0),
|
||||
additionalImageView.pin(.left, to: .left, of: additionalImageContainerView, withInset: 0),
|
||||
additionalImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 0),
|
||||
additionalImageView.pin(.right, to: .right, of: additionalImageContainerView, withInset: 0),
|
||||
additionalAnimatedImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 0),
|
||||
additionalAnimatedImageView.pin(.left, to: .left, of: additionalImageContainerView, withInset: 0),
|
||||
additionalAnimatedImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 0),
|
||||
additionalAnimatedImageView.pin(.right, to: .right, of: additionalImageContainerView, withInset: 0)
|
||||
]
|
||||
|
||||
// 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.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))
|
||||
|
||||
clipsToBounds = true
|
||||
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)
|
||||
|
||||
// Activate the image edge constraints
|
||||
imageEdgeConstraints.forEach { $0.isActive = true }
|
||||
additionalImageEdgeConstraints.forEach { $0.isActive = true }
|
||||
|
||||
profileIconTopConstraint = profileIconImageView.pin(
|
||||
.top,
|
||||
to: .top,
|
||||
of: profileIconBackgroundView,
|
||||
withInset: 0
|
||||
)
|
||||
profileIconImageView.pin(.left, to: .left, of: profileIconBackgroundView)
|
||||
profileIconImageView.pin(.right, to: .right, of: profileIconBackgroundView)
|
||||
profileIconBottomConstraint = profileIconImageView.pin(
|
||||
.bottom,
|
||||
to: .bottom,
|
||||
of: profileIconBackgroundView,
|
||||
withInset: 0
|
||||
)
|
||||
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: 0
|
||||
)
|
||||
additionalProfileIconImageView.pin(.left, to: .left, of: additionalProfileIconBackgroundView)
|
||||
additionalProfileIconImageView.pin(.right, to: .right, of: additionalProfileIconBackgroundView)
|
||||
additionalProfileIconBottomConstraint = additionalProfileIconImageView.pin(
|
||||
.bottom,
|
||||
to: .bottom,
|
||||
of: additionalProfileIconBackgroundView,
|
||||
withInset: 0
|
||||
)
|
||||
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,
|
||||
topConstraint: NSLayoutConstraint,
|
||||
leftAlignConstraint: NSLayoutConstraint,
|
||||
rightAlignConstraint: NSLayoutConstraint,
|
||||
bottomConstraint: NSLayoutConstraint
|
||||
) {
|
||||
backgroundView.isHidden = (icon == .none)
|
||||
leftAlignConstraint.isActive = (
|
||||
icon == .none ||
|
||||
icon == .crown
|
||||
)
|
||||
rightAlignConstraint.isActive = (
|
||||
icon == .rightPlus
|
||||
)
|
||||
topConstraint.constant = icon.iconVerticalInset(for: size)
|
||||
bottomConstraint.constant = -icon.iconVerticalInset(for: size)
|
||||
|
||||
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",
|
||||
withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)
|
||||
)
|
||||
imageView.themeTintColor = .black
|
||||
backgroundView.themeBackgroundColor = .primary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
private func prepareForReuse() {
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.isHidden = true
|
||||
animatedImageView.contentMode = .scaleAspectFill
|
||||
animatedImageView.isHidden = true
|
||||
imageContainerView.clipsToBounds = clipsToBounds
|
||||
imageContainerView.themeBackgroundColor = .backgroundSecondary
|
||||
additionalImageContainerView.isHidden = true
|
||||
animatedImageView.image = nil
|
||||
additionalImageView.image = nil
|
||||
additionalAnimatedImageView.image = nil
|
||||
additionalImageView.isHidden = true
|
||||
additionalAnimatedImageView.isHidden = true
|
||||
additionalImageContainerView.clipsToBounds = clipsToBounds
|
||||
|
||||
imageViewTopConstraint.isActive = false
|
||||
imageViewLeadingConstraint.isActive = false
|
||||
imageViewCenterXConstraint.isActive = true
|
||||
imageViewCenterYConstraint.isActive = true
|
||||
profileIconBackgroundView.isHidden = true
|
||||
profileIconBackgroundLeftAlignConstraint.isActive = false
|
||||
profileIconBackgroundRightAlignConstraint.isActive = false
|
||||
additionalProfileIconBackgroundView.isHidden = true
|
||||
additionalProfileIconBackgroundLeftAlignConstraint.isActive = false
|
||||
additionalProfileIconBackgroundRightAlignConstraint.isActive = false
|
||||
imageEdgeConstraints.forEach { $0.constant = 0 }
|
||||
additionalImageEdgeConstraints.forEach { $0.constant = 0 }
|
||||
}
|
||||
|
||||
public func update(
|
||||
_ info: Info,
|
||||
additionalInfo: Info? = nil
|
||||
) {
|
||||
prepareForReuse()
|
||||
|
||||
// Sort out the icon first
|
||||
updateIconView(
|
||||
icon: info.icon,
|
||||
imageView: profileIconImageView,
|
||||
backgroundView: profileIconBackgroundView,
|
||||
topConstraint: profileIconTopConstraint,
|
||||
leftAlignConstraint: profileIconBackgroundLeftAlignConstraint,
|
||||
rightAlignConstraint: profileIconBackgroundRightAlignConstraint,
|
||||
bottomConstraint: profileIconBottomConstraint
|
||||
)
|
||||
|
||||
// Populate the main imageView
|
||||
switch info.imageData?.guessedImageFormat {
|
||||
case .gif, .webp: animatedImageView.image = info.imageData.map { YYImage(data: $0) }
|
||||
default:
|
||||
imageView.image = info.imageData
|
||||
.map {
|
||||
guard info.renderingMode != .automatic else { return UIImage(data: $0) }
|
||||
|
||||
return UIImage(data: $0)?.withRenderingMode(info.renderingMode)
|
||||
}
|
||||
}
|
||||
|
||||
imageView.themeTintColor = info.themeTintColor
|
||||
imageView.isHidden = (imageView.image == nil)
|
||||
animatedImageView.themeTintColor = info.themeTintColor
|
||||
animatedImageView.isHidden = (animatedImageView.image == nil)
|
||||
imageContainerView.themeBackgroundColor = info.backgroundColor
|
||||
imageContainerView.themeBackgroundColorForced = info.forcedBackgroundColor
|
||||
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
||||
imageEdgeConstraints.enumerated().forEach { index, constraint in
|
||||
switch index % 4 {
|
||||
case 0: constraint.constant = info.inset.top
|
||||
case 1: constraint.constant = info.inset.left
|
||||
case 2: constraint.constant = -info.inset.bottom
|
||||
case 3: constraint.constant = -info.inset.right
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is a second image (if not then set the size and finish)
|
||||
guard let additionalInfo: Info = additionalInfo else {
|
||||
imageViewWidthConstraint.constant = size.imageSize
|
||||
imageViewHeightConstraint.constant = size.imageSize
|
||||
imageContainerView.layer.cornerRadius = (imageContainerView.clipsToBounds ? (size.imageSize / 2) : 0)
|
||||
return
|
||||
}
|
||||
|
||||
// Sort out the additional icon first
|
||||
updateIconView(
|
||||
icon: additionalInfo.icon,
|
||||
imageView: additionalProfileIconImageView,
|
||||
backgroundView: additionalProfileIconBackgroundView,
|
||||
topConstraint: additionalProfileIconTopConstraint,
|
||||
leftAlignConstraint: additionalProfileIconBackgroundLeftAlignConstraint,
|
||||
rightAlignConstraint: additionalProfileIconBackgroundRightAlignConstraint,
|
||||
bottomConstraint: additionalProfileIconBottomConstraint
|
||||
)
|
||||
|
||||
// Set the additional image content and reposition the image views correctly
|
||||
switch additionalInfo.imageData?.guessedImageFormat {
|
||||
case .gif, .webp: additionalAnimatedImageView.image = additionalInfo.imageData.map { YYImage(data: $0) }
|
||||
default:
|
||||
additionalImageView.image = additionalInfo.imageData
|
||||
.map {
|
||||
guard additionalInfo.renderingMode != .automatic else { return UIImage(data: $0) }
|
||||
|
||||
return UIImage(data: $0)?.withRenderingMode(additionalInfo.renderingMode)
|
||||
}
|
||||
}
|
||||
|
||||
additionalImageView.themeTintColor = additionalInfo.themeTintColor
|
||||
additionalImageView.isHidden = (additionalImageView.image == nil)
|
||||
additionalAnimatedImageView.themeTintColor = additionalInfo.themeTintColor
|
||||
additionalAnimatedImageView.isHidden = (additionalAnimatedImageView.image == nil)
|
||||
additionalImageContainerView.isHidden = false
|
||||
|
||||
switch (info.backgroundColor, info.forcedBackgroundColor) {
|
||||
case (_, .some(let color)): additionalImageContainerView.themeBackgroundColorForced = color
|
||||
case (.some(let color), _): additionalImageContainerView.themeBackgroundColor = color
|
||||
default: additionalImageContainerView.themeBackgroundColor = .primary
|
||||
}
|
||||
|
||||
additionalImageEdgeConstraints.enumerated().forEach { index, constraint in
|
||||
switch index % 4 {
|
||||
case 0: constraint.constant = additionalInfo.inset.top
|
||||
case 1: constraint.constant = additionalInfo.inset.left
|
||||
case 2: constraint.constant = -additionalInfo.inset.bottom
|
||||
case 3: constraint.constant = -additionalInfo.inset.right
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
imageViewTopConstraint.isActive = true
|
||||
imageViewLeadingConstraint.isActive = true
|
||||
imageViewCenterXConstraint.isActive = false
|
||||
imageViewCenterYConstraint.isActive = false
|
||||
|
||||
imageViewWidthConstraint.constant = size.multiImageSize
|
||||
imageViewHeightConstraint.constant = size.multiImageSize
|
||||
imageContainerView.layer.cornerRadius = (imageContainerView.clipsToBounds ? (size.multiImageSize / 2) : 0)
|
||||
additionalImageViewWidthConstraint.constant = size.multiImageSize
|
||||
additionalImageViewHeightConstraint.constant = size.multiImageSize
|
||||
additionalImageContainerView.layer.cornerRadius = (additionalImageContainerView.clipsToBounds ?
|
||||
(size.multiImageSize / 2) :
|
||||
0
|
||||
)
|
||||
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(LKIdenticon)
|
||||
public final class Identicon: NSObject {
|
||||
private static let placeholderCache: Atomic<NSCache<NSString, UIImage>> = {
|
||||
let result = NSCache<NSString, UIImage>()
|
||||
result.countLimit = 50
|
||||
|
||||
return Atomic(result)
|
||||
}()
|
||||
|
||||
@objc public static func generatePlaceholderIcon(seed: String, text: String, size: CGFloat) -> UIImage {
|
||||
let icon = PlaceholderIcon(seed: seed)
|
||||
|
||||
var content: String = (text.hasSuffix("\(String(seed.suffix(4))))") ?
|
||||
(text.split(separator: "(")
|
||||
.first
|
||||
.map { String($0) })
|
||||
.defaulting(to: text) :
|
||||
text
|
||||
)
|
||||
|
||||
if content.count > 2 && SessionId.Prefix(from: content) != nil {
|
||||
content.removeFirst(2)
|
||||
}
|
||||
|
||||
let initials: String = content
|
||||
.split(separator: " ")
|
||||
.compactMap { word in word.first.map { String($0) } }
|
||||
.joined()
|
||||
let cacheKey: String = "\(content)-\(Int(floor(size)))"
|
||||
|
||||
if let cachedIcon: UIImage = placeholderCache.wrappedValue.object(forKey: cacheKey as NSString) {
|
||||
return cachedIcon
|
||||
}
|
||||
|
||||
let layer = icon.generateLayer(
|
||||
with: size,
|
||||
text: (initials.count >= 2 ?
|
||||
initials.substring(to: 2).uppercased() :
|
||||
content.substring(to: 2).uppercased()
|
||||
)
|
||||
)
|
||||
|
||||
let rect = CGRect(origin: CGPoint.zero, size: layer.frame.size)
|
||||
let renderer = UIGraphicsImageRenderer(size: rect.size)
|
||||
let result = renderer.image { layer.render(in: $0.cgContext) }
|
||||
|
||||
placeholderCache.mutate { $0.setObject(result, forKey: cacheKey as NSString) }
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue