Skip to content

Commit 364b84d

Browse files
jb55claude
andcommitted
nostrverse: clean up scene objects when expiring stale users
maybe_expire now returns the removed RoomUsers so callers can remove their avatar scene objects, preventing lingering 3D models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f76da47 commit 364b84d

File tree

2 files changed

+31
-13
lines changed

2 files changed

+31
-13
lines changed

crates/notedeck_nostrverse/src/lib.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,11 +563,20 @@ impl NostrverseApp {
563563
}
564564

565565
// Expire stale remote users (throttled to every ~10s)
566-
let removed = self
566+
let expired = self
567567
.presence_expiry
568568
.maybe_expire(&mut self.state.users, now);
569-
if removed > 0 {
570-
tracing::info!("Expired {} stale users", removed);
569+
if !expired.is_empty() {
570+
tracing::info!("Expired {} stale users", expired.len());
571+
// Clean up scene objects so avatars don't linger in the 3D scene
572+
if let Some(renderer) = &self.renderer {
573+
let mut r = renderer.renderer.lock().unwrap();
574+
for user in &expired {
575+
if let Some(scene_id) = user.scene_object_id {
576+
r.remove_object(scene_id);
577+
}
578+
}
579+
}
571580
}
572581
}
573582

crates/notedeck_nostrverse/src/presence.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,15 +222,23 @@ impl PresenceExpiry {
222222
Self { last_check: 0.0 }
223223
}
224224

225-
/// Maybe expire stale users. Returns the number removed (0 if check was skipped).
226-
pub fn maybe_expire(&mut self, users: &mut Vec<RoomUser>, now: f64) -> usize {
225+
/// Maybe expire stale users. Returns removed users so callers can clean up
226+
/// their scene objects. Empty if the check was throttled.
227+
pub fn maybe_expire(&mut self, users: &mut Vec<RoomUser>, now: f64) -> Vec<RoomUser> {
227228
if now - self.last_check < EXPIRY_CHECK_INTERVAL {
228-
return 0;
229+
return Vec::new();
229230
}
230231
self.last_check = now;
231-
let before = users.len();
232-
users.retain(|u| u.is_self || (now - u.last_seen) < STALE_TIMEOUT);
233-
before - users.len()
232+
let mut expired = Vec::new();
233+
let mut i = 0;
234+
while i < users.len() {
235+
if !users[i].is_self && (now - users[i].last_seen) >= STALE_TIMEOUT {
236+
expired.push(users.swap_remove(i));
237+
} else {
238+
i += 1;
239+
}
240+
}
241+
expired
234242
}
235243
}
236244

@@ -266,18 +274,19 @@ mod tests {
266274
let mut expiry = PresenceExpiry::new();
267275

268276
// First call at t=5 — too soon (< 10s from init at 0.0), skipped
269-
assert_eq!(expiry.maybe_expire(&mut users, 5.0), 0);
277+
assert!(expiry.maybe_expire(&mut users, 5.0).is_empty());
270278
assert_eq!(users.len(), 3); // no one removed
271279

272280
// At t=100 — enough time, bob is stale
273-
let removed = expiry.maybe_expire(&mut users, 100.0);
274-
assert_eq!(removed, 1);
281+
let expired = expiry.maybe_expire(&mut users, 100.0);
282+
assert_eq!(expired.len(), 1);
283+
assert_eq!(expired[0].display_name, "bob");
275284
assert_eq!(users.len(), 2);
276285
assert!(users.iter().any(|u| u.is_self));
277286
assert!(users.iter().any(|u| u.display_name == "alice"));
278287

279288
// Immediately again at t=101 — throttled, skipped
280-
assert_eq!(expiry.maybe_expire(&mut users, 101.0), 0);
289+
assert!(expiry.maybe_expire(&mut users, 101.0).is_empty());
281290
}
282291

283292
#[test]

0 commit comments

Comments
 (0)