forked from shadowfacts/Tusker
Convert rest of notifications screen to collection view
This commit is contained in:
parent
a133955489
commit
3181c47fde
|
@ -15,7 +15,7 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
|||
public let statusState: CollapseState?
|
||||
|
||||
@MainActor
|
||||
init?(notifications: [Notification]) {
|
||||
public init?(notifications: [Notification]) {
|
||||
guard !notifications.isEmpty else { return nil }
|
||||
self.notifications = notifications
|
||||
self.id = notifications.first!.id
|
||||
|
|
|
@ -225,7 +225,7 @@ class FollowRequestNotificationCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc private func rejectButtonPressed() {
|
||||
@objc func rejectButtonPressed() {
|
||||
acceptButton.isEnabled = false
|
||||
rejectButton.isEnabled = false
|
||||
|
||||
|
@ -251,7 +251,7 @@ class FollowRequestNotificationCollectionViewCell: UICollectionViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func acceptButtonPressed() {
|
||||
@objc func acceptButtonPressed() {
|
||||
acceptButton.isEnabled = false
|
||||
rejectButton.isEnabled = false
|
||||
|
||||
|
|
|
@ -35,9 +35,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
|
||||
self.controller = TimelineLikeController(delegate: self)
|
||||
|
||||
// todo: title
|
||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Notifications"))
|
||||
// todo: user activity
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -49,8 +47,38 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
config.backgroundColor = .appBackground
|
||||
// todo: swipe actions
|
||||
// todo: separators
|
||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||
}
|
||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in
|
||||
let dismissAction = UIContextualAction(style: .destructive, title: "Dismiss") { _, _, completion in
|
||||
Task {
|
||||
await self.dismissNotificationsInGroup(at: indexPath)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
dismissAction.accessibilityLabel = "Dismiss Notification"
|
||||
dismissAction.image = UIImage(systemName: "clear.fill")
|
||||
|
||||
let cellConfig = (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||
let config = UISwipeActionsConfiguration(actions: (cellConfig?.actions ?? []) + [dismissAction])
|
||||
config.performsFirstActionWithFullSwipe = cellConfig?.performsFirstActionWithFullSwipe ?? false
|
||||
return config
|
||||
}
|
||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in
|
||||
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
|
||||
return sectionSeparatorConfiguration
|
||||
}
|
||||
var config = sectionSeparatorConfiguration
|
||||
if item.hidesSeparators {
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = .hidden
|
||||
} else {
|
||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
}
|
||||
return config
|
||||
}
|
||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
|
@ -60,8 +88,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
// todo: drag
|
||||
//collectionView.dragDelegate = self
|
||||
collectionView.dragDelegate = self
|
||||
collectionView.allowsFocus = true
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(collectionView)
|
||||
|
@ -74,6 +101,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
|
||||
registerTimelineLikeCells()
|
||||
dataSource = createDataSource()
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
collectionView.refreshControl = UIRefreshControl()
|
||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
|
@ -148,7 +180,49 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
}
|
||||
|
||||
@objc func refresh() {
|
||||
// todo: refresh
|
||||
Task { @MainActor in
|
||||
if case .notLoadedInitial = controller.state {
|
||||
await controller.loadInitial()
|
||||
} else {
|
||||
await controller.loadNewer()
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
collectionView.refreshControl?.endRefreshing()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissNotificationsInGroup(at indexPath: IndexPath) async {
|
||||
guard case .group(let group) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
let notifications = group.notifications
|
||||
let dismissFailedIndices = await withTaskGroup(of: (Int, Bool).self) { group -> [Int] in
|
||||
for (index, notification) in notifications.enumerated() {
|
||||
group.addTask {
|
||||
do {
|
||||
_ = try await self.mastodonController.run(Notification.dismiss(id: notification.id))
|
||||
return (index, true)
|
||||
} catch {
|
||||
return (index, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
return await group.reduce(into: [], { partialResult, value in
|
||||
if !value.1 {
|
||||
partialResult.append(value.0)
|
||||
}
|
||||
})
|
||||
}
|
||||
var snapshot = dataSource.snapshot()
|
||||
if dismissFailedIndices.isEmpty {
|
||||
snapshot.deleteItems([.group(group)])
|
||||
} else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count {
|
||||
let dismissFailed = dismissFailedIndices.sorted().map { notifications[$0] }
|
||||
snapshot.insertItems([.group(NotificationGroup(notifications: dismissFailed)!)], afterItem: .group(group))
|
||||
snapshot.deleteItems([.group(group)])
|
||||
}
|
||||
await apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -185,6 +259,15 @@ extension NotificationsCollectionViewController {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var hidesSeparators: Bool {
|
||||
switch self {
|
||||
case .loadingIndicator, .confirmLoadMore:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -330,12 +413,104 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
|||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
// todo
|
||||
guard case .group(let group) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
switch group.kind {
|
||||
case .mention, .status, .poll, .update:
|
||||
let statusID = group.notifications.first!.status!.id
|
||||
let state = group.statusState?.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
|
||||
let accountIDs = group.notifications.map(\.account.id)
|
||||
let vc = StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: mastodonController)
|
||||
show(vc)
|
||||
case .follow:
|
||||
let accountIDs = group.notifications.map(\.account.id)
|
||||
switch accountIDs.count {
|
||||
case 0:
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
case 1:
|
||||
selected(account: accountIDs.first!)
|
||||
default:
|
||||
let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
||||
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
|
||||
show(vc)
|
||||
}
|
||||
case .followRequest:
|
||||
selected(account: group.notifications.first!.account.id)
|
||||
case .unknown:
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
// todo
|
||||
return nil
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard case .group(let group) = dataSource.itemIdentifier(for: indexPath),
|
||||
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
switch group.kind {
|
||||
case .mention, .status, .poll, .update:
|
||||
guard let statusID = group.notifications.first?.status?.id,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
return nil
|
||||
}
|
||||
let state = group.statusState?.copy() ?? .unknown
|
||||
return UIContextMenuConfiguration {
|
||||
ConversationViewController(for: statusID, state: state, mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
UIMenu(children: self.actionsForStatus(status, source: .view(cell), includeStatusButtonActions: group.kind == .poll || group.kind == .update))
|
||||
}
|
||||
case .favourite, .reblog:
|
||||
return UIContextMenuConfiguration(previewProvider: {
|
||||
let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog
|
||||
let statusID = group.notifications.first!.status!.id
|
||||
let accountIDs = group.notifications.map(\.account.id)
|
||||
return StatusActionAccountListViewController(actionType: type, statusID: statusID, statusState: .unknown, accountIDs: accountIDs, mastodonController: self.mastodonController)
|
||||
})
|
||||
case .follow:
|
||||
let accountIDs = group.notifications.map(\.account.id)
|
||||
return UIContextMenuConfiguration {
|
||||
if accountIDs.count == 1 {
|
||||
return ProfileViewController(accountID: accountIDs.first!, mastodonController: self.mastodonController)
|
||||
} else {
|
||||
let vc = AccountListViewController(accountIDs: accountIDs, mastodonController: self.mastodonController)
|
||||
vc.title = NSLocalizedString("Followed By", comment: "followed by accounts list title")
|
||||
return vc
|
||||
}
|
||||
} actionProvider: { _ in
|
||||
if accountIDs.count == 1 {
|
||||
return UIMenu(children: self.actionsForProfile(accountID: accountIDs.first!, source: .view(cell)))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case .followRequest:
|
||||
let accountID = group.notifications.first!.account.id
|
||||
return UIContextMenuConfiguration {
|
||||
ProfileViewController(accountID: accountID, mastodonController: self.mastodonController)
|
||||
} actionProvider: { _ in
|
||||
let cell = cell as! FollowRequestNotificationCollectionViewCell
|
||||
let acceptRejectChildren = [
|
||||
UIAction(title: "Accept", image: UIImage(systemName: "checkmark.circle"), handler: { _ in cell.acceptButtonPressed() }),
|
||||
UIAction(title: "Reject", image: UIImage(systemName: "xmark.circle"), handler: { _ in cell.rejectButtonPressed() }),
|
||||
]
|
||||
let acceptRejectMenu: UIMenu
|
||||
if #available(iOS 16.0, *) {
|
||||
acceptRejectMenu = UIMenu(options: .displayInline, preferredElementSize: .medium, children: acceptRejectChildren)
|
||||
} else {
|
||||
acceptRejectMenu = UIMenu(options: .displayInline, children: acceptRejectChildren)
|
||||
}
|
||||
return UIMenu(children: [
|
||||
acceptRejectMenu,
|
||||
UIMenu(options: .displayInline, children: self.actionsForProfile(accountID: accountID, source: .view(cell))),
|
||||
])
|
||||
}
|
||||
case .unknown:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
|
@ -343,6 +518,41 @@ 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 {
|
||||
return []
|
||||
}
|
||||
switch group.kind {
|
||||
case .mention, .status:
|
||||
// not combiend with .poll and .update below, b/c TimelineStatusCollectionViewCell handles checking whether the poll view is tracking
|
||||
let cell = collectionView.cellForItem(at: indexPath) as! TimelineStatusCollectionViewCell
|
||||
return cell.dragItemsForBeginning(session: session)
|
||||
case .poll, .update:
|
||||
let status = group.notifications.first!.status!
|
||||
let provider = NSItemProvider(object: URL(status.url!)! as NSURL)
|
||||
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
case .favourite, .reblog:
|
||||
return []
|
||||
case .follow, .followRequest:
|
||||
guard group.notifications.count == 1 else {
|
||||
return []
|
||||
}
|
||||
let account = group.notifications.first!.account
|
||||
let provider = NSItemProvider(object: account.url as NSURL)
|
||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: mastodonController.accountInfo!.id)
|
||||
activity.displaysAuxiliaryScene = true
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
case .unknown:
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationsCollectionViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue