From 29f29358fd14d316426174551a6b27389a8312c620f526d27d0a3353e24100fd Mon Sep 17 00:00:00 2001 From: Evan Carroll Date: Tue, 20 Jan 2026 12:39:24 -0600 Subject: [PATCH] feat: slug in url if /t is enabled --- apps/chattyness-app/src/app.rs | 9 ++ crates/chattyness-user-ui/src/pages/realm.rs | 154 +++++++++++++++++-- crates/chattyness-user-ui/src/routes.rs | 9 ++ 3 files changed, 162 insertions(+), 10 deletions(-) diff --git a/apps/chattyness-app/src/app.rs b/apps/chattyness-app/src/app.rs index bf7a632..73b8f73 100644 --- a/apps/chattyness-app/src/app.rs +++ b/apps/chattyness-app/src/app.rs @@ -259,6 +259,15 @@ pub fn CombinedApp() -> impl IntoView { + // ========================================== // Admin routes (lazy loading) diff --git a/crates/chattyness-user-ui/src/pages/realm.rs b/crates/chattyness-user-ui/src/pages/realm.rs index b2034c4..ef81a32 100644 --- a/crates/chattyness-user-ui/src/pages/realm.rs +++ b/crates/chattyness-user-ui/src/pages/realm.rs @@ -44,6 +44,9 @@ pub fn RealmPage() -> impl IntoView { let slug = Signal::derive(move || params.read().get("slug").unwrap_or_default()); + // Scene slug from URL (for direct scene navigation) + let scene_slug_param = Signal::derive(move || params.read().get("scene_slug")); + // Channel member state let (members, set_members) = signal(Vec::::new()); let (channel_id, set_channel_id) = signal(Option::::None); @@ -350,10 +353,12 @@ pub fn RealmPage() -> impl IntoView { #[cfg(feature = "hydrate")] let on_teleport_approved = Callback::new(move |info: TeleportInfo| { let scene_id = info.scene_id; - let scene_slug = info.scene_slug; + let scene_slug = info.scene_slug.clone(); let realm_slug = slug.get_untracked(); // Fetch the new scene data to update the canvas background + let scene_slug_for_url = scene_slug.clone(); + let realm_slug_for_url = realm_slug.clone(); spawn_local(async move { use gloo_net::http::Request; let response = Request::get(&format!( @@ -370,6 +375,26 @@ pub fn RealmPage() -> impl IntoView { if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) { set_scene_dimensions.set((w as f64, h as f64)); } + + // Update URL to reflect new scene + if let Some(window) = web_sys::window() { + if let Ok(history) = window.history() { + let new_url = if scene.is_entry_point { + format!("/realms/{}", realm_slug_for_url) + } else { + format!( + "/realms/{}/scenes/{}", + realm_slug_for_url, scene_slug_for_url + ) + }; + let _ = history.replace_state_with_url( + &wasm_bindgen::JsValue::NULL, + "", + Some(&new_url), + ); + } + } + // Update the current scene for the viewer set_current_scene.set(Some(scene)); } @@ -408,16 +433,125 @@ pub fn RealmPage() -> impl IntoView { // uses scenes directly. Proper channel infrastructure can be added later. #[cfg(feature = "hydrate")] { - Effect::new(move |_| { - let Some(scene) = entry_scene.get().flatten() else { - return; - }; - set_channel_id.set(Some(scene.id)); - set_current_scene.set(Some(scene.clone())); + // Track whether we've handled initial scene load to prevent double-loading + let initial_scene_handled = StoredValue::new_local(false); - // Extract scene dimensions from bounds_wkt - if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) { - set_scene_dimensions.set((w as f64, h as f64)); + Effect::new(move |_| { + // Skip if already handled + if initial_scene_handled.get_value() { + return; + } + + let url_scene_slug = scene_slug_param.get(); + let has_url_scene = url_scene_slug + .as_ref() + .is_some_and(|s| !s.is_empty()); + + if has_url_scene { + // URL has a scene slug - wait for realm data to check if teleport is allowed + let Some(realm_with_role) = realm_data.get().flatten() else { + return; + }; + + let realm_slug_val = slug.get(); + let scene_slug_val = url_scene_slug.unwrap(); + + if !realm_with_role.realm.allow_user_teleport { + // Teleport disabled - redirect to base realm URL and show error + initial_scene_handled.set_value(true); + set_error_message.set(Some( + "Direct scene access is disabled for this realm".to_string(), + )); + + // Redirect to base realm URL + let navigate = use_navigate(); + navigate( + &format!("/realms/{}", realm_slug_val), + leptos_router::NavigateOptions { + replace: true, + ..Default::default() + }, + ); + + // Auto-dismiss error after 5 seconds + use gloo_timers::callback::Timeout; + Timeout::new(5000, move || { + set_error_message.set(None); + }) + .forget(); + + // Fall back to entry scene + if let Some(scene) = entry_scene.get().flatten() { + set_channel_id.set(Some(scene.id)); + set_current_scene.set(Some(scene.clone())); + if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) { + set_scene_dimensions.set((w as f64, h as f64)); + } + } + return; + } + + // Teleport allowed - fetch the specific scene + initial_scene_handled.set_value(true); + spawn_local(async move { + use gloo_net::http::Request; + let response = Request::get(&format!( + "/api/realms/{}/scenes/{}", + realm_slug_val, scene_slug_val + )) + .send() + .await; + + if let Ok(resp) = response { + if resp.ok() { + if let Ok(scene) = resp.json::().await { + set_channel_id.set(Some(scene.id)); + if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) { + set_scene_dimensions.set((w as f64, h as f64)); + } + set_current_scene.set(Some(scene)); + return; + } + } + } + + // Scene not found - show error and fall back to entry scene + set_error_message.set(Some(format!( + "Scene '{}' not found", + scene_slug_val + ))); + + // Update URL to base realm URL + if let Some(window) = web_sys::window() { + if let Ok(history) = window.history() { + let _ = history.replace_state_with_url( + &wasm_bindgen::JsValue::NULL, + "", + Some(&format!("/realms/{}", realm_slug_val)), + ); + } + } + + // Auto-dismiss error after 5 seconds + use gloo_timers::callback::Timeout; + Timeout::new(5000, move || { + set_error_message.set(None); + }) + .forget(); + }); + } else { + // No URL scene slug - use entry scene + let Some(scene) = entry_scene.get().flatten() else { + return; + }; + initial_scene_handled.set_value(true); + set_channel_id.set(Some(scene.id)); + set_current_scene.set(Some(scene.clone())); + + // Extract scene dimensions from bounds_wkt + if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) { + set_scene_dimensions.set((w as f64, h as f64)); + } } }); } diff --git a/crates/chattyness-user-ui/src/routes.rs b/crates/chattyness-user-ui/src/routes.rs index b6febbe..f523062 100644 --- a/crates/chattyness-user-ui/src/routes.rs +++ b/crates/chattyness-user-ui/src/routes.rs @@ -31,6 +31,15 @@ pub fn UserRoutes() -> impl IntoView { + } }