From d0cfeee79fad348ff76e052d8516e3a4a665194e Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 26 Apr 2024 11:11:06 -0700 Subject: [PATCH 1/2] readme: make it clear that nix is optional for non-android dev Signed-off-by: William Casarin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a1bb917..3f5edc70 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ $ ./target/release/notedeck "$(cat queries/timeline.json)" "$(cat queries/notifi First, install [nix][nix] if you don't have it. -The `shell.nix` provides a reproducible build environment for android and rust. I recommend using [direnv][direnv] to load this environment when you `cd` into the directory. +The `shell.nix` provides a reproducible build environment, mainly for android but it also includes rust tools if you don't have those installed. It will likely work without nix if you are just looking to do non-android dev and have the rust toolchain already installed. If you decide to use nix, I recommend using [direnv][direnv] to load the nix shell environment when you `cd` into the directory. If you don't have [direnv][direnv], enter the dev shell via: From 26128c3456f1013a09a9bbf2d98965574dbfa232 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 28 Apr 2024 11:03:47 -0700 Subject: [PATCH 2/2] use egui_virtual_list for rendering absolutely insane performance increase Fixes: https://github.com/damus-io/notedeck/issues/32 Suggested-by: @lucasmerlin Signed-off-by: William Casarin --- Cargo.lock | 31 +++++++++-- Cargo.toml | 1 + queries/hashtags.json | 2 +- queries/notifications.json | 2 +- src/app.rs | 101 +++------------------------------- src/timeline.rs | 107 +++++++++++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 102 deletions(-) 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;