damus

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

commit 214e45a98b85208375de68ae5da4fee15679d1b7
parent 2a8b9f75c13d4b70cf77adc5d45e124b2749ad96
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 25 Jan 2023 12:50:04 -0800

Add muting and mutelists

- Filter muted posts from feed on mute
- List muted users in sidebar

Changelog-Added: Added ability to block users

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 20++++++++++++++++++++
Adamus/Components/UserView.swift | 42++++++++++++++++++++++++++++++++++++++++++
Mdamus/ContentView.swift | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/Contacts.swift | 38++++++++++++++++++++++++++++++++++++++
Mdamus/Models/HomeModel.swift | 44+++++++++++++++++++++++++++++++++++++++++---
Adamus/Models/MutelistModel.swift | 18++++++++++++++++++
Mdamus/Nostr/NostrFilter.swift | 2++
Mdamus/Nostr/NostrKind.swift | 1+
Mdamus/Util/Notifications.swift | 9+++++++++
Mdamus/Views/Events/EventMenu.swift | 6++++++
Mdamus/Views/FollowingView.swift | 21+--------------------
Adamus/Views/Muting/MutelistView.swift | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/ProfileView.swift | 4++++
Mdamus/Views/SideMenuView.swift | 22++++++----------------
14 files changed, 349 insertions(+), 39 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -159,6 +159,9 @@ 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD529817F5B00D66079 /* ReportView.swift */; }; 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD72981980C00D66079 /* Lists.swift */; }; 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDB2981A19E00D66079 /* ListTests.swift */; }; + 4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */; }; + 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE02981A83900D66079 /* MutelistView.swift */; }; + 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE22981BC7D00D66079 /* UserView.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 */; }; @@ -395,6 +398,9 @@ 4CF0ABD529817F5B00D66079 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; }; 4CF0ABD72981980C00D66079 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = "<group>"; }; 4CF0ABDB2981A19E00D66079 /* ListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = "<group>"; }; + 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistModel.swift; sourceTree = "<group>"; }; + 4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; }; + 4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.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>"; }; @@ -538,6 +544,7 @@ 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, 7C45AE70297353390031D7BC /* KFImageModel.swift */, 4CF0ABD32980996B00D66079 /* Report.swift */, + 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -545,6 +552,7 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4CF0ABDF2981A83000D66079 /* Muting */, 4CC7AAEE297F11B300430951 /* Events */, 4CB88394296F7F8100DC99E7 /* Reactions */, 4CB88387296AF97C00DC99E7 /* ActionBar */, @@ -686,6 +694,7 @@ 4CB8838C296F710400DC99E7 /* Reposted.swift */, 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */, 4CC7AAEC297F0B9E00430951 /* Highlight.swift */, + 4CF0ABE22981BC7D00D66079 /* UserView.swift */, ); path = Components; sourceTree = "<group>"; @@ -775,6 +784,14 @@ name = Frameworks; sourceTree = "<group>"; }; + 4CF0ABDF2981A83000D66079 /* Muting */ = { + isa = PBXGroup; + children = ( + 4CF0ABE02981A83900D66079 /* MutelistView.swift */, + ); + path = Muting; + sourceTree = "<group>"; + }; F7F0BA23297892AE009531F3 /* Modifiers */ = { isa = PBXGroup; children = ( @@ -969,6 +986,7 @@ 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */, F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, + 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, @@ -988,6 +1006,7 @@ 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, + 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */, @@ -1060,6 +1079,7 @@ 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, + 4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */, 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, diff --git a/damus/Components/UserView.swift b/damus/Components/UserView.swift @@ -0,0 +1,42 @@ +// +// UserView.swift +// damus +// +// Created by William Casarin on 2023-01-25. +// + +import SwiftUI + +struct UserView: View { + let damus_state: DamusState + let pubkey: String + + var body: some View { + let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) + let followers = FollowersModel(damus_state: damus_state, target: pubkey) + let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers) + + NavigationLink(destination: pv) { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) + + VStack(alignment: .leading) { + let profile = damus_state.profiles.lookup(id: pubkey) + ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false) + if let about = profile?.about { + Text(about) + .lineLimit(3) + .font(.footnote) + } + } + + Spacer() + } + .buttonStyle(PlainButtonStyle()) + } +} + +struct UserView_Previews: PreviewProvider { + static var previews: some View { + UserView(damus_state: test_damus_state(), pubkey: "pk") + } +} diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -81,6 +81,10 @@ struct ContentView: View { @State var profile_open: Bool = false @State var thread_open: Bool = false @State var search_open: Bool = false + @State var blocking: String? = nil + @State var confirm_block: Bool = false + @State var user_blocked_confirm: Bool = false + @State var confirm_overwrite_mutelist: Bool = false @State var filter_state : FilterState = .posts_and_replies @State private var isSideBarOpened = false @StateObject var home: HomeModel = HomeModel() @@ -348,6 +352,11 @@ struct ContentView: View { let target = notif.object as! ReportTarget self.active_sheet = .report(target) } + .onReceive(handle_notify(.block)) { notif in + let pubkey = notif.object as! String + self.blocking = pubkey + self.confirm_block = true + } .onReceive(handle_notify(.broadcast_event)) { obj in let ev = obj.object as! NostrEvent self.damus_state?.pool.send(.event(ev)) @@ -422,6 +431,91 @@ struct ContentView: View { .onReceive(timer) { n in self.damus_state?.pool.connect_to_disconnected() } + .onReceive(handle_notify(.new_mutes)) { notif in + home.filter_muted() + } + .alert("User blocked", isPresented: $user_blocked_confirm, actions: { + Button("Thanks!") { + user_blocked_confirm = false + } + }, message: { + if let pubkey = self.blocking { + let profile = damus_state!.profiles.lookup(id: pubkey) + let name = Profile.displayName(profile: profile, pubkey: pubkey) + Text("\(name) has been blocked") + } else { + Text("User has been blocked") + } + }) + .alert("Create new mutelist", isPresented: $confirm_overwrite_mutelist, actions: { + Button("Yes, Overwrite") { + guard let ds = damus_state else { + return + } + + guard let keypair = ds.keypair.to_full() else { + return + } + + guard let pubkey = blocking else { + return + } + + guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else { + return + } + + damus_state?.contacts.set_mutelist(mutelist) + ds.pool.send(.event(mutelist)) + + confirm_overwrite_mutelist = false + confirm_block = false + user_blocked_confirm = true + } + + Button("Cancel") { + confirm_overwrite_mutelist = false + confirm_block = false + } + }, message: { + Text("No block list found, create a new one? This will overwrite any previous block lists.") + }) + .alert("Block User", isPresented: $confirm_block, actions: { + Button("Block") { + guard let ds = damus_state else { + return + } + + if ds.contacts.mutelist == nil { + confirm_overwrite_mutelist = true + } else { + guard let keypair = ds.keypair.to_full() else { + return + } + guard let pubkey = blocking else { + return + } + + guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else { + return + } + damus_state?.contacts.set_mutelist(ev) + ds.pool.send(.event(ev)) + } + } + + Button("Cancel") { + confirm_block = false + } + }, message: { + if let pubkey = blocking { + let profile = damus_state?.profiles.lookup(id: pubkey) + let name = Profile.displayName(profile: profile, pubkey: pubkey) + Text("Block \(name)?") + } else { + Text("Could not find user to block...") + } + }) } func switch_timeline(_ timeline: Timeline) { diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift @@ -11,13 +11,51 @@ import Foundation class Contacts { private var friends: Set<String> = Set() private var friend_of_friends: Set<String> = Set() + private var muted: Set<String> = Set() + let our_pubkey: String var event: NostrEvent? + var mutelist: NostrEvent? init(our_pubkey: String) { self.our_pubkey = our_pubkey } + func is_muted(_ pk: String) -> Bool { + return muted.contains(pk) + } + + func set_mutelist(_ ev: NostrEvent) { + let oldlist = self.mutelist + self.mutelist = ev + + let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? []) + let new = Set(ev.referenced_pubkeys.map({ $0.ref_id })) + let diff = old.symmetricDifference(new) + + var new_mutes = Array<String>() + var new_unmutes = Array<String>() + + for d in diff { + if new.contains(d) { + new_mutes.append(d) + } else { + new_unmutes.append(d) + } + } + + // TODO: set local mutelist here + self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id })) + + if new_mutes.count > 0 { + notify(.new_mutes, new_mutes) + } + + if new_unmutes.count > 0 { + notify(.new_unmutes, new_unmutes) + } + } + func get_friendosphere() -> [String] { var fs = get_friend_list() fs.append(contentsOf: get_friend_of_friend_list()) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -98,6 +98,8 @@ class HomeModel: ObservableObject { handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) case .metadata: handle_metadata_event(ev) + case .list: + handle_list_event(ev) case .boost: handle_boost_event(sub_id: sub_id, ev) case .like: @@ -124,6 +126,12 @@ class HomeModel: ObservableObject { func handle_channel_meta(_ ev: NostrEvent) { } + func filter_muted() { + self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) } + self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) } + self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) } + } + func handle_delete_event(_ ev: NostrEvent) { guard ev.is_valid else { return @@ -274,7 +282,11 @@ class HomeModel: ObservableObject { var our_contacts_filter = NostrFilter.filter_kinds([3, 0]) our_contacts_filter.authors = [damus_state.pubkey] - + + var our_blocklist_filter = NostrFilter.filter_kinds([30000]) + our_blocklist_filter.parameter = "mute" + our_blocklist_filter.authors = [damus_state.pubkey] + var dms_filter = NostrFilter.filter_kinds([ NostrKind.dm.rawValue, ]) @@ -311,7 +323,7 @@ class HomeModel: ObservableObject { var home_filters = [home_filter] var notifications_filters = [notifications_filter] - var contacts_filters = [contacts_filter, our_contacts_filter] + var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter] var dms_filters = [dms_filter, our_dms_filter] let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:] @@ -335,7 +347,30 @@ class HomeModel: ObservableObject { pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid))) } } - + + func handle_list_event(_ ev: NostrEvent) { + // we only care about our lists + guard ev.pubkey == damus_state.pubkey else { + return + } + + if let mutelist = damus_state.contacts.mutelist { + if ev.created_at <= mutelist.created_at { + return + } + } + + guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else { + return + } + + guard name.ref_id == "mute" else { + return + } + + damus_state.contacts.set_mutelist(ev) + } + func handle_metadata_event(_ ev: NostrEvent) { process_metadata_event(profiles: damus_state.profiles, ev: ev) } @@ -376,6 +411,9 @@ class HomeModel: ObservableObject { } func should_hide_event(_ ev: NostrEvent) -> Bool { + if damus_state.contacts.is_muted(ev.pubkey) { + return true + } return !ev.should_show_event } diff --git a/damus/Models/MutelistModel.swift b/damus/Models/MutelistModel.swift @@ -0,0 +1,18 @@ +// +// ListModel.swift +// damus +// +// Created by William Casarin on 2023-01-25. +// + +import Foundation + + +/* + class MutelistModel: ObservableObject { + let contacts: Contacts + + @Published var users: [String] + + } + */ diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift @@ -17,6 +17,7 @@ struct NostrFilter: Codable { var limit: UInt32? var authors: [String]? var hashtag: [String]? = nil + var parameter: String? = nil private enum CodingKeys : String, CodingKey { case ids @@ -24,6 +25,7 @@ struct NostrFilter: Codable { case referenced_ids = "#e" case pubkeys = "#p" case hashtag = "#t" + case parameter = "#d" case since case until case authors diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -19,4 +19,5 @@ enum NostrKind: Int { case channel_create = 40 case channel_meta = 41 case chat = 42 + case list = 30000 } diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift @@ -86,6 +86,15 @@ extension Notification.Name { static var report: Notification.Name { return Notification.Name("report") } + static var block: Notification.Name { + return Notification.Name("block") + } + static var new_mutes: Notification.Name { + return Notification.Name("new_mutes") + } + static var new_unmutes: Notification.Name { + return Notification.Name("new_unmutes") + } } func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift @@ -44,6 +44,12 @@ struct EventMenuContext: View { } label: { Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble") } + + Button { + notify(.block, event.pubkey) + } label: { + Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon") + } Button { NotificationCenter.default.post(name: .broadcast_event, object: event) diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift @@ -15,26 +15,7 @@ struct FollowUserView: View { var body: some View { HStack { - let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state) - let followers = FollowersModel(damus_state: damus_state, target: target.pubkey) - let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers) - - NavigationLink(destination: pv) { - ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) - - VStack(alignment: .leading) { - let profile = damus_state.profiles.lookup(id: target.pubkey) - ProfileName(pubkey: target.pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false) - if let about = profile?.about { - Text(FollowUserView.markdown.process(about)) - .lineLimit(3) - .font(.footnote) - } - } - - Spacer() - } - .buttonStyle(PlainButtonStyle()) + UserView(damus_state: damus_state, pubkey: target.pubkey) FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey)) } diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift @@ -0,0 +1,67 @@ +// +// MutelistView.swift +// damus +// +// Created by William Casarin on 2023-01-25. +// + +import SwiftUI + +struct MutelistView: View { + let damus_state: DamusState + @State var users: [String] + + func RemoveAction(pubkey: String) -> some View { + Button { + guard let mutelist = damus_state.contacts.mutelist else { + return + } + + guard let keypair = damus_state.keypair.to_full() else { + return + } + + guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: pubkey) else { + return + } + + damus_state.contacts.set_mutelist(new_ev) + damus_state.pool.send(.event(new_ev)) + users = get_mutelist_users(new_ev) + } label: { + Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash") + } + .tint(.red) + } + + + var body: some View { + List(users, id: \.self) { pubkey in + UserView(damus_state: damus_state, pubkey: pubkey) + .id(pubkey) + .swipeActions { + RemoveAction(pubkey: pubkey) + } + } + .navigationTitle("Blocked Users") + } +} + + +func get_mutelist_users(_ mlist: NostrEvent?) -> [String] { + guard let mutelist = mlist else { + return [] + } + + return mutelist.tags.reduce(into: Array<String>()) { pks, tag in + if tag.count >= 2 && tag[0] == "p" { + pks.append(tag[1]) + } + } +} + +struct MutelistView_Previews: PreviewProvider { + static var previews: some View { + MutelistView(damus_state: test_damus_state(), users: [test_event.pubkey, test_event.pubkey+"hi"]) + } +} diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -383,6 +383,10 @@ struct ProfileView: View { let target: ReportTarget = .user(profile.pubkey) notify(.report, target) } + + Button("Block") { + notify(.block, profile.pubkey) + } } .ignoresSafeArea() } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift @@ -75,22 +75,6 @@ struct SideMenuView: View { Divider() .padding(.trailing,40) - - /* - HStack(alignment: .bottom) { - Text("69,420") - .foregroundColor(.accentColor) - .font(.largeTitle) - Text("SATS") - .font(.caption) - .padding(.bottom,6) - } - - Divider() - .padding(.trailing,40) - */ - - // THERE IS A LIMIT OF 10 NAVIGATIONLINKS!!! (Consider some in other views) NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) { Label(NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person") @@ -123,6 +107,12 @@ struct SideMenuView: View { }) */ + NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) { + Label(NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon") + .font(.title2) + .foregroundColor(textColor()) + } + NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) { Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear") .font(.title2)