mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Notifications tab
This commit is contained in:
parent
00605ff212
commit
79b1c531f0
7 changed files with 266 additions and 106 deletions
|
@ -17,6 +17,7 @@
|
||||||
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
|
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
|
||||||
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
|
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
|
||||||
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA16254CA823009094DF /* StatusBodyView.swift */; };
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
|
@ -124,6 +125,7 @@
|
||||||
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
|
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
|
||||||
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
|
||||||
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D036AA16254CA823009094DF /* StatusBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBodyView.swift; sourceTree = "<group>"; };
|
||||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -278,6 +280,7 @@
|
||||||
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */,
|
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */,
|
||||||
D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */,
|
D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */,
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
||||||
|
D036AA16254CA823009094DF /* StatusBodyView.swift */,
|
||||||
D0625E58250F092900502611 /* StatusListCell.swift */,
|
D0625E58250F092900502611 /* StatusListCell.swift */,
|
||||||
D00CB2EC2533ACC00080096B /* StatusView.swift */,
|
D00CB2EC2533ACC00080096B /* StatusView.swift */,
|
||||||
);
|
);
|
||||||
|
@ -645,6 +648,7 @@
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||||
|
|
|
@ -391,7 +391,7 @@ private extension TableViewController {
|
||||||
view.isDescendant(of: visibleView),
|
view.isDescendant(of: visibleView),
|
||||||
let superview = view.superview,
|
let superview = view.superview,
|
||||||
let attachmentsViewClosestToCenter = tableView.visibleCells
|
let attachmentsViewClosestToCenter = tableView.visibleCells
|
||||||
.compactMap({ ($0.contentView as? StatusView)?.attachmentsView })
|
.compactMap({ ($0.contentView as? StatusView)?.bodyView.attachmentsView })
|
||||||
.filter(\.shouldAutoplay)
|
.filter(\.shouldAutoplay)
|
||||||
.min(by: {
|
.min(by: {
|
||||||
abs(superview.convert($0.frame, from: $0.superview).midY - view.frame.midY)
|
abs(superview.convert($0.frame, from: $0.superview).midY - view.frame.midY)
|
||||||
|
|
|
@ -165,6 +165,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
||||||
let item = items.value[indexPath.section][indexPath.item]
|
let item = items.value[indexPath.section][indexPath.item]
|
||||||
let cachedViewModel = viewModelCache[item]?.viewModel
|
let cachedViewModel = viewModelCache[item]?.viewModel
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ServiceLayer
|
||||||
|
|
||||||
public final class NotificationViewModel: CollectionItemViewModel, ObservableObject {
|
public final class NotificationViewModel: CollectionItemViewModel, ObservableObject {
|
||||||
public let accountViewModel: AccountViewModel
|
public let accountViewModel: AccountViewModel
|
||||||
|
public let statusViewModel: StatusViewModel?
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let notificationService: NotificationService
|
private let notificationService: NotificationService
|
||||||
|
@ -20,6 +21,15 @@ public final class NotificationViewModel: CollectionItemViewModel, ObservableObj
|
||||||
accountService: notificationService.navigationService.accountService(
|
accountService: notificationService.navigationService.accountService(
|
||||||
account: notificationService.notification.account),
|
account: notificationService.notification.account),
|
||||||
identification: identification)
|
identification: identification)
|
||||||
|
|
||||||
|
if let status = notificationService.notification.status {
|
||||||
|
statusViewModel = StatusViewModel(
|
||||||
|
statusService: notificationService.navigationService.statusService(status: status),
|
||||||
|
identification: identification)
|
||||||
|
} else {
|
||||||
|
statusViewModel = nil
|
||||||
|
}
|
||||||
|
|
||||||
self.events = eventsSubject.eraseToAnyPublisher()
|
self.events = eventsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,4 +38,14 @@ public extension NotificationViewModel {
|
||||||
var type: MastodonNotification.NotificationType {
|
var type: MastodonNotification.NotificationType {
|
||||||
notificationService.notification.type
|
notificationService.notification.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func accountSelected() {
|
||||||
|
eventsSubject.send(
|
||||||
|
Just(.navigation(
|
||||||
|
.profile(
|
||||||
|
notificationService.navigationService.profileService(
|
||||||
|
account: notificationService.notification.account))))
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class NotificationView: UIView {
|
final class NotificationView: UIView {
|
||||||
private let iconImageView = UIImageView()
|
private let iconImageView = UIImageView()
|
||||||
|
private let avatarImageView = AnimatedImageView()
|
||||||
|
private let avatarButton = UIButton()
|
||||||
private let typeLabel = UILabel()
|
private let typeLabel = UILabel()
|
||||||
|
private let displayNameLabel = UILabel()
|
||||||
|
private let accountLabel = UILabel()
|
||||||
|
private let statusBodyView = StatusBodyView()
|
||||||
private var notificationConfiguration: NotificationContentConfiguration
|
private var notificationConfiguration: NotificationContentConfiguration
|
||||||
|
|
||||||
init(configuration: NotificationContentConfiguration) {
|
init(configuration: NotificationContentConfiguration) {
|
||||||
|
@ -37,31 +43,89 @@ extension NotificationView: UIContentView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NotificationView {
|
private extension NotificationView {
|
||||||
|
// swiftlint:disable function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
let stackView = UIStackView()
|
let containerStackView = UIStackView()
|
||||||
|
let sideStackView = UIStackView()
|
||||||
|
let mainStackView = UIStackView()
|
||||||
|
|
||||||
addSubview(stackView)
|
addSubview(containerStackView)
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stackView.spacing = .compactSpacing
|
containerStackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
stackView.addArrangedSubview(iconImageView)
|
sideStackView.axis = .vertical
|
||||||
|
sideStackView.alignment = .trailing
|
||||||
|
sideStackView.spacing = .compactSpacing
|
||||||
|
sideStackView.addArrangedSubview(iconImageView)
|
||||||
|
sideStackView.addArrangedSubview(avatarImageView)
|
||||||
|
sideStackView.addArrangedSubview(UIView())
|
||||||
|
containerStackView.addArrangedSubview(sideStackView)
|
||||||
|
|
||||||
|
mainStackView.axis = .vertical
|
||||||
|
mainStackView.spacing = .compactSpacing
|
||||||
|
mainStackView.addArrangedSubview(typeLabel)
|
||||||
|
mainStackView.addSubview(UIView())
|
||||||
|
mainStackView.addArrangedSubview(statusBodyView)
|
||||||
|
mainStackView.addArrangedSubview(displayNameLabel)
|
||||||
|
mainStackView.addArrangedSubview(accountLabel)
|
||||||
|
containerStackView.addArrangedSubview(mainStackView)
|
||||||
|
|
||||||
|
iconImageView.contentMode = .scaleAspectFit
|
||||||
iconImageView.setContentHuggingPriority(.required, for: .horizontal)
|
iconImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
|
||||||
stackView.addArrangedSubview(typeLabel)
|
avatarImageView.layer.cornerRadius = .avatarDimension / 2
|
||||||
typeLabel.font = .preferredFont(forTextStyle: .body)
|
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.adjustsFontForContentSizeCategory = true
|
||||||
|
typeLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
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([
|
NSLayoutConstraint.activate([
|
||||||
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
containerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
containerStackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
containerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
|
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)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyNotificationConfiguration() {
|
func applyNotificationConfiguration() {
|
||||||
let viewModel = notificationConfiguration.viewModel
|
let viewModel = notificationConfiguration.viewModel
|
||||||
|
|
||||||
|
avatarImageView.kf.setImage(with: viewModel.accountViewModel.avatarURL())
|
||||||
|
|
||||||
switch viewModel.type {
|
switch viewModel.type {
|
||||||
case .follow:
|
case .follow:
|
||||||
typeLabel.attributedText = "notifications.followed-you".localizedBolding(
|
typeLabel.attributedText = "notifications.followed-you".localizedBolding(
|
||||||
|
@ -96,10 +160,28 @@ private extension NotificationView {
|
||||||
iconImageView.tintColor = nil
|
iconImageView.tintColor = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.statusViewModel == nil {
|
||||||
|
let mutableDisplayName = NSMutableAttributedString(string: viewModel.accountViewModel.displayName)
|
||||||
|
|
||||||
|
mutableDisplayName.insert(emoji: viewModel.accountViewModel.emoji, view: displayNameLabel)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
iconImageView.image = UIImage(
|
iconImageView.image = UIImage(
|
||||||
systemName: viewModel.type.systemImageName,
|
systemName: viewModel.type.systemImageName,
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium))
|
withConfiguration: UIImage.SymbolConfiguration(scale: .medium))
|
||||||
}
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonNotification.NotificationType {
|
extension MastodonNotification.NotificationType {
|
||||||
|
|
138
Views/Status/StatusBodyView.swift
Normal file
138
Views/Status/StatusBodyView.swift
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class StatusBodyView: UIView {
|
||||||
|
let spoilerTextLabel = UILabel()
|
||||||
|
let toggleShowContentButton = UIButton(type: .system)
|
||||||
|
let contentTextView = TouchFallthroughTextView()
|
||||||
|
let attachmentsView = StatusAttachmentsView()
|
||||||
|
let pollView = PollView()
|
||||||
|
let cardView = CardView()
|
||||||
|
|
||||||
|
var viewModel: StatusViewModel? {
|
||||||
|
didSet {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
let isContextParent = viewModel.configuration.isContextParent
|
||||||
|
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
||||||
|
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
||||||
|
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
||||||
|
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
||||||
|
|
||||||
|
contentTextView.shouldFallthrough = !isContextParent
|
||||||
|
|
||||||
|
mutableContent.removeAttribute(.font, range: contentRange)
|
||||||
|
mutableContent.addAttributes(
|
||||||
|
[.font: contentFont, .foregroundColor: UIColor.label],
|
||||||
|
range: contentRange)
|
||||||
|
mutableContent.insert(emoji: viewModel.contentEmoji, view: contentTextView)
|
||||||
|
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
||||||
|
contentTextView.attributedText = mutableContent
|
||||||
|
contentTextView.isHidden = contentTextView.text == ""
|
||||||
|
|
||||||
|
mutableSpoilerText.insert(emoji: viewModel.contentEmoji, view: spoilerTextLabel)
|
||||||
|
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
||||||
|
spoilerTextLabel.font = contentFont
|
||||||
|
spoilerTextLabel.attributedText = mutableSpoilerText
|
||||||
|
spoilerTextLabel.isHidden = spoilerTextLabel.text == ""
|
||||||
|
toggleShowContentButton.setTitle(
|
||||||
|
viewModel.shouldShowContent
|
||||||
|
? NSLocalizedString("status.show-less", comment: "")
|
||||||
|
: NSLocalizedString("status.show-more", comment: ""),
|
||||||
|
for: .normal)
|
||||||
|
toggleShowContentButton.isHidden = viewModel.spoilerText == ""
|
||||||
|
|
||||||
|
contentTextView.isHidden = !viewModel.shouldShowContent
|
||||||
|
|
||||||
|
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
|
||||||
|
attachmentsView.viewModel = viewModel
|
||||||
|
|
||||||
|
pollView.isHidden = viewModel.pollOptions.count == 0
|
||||||
|
pollView.viewModel = viewModel
|
||||||
|
|
||||||
|
cardView.viewModel = viewModel.cardViewModel
|
||||||
|
cardView.isHidden = viewModel.cardViewModel == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusBodyView: UITextViewDelegate {
|
||||||
|
func textView(
|
||||||
|
_ textView: UITextView,
|
||||||
|
shouldInteractWith URL: URL,
|
||||||
|
in characterRange: NSRange,
|
||||||
|
interaction: UITextItemInteraction) -> Bool {
|
||||||
|
switch interaction {
|
||||||
|
case .invokeDefaultAction:
|
||||||
|
viewModel?.urlSelected(URL)
|
||||||
|
return false
|
||||||
|
case .preview: return false
|
||||||
|
case .presentActions: return false
|
||||||
|
@unknown default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension StatusBodyView {
|
||||||
|
func initialSetup() {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.spacing = .compactSpacing
|
||||||
|
|
||||||
|
spoilerTextLabel.numberOfLines = 0
|
||||||
|
spoilerTextLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
stackView.addArrangedSubview(spoilerTextLabel)
|
||||||
|
|
||||||
|
toggleShowContentButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
toggleShowContentButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||||
|
toggleShowContentButton.addAction(
|
||||||
|
UIAction { [weak self] _ in self?.viewModel?.toggleShowContent() },
|
||||||
|
for: .touchUpInside)
|
||||||
|
stackView.addArrangedSubview(toggleShowContentButton)
|
||||||
|
|
||||||
|
contentTextView.adjustsFontForContentSizeCategory = true
|
||||||
|
contentTextView.isScrollEnabled = false
|
||||||
|
contentTextView.backgroundColor = .clear
|
||||||
|
contentTextView.delegate = self
|
||||||
|
stackView.addArrangedSubview(contentTextView)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(attachmentsView)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(pollView)
|
||||||
|
|
||||||
|
cardView.button.addAction(
|
||||||
|
UIAction { [weak self] _ in
|
||||||
|
guard
|
||||||
|
let viewModel = self?.viewModel,
|
||||||
|
let url = viewModel.cardViewModel?.url
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
viewModel.urlSelected(url)
|
||||||
|
},
|
||||||
|
for: .touchUpInside)
|
||||||
|
stackView.addArrangedSubview(cardView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,12 +12,7 @@ final class StatusView: UIView {
|
||||||
let displayNameLabel = UILabel()
|
let displayNameLabel = UILabel()
|
||||||
let accountLabel = UILabel()
|
let accountLabel = UILabel()
|
||||||
let timeLabel = UILabel()
|
let timeLabel = UILabel()
|
||||||
let spoilerTextLabel = UILabel()
|
let bodyView = StatusBodyView()
|
||||||
let toggleShowContentButton = UIButton(type: .system)
|
|
||||||
let contentTextView = TouchFallthroughTextView()
|
|
||||||
let attachmentsView = StatusAttachmentsView()
|
|
||||||
let pollView = PollView()
|
|
||||||
let cardView = CardView()
|
|
||||||
let contextParentTimeLabel = UILabel()
|
let contextParentTimeLabel = UILabel()
|
||||||
let timeApplicationDividerLabel = UILabel()
|
let timeApplicationDividerLabel = UILabel()
|
||||||
let applicationButton = UIButton(type: .system)
|
let applicationButton = UIButton(type: .system)
|
||||||
|
@ -145,38 +140,7 @@ private extension StatusView {
|
||||||
nameAccountContainerStackView.addArrangedSubview(nameAccountTimeStackView)
|
nameAccountContainerStackView.addArrangedSubview(nameAccountTimeStackView)
|
||||||
mainStackView.addArrangedSubview(nameAccountContainerStackView)
|
mainStackView.addArrangedSubview(nameAccountContainerStackView)
|
||||||
|
|
||||||
spoilerTextLabel.numberOfLines = 0
|
mainStackView.addArrangedSubview(bodyView)
|
||||||
spoilerTextLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
mainStackView.addArrangedSubview(spoilerTextLabel)
|
|
||||||
|
|
||||||
toggleShowContentButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
|
||||||
toggleShowContentButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
|
||||||
toggleShowContentButton.addAction(
|
|
||||||
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleShowContent() },
|
|
||||||
for: .touchUpInside)
|
|
||||||
mainStackView.addArrangedSubview(toggleShowContentButton)
|
|
||||||
|
|
||||||
contentTextView.adjustsFontForContentSizeCategory = true
|
|
||||||
contentTextView.isScrollEnabled = false
|
|
||||||
contentTextView.backgroundColor = .clear
|
|
||||||
contentTextView.delegate = self
|
|
||||||
mainStackView.addArrangedSubview(contentTextView)
|
|
||||||
|
|
||||||
mainStackView.addArrangedSubview(attachmentsView)
|
|
||||||
|
|
||||||
mainStackView.addArrangedSubview(pollView)
|
|
||||||
|
|
||||||
cardView.button.addAction(
|
|
||||||
UIAction { [weak self] _ in
|
|
||||||
guard
|
|
||||||
let viewModel = self?.statusConfiguration.viewModel,
|
|
||||||
let url = viewModel.cardViewModel?.url
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
viewModel.urlSelected(url)
|
|
||||||
},
|
|
||||||
for: .touchUpInside)
|
|
||||||
mainStackView.addArrangedSubview(cardView)
|
|
||||||
|
|
||||||
contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote)
|
contextParentTimeLabel.font = .preferredFont(forTextStyle: .footnote)
|
||||||
contextParentTimeLabel.adjustsFontForContentSizeCategory = true
|
contextParentTimeLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
@ -296,15 +260,10 @@ private extension StatusView {
|
||||||
func applyStatusConfiguration() {
|
func applyStatusConfiguration() {
|
||||||
let viewModel = statusConfiguration.viewModel
|
let viewModel = statusConfiguration.viewModel
|
||||||
let isContextParent = viewModel.configuration.isContextParent
|
let isContextParent = viewModel.configuration.isContextParent
|
||||||
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||||
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
|
||||||
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
|
||||||
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
|
||||||
|
|
||||||
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
||||||
|
|
||||||
contentTextView.shouldFallthrough = !isContextParent
|
|
||||||
sideStackView.isHidden = isContextParent
|
sideStackView.isHidden = isContextParent
|
||||||
avatarImageView.removeFromSuperview()
|
avatarImageView.removeFromSuperview()
|
||||||
|
|
||||||
|
@ -326,25 +285,11 @@ private extension StatusView {
|
||||||
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
inReplyToView.isHidden = !viewModel.configuration.isReplyInContext
|
||||||
hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing
|
hasReplyFollowingView.isHidden = !viewModel.configuration.hasReplyFollowing
|
||||||
|
|
||||||
if
|
if viewModel.isReblog {
|
||||||
viewModel.isReblog {
|
infoLabel.attributedText = "status.reblogged-by".localizedBolding(
|
||||||
let metaText = String.localizedStringWithFormat(
|
displayName: viewModel.rebloggedByDisplayName,
|
||||||
NSLocalizedString("status.reblogged-by", comment: ""),
|
emoji: viewModel.rebloggedByDisplayNameEmoji,
|
||||||
viewModel.rebloggedByDisplayName)
|
label: infoLabel)
|
||||||
let mutableInfoText = NSMutableAttributedString(string: metaText)
|
|
||||||
|
|
||||||
let range = (mutableInfoText.string as NSString).range(of: viewModel.rebloggedByDisplayName)
|
|
||||||
|
|
||||||
if range.location != NSNotFound,
|
|
||||||
let boldFontDescriptor = infoLabel.font.fontDescriptor.withSymbolicTraits([.traitBold]) {
|
|
||||||
let boldFont = UIFont(descriptor: boldFontDescriptor, size: infoLabel.font.pointSize)
|
|
||||||
|
|
||||||
mutableInfoText.setAttributes([NSAttributedString.Key.font: boldFont], range: range)
|
|
||||||
}
|
|
||||||
|
|
||||||
mutableInfoText.insert(emoji: viewModel.rebloggedByDisplayNameEmoji, view: infoLabel)
|
|
||||||
mutableInfoText.resizeAttachments(toLineHeight: infoLabel.font.lineHeight)
|
|
||||||
infoLabel.attributedText = mutableInfoText
|
|
||||||
infoIcon.image = UIImage(
|
infoIcon.image = UIImage(
|
||||||
systemName: "arrow.2.squarepath",
|
systemName: "arrow.2.squarepath",
|
||||||
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
|
||||||
|
@ -362,33 +307,10 @@ private extension StatusView {
|
||||||
infoIcon.isHidden = true
|
infoIcon.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableContent.removeAttribute(.font, range: contentRange)
|
|
||||||
mutableContent.addAttributes(
|
|
||||||
[.font: contentFont, .foregroundColor: UIColor.label],
|
|
||||||
range: contentRange)
|
|
||||||
mutableContent.insert(emoji: viewModel.contentEmoji, view: contentTextView)
|
|
||||||
mutableContent.resizeAttachments(toLineHeight: contentFont.lineHeight)
|
|
||||||
contentTextView.attributedText = mutableContent
|
|
||||||
contentTextView.isHidden = contentTextView.text == ""
|
|
||||||
|
|
||||||
mutableDisplayName.insert(emoji: viewModel.displayNameEmoji, view: displayNameLabel)
|
mutableDisplayName.insert(emoji: viewModel.displayNameEmoji, view: displayNameLabel)
|
||||||
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||||
displayNameLabel.attributedText = mutableDisplayName
|
displayNameLabel.attributedText = mutableDisplayName
|
||||||
|
|
||||||
mutableSpoilerText.insert(emoji: viewModel.contentEmoji, view: spoilerTextLabel)
|
|
||||||
mutableSpoilerText.resizeAttachments(toLineHeight: spoilerTextLabel.font.lineHeight)
|
|
||||||
spoilerTextLabel.font = contentFont
|
|
||||||
spoilerTextLabel.attributedText = mutableSpoilerText
|
|
||||||
spoilerTextLabel.isHidden = spoilerTextLabel.text == ""
|
|
||||||
toggleShowContentButton.setTitle(
|
|
||||||
viewModel.shouldShowContent
|
|
||||||
? NSLocalizedString("status.show-less", comment: "")
|
|
||||||
: NSLocalizedString("status.show-more", comment: ""),
|
|
||||||
for: .normal)
|
|
||||||
toggleShowContentButton.isHidden = viewModel.spoilerText == ""
|
|
||||||
|
|
||||||
contentTextView.isHidden = !viewModel.shouldShowContent
|
|
||||||
|
|
||||||
nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal
|
nameAccountTimeStackView.axis = isContextParent ? .vertical : .horizontal
|
||||||
nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill
|
nameAccountTimeStackView.alignment = isContextParent ? .leading : .fill
|
||||||
nameAccountTimeStackView.spacing = isContextParent ? 0 : .compactSpacing
|
nameAccountTimeStackView.spacing = isContextParent ? 0 : .compactSpacing
|
||||||
|
@ -407,14 +329,7 @@ private extension StatusView {
|
||||||
timeLabel.text = viewModel.time
|
timeLabel.text = viewModel.time
|
||||||
timeLabel.isHidden = isContextParent
|
timeLabel.isHidden = isContextParent
|
||||||
|
|
||||||
attachmentsView.isHidden = viewModel.attachmentViewModels.count == 0
|
bodyView.viewModel = viewModel
|
||||||
attachmentsView.viewModel = viewModel
|
|
||||||
|
|
||||||
pollView.isHidden = viewModel.pollOptions.count == 0
|
|
||||||
pollView.viewModel = viewModel
|
|
||||||
|
|
||||||
cardView.viewModel = viewModel.cardViewModel
|
|
||||||
cardView.isHidden = viewModel.cardViewModel == nil
|
|
||||||
|
|
||||||
contextParentTimeLabel.text = viewModel.contextParentTime
|
contextParentTimeLabel.text = viewModel.contextParentTime
|
||||||
timeApplicationDividerLabel.isHidden = viewModel.applicationName == nil
|
timeApplicationDividerLabel.isHidden = viewModel.applicationName == nil
|
||||||
|
|
Loading…
Reference in a new issue