threads: add initial thread support
This is a really dumb and broken version of threads, but it will be our foundation for future changes. All it currently does is load whatever notes we have locally for a thread in chronological order. It currently does not open any subscriptions. It is not clear what is replying to what, but hey, its a start. Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
24
src/app.rs
24
src/app.rs
@@ -10,6 +10,7 @@ use crate::note::NoteRef;
|
|||||||
use crate::notecache::{CachedNote, NoteCache};
|
use crate::notecache::{CachedNote, NoteCache};
|
||||||
use crate::relay_pool_manager::RelayPoolManager;
|
use crate::relay_pool_manager::RelayPoolManager;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
|
use crate::thread::Threads;
|
||||||
use crate::timeline;
|
use crate::timeline;
|
||||||
use crate::timeline::{MergeKind, Timeline, ViewFilter};
|
use crate::timeline::{MergeKind, Timeline, ViewFilter};
|
||||||
use crate::ui::note::PostAction;
|
use crate::ui::note::PostAction;
|
||||||
@@ -53,10 +54,11 @@ pub struct Damus {
|
|||||||
|
|
||||||
pub timelines: Vec<Timeline>,
|
pub timelines: Vec<Timeline>,
|
||||||
pub selected_timeline: i32,
|
pub selected_timeline: i32,
|
||||||
pub drafts: Drafts,
|
|
||||||
|
|
||||||
pub img_cache: ImageCache,
|
|
||||||
pub ndb: Ndb,
|
pub ndb: Ndb,
|
||||||
|
pub drafts: Drafts,
|
||||||
|
pub threads: Threads,
|
||||||
|
pub img_cache: ImageCache,
|
||||||
pub account_manager: AccountManager,
|
pub account_manager: AccountManager,
|
||||||
|
|
||||||
frame_history: crate::frame_history::FrameHistory,
|
frame_history: crate::frame_history::FrameHistory,
|
||||||
@@ -820,6 +822,7 @@ impl Damus {
|
|||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
is_mobile,
|
is_mobile,
|
||||||
|
threads: Threads::default(),
|
||||||
drafts: Drafts::default(),
|
drafts: Drafts::default(),
|
||||||
state: DamusState::Initializing,
|
state: DamusState::Initializing,
|
||||||
img_cache: ImageCache::new(imgcache_dir),
|
img_cache: ImageCache::new(imgcache_dir),
|
||||||
@@ -849,6 +852,7 @@ impl Damus {
|
|||||||
config.set_ingester_threads(2);
|
config.set_ingester_threads(2);
|
||||||
Self {
|
Self {
|
||||||
is_mobile,
|
is_mobile,
|
||||||
|
threads: Threads::default(),
|
||||||
drafts: Drafts::default(),
|
drafts: Drafts::default(),
|
||||||
state: DamusState::Initializing,
|
state: DamusState::Initializing,
|
||||||
pool: RelayPool::new(),
|
pool: RelayPool::new(),
|
||||||
@@ -1013,11 +1017,6 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
Route::Thread(_key) => {
|
|
||||||
ui.label("thread view");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
Route::Relays => {
|
Route::Relays => {
|
||||||
let pool = &mut app_ctx.borrow_mut().pool;
|
let pool = &mut app_ctx.borrow_mut().pool;
|
||||||
let manager = RelayPoolManager::new(pool);
|
let manager = RelayPoolManager::new(pool);
|
||||||
@@ -1025,6 +1024,17 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route::Thread(id) => {
|
||||||
|
let app = &mut app_ctx.borrow_mut();
|
||||||
|
if let Ok(txn) = Transaction::new(&app.ndb) {
|
||||||
|
if let Ok(note) = app.ndb.get_note_by_id(&txn, id.bytes()) {
|
||||||
|
ui::ThreadView::new(app, timeline_ind, ¬e).ui(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
Route::Reply(id) => {
|
Route::Reply(id) => {
|
||||||
let mut app = app_ctx.borrow_mut();
|
let mut app = app_ctx.borrow_mut();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub mod relay_pool_manager;
|
|||||||
mod result;
|
mod result;
|
||||||
mod route;
|
mod route;
|
||||||
mod test_data;
|
mod test_data;
|
||||||
|
mod thread;
|
||||||
mod time;
|
mod time;
|
||||||
mod timecache;
|
mod timecache;
|
||||||
mod timeline;
|
mod timeline;
|
||||||
|
|||||||
84
src/thread.rs
Normal file
84
src/thread.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use crate::note::NoteRef;
|
||||||
|
use crate::timeline::{TimelineView, ViewFilter};
|
||||||
|
use nostrdb::{Ndb, Transaction};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Thread {
|
||||||
|
pub view: TimelineView,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = TimelineView::new_with_capacity(ViewFilter::NotesAndReplies, cap);
|
||||||
|
view.notes = notes;
|
||||||
|
|
||||||
|
Thread { view }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Threads {
|
||||||
|
threads: HashMap<[u8; 32], Thread>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Threads {
|
||||||
|
pub fn thread_mut(&mut self, ndb: &Ndb, txn: &Transaction, root_id: &[u8; 32]) -> &mut Thread {
|
||||||
|
// 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.threads.contains_key(root_id) {
|
||||||
|
return self.threads.get_mut(root_id).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// looks like we don't have this thread yet, populate it
|
||||||
|
// TODO: should we do this in the caller?
|
||||||
|
let root = if let Ok(root) = ndb.get_note_by_id(txn, root_id) {
|
||||||
|
root
|
||||||
|
} else {
|
||||||
|
debug!("couldnt find root note for id {}", hex::encode(root_id));
|
||||||
|
self.threads.insert(root_id.to_owned(), Thread::new(vec![]));
|
||||||
|
return self.threads.get_mut(root_id).unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
// we don't have the thread, query for it!
|
||||||
|
let filter = vec![
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds(vec![1])
|
||||||
|
.event(root.id())
|
||||||
|
.build(),
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds(vec![1])
|
||||||
|
.ids(vec![*root.id()])
|
||||||
|
.build(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: what should be the max results ?
|
||||||
|
let notes = if let Ok(mut results) = ndb.query(txn, filter, 10000) {
|
||||||
|
results.reverse();
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(NoteRef::from_query_result)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"got no results from thread lookup for {}",
|
||||||
|
hex::encode(root.id())
|
||||||
|
);
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("found thread with {} notes", notes.len());
|
||||||
|
self.threads.insert(root_id.to_owned(), Thread::new(notes));
|
||||||
|
self.threads.get_mut(root_id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
//fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread {
|
||||||
|
//}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ pub mod preview;
|
|||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
pub mod side_panel;
|
pub mod side_panel;
|
||||||
|
pub mod thread;
|
||||||
pub mod username;
|
pub mod username;
|
||||||
|
|
||||||
pub use account_management::AccountManagementView;
|
pub use account_management::AccountManagementView;
|
||||||
@@ -22,6 +23,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
|
|||||||
pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview};
|
pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview};
|
||||||
pub use relay::RelayView;
|
pub use relay::RelayView;
|
||||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||||
|
pub use thread::ThreadView;
|
||||||
pub use username::Username;
|
pub use username::Username;
|
||||||
|
|
||||||
use egui::Margin;
|
use egui::Margin;
|
||||||
|
|||||||
85
src/ui/thread.rs
Normal file
85
src/ui/thread.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use crate::{ui, Damus};
|
||||||
|
use nostrdb::{Note, NoteReply};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub struct ThreadView<'a> {
|
||||||
|
app: &'a mut Damus,
|
||||||
|
timeline: usize,
|
||||||
|
selected_note: &'a Note<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ThreadView<'a> {
|
||||||
|
pub fn new(app: &'a mut Damus, timeline: usize, selected_note: &'a Note<'a>) -> Self {
|
||||||
|
ThreadView {
|
||||||
|
app,
|
||||||
|
timeline,
|
||||||
|
selected_note,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let txn = self.selected_note.txn().unwrap();
|
||||||
|
let key = self.selected_note.key().unwrap();
|
||||||
|
let scroll_id = egui::Id::new((
|
||||||
|
"threadscroll",
|
||||||
|
self.app.timelines[self.timeline].selected_view,
|
||||||
|
self.timeline,
|
||||||
|
key,
|
||||||
|
));
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.")
|
||||||
|
.color(egui::Color32::RED),
|
||||||
|
);
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_source(scroll_id)
|
||||||
|
.animated(false)
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let root_id = NoteReply::new(self.selected_note.tags())
|
||||||
|
.root()
|
||||||
|
.map_or_else(|| self.selected_note.id(), |nr| nr.id);
|
||||||
|
|
||||||
|
let (len, list) = {
|
||||||
|
let thread = self.app.threads.thread_mut(&self.app.ndb, txn, root_id);
|
||||||
|
let len = thread.view.notes.len();
|
||||||
|
(len, &mut thread.view.list)
|
||||||
|
};
|
||||||
|
|
||||||
|
list.clone()
|
||||||
|
.borrow_mut()
|
||||||
|
.ui_custom_layout(ui, len, |ui, start_index| {
|
||||||
|
ui.spacing_mut().item_spacing.y = 0.0;
|
||||||
|
ui.spacing_mut().item_spacing.x = 4.0;
|
||||||
|
|
||||||
|
let note_key = {
|
||||||
|
let thread = self.app.threads.thread_mut(&self.app.ndb, txn, root_id);
|
||||||
|
thread.view.notes[start_index].key
|
||||||
|
};
|
||||||
|
|
||||||
|
let note = if let Ok(note) = self.app.ndb.get_note_by_key(txn, note_key) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
warn!("failed to query note {:?}", note_key);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui::padding(8.0, ui, |ui| {
|
||||||
|
let textmode = self.app.textmode;
|
||||||
|
let resp = ui::NoteView::new(self.app, ¬e)
|
||||||
|
.note_previews(!textmode)
|
||||||
|
.show(ui);
|
||||||
|
|
||||||
|
if let Some(action) = resp.action {
|
||||||
|
action.execute(self.app, self.timeline, note.id());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui::hline(ui);
|
||||||
|
//ui.add(egui::Separator::default().spacing(0.0));
|
||||||
|
|
||||||
|
1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user