notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 84838478b35af2a91612d069399bfb048d044a8c
parent d5956319660823d86d875f17144a4258c7f4b690
Author: Ken Sedgwick <ken@bonsai.com>
Date:   Tue, 19 Nov 2024 12:59:53 -0800

Skip muted content

Diffstat:
Msrc/accounts/mod.rs | 22++++++++++++++++++----
Msrc/actionbar.rs | 39+++++++++++++++++++++++++++++++--------
Msrc/app.rs | 3+++
Msrc/multi_subscriber.rs | 13+++++++++++--
Msrc/muted.rs | 2++
Msrc/nav.rs | 3+++
Msrc/notes_holder.rs | 15++++++++++-----
Msrc/profile.rs | 6+++++-
Msrc/thread.rs | 2++
Msrc/timeline/mod.rs | 27+++++++++++++++++++++------
Msrc/timeline/route.rs | 19++++++++++++++-----
Msrc/ui/add_column.rs | 1+
Msrc/ui/profile/mod.rs | 12+++++++++---
Msrc/ui/thread.rs | 7++++---
14 files changed, 134 insertions(+), 37 deletions(-)

diff --git a/src/accounts/mod.rs b/src/accounts/mod.rs @@ -1,11 +1,12 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; use url::Url; use uuid::Uuid; use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, RelayPool}; -use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction}; +use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; use crate::{ column::Columns, @@ -121,7 +122,7 @@ pub struct AccountMutedData { filter: Filter, subid: String, sub: Option<Subscription>, - muted: Muted, + muted: Arc<Muted>, } impl AccountMutedData { @@ -160,7 +161,7 @@ impl AccountMutedData { filter, subid, sub: Some(ndbsub), - muted, + muted: Arc::new(muted), } } @@ -440,6 +441,19 @@ impl Accounts { self.key_store.select_key(None); } + pub fn mutefun(&self) -> Box<dyn Fn(&Note) -> bool> { + if let Some(index) = self.currently_selected_account { + if let Some(account) = self.accounts.get(index) { + let pubkey = account.pubkey.bytes(); + if let Some(account_data) = self.account_data.get(pubkey) { + let muted = Arc::clone(&account_data.muted.muted); + return Box::new(move |note: &Note| muted.is_muted(note)); + } + } + } + Box::new(|_: &Note| false) + } + pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { for data in self.account_data.values() { pool.send_to( @@ -510,7 +524,7 @@ impl Accounts { let txn = Transaction::new(ndb).expect("txn"); let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted); - data.muted.muted = muted; + data.muted.muted = Arc::new(muted); changed = true; } } diff --git a/src/actionbar.rs b/src/actionbar.rs @@ -1,5 +1,6 @@ use crate::{ column::Columns, + muted::MuteFun, note::NoteRef, notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, @@ -32,6 +33,7 @@ pub enum NotesHolderResult { /// making sure the thread is up to date. In a sense, it's a model for /// the thread view. We don't have a concept of model/view/controller etc /// in egui, but this is the closest thing to that. +#[allow(clippy::too_many_arguments)] fn open_thread( ndb: &Ndb, txn: &Transaction, @@ -40,11 +42,12 @@ fn open_thread( pool: &mut RelayPool, threads: &mut NotesHolderStorage<Thread>, selected_note: &[u8; 32], + is_muted: &MuteFun, ) -> Option<NotesHolderResult> { router.route_to(Route::thread(NoteId::new(selected_note.to_owned()))); let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); - Thread::open(ndb, note_cache, txn, pool, threads, root_id) + Thread::open(ndb, note_cache, txn, pool, threads, root_id, is_muted) } impl NoteAction { @@ -58,6 +61,7 @@ impl NoteAction { note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, + is_muted: &MuteFun, ) -> Option<NotesHolderResult> { match self { NoteAction::Reply(note_id) => { @@ -65,13 +69,28 @@ impl NoteAction { None } - NoteAction::OpenThread(note_id) => { - open_thread(ndb, txn, router, note_cache, pool, threads, note_id.bytes()) - } + NoteAction::OpenThread(note_id) => open_thread( + ndb, + txn, + router, + note_cache, + pool, + threads, + note_id.bytes(), + is_muted, + ), NoteAction::OpenProfile(pubkey) => { router.route_to(Route::profile(pubkey)); - Profile::open(ndb, note_cache, txn, pool, profiles, pubkey.bytes()) + Profile::open( + ndb, + note_cache, + txn, + pool, + profiles, + pubkey.bytes(), + is_muted, + ) } NoteAction::Quote(note_id) => { @@ -93,10 +112,13 @@ impl NoteAction { note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, + is_muted: &MuteFun, ) { let router = columns.column_mut(col).router_mut(); - if let Some(br) = self.execute(ndb, router, threads, profiles, note_cache, pool, txn) { - br.process(ndb, note_cache, txn, threads); + if let Some(br) = self.execute( + ndb, router, threads, profiles, note_cache, pool, txn, is_muted, + ) { + br.process(ndb, note_cache, txn, threads, is_muted); } } } @@ -112,12 +134,13 @@ impl NotesHolderResult { note_cache: &mut NoteCache, txn: &Transaction, storage: &mut NotesHolderStorage<N>, + is_muted: &MuteFun, ) { match self { // update the thread for next render if we have new notes NotesHolderResult::NewNotes(new_notes) => { let holder = storage - .notes_holder_mutated(ndb, note_cache, txn, &new_notes.id) + .notes_holder_mutated(ndb, note_cache, txn, &new_notes.id, is_muted) .get_ptr(); new_notes.process(holder); } diff --git a/src/app.rs b/src/app.rs @@ -147,6 +147,7 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { &mut damus.pool, &mut damus.note_cache, timeline, + &damus.accounts.mutefun(), ) }; @@ -160,6 +161,7 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { &txn, &mut damus.unknown_ids, &mut damus.note_cache, + &damus.accounts.mutefun(), ) { error!("poll_notes_into_view: {err}"); } @@ -208,6 +210,7 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { &damus.ndb, &mut damus.note_cache, &mut damus.columns, + &damus.accounts.mutefun(), ) { warn!("update_damus init: {err}"); } diff --git a/src/multi_subscriber.rs b/src/multi_subscriber.rs @@ -3,7 +3,7 @@ use nostrdb::{Ndb, Note, Transaction}; use tracing::{debug, error, info}; use uuid::Uuid; -use crate::{filter::UnifiedSubscription, note::NoteRef, Error}; +use crate::{filter::UnifiedSubscription, muted::MuteFun, note::NoteRef, Error}; pub struct MultiSubscriber { filters: Vec<Filter>, @@ -105,7 +105,12 @@ impl MultiSubscriber { } } - pub fn poll_for_notes(&mut self, ndb: &Ndb, txn: &Transaction) -> Result<Vec<NoteRef>, Error> { + pub fn poll_for_notes( + &mut self, + ndb: &Ndb, + txn: &Transaction, + is_muted: &MuteFun, + ) -> Result<Vec<NoteRef>, Error> { let sub = self.sub.as_ref().ok_or(Error::no_active_sub())?; let new_note_keys = ndb.poll_for_notes(sub.local, 500); @@ -123,6 +128,10 @@ impl MultiSubscriber { continue; }; + if is_muted(&note) { + continue; + } + notes.push(note); } diff --git a/src/muted.rs b/src/muted.rs @@ -3,6 +3,8 @@ use std::collections::BTreeSet; use tracing::debug; +pub type MuteFun = dyn Fn(&Note) -> bool; + #[derive(Default)] pub struct Muted { // TODO - implement private mutes diff --git a/src/nav.rs b/src/nav.rs @@ -85,6 +85,7 @@ impl RenderNavResponse { &mut app.note_cache, &mut app.pool, &txn, + &app.accounts.mutefun(), ); } } @@ -109,6 +110,7 @@ impl RenderNavResponse { &mut app.threads, &mut app.pool, root_id, + &app.accounts.mutefun(), ); } @@ -120,6 +122,7 @@ impl RenderNavResponse { &mut app.profiles, &mut app.pool, pubkey.bytes(), + &app.accounts.mutefun(), ); } col_changed = true; diff --git a/src/notes_holder.rs b/src/notes_holder.rs @@ -5,7 +5,7 @@ use nostrdb::{Ndb, Transaction}; use tracing::{debug, info, warn}; use crate::{ - actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, note::NoteRef, + actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, muted::MuteFun, note::NoteRef, notecache::NoteCache, timeline::TimelineTab, unknowns::NoteRefsUnkIdAction, Error, Result, }; @@ -55,6 +55,7 @@ impl<M: NotesHolder> NotesHolderStorage<M> { note_cache: &mut NoteCache, txn: &Transaction, id: &[u8; 32], + is_muted: &MuteFun, ) -> Vitality<'a, M> { // we can't use the naive hashmap entry API here because lookups // require a copy, wait until we have a raw entry api. We could @@ -88,7 +89,7 @@ impl<M: NotesHolder> NotesHolderStorage<M> { self.id_to_object.insert( id.to_owned(), - M::new_notes_holder(txn, ndb, note_cache, id, M::filters(id), notes), + M::new_notes_holder(txn, ndb, note_cache, id, M::filters(id), notes, is_muted), ); Vitality::Fresh(self.id_to_object.get_mut(id).unwrap()) } @@ -107,6 +108,7 @@ pub trait NotesHolder { id: &[u8; 32], filters: Vec<Filter>, notes: Vec<NoteRef>, + is_muted: &MuteFun, ) -> Self; #[must_use = "process_action must be handled in the Ok(action) case"] @@ -114,10 +116,11 @@ pub trait NotesHolder { &mut self, txn: &Transaction, ndb: &Ndb, + is_muted: &MuteFun, ) -> Result<NoteRefsUnkIdAction> { if let Some(multi_subscriber) = self.get_multi_subscriber() { let reversed = true; - let note_refs: Vec<NoteRef> = multi_subscriber.poll_for_notes(ndb, txn)?; + let note_refs: Vec<NoteRef> = multi_subscriber.poll_for_notes(ndb, txn, is_muted)?; self.get_view().insert(&note_refs, reversed); Ok(NoteRefsUnkIdAction::new(note_refs)) } else { @@ -156,9 +159,10 @@ pub trait NotesHolder { notes_holder_storage: &mut NotesHolderStorage<M>, pool: &mut RelayPool, id: &[u8; 32], + is_muted: &MuteFun, ) { let notes_holder = notes_holder_storage - .notes_holder_mutated(ndb, note_cache, txn, id) + .notes_holder_mutated(ndb, note_cache, txn, id, is_muted) .get_ptr(); if let Some(multi_subscriber) = notes_holder.get_multi_subscriber() { @@ -173,8 +177,9 @@ pub trait NotesHolder { pool: &mut RelayPool, storage: &mut NotesHolderStorage<M>, id: &[u8; 32], + is_muted: &MuteFun, ) -> Option<NotesHolderResult> { - let vitality = storage.notes_holder_mutated(ndb, note_cache, txn, id); + let vitality = storage.notes_holder_mutated(ndb, note_cache, txn, id, is_muted); let (holder, result) = match vitality { Vitality::Stale(holder) => { diff --git a/src/profile.rs b/src/profile.rs @@ -4,6 +4,7 @@ use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction}; use crate::{ filter::{self, FilterState}, multi_subscriber::MultiSubscriber, + muted::MuteFun, note::NoteRef, notecache::NoteCache, notes_holder::NotesHolder, @@ -61,11 +62,12 @@ impl Profile { source: PubkeySource, filters: Vec<Filter>, notes: Vec<NoteRef>, + is_muted: &MuteFun, ) -> Self { let mut timeline = Timeline::new(TimelineKind::profile(source), FilterState::ready(filters)); - copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes); + copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes, is_muted); Profile { timeline, @@ -111,6 +113,7 @@ impl NotesHolder for Profile { id: &[u8; 32], filters: Vec<Filter>, notes: Vec<NoteRef>, + is_muted: &MuteFun, ) -> Self { Profile::new( txn, @@ -119,6 +122,7 @@ impl NotesHolder for Profile { PubkeySource::Explicit(Pubkey::new(*id)), filters, notes, + is_muted, ) } diff --git a/src/thread.rs b/src/thread.rs @@ -1,5 +1,6 @@ use crate::{ multi_subscriber::MultiSubscriber, + muted::MuteFun, note::NoteRef, notecache::NoteCache, notes_holder::NotesHolder, @@ -74,6 +75,7 @@ impl NotesHolder for Thread { _: &[u8; 32], _: Vec<Filter>, notes: Vec<NoteRef>, + _: &MuteFun, ) -> Self { Thread::new(notes) } diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs @@ -2,6 +2,7 @@ use crate::{ column::Columns, error::{Error, FilterError}, filter::{self, FilterState, FilterStates}, + muted::MuteFun, note::NoteRef, notecache::{CachedNote, NoteCache}, subscriptions::{self, SubKind, Subscriptions}, @@ -277,6 +278,7 @@ impl Timeline { txn: &Transaction, unknown_ids: &mut UnknownIds, note_cache: &mut NoteCache, + is_muted: &MuteFun, ) -> Result<()> { let timeline = timelines .get_mut(timeline_idx) @@ -299,6 +301,9 @@ impl Timeline { error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key); continue; }; + if is_muted(&note) { + continue; + } UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note); @@ -407,10 +412,11 @@ pub fn setup_new_timeline( pool: &mut RelayPool, note_cache: &mut NoteCache, since_optimize: bool, + is_muted: &MuteFun, ) { // if we're ready, setup local subs - if is_timeline_ready(ndb, pool, note_cache, timeline) { - if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) { + if is_timeline_ready(ndb, pool, note_cache, timeline, is_muted) { + if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline, is_muted) { error!("setup_new_timeline: {err}"); } } @@ -540,6 +546,7 @@ fn setup_initial_timeline( timeline: &mut Timeline, note_cache: &mut NoteCache, filters: &[Filter], + is_muted: &MuteFun, ) -> Result<()> { timeline.subscription = Some(ndb.subscribe(filters)?); let txn = Transaction::new(ndb)?; @@ -554,7 +561,7 @@ fn setup_initial_timeline( .map(NoteRef::from_query_result) .collect(); - copy_notes_into_timeline(timeline, &txn, ndb, note_cache, notes); + copy_notes_into_timeline(timeline, &txn, ndb, note_cache, notes, is_muted); Ok(()) } @@ -565,6 +572,7 @@ pub fn copy_notes_into_timeline( ndb: &Ndb, note_cache: &mut NoteCache, notes: Vec<NoteRef>, + is_muted: &MuteFun, ) { let filters = { let views = &timeline.views; @@ -576,6 +584,9 @@ pub fn copy_notes_into_timeline( for note_ref in notes { for (view, filter) in filters.iter().enumerate() { if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) { + if is_muted(&note) { + continue; + } if filter( note_cache.cached_note_or_insert_mut(note_ref.key, &note), &note, @@ -591,9 +602,10 @@ pub fn setup_initial_nostrdb_subs( ndb: &Ndb, note_cache: &mut NoteCache, columns: &mut Columns, + is_muted: &MuteFun, ) -> Result<()> { for timeline in columns.timelines_mut() { - if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) { + if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline, is_muted) { error!("setup_initial_nostrdb_subs: {err}"); } } @@ -605,6 +617,7 @@ fn setup_timeline_nostrdb_sub( ndb: &Ndb, note_cache: &mut NoteCache, timeline: &mut Timeline, + is_muted: &MuteFun, ) -> Result<()> { let filter_state = timeline .filter @@ -612,7 +625,7 @@ fn setup_timeline_nostrdb_sub( .ok_or(Error::empty_contact_list())? .to_owned(); - setup_initial_timeline(ndb, timeline, note_cache, &filter_state)?; + setup_initial_timeline(ndb, timeline, note_cache, &filter_state, is_muted)?; Ok(()) } @@ -626,6 +639,7 @@ pub fn is_timeline_ready( pool: &mut RelayPool, note_cache: &mut NoteCache, timeline: &mut Timeline, + is_muted: &MuteFun, ) -> bool { // TODO: we should debounce the filter states a bit to make sure we have // seen all of the different contact lists from each relay @@ -680,7 +694,8 @@ pub fn is_timeline_ready( // we just switched to the ready state, we should send initial // queries and setup the local subscription info!("Found contact list! Setting up local and remote contact list query"); - setup_initial_timeline(ndb, timeline, note_cache, &filter).expect("setup init"); + setup_initial_timeline(ndb, timeline, note_cache, &filter, is_muted) + .expect("setup init"); timeline .filter .set_relay_state(relay_id, FilterState::ready(filter.clone())); diff --git a/src/timeline/route.rs b/src/timeline/route.rs @@ -3,6 +3,7 @@ use crate::{ column::Columns, draft::Drafts, imgcache::ImageCache, + muted::MuteFun, nav::RenderNavAction, notecache::NoteCache, notes_holder::NotesHolderStorage, @@ -82,7 +83,7 @@ pub fn render_timeline_route( textmode, ) .id_source(egui::Id::new(("threadscroll", col))) - .ui(ui) + .ui(ui, &accounts.mutefun()) .map(Into::into), TimelineRoute::Reply(id) => { @@ -118,9 +119,16 @@ pub fn render_timeline_route( action.map(Into::into) } - TimelineRoute::Profile(pubkey) => { - render_profile_route(&pubkey, ndb, profiles, img_cache, note_cache, col, ui) - } + TimelineRoute::Profile(pubkey) => render_profile_route( + &pubkey, + ndb, + profiles, + img_cache, + note_cache, + col, + ui, + &accounts.mutefun(), + ), TimelineRoute::Quote(id) => { let txn = Transaction::new(ndb).expect("txn"); @@ -157,6 +165,7 @@ pub fn render_profile_route( note_cache: &mut NoteCache, col: usize, ui: &mut egui::Ui, + is_muted: &MuteFun, ) -> Option<RenderNavAction> { let note_action = ProfileView::new( pubkey, @@ -167,7 +176,7 @@ pub fn render_profile_route( img_cache, NoteOptions::default(), ) - .ui(ui); + .ui(ui, is_muted); note_action.map(RenderNavAction::NoteAction) } diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs @@ -374,6 +374,7 @@ pub fn render_add_column_routes( &mut app.pool, &mut app.note_cache, app.since_optimize, + &app.accounts.mutefun(), ); app.columns_mut().add_timeline_to_column(col, timeline); } diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs @@ -9,7 +9,7 @@ pub use picture::ProfilePic; pub use preview::ProfilePreview; use crate::{ - actionbar::NoteAction, imgcache::ImageCache, notecache::NoteCache, + actionbar::NoteAction, imgcache::ImageCache, muted::MuteFun, notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, }; @@ -46,7 +46,7 @@ impl<'a> ProfileView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { + pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option<NoteAction> { let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey)); ScrollArea::vertical() @@ -58,7 +58,13 @@ impl<'a> ProfileView<'a> { } let profile = self .profiles - .notes_holder_mutated(self.ndb, self.note_cache, &txn, self.pubkey.bytes()) + .notes_holder_mutated( + self.ndb, + self.note_cache, + &txn, + self.pubkey.bytes(), + is_muted, + ) .get_ptr(); profile.timeline.selected_view = tabs_ui(ui); diff --git a/src/ui/thread.rs b/src/ui/thread.rs @@ -1,6 +1,7 @@ use crate::{ actionbar::NoteAction, imgcache::ImageCache, + muted::MuteFun, notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, thread::Thread, @@ -52,7 +53,7 @@ impl<'a> ThreadView<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> { + pub fn ui(&mut self, ui: &mut egui::Ui, is_muted: &MuteFun) -> Option<NoteAction> { let txn = Transaction::new(self.ndb).expect("txn"); let selected_note_key = if let Ok(key) = self @@ -97,13 +98,13 @@ impl<'a> ThreadView<'a> { let thread = self .threads - .notes_holder_mutated(self.ndb, self.note_cache, &txn, root_id) + .notes_holder_mutated(self.ndb, self.note_cache, &txn, root_id, is_muted) .get_ptr(); // TODO(jb55): skip poll if ThreadResult is fresh? // poll for new notes and insert them into our existing notes - match thread.poll_notes_into_view(&txn, self.ndb) { + match thread.poll_notes_into_view(&txn, self.ndb, is_muted) { Ok(action) => { action.process_action(&txn, self.ndb, self.unknown_ids, self.note_cache) }