diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index 92c4303..cb33c73 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -2,7 +2,7 @@ use crate::{ lock_r, models::app_data::UserData, services::{ - friend_active_doll_sprite, presence_modules::models::ModuleMetadata, sprite, + friends, presence_modules::models::ModuleMetadata, sprite, }, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; @@ -38,7 +38,7 @@ pub fn get_active_doll_sprite_base64() -> Result, String> { #[tauri::command] #[specta::specta] pub fn get_friend_active_doll_sprites_base64( -) -> Result { - friend_active_doll_sprite::sync_from_app_data(); - Ok(friend_active_doll_sprite::get_snapshot()) +) -> Result { + friends::sync_active_doll_sprites_from_app_data(); + Ok(friends::get_active_doll_sprites_snapshot()) } diff --git a/src-tauri/src/init/lifecycle.rs b/src-tauri/src/init/lifecycle.rs index 982038c..1e11724 100644 --- a/src-tauri/src/init/lifecycle.rs +++ b/src-tauri/src/init/lifecycle.rs @@ -53,7 +53,7 @@ async fn disconnect_user_profile() { /// Destructs the user session and show health manager window /// with error message, offering troubleshooting options. -pub async fn handle_disasterous_failure(error_message: Option) { +pub async fn handle_disastrous_failure(error_message: Option) { destruct_user_session().await; open_health_manager_window(error_message); } diff --git a/src-tauri/src/init/mod.rs b/src-tauri/src/init/mod.rs index 329d2a8..8194055 100644 --- a/src-tauri/src/init/mod.rs +++ b/src-tauri/src/init/mod.rs @@ -1,6 +1,6 @@ use crate::{ init::{ - lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health}, + lifecycle::{construct_user_session, handle_disastrous_failure, validate_server_health}, tracing::init_logging, }, services::{ @@ -28,7 +28,7 @@ pub async fn launch_app() { init_modules(); if let Err(err) = validate_server_health().await { - handle_disasterous_failure(Some(err.to_string())).await; + handle_disastrous_failure(Some(err.to_string())).await; return; } diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 5883278..36910ff 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -13,8 +13,8 @@ use crate::{ interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, }, services::{ - cursor::CursorPositions, friend_active_doll_sprite::FriendActiveDollSpritesDto, - friend_cursor::FriendCursorPositionsDto, + cursor::CursorPositions, + friends::{FriendActiveDollSpritesDto, FriendCursorPositionsDto}, }, }; diff --git a/src-tauri/src/services/friend_active_doll_sprite.rs b/src-tauri/src/services/friends/active_doll_sprites.rs similarity index 100% rename from src-tauri/src/services/friend_active_doll_sprite.rs rename to src-tauri/src/services/friends/active_doll_sprites.rs diff --git a/src-tauri/src/services/friend_cursor.rs b/src-tauri/src/services/friends/cursor_positions.rs similarity index 96% rename from src-tauri/src/services/friend_cursor.rs rename to src-tauri/src/services/friends/cursor_positions.rs index e7d5305..a780b6f 100644 --- a/src-tauri/src/services/friend_cursor.rs +++ b/src-tauri/src/services/friends/cursor_positions.rs @@ -103,10 +103,8 @@ pub fn set_active_doll(user_id: &str, has_active_doll: bool) { .active_dolls .insert(user_id.to_string(), has_active_doll); - if !has_active_doll { - if projection.positions.remove(user_id).is_some() { - emit_snapshot(&projection.positions); - } + if !has_active_doll && projection.positions.remove(user_id).is_some() { + emit_snapshot(&projection.positions); } } diff --git a/src-tauri/src/services/friends/mod.rs b/src-tauri/src/services/friends/mod.rs new file mode 100644 index 0000000..6903521 --- /dev/null +++ b/src-tauri/src/services/friends/mod.rs @@ -0,0 +1,39 @@ +mod active_doll_sprites; +mod cursor_positions; + +use crate::{models::dolls::DollDto, services::cursor::CursorPositions}; + +pub use active_doll_sprites::FriendActiveDollSpritesDto; +pub use cursor_positions::FriendCursorPositionsDto; + +pub fn sync_from_app_data() { + active_doll_sprites::sync_from_app_data(); + cursor_positions::sync_from_app_data(); +} + +pub fn clear() { + active_doll_sprites::clear(); + cursor_positions::clear(); +} + +pub fn remove_friend(user_id: &str) { + active_doll_sprites::remove_friend(user_id); + cursor_positions::remove_friend(user_id); +} + +pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) { + active_doll_sprites::set_active_doll(user_id, doll); + cursor_positions::set_active_doll(user_id, doll.is_some()); +} + +pub fn update_cursor_position(user_id: String, position: CursorPositions) { + cursor_positions::update_position(user_id, position); +} + +pub fn sync_active_doll_sprites_from_app_data() { + active_doll_sprites::sync_from_app_data(); +} + +pub fn get_active_doll_sprites_snapshot() -> FriendActiveDollSpritesDto { + active_doll_sprites::get_snapshot() +} diff --git a/src-tauri/src/services/health_monitor.rs b/src-tauri/src/services/health_monitor.rs index d0b94d3..321224d 100644 --- a/src-tauri/src/services/health_monitor.rs +++ b/src-tauri/src/services/health_monitor.rs @@ -1,5 +1,5 @@ use crate::{ - init::lifecycle::{handle_disasterous_failure, validate_server_health}, + init::lifecycle::{handle_disastrous_failure, validate_server_health}, lock_w, services::ws::client::establish_websocket_connection, state::FDOLL, @@ -44,7 +44,7 @@ pub async fn start_health_monitor() { if consecutive_failures >= MAX_FAILURES { info!("Server appears unreachable after {} attempts, triggering recovery", MAX_FAILURES); - handle_disasterous_failure(Some(format!( + handle_disastrous_failure(Some(format!( "Lost connection to server: {}", e ))) diff --git a/src-tauri/src/services/interaction.rs b/src-tauri/src/services/interaction.rs index 76b5858..accff71 100644 --- a/src-tauri/src/services/interaction.rs +++ b/src-tauri/src/services/interaction.rs @@ -1,59 +1,12 @@ -use serde_json::json; -use tracing::error; +use tracing::warn; -use crate::{ - lock_r, models::interaction::SendInteractionDto, services::ws::WS_EVENT, state::FDOLL, -}; +use crate::{models::interaction::SendInteractionDto, services::ws::{ws_emit_soft, WS_EVENT}}; pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> { - // Check if WS is initialized - let client = { - let guard = lock_r!(FDOLL); - if let Some(clients) = &guard.network.clients { - if clients.is_ws_initialized { - clients.ws_client.clone() - } else { - return Err("WebSocket not initialized".to_string()); - } - } else { - return Err("App not fully initialized".to_string()); - } - }; - - if let Some(socket) = client { - // Prepare payload for client-send-interaction event - // The DTO structure matches what the server expects: - // { recipientUserId, content, type } (handled by serde rename_all="camelCase") - - let payload = json!({ - "recipientUserId": dto.recipient_user_id, - "content": dto.content, - "type": dto.type_ - }); - - // Blocking emission because rust_socketio::Client::emit is synchronous/blocking - // but we are in an async context. Ideally we spawn_blocking. - let spawn_result = tauri::async_runtime::spawn_blocking(move || { - socket.emit(WS_EVENT::CLIENT_SEND_INTERACTION, payload) + ws_emit_soft(WS_EVENT::CLIENT_SEND_INTERACTION, dto) + .await + .map_err(|err| { + warn!("Failed to send interaction: {}", err); + err }) - .await; - - match spawn_result { - Ok(emit_result) => match emit_result { - Ok(_) => Ok(()), - Err(e) => { - let err_msg = format!("Failed to emit interaction event: {}", e); - error!("{}", err_msg); - Err(err_msg) - } - }, - Err(e) => { - let err_msg = format!("Failed to spawn blocking task for interaction emit: {}", e); - error!("{}", err_msg); - Err(err_msg) - } - } - } else { - Err("WebSocket client not available".to_string()) - } } diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 37a1054..85d9fa7 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,6 +1,7 @@ use tauri::Manager; use crate::get_app_handle; +use tracing::warn; pub mod app_events; pub mod app_menu; @@ -8,8 +9,7 @@ pub mod auth; pub mod client_config_manager; pub mod cursor; pub mod doll_editor; -pub mod friend_active_doll_sprite; -pub mod friend_cursor; +pub mod friends; pub mod health_manager; pub mod health_monitor; pub mod interaction; @@ -25,6 +25,8 @@ pub fn close_all_windows() { let app_handle = get_app_handle(); let webview_windows = app_handle.webview_windows(); for window in webview_windows { - window.1.close().unwrap(); + if let Err(err) = window.1.close() { + warn!("Failed to close window '{}': {}", window.0, err); + } } } diff --git a/src-tauri/src/services/presence_modules/runtime.rs b/src-tauri/src/services/presence_modules/runtime.rs index 93396ce..05041ec 100644 --- a/src-tauri/src/services/presence_modules/runtime.rs +++ b/src-tauri/src/services/presence_modules/runtime.rs @@ -5,7 +5,6 @@ use tracing::{error, info, warn}; use crate::models::event_payloads::{UserStatusPayload, UserStatusState}; use crate::services::ws::user_status::report_user_status; -use crate::services::ws::{ws_emit_soft, WS_EVENT}; use super::models::PresenceStatus; @@ -48,7 +47,9 @@ async fn update_status(status: PresenceStatus) { presence_status: status, state: UserStatusState::Idle, }; - report_user_status(user_status).await; + if let Err(e) = report_user_status(user_status).await { + warn!("User status report failed: {}", e); + } } async fn update_status_async(status: PresenceStatus) { @@ -56,7 +57,7 @@ async fn update_status_async(status: PresenceStatus) { presence_status: status, state: UserStatusState::Idle, }; - if let Err(e) = ws_emit_soft(WS_EVENT::CLIENT_REPORT_USER_STATUS, payload).await { + if let Err(e) = report_user_status(payload).await { warn!("User status report failed: {}", e); } } diff --git a/src-tauri/src/services/scene.rs b/src-tauri/src/services/scene.rs index 8047a08..a4adbcc 100644 --- a/src-tauri/src/services/scene.rs +++ b/src-tauri/src/services/scene.rs @@ -35,6 +35,7 @@ fn scene_interactive_state() -> Arc { pub fn update_scene_interactive(interactive: bool, should_click: bool) { let app_handle = get_app_handle(); + scene_interactive_state().store(interactive, Ordering::SeqCst); // If we are forcing interactive to false (e.g. background click), clear any open menus // This prevents the loop from immediately re-enabling it if the frontend hasn't updated yet diff --git a/src-tauri/src/services/ws/client.rs b/src-tauri/src/services/ws/client.rs index f1ea516..fa18e3e 100644 --- a/src-tauri/src/services/ws/client.rs +++ b/src-tauri/src/services/ws/client.rs @@ -23,7 +23,7 @@ pub async fn establish_websocket_connection() { return; // Success } else { // Connection failed, trigger disaster recovery - crate::init::lifecycle::handle_disasterous_failure( + crate::init::lifecycle::handle_disastrous_failure( Some("WebSocket connection failed. Please check your network and try again.".to_string()) ).await; return; @@ -32,9 +32,9 @@ pub async fn establish_websocket_connection() { sleep(BACKOFF).await; } - + // If we exhausted retries without valid token - crate::init::lifecycle::handle_disasterous_failure( + crate::init::lifecycle::handle_disastrous_failure( Some("Failed to authenticate. Please restart and sign in again.".to_string()) ).await; } diff --git a/src-tauri/src/services/ws/emitter.rs b/src-tauri/src/services/ws/emitter.rs index d99eb1e..10423e1 100644 --- a/src-tauri/src/services/ws/emitter.rs +++ b/src-tauri/src/services/ws/emitter.rs @@ -5,7 +5,7 @@ use tauri_specta::Event; use tracing::{error, warn}; use crate::{ - get_app_handle, init::lifecycle::handle_disasterous_failure, lock_r, lock_w, state::FDOLL, + get_app_handle, init::lifecycle::handle_disastrous_failure, lock_r, lock_w, state::FDOLL, }; /// Acquire WebSocket client and initialization state from app state @@ -87,7 +87,7 @@ pub async fn ws_emit( Ok(_) => Ok(()), Err(err_msg) => { error!("[critical] {}", err_msg); - handle_disasterous_failure(Some(err_msg.clone())).await; + handle_disastrous_failure(Some(err_msg.clone())).await; Err(err_msg) } } diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index 8209bf5..7a5b371 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -12,7 +12,7 @@ use crate::services::app_events::{ }; use crate::services::{ cursor::{normalized_to_absolute, CursorPositions}, - friend_active_doll_sprite, friend_cursor, + friends, }; use crate::state::AppDataRefreshScope; @@ -62,7 +62,7 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { let mapped_pos = &friend_data.position; let raw_pos = normalized_to_absolute(mapped_pos); - friend_cursor::update_position( + friends::update_cursor_position( friend_data.user_id, CursorPositions { raw: raw_pos, @@ -77,8 +77,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "friend-disconnected") { - friend_active_doll_sprite::remove_friend(&data.user_id); - friend_cursor::remove_friend(&data.user_id); + friends::remove_friend(&data.user_id); emitter::emit_to_frontend_typed(&FriendDisconnected(data)); } } @@ -112,8 +111,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { payload, "friend-active-doll-changed", ) { - friend_active_doll_sprite::set_active_doll(&data.friend_id, data.doll.as_ref()); - friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some()); + friends::set_active_doll(&data.friend_id, data.doll.as_ref()); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); } } diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs index dc25ceb..2a44eaa 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -16,7 +16,7 @@ static USER_STATUS_REPORT_DEBOUNCE: Lazy>>> = Lazy::new(|| Mutex::new(None)); /// Report user status to WebSocket server with debouncing -pub async fn report_user_status(status: UserStatusPayload) { +pub async fn report_user_status(status: UserStatusPayload) -> Result<(), String> { let mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await; // Cancel previous pending report @@ -25,7 +25,7 @@ pub async fn report_user_status(status: UserStatusPayload) { } if !status.has_presence_content() { - return; + return Ok(()); } if let Err(e) = UserStatusChanged(status.clone()).emit(crate::get_app_handle()) { @@ -43,4 +43,5 @@ pub async fn report_user_status(status: UserStatusPayload) { }); *debouncer = Some(handle); + Ok(()) } diff --git a/src-tauri/src/state/ui.rs b/src-tauri/src/state/ui.rs index 8fc723f..a19b150 100644 --- a/src-tauri/src/state/ui.rs +++ b/src-tauri/src/state/ui.rs @@ -3,7 +3,7 @@ use crate::{ remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, services::{ app_events::{ActiveDollSpriteChanged, AppDataRefreshed}, - friend_active_doll_sprite, friend_cursor, sprite, + friends, sprite, }, state::FDOLL, }; @@ -165,8 +165,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { let mut guard = lock_w!(crate::state::FDOLL); guard.user_data.friends = Some(friends); drop(guard); - friend_active_doll_sprite::sync_from_app_data(); - friend_cursor::sync_from_app_data(); + friends::sync_from_app_data(); } Err(e) => { warn!("Failed to fetch friends list: {}", e); @@ -287,6 +286,5 @@ pub fn clear_app_data() { guard.user_data.user = None; guard.user_data.friends = None; drop(guard); - friend_active_doll_sprite::clear(); - friend_cursor::clear(); + friends::clear(); }