mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-27 02:21:07 +00:00
274 lines
11 KiB
Swift
274 lines
11 KiB
Swift
// 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"
|
|
}
|
|
}
|
|
}
|