integrate new threads conception

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-06-17 12:49:09 -04:00
parent f6753bae97
commit d560e84eab
10 changed files with 260 additions and 185 deletions

View File

@@ -625,6 +625,7 @@ fn chrome_handle_app_action(
cols, cols,
0, 0,
&mut columns.timeline_cache, &mut columns.timeline_cache,
&mut columns.threads,
ctx.note_cache, ctx.note_cache,
ctx.pool, ctx.pool,
&txn, &txn,

View File

@@ -41,8 +41,9 @@ struct NoteActionResponse {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn execute_note_action( fn execute_note_action(
action: NoteAction, action: NoteAction,
ndb: &Ndb, ndb: &mut Ndb,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
threads: &mut Threads,
note_cache: &mut NoteCache, note_cache: &mut NoteCache,
pool: &mut RelayPool, pool: &mut RelayPool,
txn: &Transaction, txn: &Transaction,
@@ -52,6 +53,7 @@ fn execute_note_action(
images: &mut Images, images: &mut Images,
router_type: RouterType, router_type: RouterType,
ui: &mut egui::Ui, ui: &mut egui::Ui,
col: usize,
) -> NoteActionResponse { ) -> NoteActionResponse {
let mut timeline_res = None; let mut timeline_res = None;
let mut router_action = None; let mut router_action = None;
@@ -74,13 +76,16 @@ fn execute_note_action(
break 'ex; break 'ex;
}; };
let kind = TimelineKind::Thread(thread_selection); timeline_res = threads
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); .open(ndb, txn, pool, &thread_selection, preview, col)
// NOTE!!: you need the note_id to timeline root id thing .map(NotesOpenResult::Thread);
timeline_res = timeline_cache let route = Route::Thread(thread_selection);
.open(ndb, note_cache, txn, pool, &kind)
.map(NotesOpenResult::Timeline); router_action = Some(RouterAction::Overlay {
route,
make_new: preview,
});
} }
NoteAction::Hashtag(htag) => { NoteAction::Hashtag(htag) => {
let kind = TimelineKind::Hashtag(htag.clone()); let kind = TimelineKind::Hashtag(htag.clone());
@@ -151,10 +156,11 @@ fn execute_note_action(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn execute_and_process_note_action( pub fn execute_and_process_note_action(
action: NoteAction, action: NoteAction,
ndb: &Ndb, ndb: &mut Ndb,
columns: &mut Columns, columns: &mut Columns,
col: usize, col: usize,
timeline_cache: &mut TimelineCache, timeline_cache: &mut TimelineCache,
threads: &mut Threads,
note_cache: &mut NoteCache, note_cache: &mut NoteCache,
pool: &mut RelayPool, pool: &mut RelayPool,
txn: &Transaction, txn: &Transaction,
@@ -179,6 +185,7 @@ pub fn execute_and_process_note_action(
action, action,
ndb, ndb,
timeline_cache, timeline_cache,
threads,
note_cache, note_cache,
pool, pool,
txn, txn,
@@ -188,6 +195,7 @@ pub fn execute_and_process_note_action(
images, images,
router_type, router_type,
ui, ui,
col,
); );
if let Some(br) = resp.timeline_res { if let Some(br) = resp.timeline_res {
@@ -195,7 +203,9 @@ pub fn execute_and_process_note_action(
NotesOpenResult::Timeline(timeline_open_result) => { NotesOpenResult::Timeline(timeline_open_result) => {
timeline_open_result.process(ndb, note_cache, txn, timeline_cache, unknown_ids); 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, unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache, 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) { let timeline = if let Some(profile) = timeline_cache.timelines.get_mut(&self.id) {
profile profile

View File

@@ -8,7 +8,7 @@ use crate::{
storage, storage,
subscriptions::{SubKind, Subscriptions}, subscriptions::{SubKind, Subscriptions},
support::Support, support::Support,
timeline::{self, TimelineCache}, timeline::{self, thread::Threads, TimelineCache},
ui::{self, DesktopSidePanel}, ui::{self, DesktopSidePanel},
view_state::ViewState, view_state::ViewState,
Result, Result,
@@ -45,6 +45,7 @@ pub struct Damus {
pub subscriptions: Subscriptions, pub subscriptions: Subscriptions,
pub support: Support, pub support: Support,
pub jobs: JobsCache, pub jobs: JobsCache,
pub threads: Threads,
//frame_history: crate::frame_history::FrameHistory, //frame_history: crate::frame_history::FrameHistory,
@@ -443,6 +444,8 @@ impl Damus {
ctx.accounts.with_fallback(FALLBACK_PUBKEY()); ctx.accounts.with_fallback(FALLBACK_PUBKEY());
let threads = Threads::default();
Self { Self {
subscriptions: Subscriptions::default(), subscriptions: Subscriptions::default(),
since_optimize: parsed_args.since_optimize, since_optimize: parsed_args.since_optimize,
@@ -458,6 +461,7 @@ impl Damus {
debug, debug,
unrecognized_args, unrecognized_args,
jobs, jobs,
threads,
} }
} }
@@ -502,6 +506,7 @@ impl Damus {
decks_cache, decks_cache,
unrecognized_args: BTreeSet::default(), unrecognized_args: BTreeSet::default(),
jobs: JobsCache::default(), jobs: JobsCache::default(),
threads: Threads::default(),
} }
} }

View File

@@ -8,7 +8,10 @@ use crate::{
profile_state::ProfileState, profile_state::ProfileState,
relay_pool_manager::RelayPoolManager, relay_pool_manager::RelayPoolManager,
route::{Route, Router, SingletonRouter}, route::{Route, Router, SingletonRouter},
timeline::{route::render_timeline_route, TimelineCache}, timeline::{
route::{render_thread_route, render_timeline_route},
TimelineCache,
},
ui::{ ui::{
self, self,
add_column::render_add_column_routes, add_column::render_add_column_routes,
@@ -210,7 +213,7 @@ fn process_nav_resp(
if let Some(action) = response.action { if let Some(action) = response.action {
match action { match action {
NavAction::Returned(_) => { NavAction::Returned(return_type) => {
let r = app let r = app
.columns_mut(ctx.accounts) .columns_mut(ctx.accounts)
.column_mut(col) .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); process_result = Some(ProcessNavResult::SwitchOccurred);
} }
@@ -355,6 +364,7 @@ fn process_render_nav_action(
get_active_columns_mut(ctx.accounts, &mut app.decks_cache), get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
col, col,
&mut app.timeline_cache, &mut app.timeline_cache,
&mut app.threads,
ctx.note_cache, ctx.note_cache,
ctx.pool, ctx.pool,
&txn, &txn,
@@ -426,6 +436,17 @@ fn render_nav_body(
&mut note_context, &mut note_context,
&mut app.jobs, &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) => { Route::Accounts(amr) => {
let mut action = render_accounts_route( let mut action = render_accounts_route(
ui, ui,

View File

@@ -1,5 +1,5 @@
use enostr::{NoteId, Pubkey}; use enostr::{NoteId, Pubkey};
use notedeck::{NoteZapTargetOwned, WalletType}; use notedeck::{NoteZapTargetOwned, RootNoteIdBuf, WalletType};
use std::{ use std::{
fmt::{self}, fmt::{self},
ops::Range, ops::Range,
@@ -20,6 +20,7 @@ use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
pub enum Route { pub enum Route {
Timeline(TimelineKind), Timeline(TimelineKind),
Thread(ThreadSelection),
Accounts(AccountsRoute), Accounts(AccountsRoute),
Reply(NoteId), Reply(NoteId),
Quote(NoteId), Quote(NoteId),
@@ -53,7 +54,7 @@ impl Route {
} }
pub fn thread(thread_selection: ThreadSelection) -> Self { pub fn thread(thread_selection: ThreadSelection) -> Self {
Route::Timeline(TimelineKind::Thread(thread_selection)) Route::Thread(thread_selection)
} }
pub fn profile(pubkey: Pubkey) -> Self { pub fn profile(pubkey: Pubkey) -> Self {
@@ -79,6 +80,18 @@ impl Route {
pub fn serialize_tokens(&self, writer: &mut TokenWriter) { pub fn serialize_tokens(&self, writer: &mut TokenWriter) {
match self { match self {
Route::Timeline(timeline_kind) => timeline_kind.serialize_tokens(writer), 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::Accounts(routes) => routes.serialize_tokens(writer),
Route::AddColumn(routes) => routes.serialize_tokens(writer), Route::AddColumn(routes) => routes.serialize_tokens(writer),
Route::Search => writer.write_token("search"), Route::Search => writer.write_token("search"),
@@ -199,6 +212,31 @@ impl Route {
Ok(Route::Search) 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<'_> { pub fn title(&self) -> ColumnTitle<'_> {
match self { match self {
Route::Timeline(kind) => kind.to_title(), Route::Timeline(kind) => kind.to_title(),
Route::Thread(_) => ColumnTitle::simple("Thread"),
Route::Reply(_id) => ColumnTitle::simple("Reply"), Route::Reply(_id) => ColumnTitle::simple("Reply"),
Route::Quote(_id) => ColumnTitle::simple("Quote"), Route::Quote(_id) => ColumnTitle::simple("Quote"),
Route::Relays => ColumnTitle::simple("Relays"), Route::Relays => ColumnTitle::simple("Relays"),
@@ -423,9 +462,9 @@ impl fmt::Display for Route {
TimelineKind::Generic(_) => write!(f, "Custom"), TimelineKind::Generic(_) => write!(f, "Custom"),
TimelineKind::Search(_) => write!(f, "Search"), TimelineKind::Search(_) => write!(f, "Search"),
TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht), TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht),
TimelineKind::Thread(_id) => write!(f, "Thread"),
TimelineKind::Profile(_id) => write!(f, "Profile"), TimelineKind::Profile(_id) => write!(f, "Profile"),
}, },
Route::Thread(_) => write!(f, "Thread"),
Route::Reply(_id) => write!(f, "Reply"), Route::Reply(_id) => write!(f, "Reply"),
Route::Quote(_id) => write!(f, "Quote"), Route::Quote(_id) => write!(f, "Quote"),
Route::Relays => write!(f, "Relays"), 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);
}
}

View File

@@ -208,8 +208,6 @@ pub enum TimelineKind {
Profile(Pubkey), Profile(Pubkey),
Thread(ThreadSelection),
Universe, Universe,
/// Generic filter, references a hash of a filter /// Generic filter, references a hash of a filter
@@ -266,7 +264,6 @@ impl Display for TimelineKind {
TimelineKind::Profile(_) => f.write_str("Profile"), TimelineKind::Profile(_) => f.write_str("Profile"),
TimelineKind::Universe => f.write_str("Universe"), TimelineKind::Universe => f.write_str("Universe"),
TimelineKind::Hashtag(_) => f.write_str("Hashtag"), TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
TimelineKind::Thread(_) => f.write_str("Thread"),
TimelineKind::Search(_) => f.write_str("Search"), TimelineKind::Search(_) => f.write_str("Search"),
} }
} }
@@ -282,7 +279,6 @@ impl TimelineKind {
TimelineKind::Universe => None, TimelineKind::Universe => None,
TimelineKind::Generic(_) => None, TimelineKind::Generic(_) => None,
TimelineKind::Hashtag(_ht) => None, TimelineKind::Hashtag(_ht) => None,
TimelineKind::Thread(_ht) => None,
TimelineKind::Search(query) => query.author(), TimelineKind::Search(query) => query.author(),
} }
} }
@@ -298,7 +294,6 @@ impl TimelineKind {
TimelineKind::Universe => true, TimelineKind::Universe => true,
TimelineKind::Generic(_) => true, TimelineKind::Generic(_) => true,
TimelineKind::Hashtag(_ht) => true, TimelineKind::Hashtag(_ht) => true,
TimelineKind::Thread(_ht) => true,
TimelineKind::Search(_q) => true, TimelineKind::Search(_q) => true,
} }
} }
@@ -321,10 +316,6 @@ impl TimelineKind {
writer.write_token("profile"); writer.write_token("profile");
PubkeySource::pubkey(*pk).serialize_tokens(writer); 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 => { TimelineKind::Universe => {
writer.write_token("universe"); writer.write_token("universe");
} }
@@ -377,12 +368,6 @@ impl TimelineKind {
TokenParser::alt( TokenParser::alt(
parser, parser,
&[ &[
|p| {
p.parse_token("thread")?;
Ok(TimelineKind::Thread(ThreadSelection::from_root_id(
RootNoteIdBuf::new_unsafe(tokenator::parse_hex_id(p)?),
)))
},
|p| { |p| {
p.parse_token("universe")?; p.parse_token("universe")?;
Ok(TimelineKind::Universe) Ok(TimelineKind::Universe)
@@ -425,10 +410,6 @@ impl TimelineKind {
TimelineKind::Profile(pk) TimelineKind::Profile(pk)
} }
pub fn thread(selected_note: ThreadSelection) -> Self {
TimelineKind::Thread(selected_note)
}
pub fn is_notifications(&self) -> bool { pub fn is_notifications(&self) -> bool {
matches!(self, TimelineKind::Notifications(_)) matches!(self, TimelineKind::Notifications(_))
} }
@@ -474,17 +455,6 @@ impl TimelineKind {
todo!("implement generic filter lookups") 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() TimelineKind::Profile(pk) => FilterState::ready(vec![Filter::new()
.authors([pk.bytes()]) .authors([pk.bytes()])
.kinds([1]) .kinds([1])
@@ -510,8 +480,6 @@ impl TimelineKind {
TimelineTab::full_tabs(), TimelineTab::full_tabs(),
)), )),
TimelineKind::Thread(root_id) => Some(Timeline::thread(root_id)),
TimelineKind::Generic(_filter_id) => { TimelineKind::Generic(_filter_id) => {
warn!("you can't convert a TimelineKind::Generic to a Timeline"); warn!("you can't convert a TimelineKind::Generic to a Timeline");
// TODO: you actually can! just need to look up the filter id // 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::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Thread(_root_id) => ColumnTitle::simple("Thread"),
TimelineKind::Universe => ColumnTitle::simple("Universe"), TimelineKind::Universe => ColumnTitle::simple("Universe"),
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"), TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()), TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),

View File

@@ -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> { pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> {
let kind = 1; let kind = 1;
let notes_per_pk = 1; let notes_per_pk = 1;

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
nav::RenderNavAction, nav::RenderNavAction,
profile::ProfileAction, profile::ProfileAction,
timeline::{TimelineCache, TimelineKind}, timeline::{thread::Threads, ThreadSelection, TimelineCache, TimelineKind},
ui::{self, ProfileView}, ui::{self, ProfileView},
}; };
@@ -16,7 +16,7 @@ pub fn render_timeline_route(
accounts: &mut Accounts, accounts: &mut Accounts,
kind: &TimelineKind, kind: &TimelineKind,
col: usize, col: usize,
mut note_options: NoteOptions, note_options: NoteOptions,
depth: usize, depth: usize,
ui: &mut egui::Ui, ui: &mut egui::Ui,
note_context: &mut NoteContext, note_context: &mut NoteContext,
@@ -74,32 +74,40 @@ pub fn render_timeline_route(
note_action.map(RenderNavAction::NoteAction) 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);
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)
}
} }
} }
#[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(
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)] #[allow(clippy::too_many_arguments)]
pub fn render_profile_route( pub fn render_profile_route(
pubkey: &Pubkey, pubkey: &Pubkey,
@@ -139,30 +147,3 @@ pub fn render_profile_route(
None 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);
}
}

View File

@@ -1,6 +1,7 @@
use crate::column::ColumnsAction; use crate::column::ColumnsAction;
use crate::nav::RenderNavAction; use crate::nav::RenderNavAction;
use crate::nav::SwitchingAction; use crate::nav::SwitchingAction;
use crate::timeline::ThreadSelection;
use crate::{ use crate::{
column::Columns, column::Columns,
route::Route, route::Route,
@@ -437,11 +438,6 @@ impl<'a> NavTitle<'a> {
TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
TimelineKind::Thread(_) => {
// no pfp for threads
None
}
TimelineKind::Search(_sq) => { TimelineKind::Search(_sq) => {
// TODO: show author pfp if author field set? // 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::Search => Some(ui.add(ui::side_panel::search_button())),
Route::Wallet(_) => None, Route::Wallet(_) => None,
Route::CustomizeZapAmount(_) => 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 { fn title_label_value(title: &str) -> egui::Label {
egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())) egui::Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()))
.selectable(false) .selectable(false)

View File

@@ -3,21 +3,19 @@ use egui_virtual_list::VirtualList;
use enostr::KeypairUnowned; use enostr::KeypairUnowned;
use nostrdb::{Note, Transaction}; use nostrdb::{Note, Transaction};
use notedeck::note::root_note_id_from_selected_id; 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::jobs::JobsCache;
use notedeck_ui::note::NoteResponse; use notedeck_ui::note::NoteResponse;
use notedeck_ui::{NoteOptions, NoteView}; use notedeck_ui::{NoteOptions, NoteView};
use tracing::error;
use crate::timeline::thread::NoteSeenFlags; use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads};
use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind};
use crate::ui::timeline::TimelineTabView;
pub struct ThreadView<'a, 'd> { pub struct ThreadView<'a, 'd> {
timeline_cache: &'a mut TimelineCache, threads: &'a mut Threads,
unknown_ids: &'a mut UnknownIds, unknown_ids: &'a mut UnknownIds,
selected_note_id: &'a [u8; 32], selected_note_id: &'a [u8; 32],
note_options: NoteOptions, note_options: NoteOptions,
col: usize,
id_source: egui::Id, id_source: egui::Id,
is_muted: &'a MuteFun, is_muted: &'a MuteFun,
note_context: &'a mut NoteContext<'d>, note_context: &'a mut NoteContext<'d>,
@@ -28,7 +26,7 @@ pub struct ThreadView<'a, 'd> {
impl<'a, 'd> ThreadView<'a, 'd> { impl<'a, 'd> ThreadView<'a, 'd> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
timeline_cache: &'a mut TimelineCache, threads: &'a mut Threads,
unknown_ids: &'a mut UnknownIds, unknown_ids: &'a mut UnknownIds,
selected_note_id: &'a [u8; 32], selected_note_id: &'a [u8; 32],
note_options: NoteOptions, note_options: NoteOptions,
@@ -39,7 +37,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
) -> Self { ) -> Self {
let id_source = egui::Id::new("threadscroll_threadview"); let id_source = egui::Id::new("threadscroll_threadview");
ThreadView { ThreadView {
timeline_cache, threads,
unknown_ids, unknown_ids,
selected_note_id, selected_note_id,
note_options, note_options,
@@ -48,11 +46,13 @@ impl<'a, 'd> ThreadView<'a, 'd> {
note_context, note_context,
cur_acc, cur_acc,
jobs, jobs,
col: 0,
} }
} }
pub fn id_source(mut self, id: egui::Id) -> Self { pub fn id_source(mut self, col: usize) -> Self {
self.id_source = id; self.col = col;
self.id_source = egui::Id::new(("threadscroll", col));
self self
} }
@@ -73,62 +73,88 @@ impl<'a, 'd> ThreadView<'a, 'd> {
scroll_area = scroll_area.vertical_scroll_offset(offset); scroll_area = scroll_area.vertical_scroll_offset(offset);
} }
let output = scroll_area.show(ui, |ui| { let output = scroll_area.show(ui, |ui| self.notes(ui, &txn));
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 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}");
}
TimelineTabView::new(
thread_timeline.current_view(),
true,
self.note_options,
&txn,
self.is_muted,
self.note_context,
self.cur_acc,
self.jobs,
)
.show(ui)
});
ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y)); ui.data_mut(|d| d.insert_temp(offset_id, output.state.offset.y));
output.inner 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;
}
}
parent_state = ParentState::Unknown;
}
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);
}
}
let list = &mut self
.threads
.threads
.get_mut(&self.selected_note_id)
.unwrap()
.list;
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,
)
}
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]