commit 84838478b35af2a91612d069399bfb048d044a8c
parent d5956319660823d86d875f17144a4258c7f4b690
Author: Ken Sedgwick <ken@bonsai.com>
Date: Tue, 19 Nov 2024 12:59:53 -0800
Skip muted content
Diffstat:
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(¬e) {
+ 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(¬e_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(¬e) {
+ continue;
+ }
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
@@ -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(¬e) {
+ continue;
+ }
if filter(
note_cache.cached_note_or_insert_mut(note_ref.key, ¬e),
¬e,
@@ -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)
}