mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-18 17:14:21 +01:00
@@ -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};
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
/// 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;
|
||||
thread.get_view().insert(&self.notes, reversed);
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
match 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,
|
||||
);
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
276
crates/notedeck_columns/src/timeline/cache.rs
Normal file
276
crates/notedeck_columns/src/timeline/cache.rs
Normal 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, ¬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![]
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,64 +295,81 @@ 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;
|
||||
|
||||
// ViewFilter::NotesAndReplies
|
||||
if let Some(view) = timeline.view_mut(ViewFilter::NotesAndReplies) {
|
||||
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);
|
||||
}
|
||||
|
||||
//
|
||||
// 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) {
|
||||
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);
|
||||
@@ -349,9 +381,33 @@ impl Timeline {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
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 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 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)
|
||||
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}");
|
||||
}
|
||||
Err(err) => error!("{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,
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user