mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
958 lines
43 KiB
Swift
958 lines
43 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import Lucide
|
|
import SessionUIKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
import SignalUtilitiesKit
|
|
|
|
extension SessionCell {
|
|
public class AccessoryView: UIView, UISearchBarDelegate {
|
|
// Note: We set a minimum width for the 'AccessoryView' so that the titles line up
|
|
// nicely when we have a mix of icons and switches
|
|
private static let minWidth: CGFloat = 50
|
|
|
|
private var onTap: ((SessionButton?) -> Void)?
|
|
private var searchTermChanged: ((String?) -> Void)?
|
|
|
|
private var currentContentView: UIView?
|
|
private var currentAccessoryIdentifier: String?
|
|
|
|
private lazy var minWidthConstraint: NSLayoutConstraint = self.widthAnchor
|
|
.constraint(greaterThanOrEqualToConstant: AccessoryView.minWidth)
|
|
private lazy var fixedWidthConstraint: NSLayoutConstraint = self
|
|
.set(.width, to: AccessoryView.minWidth)
|
|
.setting(priority: .defaultHigh)
|
|
|
|
// MARK: - Content
|
|
|
|
func prepareForReuse() {
|
|
isHidden = true
|
|
onTap = nil
|
|
searchTermChanged = nil
|
|
currentContentView?.removeFromSuperview()
|
|
currentContentView = nil
|
|
currentAccessoryIdentifier = nil
|
|
|
|
minWidthConstraint.constant = AccessoryView.minWidth
|
|
minWidthConstraint.isActive = false
|
|
fixedWidthConstraint.constant = AccessoryView.minWidth
|
|
fixedWidthConstraint.isActive = false
|
|
|
|
invalidateIntrinsicContentSize()
|
|
}
|
|
|
|
public func update(
|
|
with accessory: Accessory?,
|
|
tintColor: ThemeValue,
|
|
isEnabled: Bool,
|
|
maxContentWidth: CGFloat,
|
|
using dependencies: Dependencies
|
|
) {
|
|
guard let accessory: Accessory = accessory else { return }
|
|
|
|
/// If the identifier hasn't changed then no need to reconstruct the content
|
|
guard accessory.viewIdentifier != currentAccessoryIdentifier else {
|
|
configure(
|
|
view: currentContentView,
|
|
accessory: accessory,
|
|
tintColor: tintColor,
|
|
isEnabled: isEnabled,
|
|
using: dependencies
|
|
)
|
|
return
|
|
}
|
|
|
|
/// Otherwise we do need to reconstruct and layout the content
|
|
prepareForReuse()
|
|
self.isHidden = false
|
|
|
|
let maybeView: UIView? = createView(
|
|
accessory: accessory,
|
|
maxContentWidth: maxContentWidth,
|
|
using: dependencies
|
|
)
|
|
|
|
if let newView: UIView = maybeView {
|
|
addSubview(newView)
|
|
layout(view: newView, accessory: accessory)
|
|
}
|
|
|
|
configure(
|
|
view: maybeView,
|
|
accessory: accessory,
|
|
tintColor: tintColor,
|
|
isEnabled: isEnabled,
|
|
using: dependencies
|
|
)
|
|
|
|
currentContentView = maybeView
|
|
currentAccessoryIdentifier = accessory.viewIdentifier
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
func touchedView(_ touch: UITouch) -> UIView {
|
|
switch (currentContentView, currentContentView?.subviews.first, currentContentView?.subviews.last) {
|
|
case (let label as SessionHighlightingBackgroundLabel, _, _),
|
|
(_, let label as SessionHighlightingBackgroundLabel, _):
|
|
let localPoint: CGPoint = touch.location(in: label)
|
|
|
|
return (label.bounds.contains(localPoint) ? label : self)
|
|
case (let profilePictureView as ProfilePictureView, _, _):
|
|
let localPoint: CGPoint = touch.location(in: profilePictureView)
|
|
|
|
return profilePictureView.getTouchedView(from: localPoint)
|
|
|
|
case (_, let qrCodeImageView as UIImageView , .some(let profileIcon)):
|
|
let localPoint: CGPoint = touch.location(in: profileIcon)
|
|
|
|
return (profileIcon.bounds.contains(localPoint) ? profileIcon : qrCodeImageView)
|
|
|
|
default: return self
|
|
}
|
|
}
|
|
|
|
func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
|
switch (currentContentView, currentContentView?.subviews.first) {
|
|
case (let label as SessionHighlightingBackgroundLabel, _),
|
|
(_, let label as SessionHighlightingBackgroundLabel):
|
|
label.setHighlighted(highlighted, animated: animated)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
func setSelected(_ selected: Bool, animated: Bool) {
|
|
switch (currentContentView, currentContentView?.subviews.first) {
|
|
case (let label as SessionHighlightingBackgroundLabel, _),
|
|
(_, let label as SessionHighlightingBackgroundLabel):
|
|
label.setSelected(selected, animated: animated)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
@objc private func buttonTapped() {
|
|
guard let button: SessionButton = currentContentView as? SessionButton else { return }
|
|
|
|
onTap?(button)
|
|
}
|
|
|
|
// MARK: - UISearchBarDelegate
|
|
|
|
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
|
searchTermChanged?(searchText)
|
|
}
|
|
|
|
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
|
searchBar.setShowsCancelButton(true, animated: true)
|
|
}
|
|
|
|
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
|
searchBar.setShowsCancelButton(false, animated: true)
|
|
}
|
|
|
|
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
|
searchBar.endEditing(true)
|
|
}
|
|
|
|
// MARK: - View Construction
|
|
|
|
private func createView(
|
|
accessory: Accessory,
|
|
maxContentWidth: CGFloat,
|
|
using dependencies: Dependencies
|
|
) -> UIView? {
|
|
switch accessory {
|
|
case is SessionCell.AccessoryConfig.QRCode:
|
|
return createQRCodeView()
|
|
|
|
case is SessionCell.AccessoryConfig.ProBadge:
|
|
return SessionProBadge(size: .small)
|
|
|
|
case is SessionCell.AccessoryConfig.Icon:
|
|
return createIconView(using: dependencies)
|
|
|
|
case is SessionCell.AccessoryConfig.IconAsync:
|
|
return createIconView(using: dependencies)
|
|
|
|
case is SessionCell.AccessoryConfig.Toggle: return createToggleView()
|
|
case is SessionCell.AccessoryConfig.DropDown:
|
|
return createDropDownView(maxContentWidth: maxContentWidth)
|
|
|
|
case is SessionCell.AccessoryConfig.Radio: return createRadioView()
|
|
|
|
case is SessionCell.AccessoryConfig.HighlightingBackgroundLabel:
|
|
return createHighlightingBackgroundLabelView(maxContentWidth: maxContentWidth)
|
|
|
|
case is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio:
|
|
return createHighlightingBackgroundLabelAndRadioView(maxContentWidth: maxContentWidth)
|
|
|
|
case is SessionCell.AccessoryConfig.DisplayPicture: return createDisplayPictureView()
|
|
case is SessionCell.AccessoryConfig.Search: return createSearchView()
|
|
case is SessionCell.AccessoryConfig.Button: return createButtonView()
|
|
case is SessionCell.AccessoryConfig.ActivityIndicator: return createActivityIndicatorView()
|
|
case let accessory as SessionCell.AccessoryConfig.AnyCustom:
|
|
return accessory.createView(
|
|
maxContentWidth: maxContentWidth,
|
|
using: dependencies
|
|
)
|
|
|
|
default:
|
|
/// If we get an unknown case then just hide again
|
|
self.isHidden = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func layout(view: UIView?, accessory: Accessory) {
|
|
switch accessory {
|
|
case let accessory as SessionCell.AccessoryConfig.QRCode:
|
|
layoutQRCodeView(view)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.ProBadge:
|
|
layoutProBadgeView(view, size: accessory.proBadgeSize)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Icon:
|
|
layoutIconView(
|
|
view,
|
|
iconSize: accessory.iconSize,
|
|
shouldFill: accessory.shouldFill,
|
|
pin: accessory.pinEdges
|
|
)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.IconAsync:
|
|
layoutIconView(
|
|
view,
|
|
iconSize: accessory.iconSize,
|
|
shouldFill: accessory.shouldFill,
|
|
pin: accessory.pinEdges
|
|
)
|
|
|
|
case is SessionCell.AccessoryConfig.Toggle: layoutToggleView(view)
|
|
case is SessionCell.AccessoryConfig.DropDown: layoutDropDownView(view)
|
|
case let accessory as SessionCell.AccessoryConfig.Radio:
|
|
layoutRadioView(view, size: accessory.size)
|
|
|
|
case is SessionCell.AccessoryConfig.HighlightingBackgroundLabel:
|
|
layoutHighlightingBackgroundLabelView(view)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio:
|
|
layoutHighlightingBackgroundLabelAndRadioView(view, size: accessory.size)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.DisplayPicture:
|
|
layoutDisplayPictureView(view, size: accessory.size)
|
|
|
|
case is SessionCell.AccessoryConfig.Search: layoutSearchView(view)
|
|
case is SessionCell.AccessoryConfig.Button: layoutButtonView(view)
|
|
case is SessionCell.AccessoryConfig.ActivityIndicator: layoutActivityIndicatorView(view)
|
|
case let accessory as SessionCell.AccessoryConfig.AnyCustom:
|
|
layoutCustomView(view, size: accessory.size)
|
|
|
|
// If we get an unknown case then just hide again
|
|
default: self.isHidden = true
|
|
}
|
|
}
|
|
|
|
private func configure(
|
|
view: UIView?,
|
|
accessory: Accessory,
|
|
tintColor: ThemeValue,
|
|
isEnabled: Bool,
|
|
using dependencies: Dependencies
|
|
) {
|
|
switch accessory {
|
|
case let accessory as SessionCell.AccessoryConfig.QRCode:
|
|
configureQRCodeView(view, accessory)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.ProBadge:
|
|
configureProBadgeView(view, tintColor: .primary)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Icon:
|
|
configureIconView(view, accessory, tintColor: tintColor)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.IconAsync:
|
|
configureIconView(view, accessory, tintColor: tintColor)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Toggle:
|
|
configureToggleView(view, accessory, isEnabled: isEnabled)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.DropDown:
|
|
configureDropDown(view, accessory)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Radio:
|
|
configureRadioView(view, accessory, isEnabled: isEnabled)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabel:
|
|
configureHighlightingBackgroundLabelView(view, accessory, tintColor: tintColor)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio:
|
|
configureHighlightingBackgroundLabelAndRadioView(
|
|
view,
|
|
accessory,
|
|
tintColor: tintColor,
|
|
isEnabled: isEnabled
|
|
)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.DisplayPicture:
|
|
configureDisplayPictureView(view, accessory, using: dependencies)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Search:
|
|
configureSearchView(view, accessory)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.Button:
|
|
configureButtonView(view, accessory)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.ActivityIndicator:
|
|
configureActivityIndicatorView(view, accessory)
|
|
|
|
case let accessory as SessionCell.AccessoryConfig.AnyCustom:
|
|
configureCustomView(view, accessory)
|
|
|
|
// If we get an unknown case then just hide again
|
|
default: self.isHidden = true
|
|
}
|
|
}
|
|
|
|
// MARK: -- QRCode
|
|
|
|
private func createQRCodeView() -> UIView {
|
|
let result: UIView = UIView()
|
|
result.layer.cornerRadius = 10
|
|
|
|
let qrCodeImageView: UIImageView = UIImageView()
|
|
qrCodeImageView.contentMode = .scaleAspectFit
|
|
|
|
result.addSubview(qrCodeImageView)
|
|
qrCodeImageView.pin(to: result, withInset: Values.smallSpacing)
|
|
result.set(.width, to: 190)
|
|
result.set(.height, to: 190)
|
|
|
|
let iconImageView: UIImageView = UIImageView(
|
|
image: UIImage(named: "ic_user_round_fill")?
|
|
.withRenderingMode(.alwaysTemplate)
|
|
)
|
|
iconImageView.contentMode = .scaleAspectFit
|
|
iconImageView.set(.width, to: 18)
|
|
iconImageView.set(.height, to: 18)
|
|
iconImageView.themeTintColor = .black
|
|
|
|
let iconBackgroudView: UIView = UIView()
|
|
iconBackgroudView.themeBackgroundColor = .primary
|
|
iconBackgroudView.set(.width, to: 33)
|
|
iconBackgroudView.set(.height, to: 33)
|
|
iconBackgroudView.layer.cornerRadius = 16.5
|
|
iconBackgroudView.layer.masksToBounds = true
|
|
|
|
iconBackgroudView.addSubview(iconImageView)
|
|
iconImageView.center(in: iconBackgroudView)
|
|
|
|
result.addSubview(iconBackgroudView)
|
|
iconBackgroudView.pin(.top, to: .top, of: result, withInset: -10)
|
|
iconBackgroudView.pin(.trailing, to: .trailing, of: result, withInset: 17)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutQRCodeView(_ view: UIView?) {
|
|
guard let view: UIView = view else { return }
|
|
|
|
view.pin(to: self)
|
|
fixedWidthConstraint.constant = 190
|
|
fixedWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureQRCodeView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.QRCode) {
|
|
guard
|
|
let backgroundView: UIView = view,
|
|
let qrCodeImageView: UIImageView = view?.subviews.first as? UIImageView
|
|
else { return }
|
|
|
|
let backgroundThemeColor: ThemeValue = (accessory.themeStyle == .light ? .backgroundSecondary : .textPrimary)
|
|
let qrCodeThemeColor: ThemeValue = (accessory.themeStyle == .light ? .textPrimary : .backgroundPrimary)
|
|
let qrCodeImage: UIImage = QRCode
|
|
.generate(for: accessory.string, hasBackground: accessory.hasBackground, iconName: accessory.logo)
|
|
.withRenderingMode(.alwaysTemplate)
|
|
|
|
qrCodeImageView.image = qrCodeImage
|
|
qrCodeImageView.themeTintColor = qrCodeThemeColor
|
|
backgroundView.themeBackgroundColor = backgroundThemeColor
|
|
}
|
|
|
|
// MARK: -- Pro Badge
|
|
|
|
private func layoutProBadgeView(_ view: UIView?, size: SessionProBadge.Size) {
|
|
guard let badgeView: SessionProBadge = view as? SessionProBadge else { return }
|
|
badgeView.size = size
|
|
let inset: CGFloat = (IconSize.medium.size - size.height) / 2
|
|
badgeView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
|
badgeView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
|
badgeView.pin(.top, to: .top, of: self, withInset: inset)
|
|
badgeView.pin(.bottom, to: .bottom, of: self, withInset: -inset)
|
|
}
|
|
|
|
private func configureProBadgeView(_ view: UIView?, tintColor: ThemeValue) {
|
|
guard let badgeView: SessionProBadge = view as? SessionProBadge else { return }
|
|
badgeView.themeBackgroundColor = tintColor
|
|
}
|
|
|
|
// MARK: -- Icon
|
|
|
|
private func createIconView(using dependencies: Dependencies) -> SessionImageView {
|
|
let result: SessionImageView = SessionImageView(
|
|
dataManager: dependencies[singleton: .imageDataManager]
|
|
)
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.clipsToBounds = true
|
|
result.layer.minificationFilter = .trilinear
|
|
result.layer.magnificationFilter = .trilinear
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutIconView(_ view: UIView?, iconSize: IconSize, shouldFill: Bool, pin edges: [UIView.HorizontalEdge]) {
|
|
guard let imageView: SessionImageView = view as? SessionImageView else { return }
|
|
|
|
imageView.set(.width, to: iconSize.size)
|
|
imageView.set(.height, to: iconSize.size)
|
|
imageView.pin(.top, to: .top, of: self)
|
|
imageView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh)
|
|
|
|
let edgeSet: Set<UIView.HorizontalEdge> = Set(edges)
|
|
let hasLeadingPin = (edgeSet.contains(.leading) || edgeSet.contains(.left))
|
|
let hasTrailingPin = (edgeSet.contains(.trailing) || edgeSet.contains(.right))
|
|
|
|
/// If we want to pin to both edges then we should actually center instead (otherwise this will cause constraint violations)
|
|
if hasLeadingPin && hasTrailingPin {
|
|
imageView.center(.horizontal, in: self)
|
|
}
|
|
else {
|
|
let shouldInvertPadding: [UIView.HorizontalEdge] = [.right, .trailing]
|
|
|
|
for edge in edges {
|
|
let inset: CGFloat = (
|
|
(shouldFill ? 0 : Values.smallSpacing) *
|
|
(shouldInvertPadding.contains(edge) ? -1 : 1)
|
|
)
|
|
|
|
imageView.pin(edge, to: edge, of: self, withInset: inset)
|
|
}
|
|
}
|
|
|
|
fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant)
|
|
minWidthConstraint.isActive = !fixedWidthConstraint.isActive
|
|
}
|
|
|
|
private func configureIconView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Icon, tintColor: ThemeValue) {
|
|
guard let imageView: SessionImageView = view as? SessionImageView else { return }
|
|
|
|
imageView.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
imageView.accessibilityLabel = accessory.accessibility?.label
|
|
imageView.themeTintColor = (accessory.customTint ?? tintColor)
|
|
imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
|
|
|
switch (accessory.icon, accessory.image) {
|
|
case (.some(let icon), _):
|
|
imageView.image = Lucide
|
|
.image(icon: icon, size: accessory.iconSize.size)?
|
|
.withRenderingMode(.alwaysTemplate)
|
|
|
|
case (.none, .some(let image)): imageView.image = image
|
|
case (.none, .none): imageView.image = nil
|
|
}
|
|
}
|
|
|
|
// MARK: -- IconAsync
|
|
|
|
private func configureIconView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.IconAsync, tintColor: ThemeValue) {
|
|
guard let imageView: SessionImageView = view as? SessionImageView else { return }
|
|
|
|
imageView.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
imageView.accessibilityLabel = accessory.accessibility?.label
|
|
imageView.themeTintColor = (accessory.customTint ?? tintColor)
|
|
imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
|
|
|
switch accessory.source {
|
|
case .none: imageView.image = nil
|
|
case .some(let source): imageView.loadImage(source)
|
|
}
|
|
}
|
|
|
|
// MARK: -- Toggle
|
|
|
|
private func createToggleView() -> UISwitch {
|
|
let result: UISwitch = UISwitch()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.isUserInteractionEnabled = false // Triggered by didSelectCell instead
|
|
result.themeOnTintColor = .primary
|
|
result.setContentHugging(to: .required)
|
|
result.setCompressionResistance(to: .required)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutToggleView(_ view: UIView?) {
|
|
guard let toggleSwitch: UISwitch = view as? UISwitch else { return }
|
|
|
|
toggleSwitch.pin(to: self)
|
|
fixedWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureToggleView(
|
|
_ view: UIView?,
|
|
_ accessory: SessionCell.AccessoryConfig.Toggle,
|
|
isEnabled: Bool
|
|
) {
|
|
guard let toggleSwitch: UISwitch = view as? UISwitch else { return }
|
|
|
|
toggleSwitch.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
toggleSwitch.accessibilityLabel = accessory.accessibility?.label
|
|
toggleSwitch.isEnabled = isEnabled
|
|
toggleSwitch.setOn(accessory.oldValue, animated: false)
|
|
|
|
// Dispatch so the cell reload doesn't conflict with the setting change animation
|
|
if accessory.oldValue != accessory.value {
|
|
Task { @MainActor [weak toggleSwitch] in
|
|
try? await Task.sleep(for: .microseconds(10))
|
|
toggleSwitch?.setOn(accessory.value, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -- DropDown
|
|
|
|
private func createDropDownView(maxContentWidth: CGFloat) -> UIView {
|
|
let result: UIStackView = UIStackView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.axis = .horizontal
|
|
result.distribution = .fill
|
|
result.alignment = .center
|
|
result.spacing = Values.verySmallSpacing
|
|
|
|
let imageView: UIImageView = UIImageView(image: UIImage(systemName: "arrowtriangle.down.fill"))
|
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
imageView.themeTintColor = .textPrimary
|
|
imageView.set(.width, to: 10)
|
|
imageView.set(.height, to: 10)
|
|
|
|
let label: UILabel = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.font = .systemFont(ofSize: Values.smallFontSize, weight: .medium)
|
|
label.themeTextColor = .textPrimary
|
|
label.setContentHugging(to: .required)
|
|
label.setCompressionResistance(to: .required)
|
|
label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width
|
|
label.numberOfLines = 0
|
|
|
|
result.addArrangedSubview(imageView)
|
|
result.addArrangedSubview(label)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutDropDownView(_ view: UIView?) {
|
|
guard let stackView: UIStackView = view as? UIStackView else { return }
|
|
|
|
stackView.pin(.top, to: .top, of: self)
|
|
stackView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
|
stackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
|
stackView.pin(.bottom, to: .bottom, of: self)
|
|
minWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureDropDown(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.DropDown) {
|
|
guard
|
|
let stackView: UIStackView = view as? UIStackView,
|
|
let label: UILabel = stackView.arrangedSubviews.last as? UILabel
|
|
else { return }
|
|
|
|
label.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
label.accessibilityLabel = accessory.accessibility?.label
|
|
label.text = accessory.dynamicString()
|
|
}
|
|
|
|
// MARK: -- Radio
|
|
|
|
private func createRadioView() -> UIView {
|
|
let result: UIView = UIView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.isUserInteractionEnabled = false
|
|
result.isAccessibilityElement = true
|
|
result.layer.borderWidth = 1
|
|
|
|
let radioView: UIView = UIView()
|
|
radioView.translatesAutoresizingMaskIntoConstraints = false
|
|
radioView.isUserInteractionEnabled = false
|
|
radioView.isHidden = true
|
|
|
|
result.addSubview(radioView)
|
|
radioView.center(in: result)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutRadioView(_ view: UIView?, size: SessionCell.AccessoryConfig.Radio.Size) {
|
|
guard
|
|
let radioBorderView: UIView = view,
|
|
let radioView: UIView = radioBorderView.subviews.first
|
|
else { return }
|
|
|
|
radioBorderView.layer.cornerRadius = (size.borderSize / 2)
|
|
radioView.layer.cornerRadius = (size.selectionSize / 2)
|
|
radioView.set(.width, to: size.selectionSize)
|
|
radioView.set(.height, to: size.selectionSize)
|
|
radioBorderView.set(.width, to: size.borderSize)
|
|
radioBorderView.set(.height, to: size.borderSize)
|
|
radioBorderView.pin(.top, to: .top, of: self)
|
|
radioBorderView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
|
radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
|
radioBorderView.pin(.bottom, to: .bottom, of: self)
|
|
.setting(priority: .defaultHigh)
|
|
}
|
|
|
|
private func configureRadioView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Radio, isEnabled: Bool) {
|
|
guard
|
|
let radioBorderView: UIView = view,
|
|
let radioView: UIView = radioBorderView.subviews.first
|
|
else { return }
|
|
|
|
let wasOldSelection: Bool = (!accessory.isSelected && accessory.wasSavedSelection)
|
|
|
|
radioBorderView.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
radioBorderView.accessibilityLabel = accessory.accessibility?.label
|
|
|
|
if accessory.isSelected || wasOldSelection {
|
|
radioBorderView.accessibilityTraits.insert(.selected)
|
|
radioBorderView.accessibilityValue = "selected"
|
|
} else {
|
|
radioBorderView.accessibilityTraits.remove(.selected)
|
|
radioBorderView.accessibilityValue = nil
|
|
}
|
|
|
|
radioBorderView.themeBorderColor = {
|
|
guard isEnabled else { return .radioButton_disabledBorder }
|
|
|
|
return (accessory.isSelected ?
|
|
.radioButton_selectedBorder :
|
|
.radioButton_unselectedBorder
|
|
)
|
|
}()
|
|
|
|
radioView.alpha = (wasOldSelection ? 0.3 : 1)
|
|
radioView.isHidden = (!accessory.isSelected && !accessory.wasSavedSelection)
|
|
radioView.themeBackgroundColor = {
|
|
guard isEnabled else {
|
|
return (accessory.isSelected || wasOldSelection ?
|
|
.radioButton_disabledSelectedBackground :
|
|
.radioButton_disabledUnselectedBackground
|
|
)
|
|
}
|
|
|
|
return (accessory.isSelected || wasOldSelection ?
|
|
.radioButton_selectedBackground :
|
|
.radioButton_unselectedBackground
|
|
)
|
|
}()
|
|
}
|
|
|
|
// MARK: -- HighlightingBackgroundLabel
|
|
|
|
private func createHighlightingBackgroundLabelView(maxContentWidth: CGFloat) -> UIView {
|
|
let result: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel()
|
|
result.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutHighlightingBackgroundLabelView(_ view: UIView?) {
|
|
guard let label: SessionHighlightingBackgroundLabel = view as? SessionHighlightingBackgroundLabel else {
|
|
return
|
|
}
|
|
|
|
label.pin(.top, to: .top, of: self)
|
|
label.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
|
label.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
|
label.pin(.bottom, to: .bottom, of: self)
|
|
minWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureHighlightingBackgroundLabelView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.HighlightingBackgroundLabel, tintColor: ThemeValue) {
|
|
guard let label: SessionHighlightingBackgroundLabel = view as? SessionHighlightingBackgroundLabel else {
|
|
return
|
|
}
|
|
|
|
label.isAccessibilityElement = (accessory.accessibility != nil)
|
|
label.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
label.accessibilityLabel = accessory.accessibility?.label
|
|
label.text = accessory.title
|
|
label.themeTextColor = tintColor
|
|
}
|
|
|
|
// MARK: -- HighlightingBackgroundLabelAndRadio
|
|
|
|
private func createHighlightingBackgroundLabelAndRadioView(maxContentWidth: CGFloat) -> UIView {
|
|
let result: UIView = UIView()
|
|
let label: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel()
|
|
let radio: UIView = createRadioView()
|
|
label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width
|
|
|
|
result.addSubview(label)
|
|
result.addSubview(radio)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutHighlightingBackgroundLabelAndRadioView(_ view: UIView?, size: SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio.Size) {
|
|
guard
|
|
let view: UIView = view,
|
|
let label: SessionHighlightingBackgroundLabel = view.subviews.first as? SessionHighlightingBackgroundLabel,
|
|
let radioBorderView: UIView = view.subviews.last,
|
|
let radioView: UIView = radioBorderView.subviews.first
|
|
else { return }
|
|
|
|
label.pin(to: self)
|
|
|
|
label.pin(.top, to: .top, of: self)
|
|
label.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing)
|
|
label.pin(.trailing, to: .leading, of: radioBorderView, withInset: -Values.smallSpacing)
|
|
label.pin(.bottom, to: .bottom, of: self)
|
|
|
|
radioBorderView.layer.cornerRadius = (size.borderSize / 2)
|
|
radioView.layer.cornerRadius = (size.selectionSize / 2)
|
|
radioView.set(.width, to: size.selectionSize)
|
|
radioView.set(.height, to: size.selectionSize)
|
|
radioBorderView.set(.width, to: size.borderSize)
|
|
radioBorderView.set(.height, to: size.borderSize)
|
|
radioBorderView.center(.vertical, in: self)
|
|
radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing)
|
|
minWidthConstraint.isActive = true
|
|
|
|
view.pin(to: self)
|
|
}
|
|
|
|
private func configureHighlightingBackgroundLabelAndRadioView(
|
|
_ view: UIView?,
|
|
_ accessory: SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio,
|
|
tintColor: ThemeValue,
|
|
isEnabled: Bool
|
|
) {
|
|
guard
|
|
let view: UIView = view,
|
|
let label: SessionHighlightingBackgroundLabel = view.subviews.first as? SessionHighlightingBackgroundLabel,
|
|
let radioBorderView: UIView = view.subviews.last,
|
|
let radioView: UIView = radioBorderView.subviews.first
|
|
else { return }
|
|
|
|
let wasOldSelection: Bool = (!accessory.isSelected && accessory.wasSavedSelection)
|
|
|
|
label.isAccessibilityElement = (accessory.accessibility != nil)
|
|
label.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
label.accessibilityLabel = accessory.accessibility?.label
|
|
label.text = accessory.title
|
|
label.themeTextColor = tintColor
|
|
|
|
radioBorderView.isAccessibilityElement = true
|
|
radioBorderView.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
radioBorderView.accessibilityLabel = accessory.accessibility?.label
|
|
|
|
if accessory.isSelected || wasOldSelection {
|
|
radioView.accessibilityTraits.insert(.selected)
|
|
radioView.accessibilityValue = "selected"
|
|
} else {
|
|
radioView.accessibilityTraits.remove(.selected)
|
|
radioView.accessibilityValue = nil
|
|
}
|
|
|
|
radioBorderView.themeBorderColor = {
|
|
guard isEnabled else { return .radioButton_disabledBorder }
|
|
|
|
return (accessory.isSelected ?
|
|
.radioButton_selectedBorder :
|
|
.radioButton_unselectedBorder
|
|
)
|
|
}()
|
|
radioView.alpha = (wasOldSelection ? 0.3 : 1)
|
|
radioView.isHidden = (!accessory.isSelected && !accessory.wasSavedSelection)
|
|
radioView.themeBackgroundColor = {
|
|
guard isEnabled else {
|
|
return (accessory.isSelected || wasOldSelection ?
|
|
.radioButton_disabledSelectedBackground :
|
|
.radioButton_disabledUnselectedBackground
|
|
)
|
|
}
|
|
|
|
return (accessory.isSelected || wasOldSelection ?
|
|
.radioButton_selectedBackground :
|
|
.radioButton_unselectedBackground
|
|
)
|
|
}()
|
|
}
|
|
|
|
// MARK: -- DisplayPicture
|
|
|
|
private func createDisplayPictureView() -> ProfilePictureView {
|
|
return ProfilePictureView(size: .list, dataManager: nil)
|
|
}
|
|
|
|
private func layoutDisplayPictureView(_ view: UIView?, size: ProfilePictureView.Info.Size) {
|
|
guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return }
|
|
|
|
profilePictureView.size = size
|
|
profilePictureView.pin(.top, to: .top, of: self)
|
|
profilePictureView.pin(.leading, to: .leading, of: self)
|
|
profilePictureView.pin(.trailing, to: .trailing, of: self)
|
|
profilePictureView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh)
|
|
fixedWidthConstraint.constant = (size.viewSize)
|
|
fixedWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureDisplayPictureView(
|
|
_ view: UIView?,
|
|
_ accessory: SessionCell.AccessoryConfig.DisplayPicture,
|
|
using dependencies: Dependencies
|
|
) {
|
|
guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return }
|
|
|
|
profilePictureView.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
profilePictureView.accessibilityLabel = accessory.accessibility?.label
|
|
profilePictureView.isAccessibilityElement = (accessory.accessibility != nil)
|
|
profilePictureView.setDataManager(dependencies[singleton: .imageDataManager])
|
|
profilePictureView.update(
|
|
publicKey: accessory.id,
|
|
threadVariant: accessory.threadVariant,
|
|
displayPictureUrl: accessory.displayPictureUrl,
|
|
profile: accessory.profile,
|
|
profileIcon: accessory.profileIcon,
|
|
additionalProfile: accessory.additionalProfile,
|
|
additionalProfileIcon: accessory.additionalProfileIcon,
|
|
using: dependencies
|
|
)
|
|
}
|
|
|
|
// MARK: -- Search
|
|
|
|
private func createSearchView() -> ContactsSearchBar {
|
|
let result: ContactsSearchBar = ContactsSearchBar()
|
|
result.themeTintColor = .textPrimary
|
|
result.themeBackgroundColor = .clear
|
|
result.searchTextField.themeBackgroundColor = .backgroundSecondary
|
|
result.delegate = self
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutSearchView(_ view: UIView?) {
|
|
guard let searchBar: ContactsSearchBar = view as? ContactsSearchBar else { return }
|
|
|
|
searchBar.pin(.top, to: .top, of: self)
|
|
searchBar.pin(.leading, to: .leading, of: self, withInset: -8) // Removing default inset
|
|
searchBar.pin(.trailing, to: .trailing, of: self, withInset: 8) // Removing default inset
|
|
searchBar.pin(.bottom, to: .bottom, of: self)
|
|
}
|
|
|
|
private func configureSearchView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Search) {
|
|
guard let searchBar: ContactsSearchBar = view as? ContactsSearchBar else { return }
|
|
|
|
self.searchTermChanged = accessory.searchTermChanged
|
|
searchBar.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
searchBar.accessibilityLabel = accessory.accessibility?.label
|
|
searchBar.placeholder = accessory.placeholder
|
|
}
|
|
|
|
// MARK: -- Button
|
|
|
|
private func createButtonView() -> SessionButton {
|
|
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
|
result.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
|
|
|
return result
|
|
}
|
|
|
|
private func layoutButtonView(_ view: UIView?) {
|
|
guard let button: SessionButton = view as? SessionButton else { return }
|
|
|
|
button.pin(.top, to: .top, of: self)
|
|
button.pin(.leading, to: .leading, of: self)
|
|
button.pin(.trailing, to: .trailing, of: self)
|
|
button.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh)
|
|
minWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureButtonView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Button) {
|
|
guard let button: SessionButton = view as? SessionButton else { return }
|
|
|
|
self.onTap = accessory.run
|
|
button.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
button.accessibilityLabel = accessory.accessibility?.label
|
|
button.setTitle(accessory.title, for: .normal)
|
|
button.style = accessory.style
|
|
button.isHidden = false
|
|
}
|
|
|
|
// MARK: -- ActivityIndicator
|
|
|
|
private func createActivityIndicatorView() -> UIActivityIndicatorView {
|
|
return UIActivityIndicatorView(style: .medium)
|
|
}
|
|
|
|
private func layoutActivityIndicatorView(_ view: UIView?) {
|
|
guard let indicator: UIActivityIndicatorView = view as? UIActivityIndicatorView else { return }
|
|
|
|
indicator.pin(.top, to: .top, of: self)
|
|
indicator.pin(.leading, to: .leading, of: self)
|
|
indicator.pin(.trailing, to: .trailing, of: self)
|
|
indicator.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh)
|
|
minWidthConstraint.isActive = true
|
|
}
|
|
|
|
private func configureActivityIndicatorView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.ActivityIndicator) {
|
|
guard let indicator: UIActivityIndicatorView = view as? UIActivityIndicatorView else { return }
|
|
|
|
indicator.themeColor = accessory.themeColor
|
|
indicator.startAnimating()
|
|
}
|
|
|
|
// MARK: -- Custom
|
|
|
|
private func layoutCustomView(_ view: UIView?, size: SessionCell.Accessory.Size) {
|
|
guard let view: UIView = view else { return }
|
|
|
|
switch size {
|
|
case .fixed(let width, let height):
|
|
view.set(.width, to: width)
|
|
view.set(.height, to: height)
|
|
fixedWidthConstraint.isActive = (width <= fixedWidthConstraint.constant)
|
|
minWidthConstraint.isActive = !fixedWidthConstraint.isActive
|
|
|
|
case .minWidth(let height):
|
|
view.set(.width, to: .width, of: self)
|
|
view.set(.height, to: height)
|
|
fixedWidthConstraint.isActive = true
|
|
|
|
case .fillWidth(let height):
|
|
view.set(.width, to: .width, of: self)
|
|
view.set(.height, to: height)
|
|
minWidthConstraint.isActive = true
|
|
|
|
case .fillWidthWrapHeight:
|
|
view.set(.width, to: .width, of: self)
|
|
view.setContentHugging(.vertical, to: .required)
|
|
view.setCompressionResistance(.vertical, to: .required)
|
|
minWidthConstraint.isActive = true
|
|
}
|
|
|
|
view.pin(.top, to: .top, of: self)
|
|
view.pin(.leading, to: .leading, of: self)
|
|
view.pin(.trailing, to: .trailing, of: self)
|
|
view.pin(.bottom, to: .bottom, of: self)
|
|
}
|
|
|
|
private func configureCustomView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.AnyCustom) {
|
|
view?.accessibilityIdentifier = accessory.accessibility?.identifier
|
|
view?.accessibilityLabel = accessory.accessibility?.label
|
|
}
|
|
}
|
|
}
|