591 lines
27 KiB
Swift
591 lines
27 KiB
Swift
//
|
|
// NotificationsCollectionViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 5/6/23.
|
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import Combine
|
|
import Sentry
|
|
|
|
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
|
|
|
weak var mastodonController: MastodonController!
|
|
|
|
private let allowedTypes: [Pachyderm.Notification.Kind]
|
|
private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow]
|
|
|
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
|
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
|
|
|
private(set) var collectionView: UICollectionView!
|
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
|
|
private var newer: RequestRange?
|
|
private var older: RequestRange?
|
|
|
|
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
|
self.allowedTypes = allowedTypes
|
|
self.mastodonController = mastodonController
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
self.controller = TimelineLikeController(delegate: self)
|
|
|
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Notifications"))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
|
config.backgroundColor = .appBackground
|
|
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 {
|
|
section.contentInsetsReference = .readableContent
|
|
}
|
|
return section
|
|
}
|
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
collectionView.delegate = self
|
|
collectionView.dragDelegate = self
|
|
collectionView.allowsFocus = true
|
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(collectionView)
|
|
NSLayoutConstraint.activate([
|
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
|
|
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> {
|
|
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
let statusID = itemIdentifier.notifications.first!.status!.id
|
|
let statusState = itemIdentifier.statusState!
|
|
cell.updateUI(statusID: statusID, state: statusState, filterResult: .allow, precomputedContent: nil)
|
|
}
|
|
let actionGroupCell = UICollectionView.CellRegistration<ActionNotificationGroupCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.updateUI(group: itemIdentifier)
|
|
}
|
|
let followCell = UICollectionView.CellRegistration<FollowNotificationGroupCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.updateUI(group: itemIdentifier)
|
|
}
|
|
let followRequestCell = UICollectionView.CellRegistration<FollowRequestNotificationCollectionViewCell, Pachyderm.Notification> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.updateUI(notification: itemIdentifier)
|
|
}
|
|
let pollCell = UICollectionView.CellRegistration<PollFinishedNotificationCollectionViewCell, Pachyderm.Notification> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.updateUI(notification: itemIdentifier)
|
|
}
|
|
let updateCell = UICollectionView.CellRegistration<StatusUpdatedNotificationCollectionViewCell, Pachyderm.Notification> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.updateUI(notification: itemIdentifier)
|
|
}
|
|
let unknownCell = UICollectionView.CellRegistration<UICollectionViewListCell, ()> { cell, indexPath, itemIdentifier in
|
|
var config = cell.defaultContentConfiguration()
|
|
config.text = "Unknown Notification"
|
|
cell.contentConfiguration = config
|
|
}
|
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
|
switch itemIdentifier {
|
|
case .group(let group):
|
|
switch group.kind {
|
|
case .status, .mention:
|
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: group)
|
|
case .favourite, .reblog:
|
|
return collectionView.dequeueConfiguredReusableCell(using: actionGroupCell, for: indexPath, item: group)
|
|
case .follow:
|
|
return collectionView.dequeueConfiguredReusableCell(using: followCell, for: indexPath, item: group)
|
|
case .followRequest:
|
|
return collectionView.dequeueConfiguredReusableCell(using: followRequestCell, for: indexPath, item: group.notifications.first!)
|
|
case .poll:
|
|
return collectionView.dequeueConfiguredReusableCell(using: pollCell, for: indexPath, item: group.notifications.first!)
|
|
case .update:
|
|
return collectionView.dequeueConfiguredReusableCell(using: updateCell, for: indexPath, item: group.notifications.first!)
|
|
default:
|
|
return collectionView.dequeueConfiguredReusableCell(using: unknownCell, for: indexPath, item: ())
|
|
}
|
|
case .loadingIndicator:
|
|
return self.loadingIndicatorCell(for: indexPath)
|
|
case .confirmLoadMore:
|
|
return self.confirmLoadMoreCell(for: indexPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
clearSelectionOnAppear(animated: animated)
|
|
|
|
if case .notLoadedInitial = controller.state {
|
|
Task {
|
|
await controller.loadInitial()
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func 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)
|
|
}
|
|
|
|
}
|
|
|
|
extension NotificationsCollectionViewController {
|
|
enum Section: TimelineLikeCollectionViewSection {
|
|
case notifications
|
|
case footer
|
|
|
|
static var entries: Self { .notifications }
|
|
}
|
|
enum Item: TimelineLikeCollectionViewItem {
|
|
case group(NotificationGroup)
|
|
case loadingIndicator
|
|
case confirmLoadMore
|
|
|
|
static func fromTimelineItem(_ item: NotificationGroup) -> Self {
|
|
return .group(item)
|
|
}
|
|
|
|
var group: NotificationGroup? {
|
|
if case .group(let group) = self {
|
|
return group
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var isSelectable: Bool {
|
|
switch self {
|
|
case .group(_):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var hidesSeparators: Bool {
|
|
switch self {
|
|
case .loadingIndicator, .confirmLoadMore:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: TimelineLikeControllerDelegate
|
|
extension NotificationsCollectionViewController {
|
|
typealias TimelineItem = NotificationGroup
|
|
|
|
private static let pageSize = 40
|
|
|
|
private func request(range: RequestRange) -> Request<[Pachyderm.Notification]> {
|
|
if mastodonController.instanceFeatures.notificationsAllowedTypes {
|
|
return Client.getNotifications(allowedTypes: allowedTypes, range: range)
|
|
} else {
|
|
var types = Set(Notification.Kind.allCases)
|
|
types.remove(.unknown)
|
|
allowedTypes.forEach { types.remove($0) }
|
|
return Client.getNotifications(excludedTypes: Array(types), range: range)
|
|
}
|
|
}
|
|
|
|
private func validateNotifications(_ notifications: [Pachyderm.Notification]) -> [Pachyderm.Notification] {
|
|
return notifications.compactMap { notif in
|
|
if notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite) {
|
|
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
|
crumb.data = [
|
|
"id": notif.id,
|
|
"type": notif.kind.rawValue,
|
|
"created_at": notif.createdAt.formatted(.iso8601),
|
|
"account": notif.account.id,
|
|
]
|
|
SentrySDK.addBreadcrumb(crumb)
|
|
return nil
|
|
} else {
|
|
return notif
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadInitial() async throws -> [NotificationGroup] {
|
|
let request = self.request(range: .count(NotificationsCollectionViewController.pageSize))
|
|
let (notifications, _) = try await mastodonController.run(request)
|
|
|
|
if !notifications.isEmpty {
|
|
self.newer = .after(id: notifications.first!.id, count: NotificationsCollectionViewController.pageSize)
|
|
self.older = .before(id: notifications.last!.id, count: NotificationsCollectionViewController.pageSize)
|
|
}
|
|
|
|
let validated = validateNotifications(notifications)
|
|
|
|
await withCheckedContinuation { continuation in
|
|
mastodonController.persistentContainer.addAll(notifications: validated) {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
|
|
return NotificationGroup.createGroups(notifications: validated, only: self.groupTypes)
|
|
}
|
|
|
|
func loadNewer() async throws -> [NotificationGroup] {
|
|
guard let newer else {
|
|
throw Error.noNewer
|
|
}
|
|
|
|
let request = self.request(range: newer)
|
|
let (notifications, _) = try await mastodonController.run(request)
|
|
|
|
if !notifications.isEmpty {
|
|
self.newer = .after(id: notifications.first!.id, count: NotificationsCollectionViewController.pageSize)
|
|
}
|
|
|
|
let validated = validateNotifications(notifications)
|
|
guard !validated.isEmpty else {
|
|
throw Error.allCaughtUp
|
|
}
|
|
|
|
await withCheckedContinuation { continuation in
|
|
mastodonController.persistentContainer.addAll(notifications: validated) {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
|
|
let newerGroups = NotificationGroup.createGroups(notifications: validated, only: self.groupTypes)
|
|
let existingGroups = dataSource.snapshot().itemIdentifiers(inSection: .notifications).compactMap(\.group)
|
|
return NotificationGroup.mergeGroups(first: newerGroups, second: existingGroups, only: self.groupTypes)
|
|
}
|
|
|
|
func handlePrependItems(_ timelineItems: [NotificationGroup]) async {
|
|
let topItem = dataSource.snapshot().itemIdentifiers(inSection: .notifications).first
|
|
|
|
// we always replace all, because new items are merged with existing ones
|
|
await handleReplaceAllItems(timelineItems)
|
|
|
|
// preserve the scroll position
|
|
// todo: this won't work for cmd+r when not at top
|
|
if let topID = topItem?.group?.notifications.first?.id {
|
|
// the exact item may have changed, due to merging
|
|
let newTopGroup = timelineItems.first {
|
|
$0.notifications.contains {
|
|
$0.id == topID
|
|
}
|
|
}!
|
|
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup)) {
|
|
collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadOlder() async throws -> [NotificationGroup] {
|
|
guard let older else {
|
|
throw Error.noOlder
|
|
}
|
|
|
|
let request = self.request(range: older)
|
|
let (notifications, _) = try await mastodonController.run(request)
|
|
|
|
if !notifications.isEmpty {
|
|
self.older = .before(id: notifications.last!.id, count: NotificationsCollectionViewController.pageSize)
|
|
}
|
|
|
|
let validated = validateNotifications(notifications)
|
|
|
|
await withCheckedContinuation { continuation in
|
|
mastodonController.persistentContainer.addAll(notifications: validated) {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
|
|
let olderGroups = NotificationGroup.createGroups(notifications: validated, only: self.groupTypes)
|
|
let existingGroups = dataSource.snapshot().itemIdentifiers(inSection: .notifications).compactMap(\.group)
|
|
return NotificationGroup.mergeGroups(first: existingGroups, second: olderGroups, only: self.groupTypes)
|
|
}
|
|
|
|
func handleAppendItems(_ timelineItems: [NotificationGroup]) async {
|
|
await handleReplaceAllItems(timelineItems)
|
|
}
|
|
|
|
enum Error: TimelineLikeCollectionViewError {
|
|
case noNewer
|
|
case noOlder
|
|
case allCaughtUp
|
|
}
|
|
}
|
|
|
|
extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
|
guard case .notifications = dataSource.sectionIdentifier(for: indexPath.section) else {
|
|
return
|
|
}
|
|
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
|
if indexPath.row == itemsInSection - 1 {
|
|
Task {
|
|
await controller.loadOlder()
|
|
}
|
|
}
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
|
return dataSource.itemIdentifier(for: indexPath)?.isSelectable ?? false
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
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, 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) {
|
|
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
|
}
|
|
}
|
|
|
|
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 }
|
|
}
|
|
|
|
extension NotificationsCollectionViewController: MenuActionProvider {
|
|
}
|
|
|
|
extension NotificationsCollectionViewController: StatusCollectionViewCellDelegate {
|
|
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
|
if let indexPath = collectionView.indexPath(for: cell) {
|
|
var snapshot = dataSource.snapshot()
|
|
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
|
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
|
}
|
|
}
|
|
|
|
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
|
}
|
|
}
|