mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-28 11:01:02 +00:00
Domain blocks
This commit is contained in:
parent
30cedb503f
commit
43e58bce35
15 changed files with 305 additions and 8 deletions
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
"account.%@-followers" = "%@'s Followers";
|
"account.%@-followers" = "%@'s Followers";
|
||||||
"account.block" = "Block";
|
"account.block" = "Block";
|
||||||
|
"account.block.confirm-%@" = "Block %@?";
|
||||||
|
"account.domain-block-%@" = "Block domain %@";
|
||||||
|
"account.domain-block.confirm-%@" = "Block domain %@?";
|
||||||
|
"account.domain-unblock-%@" = "Unblock domain %@";
|
||||||
|
"account.domain-unblock.confirm-%@" = "Unblock domain %@?";
|
||||||
"account.field.verified" = "Verified %@";
|
"account.field.verified" = "Verified %@";
|
||||||
"account.follow" = "Follow";
|
"account.follow" = "Follow";
|
||||||
"account.following" = "Following";
|
"account.following" = "Following";
|
||||||
|
@ -15,6 +20,7 @@
|
||||||
"account.media" = "Media";
|
"account.media" = "Media";
|
||||||
"account.show-reblogs" = "Show boosts";
|
"account.show-reblogs" = "Show boosts";
|
||||||
"account.unblock" = "Unblock";
|
"account.unblock" = "Unblock";
|
||||||
|
"account.unblock.confirm-%@" = "Unblock %@?";
|
||||||
"account.unfollow" = "Unfollow";
|
"account.unfollow" = "Unfollow";
|
||||||
"account.unmute" = "Unmute";
|
"account.unmute" = "Unmute";
|
||||||
"add" = "Add";
|
"add" = "Add";
|
||||||
|
@ -53,6 +59,7 @@
|
||||||
"pending.pending-confirmation" = "Your account is pending confirmation";
|
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||||
"preferences" = "Preferences";
|
"preferences" = "Preferences";
|
||||||
"preferences.app" = "App Preferences";
|
"preferences.app" = "App Preferences";
|
||||||
|
"preferences.blocked-domains" = "Blocked Domains";
|
||||||
"preferences.blocked-users" = "Blocked Users";
|
"preferences.blocked-users" = "Blocked Users";
|
||||||
"preferences.media" = "Media";
|
"preferences.media" = "Media";
|
||||||
"preferences.media.use-system-reduce-motion" = "Use system reduce motion setting";
|
"preferences.media.use-system-reduce-motion" = "Use system reduce motion setting";
|
||||||
|
|
|
@ -8,6 +8,8 @@ public enum EmptyEndpoint {
|
||||||
case oauthRevoke(token: String, clientId: String, clientSecret: String)
|
case oauthRevoke(token: String, clientId: String, clientSecret: String)
|
||||||
case deleteList(id: List.Id)
|
case deleteList(id: List.Id)
|
||||||
case deleteFilter(id: Filter.Id)
|
case deleteFilter(id: Filter.Id)
|
||||||
|
case blockDomain(String)
|
||||||
|
case unblockDomain(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EmptyEndpoint: Endpoint {
|
extension EmptyEndpoint: Endpoint {
|
||||||
|
@ -21,6 +23,8 @@ extension EmptyEndpoint: Endpoint {
|
||||||
return defaultContext + ["lists"]
|
return defaultContext + ["lists"]
|
||||||
case .deleteFilter:
|
case .deleteFilter:
|
||||||
return defaultContext + ["filters"]
|
return defaultContext + ["filters"]
|
||||||
|
case .blockDomain, .unblockDomain:
|
||||||
|
return defaultContext + ["domain_blocks"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,14 +34,16 @@ extension EmptyEndpoint: Endpoint {
|
||||||
return ["revoke"]
|
return ["revoke"]
|
||||||
case let .deleteList(id), let .deleteFilter(id):
|
case let .deleteList(id), let .deleteFilter(id):
|
||||||
return [id]
|
return [id]
|
||||||
|
case .blockDomain, .unblockDomain:
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
switch self {
|
switch self {
|
||||||
case .oauthRevoke:
|
case .oauthRevoke, .blockDomain:
|
||||||
return .post
|
return .post
|
||||||
case .deleteList, .deleteFilter:
|
case .deleteList, .deleteFilter, .unblockDomain:
|
||||||
return .delete
|
return .delete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +52,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 .blockDomain(domain), let .unblockDomain(domain):
|
||||||
|
return ["domain": domain]
|
||||||
case .deleteList, .deleteFilter:
|
case .deleteList, .deleteFilter:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum StringsEndpoint {
|
||||||
|
case domainBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StringsEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [String]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case .domainBlocks:
|
||||||
|
return ["domain_blocks"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .domainBlocks:
|
||||||
|
return .get
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonAPI
|
||||||
|
import Stubbing
|
||||||
|
|
||||||
|
extension StringsEndpoint: Stubbing {
|
||||||
|
public func data(url: URL) -> Data? {
|
||||||
|
try? JSONSerialization.data(withJSONObject: ["ok.lol"])
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@
|
||||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D812544D80000B1EBEF /* PollOptionButton.swift */; };
|
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D812544D80000B1EBEF /* PollOptionButton.swift */; };
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */; };
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */; };
|
||||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */; };
|
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */; };
|
||||||
|
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */; };
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
|
@ -159,6 +160,7 @@
|
||||||
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionButton.swift; sourceTree = "<group>"; };
|
D08B8D812544D80000B1EBEF /* PollOptionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionButton.swift; sourceTree = "<group>"; };
|
||||||
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultView.swift; sourceTree = "<group>"; };
|
D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultView.swift; sourceTree = "<group>"; };
|
||||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = "<group>"; };
|
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainBlocksView.swift; sourceTree = "<group>"; };
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
|
@ -361,6 +363,7 @@
|
||||||
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
||||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
|
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
|
@ -684,6 +687,7 @@
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
||||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
||||||
|
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */,
|
||||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
||||||
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
||||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||||
|
|
|
@ -34,6 +34,8 @@ public extension AccountService {
|
||||||
account.url.host == mastodonAPIClient.instanceURL.host
|
account.url.host == mastodonAPIClient.instanceURL.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var domain: String? { account.url.host }
|
||||||
|
|
||||||
func follow() -> AnyPublisher<Never, Error> {
|
func follow() -> AnyPublisher<Never, Error> {
|
||||||
relationshipAction(.accountsFollow(id: account.id))
|
relationshipAction(.accountsFollow(id: account.id))
|
||||||
}
|
}
|
||||||
|
@ -91,6 +93,18 @@ public extension AccountService {
|
||||||
mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher()
|
mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainBlock() -> AnyPublisher<Never, Error> {
|
||||||
|
guard let domain = domain else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
return domainAction(EmptyEndpoint.blockDomain(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func domainUnblock() -> AnyPublisher<Never, Error> {
|
||||||
|
guard let domain = domain else { return Fail(error: URLError(.badURL)).eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
return domainAction(EmptyEndpoint.unblockDomain(domain))
|
||||||
|
}
|
||||||
|
|
||||||
func followingService() -> AccountListService {
|
func followingService() -> AccountListService {
|
||||||
AccountListService(
|
AccountListService(
|
||||||
endpoint: .accountsFollowing(id: account.id),
|
endpoint: .accountsFollowing(id: account.id),
|
||||||
|
@ -114,4 +128,12 @@ private extension AccountService {
|
||||||
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainAction(_ endpoint: EmptyEndpoint) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(endpoint)
|
||||||
|
.flatMap { _ in mastodonAPIClient.request(RelationshipsEndpoint.relationships(ids: [account.id])) }
|
||||||
|
.flatMap { contentDatabase.insert(relationships: $0) }
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct DomainBlocksService {
|
||||||
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
public init(mastodonAPIClient: MastodonAPIClient) {
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension DomainBlocksService {
|
||||||
|
func request(maxId: String?) -> AnyPublisher<[String], Error> {
|
||||||
|
mastodonAPIClient.pagedRequest(StringsEndpoint.domainBlocks, maxId: maxId)
|
||||||
|
.handleEvents(receiveOutput: {
|
||||||
|
if let maxId = $0.info.maxId {
|
||||||
|
nextPageMaxIdSubject.send(maxId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(\.result)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(domain: String) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(EmptyEndpoint.unblockDomain(domain)).ignoreOutput().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -224,6 +224,10 @@ public extension IdentityService {
|
||||||
func conversationsService() -> ConversationsService {
|
func conversationsService() -> ConversationsService {
|
||||||
ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainBlocksService() -> DomainBlocksService {
|
||||||
|
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityService {
|
private extension IdentityService {
|
||||||
|
|
|
@ -114,18 +114,71 @@ private extension ProfileViewController {
|
||||||
if relationship.blocking {
|
if relationship.blocking {
|
||||||
actions.append(UIAction(
|
actions.append(UIAction(
|
||||||
title: NSLocalizedString("account.unblock", comment: ""),
|
title: NSLocalizedString("account.unblock", comment: ""),
|
||||||
image: UIImage(systemName: "slash.circle")) { _ in
|
image: UIImage(systemName: "slash.circle"),
|
||||||
accountViewModel.unblock()
|
attributes: .destructive) { [weak self] _ in
|
||||||
})
|
self?.confirm(message: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.unblock.confirm-%@", comment: ""),
|
||||||
|
accountViewModel.accountName)) {
|
||||||
|
accountViewModel.unblock()
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
actions.append(UIAction(
|
actions.append(UIAction(
|
||||||
title: NSLocalizedString("account.block", comment: ""),
|
title: NSLocalizedString("account.block", comment: ""),
|
||||||
image: UIImage(systemName: "slash.circle"),
|
image: UIImage(systemName: "slash.circle"),
|
||||||
attributes: .destructive) { _ in
|
attributes: .destructive) { [weak self] _ in
|
||||||
accountViewModel.block()
|
self?.confirm(message: String.localizedStringWithFormat(
|
||||||
})
|
NSLocalizedString("account.block.confirm-%@", comment: ""),
|
||||||
|
accountViewModel.accountName)) {
|
||||||
|
accountViewModel.block()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !accountViewModel.isLocal, let domain = accountViewModel.domain {
|
||||||
|
if relationship.domainBlocking {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.domain-unblock-%@", comment: ""),
|
||||||
|
domain),
|
||||||
|
image: UIImage(systemName: "slash.circle"),
|
||||||
|
attributes: .destructive) { [weak self] _ in
|
||||||
|
self?.confirm(message: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.domain-unblock.confirm-%@", comment: ""),
|
||||||
|
domain)) {
|
||||||
|
accountViewModel.domainUnblock()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.domain-block-%@", comment: ""),
|
||||||
|
domain),
|
||||||
|
image: UIImage(systemName: "slash.circle"),
|
||||||
|
attributes: .destructive) { [weak self] _ in
|
||||||
|
self?.confirm(message: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.domain-block.confirm-%@", comment: ""),
|
||||||
|
domain)) {
|
||||||
|
accountViewModel.domainBlock()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UIMenu(children: actions)
|
return UIMenu(children: actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirm(message: String, action: @escaping () -> Void) {
|
||||||
|
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
|
||||||
|
|
||||||
|
let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel, handler: nil)
|
||||||
|
let okAction = UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .destructive) { _ in
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
|
||||||
|
present(alertController, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,4 +86,8 @@ public extension ReportViewModel {
|
||||||
identification: .preview)
|
identification: .preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension DomainBlocksViewModel {
|
||||||
|
static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
|
||||||
|
}
|
||||||
|
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
|
@ -28,6 +28,10 @@ public extension AccountViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isLocal: Bool { accountService.isLocal }
|
||||||
|
|
||||||
|
var domain: String? { accountService.domain }
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
accountService.account.displayName.isEmpty ? accountService.account.acct : accountService.account.displayName
|
accountService.account.displayName.isEmpty ? accountService.account.acct : accountService.account.displayName
|
||||||
}
|
}
|
||||||
|
@ -131,6 +135,14 @@ public extension AccountViewModel {
|
||||||
func set(note: String) {
|
func set(note: String) {
|
||||||
ignorableOutputEvent(accountService.set(note: note))
|
ignorableOutputEvent(accountService.set(note: note))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainBlock() {
|
||||||
|
ignorableOutputEvent(accountService.domainBlock())
|
||||||
|
}
|
||||||
|
|
||||||
|
func domainUnblock() {
|
||||||
|
ignorableOutputEvent(accountService.domainUnblock())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AccountViewModel {
|
private extension AccountViewModel {
|
||||||
|
|
53
ViewModels/Sources/ViewModels/DomainBlocksViewModel.swift
Normal file
53
ViewModels/Sources/ViewModels/DomainBlocksViewModel.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class DomainBlocksViewModel: ObservableObject {
|
||||||
|
@Published public private(set) var domainBlocks = [String]()
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
@Published public private(set) var loading = false
|
||||||
|
|
||||||
|
private let service: DomainBlocksService
|
||||||
|
private var nextPageMaxId: String?
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
public init(service: DomainBlocksService) {
|
||||||
|
self.service = service
|
||||||
|
|
||||||
|
service.nextPageMaxId
|
||||||
|
.sink { [weak self] in self?.nextPageMaxId = $0 }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension DomainBlocksViewModel {
|
||||||
|
func request() {
|
||||||
|
service.request(maxId: nextPageMaxId)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.handleEvents(receiveSubscription: { [weak self] _ in self?.loading = true })
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.loading = false
|
||||||
|
self.domainBlocks.append(contentsOf: Set($0).subtracting(Set(self.domainBlocks)))
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(domain: String) {
|
||||||
|
service.delete(domain: domain)
|
||||||
|
.collect()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.sink { [weak self] in
|
||||||
|
if case .finished = $0 {
|
||||||
|
self?.domainBlocks.removeAll { $0 == domain }
|
||||||
|
}
|
||||||
|
} receiveValue: { _ in }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,4 +29,8 @@ public extension PreferencesViewModel {
|
||||||
collectionService: identification.service.service(accountList: .blocks),
|
collectionService: identification.service.service(accountList: .blocks),
|
||||||
identification: identification)
|
identification: identification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func domainBlocksViewModel() -> DomainBlocksViewModel {
|
||||||
|
DomainBlocksViewModel(service: identification.service.domainBlocksService())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
50
Views/DomainBlocksView.swift
Normal file
50
Views/DomainBlocksView.swift
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct DomainBlocksView: View {
|
||||||
|
@StateObject var viewModel: DomainBlocksViewModel
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
ForEach(viewModel.domainBlocks, id: \.self) { domain in
|
||||||
|
Text(domain)
|
||||||
|
.onAppear {
|
||||||
|
if domain == viewModel.domainBlocks.last {
|
||||||
|
viewModel.request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete {
|
||||||
|
guard let index = $0.first else { return }
|
||||||
|
|
||||||
|
viewModel.delete(domain: viewModel.domainBlocks[index])
|
||||||
|
}
|
||||||
|
if viewModel.loading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.request()
|
||||||
|
}
|
||||||
|
.navigationTitle("preferences.blocked-domains")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import PreviewViewModels
|
||||||
|
|
||||||
|
struct DomainBlocksView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationView {
|
||||||
|
DomainBlocksView(viewModel: .preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -27,6 +27,8 @@ struct PreferencesView: View {
|
||||||
NavigationLink("preferences.blocked-users",
|
NavigationLink("preferences.blocked-users",
|
||||||
destination: TableView(viewModelClosure: viewModel.blockedUsersViewModel)
|
destination: TableView(viewModelClosure: viewModel.blockedUsersViewModel)
|
||||||
.navigationTitle(Text("preferences.blocked-users")))
|
.navigationTitle(Text("preferences.blocked-users")))
|
||||||
|
NavigationLink("preferences.blocked-domains",
|
||||||
|
destination: DomainBlocksView(viewModel: viewModel.domainBlocksViewModel()))
|
||||||
}
|
}
|
||||||
Section(header: Text("preferences.app")) {
|
Section(header: Text("preferences.app")) {
|
||||||
NavigationLink("preferences.media",
|
NavigationLink("preferences.media",
|
||||||
|
|
Loading…
Reference in a new issue