damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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:
Mdamus.xcodeproj/project.pbxproj | 12++++++++++++
Mdamus/ContentView.swift | 22++++++++++++++++++++++
Adamus/Models/Report.swift | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Util/Notifications.swift | 75+++------------------------------------------------------------------------
Mdamus/Views/EventView.swift | 32+++-----------------------------
Mdamus/Views/Events/EmbeddedEventView.swift | 1+
Adamus/Views/Events/EventMenu.swift | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Events/SelectedEventView.swift | 1+
Adamus/Views/ReportView.swift | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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") + + } + } +}