notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 28e2e7edd53fdf62636380a102b953ee52654704
parent 69054d71ca020ad74cc5d99d7f3064e0f2a5f5db
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 28 Apr 2024 17:55:29 -0700

Merge remote-tracking branch 'github/virtual-list'

Diffstat:
MCargo.lock | 31++++++++++++++++++++++++++-----
MCargo.toml | 1+
MREADME.md | 2+-
Mqueries/hashtags.json | 2+-
Mqueries/notifications.json | 2+-
Msrc/app.rs | 101+++++--------------------------------------------------------------------------
Msrc/timeline.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 143 insertions(+), 103 deletions(-)

diff --git 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", ] @@ -1146,6 +1146,16 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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]] @@ -4680,6 +4691,16 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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 @@ -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/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: diff --git 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 @@ -1 +1 @@ -[{"limit": 100, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}] +[{"limit": 1000, "kinds":[1], "#p": ["32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]}] diff --git 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<Ordering> { - Some(self.cmp(other)) - } -} - -struct Timeline { - pub filter: Vec<Filter>, - pub notes: Vec<NoteRef>, - pub subscription: Option<Subscription>, -} - -impl Timeline { - pub fn new(filter: Vec<Filter>) -> Self { - let notes: Vec<NoteRef> = Vec::with_capacity(1000); - let subscription: Option<Subscription> = 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<Timeline>, + pub timelines: Vec<Timeline>, 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, &note); - 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 @@ -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<Ordering> { + Some(self.cmp(other)) + } +} + +pub struct Timeline { + pub filter: Vec<Filter>, + pub notes: Vec<NoteRef>, + + /// Our nostrdb subscription + pub subscription: Option<Subscription>, + + /// State for our virtual list egui widget + pub list: Arc<Mutex<VirtualList>>, +} + +impl Timeline { + pub fn new(filter: Vec<Filter>) -> Self { + let notes: Vec<NoteRef> = Vec::with_capacity(1000); + let subscription: Option<Subscription> = 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, &note); + ui.add(note_ui); + ui.add(egui::Separator::default().spacing(0.0)); + + 1 + }); + }); +} + pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> Vec<T> { let mut merged = Vec::with_capacity(vec1.len() + vec2.len()); let mut i = 0;