update to support user expire, timeout, and disconnect

This commit is contained in:
Evan Carroll 2026-01-17 23:47:02 -06:00
parent fe65835f4a
commit 5fcd49e847
16 changed files with 744 additions and 238 deletions

View file

@ -12,13 +12,15 @@ use leptos_router::hooks::use_params_map;
use uuid::Uuid;
use crate::components::{
ActiveBubble, AvatarEditorPopup, Card, ChatInput, ChatMessage, EmotionKeybindings,
ActiveBubble, AvatarEditorPopup, Card, ChatInput, EmotionKeybindings, FadingMember,
InventoryPopup, KeybindingsPopup, MessageLog, RealmHeader, RealmSceneViewer, SettingsPopup,
ViewerSettings, DEFAULT_BUBBLE_TIMEOUT_MS,
ViewerSettings,
};
#[cfg(feature = "hydrate")]
use crate::components::use_channel_websocket;
use crate::utils::{parse_bounds_dimensions, LocalStoragePersist};
use crate::components::{use_channel_websocket, ChatMessage, DEFAULT_BUBBLE_TIMEOUT_MS, FADE_DURATION_MS};
use crate::utils::LocalStoragePersist;
#[cfg(feature = "hydrate")]
use crate::utils::parse_bounds_dimensions;
use chattyness_db::models::{
AvatarWithPaths, ChannelMemberWithAvatar, EmotionAvailability, LooseProp, RealmRole,
RealmWithUserRole, Scene,
@ -82,6 +84,9 @@ pub fn RealmPage() -> impl IntoView {
// Loose props state
let (loose_props, set_loose_props) = signal(Vec::<LooseProp>::new());
// Fading members state (members that are fading out after timeout disconnect)
let (fading_members, set_fading_members) = signal(Vec::<FadingMember>::new());
// Track user's current position for saving on beforeunload
let (current_position, set_current_position) = signal((400.0_f64, 300.0_f64));
@ -173,6 +178,15 @@ pub fn RealmPage() -> impl IntoView {
// WebSocket connection for real-time updates
#[cfg(feature = "hydrate")]
let on_members_update = Callback::new(move |new_members: Vec<ChannelMemberWithAvatar>| {
// When members are updated (including rejoins), remove any matching fading members
set_fading_members.update(|fading| {
fading.retain(|f| {
!new_members.iter().any(|m| {
m.member.user_id == f.member.member.user_id
&& m.member.guest_session_id == f.member.member.guest_session_id
})
});
});
set_members.set(new_members);
});
@ -217,6 +231,19 @@ pub fn RealmPage() -> impl IntoView {
});
});
// Callback when a member starts fading (timeout disconnect)
#[cfg(feature = "hydrate")]
let on_member_fading = Callback::new(move |fading: FadingMember| {
set_fading_members.update(|members| {
// Remove any existing entry for this user (shouldn't happen, but be safe)
members.retain(|m| {
m.member.member.user_id != fading.member.member.user_id
|| m.member.member.guest_session_id != fading.member.member.guest_session_id
});
members.push(fading);
});
});
#[cfg(feature = "hydrate")]
let (_ws_state, ws_sender) = use_channel_websocket(
slug,
@ -226,6 +253,7 @@ pub fn RealmPage() -> impl IntoView {
on_loose_props_sync,
on_prop_dropped,
on_prop_picked_up,
on_member_fading,
);
// Set channel ID and scene dimensions when scene loads
@ -246,16 +274,21 @@ pub fn RealmPage() -> impl IntoView {
});
}
// Cleanup expired speech bubbles every 5 seconds
// Cleanup expired speech bubbles and fading members every second
#[cfg(feature = "hydrate")]
{
use gloo_timers::callback::Interval;
let cleanup_interval = Interval::new(5000, move || {
let cleanup_interval = Interval::new(1000, move || {
let now = js_sys::Date::now() as i64;
// Clean up expired bubbles
set_active_bubbles.update(|bubbles| {
bubbles.retain(|_, bubble| bubble.expires_at > now);
});
// Clean up completed fading members
set_fading_members.update(|members| {
members.retain(|m| now - m.fade_start < FADE_DURATION_MS);
});
});
// Keep interval alive
std::mem::forget(cleanup_interval);
@ -638,6 +671,7 @@ pub fn RealmPage() -> impl IntoView {
s.save();
});
})
fading_members=Signal::derive(move || fading_members.get())
/>
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
<ChatInput