Support filtering on Notifications screen
This commit is contained in:
parent
b909a633a6
commit
dc83172aea
|
@ -14,6 +14,7 @@ import Sentry
|
|||
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
private let filterer: Filterer
|
||||
|
||||
private let allowedTypes: [Pachyderm.Notification.Kind]
|
||||
private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow]
|
||||
|
@ -31,6 +32,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
self.allowedTypes = allowedTypes
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications)
|
||||
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
|
||||
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self))
|
||||
|
@ -73,6 +79,10 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
if item.hidesSeparators {
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
} else if case .group(_, _, .some(let filterState)) = item,
|
||||
self.filterer.isKnownHide(state: filterState) {
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
} else {
|
||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
|
@ -106,14 +116,18 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
collectionView.refreshControl = UIRefreshControl()
|
||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||
#endif
|
||||
|
||||
filterer.filtersChanged = { [unowned self] actionsChanged in
|
||||
self.reapplyFilters(actionsChanged: actionsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (NotificationGroup, CollapseState)> { [unowned self] cell, indexPath, itemIdentifier in
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (NotificationGroup, CollapseState, Filterer.Result, NSAttributedString?)> { [unowned self] cell, indexPath, itemIdentifier in
|
||||
cell.delegate = self
|
||||
let statusID = itemIdentifier.0.notifications.first!.status!.id
|
||||
let statusState = itemIdentifier.1
|
||||
cell.updateUI(statusID: statusID, state: statusState, filterResult: .allow, precomputedContent: nil)
|
||||
cell.updateUI(statusID: statusID, state: statusState, filterResult: itemIdentifier.2, precomputedContent: itemIdentifier.3)
|
||||
}
|
||||
let actionGroupCell = UICollectionView.CellRegistration<ActionNotificationGroupCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
|
||||
cell.delegate = self
|
||||
|
@ -140,12 +154,23 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
config.text = "Unknown Notification"
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in
|
||||
}
|
||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case .group(let group, let collapseState):
|
||||
case .group(let group, let collapseState, let filterState):
|
||||
switch group.kind {
|
||||
case .status, .mention:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (group, collapseState!))
|
||||
let (result, precomputedContent) = self.filterer.resolve(state: filterState!) {
|
||||
let id = group.notifications.first!.status!.id
|
||||
return (self.mastodonController.persistentContainer.status(for: id)!, false)
|
||||
}
|
||||
switch result {
|
||||
case .allow, .warn(_):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (group, collapseState!, result, precomputedContent))
|
||||
case .hide:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
|
||||
}
|
||||
case .favourite, .reblog:
|
||||
return collectionView.dequeueConfiguredReusableCell(using: actionGroupCell, for: indexPath, item: group)
|
||||
case .follow:
|
||||
|
@ -192,8 +217,44 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
}
|
||||
|
||||
private func filterResult(state: FilterState, statusID: String) -> (Filterer.Result, NSAttributedString?) {
|
||||
let status = {
|
||||
let status = self.mastodonController.persistentContainer.status(for: statusID)!
|
||||
// if the status is a reblog of another one, filter based on that one
|
||||
if let reblogged = status.reblog {
|
||||
return (reblogged, true)
|
||||
} else {
|
||||
return (status, false)
|
||||
}
|
||||
}
|
||||
return filterer.resolve(state: state, status: status)
|
||||
}
|
||||
|
||||
private func reapplyFilters(actionsChanged: Bool) {
|
||||
let visible = collectionView.indexPathsForVisibleItems
|
||||
let items = visible
|
||||
.compactMap { dataSource.itemIdentifier(for: $0) }
|
||||
.filter {
|
||||
if case .group(_, _, .some(_)) = $0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
if actionsChanged {
|
||||
snapshot.reloadItems(items)
|
||||
} else {
|
||||
snapshot.reconfigureItems(items)
|
||||
}
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
private func dismissNotificationsInGroup(at indexPath: IndexPath) async {
|
||||
guard case .group(let group, let collapseState) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard case .group(let group, let collapseState, let filterState) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
let notifications = group.notifications
|
||||
|
@ -216,11 +277,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
if dismissFailedIndices.isEmpty {
|
||||
snapshot.deleteItems([.group(group, collapseState)])
|
||||
snapshot.deleteItems([.group(group, collapseState, filterState)])
|
||||
} else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count {
|
||||
let dismissFailed = dismissFailedIndices.sorted().map { notifications[$0] }
|
||||
snapshot.insertItems([.group(NotificationGroup(notifications: dismissFailed)!, collapseState)], afterItem: .group(group, collapseState))
|
||||
snapshot.deleteItems([.group(group, collapseState)])
|
||||
snapshot.insertItems([.group(NotificationGroup(notifications: dismissFailed)!, collapseState, filterState)], afterItem: .group(group, collapseState, filterState))
|
||||
snapshot.deleteItems([.group(group, collapseState, filterState)])
|
||||
}
|
||||
await apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
|
@ -235,22 +296,22 @@ extension NotificationsCollectionViewController {
|
|||
static var entries: Self { .notifications }
|
||||
}
|
||||
enum Item: TimelineLikeCollectionViewItem {
|
||||
case group(NotificationGroup, CollapseState?)
|
||||
case group(NotificationGroup, CollapseState?, FilterState?)
|
||||
case loadingIndicator
|
||||
case confirmLoadMore
|
||||
|
||||
static func fromTimelineItem(_ item: NotificationGroup) -> Self {
|
||||
switch item.kind {
|
||||
case .mention, .status:
|
||||
return .group(item, .unknown)
|
||||
return .group(item, .unknown, .unknown)
|
||||
default:
|
||||
return .group(item, nil)
|
||||
return .group(item, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.group(let a, _), .group(let b, _)):
|
||||
case (.group(let a, _, _), .group(let b, _, _)):
|
||||
return a == b
|
||||
case (.loadingIndicator, .loadingIndicator):
|
||||
return true
|
||||
|
@ -263,7 +324,7 @@ extension NotificationsCollectionViewController {
|
|||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .group(let group, _):
|
||||
case .group(let group, _, _):
|
||||
hasher.combine(0)
|
||||
hasher.combine(group)
|
||||
case .loadingIndicator:
|
||||
|
@ -274,7 +335,7 @@ extension NotificationsCollectionViewController {
|
|||
}
|
||||
|
||||
var group: NotificationGroup? {
|
||||
if case .group(let group, _) = self {
|
||||
if case .group(let group, _, _) = self {
|
||||
return group
|
||||
} else {
|
||||
return nil
|
||||
|
@ -283,7 +344,7 @@ extension NotificationsCollectionViewController {
|
|||
|
||||
var isSelectable: Bool {
|
||||
switch self {
|
||||
case .group(_, _):
|
||||
case .group(_, _, _):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -399,7 +460,7 @@ extension NotificationsCollectionViewController {
|
|||
$0.id == topID
|
||||
}
|
||||
}!
|
||||
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup, nil)) {
|
||||
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup, nil, nil)) {
|
||||
collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
|
@ -459,14 +520,24 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
|||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard case .group(let group, let collapseState) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .group(let group, let collapseState, let filterState) = item else {
|
||||
return
|
||||
}
|
||||
switch group.kind {
|
||||
case .mention, .status, .poll, .update:
|
||||
let statusID = group.notifications.first!.status!.id
|
||||
let state = collapseState?.copy() ?? .unknown
|
||||
selected(status: statusID, state: state)
|
||||
if let filterState,
|
||||
filterState.isWarning == true {
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
} else {
|
||||
let statusID = group.notifications.first!.status!.id
|
||||
let state = collapseState?.copy() ?? .unknown
|
||||
selected(status: statusID, state: state)
|
||||
}
|
||||
case .favourite, .reblog:
|
||||
let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog
|
||||
let statusID = group.notifications.first!.status!.id
|
||||
|
@ -493,7 +564,7 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
|||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard case .group(let group, let collapseState) = dataSource.itemIdentifier(for: indexPath),
|
||||
guard case .group(let group, let collapseState, _) = dataSource.itemIdentifier(for: indexPath),
|
||||
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
@ -566,7 +637,7 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
|||
|
||||
extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard case .group(let group, _) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
guard case .group(let group, _, _) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return []
|
||||
}
|
||||
switch group.kind {
|
||||
|
@ -616,5 +687,13 @@ extension NotificationsCollectionViewController: StatusCollectionViewCellDelegat
|
|||
}
|
||||
|
||||
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
||||
if let indexPath = collectionView.indexPath(for: cell),
|
||||
let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case .group(_, _, .some(let filterState)) = item {
|
||||
filterer.setResult(.allow, for: filterState)
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([item])
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,12 +119,6 @@ extension StatusCollectionViewCell {
|
|||
favoriteButton.isEnabled = mastodonController.loggedIn
|
||||
|
||||
let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
|
||||
// let didResolve = statusState.resolveFor(status: status) {
|
||||
//// // layout so that we can take the content height into consideration when deciding whether to collapse
|
||||
//// layoutIfNeeded()
|
||||
//// return contentContainer.visibleSubviewHeight
|
||||
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
|
||||
// }
|
||||
if didResolve {
|
||||
if statusState.collapsible! && showStatusAutomatically {
|
||||
statusState.collapsed = false
|
||||
|
|
Loading…
Reference in New Issue