notedeck

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

commit d1d3dd943b491e839ed57cd263c57aeeaba736f2
parent 364b84de718e4fe30c33276a9afb09cec7d2c0ec
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 27 Feb 2026 13:57:15 -0800

nostrverse: reject out-of-order presence events

Track event_created_at on RoomUser and skip presence updates
whose created_at is older, preventing stale replaceable events
from overwriting newer positions.

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

Diffstat:
Mcrates/notedeck_nostrverse/src/presence.rs | 7+++++++
Mcrates/notedeck_nostrverse/src/room_state.rs | 4++++
2 files changed, 11 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_nostrverse/src/presence.rs b/crates/notedeck_nostrverse/src/presence.rs @@ -189,20 +189,27 @@ pub fn poll_presence( } let velocity = nostr_events::parse_presence_velocity(note); + let created_at = note.created_at(); // Update or insert user if let Some(user) = users.iter_mut().find(|u| u.pubkey == pubkey) { + // Skip stale events — replaceable events can arrive out of order + if created_at <= user.event_created_at { + continue; + } // Update authoritative state; preserve display_position for smooth lerp user.position = position; user.velocity = velocity; user.update_time = now; user.last_seen = now; + user.event_created_at = created_at; } else { let mut user = RoomUser::new(pubkey, "anon".to_string(), position); user.velocity = velocity; user.display_position = position; // snap on first appearance user.update_time = now; user.last_seen = now; + user.event_created_at = created_at; users.push(user); } changed = true; diff --git a/crates/notedeck_nostrverse/src/room_state.rs b/crates/notedeck_nostrverse/src/room_state.rs @@ -231,6 +231,9 @@ pub struct RoomUser { pub is_self: bool, /// Monotonic timestamp (seconds) of last presence update pub last_seen: f64, + /// Nostr event created_at of the latest accepted presence event. + /// Used to ignore out-of-order replaceable events. + pub event_created_at: u64, /// Runtime: renderbud scene object handle for avatar pub scene_object_id: Option<ObjectId>, /// Runtime: loaded model handle for avatar @@ -248,6 +251,7 @@ impl RoomUser { update_time: 0.0, is_self: false, last_seen: 0.0, + event_created_at: 0, scene_object_id: None, model_handle: None, }