Merge remote-tracking branch 'github/pr/1087' into notifications

This commit is contained in:
William Casarin
2025-08-26 09:58:50 -07:00
8 changed files with 117 additions and 52 deletions

1
Cargo.lock generated
View File

@@ -3527,6 +3527,7 @@ dependencies = [
"hashbrown 0.15.4", "hashbrown 0.15.4",
"hex", "hex",
"image", "image",
"indexmap 2.9.0",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lightning-invoice", "lightning-invoice",
"md5", "md5",

View File

@@ -50,6 +50,7 @@ md5 = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
regex = "1" regex = "1"
chrono = { workspace = true } chrono = { workspace = true }
indexmap = {workspace = true}
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@@ -1,6 +1,5 @@
use std::collections::HashMap;
use enostr::{Pubkey, RelayPool}; use enostr::{Pubkey, RelayPool};
use indexmap::IndexMap;
use nostrdb::{Filter, Ndb, Note, Transaction}; use nostrdb::{Filter, Ndb, Note, Transaction};
use uuid::Uuid; use uuid::Uuid;
@@ -10,7 +9,7 @@ use crate::{UnifiedSubscription, UnknownIds};
#[derive(Debug)] #[derive(Debug)]
pub struct Nip51SetCache { pub struct Nip51SetCache {
pub sub: UnifiedSubscription, pub sub: UnifiedSubscription,
cached_notes: HashMap<PackId, Nip51Set>, cached_notes: IndexMap<PackId, Nip51Set>,
} }
type PackId = String; type PackId = String;
@@ -24,7 +23,7 @@ impl Nip51SetCache {
nip51_set_filter: Vec<Filter>, nip51_set_filter: Vec<Filter>,
) -> Option<Self> { ) -> Option<Self> {
let subid = Uuid::new_v4().to_string(); let subid = Uuid::new_v4().to_string();
let mut cached_notes = HashMap::default(); let mut cached_notes = IndexMap::default();
let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) { let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) {
Some(results.into_iter().map(|r| r.note).collect()) Some(results.into_iter().map(|r| r.note).collect())
@@ -73,11 +72,23 @@ impl Nip51SetCache {
pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> { pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> {
self.cached_notes.values() 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( fn add(
notes: Vec<Note>, notes: Vec<Note>,
cache: &mut HashMap<PackId, Nip51Set>, cache: &mut IndexMap<PackId, Nip51Set>,
ndb: &Ndb, ndb: &Ndb,
txn: &Transaction, txn: &Transaction,
unknown_ids: &mut UnknownIds, unknown_ids: &mut UnknownIds,

View File

@@ -78,7 +78,7 @@ pub fn render_accounts_route(
app_ctx: &mut AppContext, app_ctx: &mut AppContext,
jobs: &mut JobsCache, jobs: &mut JobsCache,
login_state: &mut AcquireKeyState, login_state: &mut AcquireKeyState,
onboarding: &Onboarding, onboarding: &mut Onboarding,
follow_packs_ui: &mut Nip51SetUiCache, follow_packs_ui: &mut Nip51SetUiCache,
route: AccountsRoute, route: AccountsRoute,
) -> Option<AccountsResponse> { ) -> Option<AccountsResponse> {

View File

@@ -591,7 +591,7 @@ fn render_nav_body(
ctx, ctx,
&mut app.jobs, &mut app.jobs,
&mut app.view_state.login, &mut app.view_state.login,
&app.onboarding, &mut app.onboarding,
&mut app.view_state.follow_packs, &mut app.view_state.follow_packs,
*amr, *amr,
) else { ) else {

View File

@@ -1,3 +1,6 @@
use std::{cell::RefCell, rc::Rc};
use egui_virtual_list::VirtualList;
use enostr::{Pubkey, RelayPool}; use enostr::{Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, NoteKey, Transaction}; use nostrdb::{Filter, Ndb, NoteKey, Transaction};
use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds}; use notedeck::{create_nip51_set, filter::default_limit, Nip51SetCache, UnknownIds};
@@ -16,6 +19,7 @@ enum OnboardingState {
#[derive(Default)] #[derive(Default)]
pub struct Onboarding { pub struct Onboarding {
state: Option<Result<OnboardingState, OnboardingError>>, state: Option<Result<OnboardingState, OnboardingError>>,
pub list: Rc<RefCell<VirtualList>>,
} }
impl Onboarding { impl Onboarding {

View File

@@ -5,14 +5,14 @@ use nostrdb::Ndb;
use notedeck::{Images, JobPool, JobsCache, Localization}; use notedeck::{Images, JobPool, JobsCache, Localization};
use notedeck_ui::{ use notedeck_ui::{
colors, colors,
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetFlags, Nip51SetWidgetResponse}, nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
}; };
use crate::{onboarding::Onboarding, ui::widgets::styled_button}; use crate::{onboarding::Onboarding, ui::widgets::styled_button};
/// Display Follow Packs for the user to choose from authors trusted by the Damus team /// Display Follow Packs for the user to choose from authors trusted by the Damus team
pub struct FollowPackOnboardingView<'a> { pub struct FollowPackOnboardingView<'a> {
onboarding: &'a Onboarding, onboarding: &'a mut Onboarding,
ui_state: &'a mut Nip51SetUiCache, ui_state: &'a mut Nip51SetUiCache,
ndb: &'a Ndb, ndb: &'a Ndb,
images: &'a mut Images, images: &'a mut Images,
@@ -33,7 +33,7 @@ pub enum FollowPacksResponse {
impl<'a> FollowPackOnboardingView<'a> { impl<'a> FollowPackOnboardingView<'a> {
pub fn new( pub fn new(
onboarding: &'a Onboarding, onboarding: &'a mut Onboarding,
ui_state: &'a mut Nip51SetUiCache, ui_state: &'a mut Nip51SetUiCache,
ndb: &'a Ndb, ndb: &'a Ndb,
images: &'a mut Images, images: &'a mut Images,
@@ -71,24 +71,37 @@ impl<'a> FollowPackOnboardingView<'a> {
.max_height(max_height) .max_height(max_height)
.show(ui, |ui| { .show(ui, |ui| {
egui::Frame::new().inner_margin(8.0).show(ui, |ui| { egui::Frame::new().inner_margin(8.0).show(ui, |ui| {
if let Some(resp) = Nip51SetWidget::new( self.onboarding.list.borrow_mut().ui_custom_layout(
follow_pack_state, ui,
self.ui_state, follow_pack_state.len(),
self.ndb, |ui, index| {
self.loc, let resp = Nip51SetWidget::new(
self.images, follow_pack_state,
self.job_pool, self.ui_state,
self.jobs, self.ndb,
) self.loc,
.with_flags(Nip51SetWidgetFlags::TRUST_IMAGES) self.images,
.ui(ui) self.job_pool,
{ self.jobs,
match resp { )
Nip51SetWidgetResponse::ViewProfile(pubkey) => { .with_flags(Nip51SetWidgetFlags::TRUST_IMAGES)
action = Some(OnboardingResponse::ViewProfile(pubkey)); .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
}
},
);
}) })
}); });

View File

@@ -42,7 +42,7 @@ impl Default for Nip51SetWidgetFlags {
} }
} }
pub enum Nip51SetWidgetResponse { pub enum Nip51SetWidgetAction {
ViewProfile(Pubkey), ViewProfile(Pubkey),
} }
@@ -73,32 +73,62 @@ impl<'a> Nip51SetWidget<'a> {
self self
} }
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Nip51SetWidgetResponse> { 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<Nip51SetWidgetAction> {
let mut resp = None; let mut resp = None;
for pack in self.state.iter() { for pack in self.state.iter() {
if should_skip(pack, &self.flags) { let res = self.render_set(ui, pack);
continue;
if let Some(action) = res.action {
resp = Some(action);
} }
egui::Frame::new() if !res.rendered {
.corner_radius(CornerRadius::same(8)) continue;
.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);
}
});
ui.add_space(8.0); ui.add_space(8.0);
} }
@@ -107,6 +137,11 @@ impl<'a> Nip51SetWidget<'a> {
} }
} }
pub struct Nip51SetWidgetResponse {
pub action: Option<Nip51SetWidgetAction>,
pub rendered: bool,
}
fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool { fn should_skip(set: &Nip51Set, required: &Nip51SetWidgetFlags) -> bool {
(required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none()) (required.contains(Nip51SetWidgetFlags::REQUIRES_TITLE) && set.title.is_none())
|| (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none()) || (required.contains(Nip51SetWidgetFlags::REQUIRES_IMAGE) && set.image.is_none())
@@ -126,7 +161,7 @@ fn render_pack(
jobs: &mut JobsCache, jobs: &mut JobsCache,
loc: &mut Localization, loc: &mut Localization,
image_trusted: bool, image_trusted: bool,
) -> Option<Nip51SetWidgetResponse> { ) -> Option<Nip51SetWidgetAction> {
let max_img_size = vec2(ui.available_width(), 200.0); let max_img_size = vec2(ui.available_width(), 200.0);
ui.allocate_new_ui(UiBuilder::new(), |ui| 's: { ui.allocate_new_ui(UiBuilder::new(), |ui| 's: {
@@ -210,7 +245,7 @@ fn render_pack(
ui.separator(); ui.separator();
if render_profile_item(ui, images, m_profile.as_ref(), cur_state) { if render_profile_item(ui, images, m_profile.as_ref(), cur_state) {
resp = Some(Nip51SetWidgetResponse::ViewProfile(*pk)); resp = Some(Nip51SetWidgetAction::ViewProfile(*pk));
} }
} }