feat: add teleport
This commit is contained in:
parent
226c2e02b5
commit
32e5e42462
11 changed files with 603 additions and 16 deletions
|
|
@ -20,14 +20,14 @@ use crate::components::{
|
|||
#[cfg(feature = "hydrate")]
|
||||
use crate::components::{
|
||||
ChannelMemberInfo, ChatMessage, DEFAULT_BUBBLE_TIMEOUT_MS, FADE_DURATION_MS, HistoryEntry,
|
||||
WsError, add_to_history, use_channel_websocket,
|
||||
TeleportInfo, WsError, add_to_history, use_channel_websocket,
|
||||
};
|
||||
use crate::utils::LocalStoragePersist;
|
||||
#[cfg(feature = "hydrate")]
|
||||
use crate::utils::parse_bounds_dimensions;
|
||||
use chattyness_db::models::{
|
||||
AvatarWithPaths, ChannelMemberWithAvatar, EmotionAvailability, LooseProp, RealmRole,
|
||||
RealmWithUserRole, Scene,
|
||||
RealmWithUserRole, Scene, SceneSummary,
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
use chattyness_db::ws_messages::ClientMessage;
|
||||
|
|
@ -123,6 +123,15 @@ pub fn RealmPage() -> impl IntoView {
|
|||
// Reconnection trigger - increment to force WebSocket reconnection
|
||||
let (reconnect_trigger, set_reconnect_trigger) = signal(0u32);
|
||||
|
||||
// Current scene (changes when teleporting)
|
||||
let (current_scene, set_current_scene) = signal(Option::<Scene>::None);
|
||||
|
||||
// Available scenes for teleportation (cached on load)
|
||||
let (available_scenes, set_available_scenes) = signal(Vec::<SceneSummary>::new());
|
||||
|
||||
// Whether teleportation is allowed in this realm
|
||||
let (allow_user_teleport, set_allow_user_teleport) = signal(false);
|
||||
|
||||
let realm_data = LocalResource::new(move || {
|
||||
let slug = slug.get();
|
||||
async move {
|
||||
|
|
@ -324,6 +333,8 @@ pub fn RealmPage() -> impl IntoView {
|
|||
// Display user-friendly error message
|
||||
let msg = match error.code.as_str() {
|
||||
"WHISPER_TARGET_NOT_FOUND" => error.message,
|
||||
"TELEPORT_DISABLED" => error.message,
|
||||
"SCENE_NOT_FOUND" => error.message,
|
||||
_ => format!("Error: {}", error.message),
|
||||
};
|
||||
set_error_message.set(Some(msg));
|
||||
|
|
@ -335,6 +346,47 @@ pub fn RealmPage() -> impl IntoView {
|
|||
.forget();
|
||||
});
|
||||
|
||||
// Callback for teleport approval - navigate to new scene
|
||||
#[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 realm_slug = slug.get_untracked();
|
||||
|
||||
// Fetch the new scene data to update the canvas background
|
||||
spawn_local(async move {
|
||||
use gloo_net::http::Request;
|
||||
let response = Request::get(&format!(
|
||||
"/api/realms/{}/scenes/{}",
|
||||
realm_slug, scene_slug
|
||||
))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Ok(resp) = response {
|
||||
if resp.ok() {
|
||||
if let Ok(scene) = resp.json::<Scene>().await {
|
||||
// Update scene dimensions from the new scene
|
||||
if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) {
|
||||
set_scene_dimensions.set((w as f64, h as f64));
|
||||
}
|
||||
// Update the current scene for the viewer
|
||||
set_current_scene.set(Some(scene));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update channel_id to trigger WebSocket reconnection
|
||||
set_channel_id.set(Some(scene_id));
|
||||
|
||||
// Clear members since we're switching scenes
|
||||
set_members.set(Vec::new());
|
||||
|
||||
// Trigger a reconnect to ensure fresh connection
|
||||
set_reconnect_trigger.update(|t| *t += 1);
|
||||
});
|
||||
});
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
let (ws_state, ws_sender) = use_channel_websocket(
|
||||
slug,
|
||||
|
|
@ -348,9 +400,10 @@ pub fn RealmPage() -> impl IntoView {
|
|||
on_member_fading,
|
||||
Some(on_welcome),
|
||||
Some(on_ws_error),
|
||||
Some(on_teleport_approved),
|
||||
);
|
||||
|
||||
// Set channel ID and scene dimensions when scene loads
|
||||
// Set channel ID, current scene, and scene dimensions when entry scene loads
|
||||
// Note: Currently using scene.id as the channel_id since channel_members
|
||||
// uses scenes directly. Proper channel infrastructure can be added later.
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
|
@ -360,6 +413,7 @@ pub fn RealmPage() -> impl IntoView {
|
|||
return;
|
||||
};
|
||||
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) {
|
||||
|
|
@ -368,6 +422,44 @@ pub fn RealmPage() -> impl IntoView {
|
|||
});
|
||||
}
|
||||
|
||||
// Fetch available scenes and realm settings when realm loads
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
Effect::new(move |_| {
|
||||
let Some(realm_with_role) = realm_data.get().flatten() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Set allow_user_teleport from realm settings
|
||||
set_allow_user_teleport.set(realm_with_role.realm.allow_user_teleport);
|
||||
|
||||
// Fetch scenes list for teleport command
|
||||
let current_slug = slug.get();
|
||||
if current_slug.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
spawn_local(async move {
|
||||
use gloo_net::http::Request;
|
||||
let response = Request::get(&format!("/api/realms/{}/scenes", current_slug))
|
||||
.send()
|
||||
.await;
|
||||
if let Ok(resp) = response {
|
||||
if resp.ok() {
|
||||
if let Ok(scenes) = resp.json::<Vec<SceneSummary>>().await {
|
||||
// Filter out hidden scenes
|
||||
let visible_scenes: Vec<SceneSummary> = scenes
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_hidden)
|
||||
.collect();
|
||||
set_available_scenes.set(visible_scenes);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup expired speech bubbles and fading members every second
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
|
|
@ -729,11 +821,16 @@ pub fn RealmPage() -> impl IntoView {
|
|||
let realm_slug_for_viewer = realm_slug_val.clone();
|
||||
#[cfg(feature = "hydrate")]
|
||||
let ws_sender_clone = ws_sender.clone();
|
||||
// Read current_scene in reactive context (before .map())
|
||||
// so changes trigger re-render
|
||||
let current_scene_val = current_scene.get();
|
||||
entry_scene
|
||||
.get()
|
||||
.map(|maybe_scene| {
|
||||
match maybe_scene {
|
||||
Some(scene) => {
|
||||
Some(entry_scene_data) => {
|
||||
// Use current_scene if set (after teleport), otherwise use entry scene
|
||||
let display_scene = current_scene_val.clone().unwrap_or_else(|| entry_scene_data.clone());
|
||||
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());
|
||||
|
|
@ -755,10 +852,22 @@ pub fn RealmPage() -> impl IntoView {
|
|||
let on_whisper_request_cb = Callback::new(move |target: String| {
|
||||
set_whisper_target.set(Some(target));
|
||||
});
|
||||
let scenes_signal = Signal::derive(move || available_scenes.get());
|
||||
let teleport_enabled_signal = Signal::derive(move || allow_user_teleport.get());
|
||||
#[cfg(feature = "hydrate")]
|
||||
let ws_for_teleport = ws_sender_clone.clone();
|
||||
let on_teleport_cb = Callback::new(move |scene_id: Uuid| {
|
||||
#[cfg(feature = "hydrate")]
|
||||
ws_for_teleport.with_value(|sender| {
|
||||
if let Some(send_fn) = sender {
|
||||
send_fn(ClientMessage::Teleport { scene_id });
|
||||
}
|
||||
});
|
||||
});
|
||||
view! {
|
||||
<div class="relative w-full">
|
||||
<RealmSceneViewer
|
||||
scene=scene
|
||||
scene=display_scene
|
||||
realm_slug=realm_slug_for_viewer.clone()
|
||||
members=members_signal
|
||||
active_bubbles=active_bubbles_signal
|
||||
|
|
@ -789,6 +898,9 @@ pub fn RealmPage() -> impl IntoView {
|
|||
on_open_settings=on_open_settings_cb
|
||||
on_open_inventory=on_open_inventory_cb
|
||||
whisper_target=whisper_target_signal
|
||||
scenes=scenes_signal
|
||||
allow_user_teleport=teleport_enabled_signal
|
||||
on_teleport=on_teleport_cb
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue