Browse Source

Add muting and mutelists

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

Changelog-Added: Added ability to block users
zaps
William Casarin 2 years ago
parent
commit
214e45a98b
  1. 20
      damus.xcodeproj/project.pbxproj
  2. 42
      damus/Components/UserView.swift
  3. 94
      damus/ContentView.swift
  4. 38
      damus/Models/Contacts.swift
  5. 40
      damus/Models/HomeModel.swift
  6. 18
      damus/Models/MutelistModel.swift
  7. 2
      damus/Nostr/NostrFilter.swift
  8. 1
      damus/Nostr/NostrKind.swift
  9. 9
      damus/Util/Notifications.swift
  10. 6
      damus/Views/Events/EventMenu.swift
  11. 21
      damus/Views/FollowingView.swift
  12. 67
      damus/Views/Muting/MutelistView.swift
  13. 4
      damus/Views/ProfileView.swift
  14. 22
      damus/Views/SideMenuView.swift

20
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 */,

42
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")
}
}

94
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) {

38
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())

40
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
@ -275,6 +283,10 @@ 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] } ?? [:]
@ -336,6 +348,29 @@ class HomeModel: ObservableObject {
}
}
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
}

18
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]
}
*/

2
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

1
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
}

9
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 {

6
damus/Views/Events/EventMenu.swift

@ -45,6 +45,12 @@ struct EventMenuContext: View {
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)
} label: {

21
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))
}

67
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"])
}
}

4
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()
}

22
damus/Views/SideMenuView.swift

@ -76,22 +76,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")
.font(.title2)
@ -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)

Loading…
Cancel
Save