// Copyright © 2020 Metabolist. All rights reserved. import Mastodon import SDWebImage import UIKit import ViewModels final class NotificationView: UIView { private let iconImageView = UIImageView() private let avatarImageView = SDAnimatedImageView() private let avatarButton = UIButton() private let typeLabel = AnimatedAttachmentLabel() private let timeLabel = UILabel() private let displayNameLabel = AnimatedAttachmentLabel() private let accountLabel = UILabel() private let statusBodyView = StatusBodyView() private var notificationConfiguration: NotificationContentConfiguration init(configuration: NotificationContentConfiguration) { notificationConfiguration = configuration super.init(frame: .zero) initialSetup() applyNotificationConfiguration() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension NotificationView { static func estimatedHeight(width: CGFloat, identityContext: IdentityContext, notification: MastodonNotification, configuration: CollectionItem.StatusConfiguration?) -> CGFloat { let bodyWidth = width - .defaultSpacing - .avatarDimension var height = CGFloat.defaultSpacing * 2 + UIFont.preferredFont(forTextStyle: .headline).lineHeight + .compactSpacing if let status = notification.status { height += StatusBodyView.estimatedHeight( width: bodyWidth, identityContext: identityContext, status: status, configuration: configuration ?? .default) } else { height += UIFont.preferredFont(forTextStyle: .headline).lineHeight + .compactSpacing + UIFont.preferredFont(forTextStyle: .subheadline).lineHeight } return height } } extension NotificationView: UIContentView { var configuration: UIContentConfiguration { get { notificationConfiguration } set { guard let notificationConfiguration = newValue as? NotificationContentConfiguration else { return } self.notificationConfiguration = notificationConfiguration applyNotificationConfiguration() } } } private extension NotificationView { // swiftlint:disable function_body_length func initialSetup() { let containerStackView = UIStackView() let sideStackView = UIStackView() let typeTimeStackView = UIStackView() let mainStackView = UIStackView() addSubview(containerStackView) containerStackView.translatesAutoresizingMaskIntoConstraints = false containerStackView.spacing = .defaultSpacing containerStackView.alignment = .top sideStackView.axis = .vertical sideStackView.alignment = .trailing sideStackView.spacing = .compactSpacing sideStackView.addArrangedSubview(iconImageView) sideStackView.addArrangedSubview(avatarImageView) containerStackView.addArrangedSubview(sideStackView) typeTimeStackView.spacing = .compactSpacing typeTimeStackView.alignment = .top mainStackView.axis = .vertical mainStackView.spacing = .compactSpacing typeTimeStackView.addArrangedSubview(typeLabel) typeTimeStackView.addArrangedSubview(timeLabel) mainStackView.addArrangedSubview(typeTimeStackView) mainStackView.addArrangedSubview(statusBodyView) mainStackView.addArrangedSubview(displayNameLabel) mainStackView.addArrangedSubview(accountLabel) containerStackView.addArrangedSubview(mainStackView) iconImageView.contentMode = .scaleAspectFit iconImageView.setContentHuggingPriority(.required, for: .horizontal) avatarImageView.layer.cornerRadius = .avatarDimension / 2 avatarImageView.clipsToBounds = true let avatarHeightConstraint = avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension) avatarHeightConstraint.priority = .justBelowMax avatarButton.translatesAutoresizingMaskIntoConstraints = false avatarImageView.addSubview(avatarButton) avatarImageView.isUserInteractionEnabled = true avatarButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) avatarButton.addAction( UIAction { [weak self] _ in self?.notificationConfiguration.viewModel.accountSelected() }, for: .touchUpInside) typeLabel.font = .preferredFont(forTextStyle: .headline) typeLabel.adjustsFontForContentSizeCategory = true typeLabel.numberOfLines = 0 timeLabel.font = .preferredFont(forTextStyle: .subheadline) timeLabel.adjustsFontForContentSizeCategory = true timeLabel.textColor = .secondaryLabel timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal) timeLabel.setContentHuggingPriority(.required, for: .horizontal) statusBodyView.alpha = 0.5 statusBodyView.isUserInteractionEnabled = false displayNameLabel.font = .preferredFont(forTextStyle: .headline) displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.numberOfLines = 0 accountLabel.font = .preferredFont(forTextStyle: .subheadline) accountLabel.adjustsFontForContentSizeCategory = true accountLabel.textColor = .secondaryLabel accountLabel.numberOfLines = 0 NSLayoutConstraint.activate([ containerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), containerStackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), containerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor), avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), avatarHeightConstraint, sideStackView.widthAnchor.constraint(equalToConstant: .avatarDimension), iconImageView.centerYAnchor.constraint(equalTo: typeLabel.centerYAnchor), avatarButton.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor), avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor) ]) isAccessibilityElement = true } func applyNotificationConfiguration() { let viewModel = notificationConfiguration.viewModel avatarImageView.sd_setImage(with: viewModel.accountViewModel.avatarURL()) switch viewModel.type { case .follow: typeLabel.attributedText = "notifications.followed-you".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, label: typeLabel, identityContext: viewModel.identityContext) iconImageView.tintColor = nil case .reblog: typeLabel.attributedText = "notifications.reblogged-your-status".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, label: typeLabel, identityContext: viewModel.identityContext) iconImageView.tintColor = .systemGreen case .favourite: typeLabel.attributedText = "notifications.favourited-your-status".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, label: typeLabel, identityContext: viewModel.identityContext) iconImageView.tintColor = .systemYellow case .poll: typeLabel.text = NSLocalizedString( viewModel.accountViewModel.isSelf ? "notifications.your-poll-ended" : "notifications.poll-ended", comment: "") iconImageView.tintColor = nil default: typeLabel.attributedText = "notifications.unknown".localizedBolding( displayName: viewModel.accountViewModel.displayName, emojis: viewModel.accountViewModel.emojis, label: typeLabel, identityContext: viewModel.identityContext) iconImageView.tintColor = nil } if viewModel.statusViewModel == nil { let mutableDisplayName = NSMutableAttributedString(string: viewModel.accountViewModel.displayName) mutableDisplayName.insert(emojis: viewModel.accountViewModel.emojis, view: displayNameLabel, identityContext: viewModel.identityContext) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight) displayNameLabel.attributedText = mutableDisplayName accountLabel.text = viewModel.accountViewModel.accountName statusBodyView.isHidden = true displayNameLabel.isHidden = false accountLabel.isHidden = false } else { statusBodyView.viewModel = viewModel.statusViewModel statusBodyView.isHidden = false displayNameLabel.isHidden = true accountLabel.isHidden = true } timeLabel.text = viewModel.time timeLabel.accessibilityLabel = viewModel.accessibilityTime iconImageView.image = UIImage( systemName: viewModel.type.systemImageName, withConfiguration: UIImage.SymbolConfiguration(scale: .medium)) let accessibilityAttributedLabel = NSMutableAttributedString(string: "") if let typeText = typeLabel.attributedText { accessibilityAttributedLabel.appendWithSeparator(typeText) } if !statusBodyView.isHidden, let statusBodyAccessibilityAttributedLabel = statusBodyView.accessibilityAttributedLabel { accessibilityAttributedLabel.appendWithSeparator(statusBodyAccessibilityAttributedLabel) } else if !accountLabel.isHidden, let accountText = accountLabel.text { accessibilityAttributedLabel.appendWithSeparator(accountText) } if let accessibilityTime = viewModel.accessibilityTime { accessibilityAttributedLabel.appendWithSeparator(accessibilityTime) } self.accessibilityAttributedLabel = accessibilityAttributedLabel } // swiftlint:enable function_body_length } extension MastodonNotification.NotificationType { var systemImageName: String { switch self { case .follow, .followRequest: return "person.badge.plus" case .reblog: return "arrow.2.squarepath" case .favourite: return "star.fill" case .poll: return "chart.bar.doc.horizontal" case .status: return "house" case .mention, .unknown: return "at" } } }