commit ad87a624864859d55770bb37c70c038aa4e5a300
parent 209a1c32137abc55021fc3ebda4147b8e059520b
Author: William Casarin <jb55@jb55.com>
Date: Wed, 25 Jan 2023 08:11:21 -0800
[appstore] Report Content
This view provides a way to report content (nudity, illegal, spam) to
relays. Clients can use this information to filter or warn if they
choose to.
This is needed for the appstore release
Changelog-Added: Added a way to report content
Diffstat:
9 files changed, 309 insertions(+), 101 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -133,6 +133,7 @@
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
+ 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
@@ -154,6 +155,8 @@
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */; };
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */; };
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */; };
+ 4CF0ABD42980996B00D66079 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD32980996B00D66079 /* Report.swift */; };
+ 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD529817F5B00D66079 /* ReportView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
@@ -361,6 +364,7 @@
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
+ 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
@@ -385,6 +389,8 @@
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = "<group>"; };
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowView.swift; sourceTree = "<group>"; };
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = "<group>"; };
+ 4CF0ABD32980996B00D66079 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = "<group>"; };
+ 4CF0ABD529817F5B00D66079 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
@@ -527,6 +533,7 @@
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
+ 4CF0ABD32980996B00D66079 /* Report.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -584,6 +591,7 @@
9609F057296E220800069BF3 /* BannerImageView.swift */,
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
6439E013296790CF0020672B /* ProfileZoomView.swift */,
+ 4CF0ABD529817F5B00D66079 /* ReportView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -655,6 +663,7 @@
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
+ 4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -1020,6 +1029,7 @@
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
+ 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
@@ -1053,6 +1063,7 @@
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
+ 4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
@@ -1061,6 +1072,7 @@
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
+ 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -26,10 +26,12 @@ struct TimestampedProfile {
enum Sheets: Identifiable {
case post
+ case report(ReportTarget)
case reply(NostrEvent)
var id: String {
switch self {
+ case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
}
@@ -229,6 +231,20 @@ struct ContentView: View {
}
}
+ func MaybeReportView(target: ReportTarget) -> some View {
+ Group {
+ if let ds = damus_state {
+ if let sec = ds.keypair.privkey {
+ ReportView(pool: ds.pool, target: target, privkey: sec)
+ } else {
+ EmptyView()
+ }
+ } else {
+ EmptyView()
+ }
+ }
+ }
+
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
@@ -279,6 +295,8 @@ struct ContentView: View {
}
.sheet(item: $active_sheet) { item in
switch item {
+ case .report(let target):
+ MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, references: [])
case .reply(let event):
@@ -326,6 +344,10 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
+ .onReceive(handle_notify(.report)) { notif in
+ let target = notif.object as! ReportTarget
+ self.active_sheet = .report(target)
+ }
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
self.damus_state?.pool.send(.event(ev))
diff --git a/damus/Models/Report.swift b/damus/Models/Report.swift
@@ -0,0 +1,59 @@
+//
+// Report.swift
+// damus
+//
+// Created by William Casarin on 2023-01-24.
+//
+
+import Foundation
+
+enum ReportType: String {
+ case explicit
+ case illegal
+ case spam
+ case impersonation
+}
+
+struct ReportNoteTarget {
+ let pubkey: String
+ let note_id: String
+}
+
+enum ReportTarget {
+ case user(String)
+ case note(ReportNoteTarget)
+}
+
+struct Report {
+ let type: ReportType
+ let target: ReportTarget
+ let message: String
+}
+
+func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
+ var tags: [[String]]
+ switch target {
+ case .user(let pubkey):
+ tags = [["p", pubkey]]
+ case .note(let notet):
+ tags = [["e", notet.note_id], ["p", notet.pubkey]]
+ }
+
+ tags.append(["report", type.rawValue])
+ return tags
+}
+
+func create_report_event(privkey: String, report: Report) -> NostrEvent? {
+ guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
+ return nil
+ }
+
+ let kind = 1984
+ let tags = create_report_tags(target: report.target, type: report.type)
+ let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags)
+
+ ev.id = calculate_event_id(ev: ev)
+ ev.sig = sign_event(privkey: privkey, ev: ev)
+
+ return ev
+}
diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift
@@ -11,150 +11,81 @@ extension Notification.Name {
static var thread_focus: Notification.Name {
return Notification.Name("thread focus")
}
-}
-
-extension Notification.Name {
static var relays_changed: Notification.Name {
return Notification.Name("relays_changed")
}
-}
-
-extension Notification.Name {
static var select_event: Notification.Name {
return Notification.Name("select_event")
}
-}
-
-extension Notification.Name {
static var select_quote: Notification.Name {
return Notification.Name("select quote")
}
-}
-
-extension Notification.Name {
static var reply: Notification.Name {
return Notification.Name("reply")
}
-}
-
-extension Notification.Name {
static var profile_updated: Notification.Name {
return Notification.Name("profile_updated")
}
-}
-
-extension Notification.Name {
static var switched_timeline: Notification.Name {
return Notification.Name("switched_timeline")
}
-}
-
-extension Notification.Name {
static var liked: Notification.Name {
return Notification.Name("liked")
}
-}
-
-extension Notification.Name {
static var open_profile: Notification.Name {
return Notification.Name("open_profile")
}
-}
-
-extension Notification.Name {
static var scroll_to_top: Notification.Name {
return Notification.Name("scroll_to_to")
}
-}
-
-extension Notification.Name {
static var broadcast_event: Notification.Name {
return Notification.Name("broadcast event")
}
-}
-
-extension Notification.Name {
static var open_thread: Notification.Name {
return Notification.Name("open thread")
}
-}
-
-extension Notification.Name {
static var notice: Notification.Name {
return Notification.Name("notice")
}
-}
-
-extension Notification.Name {
static var like: Notification.Name {
return Notification.Name("like note")
}
-}
-
-extension Notification.Name {
static var delete: Notification.Name {
return Notification.Name("delete note")
}
-}
-
-extension Notification.Name {
static var post: Notification.Name {
return Notification.Name("send post")
}
-}
-
-extension Notification.Name {
static var boost: Notification.Name {
return Notification.Name("boost")
}
-}
-
-extension Notification.Name {
static var boosted: Notification.Name {
return Notification.Name("boosted")
}
-}
-
-extension Notification.Name {
static var follow: Notification.Name {
return Notification.Name("follow")
}
-}
-
-extension Notification.Name {
static var unfollow: Notification.Name {
return Notification.Name("unfollow")
}
-}
-
-extension Notification.Name {
static var login: Notification.Name {
return Notification.Name("login")
}
-}
-
-extension Notification.Name {
static var logout: Notification.Name {
return Notification.Name("logout")
}
-}
-
-extension Notification.Name {
static var followed: Notification.Name {
return Notification.Name("followed")
}
-}
-
-extension Notification.Name {
static var chatroom_meta: Notification.Name {
return Notification.Name("chatroom_meta")
}
-}
-
-extension Notification.Name {
static var unfollowed: Notification.Name {
return Notification.Name("unfollowed")
}
+ static var report: Notification.Name {
+ return Notification.Name("report")
+ }
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -100,6 +100,8 @@ struct EventView: View {
Text("\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
+
+ Spacer()
}
EventBody(damus_state: damus, event: event, size: .normal)
@@ -171,35 +173,7 @@ extension View {
func event_context_menu(_ event: NostrEvent, pubkey: String, privkey: String?) -> some View {
return self.contextMenu {
- Button {
- UIPasteboard.general.string = event.get_content(privkey)
- } label: {
- Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
- }
-
- Button {
- UIPasteboard.general.string = bech32_pubkey(pubkey) ?? pubkey
- } label: {
- Label(NSLocalizedString("Copy User ID", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
- }
-
- Button {
- UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
- } label: {
- Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
- }
-
- Button {
- UIPasteboard.general.string = event_to_json(ev: event)
- } label: {
- Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "j.square.on.square")
- }
-
- Button {
- NotificationCenter.default.post(name: .broadcast_event, object: event)
- } label: {
- Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
- }
+ EventMenuContext(event: event, privkey: privkey, pubkey: pubkey)
}
}
diff --git a/damus/Views/Events/EmbeddedEventView.swift b/damus/Views/Events/EmbeddedEventView.swift
@@ -23,6 +23,7 @@ struct EmbeddedEventView: View {
EventBody(damus_state: damus_state, event: event, size: .small)
}
+ .event_context_menu(event, pubkey: pubkey, privkey: damus_state.keypair.privkey)
}
}
diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift
@@ -0,0 +1,93 @@
+//
+// EventMenu.swift
+// damus
+//
+// Created by William Casarin on 2023-01-23.
+//
+
+import SwiftUI
+
+struct EventMenuContext: View {
+ let event: NostrEvent
+ let privkey: String?
+ let pubkey: String
+
+ var body: some View {
+
+ Button {
+ UIPasteboard.general.string = event.get_content(privkey)
+ } label: {
+ Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
+ }
+
+ Button {
+ UIPasteboard.general.string = bech32_pubkey(pubkey) ?? pubkey
+ } label: {
+ Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
+ }
+
+ Button {
+ UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
+ } label: {
+ Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
+ }
+
+ Button {
+ UIPasteboard.general.string = event_to_json(ev: event)
+ } label: {
+ Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
+ }
+
+ Button {
+ let target: ReportTarget = .note(ReportNoteTarget(pubkey: event.pubkey, note_id: event.id))
+ notify(.report, target)
+ } label: {
+ Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
+ }
+
+ Button {
+ NotificationCenter.default.post(name: .broadcast_event, object: event)
+ } label: {
+ Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
+ }
+ }
+}
+
+/*
+struct EventMenu: UIViewRepresentable {
+
+ typealias UIViewType = UIButton
+
+ let saveAction = UIAction(title: "") { action in }
+ let saveMenu = UIMenu(title: "", children: [
+ UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
+ //code action for menu item
+ },
+ UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
+ //code action for menu item
+ },
+ UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
+ //code action for menu item
+ },
+ ])
+
+ func makeUIView(context: Context) -> UIButton {
+ let button = UIButton(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
+ button.showsMenuAsPrimaryAction = true
+ button.menu = saveMenu
+
+ return button
+ }
+
+ func updateUIView(_ uiView: UIButton, context: Context) {
+ uiView.setImage(UIImage(systemName: "plus"), for: .normal)
+ }
+}
+
+struct EventMenu_Previews: PreviewProvider {
+ static var previews: some View {
+ EventMenu(event: test_event, privkey: nil, pubkey: test_event.pubkey)
+ }
+}
+
+*/
diff --git a/damus/Views/Events/SelectedEventView.swift b/damus/Views/Events/SelectedEventView.swift
@@ -49,6 +49,7 @@ struct SelectedEventView: View {
.padding([.top], 4)
}
.padding([.leading], 2)
+ .event_context_menu(event, pubkey: pubkey, privkey: damus.keypair.privkey)
}
}
}
diff --git a/damus/Views/ReportView.swift b/damus/Views/ReportView.swift
@@ -0,0 +1,115 @@
+//
+// ReportView.swift
+// damus
+//
+// Created by William Casarin on 2023-01-25.
+//
+
+import SwiftUI
+
+struct ReportView: View {
+ let pool: RelayPool
+ let target: ReportTarget
+ let privkey: String
+
+ @State var report_sent: Bool = false
+ @State var report_id: String = ""
+
+ var body: some View {
+ if report_sent {
+ Success
+ } else {
+ MainForm
+ }
+ }
+
+ var Success: some View {
+ VStack(alignment: .center, spacing: 20) {
+ Text("Report sent!")
+ .font(.headline)
+
+ Text("Relays have been notified and clients will be able to use this information to filter content. Thank you!")
+
+ Text("Report ID:")
+
+ Text(report_id)
+
+ Button("Copy Report ID") {
+ UIPasteboard.general.string = report_id
+ let g = UIImpactFeedbackGenerator(style: .medium)
+ g.impactOccurred()
+ }
+ }
+ .padding()
+ }
+
+ func do_send_report(type: ReportType) {
+ guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: .spam) else {
+ return
+ }
+
+ guard let note_id = bech32_note_id(ev.id) else {
+ return
+ }
+
+ report_sent = true
+ report_id = note_id
+ }
+
+ var MainForm: some View {
+ VStack {
+
+ Text("Report")
+ .font(.headline)
+ .padding()
+
+ Form {
+ Section(content: {
+ Button("It's spam") {
+ do_send_report(type: .spam)
+ }
+
+ Button("Nudity or explicit content") {
+ do_send_report(type: .explicit)
+ }
+
+ Button("Illegal content") {
+ do_send_report(type: .illegal)
+ }
+
+ if case .user = target {
+ Button("They are impersonating someone") {
+ do_send_report(type: .impersonation)
+ }
+ }
+ }, header: {
+ Text("What do you want to report?")
+ }, footer: {
+ Text("Your report will be sent to the relays you are connected to")
+ })
+ }
+ }
+ }
+}
+
+func send_report(privkey: String, pool: RelayPool, target: ReportTarget, type: ReportType) -> NostrEvent? {
+ let report = Report(type: type, target: target, message: "")
+ guard let ev = create_report_event(privkey: privkey, report: report) else {
+ return nil
+ }
+ pool.send(.event(ev))
+ return ev
+}
+
+struct ReportView_Previews: PreviewProvider {
+ static var previews: some View {
+ let ds = test_damus_state()
+ VStack {
+
+ ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "")
+
+ ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id")
+
+ }
+ }
+}