notedeck

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

commit d560e84eab3c64070fb99ffac7802c24cdf91190
parent f6753bae975cd0453eeb5426dbcefab37ac82008
Author: kernelkind <kernelkind@gmail.com>
Date:   Tue, 17 Jun 2025 12:49:09 -0400

integrate new threads conception

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_chrome/src/chrome.rs | 1+
Mcrates/notedeck_columns/src/actionbar.rs | 30++++++++++++++++++++----------
Mcrates/notedeck_columns/src/app.rs | 7++++++-
Mcrates/notedeck_columns/src/nav.rs | 25+++++++++++++++++++++++--
Mcrates/notedeck_columns/src/route.rs | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/notedeck_columns/src/timeline/kind.rs | 33---------------------------------
Mcrates/notedeck_columns/src/timeline/mod.rs | 18------------------
Mcrates/notedeck_columns/src/timeline/route.rs | 83+++++++++++++++++++++++++++++++------------------------------------------------
Mcrates/notedeck_columns/src/ui/column/header.rs | 26+++++++++++++++++++++-----
Mcrates/notedeck_columns/src/ui/thread.rs | 146++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
10 files changed, 258 insertions(+), 183 deletions(-)

diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -625,6 +625,7 @@ fn chrome_handle_app_action( cols, 0, &mut columns.timeline_cache, + &mut columns.threads, ctx.note_cache, ctx.pool, &txn, diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs @@ -41,8 +41,9 @@ struct NoteActionResponse { #[allow(clippy::too_many_arguments)] fn execute_note_action( action: NoteAction, - ndb: &Ndb, + ndb: &mut Ndb, timeline_cache: &mut TimelineCache, + threads: &mut Threads, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, @@ -52,6 +53,7 @@ fn execute_note_action( images: &mut Images, router_type: RouterType, ui: &mut egui::Ui, + col: usize, ) -> NoteActionResponse { let mut timeline_res = None; let mut router_action = None; @@ -74,13 +76,16 @@ fn execute_note_action( break 'ex; }; - let kind = TimelineKind::Thread(thread_selection); - router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); - // NOTE!!: you need the note_id to timeline root id thing + timeline_res = threads + .open(ndb, txn, pool, &thread_selection, preview, col) + .map(NotesOpenResult::Thread); - timeline_res = timeline_cache - .open(ndb, note_cache, txn, pool, &kind) - .map(NotesOpenResult::Timeline); + let route = Route::Thread(thread_selection); + + router_action = Some(RouterAction::Overlay { + route, + make_new: preview, + }); } NoteAction::Hashtag(htag) => { let kind = TimelineKind::Hashtag(htag.clone()); @@ -151,10 +156,11 @@ fn execute_note_action( #[allow(clippy::too_many_arguments)] pub fn execute_and_process_note_action( action: NoteAction, - ndb: &Ndb, + ndb: &mut Ndb, columns: &mut Columns, col: usize, timeline_cache: &mut TimelineCache, + threads: &mut Threads, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, @@ -179,6 +185,7 @@ pub fn execute_and_process_note_action( action, ndb, timeline_cache, + threads, note_cache, pool, txn, @@ -188,6 +195,7 @@ pub fn execute_and_process_note_action( images, router_type, ui, + col, ); if let Some(br) = resp.timeline_res { @@ -195,7 +203,9 @@ pub fn execute_and_process_note_action( NotesOpenResult::Timeline(timeline_open_result) => { timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids); } - NotesOpenResult::Thread(new_thread_notes) => todo!(), + NotesOpenResult::Thread(thread_open_result) => { + thread_open_result.process(threads, ndb, txn, unknown_ids, note_cache); + } } } @@ -258,7 +268,7 @@ impl NewNotes { unknown_ids: &mut UnknownIds, note_cache: &mut NoteCache, ) { - let reversed = matches!(&self.id, TimelineKind::Thread(_)); + let reversed = false; let timeline = if let Some(profile) = timeline_cache.timelines.get_mut(&self.id) { profile diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -8,7 +8,7 @@ use crate::{ storage, subscriptions::{SubKind, Subscriptions}, support::Support, - timeline::{self, TimelineCache}, + timeline::{self, thread::Threads, TimelineCache}, ui::{self, DesktopSidePanel}, view_state::ViewState, Result, @@ -45,6 +45,7 @@ pub struct Damus { pub subscriptions: Subscriptions, pub support: Support, pub jobs: JobsCache, + pub threads: Threads, //frame_history: crate::frame_history::FrameHistory, @@ -443,6 +444,8 @@ impl Damus { ctx.accounts.with_fallback(FALLBACK_PUBKEY()); + let threads = Threads::default(); + Self { subscriptions: Subscriptions::default(), since_optimize: parsed_args.since_optimize, @@ -458,6 +461,7 @@ impl Damus { debug, unrecognized_args, jobs, + threads, } } @@ -502,6 +506,7 @@ impl Damus { decks_cache, unrecognized_args: BTreeSet::default(), jobs: JobsCache::default(), + threads: Threads::default(), } } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -8,7 +8,10 @@ use crate::{ profile_state::ProfileState, relay_pool_manager::RelayPoolManager, route::{Route, Router, SingletonRouter}, - timeline::{route::render_timeline_route, TimelineCache}, + timeline::{ + route::{render_thread_route, render_timeline_route}, + TimelineCache, + }, ui::{ self, add_column::render_add_column_routes, @@ -210,7 +213,7 @@ fn process_nav_resp( if let Some(action) = response.action { match action { - NavAction::Returned(_) => { + NavAction::Returned(return_type) => { let r = app .columns_mut(ctx.accounts) .column_mut(col) @@ -223,6 +226,12 @@ fn process_nav_resp( } }; + if let Some(Route::Thread(selection)) = &r { + tracing::info!("Return type: {:?}", return_type); + app.threads + .close(ctx.ndb, ctx.pool, selection, return_type, col); + } + process_result = Some(ProcessNavResult::SwitchOccurred); } @@ -355,6 +364,7 @@ fn process_render_nav_action( get_active_columns_mut(ctx.accounts, &mut app.decks_cache), col, &mut app.timeline_cache, + &mut app.threads, ctx.note_cache, ctx.pool, &txn, @@ -426,6 +436,17 @@ fn render_nav_body( &mut note_context, &mut app.jobs, ), + Route::Thread(selection) => render_thread_route( + ctx.unknown_ids, + &mut app.threads, + ctx.accounts, + selection, + col, + app.note_options, + ui, + &mut note_context, + &mut app.jobs, + ), Route::Accounts(amr) => { let mut action = render_accounts_route( ui, diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs @@ -1,5 +1,5 @@ use enostr::{NoteId, Pubkey}; -use notedeck::{NoteZapTargetOwned, WalletType}; +use notedeck::{NoteZapTargetOwned, RootNoteIdBuf, WalletType}; use std::{ fmt::{self}, ops::Range, @@ -20,6 +20,7 @@ use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; #[derive(Clone, Eq, PartialEq, Debug)] pub enum Route { Timeline(TimelineKind), + Thread(ThreadSelection), Accounts(AccountsRoute), Reply(NoteId), Quote(NoteId), @@ -53,7 +54,7 @@ impl Route { } pub fn thread(thread_selection: ThreadSelection) -> Self { - Route::Timeline(TimelineKind::Thread(thread_selection)) + Route::Thread(thread_selection) } pub fn profile(pubkey: Pubkey) -> Self { @@ -79,6 +80,18 @@ impl Route { pub fn serialize_tokens(&self, writer: &mut TokenWriter) { match self { Route::Timeline(timeline_kind) => timeline_kind.serialize_tokens(writer), + Route::Thread(selection) => { + writer.write_token("thread"); + + if let Some(reply) = selection.selected_note { + writer.write_token("root"); + writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex()); + writer.write_token("reply"); + writer.write_token(&reply.hex()); + } else { + writer.write_token(&NoteId::new(*selection.root_id.bytes()).hex()); + } + } Route::Accounts(routes) => routes.serialize_tokens(writer), Route::AddColumn(routes) => routes.serialize_tokens(writer), Route::Search => writer.write_token("search"), @@ -199,6 +212,31 @@ impl Route { Ok(Route::Search) }) }, + |p| { + p.parse_all(|p| { + p.parse_token("thread")?; + p.parse_token("root")?; + + let root = tokenator::parse_hex_id(p)?; + + p.parse_token("reply")?; + + let selected = tokenator::parse_hex_id(p)?; + + Ok(Route::Thread(ThreadSelection { + root_id: RootNoteIdBuf::new_unsafe(root), + selected_note: Some(NoteId::new(selected)), + })) + }) + }, + |p| { + p.parse_all(|p| { + p.parse_token("thread")?; + Ok(Route::Thread(ThreadSelection::from_root_id( + RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?), + ))) + }) + }, ], ) } @@ -206,6 +244,7 @@ impl Route { pub fn title(&self) -> ColumnTitle<'_> { match self { Route::Timeline(kind) => kind.to_title(), + Route::Thread(_) => ColumnTitle::simple("Thread"), Route::Reply(_id) => ColumnTitle::simple("Reply"), Route::Quote(_id) => ColumnTitle::simple("Quote"), Route::Relays => ColumnTitle::simple("Relays"), @@ -423,9 +462,9 @@ impl fmt::Display for Route { TimelineKind::Generic(_) => write!(f, "Custom"), TimelineKind::Search(_) => write!(f, "Search"), TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht), - TimelineKind::Thread(_id) => write!(f, "Thread"), TimelineKind::Profile(_id) => write!(f, "Profile"), }, + Route::Thread(_) => write!(f, "Thread"), Route::Reply(_id) => write!(f, "Reply"), Route::Quote(_id) => write!(f, "Quote"), Route::Relays => write!(f, "Relays"), @@ -482,3 +521,30 @@ impl<R: Clone> Default for SingletonRouter<R> { } } } + +#[cfg(test)] +mod tests { + use enostr::NoteId; + use tokenator::{TokenParser, TokenWriter}; + + use crate::{timeline::ThreadSelection, Route}; + use enostr::Pubkey; + use notedeck::RootNoteIdBuf; + + #[test] + fn test_thread_route_serialize() { + let note_id_hex = "1c54e5b0c386425f7e017d9e068ddef8962eb2ce1bb08ed27e24b93411c12e60"; + let note_id = NoteId::from_hex(note_id_hex).unwrap(); + let data_str = format!("thread:{}", note_id_hex); + let data = &data_str.split(":").collect::<Vec<&str>>(); + let mut token_writer = TokenWriter::default(); + let mut parser = TokenParser::new(&data); + let parsed = Route::parse(&mut parser, &Pubkey::new(*note_id.bytes())).unwrap(); + let expected = Route::Thread(ThreadSelection::from_root_id(RootNoteIdBuf::new_unsafe( + *note_id.bytes(), + ))); + parsed.serialize_tokens(&mut token_writer); + assert_eq!(expected, parsed); + assert_eq!(token_writer.str(), data_str); + } +} diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs @@ -208,8 +208,6 @@ pub enum TimelineKind { Profile(Pubkey), - Thread(ThreadSelection), - Universe, /// Generic filter, references a hash of a filter @@ -266,7 +264,6 @@ 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"), TimelineKind::Search(_) => f.write_str("Search"), } } @@ -282,7 +279,6 @@ impl TimelineKind { TimelineKind::Universe => None, TimelineKind::Generic(_) => None, TimelineKind::Hashtag(_ht) => None, - TimelineKind::Thread(_ht) => None, TimelineKind::Search(query) => query.author(), } } @@ -298,7 +294,6 @@ impl TimelineKind { TimelineKind::Universe => true, TimelineKind::Generic(_) => true, TimelineKind::Hashtag(_ht) => true, - TimelineKind::Thread(_ht) => true, TimelineKind::Search(_q) => true, } } @@ -321,10 +316,6 @@ impl TimelineKind { writer.write_token("profile"); PubkeySource::pubkey(*pk).serialize_tokens(writer); } - TimelineKind::Thread(root_note_id) => { - writer.write_token("thread"); - writer.write_token(&root_note_id.root_id.hex()); - } TimelineKind::Universe => { writer.write_token("universe"); } @@ -378,12 +369,6 @@ impl TimelineKind { parser, &[ |p| { - p.parse_token("thread")?; - Ok(TimelineKind::Thread(ThreadSelection::from_root_id( - RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?), - ))) - }, - |p| { p.parse_token("universe")?; Ok(TimelineKind::Universe) }, @@ -425,10 +410,6 @@ impl TimelineKind { TimelineKind::Profile(pk) } - pub fn thread(selected_note: ThreadSelection) -> Self { - TimelineKind::Thread(selected_note) - } - pub fn is_notifications(&self) -> bool { matches!(self, TimelineKind::Notifications(_)) } @@ -474,17 +455,6 @@ impl TimelineKind { todo!("implement generic filter lookups") } - TimelineKind::Thread(selection) => FilterState::ready(vec![ - nostrdb::Filter::new() - .kinds([1]) - .event(selection.root_id.bytes()) - .build(), - nostrdb::Filter::new() - .ids([selection.root_id.bytes()]) - .limit(1) - .build(), - ]), - TimelineKind::Profile(pk) => FilterState::ready(vec![Filter::new() .authors([pk.bytes()]) .kinds([1]) @@ -510,8 +480,6 @@ impl TimelineKind { TimelineTab::full_tabs(), )), - TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)), - TimelineKind::Generic(_filter_id) => { warn!("you can't convert a TimelineKind::Generic to a Timeline"); // TODO: you actually can! just need to look up the filter id @@ -609,7 +577,6 @@ impl TimelineKind { }, TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"), TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), - TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"), TimelineKind::Universe => ColumnTitle::simple("Universe"), TimelineKind::Generic(_) => ColumnTitle::simple("Custom"), TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()), diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -214,24 +214,6 @@ impl Timeline { )) } - pub fn thread(selection: ThreadSelection) -> Self { - let filter = vec![ - nostrdb::Filter::new() - .kinds([1]) - .event(selection.root_id.bytes()) - .build(), - nostrdb::Filter::new() - .ids([selection.root_id.bytes()]) - .limit(1) - .build(), - ]; - Timeline::new( - TimelineKind::Thread(selection), - FilterState::ready(filter), - TimelineTab::only_notes_and_replies(), - ) - } - pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> { let kind = 1; let notes_per_pk = 1; diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs @@ -1,7 +1,7 @@ use crate::{ nav::RenderNavAction, profile::ProfileAction, - timeline::{TimelineCache, TimelineKind}, + timeline::{thread::Threads, ThreadSelection, TimelineCache, TimelineKind}, ui::{self, ProfileView}, }; @@ -16,7 +16,7 @@ pub fn render_timeline_route( accounts: &mut Accounts, kind: &TimelineKind, col: usize, - mut note_options: NoteOptions, + note_options: NoteOptions, depth: usize, ui: &mut egui::Ui, note_context: &mut NoteContext, @@ -74,30 +74,38 @@ pub fn render_timeline_route( note_action.map(RenderNavAction::NoteAction) } } + } +} - TimelineKind::Thread(id) => { - // don't truncate thread notes for now, since they are - // default truncated everywher eelse - note_options.set_truncate(false); - - // text is selectable in threads - note_options.set_selectable_text(true); +#[allow(clippy::too_many_arguments)] +pub fn render_thread_route( + unknown_ids: &mut UnknownIds, + threads: &mut Threads, + accounts: &mut Accounts, + selection: &ThreadSelection, + col: usize, + mut note_options: NoteOptions, + ui: &mut egui::Ui, + note_context: &mut NoteContext, + jobs: &mut JobsCache, +) -> Option<RenderNavAction> { + // don't truncate thread notes for now, since they are + // default truncated everywher eelse + note_options.set_truncate(false); - ui::ThreadView::new( - timeline_cache, - unknown_ids, - id.selected_or_root(), - note_options, - &accounts.mutefun(), - note_context, - &accounts.get_selected_account().map(|a| (&a.key).into()), - jobs, - ) - .id_source(egui::Id::new(("threadscroll", col))) - .ui(ui) - .map(Into::into) - } - } + ui::ThreadView::new( + threads, + unknown_ids, + selection.selected_or_root(), + note_options, + &accounts.mutefun(), + note_context, + &accounts.get_selected_account().map(|a| (&a.key).into()), + jobs, + ) + .id_source(col) + .ui(ui) + .map(Into::into) } #[allow(clippy::too_many_arguments)] @@ -139,30 +147,3 @@ pub fn render_profile_route( None } } - -#[cfg(test)] -mod tests { - use enostr::NoteId; - use tokenator::{TokenParser, TokenWriter}; - - use crate::timeline::{ThreadSelection, TimelineKind}; - use enostr::Pubkey; - use notedeck::RootNoteIdBuf; - - #[test] - fn test_timeline_route_serialize() { - let note_id_hex = "1c54e5b0c386425f7e017d9e068ddef8962eb2ce1bb08ed27e24b93411c12e60"; - let note_id = NoteId::from_hex(note_id_hex).unwrap(); - let data_str = format!("thread:{}", note_id_hex); - let data = &data_str.split(":").collect::<Vec<&str>>(); - let mut token_writer = TokenWriter::default(); - let mut parser = TokenParser::new(&data); - let parsed = TimelineKind::parse(&mut parser, &Pubkey::new(*note_id.bytes())).unwrap(); - let expected = TimelineKind::Thread(ThreadSelection::from_root_id( - RootNoteIdBuf::new_unsafe(*note_id.bytes()), - )); - parsed.serialize_tokens(&mut token_writer); - assert_eq!(expected, parsed); - assert_eq!(token_writer.str(), data_str); - } -} diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs @@ -1,6 +1,7 @@ use crate::column::ColumnsAction; use crate::nav::RenderNavAction; use crate::nav::SwitchingAction; +use crate::timeline::ThreadSelection; use crate::{ column::Columns, route::Route, @@ -437,11 +438,6 @@ impl<'a> NavTitle<'a> { TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), - TimelineKind::Thread(_) => { - // no pfp for threads - None - } - TimelineKind::Search(_sq) => { // TODO: show author pfp if author field set? @@ -467,6 +463,9 @@ impl<'a> NavTitle<'a> { Route::Search => Some(ui.add(ui::side_panel::search_button())), Route::Wallet(_) => None, Route::CustomizeZapAmount(_) => None, + Route::Thread(thread_selection) => { + Some(self.thread_pfp(ui, thread_selection, pfp_size)) + } } } @@ -488,6 +487,23 @@ impl<'a> NavTitle<'a> { } } + fn thread_pfp( + &mut self, + ui: &mut egui::Ui, + selection: &ThreadSelection, + pfp_size: f32, + ) -> egui::Response { + let txn = Transaction::new(self.ndb).unwrap(); + + if let Ok(note) = self.ndb.get_note_by_id(&txn, selection.selected_or_root()) { + if let Some(mut pfp) = self.pubkey_pfp(&txn, note.pubkey(), pfp_size) { + return ui.add(&mut pfp); + } + } + + ui.add(&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size)) + } + fn title_label_value(title: &str) -> egui::Label { egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())) .selectable(false) diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs @@ -3,21 +3,19 @@ use egui_virtual_list::VirtualList; use enostr::KeypairUnowned; use nostrdb::{Note, Transaction}; use notedeck::note::root_note_id_from_selected_id; -use notedeck::{MuteFun, NoteAction, NoteContext, RootNoteId, UnknownIds}; +use notedeck::{MuteFun, NoteAction, NoteContext, UnknownIds}; use notedeck_ui::jobs::JobsCache; use notedeck_ui::note::NoteResponse; use notedeck_ui::{NoteOptions, NoteView}; -use tracing::error; -use crate::timeline::thread::NoteSeenFlags; -use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind}; -use crate::ui::timeline::TimelineTabView; +use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads}; pub struct ThreadView<'a, 'd> { - timeline_cache: &'a mut TimelineCache, + threads: &'a mut Threads, unknown_ids: &'a mut UnknownIds, selected_note_id: &'a [u8; 32], note_options: NoteOptions, + col: usize, id_source: egui::Id, is_muted: &'a MuteFun, note_context: &'a mut NoteContext<'d>, @@ -28,7 +26,7 @@ pub struct ThreadView<'a, 'd> { impl<'a, 'd> ThreadView<'a, 'd> { #[allow(clippy::too_many_arguments)] pub fn new( - timeline_cache: &'a mut TimelineCache, + threads: &'a mut Threads, unknown_ids: &'a mut UnknownIds, selected_note_id: &'a [u8; 32], note_options: NoteOptions, @@ -39,7 +37,7 @@ impl<'a, 'd> ThreadView<'a, 'd> { ) -> Self { let id_source = egui::Id::new("threadscroll_threadview"); ThreadView { - timeline_cache, + threads, unknown_ids, selected_note_id, note_options, @@ -48,11 +46,13 @@ impl<'a, 'd> ThreadView<'a, 'd> { note_context, cur_acc, jobs, + col: 0, } } - pub fn id_source(mut self, id: egui::Id) -> Self { - self.id_source = id; + pub fn id_source(mut self, col: usize) -> Self { + self.col = col; + self.id_source = egui::Id::new(("threadscroll", col)); self } @@ -73,61 +73,87 @@ impl<'a, 'd> ThreadView<'a, 'd> { scroll_area = scroll_area.vertical_scroll_offset(offset); } - let output = scroll_area.show(ui, |ui| { - let root_id = match RootNoteId::new( - self.note_context.ndb, - self.note_context.note_cache, - &txn, - self.selected_note_id, - ) { - Ok(root_id) => root_id, - - Err(err) => { - ui.label(format!("Error loading thread: {:?}", err)); - return None; + let output = scroll_area.show(ui, |ui| self.notes(ui, &txn)); + + ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y)); + + output.inner + } + + fn notes(&mut self, ui: &mut egui::Ui, txn: &Transaction) -> Option<NoteAction> { + let Ok(cur_note) = self + .note_context + .ndb + .get_note_by_id(txn, self.selected_note_id) + else { + let id = *self.selected_note_id; + tracing::error!("ndb: Did not find note {}", enostr::NoteId::new(id).hex()); + return None; + }; + + self.threads.update( + &cur_note, + self.note_context.note_cache, + self.note_context.ndb, + txn, + self.unknown_ids, + self.col, + ); + + let cur_node = self.threads.threads.get(&self.selected_note_id).unwrap(); + + let full_chain = cur_node.have_all_ancestors; + let mut note_builder = ThreadNoteBuilder::new(cur_note); + + let mut parent_state = cur_node.prev.clone(); + while let ParentState::Parent(id) = parent_state { + if let Ok(note) = self.note_context.ndb.get_note_by_id(txn, id.bytes()) { + note_builder.add_chain(note); + if let Some(res) = self.threads.threads.get(&id.bytes()) { + parent_state = res.prev.clone(); + continue; } - }; - - let thread_timeline = self - .timeline_cache - .notes( - self.note_context.ndb, - self.note_context.note_cache, - &txn, - &TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())), - ) - .get_ptr(); - - // TODO(jb55): skip poll if ThreadResult is fresh? - - let reversed = true; - // poll for new notes and insert them into our existing notes - if let Err(err) = thread_timeline.poll_notes_into_view( - self.note_context.ndb, - &txn, - self.unknown_ids, - self.note_context.note_cache, - reversed, - ) { - error!("error polling notes into thread timeline: {err}"); } + parent_state = ParentState::Unknown; + } - TimelineTabView::new( - thread_timeline.current_view(), - true, - self.note_options, - &txn, - self.is_muted, - self.note_context, - self.cur_acc, - self.jobs, - ) - .show(ui) - }); + for note_ref in &cur_node.replies { + if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) { + note_builder.add_reply(note); + } + } - ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y)); + let list = &mut self + .threads + .threads + .get_mut(&self.selected_note_id) + .unwrap() + .list; - output.inner + let notes = note_builder.into_notes(&mut self.threads.seen_flags); + + if !full_chain { + // TODO(kernelkind): insert UI denoting we don't have the full chain yet + ui.colored_label(ui.visuals().error_fg_color, "LOADING NOTES"); + } + + let zapping_acc = self + .cur_acc + .as_ref() + .filter(|_| self.note_context.current_account_has_wallet) + .or(self.cur_acc.as_ref()); + + show_notes( + ui, + list, + &notes, + self.note_context, + zapping_acc, + self.note_options, + self.jobs, + txn, + self.is_muted, + ) } }