fix: all remaining bugs with props
This commit is contained in:
parent
5e14481714
commit
475d1ef90a
8 changed files with 123 additions and 16 deletions
|
|
@ -311,17 +311,18 @@ pub async fn pick_up_loose_prop<'e>(
|
||||||
'[]'::jsonb,
|
'[]'::jsonb,
|
||||||
now()
|
now()
|
||||||
FROM source_info si
|
FROM source_info si
|
||||||
RETURNING id, prop_name, prop_asset_path, layer, is_transferable, is_portable, is_droppable, acquired_at
|
RETURNING id, server_prop_id, realm_prop_id, prop_name, prop_asset_path, layer, is_transferable, is_portable, is_droppable, origin, acquired_at
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
ii.id,
|
ii.id,
|
||||||
|
COALESCE(ii.server_prop_id, ii.realm_prop_id) as prop_id,
|
||||||
ii.prop_name,
|
ii.prop_name,
|
||||||
ii.prop_asset_path,
|
ii.prop_asset_path,
|
||||||
ii.layer,
|
ii.layer,
|
||||||
ii.is_transferable,
|
ii.is_transferable,
|
||||||
ii.is_portable,
|
ii.is_portable,
|
||||||
ii.is_droppable,
|
ii.is_droppable,
|
||||||
'server_library'::server.prop_origin as origin,
|
ii.origin,
|
||||||
ii.acquired_at
|
ii.acquired_at
|
||||||
FROM inserted_item ii
|
FROM inserted_item ii
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -620,3 +621,27 @@ pub async fn cleanup_expired_props<'e>(executor: impl PgExecutor<'e>) -> Result<
|
||||||
|
|
||||||
Ok(result.rows_affected())
|
Ok(result.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a loose prop from the scene (moderator action).
|
||||||
|
///
|
||||||
|
/// This permanently removes the prop without adding it to anyone's inventory.
|
||||||
|
pub async fn delete_loose_prop<'e>(
|
||||||
|
executor: impl PgExecutor<'e>,
|
||||||
|
loose_prop_id: Uuid,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM scene.loose_props
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(loose_prop_id)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(AppError::NotFound("Loose prop not found".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,12 @@ pub enum ClientMessage {
|
||||||
/// Inventory item ID to delete.
|
/// Inventory item ID to delete.
|
||||||
inventory_item_id: Uuid,
|
inventory_item_id: Uuid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Delete a loose prop from the scene (moderator only).
|
||||||
|
DeleteLooseProp {
|
||||||
|
/// The loose prop ID to delete.
|
||||||
|
loose_prop_id: Uuid,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server-to-client WebSocket messages.
|
/// Server-to-client WebSocket messages.
|
||||||
|
|
@ -263,6 +269,14 @@ pub enum ServerMessage {
|
||||||
inventory_item_id: Uuid,
|
inventory_item_id: Uuid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// A loose prop was deleted from the scene (by a moderator).
|
||||||
|
LoosePropDeleted {
|
||||||
|
/// ID of the deleted loose prop.
|
||||||
|
prop_id: Uuid,
|
||||||
|
/// User ID who deleted it.
|
||||||
|
deleted_by_user_id: Uuid,
|
||||||
|
},
|
||||||
|
|
||||||
/// A prop was updated (scale changed) - clients should update their local copy.
|
/// A prop was updated (scale changed) - clients should update their local copy.
|
||||||
PropRefresh {
|
PropRefresh {
|
||||||
/// The updated prop with all current values.
|
/// The updated prop with all current values.
|
||||||
|
|
|
||||||
|
|
@ -1658,6 +1658,51 @@ async fn handle_socket(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ClientMessage::DeleteLooseProp { loose_prop_id } => {
|
||||||
|
// Check if user is a moderator
|
||||||
|
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_mod {
|
||||||
|
let _ = direct_tx.send(ServerMessage::Error {
|
||||||
|
code: "NOT_MODERATOR".to_string(),
|
||||||
|
message: "You do not have permission to delete props".to_string(),
|
||||||
|
}).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the prop
|
||||||
|
match loose_props::delete_loose_prop(
|
||||||
|
&mut *recv_conn,
|
||||||
|
loose_prop_id,
|
||||||
|
).await {
|
||||||
|
Ok(()) => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
tracing::debug!(
|
||||||
|
"[WS] User {} deleted prop {}",
|
||||||
|
user_id,
|
||||||
|
loose_prop_id
|
||||||
|
);
|
||||||
|
// Broadcast the deletion to all users in the channel
|
||||||
|
let _ = tx.send(ServerMessage::LoosePropDeleted {
|
||||||
|
prop_id: loose_prop_id,
|
||||||
|
deleted_by_user_id: user_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[WS] Delete prop failed: {:?}", e);
|
||||||
|
let _ = direct_tx.send(ServerMessage::Error {
|
||||||
|
code: "DELETE_PROP_FAILED".to_string(),
|
||||||
|
message: format!("{:?}", e),
|
||||||
|
}).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Close(close_frame) => {
|
Message::Close(close_frame) => {
|
||||||
|
|
|
||||||
|
|
@ -65,13 +65,6 @@ pub fn LoosePropCanvas(
|
||||||
let canvas_x = screen_x - prop_size / 2.0;
|
let canvas_x = screen_x - prop_size / 2.0;
|
||||||
let canvas_y = screen_y - prop_size / 2.0;
|
let canvas_y = screen_y - prop_size / 2.0;
|
||||||
|
|
||||||
// Add amber dashed border for locked props
|
|
||||||
let border_style = if p.is_locked {
|
|
||||||
"border: 2px dashed #f59e0b; box-sizing: border-box;"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"position: absolute; \
|
"position: absolute; \
|
||||||
left: 0; top: 0; \
|
left: 0; top: 0; \
|
||||||
|
|
@ -79,8 +72,8 @@ pub fn LoosePropCanvas(
|
||||||
z-index: {}; \
|
z-index: {}; \
|
||||||
pointer-events: auto; \
|
pointer-events: auto; \
|
||||||
width: {}px; \
|
width: {}px; \
|
||||||
height: {}px; {}",
|
height: {}px;",
|
||||||
canvas_x, canvas_y, z_index, prop_size, prop_size, border_style
|
canvas_x, canvas_y, z_index, prop_size, prop_size
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ pub fn RealmSceneViewer(
|
||||||
#[prop(optional, into)] on_prop_scale_update: Option<Callback<(Uuid, f32)>>,
|
#[prop(optional, into)] on_prop_scale_update: Option<Callback<(Uuid, f32)>>,
|
||||||
#[prop(optional, into)] on_prop_move: Option<Callback<(Uuid, f64, f64)>>,
|
#[prop(optional, into)] on_prop_move: Option<Callback<(Uuid, f64, f64)>>,
|
||||||
#[prop(optional, into)] on_prop_lock_toggle: Option<Callback<(Uuid, bool)>>,
|
#[prop(optional, into)] on_prop_lock_toggle: Option<Callback<(Uuid, bool)>>,
|
||||||
|
#[prop(optional, into)] on_prop_delete: Option<Callback<Uuid>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
|
let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
|
||||||
let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
|
let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
|
||||||
|
|
@ -131,6 +132,7 @@ pub fn RealmSceneViewer(
|
||||||
let move_mode_preview_position = RwSignal::new((0.0_f64, 0.0_f64));
|
let move_mode_preview_position = RwSignal::new((0.0_f64, 0.0_f64));
|
||||||
let (move_mode_prop_scale, set_move_mode_prop_scale) = signal(1.0_f32);
|
let (move_mode_prop_scale, set_move_mode_prop_scale) = signal(1.0_f32);
|
||||||
let (prop_context_is_locked, set_prop_context_is_locked) = signal(false);
|
let (prop_context_is_locked, set_prop_context_is_locked) = signal(false);
|
||||||
|
let (prop_context_name, set_prop_context_name) = signal(Option::<String>::None);
|
||||||
|
|
||||||
// Click handler for movement (props are now handled via context menu)
|
// Click handler for movement (props are now handled via context menu)
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
|
|
@ -221,6 +223,7 @@ pub fn RealmSceneViewer(
|
||||||
|
|
||||||
set_scale_mode_initial_scale.set(prop.scale);
|
set_scale_mode_initial_scale.set(prop.scale);
|
||||||
set_prop_context_is_locked.set(prop.is_locked);
|
set_prop_context_is_locked.set(prop.is_locked);
|
||||||
|
set_prop_context_name.set(Some(prop.prop_name.clone()));
|
||||||
set_move_mode_prop_scale.set(prop.scale);
|
set_move_mode_prop_scale.set(prop.scale);
|
||||||
let rect = canvas.get_bounding_client_rect();
|
let rect = canvas.get_bounding_client_rect();
|
||||||
set_scale_mode_prop_center.set((
|
set_scale_mode_prop_center.set((
|
||||||
|
|
@ -598,7 +601,9 @@ pub fn RealmSceneViewer(
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
open=Signal::derive(move || prop_context_menu_open.get())
|
open=Signal::derive(move || prop_context_menu_open.get())
|
||||||
position=Signal::derive(move || prop_context_menu_position.get())
|
position=Signal::derive(move || prop_context_menu_position.get())
|
||||||
header=Signal::derive(move || Some("Prop".to_string()))
|
header=Signal::derive(move || {
|
||||||
|
prop_context_name.get().or_else(|| Some("Prop".to_string()))
|
||||||
|
})
|
||||||
items=Signal::derive(move || {
|
items=Signal::derive(move || {
|
||||||
let is_mod = is_moderator.map(|s| s.get()).unwrap_or(false);
|
let is_mod = is_moderator.map(|s| s.get()).unwrap_or(false);
|
||||||
let is_locked = prop_context_is_locked.get();
|
let is_locked = prop_context_is_locked.get();
|
||||||
|
|
@ -613,12 +618,14 @@ pub fn RealmSceneViewer(
|
||||||
label: if is_locked { "Unlock" } else { "Lock" }.to_string(),
|
label: if is_locked { "Unlock" } else { "Lock" }.to_string(),
|
||||||
action: if is_locked { "unlock" } else { "lock" }.to_string(),
|
action: if is_locked { "unlock" } else { "lock" }.to_string(),
|
||||||
});
|
});
|
||||||
|
items.push(ContextMenuItem { label: "Delete".to_string(), action: "delete".to_string() });
|
||||||
}
|
}
|
||||||
items
|
items
|
||||||
})
|
})
|
||||||
on_select=Callback::new({
|
on_select=Callback::new({
|
||||||
let on_prop_lock_toggle = on_prop_lock_toggle.clone();
|
let on_prop_lock_toggle = on_prop_lock_toggle.clone();
|
||||||
let on_prop_click = on_prop_click.clone();
|
let on_prop_click = on_prop_click.clone();
|
||||||
|
let on_prop_delete = on_prop_delete.clone();
|
||||||
move |action: String| {
|
move |action: String| {
|
||||||
match action.as_str() {
|
match action.as_str() {
|
||||||
"pick_up" => {
|
"pick_up" => {
|
||||||
|
|
@ -649,6 +656,13 @@ pub fn RealmSceneViewer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"delete" => {
|
||||||
|
if let Some(prop_id) = prop_context_menu_target.get() {
|
||||||
|
if let Some(ref callback) = on_prop_delete {
|
||||||
|
callback.run(prop_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
set_prop_context_menu_open.set(false);
|
set_prop_context_menu_open.set(false);
|
||||||
|
|
@ -657,6 +671,7 @@ pub fn RealmSceneViewer(
|
||||||
on_close=Callback::new(move |_: ()| {
|
on_close=Callback::new(move |_: ()| {
|
||||||
set_prop_context_menu_open.set(false);
|
set_prop_context_menu_open.set(false);
|
||||||
set_prop_context_menu_target.set(None);
|
set_prop_context_menu_target.set(None);
|
||||||
|
set_prop_context_name.set(None);
|
||||||
})
|
})
|
||||||
/>
|
/>
|
||||||
<ScaleOverlay
|
<ScaleOverlay
|
||||||
|
|
|
||||||
|
|
@ -182,9 +182,9 @@ pub fn MoveOverlay(
|
||||||
let mouse_x = ev.client_x() as f64;
|
let mouse_x = ev.client_x() as f64;
|
||||||
let mouse_y = ev.client_y() as f64;
|
let mouse_y = ev.client_y() as f64;
|
||||||
|
|
||||||
// Get scene viewer's position
|
// Get scene canvas position (the inner container with the actual scene)
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
if let Some(viewer) = document.query_selector(".scene-viewer-container").ok().flatten() {
|
if let Some(viewer) = document.query_selector(".scene-canvas").ok().flatten() {
|
||||||
let rect = viewer.get_bounding_client_rect();
|
let rect = viewer.get_bounding_client_rect();
|
||||||
let viewer_x = mouse_x - rect.left();
|
let viewer_x = mouse_x - rect.left();
|
||||||
let viewer_y = mouse_y - rect.top();
|
let viewer_y = mouse_y - rect.top();
|
||||||
|
|
@ -246,10 +246,10 @@ pub fn MoveOverlay(
|
||||||
let ox = offset_x.get();
|
let ox = offset_x.get();
|
||||||
let oy = offset_y.get();
|
let oy = offset_y.get();
|
||||||
|
|
||||||
// Get scene viewer position in viewport
|
// Get scene canvas position in viewport (inner container with actual scene)
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
let viewer_offset = document
|
let viewer_offset = document
|
||||||
.query_selector(".scene-viewer-container")
|
.query_selector(".scene-canvas")
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
|
|
|
||||||
|
|
@ -667,6 +667,10 @@ fn handle_server_message(
|
||||||
// No scene state change needed
|
// No scene state change needed
|
||||||
PostAction::None
|
PostAction::None
|
||||||
}
|
}
|
||||||
|
ServerMessage::LoosePropDeleted { prop_id, .. } => {
|
||||||
|
// Treat deleted props the same as picked up (remove from display)
|
||||||
|
PostAction::PropPickedUp(prop_id)
|
||||||
|
}
|
||||||
ServerMessage::PropRefresh { prop } => {
|
ServerMessage::PropRefresh { prop } => {
|
||||||
PostAction::PropRefresh(prop)
|
PostAction::PropRefresh(prop)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1244,6 +1244,16 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
let ws_for_prop_delete = ws_sender_clone.clone();
|
||||||
|
let on_prop_delete_cb = Callback::new(move |prop_id: Uuid| {
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
ws_for_prop_delete.with_value(|sender| {
|
||||||
|
if let Some(send_fn) = sender {
|
||||||
|
send_fn(ClientMessage::DeleteLooseProp { loose_prop_id: prop_id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
view! {
|
view! {
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<RealmSceneViewer
|
<RealmSceneViewer
|
||||||
|
|
@ -1269,6 +1279,7 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
on_prop_scale_update=on_prop_scale_update_cb
|
on_prop_scale_update=on_prop_scale_update_cb
|
||||||
on_prop_move=on_prop_move_cb
|
on_prop_move=on_prop_move_cb
|
||||||
on_prop_lock_toggle=on_prop_lock_toggle_cb
|
on_prop_lock_toggle=on_prop_lock_toggle_cb
|
||||||
|
on_prop_delete=on_prop_delete_cb
|
||||||
/>
|
/>
|
||||||
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue