interaction system
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
models::app_data::AppData,
|
||||
models::interaction::SendInteractionDto,
|
||||
remotes::{
|
||||
dolls::{CreateDollDto, DollDto, DollsRemote, UpdateDollDto},
|
||||
friends::{
|
||||
@@ -14,6 +15,7 @@ use crate::{
|
||||
},
|
||||
cursor::start_cursor_tracking,
|
||||
doll_editor::open_doll_editor_window,
|
||||
interaction::send_interaction,
|
||||
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
||||
},
|
||||
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||
@@ -375,6 +377,11 @@ async fn logout_and_restart() -> Result<(), String> {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_interaction_cmd(dto: SendInteractionDto) -> Result<(), String> {
|
||||
send_interaction(dto).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_auth_flow() -> Result<(), String> {
|
||||
// Cancel any in-flight auth listener/state before starting a new one
|
||||
@@ -427,7 +434,8 @@ pub fn run() {
|
||||
set_scene_interactive,
|
||||
set_pet_menu_state,
|
||||
start_auth_flow,
|
||||
logout_and_restart
|
||||
logout_and_restart,
|
||||
send_interaction_cmd
|
||||
])
|
||||
.setup(|app| {
|
||||
APP_HANDLE
|
||||
|
||||
32
src-tauri/src/models/interaction.rs
Normal file
32
src-tauri/src/models/interaction.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendInteractionDto {
|
||||
pub recipient_user_id: String,
|
||||
pub content: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InteractionPayloadDto {
|
||||
pub sender_user_id: String,
|
||||
pub sender_name: String,
|
||||
pub content: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InteractionDeliveryFailedDto {
|
||||
pub recipient_user_id: String,
|
||||
pub reason: String,
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
pub mod app_data;
|
||||
pub mod interaction;
|
||||
|
||||
pub use interaction::*;
|
||||
|
||||
72
src-tauri/src/services/interaction.rs
Normal file
72
src-tauri/src/services/interaction.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use serde_json::json;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
lock_r,
|
||||
models::interaction::SendInteractionDto,
|
||||
services::ws::WS_EVENT,
|
||||
state::FDOLL,
|
||||
};
|
||||
|
||||
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.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")
|
||||
|
||||
// Note: The `type` field in DTO is mapped to `type_` in Rust struct but serialized as `type`
|
||||
// due to camelCase renaming (if we rely on TS-RS output) or manual renaming.
|
||||
// Wait, `type` is a reserved keyword in Rust so we used `type_`.
|
||||
// We need to ensure serialization maps `type_` to `type`.
|
||||
// However, serde's `rename_all = "camelCase"` handles `recipient_user_id` -> `recipientUserId`.
|
||||
// It does NOT automatically handle `type_` -> `type`.
|
||||
// We should add `#[serde(rename = "type")]` to the `type_` field in the model.
|
||||
// I will fix the model first to ensure correct serialization.
|
||||
|
||||
let payload = json!({
|
||||
"recipientUserId": dto.recipient_user_id,
|
||||
"content": dto.content,
|
||||
"type": dto.type_
|
||||
});
|
||||
|
||||
info!("Sending interaction via WS: {:?}", payload);
|
||||
|
||||
// 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)
|
||||
}).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())
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod client_config_manager;
|
||||
pub mod cursor;
|
||||
pub mod doll_editor;
|
||||
pub mod health_manager;
|
||||
pub mod interaction;
|
||||
pub mod scene;
|
||||
pub mod sprite_recolor;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -5,6 +5,7 @@ use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
get_app_handle, lock_r, lock_w,
|
||||
models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||
services::{
|
||||
client_config_manager::AppConfig,
|
||||
cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
||||
@@ -49,6 +50,9 @@ impl WS_EVENT {
|
||||
pub const DOLL_DELETED: &str = "doll_deleted";
|
||||
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
||||
pub const INITIALIZED: &str = "initialized";
|
||||
pub const INTERACTION_RECEIVED: &str = "interaction-received";
|
||||
pub const INTERACTION_DELIVERY_FAILED: &str = "interaction-delivery-failed";
|
||||
pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction";
|
||||
}
|
||||
|
||||
fn emit_initialize(socket: &RawClient) {
|
||||
@@ -364,6 +368,60 @@ fn on_doll_deleted(payload: Payload, _socket: RawClient) {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_interaction_received(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!("Received interaction-received event: {:?}", first_value);
|
||||
|
||||
let interaction_data: Result<InteractionPayloadDto, _> =
|
||||
serde_json::from_value(first_value.clone());
|
||||
|
||||
match interaction_data {
|
||||
Ok(data) => {
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::INTERACTION_RECEIVED, data) {
|
||||
error!("Failed to emit interaction-received event: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse interaction payload: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Received interaction-received event with empty payload");
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for interaction-received"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!("Received interaction-delivery-failed event: {:?}", first_value);
|
||||
|
||||
let failure_data: Result<InteractionDeliveryFailedDto, _> =
|
||||
serde_json::from_value(first_value.clone());
|
||||
|
||||
match failure_data {
|
||||
Ok(data) => {
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::INTERACTION_DELIVERY_FAILED, data) {
|
||||
error!("Failed to emit interaction-delivery-failed event: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse interaction failure payload: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Received interaction-delivery-failed event with empty payload");
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for interaction-delivery-failed"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
||||
// and if clients are actually initialized.
|
||||
@@ -471,6 +529,11 @@ pub async fn build_ws_client(
|
||||
.on(WS_EVENT::DOLL_UPDATED, on_doll_updated)
|
||||
.on(WS_EVENT::DOLL_DELETED, on_doll_deleted)
|
||||
.on(WS_EVENT::INITIALIZED, on_initialized)
|
||||
.on(WS_EVENT::INTERACTION_RECEIVED, on_interaction_received)
|
||||
.on(
|
||||
WS_EVENT::INTERACTION_DELIVERY_FAILED,
|
||||
on_interaction_delivery_failed,
|
||||
)
|
||||
// rust-socketio fires Event::Connect on initial connect AND reconnects
|
||||
// so we resend initialization there instead of a dedicated reconnect event.
|
||||
.on(Event::Connect, on_connected)
|
||||
|
||||
Reference in New Issue
Block a user