diff --git a/Cargo.lock b/Cargo.lock index 5f36c7b0..d27ad74b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,7 +1055,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "web-time", + "web-time 0.2.4", "wgpu", "winapi", "winit", @@ -1090,7 +1090,7 @@ dependencies = [ "puffin", "thiserror", "type-map", - "web-time", + "web-time 0.2.4", "wgpu", "winit", ] @@ -1107,7 +1107,7 @@ dependencies = [ "puffin", "raw-window-handle 0.6.0", "smithay-clipboard", - "web-time", + "web-time 0.2.4", "webbrowser", "winit", ] @@ -1145,6 +1145,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "egui_virtual_list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142d3a0ad2ae4743e323ad1cb384bffd45abc36dac6f9833f0980c2b4d76af1a" +dependencies = [ + "egui", + "web-time 1.1.0", +] + [[package]] name = "ehttp" version = "0.2.0" @@ -2580,6 +2590,7 @@ dependencies = [ "eframe", "egui", "egui_extras", + "egui_virtual_list", "ehttp 0.2.0", "enostr", "env_logger 0.10.2", @@ -3059,7 +3070,7 @@ dependencies = [ "puffin", "time", "vec1", - "web-time", + "web-time 0.2.4", ] [[package]] @@ -4679,6 +4690,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webbrowser" version = "0.8.12" @@ -5156,7 +5177,7 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time", + "web-time 0.2.4", "windows-sys 0.48.0", "x11-dl", "x11rb 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index 3fa2596e..2cb5c0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ nostr-sdk = "0.29.0" strum = "0.26" strum_macros = "0.26" bitflags = "2.5.0" +egui_virtual_list = "0.3.0" [features] diff --git a/queries/hashtags.json b/queries/hashtags.json index 565cbd28..a12b04c2 100644 --- a/queries/hashtags.json +++ b/queries/hashtags.json @@ -1,4 +1,4 @@ -[{"limit": 100, +[{"limit": 1000, "kinds": [ 1 ], diff --git a/queries/notifications.json b/queries/notifications.json index 650236db..35bc9e5c 100644 --- a/queries/notifications.json +++ b/queries/notifications.json @@ -1 +1 @@ -[{"limit": 100, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}] +[{"limit": 1000, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}] diff --git a/src/app.rs b/src/app.rs index b0d0cfd8..0fc38a3d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,18 +5,16 @@ use crate::frame_history::FrameHistory; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; use crate::timeline; -use crate::ui; +use crate::timeline::{NoteRef, Timeline}; use crate::ui::is_mobile; use crate::Result; -use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Context, Frame, Margin, Style}; use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage}; -use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Subscription, Transaction}; +use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Transaction}; -use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::path::Path; @@ -31,47 +29,6 @@ pub enum DamusState { Initialized, } -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub struct NoteRef { - pub key: NoteKey, - pub created_at: u64, -} - -impl Ord for NoteRef { - fn cmp(&self, other: &Self) -> Ordering { - match self.created_at.cmp(&other.created_at) { - Ordering::Equal => self.key.cmp(&other.key), - Ordering::Less => Ordering::Greater, - Ordering::Greater => Ordering::Less, - } - } -} - -impl PartialOrd for NoteRef { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -struct Timeline { - pub filter: Vec, - pub notes: Vec, - pub subscription: Option, -} - -impl Timeline { - pub fn new(filter: Vec) -> Self { - let notes: Vec = Vec::with_capacity(1000); - let subscription: Option = None; - - Timeline { - filter, - notes, - subscription, - } - } -} - /// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct Damus { state: DamusState, @@ -80,7 +37,7 @@ pub struct Damus { pool: RelayPool, pub textmode: bool, - timelines: Vec, + pub timelines: Vec, pub img_cache: ImageCache, pub ndb: Ndb, @@ -557,52 +514,6 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { } */ -fn render_notes(ui: &mut egui::Ui, damus: &mut Damus, timeline: usize) -> Result<()> { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - let num_notes = damus.timelines[timeline].notes.len(); - let txn = Transaction::new(&damus.ndb)?; - - for i in 0..num_notes { - let note_key = damus.timelines[timeline].notes[i].key; - let note = if let Ok(note) = damus.ndb.get_note_by_key(&txn, note_key) { - note - } else { - warn!("failed to query note {:?}", note_key); - continue; - }; - - let note_ui = ui::Note::new(damus, ¬e); - ui.add(note_ui); - ui.add(egui::Separator::default().spacing(0.0)); - } - - Ok(()) -} - -fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) { - //padding(4.0, ui, |ui| ui.heading("Notifications")); - /* - let font_id = egui::TextStyle::Body.resolve(ui.style()); - let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; - */ - - egui::ScrollArea::vertical() - .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) - //.auto_shrink([false; 2]) - /* - .show_viewport(ui, |ui, viewport| { - render_notes_in_viewport(ui, app, viewport, row_height, font_id); - }); - */ - .show(ui, |ui| { - ui.spacing_mut().item_spacing.y = 0.0; - ui.spacing_mut().item_spacing.x = 4.0; - let _ = render_notes(ui, app, timeline); - }); -} - fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel { let top_margin = egui::Margin { top: 4.0, @@ -684,7 +595,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { puffin::profile_function!(); main_panel(&ctx.style()).show(ctx, |ui| { - timeline_view(ui, app, 0); + timeline::timeline_view(ui, app, 0); }); } @@ -713,7 +624,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { if app.timelines.len() == 1 { main_panel(&ctx.style()).show(ctx, |ui| { - timeline_view(ui, app, 0); + timeline::timeline_view(ui, app, 0); }); return; @@ -737,7 +648,7 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: us .clip(true) .horizontal(|mut strip| { for timeline_ind in 0..timelines { - strip.cell(|ui| timeline_view(ui, app, timeline_ind)); + strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind)); } }); } diff --git a/src/timeline.rs b/src/timeline.rs index e22bd26f..f3eef424 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -1,3 +1,110 @@ +use crate::{ui, Damus}; +use egui::containers::scroll_area::ScrollBarVisibility; +use egui_virtual_list::VirtualList; +use enostr::Filter; +use nostrdb::{NoteKey, Subscription, Transaction}; +use std::cmp::Ordering; +use std::sync::{Arc, Mutex}; + +use log::warn; + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct NoteRef { + pub key: NoteKey, + pub created_at: u64, +} + +impl Ord for NoteRef { + fn cmp(&self, other: &Self) -> Ordering { + match self.created_at.cmp(&other.created_at) { + Ordering::Equal => self.key.cmp(&other.key), + Ordering::Less => Ordering::Greater, + Ordering::Greater => Ordering::Less, + } + } +} + +impl PartialOrd for NoteRef { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +pub struct Timeline { + pub filter: Vec, + pub notes: Vec, + + /// Our nostrdb subscription + pub subscription: Option, + + /// State for our virtual list egui widget + pub list: Arc>, +} + +impl Timeline { + pub fn new(filter: Vec) -> Self { + let notes: Vec = Vec::with_capacity(1000); + let subscription: Option = None; + let list = Arc::new(Mutex::new(VirtualList::new())); + + Timeline { + filter, + notes, + subscription, + list, + } + } +} + +pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) { + //padding(4.0, ui, |ui| ui.heading("Notifications")); + /* + let font_id = egui::TextStyle::Body.resolve(ui.style()); + let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; + */ + + egui::ScrollArea::vertical() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) + //.auto_shrink([false; 2]) + /* + .show_viewport(ui, |ui, viewport| { + render_notes_in_viewport(ui, app, viewport, row_height, font_id); + }); + */ + .show(ui, |ui| { + let len = app.timelines[timeline].notes.len(); + let list = app.timelines[timeline].list.clone(); + list.lock() + .unwrap() + .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 = app.timelines[timeline].notes[start_index].key; + + let txn = if let Ok(txn) = Transaction::new(&app.ndb) { + txn + } else { + warn!("failed to create transaction for {:?}", note_key); + return 0; + }; + + let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) { + note + } else { + warn!("failed to query note {:?}", note_key); + return 0; + }; + + let note_ui = ui::Note::new(app, ¬e); + ui.add(note_ui); + ui.add(egui::Separator::default().spacing(0.0)); + + 1 + }); + }); +} + pub fn merge_sorted_vecs(vec1: &[T], vec2: &[T]) -> Vec { let mut merged = Vec::with_capacity(vec1.len() + vec2.len()); let mut i = 0;