mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-28 02:51:02 +00:00
Add/remove from lists
This commit is contained in:
parent
8b664c8b79
commit
5b360c6bc1
10 changed files with 175 additions and 4 deletions
|
@ -6,6 +6,7 @@
|
||||||
"accessibility.copy-text" = "Copy text";
|
"accessibility.copy-text" = "Copy text";
|
||||||
"account.%@-followers" = "%@'s Followers";
|
"account.%@-followers" = "%@'s Followers";
|
||||||
"account.accept-follow-request-button.accessibility-label" = "Accept follow request";
|
"account.accept-follow-request-button.accessibility-label" = "Accept follow request";
|
||||||
|
"account.add-remove-lists" = "Add/remove from lists";
|
||||||
"account.avatar.accessibility-label-%@" = "Avatar: %@";
|
"account.avatar.accessibility-label-%@" = "Avatar: %@";
|
||||||
"account.block" = "Block";
|
"account.block" = "Block";
|
||||||
"account.block-and-report" = "Block & report";
|
"account.block-and-report" = "Block & report";
|
||||||
|
|
|
@ -6,6 +6,8 @@ import Mastodon
|
||||||
|
|
||||||
public enum EmptyEndpoint {
|
public enum EmptyEndpoint {
|
||||||
case oauthRevoke(token: String, clientId: String, clientSecret: String)
|
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 deleteList(id: List.Id)
|
||||||
case deleteFilter(id: Filter.Id)
|
case deleteFilter(id: Filter.Id)
|
||||||
case blockDomain(String)
|
case blockDomain(String)
|
||||||
|
@ -19,7 +21,7 @@ extension EmptyEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke:
|
case .oauthRevoke:
|
||||||
return ["oauth"]
|
return ["oauth"]
|
||||||
case .deleteList:
|
case .addAccountsToList, .removeAccountsFromList, .deleteList:
|
||||||
return defaultContext + ["lists"]
|
return defaultContext + ["lists"]
|
||||||
case .deleteFilter:
|
case .deleteFilter:
|
||||||
return defaultContext + ["filters"]
|
return defaultContext + ["filters"]
|
||||||
|
@ -32,6 +34,8 @@ extension EmptyEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke:
|
case .oauthRevoke:
|
||||||
return ["revoke"]
|
return ["revoke"]
|
||||||
|
case let .addAccountsToList(id, _), let .removeAccountsFromList(id, _):
|
||||||
|
return [id, "accounts"]
|
||||||
case let .deleteList(id), let .deleteFilter(id):
|
case let .deleteList(id), let .deleteFilter(id):
|
||||||
return [id]
|
return [id]
|
||||||
case .blockDomain, .unblockDomain:
|
case .blockDomain, .unblockDomain:
|
||||||
|
@ -41,9 +45,9 @@ extension EmptyEndpoint: Endpoint {
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke, .blockDomain:
|
case .addAccountsToList, .oauthRevoke, .blockDomain:
|
||||||
return .post
|
return .post
|
||||||
case .deleteList, .deleteFilter, .unblockDomain:
|
case .removeAccountsFromList, .deleteList, .deleteFilter, .unblockDomain:
|
||||||
return .delete
|
return .delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +56,8 @@ extension EmptyEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case let .oauthRevoke(token, clientId, clientSecret):
|
case let .oauthRevoke(token, clientId, clientSecret):
|
||||||
return ["token": token, "client_id": clientId, "client_secret": 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):
|
case let .blockDomain(domain), let .unblockDomain(domain):
|
||||||
return ["domain": domain]
|
return ["domain": domain]
|
||||||
case .deleteList, .deleteFilter:
|
case .deleteList, .deleteFilter:
|
||||||
|
|
|
@ -6,13 +6,28 @@ import Mastodon
|
||||||
|
|
||||||
public enum ListsEndpoint {
|
public enum ListsEndpoint {
|
||||||
case lists
|
case lists
|
||||||
|
case listsWithAccount(id: Account.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ListsEndpoint: Endpoint {
|
extension ListsEndpoint: Endpoint {
|
||||||
public typealias ResultType = [List]
|
public typealias ResultType = [List]
|
||||||
|
|
||||||
|
public var context: [String] {
|
||||||
|
switch self {
|
||||||
|
case .lists:
|
||||||
|
return defaultContext
|
||||||
|
case .listsWithAccount:
|
||||||
|
return defaultContext + ["accounts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var pathComponentsInContext: [String] {
|
public var pathComponentsInContext: [String] {
|
||||||
["lists"]
|
switch self {
|
||||||
|
case .lists:
|
||||||
|
return ["lists"]
|
||||||
|
case let .listsWithAccount(id):
|
||||||
|
return [id, "lists"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
D025B16A25C4EB18001C69A8 /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D025B16925C4EB18001C69A8 /* ServiceLayer */; };
|
D025B16A25C4EB18001C69A8 /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D025B16925C4EB18001C69A8 /* ServiceLayer */; };
|
||||||
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B17D25C500BC001C69A8 /* CapsuleButton.swift */; };
|
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025B17D25C500BC001C69A8 /* CapsuleButton.swift */; };
|
||||||
D02D338D25EDA593000A35CC /* CopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D338C25EDA593000A35CC /* CopyableLabel.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 */; };
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||||
D035D8F925E4338D00E597C9 /* ImageDiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035D8F825E4338D00E597C9 /* ImageDiskCache.swift */; };
|
D035D8F925E4338D00E597C9 /* ImageDiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035D8F825E4338D00E597C9 /* ImageDiskCache.swift */; };
|
||||||
D035D8FE25E4339800E597C9 /* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -523,6 +525,7 @@
|
||||||
children = (
|
children = (
|
||||||
D021A62B25C38570008A0C0D /* AboutView.swift */,
|
D021A62B25C38570008A0C0D /* AboutView.swift */,
|
||||||
D021A63525C38ADB008A0C0D /* AcknowledgmentsView.swift */,
|
D021A63525C38ADB008A0C0D /* AcknowledgmentsView.swift */,
|
||||||
|
D02D33EE25EE04CC000A35CC /* AddRemoveFromListsView.swift */,
|
||||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
|
@ -1163,6 +1166,7 @@
|
||||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
|
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
|
||||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||||
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */,
|
||||||
|
D02D33EF25EE04CC000A35CC /* AddRemoveFromListsView.swift in Sources */,
|
||||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||||
D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */,
|
D0CEC0E125E0BB9700FEF5A6 /* NewItemsView.swift in Sources */,
|
||||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||||
|
|
|
@ -32,6 +32,22 @@ public extension AccountService {
|
||||||
|
|
||||||
var domain: String? { URL(string: account.url)?.host }
|
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> {
|
func follow() -> AnyPublisher<Never, Error> {
|
||||||
relationshipAction(.accountsFollow(id: account.id))
|
relationshipAction(.accountsFollow(id: account.id))
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,11 @@ private extension ProfileViewController {
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
func menu(accountViewModel: AccountViewModel, relationship: Relationship) -> UIMenu {
|
func menu(accountViewModel: AccountViewModel, relationship: Relationship) -> UIMenu {
|
||||||
var actions = [UIAction(
|
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: ""),
|
title: NSLocalizedString("share", comment: ""),
|
||||||
image: UIImage(systemName: "square.and.arrow.up")) { _ in
|
image: UIImage(systemName: "square.and.arrow.up")) { _ in
|
||||||
accountViewModel.share()
|
accountViewModel.share()
|
||||||
|
|
|
@ -203,6 +203,13 @@ extension TableViewController {
|
||||||
present(navigationController, animated: true)
|
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() {
|
func sizeTableHeaderFooterViews() {
|
||||||
// https://useyourloaf.com/blog/variable-height-table-view-header/
|
// https://useyourloaf.com/blog/variable-height-table-view-header/
|
||||||
if let headerView = tableView.tableHeaderView {
|
if let headerView = tableView.tableHeaderView {
|
||||||
|
|
|
@ -103,6 +103,18 @@ public extension AccountViewModel {
|
||||||
MuteViewModel(accountService: accountService, identityContext: identityContext)
|
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() {
|
func follow() {
|
||||||
ignorableOutputEvent(accountService.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)
|
||||||
|
}
|
||||||
|
}
|
40
Views/SwiftUI/AddRemoveFromListsView.swift
Normal file
40
Views/SwiftUI/AddRemoveFromListsView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue