Add/remove from lists
This commit is contained in:
parent
8b664c8b79
commit
5b360c6bc1
|
@ -6,6 +6,7 @@
|
|||
"accessibility.copy-text" = "Copy text";
|
||||
"account.%@-followers" = "%@'s Followers";
|
||||
"account.accept-follow-request-button.accessibility-label" = "Accept follow request";
|
||||
"account.add-remove-lists" = "Add/remove from lists";
|
||||
"account.avatar.accessibility-label-%@" = "Avatar: %@";
|
||||
"account.block" = "Block";
|
||||
"account.block-and-report" = "Block & report";
|
||||
|
|
|
@ -6,6 +6,8 @@ import Mastodon
|
|||
|
||||
public enum EmptyEndpoint {
|
||||
case oauthRevoke(token: String, clientId: String, clientSecret: String)
|
||||
case addAccountsToList(id: List.Id, accountIds: Set<Account.Id>)
|
||||
case removeAccountsFromList(id: List.Id, accountIds: Set<Account.Id>)
|
||||
case deleteList(id: List.Id)
|
||||
case deleteFilter(id: Filter.Id)
|
||||
case blockDomain(String)
|
||||
|
@ -19,7 +21,7 @@ extension EmptyEndpoint: Endpoint {
|
|||
switch self {
|
||||
case .oauthRevoke:
|
||||
return ["oauth"]
|
||||
case .deleteList:
|
||||
case .addAccountsToList, .removeAccountsFromList, .deleteList:
|
||||
return defaultContext + ["lists"]
|
||||
case .deleteFilter:
|
||||
return defaultContext + ["filters"]
|
||||
|
@ -32,6 +34,8 @@ extension EmptyEndpoint: Endpoint {
|
|||
switch self {
|
||||
case .oauthRevoke:
|
||||
return ["revoke"]
|
||||
case let .addAccountsToList(id, _), let .removeAccountsFromList(id, _):
|
||||
return [id, "accounts"]
|
||||
case let .deleteList(id), let .deleteFilter(id):
|
||||
return [id]
|
||||
case .blockDomain, .unblockDomain:
|
||||
|
@ -41,9 +45,9 @@ extension EmptyEndpoint: Endpoint {
|
|||
|
||||
public var method: HTTPMethod {
|
||||
switch self {
|
||||
case .oauthRevoke, .blockDomain:
|
||||
case .addAccountsToList, .oauthRevoke, .blockDomain:
|
||||
return .post
|
||||
case .deleteList, .deleteFilter, .unblockDomain:
|
||||
case .removeAccountsFromList, .deleteList, .deleteFilter, .unblockDomain:
|
||||
return .delete
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +56,8 @@ extension EmptyEndpoint: Endpoint {
|
|||
switch self {
|
||||
case let .oauthRevoke(token, clientId, clientSecret):
|
||||
return ["token": token, "client_id": clientId, "client_secret": clientSecret]
|
||||
case let .addAccountsToList(_, accountIds), let .removeAccountsFromList(_, accountIds):
|
||||
return ["account_ids": Array(accountIds)]
|
||||
case let .blockDomain(domain), let .unblockDomain(domain):
|
||||
return ["domain": domain]
|
||||
case .deleteList, .deleteFilter:
|
||||
|
|
|
@ -6,13 +6,28 @@ import Mastodon
|
|||
|
||||
public enum ListsEndpoint {
|
||||
case lists
|
||||
case listsWithAccount(id: Account.Id)
|
||||
}
|
||||
|
||||
extension ListsEndpoint: Endpoint {
|
||||
public typealias ResultType = [List]
|
||||
|
||||
public var context: [String] {
|
||||
switch self {
|
||||
case .lists:
|
||||
return defaultContext
|
||||
case .listsWithAccount:
|
||||
return defaultContext + ["accounts"]
|
||||
}
|
||||
}
|
||||
|
||||
public var pathComponentsInContext: [String] {
|
||||
["lists"]
|
||||
switch self {
|
||||
case .lists:
|
||||
return ["lists"]
|
||||
case let .listsWithAccount(id):
|
||||
return [id, "lists"]
|
||||
}
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
D025B16A25C4EB18001C69A8 /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D025B16925C4EB18001C69A8 /* ServiceLayer */; };
|
||||
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B17D25C500BC001C69A8 /* CapsuleButton.swift */; };
|
||||
D02D338D25EDA593000A35CC /* CopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D338C25EDA593000A35CC /* CopyableLabel.swift */; };
|
||||
D02D33EF25EE04CC000A35CC /* AddRemoveFromListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D33EE25EE04CC000A35CC /* AddRemoveFromListsView.swift */; };
|
||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||
D035D8F925E4338D00E597C9 /* ImageDiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035D8F825E4338D00E597C9 /* ImageDiskCache.swift */; };
|
||||
D035D8FE25E4339800E597C9 /* ImageDiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035D8F825E4338D00E597C9 /* ImageDiskCache.swift */; };
|
||||
|
@ -279,6 +280,7 @@
|
|||
D025B14C25C4E482001C69A8 /* ImageCacheConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheConfiguration.swift; sourceTree = "<group>"; };
|
||||
D025B17D25C500BC001C69A8 /* CapsuleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleButton.swift; sourceTree = "<group>"; };
|
||||
D02D338C25EDA593000A35CC /* CopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLabel.swift; sourceTree = "<group>"; };
|
||||
D02D33EE25EE04CC000A35CC /* AddRemoveFromListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoveFromListsView.swift; sourceTree = "<group>"; };
|
||||
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||
D035D8F825E4338D00E597C9 /* ImageDiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDiskCache.swift; sourceTree = "<group>"; };
|
||||
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -523,6 +525,7 @@
|
|||
children = (
|
||||
D021A62B25C38570008A0C0D /* AboutView.swift */,
|
||||
D021A63525C38ADB008A0C0D /* AcknowledgmentsView.swift */,
|
||||
D02D33EE25EE04CC000A35CC /* AddRemoveFromListsView.swift */,
|
||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
|
@ -1163,6 +1166,7 @@
|
|||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
|
||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
||||
D02D33EF25EE04CC000A35CC /* AddRemoveFromListsView.swift in Sources */,
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||
D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */,
|
||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||
|
|
|
@ -32,6 +32,22 @@ public extension AccountService {
|
|||
|
||||
var domain: String? { URL(string: account.url)?.host }
|
||||
|
||||
func lists() -> AnyPublisher<[List], Error> {
|
||||
mastodonAPIClient.request(ListsEndpoint.listsWithAccount(id: account.id))
|
||||
}
|
||||
|
||||
func addToList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.request(EmptyEndpoint.addAccountsToList(id: id, accountIds: [account.id]))
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func removeFromList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.request(EmptyEndpoint.removeAccountsFromList(id: id, accountIds: [account.id]))
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func follow() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.accountsFollow(id: account.id))
|
||||
}
|
||||
|
|
|
@ -74,6 +74,11 @@ private extension ProfileViewController {
|
|||
// swiftlint:disable:next function_body_length
|
||||
func menu(accountViewModel: AccountViewModel, relationship: Relationship) -> UIMenu {
|
||||
var actions = [UIAction(
|
||||
title: NSLocalizedString("account.add-remove-lists", comment: ""),
|
||||
image: UIImage(systemName: "scroll")) { [weak self] _ in
|
||||
self?.addRemoveFromLists(accountViewModel: accountViewModel)
|
||||
},
|
||||
UIAction(
|
||||
title: NSLocalizedString("share", comment: ""),
|
||||
image: UIImage(systemName: "square.and.arrow.up")) { _ in
|
||||
accountViewModel.share()
|
||||
|
|
|
@ -203,6 +203,13 @@ extension TableViewController {
|
|||
present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func addRemoveFromLists(accountViewModel: AccountViewModel) {
|
||||
let addRemoveFromListsView = AddRemoveFromListsView(viewModel: .init(accountViewModel: accountViewModel))
|
||||
let addRemoveFromListsController = UIHostingController(rootView: addRemoveFromListsView)
|
||||
|
||||
show(addRemoveFromListsController, sender: self)
|
||||
}
|
||||
|
||||
func sizeTableHeaderFooterViews() {
|
||||
// https://useyourloaf.com/blog/variable-height-table-view-header/
|
||||
if let headerView = tableView.tableHeaderView {
|
||||
|
|
|
@ -103,6 +103,18 @@ public extension AccountViewModel {
|
|||
MuteViewModel(accountService: accountService, identityContext: identityContext)
|
||||
}
|
||||
|
||||
func lists() -> AnyPublisher<[List], Error> {
|
||||
accountService.lists()
|
||||
}
|
||||
|
||||
func addToList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||
accountService.addToList(id: id)
|
||||
}
|
||||
|
||||
func removeFromList(id: List.Id) -> AnyPublisher<Never, Error> {
|
||||
accountService.removeFromList(id: id)
|
||||
}
|
||||
|
||||
func follow() {
|
||||
ignorableOutputEvent(accountService.follow())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Mastodon
|
||||
|
||||
final public class AddRemoveFromListsViewModel: ObservableObject {
|
||||
public let accountViewModel: AccountViewModel
|
||||
@Published public private(set) var lists = [List]()
|
||||
@Published public private(set) var listIdsWithAccount = Set<List.Id>()
|
||||
@Published public private(set) var loaded = false
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let listsViewModel: ListsViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(accountViewModel: AccountViewModel) {
|
||||
self.accountViewModel = accountViewModel
|
||||
listsViewModel = ListsViewModel(identityContext: accountViewModel.identityContext)
|
||||
|
||||
listsViewModel.$lists.assign(to: &$lists)
|
||||
listsViewModel.$alertItem.assign(to: &$alertItem)
|
||||
}
|
||||
}
|
||||
|
||||
public extension AddRemoveFromListsViewModel {
|
||||
func refreshLists() {
|
||||
listsViewModel.refreshLists()
|
||||
}
|
||||
|
||||
func fetchListsWithAccount() {
|
||||
accountViewModel.lists()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { [weak self] in
|
||||
self?.listIdsWithAccount = Set($0.map(\.id))
|
||||
self?.loaded = true
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func addToList(id: List.Id) {
|
||||
accountViewModel.addToList(id: id)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { [weak self] in
|
||||
if case .finished = $0 {
|
||||
self?.listIdsWithAccount.insert(id)
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func removeFromList(id: List.Id) {
|
||||
accountViewModel.removeFromList(id: id)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { [weak self] in
|
||||
if case .finished = $0 {
|
||||
self?.listIdsWithAccount.remove(id)
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct AddRemoveFromListsView: View {
|
||||
@StateObject var viewModel: AddRemoveFromListsViewModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if viewModel.loaded {
|
||||
List(viewModel.lists) { list in
|
||||
Button {
|
||||
if viewModel.listIdsWithAccount.contains(list.id) {
|
||||
viewModel.removeFromList(id: list.id)
|
||||
} else {
|
||||
viewModel.addToList(id: list.id)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(list.title)
|
||||
if viewModel.listIdsWithAccount.contains(list.id) {
|
||||
Spacer()
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibility(addTraits: viewModel.listIdsWithAccount.contains(list.id) ? [.isSelected] : [])
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onAppear(perform: viewModel.refreshLists)
|
||||
.onAppear(perform: viewModel.fetchListsWithAccount)
|
||||
.navigationTitle(Text("secondary-navigation.lists"))
|
||||
.alertItem($viewModel.alertItem)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue