// // PollFinishedNotificationCollectionViewCell.swift // Tusker // // Created by Shadowfacts on 5/7/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import HTMLStreamer class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell { private let iconView = UIImageView(image: UIImage(systemName: "checkmark.square.fill")).configure { $0.contentMode = .scaleAspectFit NSLayoutConstraint.activate([ $0.heightAnchor.constraint(equalToConstant: 30), $0.widthAnchor.constraint(equalToConstant: 30), ]) } private let descriptionLabel = UILabel().configure { $0.text = "A poll has finished" $0.font = .preferredFont(forTextStyle: .body) $0.adjustsFontForContentSizeCategory = true $0.setContentHuggingPriority(.init(249), for: .horizontal) } private let timestampLabel = UILabel().configure { $0.textColor = .secondaryLabel $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([ .traits: [ UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue, ] ]), size: 0) $0.adjustsFontForContentSizeCategory = true } private lazy var hStack = UIStackView(arrangedSubviews: [ descriptionLabel, timestampLabel, ]).configure { $0.axis = .horizontal $0.alignment = .fill } private let displayNameLabel = EmojiLabel().configure { $0.textColor = .secondaryLabel $0.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! $0.adjustsFontForContentSizeCategory = true $0.numberOfLines = 2 $0.lineBreakMode = .byTruncatingTail } private let contentLabel = UILabel().configure { $0.textColor = .secondaryLabel $0.font = .preferredFont(forTextStyle: .body) $0.adjustsFontForContentSizeCategory = true $0.numberOfLines = 2 $0.lineBreakMode = .byTruncatingTail } private let pollView = StatusPollView() private lazy var vStack = UIStackView(arrangedSubviews: [ hStack, displayNameLabel, contentLabel, pollView, ]).configure { $0.axis = .vertical $0.alignment = .fill } weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)? private var mastodonController: MastodonController { delegate!.apiController } private var notification: Pachyderm.Notification! private var updateTimestampWorkItem: DispatchWorkItem? deinit { updateTimestampWorkItem?.cancel() } override init(frame: CGRect) { super.init(frame: frame) iconView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(iconView) vStack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(vStack) NSLayoutConstraint.activate([ iconView.topAnchor.constraint(equalTo: vStack.topAnchor), iconView.trailingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + 50), vStack.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8), vStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), vStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), ]) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func updateConfiguration(using state: UICellConfigurationState) { backgroundConfiguration = .appListPlainCell(for: state) } func updateUI(notification: Pachyderm.Notification) { guard let statusID = notification.status?.id, let status = mastodonController.persistentContainer.status(for: statusID), let account = mastodonController.persistentContainer.account(for: notification.account.id), let poll = status.poll else { return } self.notification = notification updateTimestamp() updateDisplayName(account: account) var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) contentLabel.text = converter.convert(html: status.content) pollView.mastodonController = mastodonController pollView.delegate = delegate pollView.updateUI(status: status, poll: poll) } @objc private func updateUIForPreferences() { if let account = mastodonController.persistentContainer.account(for: notification.account.id) { self.updateDisplayName(account: account) } } private func updateDisplayName(account: AccountMO) { if Preferences.shared.hideCustomEmojiInUsernames { displayNameLabel.text = account.displayNameWithoutCustomEmoji displayNameLabel.removeEmojis() } else { displayNameLabel.text = account.displayOrUserName displayNameLabel.setEmojis(account.emojis, identifier: account.id) } } private func updateTimestamp() { guard let notification = notification else { return } timestampLabel.text = notification.createdAt.timeAgoString() let delay: DispatchTimeInterval? switch notification.createdAt.timeAgo().1 { case .second: delay = .seconds(10) case .minute: delay = .seconds(60) default: delay = nil } if let delay = delay { if updateTimestampWorkItem == nil { updateTimestampWorkItem = DispatchWorkItem { [weak self] in self?.updateTimestamp() } DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) } } else { updateTimestampWorkItem = nil } } override func prepareForReuse() { super.prepareForReuse() updateTimestampWorkItem?.cancel() } // MARK: Accessibility override var accessibilityLabel: String? { get { guard let notification else { return nil } var str = "Poll from " str += notification.account.displayNameWithoutCustomEmoji str += " finished " str += notification.createdAt.formatted(.relative(presentation: .numeric)) if let poll = notification.status?.poll, poll.options.contains(where: { ($0.votesCount ?? 0) > 0 }) { let winner = poll.options.max(by: { ($0.votesCount ?? 0) < ($1.votesCount ?? 0) })! str += ", winning option: \(winner.title)" } return str } set {} } }