fix: reconnect on ws failure
This commit is contained in:
parent
84cb4e5e78
commit
27b3658e1d
5 changed files with 430 additions and 3 deletions
|
|
@ -75,4 +75,14 @@
|
||||||
.error-message {
|
.error-message {
|
||||||
@apply p-4 bg-red-900/50 border border-red-500 rounded-lg text-red-200;
|
@apply p-4 bg-red-900/50 border border-red-500 rounded-lg text-red-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reconnection overlay spinner animation */
|
||||||
|
.reconnect-spinner {
|
||||||
|
animation: reconnect-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reconnect-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ pub mod scene_viewer;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod settings_popup;
|
pub mod settings_popup;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
pub mod reconnection_overlay;
|
||||||
pub mod ws_client;
|
pub mod ws_client;
|
||||||
|
|
||||||
pub use avatar_canvas::*;
|
pub use avatar_canvas::*;
|
||||||
|
|
@ -38,6 +39,7 @@ pub use layout::*;
|
||||||
pub use modals::*;
|
pub use modals::*;
|
||||||
pub use notification_history::*;
|
pub use notification_history::*;
|
||||||
pub use notifications::*;
|
pub use notifications::*;
|
||||||
|
pub use reconnection_overlay::*;
|
||||||
pub use scene_viewer::*;
|
pub use scene_viewer::*;
|
||||||
pub use settings::*;
|
pub use settings::*;
|
||||||
pub use settings_popup::*;
|
pub use settings_popup::*;
|
||||||
|
|
|
||||||
391
crates/chattyness-user-ui/src/components/reconnection_overlay.rs
Normal file
391
crates/chattyness-user-ui/src/components/reconnection_overlay.rs
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
//! Reconnection overlay component.
|
||||||
|
//!
|
||||||
|
//! Displays a full-screen overlay when WebSocket connection is lost,
|
||||||
|
//! with countdown timer and automatic retry logic.
|
||||||
|
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
use super::ws_client::WsState;
|
||||||
|
|
||||||
|
/// Reconnection attempt phase.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ReconnectionPhase {
|
||||||
|
/// Initial phase: 5 attempts with 5-second countdown each.
|
||||||
|
Initial { attempt: u8 },
|
||||||
|
/// Extended phase: 10 attempts with 10-second countdown each.
|
||||||
|
Extended { attempt: u8 },
|
||||||
|
/// All attempts exhausted.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReconnectionPhase {
|
||||||
|
/// Get the countdown duration in seconds for the current phase.
|
||||||
|
pub fn countdown_duration(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::Initial { .. } => 5,
|
||||||
|
Self::Extended { .. } => 10,
|
||||||
|
Self::Failed => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the maximum attempts for the current phase.
|
||||||
|
pub fn max_attempts(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Initial { .. } => 5,
|
||||||
|
Self::Extended { .. } => 10,
|
||||||
|
Self::Failed => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance to the next attempt or phase.
|
||||||
|
pub fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Initial { attempt } if attempt < 5 => Self::Initial { attempt: attempt + 1 },
|
||||||
|
Self::Initial { .. } => Self::Extended { attempt: 1 },
|
||||||
|
Self::Extended { attempt } if attempt < 10 => Self::Extended { attempt: attempt + 1 },
|
||||||
|
Self::Extended { .. } | Self::Failed => Self::Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current attempt number.
|
||||||
|
pub fn attempt(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Initial { attempt } | Self::Extended { attempt } => *attempt,
|
||||||
|
Self::Failed => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this is the initial phase.
|
||||||
|
pub fn is_initial(&self) -> bool {
|
||||||
|
matches!(self, Self::Initial { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal state for the reconnection overlay.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum OverlayState {
|
||||||
|
/// Hidden (connected).
|
||||||
|
Hidden,
|
||||||
|
/// Showing countdown.
|
||||||
|
Countdown {
|
||||||
|
phase: ReconnectionPhase,
|
||||||
|
remaining: u32,
|
||||||
|
},
|
||||||
|
/// Currently attempting to reconnect.
|
||||||
|
Reconnecting { phase: ReconnectionPhase },
|
||||||
|
/// All attempts failed.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconnection overlay component.
|
||||||
|
///
|
||||||
|
/// Shows a full-screen overlay when WebSocket connection is lost,
|
||||||
|
/// with countdown timer and automatic retry logic.
|
||||||
|
#[component]
|
||||||
|
pub fn ReconnectionOverlay(
|
||||||
|
/// WebSocket connection state to monitor.
|
||||||
|
ws_state: Signal<WsState>,
|
||||||
|
/// Callback to trigger a reconnection attempt.
|
||||||
|
on_reconnect: Callback<()>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
// Internal overlay state
|
||||||
|
let (overlay_state, set_overlay_state) = signal(OverlayState::Hidden);
|
||||||
|
|
||||||
|
// Timer handle stored for cleanup
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
let timer_handle: std::rc::Rc<std::cell::RefCell<Option<gloo_timers::callback::Interval>>> =
|
||||||
|
std::rc::Rc::new(std::cell::RefCell::new(None));
|
||||||
|
|
||||||
|
// Watch for WebSocket state changes
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
{
|
||||||
|
let timer_handle = timer_handle.clone();
|
||||||
|
|
||||||
|
Effect::new(move |_| {
|
||||||
|
let state = ws_state.get();
|
||||||
|
|
||||||
|
match state {
|
||||||
|
WsState::Connected => {
|
||||||
|
// Connection restored - hide overlay and stop timer
|
||||||
|
if let Some(timer) = timer_handle.borrow_mut().take() {
|
||||||
|
drop(timer);
|
||||||
|
}
|
||||||
|
set_overlay_state.set(OverlayState::Hidden);
|
||||||
|
}
|
||||||
|
WsState::Disconnected | WsState::Error => {
|
||||||
|
// Check current state - only start countdown if we're hidden
|
||||||
|
let current = overlay_state.get_untracked();
|
||||||
|
if matches!(current, OverlayState::Hidden) {
|
||||||
|
// Start initial countdown
|
||||||
|
let phase = ReconnectionPhase::Initial { attempt: 1 };
|
||||||
|
let duration = phase.countdown_duration();
|
||||||
|
set_overlay_state.set(OverlayState::Countdown {
|
||||||
|
phase,
|
||||||
|
remaining: duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start timer
|
||||||
|
start_countdown_timer(
|
||||||
|
timer_handle.clone(),
|
||||||
|
set_overlay_state,
|
||||||
|
on_reconnect.clone(),
|
||||||
|
);
|
||||||
|
} else if matches!(current, OverlayState::Reconnecting { .. }) {
|
||||||
|
// Reconnection attempt failed - advance to next attempt
|
||||||
|
if let OverlayState::Reconnecting { phase } = current {
|
||||||
|
let next_phase = phase.next();
|
||||||
|
if matches!(next_phase, ReconnectionPhase::Failed) {
|
||||||
|
set_overlay_state.set(OverlayState::Failed);
|
||||||
|
} else {
|
||||||
|
let duration = next_phase.countdown_duration();
|
||||||
|
set_overlay_state.set(OverlayState::Countdown {
|
||||||
|
phase: next_phase,
|
||||||
|
remaining: duration,
|
||||||
|
});
|
||||||
|
// Restart timer for next countdown
|
||||||
|
start_countdown_timer(
|
||||||
|
timer_handle.clone(),
|
||||||
|
set_overlay_state,
|
||||||
|
on_reconnect.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WsState::Connecting => {
|
||||||
|
// Currently attempting to connect - keep current state
|
||||||
|
// The reconnecting state should already be set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render based on state
|
||||||
|
move || {
|
||||||
|
let state = overlay_state.get();
|
||||||
|
|
||||||
|
match state {
|
||||||
|
OverlayState::Hidden => None,
|
||||||
|
OverlayState::Countdown { phase, remaining } => {
|
||||||
|
let (phase_text, attempt_text) = match phase {
|
||||||
|
ReconnectionPhase::Initial { attempt } => {
|
||||||
|
("Attempt", format!("{} of 5", attempt))
|
||||||
|
}
|
||||||
|
ReconnectionPhase::Extended { attempt } => {
|
||||||
|
("Extended attempt", format!("{} of 10", attempt))
|
||||||
|
}
|
||||||
|
ReconnectionPhase::Failed => ("", String::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[100] flex items-center justify-center"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="reconnect-title"
|
||||||
|
>
|
||||||
|
// Darkened backdrop
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/80 backdrop-blur-md"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
// Dialog box
|
||||||
|
<div class="relative bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 p-8 border border-gray-700 text-center">
|
||||||
|
// Countdown circle
|
||||||
|
<div class="w-20 h-20 mx-auto rounded-full bg-yellow-900/30 flex items-center justify-center mb-6">
|
||||||
|
<span class="text-4xl font-bold text-yellow-300">
|
||||||
|
{remaining}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="reconnect-title"
|
||||||
|
class="text-xl font-semibold text-white mb-2"
|
||||||
|
>
|
||||||
|
"Lost connection..."
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
{format!("attempting to reconnect in {} seconds", remaining)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-400">
|
||||||
|
{format!("{} {}", phase_text, attempt_text)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OverlayState::Reconnecting { phase } => {
|
||||||
|
let (phase_text, attempt_text) = match phase {
|
||||||
|
ReconnectionPhase::Initial { attempt } => {
|
||||||
|
("Attempt", format!("{} of 5", attempt))
|
||||||
|
}
|
||||||
|
ReconnectionPhase::Extended { attempt } => {
|
||||||
|
("Extended attempt", format!("{} of 10", attempt))
|
||||||
|
}
|
||||||
|
ReconnectionPhase::Failed => ("", String::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[100] flex items-center justify-center"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="reconnect-title"
|
||||||
|
>
|
||||||
|
// Darkened backdrop
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/80 backdrop-blur-md"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
// Dialog box
|
||||||
|
<div class="relative bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 p-8 border border-gray-700 text-center">
|
||||||
|
// Spinner
|
||||||
|
<div class="w-20 h-20 mx-auto rounded-full bg-blue-900/30 flex items-center justify-center mb-6 reconnect-spinner">
|
||||||
|
<svg
|
||||||
|
class="w-10 h-10 text-blue-400 animate-spin"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="reconnect-title"
|
||||||
|
class="text-xl font-semibold text-white mb-2"
|
||||||
|
>
|
||||||
|
"Reconnecting..."
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-300 mb-4">"Attempting to restore connection"</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-400">
|
||||||
|
{format!("{} {}", phase_text, attempt_text)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OverlayState::Failed => Some(
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[100] flex items-center justify-center"
|
||||||
|
role="alertdialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="reconnect-title"
|
||||||
|
>
|
||||||
|
// Darkened backdrop
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/80 backdrop-blur-md"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
// Dialog box
|
||||||
|
<div class="relative bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 p-8 border border-gray-700 text-center">
|
||||||
|
// Error icon
|
||||||
|
<div class="w-20 h-20 mx-auto rounded-full bg-red-900/30 flex items-center justify-center mb-6">
|
||||||
|
<svg
|
||||||
|
class="w-10 h-10 text-red-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="reconnect-title"
|
||||||
|
class="text-xl font-semibold text-white mb-2"
|
||||||
|
>
|
||||||
|
"Connection Failed"
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-300 mb-6">
|
||||||
|
"Unable to reconnect after multiple attempts. Please check your network connection and try again."
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 transition-colors duration-200"
|
||||||
|
on:click=move |_| {
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
{
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let _ = window.location().reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
"Refresh Page"
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the countdown timer.
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
fn start_countdown_timer(
|
||||||
|
timer_handle: std::rc::Rc<std::cell::RefCell<Option<gloo_timers::callback::Interval>>>,
|
||||||
|
set_overlay_state: WriteSignal<OverlayState>,
|
||||||
|
on_reconnect: Callback<()>,
|
||||||
|
) {
|
||||||
|
use gloo_timers::callback::Interval;
|
||||||
|
|
||||||
|
// Stop any existing timer
|
||||||
|
if let Some(old_timer) = timer_handle.borrow_mut().take() {
|
||||||
|
drop(old_timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new timer that ticks every second
|
||||||
|
let timer = Interval::new(1000, move || {
|
||||||
|
set_overlay_state.update(|state| {
|
||||||
|
if let OverlayState::Countdown { phase, remaining } = state {
|
||||||
|
if *remaining > 1 {
|
||||||
|
// Decrement countdown
|
||||||
|
*remaining -= 1;
|
||||||
|
} else {
|
||||||
|
// Countdown reached zero - trigger reconnection
|
||||||
|
*state = OverlayState::Reconnecting { phase: *phase };
|
||||||
|
on_reconnect.run(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
*timer_handle.borrow_mut() = Some(timer);
|
||||||
|
}
|
||||||
|
|
@ -80,6 +80,7 @@ pub struct WsError {
|
||||||
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>,
|
||||||
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>>,
|
||||||
|
|
@ -120,6 +121,8 @@ pub fn use_channel_websocket(
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
let slug = realm_slug.get();
|
let slug = realm_slug.get();
|
||||||
let ch_id = channel_id.get();
|
let ch_id = channel_id.get();
|
||||||
|
// Track reconnect_trigger to force reconnection when it changes
|
||||||
|
let _trigger = reconnect_trigger.get();
|
||||||
|
|
||||||
// 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() {
|
||||||
|
|
@ -458,6 +461,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>,
|
||||||
_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>>,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ use uuid::Uuid;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
ActiveBubble, AvatarEditorPopup, Card, ChatInput, ConversationModal, EmotionKeybindings,
|
ActiveBubble, AvatarEditorPopup, Card, ChatInput, ConversationModal, EmotionKeybindings,
|
||||||
FadingMember, InventoryPopup, KeybindingsPopup, MessageLog, NotificationHistoryModal,
|
FadingMember, InventoryPopup, KeybindingsPopup, MessageLog, NotificationHistoryModal,
|
||||||
NotificationMessage, NotificationToast, RealmHeader, RealmSceneViewer, SettingsPopup,
|
NotificationMessage, NotificationToast, RealmHeader, RealmSceneViewer, ReconnectionOverlay,
|
||||||
ViewerSettings,
|
SettingsPopup, ViewerSettings,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
|
|
@ -118,6 +118,9 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
// Error notification state (for whisper failures, etc.)
|
// Error notification state (for whisper failures, etc.)
|
||||||
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
|
||||||
|
let (reconnect_trigger, set_reconnect_trigger) = signal(0u32);
|
||||||
|
|
||||||
let realm_data = LocalResource::new(move || {
|
let realm_data = LocalResource::new(move || {
|
||||||
let slug = slug.get();
|
let slug = slug.get();
|
||||||
async move {
|
async move {
|
||||||
|
|
@ -330,9 +333,10 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
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()),
|
||||||
on_members_update,
|
on_members_update,
|
||||||
on_chat_message,
|
on_chat_message,
|
||||||
on_loose_props_sync,
|
on_loose_props_sync,
|
||||||
|
|
@ -954,6 +958,22 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconnection overlay - shown when WebSocket disconnects
|
||||||
|
{
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
let ws_state_for_overlay = ws_state;
|
||||||
|
#[cfg(not(feature = "hydrate"))]
|
||||||
|
let ws_state_for_overlay = Signal::derive(|| crate::components::ws_client::WsState::Disconnected);
|
||||||
|
view! {
|
||||||
|
<ReconnectionOverlay
|
||||||
|
ws_state=ws_state_for_overlay
|
||||||
|
on_reconnect=Callback::new(move |_: ()| {
|
||||||
|
set_reconnect_trigger.update(|t| *t += 1);
|
||||||
|
})
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue