Better filter cell and animation for showing filtered post

This commit is contained in:
Shadowfacts 2022-12-03 22:47:33 -05:00
parent 81abcfcf7b
commit a17afe247c
6 changed files with 97 additions and 36 deletions

View File

@ -64,7 +64,8 @@ class TrendingStatusesViewController: UIViewController {
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: item.0, state: item.1) // TODO: filter these
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow)
} }
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, _, _ in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, _, _ in
cell.indicator.startAnimating() cell.indicator.startAnimating()

View File

@ -113,7 +113,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.showPinned = item.2 cell.showPinned = item.2
cell.updateUI(statusID: item.0, state: item.1) cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow)
} }
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {

View File

@ -80,7 +80,7 @@ class StatusActionAccountListViewController: UIViewController {
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Void> { [unowned self] cell, indexPath, _ in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Void> { [unowned self] cell, indexPath, _ in
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: self.statusID, state: self.statusState) cell.updateUI(statusID: self.statusID, state: self.statusState, filterResult: .allow)
} }
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self

View File

@ -69,10 +69,10 @@ class InstanceTimelineViewController: TimelineViewController {
toggleSaveButton.title = toggleSaveButtonTitle toggleSaveButton.title = toggleSaveButtonTitle
} }
override func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState) { override func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result) {
cell.delegate = browsingEnabled ? self : nil cell.delegate = browsingEnabled ? self : nil
cell.overrideMastodonController = mastodonController cell.overrideMastodonController = mastodonController
cell.updateUI(statusID: id, state: state) cell.updateUI(statusID: id, state: state, filterResult: filterResult)
} }
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

View File

@ -105,27 +105,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
// separate method because InstanceTimelineViewController needs to be able to customize it // separate method because InstanceTimelineViewController needs to be able to customize it
func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState) { func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result) {
cell.delegate = self cell.delegate = self
if case .home = timeline { if case .home = timeline {
cell.showFollowedHashtags = true cell.showFollowedHashtags = true
} else { } else {
cell.showFollowedHashtags = false cell.showFollowedHashtags = false
} }
cell.updateUI(statusID: id, state: state) cell.updateUI(statusID: id, state: state, filterResult: filterResult)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result)> { [unowned self] cell, indexPath, item in
self.configureStatusCell(cell, id: item.0, state: item.1) self.configureStatusCell(cell, id: item.0, state: item.1, filterResult: item.2)
} }
let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in
} }
let filterWarningCell = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, item in
var config = cell.defaultContentConfiguration()
config.text = "Filtered: \(item)"
cell.contentConfiguration = config
}
let gapCell = UICollectionView.CellRegistration<TimelineGapCollectionViewCell, Void> { cell, indexPath, _ in let gapCell = UICollectionView.CellRegistration<TimelineGapCollectionViewCell, Void> { cell, indexPath, _ in
cell.showsIndicator = false cell.showsIndicator = false
} }
@ -143,13 +138,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
switch itemIdentifier { switch itemIdentifier {
case .status(id: let id, collapseState: let state, filterState: let filterState): case .status(id: let id, collapseState: let state, filterState: let filterState):
let status = { self.mastodonController.persistentContainer.status(for: id)! } let status = { self.mastodonController.persistentContainer.status(for: id)! }
switch filterState.resolveFor(status: status, resolver: filterer) { let result = filterState.resolveFor(status: status, resolver: filterer)
case .allow: switch result {
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state)) case .allow, .warn(_):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, result))
case .hide: case .hide:
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ()) return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
case .warn(let filterTitle):
return collectionView.dequeueConfiguredReusableCell(using: filterWarningCell, for: indexPath, item: filterTitle)
} }
case .gap: case .gap:
return collectionView.dequeueConfiguredReusableCell(using: gapCell, for: indexPath, item: ()) return collectionView.dequeueConfiguredReusableCell(using: gapCell, for: indexPath, item: ())
@ -787,11 +781,10 @@ extension TimelineViewController: UICollectionViewDelegate {
let status = mastodonController.persistentContainer.status(for: id)! let status = mastodonController.persistentContainer.status(for: id)!
if filterState.isWarning { if filterState.isWarning {
filterState.setResult(.allow) filterState.setResult(.allow)
collectionView.deselectItem(at: indexPath, animated: true)
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
snapshot.reloadItems([item]) snapshot.reconfigureItems([item])
dataSource.apply(snapshot, animatingDifferences: true) { dataSource.apply(snapshot, animatingDifferences: true)
collectionView.deselectItem(at: indexPath, animated: true)
}
} else { } else {
// if the status in the timeline is a reblog, show the status that it is a reblog of // if the status in the timeline is a reblog, show the status that it is a reblog of
selected(status: status.reblog?.id ?? id, state: collapseState.copy()) selected(status: status.reblog?.id ?? id, state: collapseState.copy())

View File

@ -242,6 +242,18 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
[replyButton, favoriteButton, reblogButton, moreButton] [replyButton, favoriteButton, reblogButton, moreButton]
} }
private var contentViewMode: ContentViewMode!
private let statusContainer = UIView().configure {
$0.translatesAutoresizingMaskIntoConstraints = false
}
private let filteredLabel = UILabel().configure {
$0.textAlignment = .center
$0.textColor = .secondaryLabel
$0.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitItalic)!, size: 0)
$0.adjustsFontForContentSizeCategory = true
$0.translatesAutoresizingMaskIntoConstraints = false
}
// MARK: Cell state // MARK: Cell state
private var mainContainerTopToReblogLabelConstraint: NSLayoutConstraint! private var mainContainerTopToReblogLabelConstraint: NSLayoutConstraint!
@ -275,37 +287,39 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
private var updateTimestampWorkItem: DispatchWorkItem? private var updateTimestampWorkItem: DispatchWorkItem?
private var hasCreatedObservers = false private var hasCreatedObservers = false
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
setContentViewMode(.status)
for subview in [timelineReasonHStack, mainContainer, actionsContainer] { for subview in [timelineReasonHStack, mainContainer, actionsContainer] {
subview.translatesAutoresizingMaskIntoConstraints = false subview.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(subview) statusContainer.addSubview(subview)
} }
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4) mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4)
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8) mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 8)
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4) mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6) mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6)
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -6) let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: statusContainer.bottomAnchor, constant: -6)
// sometimes during intermediate layouts, there are conflicting constraints, so let this one get broken temporarily, to avoid a bunch of printing // sometimes during intermediate layouts, there are conflicting constraints, so let this one get broken temporarily, to avoid a bunch of printing
metaIndicatorsBottomConstraint.priority = .init(999) metaIndicatorsBottomConstraint.priority = .init(999)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
// why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced // why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced
timelineReasonHStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), timelineReasonHStack.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 4),
timelineReasonLabel.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor), timelineReasonLabel.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor),
timelineReasonHStack.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -16), timelineReasonHStack.trailingAnchor.constraint(lessThanOrEqualTo: statusContainer.trailingAnchor, constant: -16),
mainContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), mainContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
mainContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
actionsContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
actionsContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
// yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven // yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven
actionsContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6), actionsContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6),
metaIndicatorsBottomConstraint, metaIndicatorsBottomConstraint,
]) ])
@ -422,11 +436,57 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
// MARK: Configure UI // MARK: Configure UI
func updateUI(statusID: String, state: CollapseState) { private func setContentViewMode(_ mode: ContentViewMode) {
guard mode != contentViewMode else {
return
}
contentViewMode = mode
switch mode {
case .status:
// make the offscreen view transparent so the filtered -> status animation looks better
statusContainer.layer.opacity = 1
filteredLabel.layer.opacity = 0
filteredLabel.removeFromSuperview()
contentView.addSubview(statusContainer)
NSLayoutConstraint.activate([
statusContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
statusContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
statusContainer.topAnchor.constraint(equalTo: contentView.topAnchor),
statusContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
case .filtered:
statusContainer.layer.opacity = 0
filteredLabel.layer.opacity = 1
statusContainer.removeFromSuperview()
contentView.addSubview(filteredLabel)
NSLayoutConstraint.activate([
filteredLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
filteredLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
filteredLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1),
contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: filteredLabel.bottomAnchor, multiplier: 1),
])
}
}
func updateUI(statusID: String, state: CollapseState, filterResult: Filterer.Result) {
guard var status = mastodonController.persistentContainer.status(for: statusID) else { guard var status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError() fatalError()
} }
switch filterResult {
case .allow:
setContentViewMode(.status)
case .warn(let filterTitle):
let attrStr = NSMutableAttributedString(string: "Filtered: \(filterTitle) ")
let showStr = NSAttributedString(string: "Show", attributes: [.foregroundColor: UIColor.tintColor])
attrStr.append(showStr)
filteredLabel.attributedText = attrStr
setContentViewMode(.filtered)
case .hide:
fatalError("unreachable")
}
createObservers() createObservers()
self.statusState = state self.statusState = state
@ -659,6 +719,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
extension TimelineStatusCollectionViewCell {
private enum ContentViewMode {
case status
case filtered
}
}
extension TimelineStatusCollectionViewCell: UIContextMenuInteractionDelegate { extension TimelineStatusCollectionViewCell: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return contextMenuConfigurationForAccount(sourceView: interaction.view!) return contextMenuConfigurationForAccount(sourceView: interaction.view!)