interaction system
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::app_data::AppData,
|
models::app_data::AppData,
|
||||||
|
models::interaction::SendInteractionDto,
|
||||||
remotes::{
|
remotes::{
|
||||||
dolls::{CreateDollDto, DollDto, DollsRemote, UpdateDollDto},
|
dolls::{CreateDollDto, DollDto, DollsRemote, UpdateDollDto},
|
||||||
friends::{
|
friends::{
|
||||||
@@ -14,6 +15,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
cursor::start_cursor_tracking,
|
cursor::start_cursor_tracking,
|
||||||
doll_editor::open_doll_editor_window,
|
doll_editor::open_doll_editor_window,
|
||||||
|
interaction::send_interaction,
|
||||||
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
||||||
},
|
},
|
||||||
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
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())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn send_interaction_cmd(dto: SendInteractionDto) -> Result<(), String> {
|
||||||
|
send_interaction(dto).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn start_auth_flow() -> Result<(), String> {
|
fn start_auth_flow() -> Result<(), String> {
|
||||||
// Cancel any in-flight auth listener/state before starting a new one
|
// Cancel any in-flight auth listener/state before starting a new one
|
||||||
@@ -427,7 +434,8 @@ pub fn run() {
|
|||||||
set_scene_interactive,
|
set_scene_interactive,
|
||||||
set_pet_menu_state,
|
set_pet_menu_state,
|
||||||
start_auth_flow,
|
start_auth_flow,
|
||||||
logout_and_restart
|
logout_and_restart,
|
||||||
|
send_interaction_cmd
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
APP_HANDLE
|
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 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 cursor;
|
||||||
pub mod doll_editor;
|
pub mod doll_editor;
|
||||||
pub mod health_manager;
|
pub mod health_manager;
|
||||||
|
pub mod interaction;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod sprite_recolor;
|
pub mod sprite_recolor;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
|
models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||||
services::{
|
services::{
|
||||||
client_config_manager::AppConfig,
|
client_config_manager::AppConfig,
|
||||||
cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
||||||
@@ -49,6 +50,9 @@ impl WS_EVENT {
|
|||||||
pub const DOLL_DELETED: &str = "doll_deleted";
|
pub const DOLL_DELETED: &str = "doll_deleted";
|
||||||
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
||||||
pub const INITIALIZED: &str = "initialized";
|
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) {
|
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) {
|
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||||
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
||||||
// and if clients are actually initialized.
|
// 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_UPDATED, on_doll_updated)
|
||||||
.on(WS_EVENT::DOLL_DELETED, on_doll_deleted)
|
.on(WS_EVENT::DOLL_DELETED, on_doll_deleted)
|
||||||
.on(WS_EVENT::INITIALIZED, on_initialized)
|
.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
|
// rust-socketio fires Event::Connect on initial connect AND reconnects
|
||||||
// so we resend initialization there instead of a dedicated reconnect event.
|
// so we resend initialization there instead of a dedicated reconnect event.
|
||||||
.on(Event::Connect, on_connected)
|
.on(Event::Connect, on_connected)
|
||||||
|
|||||||
33
src/events/interaction.ts
Normal file
33
src/events/interaction.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { addInteraction } from "$lib/stores/interaction-store";
|
||||||
|
import type { InteractionPayloadDto } from "../types/bindings/InteractionPayloadDto";
|
||||||
|
import type { InteractionDeliveryFailedDto } from "../types/bindings/InteractionDeliveryFailedDto";
|
||||||
|
|
||||||
|
let unlistenReceived: (() => void) | undefined;
|
||||||
|
let unlistenFailed: (() => void) | undefined;
|
||||||
|
|
||||||
|
export async function initInteractionListeners() {
|
||||||
|
unlistenReceived = await listen<InteractionPayloadDto>(
|
||||||
|
"interaction-received",
|
||||||
|
(event) => {
|
||||||
|
console.log("Received interaction:", event.payload);
|
||||||
|
addInteraction(event.payload);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
unlistenFailed = await listen<InteractionDeliveryFailedDto>(
|
||||||
|
"interaction-delivery-failed",
|
||||||
|
(event) => {
|
||||||
|
console.error("Interaction delivery failed:", event.payload);
|
||||||
|
// You might want to show a toast or alert here
|
||||||
|
alert(
|
||||||
|
`Failed to send message to user ${event.payload.recipientUserId}: ${event.payload.reason}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopInteractionListeners() {
|
||||||
|
if (unlistenReceived) unlistenReceived();
|
||||||
|
if (unlistenFailed) unlistenFailed();
|
||||||
|
}
|
||||||
23
src/lib/stores/interaction-store.ts
Normal file
23
src/lib/stores/interaction-store.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { InteractionPayloadDto } from "../../types/bindings/InteractionPayloadDto";
|
||||||
|
|
||||||
|
// Map senderUserId -> InteractionPayloadDto
|
||||||
|
export const receivedInteractions = writable<Map<string, InteractionPayloadDto>>(new Map());
|
||||||
|
|
||||||
|
export function addInteraction(interaction: InteractionPayloadDto) {
|
||||||
|
receivedInteractions.update((map) => {
|
||||||
|
// For now, we only store the latest message per user.
|
||||||
|
// In the future, we could store an array if we want a history.
|
||||||
|
const newMap = new Map(map);
|
||||||
|
newMap.set(interaction.senderUserId, interaction);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearInteraction(userId: string) {
|
||||||
|
receivedInteractions.update((map) => {
|
||||||
|
const newMap = new Map(map);
|
||||||
|
newMap.delete(userId);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
|
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
|
||||||
import { initAppDataListener } from "../events/app-data";
|
import { initAppDataListener } from "../events/app-data";
|
||||||
|
import { initInteractionListeners, stopInteractionListeners } from "../events/interaction";
|
||||||
import {
|
import {
|
||||||
initSceneInteractiveListener,
|
initSceneInteractiveListener,
|
||||||
stopSceneInteractiveListener,
|
stopSceneInteractiveListener,
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
await initCursorTracking();
|
await initCursorTracking();
|
||||||
await initAppDataListener();
|
await initAppDataListener();
|
||||||
await initSceneInteractiveListener();
|
await initSceneInteractiveListener();
|
||||||
|
await initInteractionListeners();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to initialize event listeners:", err);
|
console.error("Failed to initialize event listeners:", err);
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
stopCursorTracking();
|
stopCursorTracking();
|
||||||
stopSceneInteractiveListener();
|
stopSceneInteractiveListener();
|
||||||
|
stopInteractionListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
||||||
import PetSprite from "$lib/components/PetSprite.svelte";
|
import PetSprite from "$lib/components/PetSprite.svelte";
|
||||||
import onekoGif from "../../../assets/oneko/oneko.gif";
|
import onekoGif from "../../../assets/oneko/oneko.gif";
|
||||||
|
import {
|
||||||
|
receivedInteractions,
|
||||||
|
clearInteraction,
|
||||||
|
} from "$lib/stores/interaction-store";
|
||||||
import PetMenu from "./PetMenu.svelte";
|
import PetMenu from "./PetMenu.svelte";
|
||||||
import type { DollDto } from "../../../types/bindings/DollDto";
|
import type { DollDto } from "../../../types/bindings/DollDto";
|
||||||
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
||||||
@@ -26,6 +30,40 @@
|
|||||||
let spriteSheetUrl = onekoGif;
|
let spriteSheetUrl = onekoGif;
|
||||||
|
|
||||||
let isPetMenuOpen = false;
|
let isPetMenuOpen = false;
|
||||||
|
let receivedMessage: string | undefined = undefined;
|
||||||
|
let messageTimer: number | undefined = undefined;
|
||||||
|
|
||||||
|
// Watch for received interactions for this user
|
||||||
|
$: {
|
||||||
|
const interaction = $receivedInteractions.get(user.id);
|
||||||
|
if (interaction && interaction.content !== receivedMessage) {
|
||||||
|
console.log(`Received interaction for ${user.id}: ${interaction.content}`);
|
||||||
|
receivedMessage = interaction.content;
|
||||||
|
isPetMenuOpen = true;
|
||||||
|
|
||||||
|
// Make scene interactive so user can see it
|
||||||
|
invoke("set_scene_interactive", {
|
||||||
|
interactive: true,
|
||||||
|
shouldClick: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear existing timer if any
|
||||||
|
if (messageTimer) clearTimeout(messageTimer);
|
||||||
|
|
||||||
|
// Auto-close and clear after 8 seconds
|
||||||
|
messageTimer = setTimeout(() => {
|
||||||
|
isPetMenuOpen = false;
|
||||||
|
receivedMessage = undefined;
|
||||||
|
clearInteraction(user.id);
|
||||||
|
// We probably shouldn't disable interactivity globally here as other pets might be active,
|
||||||
|
// but 'set_pet_menu_state' in backend handles the window transparency logic per pet/menu.
|
||||||
|
// However, we did explicitly call set_scene_interactive(true).
|
||||||
|
// It might be safer to let the mouse-leave or other logic handle setting it back to false,
|
||||||
|
// or just leave it as is since the user might want to interact.
|
||||||
|
// For now, focusing on the message lifecycle.
|
||||||
|
}, 8000) as unknown as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Watch for color changes to regenerate sprite
|
// Watch for color changes to regenerate sprite
|
||||||
$: updateSprite(
|
$: updateSprite(
|
||||||
@@ -33,10 +71,22 @@
|
|||||||
doll?.configuration.colorScheme.outline,
|
doll?.configuration.colorScheme.outline,
|
||||||
);
|
);
|
||||||
|
|
||||||
$: (isInteractive, (isPetMenuOpen = false));
|
// This reactive statement forces the menu closed whenever `isInteractive` changes.
|
||||||
|
// This conflicts with our message logic because we explicitly set interactive=true when opening the menu for a message.
|
||||||
|
// We should remove this or condition it.
|
||||||
|
// The original intent was likely to close the menu if the user moves the mouse away (interactive becomes false),
|
||||||
|
// but `isInteractive` is driven by mouse hover usually.
|
||||||
|
// When we force it via invoke("set_scene_interactive", { interactive: true }), it might not reflect back into `isInteractive` prop immediately or correctly depending on how the parent passes it.
|
||||||
|
// Actually, `isInteractive` is a prop passed from +page.svelte probably based on hover state.
|
||||||
|
// If we want the menu to stay open during the message, we should probably ignore this auto-close behavior if a message is present.
|
||||||
|
|
||||||
|
$: if (!receivedMessage && !isInteractive) {
|
||||||
|
isPetMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
console.log(`Setting pet menu state for ${id}: ${isPetMenuOpen}`);
|
||||||
invoke("set_pet_menu_state", { id, open: isPetMenuOpen });
|
invoke("set_pet_menu_state", { id, open: isPetMenuOpen });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,13 +149,19 @@
|
|||||||
aria-label="Pet Menu"
|
aria-label="Pet Menu"
|
||||||
>
|
>
|
||||||
{#if doll}
|
{#if doll}
|
||||||
<PetMenu {doll} {user} />
|
<PetMenu {doll} {user} {receivedMessage} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
isPetMenuOpen = !isPetMenuOpen;
|
isPetMenuOpen = !isPetMenuOpen;
|
||||||
|
if (!isPetMenuOpen) {
|
||||||
|
// Clear message when closing menu manually
|
||||||
|
receivedMessage = undefined;
|
||||||
|
clearInteraction(user.id);
|
||||||
|
if (messageTimer) clearTimeout(messageTimer);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PetSprite
|
<PetSprite
|
||||||
|
|||||||
@@ -1,23 +1,86 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { type DollDto } from "../../../types/bindings/DollDto";
|
import { type DollDto } from "../../../types/bindings/DollDto";
|
||||||
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
||||||
|
import type { SendInteractionDto } from "../../../types/bindings/SendInteractionDto";
|
||||||
|
|
||||||
export let doll: DollDto;
|
export let doll: DollDto;
|
||||||
export let user: UserBasicDto;
|
export let user: UserBasicDto;
|
||||||
|
export let receivedMessage: string | undefined = undefined;
|
||||||
|
|
||||||
|
let showMessageInput = false;
|
||||||
|
let messageContent = "";
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
if (!messageContent.trim()) return;
|
||||||
|
|
||||||
|
const dto: SendInteractionDto = {
|
||||||
|
recipientUserId: user.id,
|
||||||
|
content: messageContent,
|
||||||
|
type: "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("send_interaction_cmd", { dto });
|
||||||
|
messageContent = "";
|
||||||
|
showMessageInput = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to send interaction:", e);
|
||||||
|
alert("Failed to send message: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
sendMessage();
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
showMessageInput = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-base-100 border border-base-200 card p-1">
|
<div class="bg-base-100 border border-base-200 card p-1 min-w-[150px]">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-row w-full items-end gap-1">
|
<div class="flex flex-row w-full items-end gap-1">
|
||||||
<p class="text-sm font-semibold">{doll.name}</p>
|
<p class="text-sm font-semibold">{doll.name}</p>
|
||||||
<p class="text-[0.6rem] opacity-50">From {user.name}</p>
|
<p class="text-[0.6rem] opacity-50">From {user.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if receivedMessage}
|
||||||
|
<div class="">
|
||||||
|
<div class="text-sm max-w-[140px]">
|
||||||
|
{receivedMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if showMessageInput}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={messageContent}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
placeholder="Type message..."
|
||||||
|
class="input input-xs input-bordered w-full"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button class="btn btn-xs btn-primary flex-1" onclick={sendMessage}
|
||||||
|
>Send</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-xs flex-1"
|
||||||
|
onclick={() => (showMessageInput = false)}>Cancel</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||||
<button>Headpat</button>
|
<button disabled>Headpat</button>
|
||||||
<button>Message</button>
|
<button onclick={() => (showMessageInput = true)}>Message</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||||
<button>Postcard</button>
|
<button disabled>Postcard</button>
|
||||||
<button>Wormhole</button>
|
<button disabled>Wormhole</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
src/types/bindings/HealthResponseDto.ts
Normal file
3
src/types/bindings/HealthResponseDto.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type HealthResponseDto = { status: string, version: string, uptimeSecs: bigint, db: string, };
|
||||||
3
src/types/bindings/InteractionDeliveryFailedDto.ts
Normal file
3
src/types/bindings/InteractionDeliveryFailedDto.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type InteractionDeliveryFailedDto = { recipientUserId: string, reason: string, };
|
||||||
3
src/types/bindings/InteractionPayloadDto.ts
Normal file
3
src/types/bindings/InteractionPayloadDto.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type InteractionPayloadDto = { senderUserId: string, senderName: string, content: string, type: string, timestamp: string, };
|
||||||
3
src/types/bindings/SendInteractionDto.ts
Normal file
3
src/types/bindings/SendInteractionDto.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type SendInteractionDto = { recipientUserId: string, content: string, type: string, };
|
||||||
Reference in New Issue
Block a user