Domain blocks
This commit is contained in:
parent
30cedb503f
commit
43e58bce35
|
@ -2,6 +2,11 @@
|
|||
|
||||
"account.%@-followers" = "%@'s Followers";
|
||||
"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.follow" = "Follow";
|
||||
"account.following" = "Following";
|
||||
|
@ -15,6 +20,7 @@
|
|||
"account.media" = "Media";
|
||||
"account.show-reblogs" = "Show boosts";
|
||||
"account.unblock" = "Unblock";
|
||||
"account.unblock.confirm-%@" = "Unblock %@?";
|
||||
"account.unfollow" = "Unfollow";
|
||||
"account.unmute" = "Unmute";
|
||||
"add" = "Add";
|
||||
|
@ -53,6 +59,7 @@
|
|||
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||
"preferences" = "Preferences";
|
||||
"preferences.app" = "App Preferences";
|
||||
"preferences.blocked-domains" = "Blocked Domains";
|
||||
"preferences.blocked-users" = "Blocked Users";
|
||||
"preferences.media" = "Media";
|
||||
"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 deleteList(id: List.Id)
|
||||
case deleteFilter(id: Filter.Id)
|
||||
case blockDomain(String)
|
||||
case unblockDomain(String)
|
||||
}
|
||||
|
||||
extension EmptyEndpoint: Endpoint {
|
||||
|
@ -21,6 +23,8 @@ extension EmptyEndpoint: Endpoint {
|
|||
return defaultContext + ["lists"]
|
||||
case .deleteFilter:
|
||||
return defaultContext + ["filters"]
|
||||
case .blockDomain, .unblockDomain:
|
||||
return defaultContext + ["domain_blocks"]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,14 +34,16 @@ extension EmptyEndpoint: Endpoint {
|
|||
return ["revoke"]
|
||||
case let .deleteList(id), let .deleteFilter(id):
|
||||
return [id]
|
||||
case .blockDomain, .unblockDomain:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
switch self {
|
||||
case .oauthRevoke:
|
||||
case .oauthRevoke, .blockDomain:
|
||||
return .post
|
||||
case .deleteList, .deleteFilter:
|
||||
case .deleteList, .deleteFilter, .unblockDomain:
|
||||
return .delete
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +52,8 @@ extension EmptyEndpoint: Endpoint {
|
|||
switch self {
|
||||
case let .oauthRevoke(token, clientId, clientSecret):
|
||||
return ["token": token, "client_id": clientId, "client_secret": clientSecret]
|
||||
case let .blockDomain(domain), let .unblockDomain(domain):
|
||||
return ["domain": domain]
|
||||
case .deleteList, .deleteFilter:
|
||||
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 */; };
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.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 */; };
|
||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||
|
@ -361,6 +363,7 @@
|
|||
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||
|
@ -684,6 +687,7 @@
|
|||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
||||
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */,
|
||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
||||
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||
|
|
|
@ -34,6 +34,8 @@ public extension AccountService {
|
|||
account.url.host == mastodonAPIClient.instanceURL.host
|
||||
}
|
||||
|
||||
var domain: String? { account.url.host }
|
||||
|
||||
func follow() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.accountsFollow(id: account.id))
|
||||
}
|
||||
|
@ -91,6 +93,18 @@ public extension AccountService {
|
|||
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 {
|
||||
AccountListService(
|
||||
endpoint: .accountsFollowing(id: account.id),
|
||||
|
@ -114,4 +128,12 @@ private extension AccountService {
|
|||
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
||||
.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 {
|
||||
ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func domainBlocksService() -> DomainBlocksService {
|
||||
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||
}
|
||||
}
|
||||
|
||||
private extension IdentityService {
|
||||
|
|
|
@ -114,18 +114,71 @@ private extension ProfileViewController {
|
|||
if relationship.blocking {
|
||||
actions.append(UIAction(
|
||||
title: NSLocalizedString("account.unblock", comment: ""),
|
||||
image: UIImage(systemName: "slash.circle")) { _ in
|
||||
image: UIImage(systemName: "slash.circle"),
|
||||
attributes: .destructive) { [weak self] _ in
|
||||
self?.confirm(message: String.localizedStringWithFormat(
|
||||
NSLocalizedString("account.unblock.confirm-%@", comment: ""),
|
||||
accountViewModel.accountName)) {
|
||||
accountViewModel.unblock()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
actions.append(UIAction(
|
||||
title: NSLocalizedString("account.block", comment: ""),
|
||||
image: UIImage(systemName: "slash.circle"),
|
||||
attributes: .destructive) { _ in
|
||||
attributes: .destructive) { [weak self] _ in
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
public extension DomainBlocksViewModel {
|
||||
static let preview = DomainBlocksViewModel(service: .init(mastodonAPIClient: .preview))
|
||||
}
|
||||
|
||||
// swiftlint:enable force_try
|
||||
|
|
|
@ -28,6 +28,10 @@ public extension AccountViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
var isLocal: Bool { accountService.isLocal }
|
||||
|
||||
var domain: String? { accountService.domain }
|
||||
|
||||
var displayName: String {
|
||||
accountService.account.displayName.isEmpty ? accountService.account.acct : accountService.account.displayName
|
||||
}
|
||||
|
@ -131,6 +135,14 @@ public extension AccountViewModel {
|
|||
func set(note: String) {
|
||||
ignorableOutputEvent(accountService.set(note: note))
|
||||
}
|
||||
|
||||
func domainBlock() {
|
||||
ignorableOutputEvent(accountService.domainBlock())
|
||||
}
|
||||
|
||||
func domainUnblock() {
|
||||
ignorableOutputEvent(accountService.domainUnblock())
|
||||
}
|
||||
}
|
||||
|
||||
private extension AccountViewModel {
|
||||
|
|
|
@ -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),
|
||||
identification: identification)
|
||||
}
|
||||
|
||||
func domainBlocksViewModel() -> DomainBlocksViewModel {
|
||||
DomainBlocksViewModel(service: identification.service.domainBlocksService())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
destination: TableView(viewModelClosure: viewModel.blockedUsersViewModel)
|
||||
.navigationTitle(Text("preferences.blocked-users")))
|
||||
NavigationLink("preferences.blocked-domains",
|
||||
destination: DomainBlocksView(viewModel: viewModel.domainBlocksViewModel()))
|
||||
}
|
||||
Section(header: Text("preferences.app")) {
|
||||
NavigationLink("preferences.media",
|
||||
|
|
Loading…
Reference in New Issue