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> {
|
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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reloadItems([item])
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
|
||||||
collectionView.deselectItem(at: indexPath, animated: true)
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
}
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.reconfigureItems([item])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: 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())
|
||||||
|
|
|
@ -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!
|
||||||
|
@ -279,33 +291,35 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
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!)
|
||||||
|
|
Loading…
Reference in New Issue