From e04cdd16d6c3ae4453de9fafc4a325abb8e26078 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 26 Nov 2022 20:13:16 -0500 Subject: [PATCH] Add preferences for status cell swipe actions Closes #249 --- Tusker.xcodeproj/project.pbxproj | 8 + Tusker/API/InstanceFeatures.swift | 17 +- Tusker/Preferences/Preferences.swift | 2 + Tusker/Preferences/StatusSwipeAction.swift | 172 ++++++++++++++++++ .../Preferences/AppearancePrefsView.swift | 10 + .../Preferences/SwipeActionsPrefsView.swift | 122 +++++++++++++ .../TimelineStatusCollectionViewCell.swift | 60 ++---- .../Status/TimelineStatusTableViewCell.swift | 94 ++-------- 8 files changed, 356 insertions(+), 129 deletions(-) create mode 100644 Tusker/Preferences/StatusSwipeAction.swift create mode 100644 Tusker/Screens/Preferences/SwipeActionsPrefsView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index ed2e9725..22e76a52 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ D61DC84628F498F200B82C6E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84528F498F200B82C6E /* Logging.swift */; }; D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */; }; D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84C28F500D200B82C6E /* ProfileViewController.swift */; }; + D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */; }; + D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */; }; D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; }; D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; }; D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; @@ -406,6 +408,8 @@ D61DC84528F498F200B82C6E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderCollectionViewCell.swift; sourceTree = ""; }; D61DC84C28F500D200B82C6E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSwipeAction.swift; sourceTree = ""; }; + D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsPrefsView.swift; sourceTree = ""; }; D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = ""; }; D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = ""; }; @@ -1031,6 +1035,7 @@ D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */, 04586B4022B2FFB10021BD04 /* PreferencesView.swift */, 04586B4222B301470021BD04 /* AppearancePrefsView.swift */, + D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */, 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */, D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */, D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */, @@ -1133,6 +1138,7 @@ D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */, D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */, D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */, + D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */, ); path = Preferences; sourceTree = ""; @@ -1849,6 +1855,7 @@ D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */, D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */, D60E2F272442372B005F8713 /* StatusMO.swift in Sources */, + D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */, D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */, D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */, D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */, @@ -1952,6 +1959,7 @@ D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */, D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */, D620483423D3801D008A63EF /* LinkTextView.swift in Sources */, + D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */, D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */, D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, diff --git a/Tusker/API/InstanceFeatures.swift b/Tusker/API/InstanceFeatures.swift index 39f5ee4d..3c0db8de 100644 --- a/Tusker/API/InstanceFeatures.swift +++ b/Tusker/API/InstanceFeatures.swift @@ -91,11 +91,20 @@ struct InstanceFeatures { } else if nodeInfo?.software.name == "hometown" { var mastoVersion: Version? var hometownVersion: Version? - // like "1.0.6+3.5.2" let parts = ver.split(separator: "+") - if parts.count == 2 { - mastoVersion = Version(string: String(parts[1])) - hometownVersion = Version(string: String(parts[0])) + if parts.count == 2, + let first = Version(string: String(parts[0])) { + if first > Version(1, 0, 8) { + // like 3.5.5+hometown-1.0.9 + mastoVersion = first + if parts[1].starts(with: "hometown-") { + hometownVersion = Version(string: String(parts[1][parts[1].index(parts[1].startIndex, offsetBy: "hometown-".count + 1)...])) + } + } else { + // like "1.0.6+3.5.2" + hometownVersion = first + mastoVersion = Version(string: String(parts[1])) + } } else { mastoVersion = Version(string: ver) } diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 5f380d14..ac4245ad 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -125,6 +125,8 @@ class Preferences: Codable, ObservableObject { @Published var showIsStatusReplyIcon = false @Published var alwaysShowStatusVisibilityIcon = false @Published var hideActionsInTimeline = false + @Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog] + @Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share] // MARK: Composing @Published var defaultPostVisibility = Status.Visibility.public diff --git a/Tusker/Preferences/StatusSwipeAction.swift b/Tusker/Preferences/StatusSwipeAction.swift new file mode 100644 index 00000000..2dd22bf4 --- /dev/null +++ b/Tusker/Preferences/StatusSwipeAction.swift @@ -0,0 +1,172 @@ +// +// StatusSwipeAction.swift +// Tusker +// +// Created by Shadowfacts on 11/26/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +enum StatusSwipeAction: String, Codable, Hashable, CaseIterable { + case reply + case favorite + case reblog + case share + case bookmark + case openInSafari + + var displayName: String { + switch self { + case .reply: + return "Reply" + case .favorite: + return "Favorite" + case .reblog: + return "Reblog" + case .share: + return "Share" + case .bookmark: + return "Bookmark" + case .openInSafari: + return "Open in Safari" + } + } + + var systemImageName: String { + switch self { + case .reply: + return "arrowshape.turn.up.left.fill" + case .favorite: + return "star.fill" + case .reblog: + return "repeat" + case .share: + return "square.and.arrow.up" + case .bookmark: + return "bookmark.fill" + case .openInSafari: + return "safari" + } + } + + func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { + switch self { + case .reply: + return createReplyAction(status: status, container: container) + case .favorite: + return createFavoriteAction(status: status, container: container) + case .reblog: + return createReblogAction(status: status, container: container) + case .share: + return createShareAction(status: status, container: container) + case .bookmark: + return createBookmarkAction(status: status, container: container) + case .openInSafari: + return createOpenInSafariAction(status: status, container: container) + } + } +} + +protocol StatusSwipeActionContainer: UIView { + var mastodonController: MastodonController! { get } + var navigationDelegate: any TuskerNavigationDelegate { get } + var toastableViewController: ToastableViewController? { get } + + // necessary b/c the reblog-handling logic only exists in the cells + func performReplyAction() +} + +private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { + guard container.mastodonController.loggedIn else { + return nil + } + let action = UIContextualAction(style: .normal, title: "Reply") { [unowned container] _, _, completion in + container.performReplyAction() + completion(true) + } + action.image = UIImage(systemName: "arrowshape.turn.up.left.fill") + action.backgroundColor = container.tintColor + return action +} + +private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { + guard container.mastodonController.loggedIn else { + return nil + } + let title = status.favourited ? "Unfavorite" : "Favorite" + let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in + Task { + await FavoriteService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleFavorite() + completion(true) + } + } + action.image = UIImage(systemName: "star.fill") + action.backgroundColor = status.favourited ? UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) : UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) + return action +} + +private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { + guard container.mastodonController.loggedIn else { + return nil + } + let title = status.reblogged ? "Unreblog" : "Reblog" + let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in + Task { + await ReblogService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleReblog() + completion(true) + } + } + action.image = UIImage(systemName: "repeat") + action.backgroundColor = status.reblogged ? UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) : container.tintColor + return action +} + +private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { + let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in + container.navigationDelegate.showMoreOptions(forStatus: status.id, sourceView: container) + completion(true) + } + // bold to more closesly match other action symbols + let config = UIImage.SymbolConfiguration(weight: .bold) + action.image = UIImage(systemName: "square.and.arrow.up")!.applyingSymbolConfiguration(config)! + action.backgroundColor = .lightGray + return action +} + +private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { + guard container.mastodonController.loggedIn else { + return nil + } + let bookmarked = status.bookmarked ?? false + let title = bookmarked ? "Unbookmark" : "Bookmark" + let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in + Task { @MainActor in + let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id) + do { + let (status, _) = try await container.mastodonController.run(request) + container.mastodonController.persistentContainer.addOrUpdate(status: status) + } catch { + if let toastable = container.toastableViewController { + let config = ToastConfiguration(from: error, with: "Error \(bookmarked ? "Unb" : "B")ookmarking", in: toastable, retryAction: nil) + toastable.showToast(configuration: config, animated: true) + } + } + completion(true) + } + } + action.image = UIImage(systemName: "bookmark.fill") + action.backgroundColor = .systemRed + return action +} + +private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { + let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in + container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) + completion(true) + } + action.image = UIImage(systemName: "safari") + action.backgroundColor = container.tintColor + return action +} diff --git a/Tusker/Screens/Preferences/AppearancePrefsView.swift b/Tusker/Screens/Preferences/AppearancePrefsView.swift index 61bde691..1ef18170 100644 --- a/Tusker/Screens/Preferences/AppearancePrefsView.swift +++ b/Tusker/Screens/Preferences/AppearancePrefsView.swift @@ -59,6 +59,16 @@ struct AppearancePrefsView : View { Toggle(isOn: $preferences.hideActionsInTimeline) { Text("Hide Actions on Timeline") } + NavigationLink("Leading Swipe Actions") { + SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions) + .edgesIgnoringSafeArea(.all) + .navigationTitle("Leading Swipe Actions") + } + NavigationLink("Trailing Swipe Actions") { + SwipeActionsPrefsView(selection: $preferences.trailingStatusSwipeActions) + .edgesIgnoringSafeArea(.all) + .navigationTitle("Trailing Swipe Actions") + } } } } diff --git a/Tusker/Screens/Preferences/SwipeActionsPrefsView.swift b/Tusker/Screens/Preferences/SwipeActionsPrefsView.swift new file mode 100644 index 00000000..b10fd63e --- /dev/null +++ b/Tusker/Screens/Preferences/SwipeActionsPrefsView.swift @@ -0,0 +1,122 @@ +// +// SwipeActionsPrefsView.swift +// Tusker +// +// Created by Shadowfacts on 11/26/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import SwiftUI + +struct SwipeActionsPrefsView: UIViewControllerRepresentable { + @Binding var selection: [StatusSwipeAction] + + typealias UIViewControllerType = SwipeActionsPrefsViewController + + func makeUIViewController(context: Context) -> SwipeActionsPrefsViewController { + return SwipeActionsPrefsViewController(selection: $selection) + } + + func updateUIViewController(_ uiViewController: SwipeActionsPrefsViewController, context: Context) { + } +} + +class SwipeActionsPrefsViewController: UIViewController, UICollectionViewDelegate { + @Binding var selection: [StatusSwipeAction] + + private var collectionView: UICollectionView { + view as! UICollectionView + } + private var dataSource: UICollectionViewDiffableDataSource! + + init(selection: Binding<[StatusSwipeAction]>) { + self._selection = selection + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in + var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + if dataSource.sectionIdentifier(for: sectionIndex) == .selected { + config.headerMode = .supplementary + } + return .list(using: config, layoutEnvironment: environment) + } + view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + dataSource = createDataSource() + } + + private func createDataSource() -> UICollectionViewDiffableDataSource { + let listCell = UICollectionView.CellRegistration { cell, indexPath, item in + var config = cell.defaultContentConfiguration() + config.text = item.displayName + config.image = UIImage(systemName: item.systemImageName) + cell.contentConfiguration = config + cell.accessories = [.reorder(displayed: .always)] + } + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in + return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier) + } + let headerCell = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in + var config = supplementaryView.defaultContentConfiguration() + config.text = "Selected" + supplementaryView.contentConfiguration = config + } + dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in + return collectionView.dequeueConfiguredReusableSupplementary(using: headerCell, for: indexPath) + } + dataSource.reorderingHandlers.canReorderItem = { _ in + return true + } + dataSource.reorderingHandlers.didReorder = { [unowned self] transaction in + guard let selectedSection = transaction.sectionTransactions.first(where: { $0.sectionIdentifier == .selected }) else { + return + } + self.selection = self.selection.applying(selectedSection.difference)! + } + return dataSource + } + + override func viewDidLoad() { + super.viewDidLoad() + + setEditing(true, animated: false) + applySnapshot(animated: false) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + guard let item = dataSource.itemIdentifier(for: indexPath), + let section = dataSource.sectionIdentifier(for: indexPath.section) else { + return + } + switch section { + case .selected: + selection.removeAll(where: { $0 == item }) + case .remainder: + selection.append(item) + } + applySnapshot(animated: true) + } + + private func applySnapshot(animated: Bool) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.selected, .remainder]) + snapshot.appendItems(selection, toSection: .selected) + snapshot.appendItems(StatusSwipeAction.allCases.filter { !selection.contains($0) }, toSection: .remainder) + dataSource.apply(snapshot, animatingDifferences: animated) + } + + enum Section { + case selected + case remainder + } + + typealias Item = StatusSwipeAction +} diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index bb870e66..70e2f206 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -578,58 +578,17 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti } func leadingSwipeActions() -> UISwipeActionsConfiguration? { - guard mastodonController.loggedIn, - let status = mastodonController.persistentContainer.status(for: statusID) else { + guard let status = mastodonController.persistentContainer.status(for: statusID) else { return nil } - - let favoriteTitle = status.favourited ? "Unfavorite" : "Favorite" - let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { [unowned self] _, _, completion in - Task { - await FavoriteService(status: status, mastodonController: self.mastodonController, presenter: self.delegate!).toggleFavorite() - completion(true) - } - } - favorite.image = UIImage(systemName: "star.fill") - favorite.backgroundColor = status.favourited ? UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) : UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) - - let reblogTitle = status.reblogged ? "Unreblog" : "Reblog" - let reblog = UIContextualAction(style: .normal, title: reblogTitle) { _, _, completion in - Task { - await ReblogService(status: status, mastodonController: self.mastodonController, presenter: self.delegate!).toggleReblog() - completion(true) - } - } - reblog.image = UIImage(systemName: "repeat") - reblog.backgroundColor = status.reblogged ? UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) : tintColor - - return UISwipeActionsConfiguration(actions: [favorite, reblog]) + return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) } func trailingSwipeActions() -> UISwipeActionsConfiguration? { - var actions = [UIContextualAction]() - - let share = UIContextualAction(style: .normal, title: "Share") { [unowned self] _, _, completion in - self.delegate?.showMoreOptions(forStatus: statusID, sourceView: self) - completion(true) + guard let status = mastodonController.persistentContainer.status(for: statusID) else { + return nil } - // bold to more closesly match other action symbols - let config = UIImage.SymbolConfiguration(weight: .bold) - share.image = UIImage(systemName: "square.and.arrow.up")!.applyingSymbolConfiguration(config)! - share.backgroundColor = .lightGray - actions.append(share) - - if mastodonController.loggedIn { - let reply = UIContextualAction(style: .normal, title: "Reply") { [unowned self] _, _, completion in - self.replyPressed() - completion(true) - } - reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill") - reply.backgroundColor = tintColor - actions.insert(reply, at: 0) - } - - return UISwipeActionsConfiguration(actions: actions) + return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) } func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] { @@ -705,3 +664,12 @@ extension TimelineStatusCollectionViewCell: UIPointerInteractionDelegate { return nil } } + +extension TimelineStatusCollectionViewCell: StatusSwipeActionContainer { + var navigationDelegate: TuskerNavigationDelegate { delegate! } + var toastableViewController: ToastableViewController? { delegate } + + func performReplyAction() { + self.replyPressed() + } +} diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index f7cc2b98..666b8351 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -319,90 +319,17 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell { extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { - guard let mastodonController = mastodonController, - mastodonController.loggedIn else { return nil } - guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - let favoriteTitle: String - let favoriteRequest: Request - let favoriteColor: UIColor - if status.favourited { - favoriteTitle = "Unfavorite" - favoriteRequest = Status.unfavourite(status.id) - favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) - - } else { - favoriteTitle = "Favorite" - favoriteRequest = Status.favourite(status.id) - favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1) + guard let status = mastodonController.persistentContainer.status(for: statusID) else { + return nil } - let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in - mastodonController.run(favoriteRequest, completion: { response in - DispatchQueue.main.async { - guard case let .success(status, _) = response else { - completion(false) - return - } - completion(true) - mastodonController.persistentContainer.addOrUpdate(status: status) - } - }) - } - favorite.image = UIImage(systemName: "star.fill") - favorite.backgroundColor = favoriteColor - - let reblogTitle: String - let reblogRequest: Request - let reblogColor: UIColor - if status.reblogged { - reblogTitle = "Unreblog" - reblogRequest = Status.unreblog(status.id) - reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1) - } else { - reblogTitle = "Reblog" - reblogRequest = Status.reblog(status.id) - reblogColor = tintColor - } - let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in - mastodonController.run(reblogRequest, completion: { response in - DispatchQueue.main.async { - guard case let .success(status, _) = response else { - completion(false) - return - } - completion(true) - mastodonController.persistentContainer.addOrUpdate(status: status) - } - }) - } - reblog.image = UIImage(systemName: "repeat") - reblog.backgroundColor = reblogColor - - return UISwipeActionsConfiguration(actions: [favorite, reblog]) + return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) } func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { - let share = UIContextualAction(style: .normal, title: "Share") { (action, view, completion) in - completion(true) - self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self) + guard let status = mastodonController.persistentContainer.status(for: statusID) else { + return nil } - // Bold to more closely match the other action symbols - let config = UIImage.SymbolConfiguration(weight: .bold) - share.image = UIImage(systemName: "square.and.arrow.up")!.applyingSymbolConfiguration(config)! - share.backgroundColor = .lightGray - - guard mastodonController.loggedIn else { - return UISwipeActionsConfiguration(actions: [share]) - } - - let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in - completion(true) - self.reply() - } - reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill") - reply.backgroundColor = tintColor - - return UISwipeActionsConfiguration(actions: [reply, share]) + return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) }) } } @@ -461,3 +388,12 @@ extension TimelineStatusTableViewCell: MenuPreviewProvider { ) } } + +extension TimelineStatusTableViewCell: StatusSwipeActionContainer { + var navigationDelegate: TuskerNavigationDelegate { delegate! } + var toastableViewController: ToastableViewController? { delegate } + + func performReplyAction() { + self.replyPressed() + } +}