// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import Combine import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit class SessionAvatarCell: UITableViewCell { var disposables: Set = Set() private var originalInputValue: String? // 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() } // MARK: - UI private let stackView: UIStackView = { let stackView: UIStackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = Values.mediumSpacing stackView.alignment = .center stackView.distribution = .equalSpacing let horizontalSpacing: CGFloat = (UIScreen.main.bounds.size.height < 568 ? Values.largeSpacing : Values.veryLargeSpacing ) stackView.layoutMargins = UIEdgeInsets( top: Values.mediumSpacing, leading: horizontalSpacing, bottom: Values.largeSpacing, trailing: horizontalSpacing ) stackView.isLayoutMarginsRelativeArrangement = true return stackView }() fileprivate let profilePictureView: ProfilePictureView = { let view: ProfilePictureView = ProfilePictureView() view.accessibilityLabel = "Profile picture" view.isAccessibilityElement = true view.translatesAutoresizingMaskIntoConstraints = false view.size = Values.largeProfilePictureSize return view }() fileprivate let displayNameContainer: UIView = { let view: UIView = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.accessibilityIdentifier = "Username" view.accessibilityLabel = "Username" view.isAccessibilityElement = true return view }() private lazy var displayNameLabel: UILabel = { let label: UILabel = UILabel() label.isAccessibilityElement = true label.translatesAutoresizingMaskIntoConstraints = false label.font = .ows_mediumFont(withSize: Values.veryLargeFontSize) label.themeTextColor = .textPrimary label.textAlignment = .center label.lineBreakMode = .byTruncatingTail label.numberOfLines = 0 return label }() fileprivate let displayNameTextField: UITextField = { let textField: TextField = TextField(placeholder: "Enter a name", usesDefaultHeight: false) textField.translatesAutoresizingMaskIntoConstraints = false textField.textAlignment = .center textField.accessibilityIdentifier = "Nickname" textField.accessibilityLabel = "Nickname" textField.isAccessibilityElement = true textField.alpha = 0 return textField }() private let descriptionSeparator: Separator = { let result: Separator = Separator() result.isHidden = true return result }() private let descriptionLabel: SRCopyableLabel = { let label: SRCopyableLabel = SRCopyableLabel() label.accessibilityLabel = "Session ID" label.translatesAutoresizingMaskIntoConstraints = false label.themeTextColor = .textPrimary label.textAlignment = .center label.lineBreakMode = .byCharWrapping label.numberOfLines = 0 return label }() private let descriptionActionStackView: UIStackView = { let stackView: UIStackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.alignment = .center stackView.distribution = .fillEqually stackView.spacing = (UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing) return stackView }() private func setupViewHierarchy() { self.themeBackgroundColor = nil self.selectedBackgroundView = UIView() contentView.addSubview(stackView) stackView.addArrangedSubview(profilePictureView) stackView.addArrangedSubview(displayNameContainer) stackView.addArrangedSubview(descriptionSeparator) stackView.addArrangedSubview(descriptionLabel) stackView.addArrangedSubview(descriptionActionStackView) displayNameContainer.addSubview(displayNameLabel) displayNameContainer.addSubview(displayNameTextField) setupLayout() } // MARK: - Layout private func setupLayout() { stackView.pin(to: contentView) profilePictureView.set(.width, to: profilePictureView.size) profilePictureView.set(.height, to: profilePictureView.size) displayNameLabel.pin(to: displayNameContainer) displayNameTextField.center(in: displayNameContainer) displayNameTextField.widthAnchor .constraint( lessThanOrEqualTo: stackView.widthAnchor, constant: -(stackView.layoutMargins.left + stackView.layoutMargins.right) ) .isActive = true descriptionSeparator.set( .width, to: .width, of: stackView, withOffset: -(stackView.layoutMargins.left + stackView.layoutMargins.right) ) descriptionActionStackView.set( .width, to: .width, of: stackView, withOffset: -(stackView.layoutMargins.left + stackView.layoutMargins.right) ) } // MARK: - Content override func prepareForReuse() { super.prepareForReuse() self.disposables = Set() self.originalInputValue = nil self.displayNameLabel.text = nil self.displayNameTextField.text = nil self.descriptionLabel.font = .ows_lightFont(withSize: Values.smallFontSize) self.descriptionLabel.text = nil self.descriptionSeparator.isHidden = true self.descriptionActionStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } } func update( threadViewModel: SessionThreadViewModel, style: SessionCell.Accessory.ThreadInfoStyle, viewController: UIViewController ) { profilePictureView.update( publicKey: threadViewModel.threadId, profile: threadViewModel.profile, additionalProfile: threadViewModel.additionalProfile, threadVariant: threadViewModel.threadVariant, openGroupProfilePictureData: threadViewModel.openGroupProfilePictureData, useFallbackPicture: ( threadViewModel.threadVariant == .openGroup && threadViewModel.openGroupProfilePictureData == nil ), showMultiAvatarForClosedGroup: true ) originalInputValue = threadViewModel.profile?.nickname displayNameLabel.text = { guard !threadViewModel.threadIsNoteToSelf else { guard let profile: Profile = threadViewModel.profile else { return Profile.truncated(id: threadViewModel.threadId, truncating: .middle) } return profile.displayName() } return threadViewModel.displayName }() descriptionLabel.font = { switch style.descriptionStyle { case .small: return .ows_lightFont(withSize: Values.smallFontSize) case .monoSmall: return Fonts.spaceMono(ofSize: Values.smallFontSize) case .monoLarge: return Fonts.spaceMono( ofSize: (isIPhone5OrSmaller ? Values.mediumFontSize : Values.largeFontSize) ) } }() descriptionLabel.text = threadViewModel.threadId descriptionLabel.isHidden = (threadViewModel.threadVariant != .contact) descriptionLabel.isUserInteractionEnabled = ( threadViewModel.threadVariant == .contact || threadViewModel.threadVariant == .openGroup ) displayNameTextField.text = threadViewModel.profile?.nickname descriptionSeparator.update(title: style.separatorTitle) descriptionSeparator.isHidden = (style.separatorTitle == nil) if (UIDevice.current.isIPad) { descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer()) } style.descriptionActions.forEach { action in let result: SessionButton = SessionButton(style: .bordered, size: .medium) result.setTitle(action.title, for: UIControl.State.normal) result.tapPublisher .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak result] _ in action.run(result) }) .store(in: &self.disposables) descriptionActionStackView.addArrangedSubview(result) } if (UIDevice.current.isIPad) { descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer()) } descriptionActionStackView.isHidden = style.descriptionActions.isEmpty } func update(isEditing: Bool, animated: Bool) { let changes = { [weak self] in self?.displayNameLabel.alpha = (isEditing ? 0 : 1) self?.displayNameTextField.alpha = (isEditing ? 1 : 0) } let completion: (Bool) -> Void = { [weak self] complete in self?.displayNameTextField.text = self?.originalInputValue self?.displayNameContainer.accessibilityLabel = self?.displayNameLabel.text } if animated { UIView.animate(withDuration: 0.25, animations: changes, completion: completion) } else { changes() completion(true) } if isEditing { displayNameTextField.becomeFirstResponder() } else { displayNameTextField.resignFirstResponder() } } } // MARK: - Compose extension CombineCompatible where Self: SessionAvatarCell { var textPublisher: AnyPublisher { return self.displayNameTextField.publisher(for: .editingChanged) .map { textField -> String in (textField.text ?? "") } .eraseToAnyPublisher() } var displayNameTapPublisher: AnyPublisher { return self.displayNameContainer.tapPublisher .map { _ in () } .eraseToAnyPublisher() } var profilePictureTapPublisher: AnyPublisher { return self.profilePictureView.tapPublisher .map { _ in () } .eraseToAnyPublisher() } }