Add report user/post feature

This commit is contained in:
Marcin Czachursk 2023-04-04 09:14:07 +02:00
parent f38d91d6be
commit 58425a26c6
14 changed files with 255 additions and 70 deletions

View File

@ -102,6 +102,7 @@
"userProfile.title.unmuted" = "Account unmuted";
"userProfile.title.blocked" = "Account blocked";
"userProfile.title.unblocked" = "Account unblocked";
"userProfile.title.report" = "Report";
"userProfile.error.notExists" = "Account does not exists.";
"userProfile.error.loadingAccountFailed" = "Error during download account from server.";
"userProfile.error.muting" = "Muting/unmuting action failed.";
@ -237,6 +238,7 @@
"status.title.bookmark" = "Bookmark";
"status.title.unbookmark" = "Unbookmark";
"status.title.comment" = "Comment";
"status.title.report" = "Report";
"status.error.loadingStatusFailed" = "Loading status failed.";
"status.error.notFound" = "Status not existing anymore.";
"status.error.loadingCommentsFailed" = "Comments cannot be downloaded.";
@ -301,3 +303,21 @@
"instance.title.pixelfedAccount" = "Pixelfed account";
"instance.error.noInstanceData" = "Instance data cannot be displayed.";
"instance.error.loadingDataFailed" = "Error during download instance data from server.";
// Mark: Report screen.
"report.navigationBar.title" = "Report";
"report.title.close" = "Close";
"report.title.send" = "Send";
"report.title.userReported" = "User has been reported";
"report.title.postReported" = "Post has been reported";
"report.title.reportType" = "Type of abuse";
"report.title.spam" = "It's a spam";
"report.title.sensitive" = "Nudity or sexual activity";
"report.title.abusive" = "Hate speech or symbols";
"report.title.underage" = "Underage account";
"report.title.violence" = "Violence or dangerous organisations";
"report.title.copyright" = "Copyright infringement";
"report.title.impersonation" = "Impersonation";
"report.title.scam" = "Bullying of harassment";
"report.title.terrorism" = "Terrorism";
"report.error.notReported" = "Error during sending report.";

View File

@ -103,6 +103,7 @@
"userProfile.title.unmuted" = "Wyciszenie wyłączone";
"userProfile.title.blocked" = "Konto zablokowane";
"userProfile.title.unblocked" = "Konto odblokowane";
"userProfile.title.report" = "Zgłoś";
"userProfile.error.notExists" = "Błąd podczas pobierania danych użytkownika.";
"userProfile.error.mute" = "Błąd podczas wyciszania użytkownika.";
"userProfile.error.block" = "Błąd podczas blokowania/odblokowywania użytkownika.";
@ -237,6 +238,7 @@
"status.title.bookmark" = "Dodaj do zakładek";
"status.title.unbookmark" = "Usuń z zakładek";
"status.title.comment" = "Skomentuj";
"status.title.report" = "Zgłoś";
"status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu.";
"status.error.notFound" = "Status już nie istnieje.";
"status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy.";
@ -292,7 +294,7 @@
"instance.title.email" = "Email";
"instance.title.version" = "Wersja";
"instance.title.users" = "Użytkownicy";
"instance.title.posts" = "Postów";
"instance.title.posts" = "Statusów";
"instance.title.domains" = "Domen";
"instance.title.registrations" = "Rejestracja";
"instance.title.approvalRequired" = "Akeptowanie rejestracji";
@ -301,3 +303,21 @@
"instance.title.pixelfedAccount" = "Konto Pixelfed";
"instance.error.noInstanceData" = "Dane instancji nie mogą zostać wyświetlone.";
"instance.error.loadingDataFailed" = "Błąd podczas pobierania danych instancji.";
// Mark: Report screen.
"report.navigationBar.title" = "Zgłoś";
"report.title.close" = "Zamknij";
"report.title.send" = "Wyślij";
"report.title.userReported" = "Użytkownik został zgłoszony.";
"report.title.postReported" = "Status został zgłoszony.";
"report.title.reportType" = "Typ nadużycia";
"report.title.spam" = "Spam";
"report.title.sensitive" = "Nagość lub aktywność seksualna";
"report.title.abusive" = "Mowa lub symbole nienawiści";
"report.title.underage" = "Konto niepełnoletniego";
"report.title.violence" = "Przemoc lub niebezpieczne organizacje";
"report.title.copyright" = "Naruszenie praw autorskich";
"report.title.impersonation" = "Podszywanie się";
"report.title.scam" = "Znęcanie się lub nękanie";
"report.title.terrorism" = "Terroryzm";
"report.error.notReported" = "Błąd podczas wysyłania zgłoszenia.";

View File

@ -8,55 +8,33 @@ import Foundation
/// Reports filed against users and/or statuses, to be taken action on by moderators.
public struct Report: Codable {
public enum ReportCategoryTye: String, Codable {
/// Unwanted or repetitive content
/// Type of report.
public enum ReportType: String, Codable {
case spam
/// A specific rule was violated
case violation
/// Some other reason
case other
case sensitive
case abusive
case underage
case violence
case copyright
case impersonation
case scam
case terrorism
}
/// The ID of the report in the database.
public let id: EntityId
/// Object type.
public enum ObjectType: String, Codable {
case post
case user
}
/// Whether an action was taken yet.
public let actionTaken: String?
public let objectType: Report.ObjectType
public let objectId: EntityId
public let type: Report.ReportType
/// When an action was taken against the report. NULLABLE String (ISO 8601 Datetime) or null.
public let actionTakenAt: String?
/// The generic reason for the report.
public let category: ReportCategoryTye
/// The reason for the report.
public let comment: String
/// Whether the report was forwarded to a remote domain.
public let forwarded: Bool
/// When the report was created. String (ISO 8601 Datetime).
public let createdAt: String
/// List od statuses in the report.
public let statusIds: [EntityId]?
/// List of the rules in ther report.
public let ruleIds: [EntityId]?
/// The account that was reported.
public let targetAccount: Account
public enum CodingKeys: String, CodingKey {
case id
case actionTaken
case actionTakenAt = "action_taken_at"
case category
case comment
case forwarded
case createdAt = "created_at"
case statusIds = "status_ids"
case ruleIds = "rule_ids"
case targetAccount = "target_account"
private enum CodingKeys: String, CodingKey {
case objectType = "object_type"
case objectId = "object_id"
case type
}
}

View File

@ -0,0 +1,21 @@
//
// https://mczachurski.dev
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
public extension PixelfedClientAuthenticated {
func report(objectType: Report.ObjectType,
objectId: EntityId,
reportType: Report.ReportType) async throws -> Report {
let request = try Self.request(
for: baseURL,
target: Pixelfed.Reports.report(objectType, objectId, reportType),
withBearerToken: token
)
return try await downloadJson(Report.self, request: request)
}
}

View File

@ -8,37 +8,36 @@ import Foundation
extension Pixelfed {
public enum Reports {
case list
case report(String, [String], String)
case report(Report.ObjectType, EntityId, Report.ReportType)
}
}
extension Pixelfed.Reports: TargetType {
private struct Request: Encodable {
let accountId: String
let statusIds: [String]
let comment: String
let objectType: Report.ObjectType
let objectId: EntityId
let reportType: Report.ReportType
private enum CodingKeys: String, CodingKey {
case accountId = "account_id"
case statusIds = "status_ids"
case comment
case objectType = "object_type"
case objectId = "object_id"
case reportType = "report_type"
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<Pixelfed.Reports.Request.CodingKeys> = encoder.container(keyedBy: Pixelfed.Reports.Request.CodingKeys.self)
try container.encode(self.accountId, forKey: Pixelfed.Reports.Request.CodingKeys.accountId)
try container.encode(self.statusIds, forKey: Pixelfed.Reports.Request.CodingKeys.statusIds)
try container.encode(self.comment, forKey: Pixelfed.Reports.Request.CodingKeys.comment)
try container.encode(self.objectType, forKey: Pixelfed.Reports.Request.CodingKeys.objectType)
try container.encode(self.objectId, forKey: Pixelfed.Reports.Request.CodingKeys.objectId)
try container.encode(self.reportType, forKey: Pixelfed.Reports.Request.CodingKeys.reportType)
}
}
private var apiPath: String { return "/api/v1/reports" }
private var apiPath: String { return "/api/v1.1/report" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .list, .report:
case .report:
return "\(apiPath)"
}
}
@ -46,8 +45,6 @@ extension Pixelfed.Reports: TargetType {
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .list:
return .get
case .report:
return .post
}
@ -64,11 +61,9 @@ extension Pixelfed.Reports: TargetType {
public var httpBody: Data? {
switch self {
case .list:
return nil
case .report(let accountId, let statusIds, let comment):
case .report(let objectType, let objectId, let reportType):
return try? JSONEncoder().encode(
Request(accountId: accountId, statusIds: statusIds, comment: comment)
Request(objectType: objectType, objectId: objectId, reportType: reportType)
)
}
}

View File

@ -14,6 +14,8 @@
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F802884E297AEED5000BDD51 /* DatabaseError.swift */; };
F805DCEF29DBED96006A1FD9 /* Client+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = F805DCEE29DBED96006A1FD9 /* Client+Report.swift */; };
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F805DCF029DBEF83006A1FD9 /* ReportView.swift */; };
F808641429756666009F035C /* NotificationRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808641329756666009F035C /* NotificationRowView.swift */; };
F8121CA8298A86D600B466C7 /* InstanceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8121CA7298A86D600B466C7 /* InstanceRowView.swift */; };
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DCE2966B600001D9973 /* ImageRowAsync.swift */; };
@ -252,6 +254,8 @@
F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = "<group>"; };
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F802884E297AEED5000BDD51 /* DatabaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = "<group>"; };
F805DCEE29DBED96006A1FD9 /* Client+Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Client+Report.swift"; sourceTree = "<group>"; };
F805DCF029DBEF83006A1FD9 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
F808641329756666009F035C /* NotificationRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowView.swift; sourceTree = "<group>"; };
F8121CA7298A86D600B466C7 /* InstanceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceRowView.swift; sourceTree = "<group>"; };
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = "<group>"; };
@ -537,6 +541,7 @@
F89AC00629A208CC00F4159F /* PlaceSelectorView.swift */,
F8E6D03229CDD52500416CCA /* EditProfileView.swift */,
F89B5CC129D01BF700549F2F /* InstanceView.swift */,
F805DCF029DBEF83006A1FD9 /* ReportView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -912,6 +917,7 @@
F8B9B350298D4B34009CC69C /* Client+Account.swift */,
F8B9B352298D4B5D009CC69C /* Client+Search.swift */,
F89AC00829A20C5C00F4159F /* Client+Places.swift */,
F805DCEE29DBED96006A1FD9 /* Client+Report.swift */,
F8B9B355298D4C1E009CC69C /* Client+Instance.swift */,
F86A4302299A9AF500DF7645 /* TipsStore.swift */,
);
@ -1183,6 +1189,7 @@
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,
F85D49872964334100751DF7 /* String+Date.swift in Sources */,
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */,
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */,
F808641429756666009F035C /* NotificationRowView.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
@ -1281,6 +1288,7 @@
F88AB05329B3613900345EDE /* PhotoUrl.swift in Sources */,
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */,
F805DCEF29DBED96006A1FD9 /* Client+Report.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */,
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,

View File

@ -62,6 +62,8 @@ extension View {
ComposeView()
case .settings:
SettingsView()
case .report(let objectType, let objectId):
ReportView(objectType: objectType, objectId: objectId)
}
}
}

View File

@ -0,0 +1,16 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension Client {
public class Reports: BaseClient {
func report(objectType: Report.ObjectType, objectId: EntityId, reportType: Report.ReportType) async throws -> Report {
return try await pixelfedClient.report(objectType: objectType, objectId: objectId, reportType: reportType)
}
}
}

View File

@ -38,6 +38,7 @@ extension Client {
public var places: Places? { return Places(pixelfedClient: self.pixelfedClient) }
public var blocks: Blocks? { return Blocks(pixelfedClient: self.pixelfedClient) }
public var mutes: Mutes? { return Mutes(pixelfedClient: self.pixelfedClient) }
public var reports: Reports? { return Reports(pixelfedClient: self.pixelfedClient) }
public var instances: Instances { return Instances() }
}

View File

@ -30,6 +30,7 @@ enum SheetDestinations: Identifiable {
case newStatusEditor
case replyToStatusEditor(status: StatusModel)
case settings
case report(objectType: Report.ObjectType, objectId: String)
public var id: String {
switch self {
@ -37,6 +38,8 @@ enum SheetDestinations: Identifiable {
return "statusEditor"
case .settings:
return "settings"
case .report:
return "report"
}
}
}

View File

@ -104,7 +104,7 @@ struct NotificationRowView: View {
EmptyView()
}
case .adminReport:
Text(self.notification.report?.comment ?? "")
Text(self.notification.report?.type.rawValue ?? "")
.multilineTextAlignment(.leading)
}
}
@ -126,10 +126,14 @@ struct NotificationRowView: View {
accountDisplayName: notification.account.displayNameWithoutEmojis,
accountUserName: notification.account.acct))
case .adminReport:
if let targetAccount = notification.report?.targetAccount {
self.routerPath.navigate(to: .userProfile(accountId: targetAccount.id,
accountDisplayName: targetAccount.displayNameWithoutEmojis,
accountUserName: targetAccount.acct))
if let objectType = notification.report?.objectType, let objectId = notification.report?.objectId {
switch objectType {
case .user:
self.routerPath.navigate(to: .userProfile(accountId: objectId, accountDisplayName: "", accountUserName: ""))
case .post:
self.routerPath.navigate(to: .status(id: objectId))
}
}
}
}

View File

@ -0,0 +1,102 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import PixelfedKit
struct ReportView: View {
@EnvironmentObject private var client: Client
@Environment(\.dismiss) private var dismiss
@State private var publishDisabled = false
@State private var reportType = Report.ReportType.sensitive
private let objectType: Report.ObjectType
private let objectId: String
private let reportTypes: [(reportType: Report.ReportType, name: LocalizedStringKey)] = [
(Report.ReportType.spam, "report.title.spam"),
(Report.ReportType.sensitive, "report.title.sensitive"),
(Report.ReportType.abusive, "report.title.abusive"),
(Report.ReportType.underage, "report.title.underage"),
(Report.ReportType.violence, "report.title.violence"),
(Report.ReportType.copyright, "report.title.copyright"),
(Report.ReportType.impersonation, "report.title.impersonation"),
(Report.ReportType.scam, "report.title.scam"),
(Report.ReportType.terrorism, "report.title.terrorism")
]
init(objectType: Report.ObjectType, objectId: String) {
self.objectType = objectType
self.objectId = objectId
}
var body: some View {
NavigationView {
Form {
Section("report.title.reportType") {
ForEach(self.reportTypes, id: \.reportType) { item in
Button {
self.reportType = item.reportType
} label: {
HStack(alignment: .center) {
Text(item.name, comment: "Report type")
.font(.subheadline)
.foregroundColor(.mainTextColor)
Spacer()
if self.reportType == item.reportType {
Image(systemName: "checkmark")
}
}
}
}
}
}
.frame(alignment: .topLeading)
.toolbar {
ToolbarItem(placement: .primaryAction) {
ActionButton {
await onSendReport()
} label: {
Text("report.title.send", comment: "Send")
}
.disabled(self.publishDisabled)
.buttonStyle(.borderedProminent)
}
ToolbarItem(placement: .cancellationAction) {
Button(NSLocalizedString("report.title.close", comment: "Close"), role: .cancel) {
self.dismiss()
}
}
}
.navigationTitle("report.navigationBar.title")
.navigationBarTitleDisplayMode(.inline)
}
}
private func onSendReport() async {
do {
if try await self.client.reports?.report(objectType: self.objectType,
objectId: self.objectId,
reportType: self.reportType) != nil {
switch self.objectType {
case .post:
ToastrService.shared.showSuccess("report.title.postReported", imageSystemName: "exclamationmark.triangle")
case .user:
ToastrService.shared.showSuccess("report.title.userReported", imageSystemName: "exclamationmark.triangle")
}
self.dismiss()
}
} catch {
ErrorService.shared.handle(error, message: "report.error.notReported", showToastr: true)
}
}
}

View File

@ -10,6 +10,7 @@ import PixelfedKit
struct UserProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@Environment(\.dismiss) private var dismiss
@ -148,6 +149,12 @@ struct UserProfileView: View {
}
}
Button {
self.routerPath.presentedSheet = .report(objectType: .user, objectId: self.accountId)
} label: {
Label(NSLocalizedString("userProfile.title.report", comment: "Report"), systemImage: "exclamationmark.triangle")
}
}, label: {
Image(systemName: "gear")
.tint(.mainTextColor)

View File

@ -108,6 +108,14 @@ struct InteractionRow: View {
Label("status.title.delete", systemImage: "trash")
}
}
} else {
Divider()
Button {
self.routerPath.presentedSheet = .report(objectType: .post, objectId: self.statusModel.id)
} label: {
Label(NSLocalizedString("status.title.report", comment: "Report"), systemImage: "exclamationmark.triangle")
}
}
} label: {
Image(systemName: "gear")