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.
452 lines
17 KiB
Swift
452 lines
17 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SessionUtilitiesKit
|
|
|
|
public class SessionCell: UITableViewCell {
|
|
public static let cornerRadius: CGFloat = 17
|
|
|
|
public enum Style {
|
|
case rounded
|
|
case roundedEdgeToEdge
|
|
case edgeToEdge
|
|
}
|
|
|
|
/// This value is here to allow the theming update callback to be released when preparing for reuse
|
|
private var instanceView: UIView = UIView()
|
|
private var position: Position?
|
|
private var subtitleExtraView: UIView?
|
|
private var onExtraActionTap: (() -> Void)?
|
|
|
|
// MARK: - UI
|
|
|
|
private var backgroundLeftConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private var backgroundRightConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private var topSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private var topSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private var botSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private var botSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
|
private lazy var leftAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leftAccessoryView)
|
|
private lazy var rightAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: rightAccessoryView)
|
|
|
|
private let cellBackgroundView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.clipsToBounds = true
|
|
result.themeBackgroundColor = .settings_tabBackground
|
|
|
|
return result
|
|
}()
|
|
|
|
private let cellSelectedBackgroundView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.themeBackgroundColor = .highlighted(.settings_tabBackground)
|
|
result.alpha = 0
|
|
|
|
return result
|
|
}()
|
|
|
|
private let topSeparator: UIView = {
|
|
let result: UIView = UIView.separator()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private let contentStackView: UIStackView = {
|
|
let result: UIStackView = UIStackView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.axis = .horizontal
|
|
result.distribution = .fill
|
|
result.alignment = .center
|
|
result.spacing = Values.mediumSpacing
|
|
result.isLayoutMarginsRelativeArrangement = true
|
|
|
|
return result
|
|
}()
|
|
|
|
public let leftAccessoryView: AccessoryView = {
|
|
let result: AccessoryView = AccessoryView()
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private let titleStackView: UIStackView = {
|
|
let result: UIStackView = UIStackView()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.axis = .vertical
|
|
result.distribution = .equalSpacing
|
|
result.alignment = .fill
|
|
result.setCompressionResistanceHorizontalLow()
|
|
result.setContentHuggingLow()
|
|
|
|
return result
|
|
}()
|
|
|
|
private let titleLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.font = .boldSystemFont(ofSize: 15)
|
|
result.themeTextColor = .textPrimary
|
|
result.numberOfLines = 0
|
|
result.setCompressionResistanceHorizontalLow()
|
|
result.setContentHuggingLow()
|
|
|
|
return result
|
|
}()
|
|
|
|
private let subtitleLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.font = .systemFont(ofSize: 13)
|
|
result.themeTextColor = .textPrimary
|
|
result.numberOfLines = 0
|
|
result.isHidden = true
|
|
result.setCompressionResistanceHorizontalLow()
|
|
result.setContentHuggingLow()
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var extraActionTopSpacingView: UIView = UIView.spacer(withHeight: Values.smallSpacing)
|
|
|
|
private lazy var extraActionButton: UIButton = {
|
|
let result: UIButton = UIButton()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.titleLabel?.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
|
result.titleLabel?.numberOfLines = 0
|
|
result.contentHorizontalAlignment = .left
|
|
result.contentEdgeInsets = UIEdgeInsets(
|
|
top: 8,
|
|
left: 0,
|
|
bottom: 0,
|
|
right: 0
|
|
)
|
|
result.addTarget(self, action: #selector(extraActionTapped), for: .touchUpInside)
|
|
result.isHidden = true
|
|
|
|
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
|
|
switch theme.interfaceStyle {
|
|
case .light: result?.setThemeTitleColor(.textPrimary, for: .normal)
|
|
default: result?.setThemeTitleColor(.primary, for: .normal)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}()
|
|
|
|
public let rightAccessoryView: AccessoryView = {
|
|
let result: AccessoryView = AccessoryView()
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private let botSeparator: UIView = {
|
|
let result: UIView = UIView.separator()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.isHidden = true
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: - Initialization
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
setupViewHierarchy()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
|
|
setupViewHierarchy()
|
|
}
|
|
|
|
private func setupViewHierarchy() {
|
|
self.themeBackgroundColor = .clear
|
|
self.selectedBackgroundView = UIView()
|
|
|
|
contentView.addSubview(cellBackgroundView)
|
|
cellBackgroundView.addSubview(cellSelectedBackgroundView)
|
|
cellBackgroundView.addSubview(topSeparator)
|
|
cellBackgroundView.addSubview(contentStackView)
|
|
cellBackgroundView.addSubview(botSeparator)
|
|
|
|
contentStackView.addArrangedSubview(leftAccessoryView)
|
|
contentStackView.addArrangedSubview(titleStackView)
|
|
contentStackView.addArrangedSubview(rightAccessoryView)
|
|
|
|
titleStackView.addArrangedSubview(titleLabel)
|
|
titleStackView.addArrangedSubview(subtitleLabel)
|
|
titleStackView.addArrangedSubview(extraActionTopSpacingView)
|
|
titleStackView.addArrangedSubview(extraActionButton)
|
|
|
|
setupLayout()
|
|
}
|
|
|
|
private func setupLayout() {
|
|
cellBackgroundView.pin(.top, to: .top, of: contentView)
|
|
backgroundLeftConstraint = cellBackgroundView.pin(.leading, to: .leading, of: contentView)
|
|
backgroundRightConstraint = cellBackgroundView.pin(.trailing, to: .trailing, of: contentView)
|
|
cellBackgroundView.pin(.bottom, to: .bottom, of: contentView)
|
|
|
|
cellSelectedBackgroundView.pin(to: cellBackgroundView)
|
|
|
|
topSeparator.pin(.top, to: .top, of: cellBackgroundView)
|
|
topSeparatorLeftConstraint = topSeparator.pin(.left, to: .left, of: cellBackgroundView)
|
|
topSeparatorRightConstraint = topSeparator.pin(.right, to: .right, of: cellBackgroundView)
|
|
|
|
contentStackView.pin(to: cellBackgroundView)
|
|
|
|
botSeparatorLeftConstraint = botSeparator.pin(.left, to: .left, of: cellBackgroundView)
|
|
botSeparatorRightConstraint = botSeparator.pin(.right, to: .right, of: cellBackgroundView)
|
|
botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView)
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
// Need to force the contentStackView to layout if needed as it might not have updated it's
|
|
// sizing yet
|
|
self.contentStackView.layoutIfNeeded()
|
|
|
|
// Position the 'subtitleExtraView' at the end of the last line of text
|
|
if
|
|
let subtitleExtraView: UIView = self.subtitleExtraView,
|
|
let subtitle: String = subtitleLabel.text,
|
|
let font: UIFont = subtitleLabel.font
|
|
{
|
|
let layoutManager: NSLayoutManager = NSLayoutManager()
|
|
let textStorage = NSTextStorage(
|
|
attributedString: NSAttributedString(
|
|
string: subtitle,
|
|
attributes: [ .font: font ]
|
|
)
|
|
)
|
|
textStorage.addLayoutManager(layoutManager)
|
|
|
|
let textContainer: NSTextContainer = NSTextContainer(
|
|
size: CGSize(
|
|
width: subtitleLabel.bounds.size.width,
|
|
height: 999
|
|
)
|
|
)
|
|
textContainer.lineFragmentPadding = 0
|
|
layoutManager.addTextContainer(textContainer)
|
|
|
|
var glyphRange: NSRange = NSRange()
|
|
layoutManager.characterRange(
|
|
forGlyphRange: NSRange(location: subtitle.glyphCount - 1, length: 1),
|
|
actualGlyphRange: &glyphRange
|
|
)
|
|
let lastGlyphRect: CGRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
|
|
// Remove and re-add the 'subtitleExtraView' to clear any old constraints
|
|
subtitleExtraView.removeFromSuperview()
|
|
contentView.addSubview(subtitleExtraView)
|
|
|
|
subtitleExtraView.pin(
|
|
.top,
|
|
to: .top,
|
|
of: subtitleLabel,
|
|
withInset: (lastGlyphRect.minY + ((lastGlyphRect.height / 2) - (subtitleExtraView.bounds.height / 2)))
|
|
)
|
|
subtitleExtraView.pin(
|
|
.leading,
|
|
to: .leading,
|
|
of: subtitleLabel,
|
|
withInset: lastGlyphRect.maxX
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Content
|
|
|
|
public override func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
|
|
self.instanceView = UIView()
|
|
self.position = nil
|
|
self.onExtraActionTap = nil
|
|
self.accessibilityIdentifier = nil
|
|
|
|
leftAccessoryView.prepareForReuse()
|
|
leftAccessoryFillConstraint.isActive = false
|
|
titleLabel.text = ""
|
|
titleLabel.themeTextColor = .textPrimary
|
|
subtitleLabel.text = ""
|
|
subtitleLabel.themeTextColor = .textPrimary
|
|
rightAccessoryView.prepareForReuse()
|
|
rightAccessoryFillConstraint.isActive = false
|
|
|
|
topSeparator.isHidden = true
|
|
subtitleLabel.isHidden = true
|
|
extraActionTopSpacingView.isHidden = true
|
|
extraActionButton.setTitle("", for: .normal)
|
|
extraActionButton.isHidden = true
|
|
botSeparator.isHidden = true
|
|
|
|
subtitleExtraView?.removeFromSuperview()
|
|
subtitleExtraView = nil
|
|
}
|
|
|
|
public func update<ID: Hashable & Differentiable>(
|
|
with info: Info<ID>,
|
|
style: Style,
|
|
position: Position
|
|
) {
|
|
self.instanceView = UIView()
|
|
self.position = position
|
|
self.subtitleExtraView = info.subtitleExtraViewGenerator?()
|
|
self.onExtraActionTap = info.extraAction?.onTap
|
|
self.accessibilityIdentifier = info.accessibilityIdentifier
|
|
self.accessibilityLabel = info.accessibilityLabel
|
|
self.isAccessibilityElement = true
|
|
|
|
let leftFitToEdge: Bool = (info.leftAccessory?.shouldFitToEdge == true)
|
|
let rightFitToEdge: Bool = (!leftFitToEdge && info.rightAccessory?.shouldFitToEdge == true)
|
|
leftAccessoryFillConstraint.isActive = leftFitToEdge
|
|
leftAccessoryView.update(
|
|
with: info.leftAccessory,
|
|
tintColor: info.tintColor,
|
|
isEnabled: info.isEnabled,
|
|
accessibilityLabel: info.leftAccessoryAccessibilityLabel
|
|
)
|
|
rightAccessoryView.update(
|
|
with: info.rightAccessory,
|
|
tintColor: info.tintColor,
|
|
isEnabled: info.isEnabled,
|
|
accessibilityLabel: info.rightAccessoryAccessibilityLabel
|
|
)
|
|
rightAccessoryFillConstraint.isActive = rightFitToEdge
|
|
contentStackView.layoutMargins = UIEdgeInsets(
|
|
top: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing),
|
|
left: (leftFitToEdge ? 0 : Values.largeSpacing),
|
|
bottom: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing),
|
|
right: (rightFitToEdge ? 0 : Values.largeSpacing)
|
|
)
|
|
|
|
titleLabel.text = info.title
|
|
titleLabel.themeTextColor = info.tintColor
|
|
subtitleLabel.text = info.subtitle
|
|
subtitleLabel.themeTextColor = info.tintColor
|
|
subtitleLabel.isHidden = (info.subtitle == nil)
|
|
extraActionTopSpacingView.isHidden = (info.extraAction == nil)
|
|
extraActionButton.setTitle(info.extraAction?.title, for: .normal)
|
|
extraActionButton.isHidden = (info.extraAction == nil)
|
|
|
|
// Styling and positioning
|
|
let defaultEdgePadding: CGFloat
|
|
cellBackgroundView.themeBackgroundColor = (info.shouldHaveBackground ?
|
|
.settings_tabBackground :
|
|
nil
|
|
)
|
|
cellSelectedBackgroundView.isHidden = (!info.isEnabled || !info.shouldHaveBackground)
|
|
|
|
switch style {
|
|
case .rounded:
|
|
defaultEdgePadding = Values.mediumSpacing
|
|
backgroundLeftConstraint.constant = Values.largeSpacing
|
|
backgroundRightConstraint.constant = -Values.largeSpacing
|
|
cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius
|
|
|
|
case .edgeToEdge:
|
|
defaultEdgePadding = 0
|
|
backgroundLeftConstraint.constant = 0
|
|
backgroundRightConstraint.constant = 0
|
|
cellBackgroundView.layer.cornerRadius = 0
|
|
|
|
case .roundedEdgeToEdge:
|
|
defaultEdgePadding = Values.mediumSpacing
|
|
backgroundLeftConstraint.constant = 0
|
|
backgroundRightConstraint.constant = 0
|
|
cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius
|
|
}
|
|
|
|
let fittedEdgePadding: CGFloat = {
|
|
func targetSize(accessory: Accessory?) -> CGFloat {
|
|
switch accessory {
|
|
case .icon(_, let iconSize, _, _), .iconAsync(let iconSize, _, _, _):
|
|
return iconSize.size
|
|
|
|
default: return defaultEdgePadding
|
|
}
|
|
}
|
|
|
|
guard leftFitToEdge else {
|
|
guard rightFitToEdge else { return defaultEdgePadding }
|
|
|
|
return targetSize(accessory: info.rightAccessory)
|
|
}
|
|
|
|
return targetSize(accessory: info.leftAccessory)
|
|
}()
|
|
topSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding)
|
|
topSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding)
|
|
botSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding)
|
|
botSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding)
|
|
|
|
switch position {
|
|
case .top:
|
|
cellBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
topSeparator.isHidden = (style != .edgeToEdge)
|
|
botSeparator.isHidden = false
|
|
|
|
case .middle:
|
|
cellBackgroundView.layer.maskedCorners = []
|
|
topSeparator.isHidden = true
|
|
botSeparator.isHidden = false
|
|
|
|
case .bottom:
|
|
cellBackgroundView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
topSeparator.isHidden = false
|
|
botSeparator.isHidden = (style != .edgeToEdge)
|
|
|
|
case .individual:
|
|
cellBackgroundView.layer.maskedCorners = [
|
|
.layerMinXMinYCorner, .layerMaxXMinYCorner,
|
|
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
|
|
]
|
|
topSeparator.isHidden = (style != .edgeToEdge)
|
|
botSeparator.isHidden = (style != .edgeToEdge)
|
|
}
|
|
}
|
|
|
|
public func update(isEditing: Bool, animated: Bool) {}
|
|
|
|
// MARK: - Interaction
|
|
|
|
public override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
|
super.setHighlighted(highlighted, animated: animated)
|
|
|
|
// If the 'cellSelectedBackgroundView' is hidden then there is no background so we
|
|
// should update the titleLabel to indicate the highlighted state
|
|
if cellSelectedBackgroundView.isHidden {
|
|
titleLabel.alpha = (highlighted ? 0.8 : 1)
|
|
}
|
|
|
|
cellSelectedBackgroundView.alpha = (highlighted ? 1 : 0)
|
|
leftAccessoryView.setHighlighted(highlighted, animated: animated)
|
|
rightAccessoryView.setHighlighted(highlighted, animated: animated)
|
|
}
|
|
|
|
public override func setSelected(_ selected: Bool, animated: Bool) {
|
|
super.setSelected(selected, animated: animated)
|
|
|
|
leftAccessoryView.setSelected(selected, animated: animated)
|
|
rightAccessoryView.setSelected(selected, animated: animated)
|
|
}
|
|
|
|
@objc private func extraActionTapped() {
|
|
onExtraActionTap?()
|
|
}
|
|
}
|