diff --git a/crates/chattyness-db/src/models.rs b/crates/chattyness-db/src/models.rs index ee1688e..3c0f37c 100644 --- a/crates/chattyness-db/src/models.rs +++ b/crates/chattyness-db/src/models.rs @@ -822,81 +822,21 @@ impl std::fmt::Display for PropOrigin { } } -/// Source of a prop, combining origin type and ID. -/// -/// This replaces the pattern of two nullable UUID columns (server_prop_id, realm_prop_id) -/// with a single enum that encodes both the source type and ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum PropSource { - /// Prop from the server-wide library (server.props) - Server(Uuid), - /// Prop from a realm-specific library (realm.props) - Realm(Uuid), - /// Prop uploaded by a user (auth.uploads) - future use - Upload(Uuid), -} - -impl PropSource { - /// Extract the UUID from any variant. - pub fn id(&self) -> Uuid { - match self { - PropSource::Server(id) | PropSource::Realm(id) | PropSource::Upload(id) => *id, - } - } -} - -impl std::fmt::Display for PropSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PropSource::Server(id) => write!(f, "server:{}", id), - PropSource::Realm(id) => write!(f, "realm:{}", id), - PropSource::Upload(id) => write!(f, "upload:{}", id), - } - } -} - -impl From<&PropSource> for PropOrigin { - fn from(source: &PropSource) -> Self { - match source { - PropSource::Server(_) => PropOrigin::ServerLibrary, - PropSource::Realm(_) => PropOrigin::RealmLibrary, - PropSource::Upload(_) => PropOrigin::UserUpload, - } - } -} - /// An inventory item (user-owned prop). #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct InventoryItem { pub id: Uuid, - /// The source prop ID (use `origin` to determine which table: Server or Realm) - pub prop_id: Option, pub prop_name: String, pub prop_asset_path: String, pub layer: Option, pub is_transferable: bool, pub is_portable: bool, pub is_droppable: bool, - /// The source of this prop (ServerLibrary, RealmLibrary, or UserUpload) pub origin: PropOrigin, pub acquired_at: DateTime, } -impl InventoryItem { - /// Get the prop source combining `prop_id` and `origin` into a `PropSource`. - /// - /// Returns `None` if `prop_id` is `None` (shouldn't happen in practice). - pub fn prop_source(&self) -> Option { - self.prop_id.map(|id| match self.origin { - PropOrigin::ServerLibrary => PropSource::Server(id), - PropOrigin::RealmLibrary => PropSource::Realm(id), - PropOrigin::UserUpload => PropSource::Upload(id), - }) - } -} - /// Response for inventory list. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InventoryResponse { @@ -939,69 +879,16 @@ pub struct PropAcquisitionListResponse { pub props: Vec, } -/// Intermediate row type for database queries returning loose props. -/// -/// This maps directly to database columns (with nullable server_prop_id/realm_prop_id) -/// and is converted to `LooseProp` with a `PropSource` enum. -#[cfg(feature = "ssr")] -#[derive(Debug, Clone, sqlx::FromRow)] -pub struct LoosePropRow { +/// A prop dropped in a channel, available for pickup. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] +pub struct LooseProp { pub id: Uuid, pub channel_id: Uuid, pub server_prop_id: Option, pub realm_prop_id: Option, pub position_x: f64, pub position_y: f64, - pub scale: f32, - pub dropped_by: Option, - pub expires_at: Option>, - pub created_at: DateTime, - pub prop_name: String, - pub prop_asset_path: String, - pub is_locked: bool, - pub locked_by: Option, -} - -#[cfg(feature = "ssr")] -impl From for LooseProp { - fn from(row: LoosePropRow) -> Self { - let source = if let Some(id) = row.server_prop_id { - PropSource::Server(id) - } else if let Some(id) = row.realm_prop_id { - PropSource::Realm(id) - } else { - // Database CHECK constraint ensures exactly one is set, - // but we need a fallback. This should never happen. - panic!("LoosePropRow has neither server_prop_id nor realm_prop_id set") - }; - - LooseProp { - id: row.id, - channel_id: row.channel_id, - source, - position_x: row.position_x, - position_y: row.position_y, - scale: row.scale, - dropped_by: row.dropped_by, - expires_at: row.expires_at, - created_at: row.created_at, - prop_name: row.prop_name, - prop_asset_path: row.prop_asset_path, - is_locked: row.is_locked, - locked_by: row.locked_by, - } - } -} - -/// A prop dropped in a channel, available for pickup. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LooseProp { - pub id: Uuid, - pub channel_id: Uuid, - /// The source of this prop (server library, realm library, or user upload). - pub source: PropSource, - pub position_x: f64, - pub position_y: f64, /// Scale factor (0.1 - 10.0) inherited from prop definition at drop time. pub scale: f32, pub dropped_by: Option, diff --git a/crates/chattyness-db/src/queries/inventory.rs b/crates/chattyness-db/src/queries/inventory.rs index 82efdcc..4c9d854 100644 --- a/crates/chattyness-db/src/queries/inventory.rs +++ b/crates/chattyness-db/src/queries/inventory.rs @@ -15,7 +15,6 @@ pub async fn list_user_inventory<'e>( r#" SELECT id, - COALESCE(server_prop_id, realm_prop_id) as prop_id, prop_name, prop_asset_path, layer, @@ -272,7 +271,6 @@ pub async fn acquire_server_prop<'e>( AND oc.is_claimed = false RETURNING id, - server_prop_id as prop_id, prop_name, prop_asset_path, layer, @@ -449,7 +447,6 @@ pub async fn acquire_realm_prop<'e>( AND oc.is_claimed = false RETURNING id, - realm_prop_id as prop_id, prop_name, prop_asset_path, layer, diff --git a/crates/chattyness-db/src/queries/loose_props.rs b/crates/chattyness-db/src/queries/loose_props.rs index 0d94981..70eb3cb 100644 --- a/crates/chattyness-db/src/queries/loose_props.rs +++ b/crates/chattyness-db/src/queries/loose_props.rs @@ -5,7 +5,7 @@ use sqlx::PgExecutor; use uuid::Uuid; -use crate::models::{InventoryItem, LooseProp, LoosePropRow, PropSource}; +use crate::models::{InventoryItem, LooseProp}; use chattyness_error::{AppError, OptionExt}; /// Ensure an instance exists for a scene. @@ -37,7 +37,7 @@ pub async fn list_channel_loose_props<'e>( executor: impl PgExecutor<'e>, channel_id: Uuid, ) -> Result, AppError> { - let rows = sqlx::query_as::<_, LoosePropRow>( + let props = sqlx::query_as::<_, LooseProp>( r#" SELECT lp.id, @@ -66,7 +66,7 @@ pub async fn list_channel_loose_props<'e>( .fetch_all(executor) .await?; - Ok(rows.into_iter().map(LooseProp::from).collect()) + Ok(props) } /// Drop a prop from inventory to the canvas. @@ -224,22 +224,12 @@ pub async fn drop_prop_to_canvas<'e>( Some(prop_name), Some(prop_asset_path), )) => { - // Construct PropSource from the nullable columns - let source = if let Some(sid) = server_prop_id { - PropSource::Server(sid) - } else if let Some(rid) = realm_prop_id { - PropSource::Realm(rid) - } else { - return Err(AppError::Internal( - "Dropped prop has neither server_prop_id nor realm_prop_id".to_string(), - )); - }; - // Success! Convert f32 positions to f64. Ok(LooseProp { id, channel_id, - source, + server_prop_id, + realm_prop_id, position_x: position_x.into(), position_y: position_y.into(), scale, @@ -321,18 +311,17 @@ pub async fn pick_up_loose_prop<'e>( '[]'::jsonb, now() FROM source_info si - RETURNING id, server_prop_id, realm_prop_id, prop_name, prop_asset_path, layer, is_transferable, is_portable, is_droppable, origin, acquired_at + RETURNING id, prop_name, prop_asset_path, layer, is_transferable, is_portable, is_droppable, acquired_at ) SELECT ii.id, - COALESCE(ii.server_prop_id, ii.realm_prop_id) as prop_id, ii.prop_name, ii.prop_asset_path, ii.layer, ii.is_transferable, ii.is_portable, ii.is_droppable, - ii.origin, + 'server_library'::server.prop_origin as origin, ii.acquired_at FROM inserted_item ii "#, @@ -362,7 +351,7 @@ pub async fn update_loose_prop_scale<'e>( )); } - let row = sqlx::query_as::<_, LoosePropRow>( + let prop = sqlx::query_as::<_, LooseProp>( r#" WITH updated AS ( UPDATE scene.loose_props @@ -409,7 +398,7 @@ pub async fn update_loose_prop_scale<'e>( .await? .or_not_found("Loose prop (may have expired)")?; - Ok(LooseProp::from(row)) + Ok(prop) } /// Get a loose prop by ID. @@ -417,7 +406,7 @@ pub async fn get_loose_prop_by_id<'e>( executor: impl PgExecutor<'e>, loose_prop_id: Uuid, ) -> Result, AppError> { - let row = sqlx::query_as::<_, LoosePropRow>( + let prop = sqlx::query_as::<_, LooseProp>( r#" SELECT lp.id, @@ -445,7 +434,7 @@ pub async fn get_loose_prop_by_id<'e>( .fetch_optional(executor) .await?; - Ok(row.map(LooseProp::from)) + Ok(prop) } /// Move a loose prop to a new position. @@ -455,7 +444,7 @@ pub async fn move_loose_prop<'e>( x: f64, y: f64, ) -> Result { - let row = sqlx::query_as::<_, LoosePropRow>( + let prop = sqlx::query_as::<_, LooseProp>( r#" WITH updated AS ( UPDATE scene.loose_props @@ -503,7 +492,7 @@ pub async fn move_loose_prop<'e>( .await? .or_not_found("Loose prop (may have expired)")?; - Ok(LooseProp::from(row)) + Ok(prop) } /// Lock a loose prop (moderator only). @@ -512,7 +501,7 @@ pub async fn lock_loose_prop<'e>( loose_prop_id: Uuid, locked_by: Uuid, ) -> Result { - let row = sqlx::query_as::<_, LoosePropRow>( + let prop = sqlx::query_as::<_, LooseProp>( r#" WITH updated AS ( UPDATE scene.loose_props @@ -559,7 +548,7 @@ pub async fn lock_loose_prop<'e>( .await? .or_not_found("Loose prop (may have expired)")?; - Ok(LooseProp::from(row)) + Ok(prop) } /// Unlock a loose prop (moderator only). @@ -567,7 +556,7 @@ pub async fn unlock_loose_prop<'e>( executor: impl PgExecutor<'e>, loose_prop_id: Uuid, ) -> Result { - let row = sqlx::query_as::<_, LoosePropRow>( + let prop = sqlx::query_as::<_, LooseProp>( r#" WITH updated AS ( UPDATE scene.loose_props @@ -613,7 +602,7 @@ pub async fn unlock_loose_prop<'e>( .await? .or_not_found("Loose prop (may have expired)")?; - Ok(LooseProp::from(row)) + Ok(prop) } /// Delete expired loose props. @@ -631,27 +620,3 @@ pub async fn cleanup_expired_props<'e>(executor: impl PgExecutor<'e>) -> Result< 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(()) -} diff --git a/crates/chattyness-db/src/ws_messages.rs b/crates/chattyness-db/src/ws_messages.rs index 821209b..5b7bc83 100644 --- a/crates/chattyness-db/src/ws_messages.rs +++ b/crates/chattyness-db/src/ws_messages.rs @@ -140,12 +140,6 @@ pub enum ClientMessage { /// Inventory item ID to delete. 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. @@ -269,14 +263,6 @@ pub enum ServerMessage { 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. PropRefresh { /// The updated prop with all current values. diff --git a/crates/chattyness-user-ui/src/api/websocket.rs b/crates/chattyness-user-ui/src/api/websocket.rs index 270886a..c053192 100644 --- a/crates/chattyness-user-ui/src/api/websocket.rs +++ b/crates/chattyness-user-ui/src/api/websocket.rs @@ -1658,51 +1658,6 @@ 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) => { diff --git a/crates/chattyness-user-ui/src/components/inventory.rs b/crates/chattyness-user-ui/src/components/inventory.rs index 9157aaf..ff18d4f 100644 --- a/crates/chattyness-user-ui/src/components/inventory.rs +++ b/crates/chattyness-user-ui/src/components/inventory.rs @@ -217,33 +217,6 @@ pub fn InventoryPopup( }); } - // Helper to update ownership status in server/realm props when an item is removed - let update_prop_ownership = move |item: &InventoryItem| { - use chattyness_db::models::PropOrigin; - - if let Some(prop_id) = item.prop_id { - match item.origin { - PropOrigin::ServerLibrary => { - set_server_props.update(|props| { - if let Some(prop) = props.iter_mut().find(|p| p.id == prop_id) { - prop.user_owns = false; - } - }); - } - PropOrigin::RealmLibrary => { - set_realm_props.update(|props| { - if let Some(prop) = props.iter_mut().find(|p| p.id == prop_id) { - prop.user_owns = false; - } - }); - } - PropOrigin::UserUpload => { - // User uploads don't appear in server/realm catalogs - } - } - } - }; - // Handle drop action via WebSocket #[cfg(feature = "hydrate")] let handle_drop = { @@ -256,28 +229,11 @@ pub fn InventoryPopup( send_fn(ClientMessage::DropProp { inventory_item_id: item_id, }); - // Find item index and update ownership before removing - let current_items = items.get(); - let removed_index = current_items.iter().position(|i| i.id == item_id); - if let Some(item) = current_items.iter().find(|i| i.id == item_id).cloned() { - update_prop_ownership(&item); - } // Optimistically remove from local list set_items.update(|items| { items.retain(|i| i.id != item_id); }); - // Select the next item (or previous if at end) - let updated_items = items.get(); - if let Some(idx) = removed_index { - if !updated_items.is_empty() { - let next_idx = idx.min(updated_items.len() - 1); - set_selected_item.set(Some(updated_items[next_idx].id)); - } else { - set_selected_item.set(None); - } - } else { - set_selected_item.set(None); - } + set_selected_item.set(None); } else { set_error.set(Some("Not connected to server".to_string())); } @@ -300,28 +256,11 @@ pub fn InventoryPopup( send_fn(ClientMessage::DeleteProp { inventory_item_id: item_id, }); - // Find item index and update ownership before removing - let current_items = items.get(); - let removed_index = current_items.iter().position(|i| i.id == item_id); - if let Some(item) = current_items.iter().find(|i| i.id == item_id).cloned() { - update_prop_ownership(&item); - } // Optimistically remove from local list set_items.update(|items| { items.retain(|i| i.id != item_id); }); - // Select the next item (or previous if at end) - let updated_items = items.get(); - if let Some(idx) = removed_index { - if !updated_items.is_empty() { - let next_idx = idx.min(updated_items.len() - 1); - set_selected_item.set(Some(updated_items[next_idx].id)); - } else { - set_selected_item.set(None); - } - } else { - set_selected_item.set(None); - } + set_selected_item.set(None); set_delete_confirm_item.set(None); } else { set_error.set(Some("Not connected to server".to_string())); @@ -371,7 +310,6 @@ pub fn InventoryPopup( on_delete_request=Callback::new(move |(id, name)| { set_delete_confirm_item.set(Some((id, name))); }) - on_delete_immediate=Callback::new(handle_delete) /> @@ -387,7 +325,7 @@ pub fn InventoryPopup( is_guest=is_guest acquire_endpoint="/api/server/inventory/request" on_acquired=Callback::new(move |_| { - // Trigger inventory refresh + // Trigger inventory refresh and reset loaded state set_my_inventory_loaded.set(false); set_inventory_refresh_trigger.update(|n| *n += 1); }) @@ -407,7 +345,7 @@ pub fn InventoryPopup( acquire_endpoint_is_realm=true realm_slug=realm_slug on_acquired=Callback::new(move |_| { - // Trigger inventory refresh + // Trigger inventory refresh and reset loaded state set_my_inventory_loaded.set(false); set_inventory_refresh_trigger.update(|n| *n += 1); }) @@ -455,95 +393,24 @@ fn MyInventoryTab( #[prop(into)] on_drop: Callback, #[prop(into)] deleting: Signal, #[prop(into)] on_delete_request: Callback<(Uuid, String)>, - /// Callback for immediate delete (Shift+Delete, no confirmation) - #[prop(into)] on_delete_immediate: Callback, ) -> impl IntoView { - // NodeRef to maintain focus on container after item removal - let container_ref = NodeRef::::new(); - - // Refocus container when selected item changes (after drop/delete) - #[cfg(feature = "hydrate")] - Effect::new(move |prev_selected: Option>| { - let current = selected_item.get(); - // If selection changed and we have a new selection, refocus container - if prev_selected.is_some() && current.is_some() { - if let Some(el) = container_ref.get() { - let _ = el.focus(); - } - } - current - }); - - // Keyboard shortcut handler - let handle_keydown = move |ev: leptos::web_sys::KeyboardEvent| { - // Don't handle if in an input field - #[cfg(feature = "hydrate")] - { - use wasm_bindgen::JsCast; - if let Some(target) = ev.target() { - if let Ok(element) = target.dyn_into::() { - let tag = element.tag_name().to_lowercase(); - if tag == "input" || tag == "textarea" { - return; - } - } - } - } - - // Get selected item info - let Some(item_id) = selected_item.get() else { return }; - let Some(item) = items.get().into_iter().find(|i| i.id == item_id) else { return }; - - // Only allow actions on droppable items - if !item.is_droppable { - return; - } - - let key = ev.key(); - let shift = ev.shift_key(); - - match key.as_str() { - "d" | "D" => { - ev.prevent_default(); - on_drop.run(item_id); - } - "Delete" => { - ev.prevent_default(); - if shift { - // Shift+Delete: immediate delete without confirmation - on_delete_immediate.run(item_id); - } else { - // Delete: delete with confirmation - on_delete_request.run((item_id, item.prop_name.clone())); - } - } - _ => {} - } - }; - view! { -
- // Loading state - -
-

"Loading inventory..."

-
-
+ // Loading state + +
+

"Loading inventory..."

+
+
- // Error state - -
-

{move || error.get().unwrap_or_default()}

-
-
+ // Error state + +
+

{move || error.get().unwrap_or_default()}

+
+
- // Empty state - + // Empty state +
"D""rop" }.into_any() - }} + {if is_dropping { "Dropping..." } else { "Drop" }} // Delete button - only shown for droppable props @@ -683,7 +546,6 @@ fn MyInventoryTab( }}
-
} } @@ -752,37 +614,12 @@ fn AcquisitionPropsTab( match response { Ok(resp) if resp.ok() => { - // Find the index of the acquired prop before updating - let current_props = props.get(); - let acquired_index = current_props.iter().position(|p| p.id == prop_id); - - // Update local state to mark prop as owned (shows green border) + // Update local state to mark prop as owned set_props.update(|props| { if let Some(prop) = props.iter_mut().find(|p| p.id == prop_id) { prop.user_owns = true; } }); - - // Select the next available (non-owned) prop - let updated_props = props.get(); - if let Some(idx) = acquired_index { - // Look for next non-owned prop starting from current position - let next_available = updated_props.iter() - .skip(idx + 1) - .find(|p| !p.user_owns && p.is_available && !(p.is_unique && p.is_claimed)) - .or_else(|| { - // If none after, look from the beginning - updated_props.iter() - .take(idx) - .find(|p| !p.user_owns && p.is_available && !(p.is_unique && p.is_claimed)) - }); - - if let Some(next_prop) = next_available { - set_selected_prop.set(Some(next_prop.id)); - } - // If no more available props, keep current selection (now shows as owned) - } - // Notify parent to refresh inventory on_acquired.run(()); } @@ -810,31 +647,7 @@ fn AcquisitionPropsTab( } }); - // Keyboard shortcut handler for Enter to acquire - let handle_keydown = { - let do_acquire = do_acquire.clone(); - move |ev: leptos::web_sys::KeyboardEvent| { - let key = ev.key(); - if key == "Enter" { - // Get selected prop info - let Some(prop_id) = selected_prop.get() else { return }; - let Some(prop) = props.get().into_iter().find(|p| p.id == prop_id) else { return }; - - // Only acquire if not already owned, not claimed (if unique), available, and not a guest - if !is_guest.get() && !prop.user_owns && prop.is_available && !(prop.is_unique && prop.is_claimed) { - ev.prevent_default(); - do_acquire.run(prop_id); - } - } - } - }; - view! { -
// Loading state
@@ -890,20 +703,18 @@ fn AcquisitionPropsTab( format!("/static/{}", prop.asset_path) }; - // Reactive lookup for ownership status (updates when props signal changes) - let user_owns = move || { - props.get().iter().find(|p| p.id == prop_id).map(|p| p.user_owns).unwrap_or(false) - }; + // Visual indicator for ownership status + let user_owns = prop.user_owns; let is_claimed = prop.is_claimed; view! { @@ -986,7 +797,6 @@ fn AcquisitionPropsTab( }}
-
} } diff --git a/crates/chattyness-user-ui/src/components/loose_prop_canvas.rs b/crates/chattyness-user-ui/src/components/loose_prop_canvas.rs index 536b00b..f93c00b 100644 --- a/crates/chattyness-user-ui/src/components/loose_prop_canvas.rs +++ b/crates/chattyness-user-ui/src/components/loose_prop_canvas.rs @@ -65,6 +65,13 @@ pub fn LoosePropCanvas( let canvas_x = screen_x - 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!( "position: absolute; \ left: 0; top: 0; \ @@ -72,8 +79,8 @@ pub fn LoosePropCanvas( z-index: {}; \ pointer-events: auto; \ width: {}px; \ - height: {}px;", - canvas_x, canvas_y, z_index, prop_size, prop_size + height: {}px; {}", + canvas_x, canvas_y, z_index, prop_size, prop_size, border_style ) }; diff --git a/crates/chattyness-user-ui/src/components/scene_viewer.rs b/crates/chattyness-user-ui/src/components/scene_viewer.rs index a69feae..e0a6b33 100644 --- a/crates/chattyness-user-ui/src/components/scene_viewer.rs +++ b/crates/chattyness-user-ui/src/components/scene_viewer.rs @@ -61,7 +61,6 @@ pub fn RealmSceneViewer( #[prop(optional, into)] on_prop_scale_update: Option>, #[prop(optional, into)] on_prop_move: Option>, #[prop(optional, into)] on_prop_lock_toggle: Option>, - #[prop(optional, into)] on_prop_delete: Option>, ) -> impl IntoView { let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false)); let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default)); @@ -132,12 +131,12 @@ pub fn RealmSceneViewer( 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 (prop_context_is_locked, set_prop_context_is_locked) = signal(false); - let (prop_context_name, set_prop_context_name) = signal(Option::::None); - // Click handler for movement (props are now handled via context menu) + // Click handler for movement or prop pickup #[cfg(feature = "hydrate")] let on_overlay_click = { let on_move = on_move.clone(); + let on_prop_click = on_prop_click.clone(); move |ev: web_sys::MouseEvent| { use wasm_bindgen::JsCast; @@ -165,7 +164,9 @@ pub fn RealmSceneViewer( } } - if clicked_prop.is_none() { + if let Some(prop_id) = clicked_prop { + on_prop_click.run(prop_id); + } else { let target = ev.current_target().unwrap(); let element: web_sys::HtmlElement = target.dyn_into().unwrap(); let rect = element.get_bounding_client_rect(); @@ -207,23 +208,14 @@ pub fn RealmSceneViewer( if let Some(prop_id_str) = canvas.get_attribute("data-prop-id") { if hit_test_canvas(&canvas, client_x, client_y) { if let Ok(prop_id) = prop_id_str.parse::() { + ev.prevent_default(); + set_prop_context_menu_position.set((client_x, client_y)); + set_prop_context_menu_target.set(Some(prop_id)); + set_prop_context_menu_open.set(true); + if let Some(prop) = loose_props.get().iter().find(|p| p.id == prop_id) { - let is_mod = is_moderator.map(|s| s.get()).unwrap_or(false); - - // Don't show menu if prop is locked and user is not a moderator - if prop.is_locked && !is_mod { - ev.prevent_default(); - return; - } - - ev.prevent_default(); - set_prop_context_menu_position.set((client_x, client_y)); - set_prop_context_menu_target.set(Some(prop_id)); - set_prop_context_menu_open.set(true); - set_scale_mode_initial_scale.set(prop.scale); 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); let rect = canvas.get_bounding_client_rect(); set_scale_mode_prop_center.set(( @@ -601,15 +593,12 @@ pub fn RealmSceneViewer( { - if let Some(prop_id) = prop_context_menu_target.get() { - on_prop_click.run(prop_id); - } - } "set_scale" => { if let Some(prop_id) = prop_context_menu_target.get() { set_scale_mode_prop_id.set(Some(prop_id)); @@ -656,13 +637,6 @@ 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); @@ -671,7 +645,6 @@ pub fn RealmSceneViewer( on_close=Callback::new(move |_: ()| { set_prop_context_menu_open.set(false); set_prop_context_menu_target.set(None); - set_prop_context_name.set(None); }) /> { - // Treat deleted props the same as picked up (remove from display) - PostAction::PropPickedUp(prop_id) - } ServerMessage::PropRefresh { prop } => { PostAction::PropRefresh(prop) } diff --git a/crates/chattyness-user-ui/src/pages/realm.rs b/crates/chattyness-user-ui/src/pages/realm.rs index 35125d5..5686a4f 100644 --- a/crates/chattyness-user-ui/src/pages/realm.rs +++ b/crates/chattyness-user-ui/src/pages/realm.rs @@ -1244,16 +1244,6 @@ 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! {
impl IntoView { on_prop_scale_update=on_prop_scale_update_cb on_prop_move=on_prop_move_cb on_prop_lock_toggle=on_prop_lock_toggle_cb - on_prop_delete=on_prop_delete_cb />