Merge improved mute functionality from Charlie
This merge adds a bunch of new features from charlie's work on the new mutelist changes: - Muted words - Mute performance optimizations - New mute list UI I needed to make a few changes to fix the tests in this merge. Otherwise it seems to work ok! Thank to Charlie for getting all of this working after many rounds of review! * branch `mute` of https://github.com/damus-io/damus: mute: fix bug with duplicate Indefinite items in MuteDurationMenu mute: fix mute hashtag from search view if no existing mutelist mute: integrate new MutelistManager mute: adding MutelistManager.swift mute: add maybe_get_content function to NdbNote mute: fix bug where mutes can't be added without existing mutelist mute: fix issue with not being able to change mute duration mute: don't mutate string when adding hashtag mute: implement fast MuteItem decoder tags: add u64 decoding function mute: migrating muted_threads to new mute list mute: adding ability to mute hashtag from SearchView mute: updating UI to support new mute list mute: adding filtering support for MuteItem events mute: receiving New Mute List Type mute: migrate Lists.swift to use new MuteItem mute: add new UI views for new mute list mute: adding new structs/enums for new mute list Changelog-Added: Add ability to mute words, add new mutelist interface (Charlie)
This commit is contained in:
@@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||
return contacts.references.contains { ref in
|
||||
switch (ref, follow) {
|
||||
case let (.hashtag(ht), .hashtag(follow_ht)):
|
||||
return ht.string() == follow_ht
|
||||
return ht.hashtag == follow_ht
|
||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||
return pk == follow_pk
|
||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||
|
||||
@@ -13,51 +13,14 @@ class Contacts {
|
||||
private var friend_of_friends: Set<Pubkey> = Set()
|
||||
/// Tracks which friends are friends of a given pubkey.
|
||||
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||
private var muted: Set<Pubkey> = Set()
|
||||
|
||||
let our_pubkey: Pubkey
|
||||
var event: NostrEvent?
|
||||
var mutelist: NostrEvent?
|
||||
|
||||
|
||||
init(our_pubkey: Pubkey) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
func is_muted(_ pk: Pubkey) -> Bool {
|
||||
return muted.contains(pk)
|
||||
}
|
||||
|
||||
func set_mutelist(_ ev: NostrEvent) {
|
||||
let oldlist = self.mutelist
|
||||
self.mutelist = ev
|
||||
|
||||
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
|
||||
let new = Set(ev.referenced_pubkeys)
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
var new_mutes = Set<Pubkey>()
|
||||
var new_unmutes = Set<Pubkey>()
|
||||
|
||||
for d in diff {
|
||||
if new.contains(d) {
|
||||
new_mutes.insert(d)
|
||||
} else {
|
||||
new_unmutes.insert(d)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: set local mutelist here
|
||||
self.muted = Set(ev.referenced_pubkeys)
|
||||
|
||||
if new_mutes.count > 0 {
|
||||
notify(.new_mutes(new_mutes))
|
||||
}
|
||||
|
||||
if new_unmutes.count > 0 {
|
||||
notify(.new_unmutes(new_unmutes))
|
||||
}
|
||||
}
|
||||
|
||||
func remove_friend(_ pubkey: Pubkey) {
|
||||
friends.remove(pubkey)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
|
||||
guard ev.known_kind == .boost else { return true }
|
||||
// This needs to use cached because it can be way too slow otherwise
|
||||
guard let inner_ev = ev.get_cached_inner_event(cache: damus_state.events) else { return true }
|
||||
return should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: inner_ev)
|
||||
return should_show_event(state: damus_state, ev: inner_ev)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class DamusState: HeadlessDamusState {
|
||||
let likes: EventCounter
|
||||
let boosts: EventCounter
|
||||
let contacts: Contacts
|
||||
let mutelist_manager: MutelistManager
|
||||
let profiles: Profiles
|
||||
let dms: DirectMessagesModel
|
||||
let previews: PreviewCache
|
||||
@@ -28,20 +29,20 @@ class DamusState: HeadlessDamusState {
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [String]
|
||||
let replies: ReplyCounter
|
||||
let muted_threads: MutedThreadsManager
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let music: MusicController?
|
||||
let video: VideoController
|
||||
let ndb: Ndb
|
||||
var purple: DamusPurple
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
|
||||
self.pool = pool
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.contacts = contacts
|
||||
self.mutelist_manager = mutelist_manager
|
||||
self.profiles = profiles
|
||||
self.dms = dms
|
||||
self.previews = previews
|
||||
@@ -56,7 +57,6 @@ class DamusState: HeadlessDamusState {
|
||||
self.postbox = postbox
|
||||
self.bootstrap_relays = bootstrap_relays
|
||||
self.replies = replies
|
||||
self.muted_threads = muted_threads
|
||||
self.wallet = wallet
|
||||
self.nav = nav
|
||||
self.music = music
|
||||
@@ -110,6 +110,7 @@ class DamusState: HeadlessDamusState {
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
contacts: Contacts(our_pubkey: empty_pub),
|
||||
mutelist_manager: MutelistManager(),
|
||||
profiles: Profiles(ndb: .empty),
|
||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||
previews: PreviewCache(),
|
||||
@@ -124,7 +125,6 @@ class DamusState: HeadlessDamusState {
|
||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||
bootstrap_relays: [],
|
||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||
muted_threads: MutedThreadsManager(keypair: kp),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
music: nil,
|
||||
|
||||
@@ -15,7 +15,7 @@ protocol HeadlessDamusState {
|
||||
var ndb: Ndb { get }
|
||||
var settings: UserSettingsStore { get }
|
||||
var contacts: Contacts { get }
|
||||
var muted_threads: MutedThreadsManager { get }
|
||||
var mutelist_manager: MutelistManager { get }
|
||||
var keypair: Keypair { get }
|
||||
var profiles: Profiles { get }
|
||||
var zaps: Zaps { get }
|
||||
|
||||
@@ -157,8 +157,10 @@ class HomeModel {
|
||||
case .metadata:
|
||||
// profile metadata processing is handled by nostrdb
|
||||
break
|
||||
case .list:
|
||||
handle_list_event(ev)
|
||||
case .list_deprecated:
|
||||
handle_old_list_event(ev)
|
||||
case .mute_list:
|
||||
handle_mute_list_event(ev)
|
||||
case .boost:
|
||||
handle_boost_event(sub_id: sub_id, ev)
|
||||
case .like:
|
||||
@@ -242,7 +244,7 @@ class HomeModel {
|
||||
process_zap_event(state: damus_state, ev: ev) { zapres in
|
||||
guard case .done(let zap) = zapres,
|
||||
zap.target.pubkey == self.damus_state.keypair.pubkey,
|
||||
should_show_event(keypair: self.damus_state.keypair, hellthreads: self.damus_state.muted_threads, contacts: self.damus_state.contacts, ev: zap.request.ev) else {
|
||||
should_show_event(state: self.damus_state, ev: zap.request.ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,11 +278,11 @@ class HomeModel {
|
||||
|
||||
func filter_events() {
|
||||
events.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
||||
}
|
||||
|
||||
self.dms.dms = dms.dms.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
||||
}
|
||||
|
||||
notifications.filter { ev in
|
||||
@@ -288,7 +290,8 @@ class HomeModel {
|
||||
return false
|
||||
}
|
||||
|
||||
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair)
|
||||
let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
|
||||
return !event_muted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,10 +464,13 @@ class HomeModel {
|
||||
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter(kinds: [.list])
|
||||
our_blocklist_filter.parameter = ["mute"]
|
||||
var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated])
|
||||
our_old_blocklist_filter.parameter = ["mute"]
|
||||
our_old_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter(kinds: [.mute_list])
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
|
||||
var dms_filter = NostrFilter(kinds: [.dm])
|
||||
|
||||
var our_dms_filter = NostrFilter(kinds: [.dm])
|
||||
@@ -488,7 +494,7 @@ class HomeModel {
|
||||
notifications_filter.limit = 500
|
||||
|
||||
var notifications_filters = [notifications_filter]
|
||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
|
||||
var dms_filters = [dms_filter, our_dms_filter]
|
||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||
|
||||
@@ -557,13 +563,32 @@ class HomeModel {
|
||||
pool.send(.subscribe(sub), to: relay_ids)
|
||||
}
|
||||
|
||||
func handle_list_event(_ ev: NostrEvent) {
|
||||
func handle_mute_list_event(_ ev: NostrEvent) {
|
||||
// we only care about our mutelist
|
||||
guard ev.pubkey == damus_state.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// we only care about the most recent mutelist
|
||||
if let mutelist = damus_state.mutelist_manager.event {
|
||||
if ev.created_at <= mutelist.created_at {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
damus_state.mutelist_manager.set_mutelist(ev)
|
||||
|
||||
migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
|
||||
}
|
||||
|
||||
func handle_old_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 {
|
||||
// we only care about the most recent mutelist
|
||||
if let mutelist = damus_state.mutelist_manager.event {
|
||||
if ev.created_at <= mutelist.created_at {
|
||||
return
|
||||
}
|
||||
@@ -573,7 +598,9 @@ class HomeModel {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.contacts.set_mutelist(ev)
|
||||
damus_state.mutelist_manager.set_mutelist(ev)
|
||||
|
||||
migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
|
||||
}
|
||||
|
||||
func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? {
|
||||
@@ -589,7 +616,7 @@ class HomeModel {
|
||||
// don't show notifications from ourselves
|
||||
guard ev.pubkey != damus_state.pubkey,
|
||||
event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey),
|
||||
should_show_event(keypair: self.damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
should_show_event(state: damus_state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -627,7 +654,7 @@ class HomeModel {
|
||||
|
||||
|
||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(state: damus_state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -656,7 +683,7 @@ class HomeModel {
|
||||
}
|
||||
|
||||
func handle_dm(_ ev: NostrEvent) {
|
||||
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(state: damus_state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1063,19 +1090,14 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
|
||||
|
||||
func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
return should_show_event(
|
||||
keypair: damus_state.keypair,
|
||||
hellthreads: damus_state.muted_threads,
|
||||
contacts: damus_state.contacts,
|
||||
state: damus_state,
|
||||
ev: event
|
||||
)
|
||||
}
|
||||
|
||||
func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
if contacts.is_muted(ev.pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if hellthreads.isMutedThread(ev, keypair: keypair) {
|
||||
func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||
let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair)
|
||||
if event_muted {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
202
damus/Models/MuteItem.swift
Normal file
202
damus/Models/MuteItem.swift
Normal file
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// MuteItem.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/13/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents an item that is muted.
|
||||
enum MuteItem: Hashable, Equatable {
|
||||
/// A user that is muted.
|
||||
///
|
||||
/// The associated type is the ``Pubkey`` that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case user(Pubkey, Date?)
|
||||
|
||||
/// A hashtag that is muted.
|
||||
///
|
||||
/// The associated type is the hashtag string that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case hashtag(Hashtag, Date?)
|
||||
|
||||
/// A word/phrase that is muted.
|
||||
///
|
||||
/// The associated type is the word/phrase that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case word(String, Date?)
|
||||
|
||||
/// A thread that is muted.
|
||||
///
|
||||
/// The associated type is the `id` of the note that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires.
|
||||
case thread(NoteId, Date?)
|
||||
|
||||
func is_expired() -> Bool {
|
||||
switch self {
|
||||
case .user(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .hashtag(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .word(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
case .thread(_, let expiration_date):
|
||||
return expiration_date ?? .distantFuture < Date()
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: MuteItem, rhs: MuteItem) -> Bool {
|
||||
// lhs is the item we want to check (ie. the item the user is attempting to display)
|
||||
// rhs is the item we want to check against (ie. the item in the mute list)
|
||||
|
||||
switch (lhs, rhs) {
|
||||
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)):
|
||||
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
||||
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
|
||||
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
||||
case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
|
||||
return lhs_word == rhs_word && !rhs.is_expired()
|
||||
case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
|
||||
return lhs_thread == rhs_thread && !rhs.is_expired()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var refTags: [String] {
|
||||
switch self {
|
||||
case .user(let pubkey, _):
|
||||
return RefId.pubkey(pubkey).tag
|
||||
case .hashtag(let hashtag, _):
|
||||
return RefId.hashtag(hashtag).tag
|
||||
case .word(let string, _):
|
||||
return ["word", string]
|
||||
case .thread(let noteId, _):
|
||||
return RefId.event(noteId).tag
|
||||
}
|
||||
}
|
||||
|
||||
var tag: [String] {
|
||||
var tag = self.refTags
|
||||
|
||||
switch self {
|
||||
case .user(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .hashtag(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .word(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
case .thread(_, let date):
|
||||
if let date {
|
||||
tag.append("\(Int(date.timeIntervalSince1970))")
|
||||
}
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .user:
|
||||
return "user"
|
||||
case .hashtag:
|
||||
return "hashtag"
|
||||
case .word:
|
||||
return "word"
|
||||
case .thread:
|
||||
return "thread"
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ tag: [String]) {
|
||||
guard let tag_id = tag.first else { return nil }
|
||||
guard let tag_content = tag[safe: 1] else { return nil }
|
||||
|
||||
let tag_expiration_date: Date? = {
|
||||
if let tag_expiration_string: String = tag[safe: 2],
|
||||
let tag_expiration_number: TimeInterval = Double(tag_expiration_string) {
|
||||
return Date(timeIntervalSince1970: tag_expiration_number)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
switch tag_id {
|
||||
case "p":
|
||||
guard let pubkey = Pubkey(hex: tag_content) else { return nil }
|
||||
self = MuteItem.user(pubkey, tag_expiration_date)
|
||||
break
|
||||
case "t":
|
||||
self = MuteItem.hashtag(Hashtag(hashtag: tag_content), tag_expiration_date)
|
||||
break
|
||||
case "word":
|
||||
self = MuteItem.word(tag_content, tag_expiration_date)
|
||||
break
|
||||
case "thread":
|
||||
guard let note_id = NoteId(hex: tag_content) else { return nil }
|
||||
self = MuteItem.thread(note_id, tag_expiration_date)
|
||||
break
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// - MARK: TagConvertible
|
||||
extension MuteItem: TagConvertible {
|
||||
enum MuteKeys: String {
|
||||
case p, t, word, e
|
||||
|
||||
init?(tag: NdbTagElem) {
|
||||
let len = tag.count
|
||||
if len == 1 {
|
||||
switch tag.single_char {
|
||||
case "p": self = .p
|
||||
case "t": self = .t
|
||||
case "e": self = .e
|
||||
default: return nil
|
||||
}
|
||||
} else if len == 4 && tag.matches_str("word", tag_len: 4) {
|
||||
self = .word
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { self.rawValue }
|
||||
}
|
||||
|
||||
static func from_tag(tag: TagSequence) -> MuteItem? {
|
||||
guard tag.count >= 2 else { return nil }
|
||||
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard let t0 = i.next(),
|
||||
let mkey = MuteKeys(tag: t0),
|
||||
let t1 = i.next()
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var expiry: Date? = nil
|
||||
if let expiry_str = i.next(), let ts = expiry_str.u64() {
|
||||
expiry = Date(timeIntervalSince1970: Double(ts))
|
||||
}
|
||||
|
||||
switch mkey {
|
||||
case .p:
|
||||
return t1.id().map({ .user(Pubkey($0), expiry) })
|
||||
case .t:
|
||||
return .hashtag(Hashtag(hashtag: t1.string()), expiry)
|
||||
case .word:
|
||||
return .word(t1.string(), expiry)
|
||||
case .e:
|
||||
guard let id = t1.id() else { return nil }
|
||||
return .thread(NoteId(id), expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
|
||||
pk_setting_key(pubkey, key: "muted_threads")
|
||||
}
|
||||
|
||||
func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||
func loadOldMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
return xs.reduce(into: [NoteId]()) { ids, k in
|
||||
@@ -20,56 +20,20 @@ func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
||||
}
|
||||
}
|
||||
|
||||
func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool {
|
||||
let uniqueMutedThreads = Array(Set(value))
|
||||
|
||||
if uniqueMutedThreads != currentValue {
|
||||
let ids = uniqueMutedThreads.map { note_id in return note_id.hex() }
|
||||
UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class MutedThreadsManager: ObservableObject {
|
||||
|
||||
private let keypair: Keypair
|
||||
|
||||
private var _mutedThreadsSet: Set<NoteId>
|
||||
private var _mutedThreads: [NoteId]
|
||||
var mutedThreads: [NoteId] {
|
||||
get {
|
||||
return _mutedThreads
|
||||
}
|
||||
set {
|
||||
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
|
||||
self._mutedThreads = newValue
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(keypair: Keypair) {
|
||||
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
|
||||
self._mutedThreadsSet = Set(_mutedThreads)
|
||||
self.keypair = keypair
|
||||
}
|
||||
|
||||
func isMutedThread(_ ev: NostrEvent, keypair: Keypair) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(keypair: keypair))
|
||||
}
|
||||
|
||||
func updateMutedThread(_ ev: NostrEvent) {
|
||||
let threadId = ev.thread_id(keypair: keypair)
|
||||
if isMutedThread(ev, keypair: keypair) {
|
||||
mutedThreads = mutedThreads.filter { $0 != threadId }
|
||||
_mutedThreadsSet.remove(threadId)
|
||||
notify(.unmute_thread(ev))
|
||||
} else {
|
||||
mutedThreads.append(threadId)
|
||||
_mutedThreadsSet.insert(threadId)
|
||||
notify(.mute_thread(ev))
|
||||
}
|
||||
}
|
||||
// We need to still use it since existing users might have their muted threads stored in UserDefaults
|
||||
// So now all it's doing is moving a users muted threads to the new kind:10000 system
|
||||
// It should not be used for any purpose beyond that
|
||||
func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) {
|
||||
// Ensure that keypair is fullkeypair
|
||||
guard let fullKeypair = keypair.to_full() else { return }
|
||||
// Load existing muted threads
|
||||
let mutedThreads = loadOldMutedThreads(pubkey: fullKeypair.pubkey)
|
||||
guard !mutedThreads.isEmpty else { return }
|
||||
// Set new muted system for those existing threads
|
||||
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||
damus_state.postbox.send(new_mutelist_event)
|
||||
// Set existing muted threads to an empty array
|
||||
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||
}
|
||||
|
||||
162
damus/Models/MutelistManager.swift
Normal file
162
damus/Models/MutelistManager.swift
Normal file
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// MutelistManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 1/28/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MutelistManager {
|
||||
private(set) var event: NostrEvent? = nil
|
||||
|
||||
var users: Set<MuteItem> = []
|
||||
var hashtags: Set<MuteItem> = []
|
||||
var threads: Set<MuteItem> = []
|
||||
var words: Set<MuteItem> = []
|
||||
|
||||
func refresh_sets() {
|
||||
guard let referenced_mute_items = event?.referenced_mute_items else { return }
|
||||
|
||||
var new_users: Set<MuteItem> = []
|
||||
var new_hashtags: Set<MuteItem> = []
|
||||
var new_threads: Set<MuteItem> = []
|
||||
var new_words: Set<MuteItem> = []
|
||||
|
||||
for mute_item in referenced_mute_items {
|
||||
switch mute_item {
|
||||
case .user:
|
||||
new_users.insert(mute_item)
|
||||
case .hashtag:
|
||||
new_hashtags.insert(mute_item)
|
||||
case .word:
|
||||
new_words.insert(mute_item)
|
||||
case .thread:
|
||||
new_threads.insert(mute_item)
|
||||
}
|
||||
}
|
||||
|
||||
users = new_users
|
||||
hashtags = new_hashtags
|
||||
threads = new_threads
|
||||
words = new_words
|
||||
}
|
||||
|
||||
func is_muted(_ item: MuteItem) -> Bool {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
return users.contains(item)
|
||||
case .hashtag(_, _):
|
||||
return hashtags.contains(item)
|
||||
case .word(_, _):
|
||||
return words.contains(item)
|
||||
case .thread(_, _):
|
||||
return threads.contains(item)
|
||||
}
|
||||
}
|
||||
|
||||
func is_event_muted(_ ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||
return event_muted_reason(ev, keypair: keypair) != nil
|
||||
}
|
||||
|
||||
func set_mutelist(_ ev: NostrEvent) {
|
||||
let oldlist = self.event
|
||||
self.event = ev
|
||||
|
||||
let old: Set<MuteItem> = oldlist?.mute_list ?? Set<MuteItem>()
|
||||
let new: Set<MuteItem> = ev.mute_list ?? Set<MuteItem>()
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
var new_mutes = Set<MuteItem>()
|
||||
var new_unmutes = Set<MuteItem>()
|
||||
|
||||
for d in diff {
|
||||
if new.contains(d) {
|
||||
add_mute_item(d)
|
||||
new_mutes.insert(d)
|
||||
} else {
|
||||
remove_mute_item(d)
|
||||
new_unmutes.insert(d)
|
||||
}
|
||||
}
|
||||
|
||||
if new_mutes.count > 0 {
|
||||
notify(.new_mutes(new_mutes))
|
||||
}
|
||||
|
||||
if new_unmutes.count > 0 {
|
||||
notify(.new_unmutes(new_unmutes))
|
||||
}
|
||||
}
|
||||
|
||||
private func add_mute_item(_ item: MuteItem) {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
users.insert(item)
|
||||
case .hashtag(_, _):
|
||||
hashtags.insert(item)
|
||||
case .word(_, _):
|
||||
words.insert(item)
|
||||
case .thread(_, _):
|
||||
threads.insert(item)
|
||||
}
|
||||
}
|
||||
|
||||
private func remove_mute_item(_ item: MuteItem) {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
users.remove(item)
|
||||
case .hashtag(_, _):
|
||||
hashtags.remove(item)
|
||||
case .word(_, _):
|
||||
words.remove(item)
|
||||
case .thread(_, _):
|
||||
threads.remove(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if an event is muted given a collection of ``MutedItem``.
|
||||
///
|
||||
/// - Parameter ev: The ``NostrEvent`` that you want to check the muted reason for.
|
||||
/// - Returns: The ``MuteItem`` that matched the event. Or `nil` if the event is not muted.
|
||||
func event_muted_reason(_ ev: NostrEvent, keypair: Keypair? = nil) -> MuteItem? {
|
||||
// Events from the current user should not be muted.
|
||||
guard keypair?.pubkey != ev.pubkey else { return nil }
|
||||
|
||||
// Check if user is muted
|
||||
let check_user_item = MuteItem.user(ev.pubkey, nil)
|
||||
if users.contains(check_user_item) {
|
||||
return check_user_item
|
||||
}
|
||||
|
||||
// Check if hashtag is muted
|
||||
for hashtag in ev.referenced_hashtags {
|
||||
let check_hashtag_item = MuteItem.hashtag(hashtag, nil)
|
||||
if hashtags.contains(check_hashtag_item) {
|
||||
return check_hashtag_item
|
||||
}
|
||||
}
|
||||
|
||||
// Check if thread is muted
|
||||
for thread_id in ev.referenced_ids {
|
||||
let check_thread_item = MuteItem.thread(thread_id, nil)
|
||||
if threads.contains(check_thread_item) {
|
||||
return check_thread_item
|
||||
}
|
||||
}
|
||||
|
||||
// Check if word is muted
|
||||
if let keypair, let content: String = ev.maybe_get_content(keypair)?.lowercased() {
|
||||
for word in words {
|
||||
if case .word(let string, _) = word {
|
||||
if content.contains(string.lowercased()) {
|
||||
return word
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -36,16 +36,11 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show notifications from muted threads.
|
||||
if state.muted_threads.isMutedThread(ev, keypair: state.keypair) {
|
||||
// Don't show notifications that match mute list.
|
||||
if state.mutelist_manager.is_event_muted(ev, keypair: state.keypair) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show notifications from muted users
|
||||
if state.contacts.is_muted(ev.pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Don't show notifications for old events
|
||||
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
|
||||
return false
|
||||
|
||||
@@ -35,7 +35,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
events.filter { should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: $0) }
|
||||
events.filter { should_show_event(state: damus_state, ev: $0) }
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject {
|
||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair)
|
||||
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair)
|
||||
{
|
||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
|
||||
@@ -28,7 +28,7 @@ class SearchModel: ObservableObject {
|
||||
|
||||
func filter_muted() {
|
||||
self.events.filter {
|
||||
should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: $0)
|
||||
should_show_event(state: state, ev: $0)
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class SearchModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: ev) else {
|
||||
guard should_show_event(state: state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user