mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
Reporting
This commit is contained in:
parent
6e7541f208
commit
579f64f089
16 changed files with 379 additions and 13 deletions
|
@ -114,6 +114,12 @@
|
|||
"notifications.poll-ended" = "A poll you have voted in has ended";
|
||||
"notifications.your-poll-ended" = "Your poll has ended";
|
||||
"notifications.unknown" = "Notification from %@";
|
||||
"report" = "Report";
|
||||
"report.hint" = "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:";
|
||||
"report.placeholder" = "Additional comments";
|
||||
"report.target-%@" = "Reporting %@";
|
||||
"report.forward.hint" = "The account is from another server. Send an anonymized copy of the report there as well?";
|
||||
"report.forward-%@" = "Forward report to %@";
|
||||
"status.reblogged-by" = "%@ boosted";
|
||||
"status.pinned-post" = "Pinned post";
|
||||
"status.show-more" = "Show More";
|
||||
|
@ -125,6 +131,7 @@
|
|||
"status.visibility.public" = "Public";
|
||||
"status.visibility.unlisted" = "Unlisted";
|
||||
"status.visibility.private" = "Private";
|
||||
"submit" = "Submit";
|
||||
"timelines.home" = "Home";
|
||||
"timelines.local" = "Local";
|
||||
"timelines.federated" = "Federated";
|
||||
|
|
12
Mastodon/Sources/Mastodon/Entities/Report.swift
Normal file
12
Mastodon/Sources/Mastodon/Entities/Report.swift
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Report: Codable, Hashable {
|
||||
public let id: Id
|
||||
public let actionTaken: Bool
|
||||
}
|
||||
|
||||
public extension Report {
|
||||
typealias Id = String
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import HTTP
|
||||
import Mastodon
|
||||
|
||||
public enum ReportEndpoint {
|
||||
case create(Elements)
|
||||
}
|
||||
|
||||
public extension ReportEndpoint {
|
||||
struct Elements {
|
||||
public let accountId: Account.Id
|
||||
public var statusIds = Set<Status.Id>()
|
||||
public var comment = ""
|
||||
public var forward = false
|
||||
|
||||
public init(accountId: Account.Id) {
|
||||
self.accountId = accountId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportEndpoint: Endpoint {
|
||||
public typealias ResultType = Report
|
||||
|
||||
public var context: [String] {
|
||||
defaultContext + ["reports"]
|
||||
}
|
||||
|
||||
public var pathComponentsInContext: [String] {
|
||||
[]
|
||||
}
|
||||
|
||||
public var jsonBody: [String: Any]? {
|
||||
switch self {
|
||||
case let .create(creation):
|
||||
var params: [String: Any] = ["account_id": creation.accountId]
|
||||
|
||||
if !creation.statusIds.isEmpty {
|
||||
params["status_ids"] = Array(creation.statusIds)
|
||||
}
|
||||
|
||||
if !creation.comment.isEmpty {
|
||||
params["comment"] = creation.comment
|
||||
}
|
||||
|
||||
if creation.forward {
|
||||
params["forward"] = creation.forward
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
public var method: HTTPMethod {
|
||||
switch self {
|
||||
case .create:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 */; };
|
||||
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 */; };
|
||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; };
|
||||
|
@ -71,6 +72,7 @@
|
|||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; };
|
||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||
|
@ -156,6 +158,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>"; };
|
||||
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>"; };
|
||||
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
@ -193,6 +196,7 @@
|
|||
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
||||
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
||||
|
@ -289,10 +293,11 @@
|
|||
D0EA593F2522AC8700804347 /* CardView.swift */,
|
||||
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */,
|
||||
D0BEB1F224F8EE8C001B0F04 /* StatusAttachmentView.swift */,
|
||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
||||
D036AA16254CA823009094DF /* StatusBodyView.swift */,
|
||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
||||
D0625E58250F092900502611 /* StatusListCell.swift */,
|
||||
D00CB2EC2533ACC00080096B /* StatusView.swift */,
|
||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */,
|
||||
);
|
||||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
|
@ -374,6 +379,7 @@
|
|||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */,
|
||||
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
||||
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
||||
|
@ -637,6 +643,7 @@
|
|||
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
||||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */,
|
||||
|
@ -683,6 +690,7 @@
|
|||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
|
||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
|
||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
|
||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import MastodonAPI
|
||||
|
||||
public typealias ReportElements = ReportEndpoint.Elements
|
|
@ -15,11 +15,11 @@ public struct AccountService {
|
|||
private let mastodonAPIClient: MastodonAPIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(account: Account,
|
||||
relationship: Relationship? = nil,
|
||||
identityProofs: [IdentityProof] = [],
|
||||
mastodonAPIClient: MastodonAPIClient,
|
||||
contentDatabase: ContentDatabase) {
|
||||
public init(account: Account,
|
||||
relationship: Relationship? = nil,
|
||||
identityProofs: [IdentityProof] = [],
|
||||
mastodonAPIClient: MastodonAPIClient,
|
||||
contentDatabase: ContentDatabase) {
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.identityProofs = identityProofs
|
||||
|
@ -30,6 +30,10 @@ public struct AccountService {
|
|||
}
|
||||
|
||||
public extension AccountService {
|
||||
var isLocal: Bool {
|
||||
account.url.host == mastodonAPIClient.instanceURL.host
|
||||
}
|
||||
|
||||
func follow() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.accountsFollow(id: account.id))
|
||||
}
|
||||
|
@ -82,6 +86,10 @@ public extension AccountService {
|
|||
func set(note: String) -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.note(note, id: account.id))
|
||||
}
|
||||
|
||||
func report(_ elements: ReportElements) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension AccountService {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Combine
|
||||
import Mastodon
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
final class ProfileViewController: TableViewController {
|
||||
|
@ -66,6 +66,7 @@ final class ProfileViewController: TableViewController {
|
|||
}
|
||||
|
||||
private extension ProfileViewController {
|
||||
// swiftlint:disable:next function_body_length
|
||||
func menu(accountViewModel: AccountViewModel, relationship: Relationship) -> UIMenu {
|
||||
var actions = [UIAction]()
|
||||
|
||||
|
@ -99,6 +100,17 @@ private extension ProfileViewController {
|
|||
})
|
||||
}
|
||||
|
||||
actions.append(UIAction(
|
||||
title: NSLocalizedString("report", comment: ""),
|
||||
image: UIImage(systemName: "flag"),
|
||||
attributes: .destructive) { [weak self] _ in
|
||||
guard let self = self,
|
||||
let reportViewModel = self.viewModel.accountViewModel?.reportViewModel()
|
||||
else { return }
|
||||
|
||||
self.report(viewModel: reportViewModel)
|
||||
})
|
||||
|
||||
if relationship.blocking {
|
||||
actions.append(UIAction(
|
||||
title: NSLocalizedString("account.unblock", comment: ""),
|
||||
|
|
|
@ -126,6 +126,13 @@ extension TableViewController {
|
|||
static let autoplayableAttachmentsView = PassthroughSubject<StatusAttachmentsView?, Never>()
|
||||
static let autoplayableAttachmentsViewNotification =
|
||||
Notification.Name("com.metabolist.metatext.attachment-view-became-autoplayable")
|
||||
|
||||
func report(viewModel: ReportViewModel) {
|
||||
let reportViewController = ReportViewController(viewModel: viewModel)
|
||||
let navigationController = UINavigationController(rootViewController: reportViewController)
|
||||
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension TableViewController: UITableViewDataSourcePrefetching {
|
||||
|
@ -325,6 +332,8 @@ private extension TableViewController {
|
|||
}
|
||||
case let .attachment(attachmentViewModel, statusViewModel):
|
||||
present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel)
|
||||
case let .report(reportViewModel):
|
||||
report(viewModel: reportViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import Combine
|
|||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import MastodonAPI
|
||||
import MastodonAPIStubs
|
||||
import MockKeychain
|
||||
import Secrets
|
||||
|
@ -13,23 +14,24 @@ import ViewModels
|
|||
|
||||
// swiftlint:disable force_try
|
||||
|
||||
let identityId = Identity.Id()
|
||||
|
||||
let db: IdentityDatabase = {
|
||||
let id = Identity.Id()
|
||||
let db = try! IdentityDatabase(inMemory: true, appGroup: "", keychain: MockKeychain.self)
|
||||
let secrets = Secrets(identityId: id, keychain: MockKeychain.self)
|
||||
let secrets = Secrets(identityId: identityId, keychain: MockKeychain.self)
|
||||
|
||||
try! secrets.setInstanceURL(.previewInstanceURL)
|
||||
try! secrets.setAccessToken(UUID().uuidString)
|
||||
|
||||
_ = db.createIdentity(id: id, url: .previewInstanceURL, authenticated: true, pending: false)
|
||||
_ = db.createIdentity(id: identityId, url: .previewInstanceURL, authenticated: true, pending: false)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
_ = db.updateInstance(.preview, id: id)
|
||||
_ = db.updateInstance(.preview, id: identityId)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
_ = db.updateAccount(.preview, id: id)
|
||||
_ = db.updateAccount(.preview, id: identityId)
|
||||
.receive(on: ImmediateScheduler.shared)
|
||||
.sink { _ in } receiveValue: { _ in }
|
||||
|
||||
|
@ -39,6 +41,22 @@ let db: IdentityDatabase = {
|
|||
let environment = AppEnvironment.mock(fixtureDatabase: db)
|
||||
let decoder = MastodonDecoder()
|
||||
|
||||
extension MastodonAPIClient {
|
||||
static let preview = MastodonAPIClient(
|
||||
session: URLSession(configuration: .stubbing),
|
||||
instanceURL: .previewInstanceURL)
|
||||
}
|
||||
|
||||
extension ContentDatabase {
|
||||
static let preview = try! ContentDatabase(
|
||||
id: identityId,
|
||||
useHomeTimelineLastReadId: false,
|
||||
useNotificationsLastReadId: false,
|
||||
inMemory: true,
|
||||
appGroup: "group.metabolist.metatext",
|
||||
keychain: MockKeychain.self)
|
||||
}
|
||||
|
||||
public extension URL {
|
||||
static let previewInstanceURL = URL(string: "https://mastodon.social")!
|
||||
}
|
||||
|
@ -59,4 +77,13 @@ public extension Identification {
|
|||
static let preview = RootViewModel.preview.navigationViewModel!.identification
|
||||
}
|
||||
|
||||
public extension ReportViewModel {
|
||||
static let preview = ReportViewModel(
|
||||
accountService: AccountService(
|
||||
account: .preview,
|
||||
mastodonAPIClient: .preview,
|
||||
contentDatabase: .preview),
|
||||
identification: .preview)
|
||||
}
|
||||
|
||||
// swiftlint:enable force_try
|
||||
|
|
|
@ -66,6 +66,10 @@ public extension AccountViewModel {
|
|||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func reportViewModel() -> ReportViewModel {
|
||||
ReportViewModel(accountService: accountService, identification: identification)
|
||||
}
|
||||
|
||||
func follow() {
|
||||
ignorableOutputEvent(accountService.follow())
|
||||
}
|
||||
|
|
|
@ -7,5 +7,6 @@ public enum CollectionItemEvent {
|
|||
case ignorableOutput
|
||||
case navigation(Navigation)
|
||||
case attachment(AttachmentViewModel, StatusViewModel)
|
||||
case report(ReportViewModel)
|
||||
case share(URL)
|
||||
}
|
||||
|
|
62
ViewModels/Sources/ViewModels/ReportViewModel.swift
Normal file
62
ViewModels/Sources/ViewModels/ReportViewModel.swift
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public final class ReportViewModel: ObservableObject {
|
||||
@Published public var elements: ReportElements
|
||||
public let events: AnyPublisher<Event, Never>
|
||||
public let statusViewModel: StatusViewModel?
|
||||
@Published public private(set) var loading = false
|
||||
@Published public var alertItem: AlertItem?
|
||||
|
||||
private let accountService: AccountService
|
||||
private let eventsSubject = PassthroughSubject<Event, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(accountService: AccountService, statusService: StatusService? = nil, identification: Identification) {
|
||||
self.accountService = accountService
|
||||
elements = ReportElements(accountId: accountService.account.id)
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
|
||||
if let statusService = statusService {
|
||||
statusViewModel = StatusViewModel(statusService: statusService, identification: identification)
|
||||
elements.statusIds.insert(statusService.status.displayStatus.id)
|
||||
} else {
|
||||
statusViewModel = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ReportViewModel {
|
||||
enum Event {
|
||||
case reported
|
||||
}
|
||||
|
||||
var accountName: String { "@".appending(accountService.account.acct) }
|
||||
|
||||
var accountHost: String {
|
||||
accountService.account.url.host ?? ""
|
||||
}
|
||||
|
||||
var isLocalAccount: Bool { accountService.isLocal }
|
||||
|
||||
func report() {
|
||||
accountService.report(elements)
|
||||
.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
|
||||
|
||||
if $0 == .finished {
|
||||
self.eventsSubject.send(.reported)
|
||||
}
|
||||
} receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -215,6 +215,17 @@ public extension StatusViewModel {
|
|||
eventsSubject.send(Just(.share(url)).setFailureType(to: Error.self).eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func reportStatus() {
|
||||
eventsSubject.send(
|
||||
Just(.report(ReportViewModel(
|
||||
accountService: statusService.navigationService.accountService(
|
||||
account: statusService.status.displayStatus.account),
|
||||
statusService: statusService,
|
||||
identification: identification)))
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func vote() {
|
||||
eventsSubject.send(
|
||||
statusService.vote(selectedOptions: pollOptionSelections)
|
||||
|
|
101
Views/ReportView.swift
Normal file
101
Views/ReportView.swift
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct ReportView: View {
|
||||
@StateObject var viewModel: ReportViewModel
|
||||
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
fileprivate var dismissHostingController: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if let statusViewModel = viewModel.statusViewModel {
|
||||
Section {
|
||||
ReportStatusView(viewModel: statusViewModel)
|
||||
.frame(height: Self.statusHeight)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
Text("report.hint")
|
||||
ZStack(alignment: .leading) {
|
||||
if viewModel.elements.comment.isEmpty {
|
||||
Text("report.placeholder").foregroundColor(.secondary)
|
||||
}
|
||||
TextEditor(text: $viewModel.elements.comment)
|
||||
}
|
||||
if !viewModel.isLocalAccount {
|
||||
VStack(alignment: .leading) {
|
||||
Text("report.forward.hint")
|
||||
Toggle("report.forward-\(viewModel.accountHost)", isOn: $viewModel.elements.forward)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
if viewModel.loading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Button("submit") {
|
||||
viewModel.report()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertItem($viewModel.alertItem)
|
||||
.onReceive(viewModel.events) {
|
||||
switch $0 {
|
||||
case .reported:
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.navigationTitle("report.target-\(viewModel.accountName)")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReportView {
|
||||
static let statusHeight: CGFloat = 100
|
||||
|
||||
func dismiss() {
|
||||
if let dismissHostingController = dismissHostingController {
|
||||
dismissHostingController()
|
||||
} else {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReportViewController: UIHostingController<ReportView> {
|
||||
init(viewModel: ReportViewModel) {
|
||||
super.init(rootView: ReportView(viewModel: viewModel))
|
||||
|
||||
rootView.dismissHostingController = { [weak self] in self?.dismiss(animated: true) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@objc required dynamic init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import PreviewViewModels
|
||||
|
||||
struct ReportView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
ReportView(viewModel: .preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
27
Views/Status/ReportStatusView.swift
Normal file
27
Views/Status/ReportStatusView.swift
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct ReportStatusView: UIViewRepresentable {
|
||||
private let configuration: StatusContentConfiguration
|
||||
|
||||
init(viewModel: StatusViewModel) {
|
||||
configuration = StatusContentConfiguration(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> StatusView {
|
||||
let view = StatusView(configuration: configuration)
|
||||
|
||||
view.alpha = 0.5
|
||||
view.buttonsStackView.isHidden = true
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isUserInteractionEnabled = false
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: StatusView, context: Context) {
|
||||
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ final class StatusView: UIView {
|
|||
let favoriteButton = UIButton()
|
||||
let shareButton = UIButton()
|
||||
let menuButton = UIButton()
|
||||
let buttonsStackView = UIStackView()
|
||||
|
||||
private let containerStackView = UIStackView()
|
||||
private let sideStackView = UIStackView()
|
||||
|
@ -35,7 +36,6 @@ final class StatusView: UIView {
|
|||
private let interactionsDividerView = UIView()
|
||||
private let interactionsStackView = UIStackView()
|
||||
private let buttonsDividerView = UIView()
|
||||
private let buttonsStackView = UIStackView()
|
||||
private let inReplyToView = UIView()
|
||||
private let hasReplyFollowingView = UIView()
|
||||
private var statusConfiguration: StatusContentConfiguration
|
||||
|
@ -205,6 +205,16 @@ private extension StatusView {
|
|||
UIAction { [weak self] _ in self?.statusConfiguration.viewModel.shareStatus() },
|
||||
for: .touchUpInside)
|
||||
|
||||
menuButton.showsMenuAsPrimaryAction = true
|
||||
menuButton.menu = UIMenu(children: [
|
||||
UIAction(
|
||||
title: NSLocalizedString("report", comment: ""),
|
||||
image: UIImage(systemName: "flag"),
|
||||
attributes: .destructive) { [weak self] _ in
|
||||
self?.statusConfiguration.viewModel.reportStatus()
|
||||
}
|
||||
])
|
||||
|
||||
for button in actionButtons {
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
|
|
Loading…
Reference in a new issue