switch to TimelineCache

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-01-19 12:42:41 -08:00
parent e52ba5937f
commit 4b542c0a74
19 changed files with 720 additions and 673 deletions

View File

@@ -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};

View File

@@ -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::{

View File

@@ -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)),
)
}

View File

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

View File

@@ -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
}
}
/*

View File

@@ -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;

View File

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

View File

@@ -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,
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,
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
match notedeck::note::root_note_id_from_selected_id(
ctx.ndb,
ctx.note_cache,
&mut app.profiles,
ctx.pool,
pubkey.bytes(),
);
&txn,
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,

View File

@@ -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(&note_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(&notes); <-- 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
}
}

View File

@@ -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,

View File

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

View File

@@ -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);
}
}

View File

@@ -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, &notes, 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(&notes); <-- 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![]
}
}

View File

@@ -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()),

View File

@@ -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,18 +295,107 @@ 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, &note),
&note,
) {
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
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) {
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, &note);
let created_at = note.created_at();
new_refs.push((
note,
NoteRef {
key: *key,
created_at,
},
));
}
for view in &mut self.views {
match view.filter {
ViewFilter::NotesAndReplies => {
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
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);
if ViewFilter::filter_notes(cached_note, note) {
filtered_refs.push(*nr);
}
}
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()))?;
@@ -302,55 +406,7 @@ impl Timeline {
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) {
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;
};
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note);
let created_at = note.created_at();
new_refs.push((note, NoteRef { 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;
// 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);
}
//
// 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);
}
}
view.insert(&filtered_refs, reversed);
}
Ok(())
self.insert(&new_note_ids, ndb, txn, unknown_ids, note_cache, reversed)
}
}
@@ -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, &notes);
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, &note),
&note,
) {
timeline.views[view].notes.push(note_ref)
}
}
}
}
}
pub fn setup_initial_nostrdb_subs(
ndb: &Ndb,
note_cache: &mut NoteCache,

View File

@@ -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(),
)

View File

@@ -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,

View File

@@ -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 =
match RootNoteId::new(self.ndb, self.note_cache, &txn, self.selected_note_id) {
Ok(root_id) => root_id,
let root_id = {
let cached_note = self
.note_cache
.cached_note_or_insert(selected_note_key, &note);
Err(err) => {
ui.label(format!("Error loading thread: {:?}", err));
return None;
}
};
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 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,

View File

@@ -286,10 +286,14 @@ impl<'a> TimelineTabView<'a> {
return 0;
};
let muted = is_muted(
&note,
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(&note, root_id.bytes())
} else {
false
};
if !muted {
ui::padding(8.0, ui, |ui| {