Add/remove from lists

This commit is contained in:
Justin Mazzocchi 2021-03-02 16:50:22 -08:00
parent 8b664c8b79
commit 5b360c6bc1
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
10 changed files with 175 additions and 4 deletions

View file

@ -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";

View file

@ -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:

View file

@ -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 {

View file

@ -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 */,

View file

@ -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))
}

View file

@ -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()

View file

@ -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 {

View file

@ -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())
}

View file

@ -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)
}
}

View file

@ -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)
}
}