Skip to content

Commit

Permalink
threads: add initial thread support
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
jb55 committed Jul 16, 2024
1 parent 469a67b commit 6c97287
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 7 deletions.
24 changes: 17 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::note::NoteRef;
use crate::notecache::{CachedNote, NoteCache};
use crate::relay_pool_manager::RelayPoolManager;
use crate::route::Route;
use crate::thread::Threads;
use crate::timeline;
use crate::timeline::{MergeKind, Timeline, ViewFilter};
use crate::ui::note::PostAction;
Expand Down Expand Up @@ -53,10 +54,11 @@ pub struct Damus {

pub timelines: Vec<Timeline>,
pub selected_timeline: i32,
pub drafts: Drafts,

pub img_cache: ImageCache,
pub ndb: Ndb,
pub drafts: Drafts,
pub threads: Threads,
pub img_cache: ImageCache,
pub account_manager: AccountManager,

frame_history: crate::frame_history::FrameHistory,
Expand Down Expand Up @@ -820,6 +822,7 @@ impl Damus {
Self {
pool,
is_mobile,
threads: Threads::default(),
drafts: Drafts::default(),
state: DamusState::Initializing,
img_cache: ImageCache::new(imgcache_dir),
Expand Down Expand Up @@ -849,6 +852,7 @@ impl Damus {
config.set_ingester_threads(2);
Self {
is_mobile,
threads: Threads::default(),
drafts: Drafts::default(),
state: DamusState::Initializing,
pool: RelayPool::new(),
Expand Down Expand Up @@ -1013,18 +1017,24 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
None
}

Route::Thread(_key) => {
ui.label("thread view");
None
}

Route::Relays => {
let pool = &mut app_ctx.borrow_mut().pool;
let manager = RelayPoolManager::new(pool);
RelayView::new(manager).ui(ui);
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, &note).ui(ui);
}
}

None
}

Route::Reply(id) => {
let mut app = app_ctx.borrow_mut();

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod relay_pool_manager;
mod result;
mod route;
mod test_data;
mod thread;
mod time;
mod timecache;
mod timeline;
Expand Down
84 changes: 84 additions & 0 deletions src/thread.rs
Original file line number Diff line number Diff line change
@@ -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 {
//}
}
2 changes: 2 additions & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod preview;
pub mod profile;
pub mod relay;
pub mod side_panel;
pub mod thread;
pub mod username;

pub use account_management::AccountManagementView;
Expand All @@ -22,6 +23,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview};
pub use relay::RelayView;
pub use side_panel::{DesktopSidePanel, SidePanelAction};
pub use thread::ThreadView;
pub use username::Username;

use egui::Margin;
Expand Down
85 changes: 85 additions & 0 deletions src/ui/thread.rs
Original file line number Diff line number Diff line change
@@ -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, &note)
.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
});
});
}
}

0 comments on commit 6c97287

Please sign in to comment.