forked from shadowfacts/Tusker
parent
c256fb4cbd
commit
e04cdd16d6
|
@ -44,6 +44,8 @@
|
||||||
D61DC84628F498F200B82C6E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84528F498F200B82C6E /* Logging.swift */; };
|
D61DC84628F498F200B82C6E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84528F498F200B82C6E /* Logging.swift */; };
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */; };
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */; };
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84C28F500D200B82C6E /* ProfileViewController.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 */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.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 = "<group>"; };
|
D61DC84528F498F200B82C6E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
||||||
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSwipeAction.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1031,6 +1035,7 @@
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
||||||
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||||
|
@ -1133,6 +1138,7 @@
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */,
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */,
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */,
|
||||||
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
|
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */,
|
||||||
|
D61F75872932DB6000C0B37F /* StatusSwipeAction.swift */,
|
||||||
);
|
);
|
||||||
path = Preferences;
|
path = Preferences;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1849,6 +1855,7 @@
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
|
D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */,
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
|
@ -1952,6 +1959,7 @@
|
||||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
|
D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */,
|
||||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */,
|
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||||
|
|
|
@ -91,11 +91,20 @@ struct InstanceFeatures {
|
||||||
} else if nodeInfo?.software.name == "hometown" {
|
} else if nodeInfo?.software.name == "hometown" {
|
||||||
var mastoVersion: Version?
|
var mastoVersion: Version?
|
||||||
var hometownVersion: Version?
|
var hometownVersion: Version?
|
||||||
// like "1.0.6+3.5.2"
|
|
||||||
let parts = ver.split(separator: "+")
|
let parts = ver.split(separator: "+")
|
||||||
if parts.count == 2 {
|
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]))
|
mastoVersion = Version(string: String(parts[1]))
|
||||||
hometownVersion = Version(string: String(parts[0]))
|
}
|
||||||
} else {
|
} else {
|
||||||
mastoVersion = Version(string: ver)
|
mastoVersion = Version(string: ver)
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,8 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var showIsStatusReplyIcon = false
|
@Published var showIsStatusReplyIcon = false
|
||||||
@Published var alwaysShowStatusVisibilityIcon = false
|
@Published var alwaysShowStatusVisibilityIcon = false
|
||||||
@Published var hideActionsInTimeline = false
|
@Published var hideActionsInTimeline = false
|
||||||
|
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||||
|
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||||
|
|
||||||
// MARK: Composing
|
// MARK: Composing
|
||||||
@Published var defaultPostVisibility = Status.Visibility.public
|
@Published var defaultPostVisibility = Status.Visibility.public
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -59,6 +59,16 @@ struct AppearancePrefsView : View {
|
||||||
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
||||||
Text("Hide Actions on Timeline")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Section, Item>!
|
||||||
|
|
||||||
|
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<Section, Item> {
|
||||||
|
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, StatusSwipeAction> { 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<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: itemIdentifier)
|
||||||
|
}
|
||||||
|
let headerCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(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<Section, Item>()
|
||||||
|
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
|
||||||
|
}
|
|
@ -578,58 +578,17 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
}
|
}
|
||||||
|
|
||||||
func leadingSwipeActions() -> UISwipeActionsConfiguration? {
|
func leadingSwipeActions() -> UISwipeActionsConfiguration? {
|
||||||
guard mastodonController.loggedIn,
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trailingSwipeActions() -> UISwipeActionsConfiguration? {
|
func trailingSwipeActions() -> UISwipeActionsConfiguration? {
|
||||||
var actions = [UIContextualAction]()
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
|
return nil
|
||||||
let share = UIContextualAction(style: .normal, title: "Share") { [unowned self] _, _, completion in
|
|
||||||
self.delegate?.showMoreOptions(forStatus: statusID, sourceView: self)
|
|
||||||
completion(true)
|
|
||||||
}
|
}
|
||||||
// bold to more closesly match other action symbols
|
return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||||
|
@ -705,3 +664,12 @@ extension TimelineStatusCollectionViewCell: UIPointerInteractionDelegate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineStatusCollectionViewCell: StatusSwipeActionContainer {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate { delegate! }
|
||||||
|
var toastableViewController: ToastableViewController? { delegate }
|
||||||
|
|
||||||
|
func performReplyAction() {
|
||||||
|
self.replyPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -319,90 +319,17 @@ extension TimelineStatusTableViewCell: SelectableTableViewCell {
|
||||||
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
|
|
||||||
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||||
guard let mastodonController = mastodonController,
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
mastodonController.loggedIn else { return nil }
|
return nil
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
|
||||||
|
|
||||||
let favoriteTitle: String
|
|
||||||
let favoriteRequest: Request<Status>
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
|
return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
|
||||||
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<Status>
|
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
|
||||||
let share = UIContextualAction(style: .normal, title: "Share") { (action, view, completion) in
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
completion(true)
|
return nil
|
||||||
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
|
|
||||||
}
|
}
|
||||||
// Bold to more closely match the other action symbols
|
return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -461,3 +388,12 @@ extension TimelineStatusTableViewCell: MenuPreviewProvider {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineStatusTableViewCell: StatusSwipeActionContainer {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate { delegate! }
|
||||||
|
var toastableViewController: ToastableViewController? { delegate }
|
||||||
|
|
||||||
|
func performReplyAction() {
|
||||||
|
self.replyPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue