notedeck

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

commit f84c97686e50e2059cac31d4849faf7cc23c13c9
parent 1f15df9ca7b5764a28c26d34ada5183b5b66ead8
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 25 Feb 2026 14:08:11 -0800

nostrverse: quick refactors — parse_vec3, self_user, subscription dedup

- Extract parse_vec3() helper in nostr_events.rs, replacing duplicated
  Vec3 parsing in parse_presence_position and parse_presence_velocity
- Add self_user()/self_user_mut() to NostrverseState, replacing 4
  inline .users.iter().find(|u| u.is_self) patterns in lib.rs
- Unify RoomSubscription and PresenceSubscription via shared
  KindSubscription base, deduplicating new() and poll() logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_nostrverse/src/lib.rs | 17++++-------------
Mcrates/notedeck_nostrverse/src/nostr_events.rs | 23+++++++++++------------
Mcrates/notedeck_nostrverse/src/room_state.rs | 10++++++++++
Mcrates/notedeck_nostrverse/src/subscriptions.rs | 58+++++++++++++++++++++++++++++++++++-----------------------
4 files changed, 60 insertions(+), 48 deletions(-)

diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs @@ -274,9 +274,7 @@ impl NostrverseApp { if let Some(renderer) = &self.renderer { let self_pos = self .state - .users - .iter() - .find(|u| u.is_self) + .self_user() .map(|u| u.position) .unwrap_or(Vec3::ZERO); let mut r = renderer.renderer.lock().unwrap(); @@ -497,9 +495,7 @@ impl NostrverseApp { if let Some(kp) = ctx.accounts.selected_filled() { let self_pos = self .state - .users - .iter() - .find(|u| u.is_self) + .self_user() .map(|u| u.position) .unwrap_or(Vec3::ZERO); @@ -525,12 +521,7 @@ impl NostrverseApp { // Assign avatar model to new users if changed { - let avatar_model = self - .state - .users - .iter() - .find(|u| u.is_self) - .and_then(|u| u.model_handle); + let avatar_model = self.state.self_user().and_then(|u| u.model_handle); if let Some(model) = avatar_model { for user in &mut self.state.users { if user.model_handle.is_none() { @@ -602,7 +593,7 @@ impl NostrverseApp { // Update self-user's position from the controller if let Some(pos) = avatar_pos - && let Some(self_user) = self.state.users.iter_mut().find(|u| u.is_self) + && let Some(self_user) = self.state.self_user_mut() { self_user.position = pos; self_user.display_position = pos; diff --git a/crates/notedeck_nostrverse/src/nostr_events.rs b/crates/notedeck_nostrverse/src/nostr_events.rs @@ -103,27 +103,26 @@ pub fn build_presence_event<'a>( .tag_str(&exp_str) } -/// Parse a presence event's position tag into a Vec3. -pub fn parse_presence_position(note: &Note<'_>) -> Option<glam::Vec3> { - let pos_str = get_tag_value(note, "position")?; - let mut parts = pos_str.split_whitespace(); +/// Parse a whitespace-separated "x y z" string into a Vec3. +fn parse_vec3(s: &str) -> Option<glam::Vec3> { + let mut parts = s.split_whitespace(); let x: f32 = parts.next()?.parse().ok()?; let y: f32 = parts.next()?.parse().ok()?; let z: f32 = parts.next()?.parse().ok()?; Some(glam::Vec3::new(x, y, z)) } +/// Parse a presence event's position tag into a Vec3. +pub fn parse_presence_position(note: &Note<'_>) -> Option<glam::Vec3> { + parse_vec3(get_tag_value(note, "position")?) +} + /// Parse a presence event's velocity tag into a Vec3. /// Returns Vec3::ZERO if no velocity tag (backward compatible with old events). pub fn parse_presence_velocity(note: &Note<'_>) -> glam::Vec3 { - let Some(vel_str) = get_tag_value(note, "velocity") else { - return glam::Vec3::ZERO; - }; - let mut parts = vel_str.split_whitespace(); - let x: f32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0.0); - let y: f32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0.0); - let z: f32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0.0); - glam::Vec3::new(x, y, z) + get_tag_value(note, "velocity") + .and_then(parse_vec3) + .unwrap_or(glam::Vec3::ZERO) } /// Extract the "a" tag (space naddr) from a presence note. diff --git a/crates/notedeck_nostrverse/src/room_state.rs b/crates/notedeck_nostrverse/src/room_state.rs @@ -291,4 +291,14 @@ impl NostrverseState { pub fn get_object_mut(&mut self, id: &str) -> Option<&mut RoomObject> { self.objects.iter_mut().find(|o| o.id == id) } + + /// Get the local user + pub fn self_user(&self) -> Option<&RoomUser> { + self.users.iter().find(|u| u.is_self) + } + + /// Get the local user mutably + pub fn self_user_mut(&mut self) -> Option<&mut RoomUser> { + self.users.iter_mut().find(|u| u.is_self) + } } diff --git a/crates/notedeck_nostrverse/src/subscriptions.rs b/crates/notedeck_nostrverse/src/subscriptions.rs @@ -1,25 +1,43 @@ //! Local nostrdb subscription management for nostrverse rooms. //! -//! Subscribes to room events (kind 37555) in the local nostrdb and -//! polls for updates each frame. No remote relay subscriptions — rooms -//! are local-only for now. +//! Subscribes to room events (kind 37555) and presence events (kind 10555) +//! in the local nostrdb and polls for updates each frame. use nostrdb::{Filter, Ndb, Note, Subscription, Transaction}; use crate::kinds; +/// A local nostrdb subscription that polls for notes of a given kind. +struct KindSubscription { + sub: Subscription, +} + +impl KindSubscription { + fn new(ndb: &Ndb, kind: u16) -> Self { + let filter = Filter::new().kinds([kind as u64]).build(); + let sub = ndb.subscribe(&[filter]).expect("kind subscription"); + Self { sub } + } + + fn poll<'a>(&self, ndb: &'a Ndb, txn: &'a Transaction) -> Vec<Note<'a>> { + ndb.poll_for_notes(self.sub, 50) + .into_iter() + .filter_map(|nk| ndb.get_note_by_key(txn, nk).ok()) + .collect() + } +} + /// Manages a local nostrdb subscription for room events. pub struct RoomSubscription { - /// Local nostrdb subscription handle - sub: Subscription, + inner: KindSubscription, } impl RoomSubscription { /// Subscribe to all room events (kind 37555) in the local nostrdb. pub fn new(ndb: &Ndb) -> Self { - let filter = Filter::new().kinds([kinds::ROOM as u64]).build(); - let sub = ndb.subscribe(&[filter]).expect("room subscription"); - Self { sub } + Self { + inner: KindSubscription::new(ndb, kinds::ROOM), + } } /// Subscribe to room events from a specific author. @@ -30,16 +48,14 @@ impl RoomSubscription { .authors([author]) .build(); let sub = ndb.subscribe(&[filter]).expect("room subscription"); - Self { sub } + Self { + inner: KindSubscription { sub }, + } } /// Poll for new room events. Returns parsed notes. pub fn poll<'a>(&self, ndb: &'a Ndb, txn: &'a Transaction) -> Vec<Note<'a>> { - let note_keys = ndb.poll_for_notes(self.sub, 50); - note_keys - .into_iter() - .filter_map(|nk| ndb.get_note_by_key(txn, nk).ok()) - .collect() + self.inner.poll(ndb, txn) } /// Query for existing room events (e.g. on startup). @@ -55,23 +71,19 @@ impl RoomSubscription { /// Manages a local nostrdb subscription for presence events (kind 10555). pub struct PresenceSubscription { - sub: Subscription, + inner: KindSubscription, } impl PresenceSubscription { /// Subscribe to presence events in the local nostrdb. pub fn new(ndb: &Ndb) -> Self { - let filter = Filter::new().kinds([kinds::PRESENCE as u64]).build(); - let sub = ndb.subscribe(&[filter]).expect("presence subscription"); - Self { sub } + Self { + inner: KindSubscription::new(ndb, kinds::PRESENCE), + } } /// Poll for new presence events. pub fn poll<'a>(&self, ndb: &'a Ndb, txn: &'a Transaction) -> Vec<Note<'a>> { - let note_keys = ndb.poll_for_notes(self.sub, 50); - note_keys - .into_iter() - .filter_map(|nk| ndb.get_note_by_key(txn, nk).ok()) - .collect() + self.inner.poll(ndb, txn) } }