commit 4b542c0a74b5cc449f66206335daf91e5297640c
parent e52ba5937fd7fda7562848da57c0a6e3356b1f65
Author: William Casarin <jb55@jb55.com>
Date: Sun, 19 Jan 2025 12:42:41 -0800
switch to TimelineCache
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
19 files changed, 709 insertions(+), 662 deletions(-)
diff --git a/crates/enostr/src/lib.rs b/crates/enostr/src/lib.rs
@@ -15,7 +15,7 @@ pub use keypair::{FilledKeypair, FullKeypair, Keypair, SerializableKeypair};
pub use nostr::SecretKey;
pub use note::{Note, NoteId};
pub use profile::Profile;
-pub use pubkey::Pubkey;
+pub use pubkey::{Pubkey, PubkeyRef};
pub use relay::message::{RelayEvent, RelayMessage};
pub use relay::pool::{PoolEvent, PoolRelay, RelayPool};
pub use relay::subs_debug::{OwnedRelayEvent, RelayLogEvent, SubsDebug, TransferStats};
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -29,7 +29,7 @@ pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use imgcache::ImageCache;
pub use muted::{MuteFun, Muted};
-pub use note::NoteRef;
+pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
pub use notecache::{CachedNote, NoteCache};
pub use result::Result;
pub use storage::{
diff --git a/crates/notedeck/src/note.rs b/crates/notedeck/src/note.rs
@@ -117,22 +117,31 @@ impl PartialOrd for NoteRef {
}
}
-pub fn root_note_id_from_selected_id<'a>(
+#[derive(Debug, Copy, Clone)]
+pub enum RootIdError {
+ NoteNotFound,
+ NoRootId,
+}
+
+pub fn root_note_id_from_selected_id<'txn, 'a>(
ndb: &Ndb,
note_cache: &mut NoteCache,
- txn: &'a Transaction,
+ txn: &'txn Transaction,
selected_note_id: &'a [u8; 32],
-) -> &'a [u8; 32] {
+) -> Result<RootNoteId<'txn>, RootIdError>
+where
+ 'a: 'txn,
+{
let selected_note_key = if let Ok(key) = ndb.get_notekey_by_id(txn, selected_note_id) {
key
} else {
- return selected_note_id;
+ return Err(RootIdError::NoteNotFound);
};
let note = if let Ok(note) = ndb.get_note_by_key(txn, selected_note_key) {
note
} else {
- return selected_note_id;
+ return Err(RootIdError::NoteNotFound);
};
note_cache
@@ -140,5 +149,8 @@ pub fn root_note_id_from_selected_id<'a>(
.reply
.borrow(note.tags())
.root()
- .map_or_else(|| selected_note_id, |nr| nr.id)
+ .map_or_else(
+ || Ok(RootNoteId::new_unsafe(selected_note_id)),
+ |rnid| Ok(RootNoteId::new_unsafe(rnid.id)),
+ )
}
diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs
@@ -1,14 +1,13 @@
use crate::{
column::Columns,
- notes_holder::{NotesHolder, NotesHolderStorage},
- profile::Profile,
route::{Route, Router},
- thread::Thread,
+ timeline::{TimelineCache, TimelineCacheKey},
};
use enostr::{NoteId, Pubkey, RelayPool};
-use nostrdb::{Ndb, Transaction};
-use notedeck::{note::root_note_id_from_selected_id, NoteCache, NoteRef};
+use nostrdb::{Ndb, NoteKey, Transaction};
+use notedeck::{note::root_note_id_from_selected_id, NoteCache, RootIdError, UnknownIds};
+use tracing::error;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum NoteAction {
@@ -18,13 +17,13 @@ pub enum NoteAction {
OpenProfile(Pubkey),
}
-pub struct NewNotes {
- pub id: [u8; 32],
- pub notes: Vec<NoteRef>,
+pub struct NewNotes<'a> {
+ pub id: TimelineCacheKey<'a>,
+ pub notes: Vec<NoteKey>,
}
-pub enum NotesHolderResult {
- NewNotes(NewNotes),
+pub enum TimelineOpenResult<'a> {
+ NewNotes(NewNotes<'a>),
}
/// open_thread is called when a note is selected and we need to navigate
@@ -33,109 +32,189 @@ pub enum NotesHolderResult {
/// 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(
+fn open_thread<'txn>(
ndb: &Ndb,
- txn: &Transaction,
+ txn: &'txn Transaction,
router: &mut Router<Route>,
note_cache: &mut NoteCache,
pool: &mut RelayPool,
- threads: &mut NotesHolderStorage<Thread>,
- selected_note: &[u8; 32],
-) -> Option<NotesHolderResult> {
+ timeline_cache: &mut TimelineCache,
+ selected_note: &'txn [u8; 32],
+) -> Option<TimelineOpenResult<'txn>> {
router.route_to(Route::thread(NoteId::new(selected_note.to_owned())));
- let root_id = root_note_id_from_selected_id(ndb, note_cache, txn, selected_note);
- Thread::open(ndb, note_cache, txn, pool, threads, root_id)
+ match root_note_id_from_selected_id(ndb, note_cache, txn, selected_note) {
+ Ok(root_id) => timeline_cache.open(
+ ndb,
+ note_cache,
+ txn,
+ pool,
+ TimelineCacheKey::thread(root_id),
+ ),
+
+ Err(RootIdError::NoteNotFound) => {
+ error!(
+ "open_thread: note not found: {}",
+ hex::encode(selected_note)
+ );
+ None
+ }
+
+ Err(RootIdError::NoRootId) => {
+ error!(
+ "open_thread: note has no root id: {}",
+ hex::encode(selected_note)
+ );
+ None
+ }
+ }
}
impl NoteAction {
#[allow(clippy::too_many_arguments)]
- pub fn execute(
- self,
+ pub fn execute<'txn, 'a>(
+ &'a self,
ndb: &Ndb,
router: &mut Router<Route>,
- threads: &mut NotesHolderStorage<Thread>,
- profiles: &mut NotesHolderStorage<Profile>,
+ timeline_cache: &mut TimelineCache,
note_cache: &mut NoteCache,
pool: &mut RelayPool,
- txn: &Transaction,
- ) -> Option<NotesHolderResult> {
+ txn: &'txn Transaction,
+ ) -> Option<TimelineOpenResult<'txn>>
+ where
+ 'a: 'txn,
+ {
match self {
NoteAction::Reply(note_id) => {
- router.route_to(Route::reply(note_id));
+ router.route_to(Route::reply(*note_id));
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,
+ timeline_cache,
+ note_id.bytes(),
+ ),
NoteAction::OpenProfile(pubkey) => {
- router.route_to(Route::profile(pubkey));
- Profile::open(ndb, note_cache, txn, pool, profiles, pubkey.bytes())
+ router.route_to(Route::profile(*pubkey));
+ timeline_cache.open(
+ ndb,
+ note_cache,
+ txn,
+ pool,
+ TimelineCacheKey::profile(pubkey.as_ref()),
+ )
}
NoteAction::Quote(note_id) => {
- router.route_to(Route::quote(note_id));
+ router.route_to(Route::quote(*note_id));
None
}
}
}
- /// Execute the NoteAction and process the NotesHolderResult
+ /// Execute the NoteAction and process the TimelineOpenResult
#[allow(clippy::too_many_arguments)]
pub fn execute_and_process_result(
self,
ndb: &Ndb,
columns: &mut Columns,
col: usize,
- threads: &mut NotesHolderStorage<Thread>,
- profiles: &mut NotesHolderStorage<Profile>,
+ timeline_cache: &mut TimelineCache,
note_cache: &mut NoteCache,
pool: &mut RelayPool,
txn: &Transaction,
+ unknown_ids: &mut UnknownIds,
) {
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, timeline_cache, note_cache, pool, txn) {
+ br.process(ndb, note_cache, txn, timeline_cache, unknown_ids);
}
}
}
-impl NotesHolderResult {
- pub fn new_notes(notes: Vec<NoteRef>, id: [u8; 32]) -> Self {
- NotesHolderResult::NewNotes(NewNotes::new(notes, id))
+impl<'a> TimelineOpenResult<'a> {
+ pub fn new_notes(notes: Vec<NoteKey>, id: TimelineCacheKey<'a>) -> Self {
+ Self::NewNotes(NewNotes::new(notes, id))
}
- pub fn process<N: NotesHolder>(
+ pub fn process(
&self,
ndb: &Ndb,
note_cache: &mut NoteCache,
txn: &Transaction,
- storage: &mut NotesHolderStorage<N>,
+ storage: &mut TimelineCache,
+ unknown_ids: &mut UnknownIds,
) {
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)
- .get_ptr();
- new_notes.process(holder);
+ TimelineOpenResult::NewNotes(new_notes) => {
+ new_notes.process(storage, ndb, txn, unknown_ids, note_cache);
}
}
}
}
-impl NewNotes {
- pub fn new(notes: Vec<NoteRef>, id: [u8; 32]) -> Self {
+impl<'a> NewNotes<'a> {
+ pub fn new(notes: Vec<NoteKey>, id: TimelineCacheKey<'a>) -> Self {
NewNotes { notes, id }
}
/// Simple helper for processing a NewThreadNotes result. It simply
- /// inserts/merges the notes into the thread cache
- pub fn process<N: NotesHolder>(&self, thread: &mut N) {
- // threads are chronological, ie reversed from reverse-chronological, the default.
- let reversed = true;
- thread.get_view().insert(&self.notes, reversed);
+ /// inserts/merges the notes into the corresponding timeline cache
+ pub fn process(
+ &self,
+ timeline_cache: &mut TimelineCache,
+ ndb: &Ndb,
+ txn: &Transaction,
+ unknown_ids: &mut UnknownIds,
+ note_cache: &mut NoteCache,
+ ) {
+ match self.id {
+ TimelineCacheKey::Profile(pubkey) => {
+ let profile = if let Some(profile) = timeline_cache.profiles.get_mut(pubkey.bytes())
+ {
+ profile
+ } else {
+ return;
+ };
+
+ let reversed = false;
+
+ if let Err(err) = profile.timeline.insert(
+ &self.notes,
+ ndb,
+ txn,
+ unknown_ids,
+ note_cache,
+ reversed,
+ ) {
+ error!("error inserting notes into profile timeline: {err}")
+ }
+ }
+
+ TimelineCacheKey::Thread(root_id) => {
+ // threads are chronological, ie reversed from reverse-chronological, the default.
+ let reversed = true;
+ let thread = if let Some(thread) = timeline_cache.threads.get_mut(root_id.bytes()) {
+ thread
+ } else {
+ return;
+ };
+
+ if let Err(err) =
+ thread
+ .timeline
+ .insert(&self.notes, ndb, txn, unknown_ids, note_cache, reversed)
+ {
+ error!("error inserting notes into thread timeline: {err}")
+ }
+ }
+ }
}
}
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -3,14 +3,10 @@ use crate::{
column::Columns,
decks::{Decks, DecksCache, FALLBACK_PUBKEY},
draft::Drafts,
- nav,
- notes_holder::NotesHolderStorage,
- profile::Profile,
- storage,
+ nav, storage,
subscriptions::{SubKind, Subscriptions},
support::Support,
- thread::Thread,
- timeline::{self, Timeline},
+ timeline::{self, TimelineCache},
ui::{self, DesktopSidePanel},
unknowns,
view_state::ViewState,
@@ -43,8 +39,7 @@ pub struct Damus {
pub decks_cache: DecksCache,
pub view_state: ViewState,
pub drafts: Drafts,
- pub threads: NotesHolderStorage<Thread>,
- pub profiles: NotesHolderStorage<Profile>,
+ pub timeline_cache: TimelineCache,
pub subscriptions: Subscriptions,
pub support: Support,
@@ -152,14 +147,15 @@ fn try_process_event(
if is_ready {
let txn = Transaction::new(app_ctx.ndb).expect("txn");
+ // only thread timelines are reversed
+ let reversed = false;
- if let Err(err) = Timeline::poll_notes_into_view(
- timeline_ind,
- current_columns.timelines_mut(),
+ if let Err(err) = current_columns.timelines_mut()[timeline_ind].poll_notes_into_view(
app_ctx.ndb,
&txn,
app_ctx.unknown_ids,
app_ctx.note_cache,
+ reversed,
) {
error!("poll_notes_into_view: {err}");
}
@@ -420,8 +416,7 @@ impl Damus {
Self {
subscriptions: Subscriptions::default(),
since_optimize: parsed_args.since_optimize,
- threads: NotesHolderStorage::default(),
- profiles: NotesHolderStorage::default(),
+ timeline_cache: TimelineCache::default(),
drafts: Drafts::default(),
state: DamusState::Initializing,
textmode: parsed_args.textmode,
@@ -464,8 +459,7 @@ impl Damus {
debug,
subscriptions: Subscriptions::default(),
since_optimize: true,
- threads: NotesHolderStorage::default(),
- profiles: NotesHolderStorage::default(),
+ timeline_cache: TimelineCache::default(),
drafts: Drafts::default(),
state: DamusState::Initializing,
textmode: false,
@@ -480,14 +474,6 @@ impl Damus {
pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> {
&mut self.subscriptions.subs
}
-
- pub fn threads(&self) -> &NotesHolderStorage<Thread> {
- &self.threads
- }
-
- pub fn threads_mut(&mut self) -> &mut NotesHolderStorage<Thread> {
- &mut self.threads
- }
}
/*
diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs
@@ -20,7 +20,6 @@ mod key_parsing;
pub mod login_manager;
mod multi_subscriber;
mod nav;
-mod notes_holder;
mod post;
mod profile;
mod profile_state;
diff --git a/crates/notedeck_columns/src/multi_subscriber.rs b/crates/notedeck_columns/src/multi_subscriber.rs
@@ -1,14 +1,13 @@
use enostr::{Filter, RelayPool};
-use nostrdb::{Ndb, Note, Transaction};
-use tracing::{debug, error, info};
+use nostrdb::Ndb;
+use tracing::{error, info};
use uuid::Uuid;
-use crate::Error;
-use notedeck::{NoteRef, UnifiedSubscription};
+use notedeck::UnifiedSubscription;
pub struct MultiSubscriber {
filters: Vec<Filter>,
- sub: Option<UnifiedSubscription>,
+ pub sub: Option<UnifiedSubscription>,
subscribers: u32,
}
@@ -105,30 +104,4 @@ impl MultiSubscriber {
)
}
}
-
- pub fn poll_for_notes(&mut self, ndb: &Ndb, txn: &Transaction) -> Result<Vec<NoteRef>, Error> {
- let sub = self.sub.as_ref().ok_or(notedeck::Error::no_active_sub())?;
- let new_note_keys = ndb.poll_for_notes(sub.local, 500);
-
- if new_note_keys.is_empty() {
- return Ok(vec![]);
- } else {
- debug!("{} new notes! {:?}", new_note_keys.len(), new_note_keys);
- }
-
- let mut notes: Vec<Note<'_>> = Vec::with_capacity(new_note_keys.len());
- for key in new_note_keys {
- let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
- note
- } else {
- continue;
- };
-
- notes.push(note);
- }
-
- let note_refs: Vec<NoteRef> = notes.iter().map(|n| NoteRef::from_note(n)).collect();
-
- Ok(note_refs)
- }
}
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -5,12 +5,10 @@ use crate::{
column::ColumnsAction,
deck_state::DeckState,
decks::{Deck, DecksAction, DecksCache},
- notes_holder::NotesHolder,
- profile::{Profile, ProfileAction, SaveProfileChanges},
+ profile::{ProfileAction, SaveProfileChanges},
profile_state::ProfileState,
relay_pool_manager::RelayPoolManager,
route::Route,
- thread::Thread,
timeline::{
route::{render_timeline_route, TimelineRoute},
Timeline,
@@ -29,7 +27,7 @@ use crate::{
Damus,
};
-use notedeck::{AccountsAction, AppContext};
+use notedeck::{AccountsAction, AppContext, RootIdError};
use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
use nostrdb::{Ndb, Transaction};
@@ -162,11 +160,11 @@ impl RenderNavResponse {
ctx.ndb,
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
col,
- &mut app.threads,
- &mut app.profiles,
+ &mut app.timeline_cache,
ctx.note_cache,
ctx.pool,
&txn,
+ ctx.unknown_ids,
);
}
@@ -195,34 +193,38 @@ impl RenderNavResponse {
.router_mut()
.pop();
let txn = Transaction::new(ctx.ndb).expect("txn");
+
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
- let root_id = {
- notedeck::note::root_note_id_from_selected_id(
- ctx.ndb,
- ctx.note_cache,
- &txn,
- id.bytes(),
- )
- };
- Thread::unsubscribe_locally(
- &txn,
+ match notedeck::note::root_note_id_from_selected_id(
ctx.ndb,
ctx.note_cache,
- &mut app.threads,
- ctx.pool,
- root_id,
- );
- }
-
- if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
- Profile::unsubscribe_locally(
&txn,
- ctx.ndb,
- ctx.note_cache,
- &mut app.profiles,
- ctx.pool,
- pubkey.bytes(),
- );
+ id.bytes(),
+ ) {
+ Ok(root_id) => {
+ if let Some(thread) =
+ app.timeline_cache.threads.get_mut(root_id.bytes())
+ {
+ if let Some(sub) = &mut thread.subscription {
+ sub.unsubscribe(ctx.ndb, ctx.pool);
+ }
+ }
+ }
+
+ Err(RootIdError::NoteNotFound) => {
+ error!("thread returned: note not found for unsub??: {}", id.hex())
+ }
+
+ Err(RootIdError::NoRootId) => {
+ error!("thread returned: note not found for unsub??: {}", id.hex())
+ }
+ }
+ } else if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
+ if let Some(profile) = app.timeline_cache.profiles.get_mut(pubkey.bytes()) {
+ if let Some(sub) = &mut profile.subscription {
+ sub.unsubscribe(ctx.ndb, ctx.pool);
+ }
+ }
}
switching_occured = true;
@@ -263,8 +265,7 @@ fn render_nav_body(
ctx.img_cache,
ctx.unknown_ids,
ctx.note_cache,
- &mut app.threads,
- &mut app.profiles,
+ &mut app.timeline_cache,
ctx.accounts,
*tlr,
col,
diff --git a/crates/notedeck_columns/src/notes_holder.rs b/crates/notedeck_columns/src/notes_holder.rs
@@ -1,215 +0,0 @@
-use std::collections::HashMap;
-
-use enostr::{Filter, RelayPool};
-use nostrdb::{Ndb, Transaction};
-use notedeck::{NoteCache, NoteRef, NoteRefsUnkIdAction};
-use tracing::{debug, info, warn};
-
-use crate::{
- actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, timeline::TimelineTab, Error,
- Result,
-};
-
-pub struct NotesHolderStorage<M: NotesHolder> {
- pub id_to_object: HashMap<[u8; 32], M>,
-}
-
-impl<M: NotesHolder> Default for NotesHolderStorage<M> {
- fn default() -> Self {
- NotesHolderStorage {
- id_to_object: HashMap::new(),
- }
- }
-}
-
-pub enum Vitality<'a, M> {
- Fresh(&'a mut M),
- Stale(&'a mut M),
-}
-
-impl<'a, M> Vitality<'a, M> {
- pub fn get_ptr(self) -> &'a mut M {
- match self {
- Self::Fresh(ptr) => ptr,
- Self::Stale(ptr) => ptr,
- }
- }
-
- pub fn is_stale(&self) -> bool {
- match self {
- Self::Fresh(_ptr) => false,
- Self::Stale(_ptr) => true,
- }
- }
-}
-
-impl<M: NotesHolder> NotesHolderStorage<M> {
- pub fn notes_holder_expected_mut(&mut self, id: &[u8; 32]) -> &mut M {
- self.id_to_object
- .get_mut(id)
- .expect("notes_holder_expected_mut used but there was no NotesHolder")
- }
-
- pub fn notes_holder_mutated<'a>(
- &'a mut self,
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- txn: &Transaction,
- id: &[u8; 32],
- ) -> 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
- // also use hashbrown?
-
- if self.id_to_object.contains_key(id) {
- return Vitality::Stale(self.notes_holder_expected_mut(id));
- }
-
- // we don't have the note holder, query for it!
- let filters = M::filters(id);
-
- let notes = if let Ok(results) = ndb.query(txn, &filters, 1000) {
- results
- .into_iter()
- .map(NoteRef::from_query_result)
- .collect()
- } else {
- debug!(
- "got no results from NotesHolder lookup for {}",
- hex::encode(id)
- );
- vec![]
- };
-
- if notes.is_empty() {
- warn!("NotesHolder query returned 0 notes? ")
- } else {
- info!("found NotesHolder with {} notes", notes.len());
- }
-
- self.id_to_object.insert(
- id.to_owned(),
- M::new_notes_holder(txn, ndb, note_cache, id, M::filters(id), notes),
- );
- Vitality::Fresh(self.id_to_object.get_mut(id).unwrap())
- }
-}
-
-pub trait NotesHolder {
- fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber>;
- fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber);
- fn get_view(&mut self) -> &mut TimelineTab;
- fn filters(for_id: &[u8; 32]) -> Vec<Filter>;
- fn filters_since(for_id: &[u8; 32], since: u64) -> Vec<Filter>;
- fn new_notes_holder(
- txn: &Transaction,
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- id: &[u8; 32],
- filters: Vec<Filter>,
- notes: Vec<NoteRef>,
- ) -> Self;
-
- #[must_use = "process_action must be handled in the Ok(action) case"]
- fn poll_notes_into_view(
- &mut self,
- txn: &Transaction,
- ndb: &Ndb,
- ) -> 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)?;
- self.get_view().insert(¬e_refs, reversed);
- Ok(NoteRefsUnkIdAction::new(note_refs))
- } else {
- Err(Error::Generic(
- "NotesHolder unexpectedly has no MultiSubscriber".to_owned(),
- ))
- }
- }
-
- /// Look for new thread notes since our last fetch
- fn new_notes(notes: &[NoteRef], id: &[u8; 32], txn: &Transaction, ndb: &Ndb) -> Vec<NoteRef> {
- if notes.is_empty() {
- return vec![];
- }
-
- let last_note = notes[0];
- let filters = Self::filters_since(id, last_note.created_at + 1);
-
- if let Ok(results) = ndb.query(txn, &filters, 1000) {
- debug!("got {} results from NotesHolder update", results.len());
- results
- .into_iter()
- .map(NoteRef::from_query_result)
- .collect()
- } else {
- debug!("got no results from NotesHolder update",);
- vec![]
- }
- }
-
- /// Local NotesHolder unsubscribe
- fn unsubscribe_locally<M: NotesHolder>(
- txn: &Transaction,
- ndb: &mut Ndb,
- note_cache: &mut NoteCache,
- notes_holder_storage: &mut NotesHolderStorage<M>,
- pool: &mut RelayPool,
- id: &[u8; 32],
- ) {
- let notes_holder = notes_holder_storage
- .notes_holder_mutated(ndb, note_cache, txn, id)
- .get_ptr();
-
- if let Some(multi_subscriber) = notes_holder.get_multi_subscriber() {
- multi_subscriber.unsubscribe(ndb, pool);
- }
- }
-
- fn open<M: NotesHolder>(
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- txn: &Transaction,
- pool: &mut RelayPool,
- storage: &mut NotesHolderStorage<M>,
- id: &[u8; 32],
- ) -> Option<NotesHolderResult> {
- let vitality = storage.notes_holder_mutated(ndb, note_cache, txn, id);
-
- let (holder, result) = match vitality {
- Vitality::Stale(holder) => {
- // The NotesHolder is stale, let's update it
- let notes = M::new_notes(&holder.get_view().notes, id, txn, ndb);
- let holder_result = if notes.is_empty() {
- None
- } else {
- Some(NotesHolderResult::new_notes(notes, id.to_owned()))
- };
-
- //
- // we can't insert and update the VirtualList now, because we
- // are already borrowing it mutably. Let's pass it as a
- // result instead
- //
- // holder.get_view().insert(¬es); <-- no
- //
- (holder, holder_result)
- }
-
- Vitality::Fresh(thread) => (thread, None),
- };
-
- let multi_subscriber = if let Some(multi_subscriber) = holder.get_multi_subscriber() {
- multi_subscriber
- } else {
- let filters = M::filters(id);
- holder.set_multi_subscriber(MultiSubscriber::new(filters));
- holder.get_multi_subscriber().unwrap()
- };
-
- multi_subscriber.subscribe(ndb, pool);
-
- result
- }
-}
diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs
@@ -1,19 +1,16 @@
use std::collections::HashMap;
-use enostr::{Filter, FullKeypair, Pubkey, RelayPool};
-use nostrdb::{
- FilterBuilder, Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord, Transaction,
-};
+use enostr::{Filter, FullKeypair, Pubkey, PubkeyRef, RelayPool};
+use nostrdb::{FilterBuilder, Ndb, Note, NoteBuildOptions, NoteBuilder, ProfileRecord};
-use notedeck::{filter::default_limit, FilterState, NoteCache, NoteRef};
+use notedeck::{filter::default_limit, FilterState};
use tracing::info;
use crate::{
multi_subscriber::MultiSubscriber,
- notes_holder::NotesHolder,
profile_state::ProfileState,
route::{Route, Router},
- timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab},
+ timeline::{PubkeySource, Timeline, TimelineKind, TimelineTab},
};
pub struct NostrName<'a> {
@@ -80,86 +77,31 @@ pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a>
pub struct Profile {
pub timeline: Timeline,
- pub multi_subscriber: Option<MultiSubscriber>,
+ pub subscription: Option<MultiSubscriber>,
}
impl Profile {
- pub fn new(
- txn: &Transaction,
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- source: PubkeySource,
- filters: Vec<Filter>,
- notes: Vec<NoteRef>,
- ) -> Self {
- let mut timeline = Timeline::new(
+ pub fn new(source: PubkeySource, filters: Vec<Filter>) -> Self {
+ let timeline = Timeline::new(
TimelineKind::profile(source),
FilterState::ready(filters),
TimelineTab::full_tabs(),
);
- copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes);
-
Profile {
timeline,
- multi_subscriber: None,
+ subscription: None,
}
}
- fn filters_raw(pk: &[u8; 32]) -> Vec<FilterBuilder> {
+ pub fn filters_raw(pk: PubkeyRef<'_>) -> Vec<FilterBuilder> {
vec![Filter::new()
- .authors([pk])
+ .authors([pk.bytes()])
.kinds([1])
.limit(default_limit())]
}
}
-impl NotesHolder for Profile {
- fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber> {
- self.multi_subscriber.as_mut()
- }
-
- fn get_view(&mut self) -> &mut crate::timeline::TimelineTab {
- self.timeline.current_view_mut()
- }
-
- fn filters(for_id: &[u8; 32]) -> Vec<enostr::Filter> {
- Profile::filters_raw(for_id)
- .into_iter()
- .map(|mut f| f.build())
- .collect()
- }
-
- fn filters_since(for_id: &[u8; 32], since: u64) -> Vec<enostr::Filter> {
- Profile::filters_raw(for_id)
- .into_iter()
- .map(|f| f.since(since).build())
- .collect()
- }
-
- fn new_notes_holder(
- txn: &Transaction,
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- id: &[u8; 32],
- filters: Vec<Filter>,
- notes: Vec<NoteRef>,
- ) -> Self {
- Profile::new(
- txn,
- ndb,
- note_cache,
- PubkeySource::Explicit(Pubkey::new(*id)),
- filters,
- notes,
- )
- }
-
- fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber) {
- self.multi_subscriber = Some(subscriber);
- }
-}
-
pub struct SaveProfileChanges {
pub kp: FullKeypair,
pub state: ProfileState,
diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs
@@ -473,6 +473,10 @@ fn serialize_route(route: &Route, columns: &Columns) -> Option<String> {
TimelineKind::Universe => {
selections.push(Selection::Keyword(Keyword::Universe))
}
+ TimelineKind::Thread(root_id) => {
+ selections.push(Selection::Keyword(Keyword::Thread));
+ selections.push(Selection::Payload(hex::encode(root_id.bytes())));
+ }
TimelineKind::Generic => {
selections.push(Selection::Keyword(Keyword::Generic))
}
diff --git a/crates/notedeck_columns/src/thread.rs b/crates/notedeck_columns/src/thread.rs
@@ -1,92 +1,27 @@
-use crate::{
- multi_subscriber::MultiSubscriber,
- notes_holder::NotesHolder,
- timeline::{TimelineTab, ViewFilter},
-};
+use crate::{multi_subscriber::MultiSubscriber, timeline::Timeline};
-use nostrdb::{Filter, FilterBuilder, Ndb, Transaction};
-use notedeck::{NoteCache, NoteRef};
+use nostrdb::FilterBuilder;
+use notedeck::{RootNoteId, RootNoteIdBuf};
-#[derive(Default)]
pub struct Thread {
- view: TimelineTab,
- pub multi_subscriber: Option<MultiSubscriber>,
+ pub timeline: Timeline,
+ pub subscription: Option<MultiSubscriber>,
}
impl Thread {
- pub fn new(notes: Vec<NoteRef>) -> Self {
- let mut cap = ((notes.len() as f32) * 1.5) as usize;
- if cap == 0 {
- cap = 25;
- }
- let mut view = TimelineTab::new_with_capacity(ViewFilter::NotesAndReplies, cap);
- view.notes = notes;
+ pub fn new(root_id: RootNoteIdBuf) -> Self {
+ let timeline = Timeline::thread(root_id);
Thread {
- view,
- multi_subscriber: None,
+ timeline,
+ subscription: None,
}
}
- pub fn view(&self) -> &TimelineTab {
- &self.view
- }
-
- pub fn view_mut(&mut self) -> &mut TimelineTab {
- &mut self.view
- }
-
- fn filters_raw(root: &[u8; 32]) -> Vec<FilterBuilder> {
+ pub fn filters_raw(root_id: RootNoteId<'_>) -> Vec<FilterBuilder> {
vec![
- nostrdb::Filter::new().kinds([1]).event(root),
- nostrdb::Filter::new().ids([root]).limit(1),
+ nostrdb::Filter::new().kinds([1]).event(root_id.bytes()),
+ nostrdb::Filter::new().ids([root_id.bytes()]).limit(1),
]
}
-
- pub fn filters_since(root: &[u8; 32], since: u64) -> Vec<Filter> {
- Self::filters_raw(root)
- .into_iter()
- .map(|fb| fb.since(since).build())
- .collect()
- }
-
- pub fn filters(root: &[u8; 32]) -> Vec<Filter> {
- Self::filters_raw(root)
- .into_iter()
- .map(|mut fb| fb.build())
- .collect()
- }
-}
-
-impl NotesHolder for Thread {
- fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber> {
- self.multi_subscriber.as_mut()
- }
-
- fn filters(for_id: &[u8; 32]) -> Vec<Filter> {
- Thread::filters(for_id)
- }
-
- fn new_notes_holder(
- _: &Transaction,
- _: &Ndb,
- _: &mut NoteCache,
- _: &[u8; 32],
- _: Vec<Filter>,
- notes: Vec<NoteRef>,
- ) -> Self {
- Thread::new(notes)
- }
-
- fn get_view(&mut self) -> &mut TimelineTab {
- &mut self.view
- }
-
- fn filters_since(for_id: &[u8; 32], since: u64) -> Vec<Filter> {
- Thread::filters_since(for_id, since)
- }
-
- fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber) {
- self.multi_subscriber = Some(subscriber);
- }
}
diff --git a/crates/notedeck_columns/src/timeline/cache.rs b/crates/notedeck_columns/src/timeline/cache.rs
@@ -0,0 +1,276 @@
+use crate::{
+ actionbar::TimelineOpenResult,
+ multi_subscriber::MultiSubscriber,
+ profile::Profile,
+ thread::Thread,
+ //subscriptions::SubRefs,
+ timeline::{PubkeySource, Timeline},
+};
+
+use notedeck::{NoteCache, NoteRef, RootNoteId, RootNoteIdBuf};
+
+use enostr::{Pubkey, PubkeyRef, RelayPool};
+use nostrdb::{Filter, FilterBuilder, Ndb, Transaction};
+use std::collections::HashMap;
+use tracing::{debug, info, warn};
+
+#[derive(Default)]
+pub struct TimelineCache {
+ pub threads: HashMap<RootNoteIdBuf, Thread>,
+ pub profiles: HashMap<Pubkey, Profile>,
+}
+
+pub enum Vitality<'a, M> {
+ Fresh(&'a mut M),
+ Stale(&'a mut M),
+}
+
+impl<'a, M> Vitality<'a, M> {
+ pub fn get_ptr(self) -> &'a mut M {
+ match self {
+ Self::Fresh(ptr) => ptr,
+ Self::Stale(ptr) => ptr,
+ }
+ }
+
+ pub fn is_stale(&self) -> bool {
+ match self {
+ Self::Fresh(_ptr) => false,
+ Self::Stale(_ptr) => true,
+ }
+ }
+}
+
+#[derive(Hash, Debug, Copy, Clone)]
+pub enum TimelineCacheKey<'a> {
+ Profile(PubkeyRef<'a>),
+ Thread(RootNoteId<'a>),
+}
+
+impl<'a> TimelineCacheKey<'a> {
+ pub fn profile(pubkey: PubkeyRef<'a>) -> Self {
+ Self::Profile(pubkey)
+ }
+
+ pub fn thread(root_id: RootNoteId<'a>) -> Self {
+ Self::Thread(root_id)
+ }
+
+ pub fn bytes(&self) -> &[u8; 32] {
+ match self {
+ Self::Profile(pk) => pk.bytes(),
+ Self::Thread(root_id) => root_id.bytes(),
+ }
+ }
+
+ /// The filters used to update our timeline cache
+ pub fn filters_raw(&self) -> Vec<FilterBuilder> {
+ match self {
+ TimelineCacheKey::Thread(root_id) => Thread::filters_raw(*root_id),
+
+ TimelineCacheKey::Profile(pubkey) => vec![Filter::new()
+ .authors([pubkey.bytes()])
+ .kinds([1])
+ .limit(notedeck::filter::default_limit())],
+ }
+ }
+
+ pub fn filters_since(&self, since: u64) -> Vec<Filter> {
+ self.filters_raw()
+ .into_iter()
+ .map(|fb| fb.since(since).build())
+ .collect()
+ }
+
+ pub fn filters(&self) -> Vec<Filter> {
+ self.filters_raw()
+ .into_iter()
+ .map(|mut fb| fb.build())
+ .collect()
+ }
+}
+
+impl TimelineCache {
+ fn contains_key(&self, key: TimelineCacheKey<'_>) -> bool {
+ match key {
+ TimelineCacheKey::Profile(pubkey) => self.profiles.contains_key(pubkey.bytes()),
+ TimelineCacheKey::Thread(root_id) => self.threads.contains_key(root_id.bytes()),
+ }
+ }
+
+ fn get_expected_mut(&mut self, key: TimelineCacheKey<'_>) -> &mut Timeline {
+ match key {
+ TimelineCacheKey::Profile(pubkey) => self
+ .profiles
+ .get_mut(pubkey.bytes())
+ .map(|p| &mut p.timeline),
+ TimelineCacheKey::Thread(root_id) => self
+ .threads
+ .get_mut(root_id.bytes())
+ .map(|t| &mut t.timeline),
+ }
+ .expect("expected notes in timline cache")
+ }
+
+ /// Insert a new profile or thread into the cache, based on the TimelineCacheKey
+ #[allow(clippy::too_many_arguments)]
+ fn insert_new(
+ &mut self,
+ id: TimelineCacheKey<'_>,
+ txn: &Transaction,
+ ndb: &Ndb,
+ notes: &[NoteRef],
+ note_cache: &mut NoteCache,
+ filters: Vec<Filter>,
+ ) {
+ match id {
+ TimelineCacheKey::Profile(pubkey) => {
+ let mut profile = Profile::new(PubkeySource::Explicit(pubkey.to_owned()), filters);
+ // insert initial notes into timeline
+ profile.timeline.insert_new(txn, ndb, note_cache, notes);
+ self.profiles.insert(pubkey.to_owned(), profile);
+ }
+
+ TimelineCacheKey::Thread(root_id) => {
+ let mut thread = Thread::new(root_id.to_owned());
+ thread.timeline.insert_new(txn, ndb, note_cache, notes);
+ self.threads.insert(root_id.to_owned(), thread);
+ }
+ }
+ }
+
+ /// Get and/or update the notes associated with this timeline
+ pub fn notes<'a>(
+ &'a mut self,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ txn: &Transaction,
+ id: TimelineCacheKey<'a>,
+ ) -> Vitality<'a, Timeline> {
+ // 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
+ // also use hashbrown?
+
+ if self.contains_key(id) {
+ return Vitality::Stale(self.get_expected_mut(id));
+ }
+
+ let filters = id.filters();
+ let notes = if let Ok(results) = ndb.query(txn, &filters, 1000) {
+ results
+ .into_iter()
+ .map(NoteRef::from_query_result)
+ .collect()
+ } else {
+ debug!("got no results from TimelineCache lookup for {:?}", id);
+ vec![]
+ };
+
+ if notes.is_empty() {
+ warn!("NotesHolder query returned 0 notes? ")
+ } else {
+ info!("found NotesHolder with {} notes", notes.len());
+ }
+
+ self.insert_new(id, txn, ndb, ¬es, note_cache, filters);
+
+ Vitality::Fresh(self.get_expected_mut(id))
+ }
+
+ pub fn subscription(
+ &mut self,
+ id: TimelineCacheKey<'_>,
+ ) -> Option<&mut Option<MultiSubscriber>> {
+ match id {
+ TimelineCacheKey::Profile(pubkey) => self
+ .profiles
+ .get_mut(pubkey.bytes())
+ .map(|p| &mut p.subscription),
+ TimelineCacheKey::Thread(root_id) => self
+ .threads
+ .get_mut(root_id.bytes())
+ .map(|t| &mut t.subscription),
+ }
+ }
+
+ pub fn open<'a>(
+ &mut self,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ txn: &Transaction,
+ pool: &mut RelayPool,
+ id: TimelineCacheKey<'a>,
+ ) -> Option<TimelineOpenResult<'a>> {
+ let result = match self.notes(ndb, note_cache, txn, id) {
+ Vitality::Stale(timeline) => {
+ // The timeline cache is stale, let's update it
+ let notes = find_new_notes(timeline.all_or_any_notes(), id, txn, ndb);
+ let cached_timeline_result = if notes.is_empty() {
+ None
+ } else {
+ let new_notes = notes.iter().map(|n| n.key).collect();
+ Some(TimelineOpenResult::new_notes(new_notes, id))
+ };
+
+ // we can't insert and update the VirtualList now, because we
+ // are already borrowing it mutably. Let's pass it as a
+ // result instead
+ //
+ // holder.get_view().insert(¬es); <-- no
+ cached_timeline_result
+ }
+
+ Vitality::Fresh(_timeline) => None,
+ };
+
+ let sub_id = if let Some(sub) = self.subscription(id) {
+ if let Some(multi_subscriber) = sub {
+ multi_subscriber.subscribe(ndb, pool);
+ multi_subscriber.sub.as_ref().map(|s| s.local)
+ } else {
+ let mut multi_sub = MultiSubscriber::new(id.filters());
+ multi_sub.subscribe(ndb, pool);
+ let sub_id = multi_sub.sub.as_ref().map(|s| s.local);
+ *sub = Some(multi_sub);
+ sub_id
+ }
+ } else {
+ None
+ };
+
+ let timeline = self.get_expected_mut(id);
+ if let Some(sub_id) = sub_id {
+ timeline.subscription = Some(sub_id);
+ }
+
+ // TODO: We have subscription ids tracked in different places. Fix this
+
+ result
+ }
+}
+
+/// Look for new thread notes since our last fetch
+fn find_new_notes(
+ notes: &[NoteRef],
+ id: TimelineCacheKey<'_>,
+ txn: &Transaction,
+ ndb: &Ndb,
+) -> Vec<NoteRef> {
+ if notes.is_empty() {
+ return vec![];
+ }
+
+ let last_note = notes[0];
+ let filters = id.filters_since(last_note.created_at + 1);
+
+ if let Ok(results) = ndb.query(txn, &filters, 1000) {
+ debug!("got {} results from NotesHolder update", results.len());
+ results
+ .into_iter()
+ .map(NoteRef::from_query_result)
+ .collect()
+ } else {
+ debug!("got no results from NotesHolder update",);
+ vec![]
+ }
+}
diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs
@@ -2,7 +2,7 @@ use crate::error::Error;
use crate::timeline::{Timeline, TimelineTab};
use enostr::{Filter, Pubkey};
use nostrdb::{Ndb, Transaction};
-use notedeck::{filter::default_limit, FilterError, FilterState};
+use notedeck::{filter::default_limit, FilterError, FilterState, RootNoteIdBuf};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, fmt::Display};
use tracing::{error, warn};
@@ -58,6 +58,9 @@ pub enum TimelineKind {
Profile(PubkeySource),
+ /// This could be any note id, doesn't need to be the root id
+ Thread(RootNoteIdBuf),
+
Universe,
/// Generic filter
@@ -75,6 +78,7 @@ impl Display for TimelineKind {
TimelineKind::Profile(_) => f.write_str("Profile"),
TimelineKind::Universe => f.write_str("Universe"),
TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
+ TimelineKind::Thread(_) => f.write_str("Thread"),
}
}
}
@@ -88,6 +92,7 @@ impl TimelineKind {
TimelineKind::Universe => None,
TimelineKind::Generic => None,
TimelineKind::Hashtag(_ht) => None,
+ TimelineKind::Thread(_ht) => None,
}
}
@@ -103,6 +108,10 @@ impl TimelineKind {
TimelineKind::Profile(pk)
}
+ pub fn thread(root_id: RootNoteIdBuf) -> Self {
+ TimelineKind::Thread(root_id)
+ }
+
pub fn is_notifications(&self) -> bool {
matches!(self, TimelineKind::Notifications(_))
}
@@ -122,6 +131,8 @@ impl TimelineKind {
TimelineTab::no_replies(),
)),
+ TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)),
+
TimelineKind::Generic => {
warn!("you can't convert a TimelineKind::Generic to a Timeline");
None
@@ -213,6 +224,7 @@ impl TimelineKind {
},
TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
+ TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"),
TimelineKind::Universe => ColumnTitle::simple("Universe"),
TimelineKind::Generic => ColumnTitle::simple("Custom"),
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),
diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs
@@ -3,11 +3,13 @@ use crate::{
decks::DecksCache,
error::Error,
subscriptions::{self, SubKind, Subscriptions},
+ thread::Thread,
Result,
};
use notedeck::{
- filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, UnknownIds,
+ filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, RootNoteIdBuf,
+ UnknownIds,
};
use std::fmt;
@@ -15,16 +17,18 @@ use std::sync::atomic::{AtomicU32, Ordering};
use egui_virtual_list::VirtualList;
use enostr::{PoolRelay, Pubkey, RelayPool};
-use nostrdb::{Filter, Ndb, Note, Subscription, Transaction};
+use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
use std::cell::RefCell;
use std::hash::Hash;
use std::rc::Rc;
use tracing::{debug, error, info, warn};
+pub mod cache;
pub mod kind;
pub mod route;
+pub use cache::{TimelineCache, TimelineCacheKey};
pub use kind::{ColumnTitle, PubkeySource, TimelineKind};
pub use route::TimelineRoute;
@@ -123,7 +127,7 @@ impl TimelineTab {
}
}
- pub fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
+ fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
if new_refs.is_empty() {
return;
}
@@ -189,7 +193,6 @@ pub struct Timeline {
pub views: Vec<TimelineTab>,
pub selected_view: usize,
- /// Our nostrdb subscription
pub subscription: Option<Subscription>,
}
@@ -210,6 +213,18 @@ impl Timeline {
))
}
+ pub fn thread(note_id: RootNoteIdBuf) -> Self {
+ let filter = Thread::filters_raw(note_id.borrow())
+ .iter_mut()
+ .map(|fb| fb.build())
+ .collect();
+ Timeline::new(
+ TimelineKind::Thread(note_id),
+ FilterState::ready(filter),
+ TimelineTab::only_notes_and_replies(),
+ )
+ }
+
pub fn hashtag(hashtag: String) -> Self {
let filter = Filter::new()
.kinds([1])
@@ -280,78 +295,119 @@ impl Timeline {
self.views.iter_mut().find(|tab| tab.filter == view)
}
- pub fn poll_notes_into_view(
- timeline_idx: usize,
- mut timelines: Vec<&mut Timeline>,
+ /// Initial insert of notes into a timeline. Subsequent inserts should
+ /// just use the insert function
+ pub fn insert_new(
+ &mut self,
+ txn: &Transaction,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ notes: &[NoteRef],
+ ) {
+ let filters = {
+ let views = &self.views;
+ let filters: Vec<fn(&CachedNote, &Note) -> bool> =
+ views.iter().map(|v| v.filter.filter()).collect();
+ filters
+ };
+
+ 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 filter(
+ note_cache.cached_note_or_insert_mut(note_ref.key, ¬e),
+ ¬e,
+ ) {
+ self.views[view].notes.push(*note_ref)
+ }
+ }
+ }
+ }
+ }
+
+ /// The main function used for inserting notes into timelines. Handles
+ /// inserting into multiple views if we have them. All timeline note
+ /// insertions should use this function.
+ pub fn insert(
+ &mut self,
+ new_note_ids: &[NoteKey],
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
+ reversed: bool,
) -> Result<()> {
- let timeline = timelines
- .get_mut(timeline_idx)
- .ok_or(Error::TimelineNotFound)?;
- let sub = timeline
- .subscription
- .ok_or(Error::App(notedeck::Error::no_active_sub()))?;
-
- let new_note_ids = ndb.poll_for_notes(sub, 500);
- if new_note_ids.is_empty() {
- return Ok(());
- } else {
- debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
- }
-
let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
for key in new_note_ids {
- let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
+ let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) {
note
} else {
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;
};
+ // Ensure that unknown ids are captured when inserting notes
+ // into the timeline
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
let created_at = note.created_at();
- new_refs.push((note, NoteRef { key, created_at }));
+ new_refs.push((
+ note,
+ NoteRef {
+ key: *key,
+ created_at,
+ },
+ ));
}
- // We're assuming reverse-chronological here (timelines). This
- // flag ensures we trigger the items_inserted_at_start
- // optimization in VirtualList. We need this flag because we can
- // insert notes into chronological order sometimes, and this
- // optimization doesn't make sense in those situations.
- let reversed = false;
+ for view in &mut self.views {
+ match view.filter {
+ ViewFilter::NotesAndReplies => {
+ let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
- // ViewFilter::NotesAndReplies
- if let Some(view) = timeline.view_mut(ViewFilter::NotesAndReplies) {
- let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
+ view.insert(&refs, reversed);
+ }
- view.insert(&refs, reversed);
- }
+ ViewFilter::Notes => {
+ let mut filtered_refs = Vec::with_capacity(new_refs.len());
+ for (note, nr) in &new_refs {
+ let cached_note = note_cache.cached_note_or_insert(nr.key, note);
- //
- // handle the filtered case (ViewFilter::Notes, no replies)
- //
- // TODO(jb55): this is mostly just copied from above, let's just use a loop
- // I initially tried this but ran into borrow checker issues
- if let Some(view) = timeline.view_mut(ViewFilter::Notes) {
- let mut filtered_refs = Vec::with_capacity(new_refs.len());
- for (note, nr) in &new_refs {
- let cached_note = note_cache.cached_note_or_insert(nr.key, note);
-
- if ViewFilter::filter_notes(cached_note, note) {
- filtered_refs.push(*nr);
+ if ViewFilter::filter_notes(cached_note, note) {
+ filtered_refs.push(*nr);
+ }
+ }
+
+ view.insert(&filtered_refs, reversed);
}
}
-
- view.insert(&filtered_refs, reversed);
}
Ok(())
}
+
+ pub fn poll_notes_into_view(
+ &mut self,
+ ndb: &Ndb,
+ txn: &Transaction,
+ unknown_ids: &mut UnknownIds,
+ note_cache: &mut NoteCache,
+ reversed: bool,
+ ) -> Result<()> {
+ let sub = self
+ .subscription
+ .ok_or(Error::App(notedeck::Error::no_active_sub()))?;
+
+ let new_note_ids = ndb.poll_for_notes(sub, 500);
+ if new_note_ids.is_empty() {
+ return Ok(());
+ } else {
+ debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
+ }
+
+ self.insert(&new_note_ids, ndb, txn, unknown_ids, note_cache, reversed)
+ }
}
pub enum MergeKind {
@@ -550,45 +606,18 @@ fn setup_initial_timeline(
timeline.subscription, timeline.filter
);
let lim = filters[0].limit().unwrap_or(filter::default_limit()) as i32;
- let notes = ndb
+
+ let notes: Vec<NoteRef> = ndb
.query(&txn, filters, lim)?
.into_iter()
.map(NoteRef::from_query_result)
.collect();
- copy_notes_into_timeline(timeline, &txn, ndb, note_cache, notes);
+ timeline.insert_new(&txn, ndb, note_cache, ¬es);
Ok(())
}
-pub fn copy_notes_into_timeline(
- timeline: &mut Timeline,
- txn: &Transaction,
- ndb: &Ndb,
- note_cache: &mut NoteCache,
- notes: Vec<NoteRef>,
-) {
- let filters = {
- let views = &timeline.views;
- let filters: Vec<fn(&CachedNote, &Note) -> bool> =
- views.iter().map(|v| v.filter.filter()).collect();
- filters
- };
-
- 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 filter(
- note_cache.cached_note_or_insert_mut(note_ref.key, ¬e),
- ¬e,
- ) {
- timeline.views[view].notes.push(note_ref)
- }
- }
- }
- }
-}
-
pub fn setup_initial_nostrdb_subs(
ndb: &Ndb,
note_cache: &mut NoteCache,
diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs
@@ -2,10 +2,8 @@ use crate::{
column::Columns,
draft::Drafts,
nav::RenderNavAction,
- notes_holder::NotesHolderStorage,
- profile::{Profile, ProfileAction},
- thread::Thread,
- timeline::{TimelineId, TimelineKind},
+ profile::ProfileAction,
+ timeline::{TimelineCache, TimelineId, TimelineKind},
ui::{
self,
note::{NoteOptions, QuoteRepostView},
@@ -34,8 +32,7 @@ pub fn render_timeline_route(
img_cache: &mut ImageCache,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
- threads: &mut NotesHolderStorage<Thread>,
- profiles: &mut NotesHolderStorage<Profile>,
+ timeline_cache: &mut TimelineCache,
accounts: &mut Accounts,
route: TimelineRoute,
col: usize,
@@ -71,7 +68,7 @@ pub fn render_timeline_route(
}
TimelineRoute::Thread(id) => ui::ThreadView::new(
- threads,
+ timeline_cache,
ndb,
note_cache,
unknown_ids,
@@ -121,9 +118,10 @@ pub fn render_timeline_route(
&pubkey,
accounts,
ndb,
- profiles,
+ timeline_cache,
img_cache,
note_cache,
+ unknown_ids,
col,
ui,
&accounts.mutefun(),
@@ -160,9 +158,10 @@ pub fn render_profile_route(
pubkey: &Pubkey,
accounts: &Accounts,
ndb: &Ndb,
- profiles: &mut NotesHolderStorage<Profile>,
+ timeline_cache: &mut TimelineCache,
img_cache: &mut ImageCache,
note_cache: &mut NoteCache,
+ unknown_ids: &mut UnknownIds,
col: usize,
ui: &mut egui::Ui,
is_muted: &MuteFun,
@@ -171,10 +170,11 @@ pub fn render_profile_route(
pubkey,
accounts,
col,
- profiles,
+ timeline_cache,
ndb,
note_cache,
img_cache,
+ unknown_ids,
is_muted,
NoteOptions::default(),
)
diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs
@@ -2,33 +2,39 @@ pub mod edit;
pub mod picture;
pub mod preview;
-use crate::profile::get_display_name;
-use crate::ui::note::NoteOptions;
-use crate::{colors, images};
-use crate::{notes_holder::NotesHolder, NostrName};
pub use edit::EditProfileView;
use egui::load::TexturePoll;
use egui::{vec2, Color32, Label, Layout, Rect, RichText, Rounding, ScrollArea, Sense, Stroke};
-use enostr::Pubkey;
+use enostr::{Pubkey, PubkeyRef};
use nostrdb::{Ndb, ProfileRecord, Transaction};
pub use picture::ProfilePic;
pub use preview::ProfilePreview;
use tracing::error;
-use crate::{actionbar::NoteAction, notes_holder::NotesHolderStorage, profile::Profile};
+use crate::{
+ actionbar::NoteAction,
+ colors, images,
+ profile::get_display_name,
+ timeline::{TimelineCache, TimelineCacheKey},
+ ui::{
+ note::NoteOptions,
+ timeline::{tabs_ui, TimelineTabView},
+ },
+ NostrName,
+};
-use super::timeline::{tabs_ui, TimelineTabView};
-use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle};
+use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, NotedeckTextStyle, UnknownIds};
pub struct ProfileView<'a> {
pubkey: &'a Pubkey,
accounts: &'a Accounts,
col_id: usize,
- profiles: &'a mut NotesHolderStorage<Profile>,
+ timeline_cache: &'a mut TimelineCache,
note_options: NoteOptions,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
+ unknown_ids: &'a mut UnknownIds,
is_muted: &'a MuteFun,
}
@@ -43,10 +49,11 @@ impl<'a> ProfileView<'a> {
pubkey: &'a Pubkey,
accounts: &'a Accounts,
col_id: usize,
- profiles: &'a mut NotesHolderStorage<Profile>,
+ timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
img_cache: &'a mut ImageCache,
+ unknown_ids: &'a mut UnknownIds,
is_muted: &'a MuteFun,
note_options: NoteOptions,
) -> Self {
@@ -54,10 +61,11 @@ impl<'a> ProfileView<'a> {
pubkey,
accounts,
col_id,
- profiles,
+ timeline_cache,
ndb,
note_cache,
img_cache,
+ unknown_ids,
note_options,
is_muted,
}
@@ -76,23 +84,33 @@ impl<'a> ProfileView<'a> {
action = Some(ProfileViewAction::EditProfile);
}
}
- let profile = self
- .profiles
- .notes_holder_mutated(self.ndb, self.note_cache, &txn, self.pubkey.bytes())
+ let profile_timeline = self
+ .timeline_cache
+ .notes(
+ self.ndb,
+ self.note_cache,
+ &txn,
+ TimelineCacheKey::Profile(PubkeyRef::new(self.pubkey.bytes())),
+ )
.get_ptr();
- profile.timeline.selected_view =
- tabs_ui(ui, profile.timeline.selected_view, &profile.timeline.views);
+ profile_timeline.selected_view =
+ tabs_ui(ui, profile_timeline.selected_view, &profile_timeline.views);
+ let reversed = false;
// poll for new notes and insert them into our existing notes
- if let Err(e) = profile.poll_notes_into_view(&txn, self.ndb) {
+ if let Err(e) = profile_timeline.poll_notes_into_view(
+ self.ndb,
+ &txn,
+ self.unknown_ids,
+ self.note_cache,
+ reversed,
+ ) {
error!("Profile::poll_notes_into_view: {e}");
}
- let reversed = false;
-
if let Some(note_action) = TimelineTabView::new(
- profile.timeline.current_view(),
+ profile_timeline.current_view(),
reversed,
self.note_options,
&txn,
diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs
@@ -1,18 +1,17 @@
use crate::{
actionbar::NoteAction,
- notes_holder::{NotesHolder, NotesHolderStorage},
- thread::Thread,
+ timeline::{TimelineCache, TimelineCacheKey},
ui::note::NoteOptions,
};
use nostrdb::{Ndb, Transaction};
-use notedeck::{ImageCache, MuteFun, NoteCache, UnknownIds};
+use notedeck::{ImageCache, MuteFun, NoteCache, RootNoteId, UnknownIds};
use tracing::error;
use super::timeline::TimelineTabView;
pub struct ThreadView<'a> {
- threads: &'a mut NotesHolderStorage<Thread>,
+ timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
unknown_ids: &'a mut UnknownIds,
@@ -26,7 +25,7 @@ pub struct ThreadView<'a> {
impl<'a> ThreadView<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
- threads: &'a mut NotesHolderStorage<Thread>,
+ timeline_cache: &'a mut TimelineCache,
ndb: &'a Ndb,
note_cache: &'a mut NoteCache,
unknown_ids: &'a mut UnknownIds,
@@ -37,7 +36,7 @@ impl<'a> ThreadView<'a> {
) -> Self {
let id_source = egui::Id::new("threadscroll_threadview");
ThreadView {
- threads,
+ timeline_cache,
ndb,
note_cache,
unknown_ids,
@@ -57,14 +56,6 @@ impl<'a> ThreadView<'a> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
let txn = Transaction::new(self.ndb).expect("txn");
- let selected_note_key =
- if let Ok(key) = self.ndb.get_notekey_by_id(&txn, self.selected_note_id) {
- key
- } else {
- // TODO: render 404 ?
- return None;
- };
-
ui.label(
egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.")
.color(egui::Color32::RED),
@@ -76,38 +67,39 @@ impl<'a> ThreadView<'a> {
.auto_shrink([false, false])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
.show(ui, |ui| {
- let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) {
- note
- } else {
- return None;
- };
-
- let root_id = {
- let cached_note = self
- .note_cache
- .cached_note_or_insert(selected_note_key, ¬e);
-
- cached_note
- .reply
- .borrow(note.tags())
- .root()
- .map_or_else(|| self.selected_note_id, |nr| nr.id)
- };
-
- let thread = self
- .threads
- .notes_holder_mutated(self.ndb, self.note_cache, &txn, root_id)
+ let root_id =
+ match RootNoteId::new(self.ndb, self.note_cache, &txn, self.selected_note_id) {
+ Ok(root_id) => root_id,
+
+ Err(err) => {
+ ui.label(format!("Error loading thread: {:?}", err));
+ return None;
+ }
+ };
+
+ let thread_timeline = self
+ .timeline_cache
+ .notes(
+ self.ndb,
+ self.note_cache,
+ &txn,
+ TimelineCacheKey::Thread(root_id),
+ )
.get_ptr();
// TODO(jb55): skip poll if ThreadResult is fresh?
+ let reversed = true;
// poll for new notes and insert them into our existing notes
- match thread.poll_notes_into_view(&txn, self.ndb) {
- Ok(action) => {
- action.process_action(&txn, self.ndb, self.unknown_ids, self.note_cache)
- }
- Err(err) => error!("{err}"),
- };
+ if let Err(err) = thread_timeline.poll_notes_into_view(
+ self.ndb,
+ &txn,
+ self.unknown_ids,
+ self.note_cache,
+ reversed,
+ ) {
+ error!("error polling notes into thread timeline: {err}");
+ }
// This is threadview. We are not the universe view...
let is_universe = false;
@@ -115,7 +107,7 @@ impl<'a> ThreadView<'a> {
note_options.set_textmode(self.textmode);
TimelineTabView::new(
- thread.view(),
+ thread_timeline.current_view(),
true,
note_options,
&txn,
diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs
@@ -286,10 +286,14 @@ impl<'a> TimelineTabView<'a> {
return 0;
};
- let muted = is_muted(
- ¬e,
- root_note_id_from_selected_id(self.ndb, self.note_cache, self.txn, note.id()),
- );
+ // should we mute the thread? we might not have it!
+ let muted = if let Ok(root_id) =
+ root_note_id_from_selected_id(self.ndb, self.note_cache, self.txn, note.id())
+ {
+ is_muted(¬e, root_id.bytes())
+ } else {
+ false
+ };
if !muted {
ui::padding(8.0, ui, |ui| {