//! Realm landing page after login. use leptos::prelude::*; use leptos::reactive::owner::LocalStorage; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; #[cfg(feature = "hydrate")] use leptos_router::hooks::use_navigate; use leptos_router::hooks::use_params_map; use crate::components::{Card, ChatInput, RealmHeader, RealmSceneViewer}; #[cfg(feature = "hydrate")] use crate::components::use_channel_websocket; use chattyness_db::models::{ ChannelMemberWithAvatar, EmotionAvailability, RealmRole, RealmWithUserRole, Scene, }; #[cfg(feature = "hydrate")] use chattyness_db::ws_messages::ClientMessage; use crate::components::ws_client::WsSender; /// Realm landing page component. #[component] pub fn RealmPage() -> impl IntoView { let params = use_params_map(); #[cfg(feature = "hydrate")] let navigate = use_navigate(); let slug = Signal::derive(move || params.read().get("slug").unwrap_or_default()); // Channel member state let (members, set_members) = signal(Vec::::new()); let (channel_id, set_channel_id) = signal(Option::::None); // Chat focus coordination let (chat_focused, set_chat_focused) = signal(false); let (focus_chat_trigger, set_focus_chat_trigger) = signal(false); // Emotion availability for emote picker let (emotion_availability, set_emotion_availability) = signal(Option::::None); // Skin preview path for emote picker (position 4 of skin layer) let (skin_preview_path, set_skin_preview_path) = signal(Option::::None); let realm_data = LocalResource::new(move || { let slug = slug.get(); async move { if slug.is_empty() { return None; } #[cfg(feature = "hydrate")] { use gloo_net::http::Request; let response = Request::get(&format!("/api/realms/{}", slug)).send().await; match response { Ok(resp) if resp.ok() => resp.json::().await.ok(), _ => None, } } #[cfg(not(feature = "hydrate"))] { let _ = slug; None:: } } }); // Fetch entry scene for the realm let entry_scene = LocalResource::new(move || { let slug = slug.get(); async move { if slug.is_empty() { return None; } #[cfg(feature = "hydrate")] { use gloo_net::http::Request; let response = Request::get(&format!("/api/realms/{}/entry-scene", slug)) .send() .await; match response { Ok(resp) if resp.ok() => resp.json::().await.ok(), _ => None, } } #[cfg(not(feature = "hydrate"))] { let _ = slug; None:: } } }); // Fetch emotion availability and avatar render data for emote picker #[cfg(feature = "hydrate")] { let slug_for_emotions = slug.clone(); Effect::new(move |_| { use gloo_net::http::Request; let current_slug = slug_for_emotions.get(); if current_slug.is_empty() { return; } // Fetch emotion availability let slug_clone = current_slug.clone(); spawn_local(async move { let response = Request::get(&format!("/api/realms/{}/avatar/emotions", slug_clone)) .send() .await; if let Ok(resp) = response { if resp.ok() { if let Ok(avail) = resp.json::().await { set_emotion_availability.set(Some(avail)); } } } }); // Fetch avatar render data for skin preview let slug_clone2 = current_slug.clone(); spawn_local(async move { use chattyness_db::models::AvatarRenderData; let response = Request::get(&format!("/api/realms/{}/avatar/current", slug_clone2)) .send() .await; if let Ok(resp) = response { if resp.ok() { if let Ok(render_data) = resp.json::().await { // Get skin layer position 4 (center) set_skin_preview_path.set(render_data.skin_layer[4].clone()); } } } }); }); } // WebSocket connection for real-time updates #[cfg(feature = "hydrate")] let on_members_update = Callback::new(move |new_members: Vec| { set_members.set(new_members); }); #[cfg(feature = "hydrate")] let (_ws_state, ws_sender) = use_channel_websocket( slug, Signal::derive(move || channel_id.get()), on_members_update, ); // Set channel ID when scene loads (triggers WebSocket connection) #[cfg(feature = "hydrate")] { Effect::new(move |_| { let Some(scene) = entry_scene.get().flatten() else { return; }; set_channel_id.set(Some(scene.id)); }); } // Handle position update via WebSocket #[cfg(feature = "hydrate")] let on_move = Callback::new(move |(x, y): (f64, f64)| { ws_sender.with_value(|sender| { if let Some(send_fn) = sender { send_fn(ClientMessage::UpdatePosition { x, y }); } }); }); #[cfg(not(feature = "hydrate"))] let on_move = Callback::new(move |(_x, _y): (f64, f64)| {}); // Handle global keyboard shortcuts (e+0-9 for emotions, : for chat focus) #[cfg(feature = "hydrate")] { use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::{closure::Closure, JsCast}; let closure_holder: Rc>>> = Rc::new(RefCell::new(None)); let closure_holder_clone = closure_holder.clone(); Effect::new(move |_| { // Cleanup previous closure if any if let Some(old_closure) = closure_holder_clone.borrow_mut().take() { if let Some(window) = web_sys::window() { let _ = window.remove_event_listener_with_callback( "keydown", old_closure.as_ref().unchecked_ref(), ); } } let current_slug = slug.get(); if current_slug.is_empty() { return; } // Track if 'e' was pressed (for e+0-9 emotion sequence) let e_pressed: Rc> = Rc::new(RefCell::new(false)); let e_pressed_clone = e_pressed.clone(); let closure = Closure::new(move |ev: web_sys::KeyboardEvent| { let key = ev.key(); // If chat is focused, let it handle all keys if chat_focused.get() { *e_pressed_clone.borrow_mut() = false; return; } // Handle ':' to focus chat input if key == ":" { set_focus_chat_trigger.set(true); // Reset trigger after a short delay so it can be triggered again use gloo_timers::callback::Timeout; Timeout::new(100, move || { set_focus_chat_trigger.set(false); }) .forget(); ev.prevent_default(); return; } // Check if 'e' key was pressed if key == "e" || key == "E" { *e_pressed_clone.borrow_mut() = true; return; } // Check for 0-9 after 'e' was pressed if *e_pressed_clone.borrow() { *e_pressed_clone.borrow_mut() = false; // Reset regardless of outcome if key.len() == 1 { if let Ok(emotion) = key.parse::() { if emotion <= 9 { #[cfg(debug_assertions)] web_sys::console::log_1( &format!("[Emotion] Sending emotion {}", emotion).into(), ); ws_sender.with_value(|sender| { if let Some(send_fn) = sender { send_fn(ClientMessage::UpdateEmotion { emotion }); } }); } } } } else { // Any other key resets the 'e' state *e_pressed_clone.borrow_mut() = false; } }); if let Some(window) = web_sys::window() { let _ = window .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()); } // Store the closure for cleanup *closure_holder_clone.borrow_mut() = Some(closure); }); } // Callback for chat focus changes let on_chat_focus_change = Callback::new(move |focused: bool| { set_chat_focused.set(focused); }); // Create logout callback (WebSocket disconnects automatically) let on_logout = Callback::new(move |_: ()| { #[cfg(feature = "hydrate")] { use gloo_net::http::Request; let navigate = navigate.clone(); spawn_local(async move { // WebSocket close handles channel leave automatically let _: Result = Request::post("/api/auth/logout").send().await; navigate("/", Default::default()); }); } }); view! {

"Loading realm..."

} }> {move || { let on_logout = on_logout.clone(); let on_move = on_move.clone(); realm_data .get() .map(|maybe_data| { match maybe_data { Some(data) => { let realm = data.realm; let user_role = data.user_role; // Determine if user can access admin // Admin visible for: Owner, Moderator, or staff let can_admin = matches!( user_role, Some(RealmRole::Owner) | Some(RealmRole::Moderator) ); // Get scene name and description for header let scene_info = entry_scene .get() .flatten() .map(|s| (s.name.clone(), s.description.clone())) .unwrap_or_else(|| ("Loading...".to_string(), None)); let realm_name = realm.name.clone(); let realm_slug_val = realm.slug.clone(); let realm_description = realm.tagline.clone(); let online_count = realm.current_user_count; let total_members = realm.member_count; let max_capacity = realm.max_users; let scene_name = scene_info.0; let scene_description = scene_info.1; view! {
// Scene viewer - full width

"Loading scene..."

} }> {move || { let on_move = on_move.clone(); let on_chat_focus_change = on_chat_focus_change.clone(); let realm_slug_for_viewer = realm_slug_val.clone(); #[cfg(feature = "hydrate")] let ws_sender_clone = ws_sender.clone(); entry_scene .get() .map(|maybe_scene| { match maybe_scene { Some(scene) => { let members_signal = Signal::derive(move || members.get()); let emotion_avail_signal = Signal::derive(move || emotion_availability.get()); let skin_path_signal = Signal::derive(move || skin_preview_path.get()); let focus_trigger_signal = Signal::derive(move || focus_chat_trigger.get()); #[cfg(feature = "hydrate")] let ws_for_chat = ws_sender_clone.clone(); #[cfg(not(feature = "hydrate"))] let ws_for_chat: StoredValue, LocalStorage> = StoredValue::new_local(None); view! {
} .into_any() } None => { view! {

"No scenes have been created for this realm yet."

} .into_any() } } }) }}
} .into_any() } None => { view! {

"Realm Not Found"

"The realm you're looking for doesn't exist or you don't have access."

"Back to Home"
} .into_any() } } }) }} } }