fix: teleport should never show reconnect dialog
This commit is contained in:
parent
32e5e42462
commit
bf3bd3dff5
5 changed files with 153 additions and 20 deletions
|
|
@ -20,6 +20,14 @@ pub struct WsConfig {
|
||||||
pub ping_interval_secs: u64,
|
pub ping_interval_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// WebSocket close codes (custom range: 4000-4999).
|
||||||
|
pub mod close_codes {
|
||||||
|
/// Scene change (user navigating to different scene).
|
||||||
|
pub const SCENE_CHANGE: u16 = 4000;
|
||||||
|
/// Server timeout (no message received within timeout period).
|
||||||
|
pub const SERVER_TIMEOUT: u16 = 4001;
|
||||||
|
}
|
||||||
|
|
||||||
/// Reason for member disconnect.
|
/// Reason for member disconnect.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,11 @@ use uuid::Uuid;
|
||||||
use chattyness_db::{
|
use chattyness_db::{
|
||||||
models::{AvatarRenderData, ChannelMemberWithAvatar, EmotionState, User},
|
models::{AvatarRenderData, ChannelMemberWithAvatar, EmotionState, User},
|
||||||
queries::{avatars, channel_members, loose_props, realms, scenes},
|
queries::{avatars, channel_members, loose_props, realms, scenes},
|
||||||
ws_messages::{ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
||||||
};
|
};
|
||||||
use chattyness_error::AppError;
|
use chattyness_error::AppError;
|
||||||
use chattyness_shared::WebSocketConfig;
|
use chattyness_shared::WebSocketConfig;
|
||||||
|
|
||||||
/// Close code for scene change (custom code).
|
|
||||||
const SCENE_CHANGE_CLOSE_CODE: u16 = 4000;
|
|
||||||
|
|
||||||
use crate::auth::AuthUser;
|
use crate::auth::AuthUser;
|
||||||
|
|
||||||
/// Channel state for broadcasting updates.
|
/// Channel state for broadcasting updates.
|
||||||
|
|
@ -397,8 +394,13 @@ async fn handle_socket(
|
||||||
// Create recv timeout from config
|
// Create recv timeout from config
|
||||||
let recv_timeout = Duration::from_secs(ws_config.recv_timeout_secs);
|
let recv_timeout = Duration::from_secs(ws_config.recv_timeout_secs);
|
||||||
|
|
||||||
|
// Channel for sending close frame requests from recv_task to send_task
|
||||||
|
let (close_tx, mut close_rx) = mpsc::channel::<(u16, String)>(1);
|
||||||
|
|
||||||
// Spawn task to handle incoming messages from client
|
// Spawn task to handle incoming messages from client
|
||||||
|
let close_tx_for_recv = close_tx.clone();
|
||||||
let recv_task = tokio::spawn(async move {
|
let recv_task = tokio::spawn(async move {
|
||||||
|
let close_tx = close_tx_for_recv;
|
||||||
let pool = pool_for_recv;
|
let pool = pool_for_recv;
|
||||||
let ws_state = ws_state_for_recv;
|
let ws_state = ws_state_for_recv;
|
||||||
let mut disconnect_reason = DisconnectReason::Graceful;
|
let mut disconnect_reason = DisconnectReason::Graceful;
|
||||||
|
|
@ -817,7 +819,7 @@ async fn handle_socket(
|
||||||
Message::Close(close_frame) => {
|
Message::Close(close_frame) => {
|
||||||
// Check close code for scene change
|
// Check close code for scene change
|
||||||
if let Some(CloseFrame { code, .. }) = close_frame {
|
if let Some(CloseFrame { code, .. }) = close_frame {
|
||||||
if code == SCENE_CHANGE_CLOSE_CODE {
|
if code == close_codes::SCENE_CHANGE {
|
||||||
disconnect_reason = DisconnectReason::SceneChange;
|
disconnect_reason = DisconnectReason::SceneChange;
|
||||||
} else {
|
} else {
|
||||||
disconnect_reason = DisconnectReason::Graceful;
|
disconnect_reason = DisconnectReason::Graceful;
|
||||||
|
|
@ -843,6 +845,12 @@ async fn handle_socket(
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Timeout elapsed - connection likely lost
|
// Timeout elapsed - connection likely lost
|
||||||
tracing::info!("[WS] Connection timeout for user {}", user_id);
|
tracing::info!("[WS] Connection timeout for user {}", user_id);
|
||||||
|
// Send close frame with timeout code so client can attempt silent reconnection
|
||||||
|
let _ = close_tx
|
||||||
|
.send((close_codes::SERVER_TIMEOUT, "timeout".to_string()))
|
||||||
|
.await;
|
||||||
|
// Brief delay to allow close frame to be sent
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
disconnect_reason = DisconnectReason::Timeout;
|
disconnect_reason = DisconnectReason::Timeout;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -852,10 +860,21 @@ async fn handle_socket(
|
||||||
(recv_conn, disconnect_reason)
|
(recv_conn, disconnect_reason)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Spawn task to forward broadcasts and direct messages to this client
|
// Spawn task to forward broadcasts, direct messages, and close frames to this client
|
||||||
let send_task = tokio::spawn(async move {
|
let send_task = tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
// Handle close frame requests (from timeout)
|
||||||
|
Some((code, reason)) = close_rx.recv() => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
tracing::debug!("[WS->Client] Sending close frame: code={}, reason={}", code, reason);
|
||||||
|
let close_frame = CloseFrame {
|
||||||
|
code,
|
||||||
|
reason: reason.into(),
|
||||||
|
};
|
||||||
|
let _ = sender.send(Message::Close(Some(close_frame))).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Handle broadcast messages
|
// Handle broadcast messages
|
||||||
Ok(msg) = rx.recv() => {
|
Ok(msg) = rx.recv() => {
|
||||||
if let Ok(json) = serde_json::to_string(&msg) {
|
if let Ok(json) = serde_json::to_string(&msg) {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,14 @@ pub fn ReconnectionOverlay(
|
||||||
}
|
}
|
||||||
set_overlay_state.set(OverlayState::Hidden);
|
set_overlay_state.set(OverlayState::Hidden);
|
||||||
}
|
}
|
||||||
|
WsState::SilentReconnecting(_) => {
|
||||||
|
// Silent reconnection in progress - keep overlay hidden
|
||||||
|
// The ws_client handles the reconnection attempts internally
|
||||||
|
if let Some(timer) = timer_handle.borrow_mut().take() {
|
||||||
|
drop(timer);
|
||||||
|
}
|
||||||
|
set_overlay_state.set(OverlayState::Hidden);
|
||||||
|
}
|
||||||
WsState::Disconnected | WsState::Error => {
|
WsState::Disconnected | WsState::Error => {
|
||||||
// Check current state - only start countdown if we're hidden
|
// Check current state - only start countdown if we're hidden
|
||||||
let current = overlay_state.get_untracked();
|
let current = overlay_state.get_untracked();
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,21 @@ use leptos::reactive::owner::LocalStorage;
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::models::EmotionState;
|
use chattyness_db::models::EmotionState;
|
||||||
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp};
|
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp};
|
||||||
use chattyness_db::ws_messages::ClientMessage;
|
use chattyness_db::ws_messages::{close_codes, ClientMessage};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::ws_messages::{DisconnectReason, ServerMessage};
|
use chattyness_db::ws_messages::{DisconnectReason, ServerMessage};
|
||||||
|
|
||||||
use super::chat_types::ChatMessage;
|
use super::chat_types::ChatMessage;
|
||||||
|
|
||||||
/// Close code for scene change (must match server constant).
|
|
||||||
pub const SCENE_CHANGE_CLOSE_CODE: u16 = 4000;
|
|
||||||
|
|
||||||
/// Duration for fade-out animation in milliseconds.
|
/// Duration for fade-out animation in milliseconds.
|
||||||
pub const FADE_DURATION_MS: i64 = 5000;
|
pub const FADE_DURATION_MS: i64 = 5000;
|
||||||
|
|
||||||
|
/// Maximum number of silent reconnection attempts before showing overlay.
|
||||||
|
pub const MAX_SILENT_RECONNECT_ATTEMPTS: u8 = 3;
|
||||||
|
|
||||||
|
/// Delay between silent reconnection attempts in milliseconds.
|
||||||
|
pub const SILENT_RECONNECT_DELAY_MS: u32 = 1000;
|
||||||
|
|
||||||
/// A member that is currently fading out after a timeout disconnect.
|
/// A member that is currently fading out after a timeout disconnect.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FadingMember {
|
pub struct FadingMember {
|
||||||
|
|
@ -43,6 +46,9 @@ pub enum WsState {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
/// Connection error occurred.
|
/// Connection error occurred.
|
||||||
Error,
|
Error,
|
||||||
|
/// Silently attempting to reconnect after server timeout.
|
||||||
|
/// The u8 is the current attempt number (1-based).
|
||||||
|
SilentReconnecting(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sender function type for WebSocket messages.
|
/// Sender function type for WebSocket messages.
|
||||||
|
|
@ -91,7 +97,7 @@ pub struct TeleportInfo {
|
||||||
pub fn use_channel_websocket(
|
pub fn use_channel_websocket(
|
||||||
realm_slug: Signal<String>,
|
realm_slug: Signal<String>,
|
||||||
channel_id: Signal<Option<uuid::Uuid>>,
|
channel_id: Signal<Option<uuid::Uuid>>,
|
||||||
reconnect_trigger: Signal<u32>,
|
reconnect_trigger: RwSignal<u32>,
|
||||||
on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
|
on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
|
||||||
on_chat_message: Callback<ChatMessage>,
|
on_chat_message: Callback<ChatMessage>,
|
||||||
on_loose_props_sync: Callback<Vec<LooseProp>>,
|
on_loose_props_sync: Callback<Vec<LooseProp>>,
|
||||||
|
|
@ -110,6 +116,11 @@ pub fn use_channel_websocket(
|
||||||
let (ws_state, set_ws_state) = signal(WsState::Disconnected);
|
let (ws_state, set_ws_state) = signal(WsState::Disconnected);
|
||||||
let ws_ref: Rc<RefCell<Option<WebSocket>>> = Rc::new(RefCell::new(None));
|
let ws_ref: Rc<RefCell<Option<WebSocket>>> = Rc::new(RefCell::new(None));
|
||||||
let members: Rc<RefCell<Vec<ChannelMemberWithAvatar>>> = Rc::new(RefCell::new(Vec::new()));
|
let members: Rc<RefCell<Vec<ChannelMemberWithAvatar>>> = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
// Track current user's ID to ignore self MemberLeft during reconnection
|
||||||
|
let current_user_id: Rc<RefCell<Option<uuid::Uuid>>> = Rc::new(RefCell::new(None));
|
||||||
|
// Flag to track intentional closes (teleport, scene change) - guarantees local state
|
||||||
|
// even if close code doesn't arrive correctly due to browser/server quirks
|
||||||
|
let is_intentional_close: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
|
||||||
|
|
||||||
// Create a stored sender function (using new_local for WASM single-threaded environment)
|
// Create a stored sender function (using new_local for WASM single-threaded environment)
|
||||||
let ws_ref_for_send = ws_ref.clone();
|
let ws_ref_for_send = ws_ref.clone();
|
||||||
|
|
@ -129,6 +140,7 @@ pub fn use_channel_websocket(
|
||||||
// Effect to manage WebSocket lifecycle
|
// Effect to manage WebSocket lifecycle
|
||||||
let ws_ref_clone = ws_ref.clone();
|
let ws_ref_clone = ws_ref.clone();
|
||||||
let members_clone = members.clone();
|
let members_clone = members.clone();
|
||||||
|
let is_intentional_close_for_cleanup = is_intentional_close.clone();
|
||||||
|
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
let slug = realm_slug.get();
|
let slug = realm_slug.get();
|
||||||
|
|
@ -138,7 +150,14 @@ pub fn use_channel_websocket(
|
||||||
|
|
||||||
// Cleanup previous connection
|
// Cleanup previous connection
|
||||||
if let Some(old_ws) = ws_ref_clone.borrow_mut().take() {
|
if let Some(old_ws) = ws_ref_clone.borrow_mut().take() {
|
||||||
let _ = old_ws.close();
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(
|
||||||
|
&format!("[WS] Closing old connection, readyState={}", old_ws.ready_state()).into(),
|
||||||
|
);
|
||||||
|
// Set flag BEFORE closing - guarantees local state even if close code doesn't arrive
|
||||||
|
*is_intentional_close_for_cleanup.borrow_mut() = true;
|
||||||
|
// Close with SCENE_CHANGE code so onclose handler knows this was intentional
|
||||||
|
let _ = old_ws.close_with_code_and_reason(close_codes::SCENE_CHANGE, "scene change");
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ch_id) = ch_id else {
|
let Some(ch_id) = ch_id else {
|
||||||
|
|
@ -205,6 +224,8 @@ pub fn use_channel_websocket(
|
||||||
let ws_ref_for_heartbeat = ws_ref.clone();
|
let ws_ref_for_heartbeat = ws_ref.clone();
|
||||||
let heartbeat_started: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
|
let heartbeat_started: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
|
||||||
let heartbeat_started_clone = heartbeat_started.clone();
|
let heartbeat_started_clone = heartbeat_started.clone();
|
||||||
|
// For tracking current user ID to ignore self MemberLeft during reconnection
|
||||||
|
let current_user_id_for_msg = current_user_id.clone();
|
||||||
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
|
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||||
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
|
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
|
||||||
let text: String = text.into();
|
let text: String = text.into();
|
||||||
|
|
@ -219,6 +240,9 @@ pub fn use_channel_websocket(
|
||||||
..
|
..
|
||||||
} = msg
|
} = msg
|
||||||
{
|
{
|
||||||
|
// Track current user ID for MemberLeft filtering
|
||||||
|
*current_user_id_for_msg.borrow_mut() = member.user_id;
|
||||||
|
|
||||||
if !*heartbeat_started_clone.borrow() {
|
if !*heartbeat_started_clone.borrow() {
|
||||||
*heartbeat_started_clone.borrow_mut() = true;
|
*heartbeat_started_clone.borrow_mut() = true;
|
||||||
let ping_interval_ms = config.ping_interval_secs * 1000;
|
let ping_interval_ms = config.ping_interval_secs * 1000;
|
||||||
|
|
@ -269,6 +293,7 @@ pub fn use_channel_websocket(
|
||||||
&on_member_fading_clone,
|
&on_member_fading_clone,
|
||||||
&on_error_clone,
|
&on_error_clone,
|
||||||
&on_teleport_approved_clone,
|
&on_teleport_approved_clone,
|
||||||
|
¤t_user_id_for_msg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,22 +303,82 @@ pub fn use_channel_websocket(
|
||||||
|
|
||||||
// onerror
|
// onerror
|
||||||
let set_ws_state_err = set_ws_state;
|
let set_ws_state_err = set_ws_state;
|
||||||
|
let ws_state_for_err = ws_state;
|
||||||
|
let reconnect_trigger_for_error = reconnect_trigger;
|
||||||
let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
|
let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
web_sys::console::error_1(&format!("[WS] Error: {:?}", e.message()).into());
|
web_sys::console::error_1(&format!("[WS] Error: {:?}", e.message()).into());
|
||||||
set_ws_state_err.set(WsState::Error);
|
|
||||||
|
// Check if we're in silent reconnection mode
|
||||||
|
let current_state = ws_state_for_err.get_untracked();
|
||||||
|
if let WsState::SilentReconnecting(attempt) = current_state {
|
||||||
|
if attempt < MAX_SILENT_RECONNECT_ATTEMPTS {
|
||||||
|
// Try another silent reconnection
|
||||||
|
let next_attempt = attempt + 1;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(
|
||||||
|
&format!(
|
||||||
|
"[WS] Silent reconnection attempt {} failed, trying attempt {}",
|
||||||
|
attempt, next_attempt
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
set_ws_state_err.set(WsState::SilentReconnecting(next_attempt));
|
||||||
|
// Schedule next reconnection attempt
|
||||||
|
let reconnect_trigger = reconnect_trigger_for_error;
|
||||||
|
gloo_timers::callback::Timeout::new(SILENT_RECONNECT_DELAY_MS, move || {
|
||||||
|
reconnect_trigger.update(|v| *v = v.wrapping_add(1));
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
} else {
|
||||||
|
// Max attempts reached, fall back to showing overlay
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(
|
||||||
|
&"[WS] Silent reconnection failed, showing reconnection overlay".into(),
|
||||||
|
);
|
||||||
|
set_ws_state_err.set(WsState::Error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set_ws_state_err.set(WsState::Error);
|
||||||
|
}
|
||||||
}) as Box<dyn FnMut(ErrorEvent)>);
|
}) as Box<dyn FnMut(ErrorEvent)>);
|
||||||
ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
|
ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
|
||||||
onerror.forget();
|
onerror.forget();
|
||||||
|
|
||||||
// onclose
|
// onclose
|
||||||
let set_ws_state_close = set_ws_state;
|
let set_ws_state_close = set_ws_state;
|
||||||
|
let reconnect_trigger_for_close = reconnect_trigger;
|
||||||
|
let is_intentional_close_for_onclose = is_intentional_close.clone();
|
||||||
let onclose = Closure::wrap(Box::new(move |e: CloseEvent| {
|
let onclose = Closure::wrap(Box::new(move |e: CloseEvent| {
|
||||||
|
let code = e.code();
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
web_sys::console::log_1(
|
web_sys::console::log_1(
|
||||||
&format!("[WS] Closed: code={}, reason={}", e.code(), e.reason()).into(),
|
&format!("[WS] Closed: code={}, reason={}", code, e.reason()).into(),
|
||||||
);
|
);
|
||||||
set_ws_state_close.set(WsState::Disconnected);
|
|
||||||
|
// Handle based on close code with defense-in-depth using flag
|
||||||
|
if code == close_codes::SERVER_TIMEOUT {
|
||||||
|
// Server timeout - attempt silent reconnection (highest priority)
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(&"[WS] Server timeout, attempting silent reconnection".into());
|
||||||
|
set_ws_state_close.set(WsState::SilentReconnecting(1));
|
||||||
|
// Schedule reconnection after delay
|
||||||
|
let reconnect_trigger = reconnect_trigger_for_close;
|
||||||
|
gloo_timers::callback::Timeout::new(SILENT_RECONNECT_DELAY_MS, move || {
|
||||||
|
reconnect_trigger.update(|v| *v = v.wrapping_add(1));
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
} else if code == close_codes::SCENE_CHANGE || *is_intentional_close_for_onclose.borrow() {
|
||||||
|
// Intentional close (scene change/teleport) - don't show disconnection
|
||||||
|
// Check both code AND flag for defense-in-depth (flag is guaranteed local state)
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(&"[WS] Intentional close, not setting Disconnected".into());
|
||||||
|
// Reset the flag for future connections
|
||||||
|
*is_intentional_close_for_onclose.borrow_mut() = false;
|
||||||
|
} else {
|
||||||
|
// Other close codes - treat as disconnection
|
||||||
|
set_ws_state_close.set(WsState::Disconnected);
|
||||||
|
}
|
||||||
}) as Box<dyn FnMut(CloseEvent)>);
|
}) as Box<dyn FnMut(CloseEvent)>);
|
||||||
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
|
||||||
onclose.forget();
|
onclose.forget();
|
||||||
|
|
@ -317,6 +402,7 @@ fn handle_server_message(
|
||||||
on_member_fading: &Callback<FadingMember>,
|
on_member_fading: &Callback<FadingMember>,
|
||||||
on_error: &Option<Callback<WsError>>,
|
on_error: &Option<Callback<WsError>>,
|
||||||
on_teleport_approved: &Option<Callback<TeleportInfo>>,
|
on_teleport_approved: &Option<Callback<TeleportInfo>>,
|
||||||
|
current_user_id: &std::rc::Rc<std::cell::RefCell<Option<uuid::Uuid>>>,
|
||||||
) {
|
) {
|
||||||
let mut members_vec = members.borrow_mut();
|
let mut members_vec = members.borrow_mut();
|
||||||
|
|
||||||
|
|
@ -343,6 +429,18 @@ fn handle_server_message(
|
||||||
guest_session_id,
|
guest_session_id,
|
||||||
reason,
|
reason,
|
||||||
} => {
|
} => {
|
||||||
|
// Check if this is our own MemberLeft due to timeout - ignore it during reconnection
|
||||||
|
// so we don't see our own avatar fade out
|
||||||
|
let own_user_id = *current_user_id.borrow();
|
||||||
|
let is_self = own_user_id.is_some() && user_id == own_user_id;
|
||||||
|
if is_self && reason == DisconnectReason::Timeout {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
web_sys::console::log_1(
|
||||||
|
&"[WS] Ignoring self MemberLeft during reconnection".into(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the member before removing
|
// Find the member before removing
|
||||||
let leaving_member = members_vec
|
let leaving_member = members_vec
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -488,7 +586,7 @@ fn handle_server_message(
|
||||||
pub fn use_channel_websocket(
|
pub fn use_channel_websocket(
|
||||||
_realm_slug: Signal<String>,
|
_realm_slug: Signal<String>,
|
||||||
_channel_id: Signal<Option<uuid::Uuid>>,
|
_channel_id: Signal<Option<uuid::Uuid>>,
|
||||||
_reconnect_trigger: Signal<u32>,
|
_reconnect_trigger: RwSignal<u32>,
|
||||||
_on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
|
_on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
|
||||||
_on_chat_message: Callback<ChatMessage>,
|
_on_chat_message: Callback<ChatMessage>,
|
||||||
_on_loose_props_sync: Callback<Vec<LooseProp>>,
|
_on_loose_props_sync: Callback<Vec<LooseProp>>,
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
let (error_message, set_error_message) = signal(Option::<String>::None);
|
let (error_message, set_error_message) = signal(Option::<String>::None);
|
||||||
|
|
||||||
// Reconnection trigger - increment to force WebSocket reconnection
|
// Reconnection trigger - increment to force WebSocket reconnection
|
||||||
let (reconnect_trigger, set_reconnect_trigger) = signal(0u32);
|
let reconnect_trigger = RwSignal::new(0u32);
|
||||||
|
|
||||||
// Current scene (changes when teleporting)
|
// Current scene (changes when teleporting)
|
||||||
let (current_scene, set_current_scene) = signal(Option::<Scene>::None);
|
let (current_scene, set_current_scene) = signal(Option::<Scene>::None);
|
||||||
|
|
@ -383,7 +383,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
set_members.set(Vec::new());
|
set_members.set(Vec::new());
|
||||||
|
|
||||||
// Trigger a reconnect to ensure fresh connection
|
// Trigger a reconnect to ensure fresh connection
|
||||||
set_reconnect_trigger.update(|t| *t += 1);
|
reconnect_trigger.update(|t| *t += 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -391,7 +391,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
let (ws_state, ws_sender) = use_channel_websocket(
|
let (ws_state, ws_sender) = use_channel_websocket(
|
||||||
slug,
|
slug,
|
||||||
Signal::derive(move || channel_id.get()),
|
Signal::derive(move || channel_id.get()),
|
||||||
Signal::derive(move || reconnect_trigger.get()),
|
reconnect_trigger,
|
||||||
on_members_update,
|
on_members_update,
|
||||||
on_chat_message,
|
on_chat_message,
|
||||||
on_loose_props_sync,
|
on_loose_props_sync,
|
||||||
|
|
@ -1087,7 +1087,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
<ReconnectionOverlay
|
<ReconnectionOverlay
|
||||||
ws_state=ws_state_for_overlay
|
ws_state=ws_state_for_overlay
|
||||||
on_reconnect=Callback::new(move |_: ()| {
|
on_reconnect=Callback::new(move |_: ()| {
|
||||||
set_reconnect_trigger.update(|t| *t += 1);
|
reconnect_trigger.update(|t| *t += 1);
|
||||||
})
|
})
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue