Better filter cell and animation for showing filtered post
This commit is contained in:
parent
81abcfcf7b
commit
a17afe247c
|
@ -64,7 +64,8 @@ class TrendingStatusesViewController: UIViewController {
|
|||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
||||
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
|
||||
cell.indicator.startAnimating()
|
||||
|
|
|
@ -113,7 +113,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
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
|
||||
switch itemIdentifier {
|
||||
|
|
|
@ -80,7 +80,7 @@ class StatusActionAccountListViewController: UIViewController {
|
|||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Void> { [unowned self] cell, indexPath, _ in
|
||||
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
|
||||
cell.delegate = self
|
||||
|
|
|
@ -69,10 +69,10 @@ class InstanceTimelineViewController: TimelineViewController {
|
|||
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.overrideMastodonController = mastodonController
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
cell.updateUI(statusID: id, state: state, filterResult: filterResult)
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
|
|
|
@ -105,27 +105,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
}
|
||||
|
||||
// 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
|
||||
if case .home = timeline {
|
||||
cell.showFollowedHashtags = true
|
||||
} else {
|
||||
cell.showFollowedHashtags = false
|
||||
}
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
cell.updateUI(statusID: id, state: state, filterResult: filterResult)
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
|
||||
self.configureStatusCell(cell, id: item.0, state: item.1)
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result)> { [unowned self] cell, indexPath, item in
|
||||
self.configureStatusCell(cell, id: item.0, state: item.1, filterResult: item.2)
|
||||
}
|
||||
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
|
||||
cell.showsIndicator = false
|
||||
}
|
||||
|
@ -143,13 +138,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
switch itemIdentifier {
|
||||
case .status(id: let id, collapseState: let state, filterState: let filterState):
|
||||
let status = { self.mastodonController.persistentContainer.status(for: id)! }
|
||||
switch filterState.resolveFor(status: status, resolver: filterer) {
|
||||
case .allow:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
||||
let result = filterState.resolveFor(status: status, resolver: filterer)
|
||||
switch result {
|
||||
case .allow, .warn(_):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, result))
|
||||
case .hide:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
|
||||
case .warn(let filterTitle):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: filterWarningCell, for: indexPath, item: filterTitle)
|
||||
}
|
||||
case .gap:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: gapCell, for: indexPath, item: ())
|
||||
|
@ -787,11 +781,10 @@ extension TimelineViewController: UICollectionViewDelegate {
|
|||
let status = mastodonController.persistentContainer.status(for: id)!
|
||||
if filterState.isWarning {
|
||||
filterState.setResult(.allow)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
snapshot.reconfigureItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
} else {
|
||||
// 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())
|
||||
|
|
|
@ -242,6 +242,18 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
[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
|
||||
|
||||
private var mainContainerTopToReblogLabelConstraint: NSLayoutConstraint!
|
||||
|
@ -275,37 +287,39 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
private var updateTimestampWorkItem: DispatchWorkItem?
|
||||
private var hasCreatedObservers = false
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
setContentViewMode(.status)
|
||||
|
||||
for subview in [timelineReasonHStack, mainContainer, actionsContainer] {
|
||||
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(subview)
|
||||
statusContainer.addSubview(subview)
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
metaIndicatorsBottomConstraint.priority = .init(999)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// 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),
|
||||
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.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
mainContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
|
||||
mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
|
||||
|
||||
actionsContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
actionsContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
|
||||
actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
|
||||
// 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,
|
||||
])
|
||||
|
@ -422,11 +436,57 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
|
||||
// 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 {
|
||||
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()
|
||||
|
||||
self.statusState = state
|
||||
|
@ -659,6 +719,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
|
||||
}
|
||||
|
||||
extension TimelineStatusCollectionViewCell {
|
||||
private enum ContentViewMode {
|
||||
case status
|
||||
case filtered
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineStatusCollectionViewCell: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return contextMenuConfigurationForAccount(sourceView: interaction.view!)
|
||||
|
|
Loading…
Reference in New Issue