From 16586006041e06e18f65fbe3ca4fa290cec22239 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:11:28 -0400 Subject: [PATCH 1/6] expose indexmap to notedeck Signed-off-by: kernelkind --- Cargo.lock | 1 + crates/notedeck/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6e32d21c..16980469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3527,6 +3527,7 @@ dependencies = [ "hashbrown 0.15.4", "hex", "image", + "indexmap 2.9.0", "jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "lightning-invoice", "md5", diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml index ed79826f..40b6dbe3 100644 --- a/crates/notedeck/Cargo.toml +++ b/crates/notedeck/Cargo.toml @@ -50,6 +50,7 @@ md5 = { workspace = true } bitflags = { workspace = true } regex = "1" chrono = { workspace = true } +indexmap = {workspace = true} [dev-dependencies] tempfile = { workspace = true } From 30c2ebdcc2c37cb6bf70a5c49a86101575a049b4 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:12:41 -0400 Subject: [PATCH 2/6] use indexmap Signed-off-by: kernelkind --- crates/notedeck/src/nip51_set.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/notedeck/src/nip51_set.rs b/crates/notedeck/src/nip51_set.rs index e5f05b0e..50603071 100644 --- a/crates/notedeck/src/nip51_set.rs +++ b/crates/notedeck/src/nip51_set.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; - use enostr::{Pubkey, RelayPool}; +use indexmap::IndexMap; use nostrdb::{Filter, Ndb, Note, Transaction}; use uuid::Uuid; @@ -10,7 +9,7 @@ use crate::{UnifiedSubscription, UnknownIds}; #[derive(Debug)] pub struct Nip51SetCache { pub sub: UnifiedSubscription, - cached_notes: HashMap, + cached_notes: IndexMap, } type PackId = String; @@ -24,7 +23,7 @@ impl Nip51SetCache { nip51_set_filter: Vec, ) -> Option { let subid = Uuid::new_v4().to_string(); - let mut cached_notes = HashMap::default(); + let mut cached_notes = IndexMap::default(); let notes: Option> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) { Some(results.into_iter().map(|r| r.note).collect()) @@ -77,7 +76,7 @@ impl Nip51SetCache { fn add( notes: Vec, - cache: &mut HashMap, + cache: &mut IndexMap, ndb: &Ndb, txn: &Transaction, unknown_ids: &mut UnknownIds, From 23f35c60bb53f52c5ec4b9044273a200aa6fe503 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:13:17 -0400 Subject: [PATCH 3/6] add Nip51SetCache helper methods Signed-off-by: kernelkind --- crates/notedeck/src/nip51_set.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/notedeck/src/nip51_set.rs b/crates/notedeck/src/nip51_set.rs index 50603071..6391d1e4 100644 --- a/crates/notedeck/src/nip51_set.rs +++ b/crates/notedeck/src/nip51_set.rs @@ -72,6 +72,18 @@ impl Nip51SetCache { pub fn iter(&self) -> impl IntoIterator { self.cached_notes.values() } + + pub fn len(&self) -> usize { + self.cached_notes.len() + } + + pub fn is_empty(&self) -> bool { + self.cached_notes.is_empty() + } + + pub fn at_index(&self, index: usize) -> Option<&Nip51Set> { + self.cached_notes.get_index(index).map(|(_, s)| s) + } } fn add( From 84e60e0642b56760bfa6fa49bd1c281fe21b30ad Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:14:02 -0400 Subject: [PATCH 4/6] add virtual list to `Onboarding` Signed-off-by: kernelkind --- crates/notedeck_columns/src/onboarding.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/notedeck_columns/src/onboarding.rs b/crates/notedeck_columns/src/onboarding.rs index 7ce1883f..2cd6774e 100644 --- a/crates/notedeck_columns/src/onboarding.rs +++ b/crates/notedeck_columns/src/onboarding.rs @@ -1,3 +1,6 @@ +use std::{cell::RefCell, rc::Rc}; + +use egui_virtual_list::VirtualList; use enostr::{Pubkey, RelayPool}; use nostrdb::{Filter, Ndb, NoteKey, Transaction}; use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds}; @@ -16,6 +19,7 @@ enum OnboardingState { #[derive(Default)] pub struct Onboarding { state: Option>, + pub list: Rc>, } impl Onboarding { From c06d18f76b9b1d0a7905e787530db2f453c320f7 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:14:52 -0400 Subject: [PATCH 5/6] prop `Onboarding` as mut Signed-off-by: kernelkind --- crates/notedeck_columns/src/accounts/mod.rs | 2 +- crates/notedeck_columns/src/nav.rs | 2 +- crates/notedeck_columns/src/ui/onboarding.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/notedeck_columns/src/accounts/mod.rs b/crates/notedeck_columns/src/accounts/mod.rs index 0e482ea0..dc1faa45 100644 --- a/crates/notedeck_columns/src/accounts/mod.rs +++ b/crates/notedeck_columns/src/accounts/mod.rs @@ -78,7 +78,7 @@ pub fn render_accounts_route( app_ctx: &mut AppContext, jobs: &mut JobsCache, login_state: &mut AcquireKeyState, - onboarding: &Onboarding, + onboarding: &mut Onboarding, follow_packs_ui: &mut Nip51SetUiCache, route: AccountsRoute, ) -> Option { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index d14594d7..83242233 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -591,7 +591,7 @@ fn render_nav_body( ctx, &mut app.jobs, &mut app.view_state.login, - &app.onboarding, + &mut app.onboarding, &mut app.view_state.follow_packs, *amr, ) else { diff --git a/crates/notedeck_columns/src/ui/onboarding.rs b/crates/notedeck_columns/src/ui/onboarding.rs index 21a718d6..8fc813f0 100644 --- a/crates/notedeck_columns/src/ui/onboarding.rs +++ b/crates/notedeck_columns/src/ui/onboarding.rs @@ -12,7 +12,7 @@ use crate::{onboarding::Onboarding, ui::widgets::styled_button}; /// Display Follow Packs for the user to choose from authors trusted by the Damus team pub struct FollowPackOnboardingView<'a> { - onboarding: &'a Onboarding, + onboarding: &'a mut Onboarding, ui_state: &'a mut Nip51SetUiCache, ndb: &'a Ndb, images: &'a mut Images, @@ -33,7 +33,7 @@ pub enum FollowPacksResponse { impl<'a> FollowPackOnboardingView<'a> { pub fn new( - onboarding: &'a Onboarding, + onboarding: &'a mut Onboarding, ui_state: &'a mut Nip51SetUiCache, ndb: &'a Ndb, images: &'a mut Images, From 8b5464641d140b137f05f4b5931587e545587c74 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 25 Aug 2025 21:16:05 -0400 Subject: [PATCH 6/6] render follow pack by index from virtual list Signed-off-by: kernelkind --- crates/notedeck_columns/src/ui/onboarding.rs | 49 ++++++----- crates/notedeck_ui/src/nip51_set.rs | 85 ++++++++++++++------ 2 files changed, 91 insertions(+), 43 deletions(-) diff --git a/crates/notedeck_columns/src/ui/onboarding.rs b/crates/notedeck_columns/src/ui/onboarding.rs index 8fc813f0..1c697bb5 100644 --- a/crates/notedeck_columns/src/ui/onboarding.rs +++ b/crates/notedeck_columns/src/ui/onboarding.rs @@ -5,7 +5,7 @@ use nostrdb::Ndb; use notedeck::{Images, JobPool, JobsCache, Localization}; use notedeck_ui::{ colors, - nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetFlags, Nip51SetWidgetResponse}, + nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags}, }; use crate::{onboarding::Onboarding, ui::widgets::styled_button}; @@ -71,24 +71,37 @@ impl<'a> FollowPackOnboardingView<'a> { .max_height(max_height) .show(ui, |ui| { egui::Frame::new().inner_margin(8.0).show(ui, |ui| { - if let Some(resp) = Nip51SetWidget::new( - follow_pack_state, - self.ui_state, - self.ndb, - self.loc, - self.images, - self.job_pool, - self.jobs, - ) - .with_flags(Nip51SetWidgetFlags::TRUST_IMAGES) - .ui(ui) - { - match resp { - Nip51SetWidgetResponse::ViewProfile(pubkey) => { - action = Some(OnboardingResponse::ViewProfile(pubkey)); + self.onboarding.list.borrow_mut().ui_custom_layout( + ui, + follow_pack_state.len(), + |ui, index| { + let resp = Nip51SetWidget::new( + follow_pack_state, + self.ui_state, + self.ndb, + self.loc, + self.images, + self.job_pool, + self.jobs, + ) + .with_flags(Nip51SetWidgetFlags::TRUST_IMAGES) + .render_at_index(ui, index); + + if let Some(cur_action) = resp.action { + match cur_action { + Nip51SetWidgetAction::ViewProfile(pubkey) => { + action = Some(OnboardingResponse::ViewProfile(pubkey)); + } + } } - } - } + + if resp.rendered { + 1 + } else { + 0 + } + }, + ); }) }); diff --git a/crates/notedeck_ui/src/nip51_set.rs b/crates/notedeck_ui/src/nip51_set.rs index f928089e..5dc9ed50 100644 --- a/crates/notedeck_ui/src/nip51_set.rs +++ b/crates/notedeck_ui/src/nip51_set.rs @@ -42,7 +42,7 @@ impl Default for Nip51SetWidgetFlags { } } -pub enum Nip51SetWidgetResponse { +pub enum Nip51SetWidgetAction { ViewProfile(Pubkey), } @@ -73,32 +73,62 @@ impl<'a> Nip51SetWidget<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + fn render_set(&mut self, ui: &mut egui::Ui, set: &Nip51Set) -> Nip51SetWidgetResponse { + if should_skip(set, &self.flags) { + return Nip51SetWidgetResponse { + action: None, + rendered: false, + }; + } + + let action = egui::Frame::new() + .corner_radius(CornerRadius::same(8)) + .fill(ui.visuals().extreme_bg_color) + .inner_margin(Margin::same(8)) + .show(ui, |ui| { + render_pack( + ui, + set, + self.ui_state, + self.ndb, + self.images, + self.job_pool, + self.jobs, + self.loc, + self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES), + ) + }) + .inner; + + Nip51SetWidgetResponse { + action, + rendered: true, + } + } + + pub fn render_at_index(&mut self, ui: &mut egui::Ui, index: usize) -> Nip51SetWidgetResponse { + let Some(set) = self.state.at_index(index) else { + return Nip51SetWidgetResponse { + action: None, + rendered: false, + }; + }; + + self.render_set(ui, set) + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { let mut resp = None; for pack in self.state.iter() { - if should_skip(pack, &self.flags) { - continue; + let res = self.render_set(ui, pack); + + if let Some(action) = res.action { + resp = Some(action); } - egui::Frame::new() - .corner_radius(CornerRadius::same(8)) - .fill(ui.visuals().extreme_bg_color) - .inner_margin(Margin::same(8)) - .show(ui, |ui| { - if let Some(cur_resp) = render_pack( - ui, - pack, - self.ui_state, - self.ndb, - self.images, - self.job_pool, - self.jobs, - self.loc, - self.flags.contains(Nip51SetWidgetFlags::TRUST_IMAGES), - ) { - resp = Some(cur_resp); - } - }); + if !res.rendered { + continue; + } ui.add_space(8.0); } @@ -107,6 +137,11 @@ impl<'a> Nip51SetWidget<'a> { } } +pub struct Nip51SetWidgetResponse { + pub action: Option, + pub rendered: bool, +} + fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool { (required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none()) || (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none()) @@ -126,7 +161,7 @@ fn render_pack( jobs: &mut JobsCache, loc: &mut Localization, image_trusted: bool, -) -> Option { +) -> Option { let max_img_size = vec2(ui.available_width(), 200.0); ui.allocate_new_ui(UiBuilder::new(), |ui| 's: { @@ -210,7 +245,7 @@ fn render_pack( ui.separator(); if render_profile_item(ui, images, m_profile.as_ref(), cur_state) { - resp = Some(Nip51SetWidgetResponse::ViewProfile(*pk)); + resp = Some(Nip51SetWidgetAction::ViewProfile(*pk)); } }