Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/SharedUI/Details/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 17 kB image not shown  

Quelle  DetailsHeaderProfileView.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import ThreemaFramework
import TipKit
import UIKit

// MARK: - DetailsHeaderProfileView.Configuration

extension DetailsHeaderProfileView {
    struct Configuration: DetailsConfiguration {
        /// Show debug background colors
        let debug = false
        
        let defaultSpacing: CGFloat = 8
        
        let customSpacingAfterProfilePicture: CGFloat = 10

        let verificationLevelHeight: CGFloat = 12
        
        let collapsedLineLimit = 2
    }
}

final class DetailsHeaderProfileView: UIStackView {
    
    /// Entity agnostic header configuration
    struct ContentConfiguration {
        /// Info for profile picture view
        let profilePictureInfo: ProfilePictureImageView.Info
        /// Name shown
        let name: String
        /// Provide image for verification level if any
        let verificationLevelImage: UIImage?
        /// Provide accessibility description for verification level if any.
        /// This should be set if `verificationLevelImage` is not `nil`.
        let verificationLevelAccessibilityLabel: String?
        /// Provides availabilityStatus in case of configuration of a contact profile.
        let availabilityStatus: WorkAvailabilityStatus?
        /// Provides bool if user is member of the group
        let isSelfMember: Bool
        
        init(
            profilePictureInfo: ProfilePictureImageView.Info,
            name: String,
            verificationLevelImage: UIImage? = nil,
            verificationLevelAccessibilityLabel: String? = nil,
            availabilityStatus: WorkAvailabilityStatus? = nil,
            isSelfMember: Bool = true
        ) {
            self.profilePictureInfo = profilePictureInfo
            self.name = name
            self.verificationLevelImage = verificationLevelImage
            self.verificationLevelAccessibilityLabel = verificationLevelAccessibilityLabel
            self.availabilityStatus = availabilityStatus
            self.isSelfMember = isSelfMember
        }
    }
    
    var contentConfiguration: ContentConfiguration {
        didSet {
            updateContent()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        if isAvailabilityStatusVisible {
            if availabilityStatusLabel.exceeds(
                lines: DetailsHeaderProfileView.configuration.collapsedLineLimit,
                availableWidth: bounds.width
            ) {
                availabilityStatusExpandImage.isHidden = false
            }
            else {
                availabilityStatusExpandImage.isHidden = true
            }
            
            if #available(iOS 26.0, *) {
                availabilityStatusView.cornerConfiguration = .uniformCorners(
                    radius: UICornerRadius(floatLiteral: 30)
                )
            }
            else {
                availabilityStatusView.layer.cornerRadius = 12
            }
        }
    }

    // MARK: - Private properties
    
    private static let configuration = Configuration()

    // We need to hold on to the observers until this object is deallocated.
    // `invalidate()` is automatically called on destruction of the observers
    // (according to the `invalidate()` header documentation).
    private var observers = [NSKeyValueObservation]()
        
    private var tipObservationTask: Task<Void, Never>?

    // MARK: Gesture recognizer
    
    private let profilePictureTappedHandler: () -> Void
    private lazy var tappedProfilePictureGestureRecognizer = UITapGestureRecognizer(
        target: self,
        action: #selector(profilePictureTapped)
    )
    
    // MARK: Subviews

    private var isAvailabilityStatusExpanded = false

    /// Profile picture of contact or group
    private lazy var profilePictureView: ProfilePictureImageView = {
        let imageView = ProfilePictureImageView(iconConfiguration: .small)

        imageView.heightAnchor.constraint(equalToConstant: DetailsHeaderProfileView.configuration.profilePictureSize)
            .isActive = true
       
        // Configure full screen image gesture recognizer
        imageView.addGestureRecognizer(tappedProfilePictureGestureRecognizer)
        imageView.isUserInteractionEnabled = true
        imageView.backgroundColor = .clear
        imageView.isAccessibilityElement = false
        
        return imageView
    }()
    
    /// Name of contact or group
    private lazy var nameLabel: CopyLabel = {
        let label = CopyLabel()
        
        label.numberOfLines = 0
        label.textAlignment = .center
        
        label.setContentHuggingPriority(.defaultHigh, for: .vertical)
        
        // This needs to be set to a nonempty string (ideally the final string) otherwise
        // `systemLayoutSizeFitting(_:)` might return the wrong height. This is due to the fact that
        // the final text is only assigned when a configuration is applied.
        label.text = " "
                
        return label
    }()
    
    /// Verification level image of person
    private lazy var verificationLevelImageView: UIImageView = {
        let imageView = UIImageView()
        
        imageView.contentMode = .scaleAspectFit
        imageView.heightAnchor
            .constraint(equalToConstant: DetailsHeaderProfileView.configuration.verificationLevelHeight).isActive = true
                
        return imageView
    }()

    private lazy var availabilityStatusLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = DetailsHeaderProfileView.configuration.collapsedLineLimit
        label.textAlignment = .center
        label.adjustsFontForContentSizeCategory = true
        label.maximumContentSizeCategory = .extraExtraExtraLarge
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textColor = .label
        label.font = .preferredFont(forTextStyle: .body)
        label.setContentHuggingPriority(.defaultLow, for: .horizontal)
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return label
    }()

    private lazy var availabilityStatusExpandImage: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "chevron.down.circle")
        imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
        imageView.maximumContentSizeCategory = .extraExtraExtraLarge
        imageView.contentMode = .scaleAspectFit
        imageView.tintColor = .label
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.setContentHuggingPriority(.required, for: .horizontal)
        imageView.isHidden = true
        return imageView
    }()
    
    private lazy var availabilityStatusView: UIView = {
        let container = UIStackView(arrangedSubviews: [availabilityStatusLabel, availabilityStatusExpandImage])
        container.axis = .horizontal
        container.alignment = .center
        container.spacing = 8
        container.backgroundColor = .tertiarySystemFill
        container.translatesAutoresizingMaskIntoConstraints = false
        container.isLayoutMarginsRelativeArrangement = true
        container.layoutMargins = .init(top: 8, left: 24, bottom: 8, right: 24)
        container.layer.masksToBounds = true
        container.isHidden = true
        container.addGestureRecognizer(
            UITapGestureRecognizer(target: self, action: #selector(handleAvailabilityStatusTap))
        )
        return container
    }()

    // MARK: - Initialization
    
    init(with contentConfiguration: ContentConfiguration, profilePictureTapped: @escaping () -> Void) {
        self.contentConfiguration = contentConfiguration
        self.profilePictureTappedHandler = profilePictureTapped
        
        super.init(frame: .zero)
        
        configureView()
        updateContent()
        addObservers()
    }
    
    @available(*, unavailable, message: "Use init(for:delegate:with:)")
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        DDLogDebug("\(#function)")
        tipObservationTask?.cancel()
        tipObservationTask = nil
    }
    
    // MARK: - Configuration
    
    private func configureView() {
        // Configure name label font
        updateNameLabelFont()
        
        // Configure profile picture layout
        profilePictureView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            profilePictureView.topAnchor.constraint(equalTo: profilePictureView.topAnchor),
            profilePictureView.leadingAnchor.constraint(equalTo: profilePictureView.leadingAnchor),
            profilePictureView.bottomAnchor.constraint(equalTo: profilePictureView.bottomAnchor),
            profilePictureView.trailingAnchor.constraint(equalTo: profilePictureView.trailingAnchor),
        ])
        
        // Configure self (stack)
        axis = .vertical
        alignment = .center
        spacing = DetailsHeaderProfileView.configuration.defaultSpacing
        
        if DetailsHeaderProfileView.configuration.debug {
            backgroundColor = .systemRed
        }
        
        // Add default subviews
        addArrangedSubview(profilePictureView)

        addArrangedSubview(availabilityStatusView)
        availabilityStatusView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0).isActive = true

        addArrangedSubview(nameLabel)

        // This needs to be set after the view is added as arranged subview
        // https://sarunw.com/posts/custom-uistackview-spacing/#caveat
        setCustomSpacing(
            DetailsHeaderProfileView.configuration.customSpacingAfterProfilePicture,
            after: isAvailabilityStatusVisible ? availabilityStatusView : profilePictureView
        )

        // Add verification level image to stack
        addArrangedSubview(verificationLevelImageView)
        
        isAccessibilityElement = true
        shouldGroupAccessibilityChildren = true
    }

    private var isAvailabilityStatusVisible: Bool {
        guard
            TargetManager.isWork,
            let availability = contentConfiguration.availabilityStatus,
            availability.category != .none
        else {
            return false
        }

        return true
    }

    private func addObservers() {
        // Dynamic type changed
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(updateNameLabelFont),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    // MARK: - Update functions
    
    private func updateContent() {
        profilePictureView.info = contentConfiguration.profilePictureInfo
        if let availabilityStatus = contentConfiguration.availabilityStatus, isAvailabilityStatusVisible {
            availabilityStatusLabel.text = availabilityStatus.text ?? availabilityStatus.category.localizedDescription
            availabilityStatusView.isHidden = false
        }
        else {
            availabilityStatusView.isHidden = true
        }
        
        if contentConfiguration.isSelfMember {
            nameLabel.attributedText = nil
            nameLabel.text = contentConfiguration.name
        }
        else {
            let attributeString = NSMutableAttributedString(string: contentConfiguration.name)
            attributeString.addAttribute(.strikethroughStyle, value: 2, range: NSMakeRange(0, attributeString.length))

            nameLabel.attributedText = attributeString
        }
        
        if let verificationLevelImage = contentConfiguration.verificationLevelImage {
            verificationLevelImageView.image = verificationLevelImage
            assert(
                contentConfiguration.verificationLevelAccessibilityLabel != nil,
                "A verification level accessibility description should be provided!"
            )
            verificationLevelImageView.accessibilityLabel = contentConfiguration.verificationLevelAccessibilityLabel
            verificationLevelImageView.isHidden = false
        }
        else {
            verificationLevelImageView.accessibilityLabel = ""
            verificationLevelImageView.isHidden = true
        }
    }

    /// Custom creation of semibold font for name label based on dynamic type
    @objc private func updateNameLabelFont() {
        nameLabel.font = DetailsHeaderProfileView.configuration.nameFont
    }
        
    // MARK: - Action
    
    @objc private func profilePictureTapped() {
        profilePictureTappedHandler()
    }

    @objc private func handleAvailabilityStatusTap() {
        guard !availabilityStatusExpandImage.isHidden else {
            return
        }
        isAvailabilityStatusExpanded.toggle()
        availabilityStatusLabel.numberOfLines = isAvailabilityStatusExpanded ? 0 : DetailsHeaderProfileView
            .configuration.collapsedLineLimit
        availabilityStatusExpandImage.image = UIImage(
            systemName: isAvailabilityStatusExpanded ? "chevron.up.circle" : "chevron.down.circle"
        )
    }
    
    func showThreemaTypeTip() {
        
        guard !ProcessInfoHelper.isRunningForScreenshots else {
            return
        }
        
        guard !UIAccessibility.isVoiceOverRunning else {
            return
        }
        
        guard !profilePictureView.typeIconImageView.isHidden else {
            return
        }
        
        let typeTip = TipKitManager.ThreemaTypeTip()
        let threemaTypeTipView = TipUIView(typeTip, arrowEdge: .top)
        threemaTypeTipView.backgroundColor = .tertiarySystemBackground
        threemaTypeTipView.translatesAutoresizingMaskIntoConstraints = false
        
        tipObservationTask = tipObservationTask ?? Task(priority: .userInitiated) { @MainActor in
            for await shouldDisplay in typeTip.shouldDisplayUpdates {
                if shouldDisplay {
                    addSubview(threemaTypeTipView)
                    
                    NSLayoutConstraint.activate([
                        threemaTypeTipView.topAnchor
                            .constraint(equalTo: profilePictureView.typeIconImageView.bottomAnchor),
                        threemaTypeTipView.leadingAnchor
                            .constraint(greaterThanOrEqualTo: safeAreaLayoutGuide.leadingAnchor),
                        threemaTypeTipView.trailingAnchor
                            .constraint(lessThanOrEqualTo: safeAreaLayoutGuide.trailingAnchor),
                        threemaTypeTipView.centerXAnchor
                            .constraint(equalTo: profilePictureView.typeIconImageView.centerXAnchor),
                    ])
                }
                else {
                    threemaTypeTipView.removeFromSuperview()
                }
            }
        }
    }
    
    func showThreemaWorkAvailabilityStatusTip() {
        guard !ProcessInfoHelper.isRunningForScreenshots else {
            return
        }
        
        guard TargetManager.isWork else {
            return
        }
        
        guard !UIAccessibility.isVoiceOverRunning else {
            return
        }
        
        guard isAvailabilityStatusVisible else {
            return
        }
        
        let threemaWorkAvailabilityStatusTip = TipKitManager.ThreemaWorkAvailabilityStatusChatTip(forChat: true)
        let threemaWorkAvailabilityStatusTipView = TipUIView(threemaWorkAvailabilityStatusTip, arrowEdge: .top)
        threemaWorkAvailabilityStatusTipView.translatesAutoresizingMaskIntoConstraints = false
        
        tipObservationTask = tipObservationTask ?? Task(priority: .userInitiated) { @MainActor in
            for await shouldDisplay in threemaWorkAvailabilityStatusTip.shouldDisplayUpdates {
                if shouldDisplay {
                    addSubview(threemaWorkAvailabilityStatusTipView)
                    
                    NSLayoutConstraint.activate([
                        threemaWorkAvailabilityStatusTipView.topAnchor
                            .constraint(equalTo: availabilityStatusView.bottomAnchor),
                        threemaWorkAvailabilityStatusTipView.leadingAnchor
                            .constraint(greaterThanOrEqualTo: safeAreaLayoutGuide.leadingAnchor),
                        threemaWorkAvailabilityStatusTipView.trailingAnchor
                            .constraint(lessThanOrEqualTo: safeAreaLayoutGuide.trailingAnchor),
                        threemaWorkAvailabilityStatusTipView.centerXAnchor
                            .constraint(equalTo: availabilityStatusView.centerXAnchor),
                    ])
                }
                else {
                    threemaWorkAvailabilityStatusTipView.removeFromSuperview()
                }
            }
        }
    }
    
    // MARK: - Accessibility
    
    override var accessibilityLabel: String? {
        get {
            var text = "\(nameLabel.accessibilityLabel ?? "") ."
            
            if let status = contentConfiguration.availabilityStatus {
                text += status.accessibilityLabelWithText!
            }
            
            return text
        }
        set { }
    }
    
    override var accessibilityValue: String? {
        get {
            var accessibilityValueString = ""
            if !profilePictureView.typeIconImageView.isHidden {
                accessibilityValueString = ThreemaUtility.otherThreemaTypeAccessibilityLabel + ". "
            }
            
            return accessibilityValueString + (verificationLevelImageView.accessibilityLabel ?? "")
        }
        
        set { }
    }
}

Messung
CHZG
869410093

¤ Dauer der Verarbeitung: 0.16 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.