From 73f9c95e37241abd9bd991a6668c94e41786e94771ba1c3c1cd763dc8da262f2 Mon Sep 17 00:00:00 2001 From: Evan Carroll Date: Fri, 23 Jan 2026 17:42:41 -0600 Subject: [PATCH] feat: add delete form inventory --- crates/chattyness-db/src/ws_messages.rs | 12 +++ .../chattyness-user-ui/src/api/websocket.rs | 33 +++++++- .../src/components/inventory.rs | 76 ++++++++++++++++++- .../src/components/ws_client.rs | 5 ++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/crates/chattyness-db/src/ws_messages.rs b/crates/chattyness-db/src/ws_messages.rs index 00ed3db..5b7bc83 100644 --- a/crates/chattyness-db/src/ws_messages.rs +++ b/crates/chattyness-db/src/ws_messages.rs @@ -134,6 +134,12 @@ pub enum ClientMessage { /// The loose prop ID to unlock. loose_prop_id: Uuid, }, + + /// Permanently delete a prop from inventory (does not drop to scene). + DeleteProp { + /// Inventory item ID to delete. + inventory_item_id: Uuid, + }, } /// Server-to-client WebSocket messages. @@ -251,6 +257,12 @@ pub enum ServerMessage { prop_id: Uuid, }, + /// A prop was permanently deleted from inventory. + PropDeleted { + /// ID of the deleted inventory item. + inventory_item_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 6636fcf..c053192 100644 --- a/crates/chattyness-user-ui/src/api/websocket.rs +++ b/crates/chattyness-user-ui/src/api/websocket.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use chattyness_db::{ models::{ActionType, AvatarRenderData, ChannelMemberWithAvatar, EmotionState, ForcedAvatarReason, User}, - queries::{avatars, channel_members, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users}, + queries::{avatars, channel_members, inventory, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users}, ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig}, }; use chattyness_error::AppError; @@ -711,6 +711,37 @@ async fn handle_socket( } } } + ClientMessage::DeleteProp { inventory_item_id } => { + match inventory::drop_inventory_item( + &mut *recv_conn, + user_id, + inventory_item_id, + ) + .await + { + Ok(()) => { + let _ = direct_tx.send(ServerMessage::PropDeleted { + inventory_item_id, + }).await; + } + Err(e) => { + let (code, message) = match &e { + chattyness_error::AppError::Forbidden(msg) => ( + "PROP_NOT_DELETABLE".to_string(), + msg.clone(), + ), + chattyness_error::AppError::NotFound(msg) => { + ("PROP_NOT_FOUND".to_string(), msg.clone()) + } + _ => ( + "DELETE_FAILED".to_string(), + format!("{:?}", e), + ), + }; + let _ = direct_tx.send(ServerMessage::Error { code, message }).await; + } + } + } ClientMessage::PickUpProp { loose_prop_id } => { // Check if prop is locked let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await { diff --git a/crates/chattyness-user-ui/src/components/inventory.rs b/crates/chattyness-user-ui/src/components/inventory.rs index 67fa342..ff18d4f 100644 --- a/crates/chattyness-user-ui/src/components/inventory.rs +++ b/crates/chattyness-user-ui/src/components/inventory.rs @@ -8,7 +8,7 @@ use chattyness_db::models::{InventoryItem, PropAcquisitionInfo}; #[cfg(feature = "hydrate")] use chattyness_db::ws_messages::ClientMessage; -use super::modals::{GuestLockedOverlay, Modal}; +use super::modals::{ConfirmModal, GuestLockedOverlay, Modal}; use super::tabs::{Tab, TabBar}; use super::ws_client::WsSender; @@ -45,6 +45,8 @@ pub fn InventoryPopup( let (error, set_error) = signal(Option::::None); let (selected_item, set_selected_item) = signal(Option::::None); let (dropping, set_dropping) = signal(false); + let (deleting, set_deleting) = signal(false); + let (delete_confirm_item, set_delete_confirm_item) = signal(Option::<(Uuid, String)>::None); // Server props state (with acquisition info for authenticated users) let (server_props, set_server_props) = signal(Vec::::new()); @@ -244,6 +246,33 @@ pub fn InventoryPopup( #[cfg(not(feature = "hydrate"))] let handle_drop = |_item_id: Uuid| {}; + // Handle delete action via WebSocket (permanent deletion) + #[cfg(feature = "hydrate")] + let handle_delete = { + move |item_id: Uuid| { + set_deleting.set(true); + ws_sender.with_value(|sender| { + if let Some(send_fn) = sender { + send_fn(ClientMessage::DeleteProp { + inventory_item_id: item_id, + }); + // Optimistically remove from local list + set_items.update(|items| { + items.retain(|i| i.id != item_id); + }); + set_selected_item.set(None); + set_delete_confirm_item.set(None); + } else { + set_error.set(Some("Not connected to server".to_string())); + } + }); + set_deleting.set(false); + } + }; + + #[cfg(not(feature = "hydrate"))] + let handle_delete = |_item_id: Uuid| {}; + view! { @@ -324,6 +357,25 @@ pub fn InventoryPopup( + + // Delete confirmation modal + {move || { + delete_confirm_item.get().map(|(item_id, item_name)| { + view! { + + } + }) + }} } @@ -339,6 +391,8 @@ fn MyInventoryTab( set_selected_item: WriteSignal>, #[prop(into)] dropping: Signal, #[prop(into)] on_drop: Callback, + #[prop(into)] deleting: Signal, + #[prop(into)] on_delete_request: Callback<(Uuid, String)>, ) -> impl IntoView { view! { // Loading state @@ -423,8 +477,11 @@ fn MyInventoryTab( let item_id = selected_item.get()?; let item = items.get().into_iter().find(|i| i.id == item_id)?; let on_drop = on_drop.clone(); + let on_delete_request = on_delete_request.clone(); let is_dropping = dropping.get(); + let is_deleting = deleting.get(); let is_droppable = item.is_droppable; + let item_name = item.prop_name.clone(); Some(view! {
@@ -452,10 +509,25 @@ fn MyInventoryTab( } } disabled=is_dropping || !is_droppable - title=if is_droppable { "" } else { "Essential prop cannot be dropped" } + title=if is_droppable { "Drop prop to scene canvas" } else { "Essential prop cannot be dropped" } > {if is_dropping { "Dropping..." } else { "Drop" }} + // Delete button - only shown for droppable props + + + // Transfer button (disabled for now)