Add UI for changing list reply policy and exclusivity

Closes #428
This commit is contained in:
Shadowfacts 2023-10-28 12:16:14 -05:00
parent 2464e2530f
commit bd21e88e8b
7 changed files with 144 additions and 11 deletions

View File

@ -175,6 +175,14 @@ public class InstanceFeatures: ObservableObject {
hasMastodonVersion(2, 8, 0) hasMastodonVersion(2, 8, 0)
} }
public var listRepliesPolicy: Bool {
hasMastodonVersion(3, 3, 0)
}
public var exclusiveLists: Bool {
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
}
public init() { public init() {
} }

View File

@ -264,6 +264,7 @@
D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; }; D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; };
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; }; D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; };
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; }; D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; };
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C041C32AED77730094D32D /* EditListSettingsService.swift */; };
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; }; D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; }; D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
@ -665,6 +666,7 @@
D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerComponents; path = Packages/TuskerComponents; sourceTree = "<group>"; }; D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerComponents; path = Packages/TuskerComponents; sourceTree = "<group>"; };
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; }; D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; };
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; }; D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
D6C041C32AED77730094D32D /* EditListSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSettingsService.swift; sourceTree = "<group>"; };
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; }; D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; }; D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
@ -1636,6 +1638,7 @@
D621733228F1D5ED004C7DB1 /* ReblogService.swift */, D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
D6F6A54F291F058600F496A8 /* CreateListService.swift */, D6F6A54F291F058600F496A8 /* CreateListService.swift */,
D6F6A551291F098700F496A8 /* RenameListService.swift */, D6F6A551291F098700F496A8 /* RenameListService.swift */,
D6C041C32AED77730094D32D /* EditListSettingsService.swift */,
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */, D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */, D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */, D61F75B0293BD85300C0B37F /* CreateFilterService.swift */,
@ -2140,6 +2143,7 @@
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */, D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */, D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */,
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */, D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */, D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,

View File

@ -0,0 +1,46 @@
//
// EditListSettingsService.swift
// Tusker
//
// Created by Shadowfacts on 10/28/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
@MainActor
class EditListSettingsService {
private let list: ListProtocol
private let mastodonController: MastodonController
private let present: (UIViewController) -> Void
init(list: ListProtocol, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
self.list = list
self.mastodonController = mastodonController
self.present = present
}
func run(title: String? = nil, replyPolicy: List.ReplyPolicy? = nil, exclusive: Bool? = nil) async {
do {
let req = List.update(
list.id,
title: title ?? list.title,
replyPolicy: replyPolicy ?? list.replyPolicy,
exclusive: exclusive ?? list.exclusive
)
let (list, _) = try await mastodonController.run(req)
mastodonController.updatedList(list)
} catch {
let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Retry", style: .default, handler: { _ in
Task {
await self.run(title: title, replyPolicy: replyPolicy, exclusive: exclusive)
}
}))
present(alert)
}
}
}

View File

@ -471,7 +471,7 @@ class MastodonController: ObservableObject {
} }
@MainActor @MainActor
func renamedList(_ list: List) { func updatedList(_ list: List) {
var new = self.lists var new = self.lists
if let index = new.firstIndex(where: { $0.id == list.id }) { if let index = new.firstIndex(where: { $0.id == list.id }) {
new[index] = list new[index] = list

View File

@ -49,7 +49,7 @@ class RenameListService {
do { do {
let req = List.update(list.id, title: title, replyPolicy: nil, exclusive: nil) let req = List.update(list.id, title: title, replyPolicy: nil, exclusive: nil)
let (list, _) = try await mastodonController.run(req) let (list, _) = try await mastodonController.run(req)
mastodonController.renamedList(list) mastodonController.updatedList(list)
} catch { } catch {
let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert) let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

View File

@ -38,7 +38,6 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
listRenamedCancellable = mastodonController.$lists listRenamedCancellable = mastodonController.$lists
.compactMap { $0.first { $0.id == list.id } } .compactMap { $0.first { $0.id == list.id } }
.removeDuplicates(by: { $0.title == $1.title })
.sink { [unowned self] in .sink { [unowned self] in
self.list = $0 self.list = $0
self.listChanged() self.listChanged()
@ -103,9 +102,27 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
navigationItem.hidesSearchBarWhenScrolling = false navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked navigationItem.preferredSearchBarPlacement = .stacked
navigationItem.renameDelegate = self
navigationItem.titleMenuProvider = { [unowned self] suggested in
var children = suggested
children.append(contentsOf: self.listSettingsMenuElements())
return UIMenu(children: children)
}
} else {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", menu: UIMenu(children: [
// uncached so that menu always reflects the current state of the list
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
var elements = self.listSettingsMenuElements()
elements.insert(UIAction(title: "Rename…", image: UIImage(systemName: "pencil"), handler: { [unowned self] _ in
RenameListService(list: self.list, mastodonController: self.mastodonController, present: {
self.present($0, animated: true)
}).run()
}), at: 0)
elementHandler(elements)
})
]))
} }
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed))
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -140,7 +157,31 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
} }
private func listChanged() { private func listChanged() {
title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title) title = list.title
}
private func listSettingsMenuElements() -> [UIMenuElement] {
var elements = [UIMenuElement]()
if mastodonController.instanceFeatures.listRepliesPolicy {
let actions = List.ReplyPolicy.allCases.map { policy in
UIAction(title: policy.actionTitle, state: list.replyPolicy == policy ? .on : .off) { [unowned self] _ in
self.setReplyPolicy(policy)
}
}
elements.append(UIMenu(title: "Show replies…", image: UIImage(systemName: "arrowshape.turn.up.left"), children: actions))
}
if mastodonController.instanceFeatures.exclusiveLists {
let actions = [
UIAction(title: "Hidden from Home", state: list.exclusive == true ? .on : .off) { [unowned self] _ in
self.setExclusive(true)
},
UIAction(title: "Shown on Home", state: list.exclusive == false ? .on : .off) { [unowned self] _ in
self.setExclusive(false)
},
]
elements.append(UIMenu(title: "Posts from this list are…", children: actions))
}
return elements
} }
@MainActor @MainActor
@ -254,13 +295,21 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
} }
} }
// MARK: - Interaction
@objc func renameButtonPressed() { private func setReplyPolicy(_ replyPolicy: List.ReplyPolicy) {
RenameListService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) }).run() Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(replyPolicy: replyPolicy)
}
} }
private func setExclusive(_ exclusive: Bool) {
Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(exclusive: exclusive)
}
}
} }
extension EditListAccountsViewController { extension EditListAccountsViewController {
@ -310,3 +359,29 @@ extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
} }
} }
} }
extension EditListAccountsViewController: UINavigationItemRenameDelegate {
func navigationItem(_: UINavigationItem, shouldEndRenamingWith title: String) -> Bool {
!title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func navigationItem(_: UINavigationItem, didEndRenamingWith title: String) {
Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(title: title)
}
}
}
private extension List.ReplyPolicy {
var actionTitle: String {
switch self {
case .followed:
"To accounts you follow"
case .list:
"To other list members"
case .none:
"Never"
}
}
}

View File

@ -59,7 +59,7 @@ class ListTimelineViewController: TimelineViewController {
func presentEdit(animated: Bool) { func presentEdit(animated: Bool) {
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController) let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed)) editListAccountsController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
let navController = UINavigationController(rootViewController: editListAccountsController) let navController = UINavigationController(rootViewController: editListAccountsController)
present(navController, animated: animated) present(navController, animated: animated)
} }