From 93e33e8d64c538f488585866cc036120575f6c98 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sat, 7 Mar 2026 01:14:41 +0800 Subject: [PATCH] frontend events system refactor --- src-tauri/src/services/app_events.rs | 20 ++++ src-tauri/src/services/ws/friend.rs | 19 ++-- src-tauri/src/services/ws/interaction.rs | 7 +- src/events/app-data.ts | 38 +++---- src/events/cursor.ts | 133 +++++++++-------------- src/events/interaction.ts | 26 +++-- src/events/listener-utils.ts | 101 +++++++++++++++++ src/events/scene-interactive.ts | 32 +++--- src/events/user-status.ts | 117 +++++++++----------- src/routes/app-menu/+page.svelte | 3 +- src/routes/app-menu/tabs/friends.svelte | 9 +- src/routes/scene/+page.svelte | 29 +++-- src/types/bindings/AppEvents.ts | 2 +- src/types/bindings/AppEventsConstants.ts | 10 ++ 14 files changed, 322 insertions(+), 224 deletions(-) create mode 100644 src/events/listener-utils.ts diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 9bad269..147db06 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -15,6 +15,16 @@ pub enum AppEvents { EditDoll, CreateDoll, UserStatusChanged, + FriendCursorPosition, + FriendDisconnected, + FriendActiveDollChanged, + FriendUserStatus, + InteractionReceived, + InteractionDeliveryFailed, + FriendRequestReceived, + FriendRequestAccepted, + FriendRequestDenied, + Unfriended, } impl AppEvents { @@ -27,6 +37,16 @@ impl AppEvents { AppEvents::EditDoll => "edit-doll", AppEvents::CreateDoll => "create-doll", AppEvents::UserStatusChanged => "user-status-changed", + AppEvents::FriendCursorPosition => "friend-cursor-position", + AppEvents::FriendDisconnected => "friend-disconnected", + AppEvents::FriendActiveDollChanged => "friend-active-doll-changed", + AppEvents::FriendUserStatus => "friend-user-status", + AppEvents::InteractionReceived => "interaction-received", + AppEvents::InteractionDeliveryFailed => "interaction-delivery-failed", + AppEvents::FriendRequestReceived => "friend-request-received", + AppEvents::FriendRequestAccepted => "friend-request-accepted", + AppEvents::FriendRequestDenied => "friend-request-denied", + AppEvents::Unfriended => "unfriended", } } } diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index f20f318..ae21be3 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -1,26 +1,27 @@ use rust_socketio::{Payload, RawClient}; use tracing::info; +use crate::services::app_events::AppEvents; use crate::services::cursor::{normalized_to_absolute, CursorPositions}; use crate::state::AppDataRefreshScope; use super::{ emitter, refresh, - types::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload, WS_EVENT}, + types::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload}, utils, }; /// Handler for friend-request-received event pub fn on_friend_request_received(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-request-received") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_RECEIVED, value); + emitter::emit_to_frontend(AppEvents::FriendRequestReceived.as_str(), value); } } /// Handler for friend-request-accepted event pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-request-accepted") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_ACCEPTED, value); + emitter::emit_to_frontend(AppEvents::FriendRequestAccepted.as_str(), value); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -28,14 +29,14 @@ pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { /// Handler for friend-request-denied event pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-request-denied") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_DENIED, value); + emitter::emit_to_frontend(AppEvents::FriendRequestDenied.as_str(), value); } } /// Handler for unfriended event pub fn on_unfriended(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "unfriended") { - emitter::emit_to_frontend(WS_EVENT::UNFRIENDED, value); + emitter::emit_to_frontend(AppEvents::Unfriended.as_str(), value); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -56,14 +57,14 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { }, }; - emitter::emit_to_frontend(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload); + emitter::emit_to_frontend(AppEvents::FriendCursorPosition.as_str(), outgoing_payload); } } /// Handler for friend-disconnected event pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-disconnected") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_DISCONNECTED, value); + emitter::emit_to_frontend(AppEvents::FriendDisconnected.as_str(), value); } } @@ -93,7 +94,7 @@ fn handle_friend_doll_change(event_name: &str, payload: Payload) { /// Handler for friend-active-doll-changed event pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-active-doll-changed") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, value); + emitter::emit_to_frontend(AppEvents::FriendActiveDollChanged.as_str(), value); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -101,6 +102,6 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { /// Handler for friend-user-status event pub fn on_friend_user_status(payload: Payload, _socket: RawClient) { if let Ok(value) = utils::extract_text_value(payload, "friend-user-status") { - emitter::emit_to_frontend(WS_EVENT::FRIEND_USER_STATUS, value); + emitter::emit_to_frontend(AppEvents::FriendUserStatus.as_str(), value); } } diff --git a/src-tauri/src/services/ws/interaction.rs b/src-tauri/src/services/ws/interaction.rs index 620c0c5..f2c8943 100644 --- a/src-tauri/src/services/ws/interaction.rs +++ b/src-tauri/src/services/ws/interaction.rs @@ -1,15 +1,16 @@ use rust_socketio::{Payload, RawClient}; use crate::models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}; +use crate::services::app_events::AppEvents; -use super::{emitter, types::WS_EVENT, utils}; +use super::{emitter, utils}; /// Handler for interaction-received event pub fn on_interaction_received(payload: Payload, _socket: RawClient) { if let Ok(data) = utils::extract_and_parse::(payload, "interaction-received") { - emitter::emit_to_frontend(WS_EVENT::INTERACTION_RECEIVED, data); + emitter::emit_to_frontend(AppEvents::InteractionReceived.as_str(), data); } } @@ -19,6 +20,6 @@ pub fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) { payload, "interaction-delivery-failed", ) { - emitter::emit_to_frontend(WS_EVENT::INTERACTION_DELIVERY_FAILED, data); + emitter::emit_to_frontend(AppEvents::InteractionDeliveryFailed.as_str(), data); } } diff --git a/src/events/app-data.ts b/src/events/app-data.ts index fb5c0d4..3d6010d 100644 --- a/src/events/app-data.ts +++ b/src/events/app-data.ts @@ -1,24 +1,27 @@ import { writable } from "svelte/store"; import { type UserData } from "../types/bindings/UserData"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; -export let appData = writable(null); +export const appData = writable(null); -let unlisten: UnlistenFn | null = null; -let isListening = false; +const subscription = createListenerSubscription(); export async function initAppDataListener() { try { - if (isListening) return; + if (subscription.isListening()) return; appData.set(await invoke("get_app_data")); - unlisten = await listen(AppEvents.AppDataRefreshed, (event) => { - console.log("app-data-refreshed", event.payload); - appData.set(event.payload); - }); - - isListening = true; + const unlisten = await listen( + AppEvents.AppDataRefreshed, + (event) => { + console.log("app-data-refreshed", event.payload); + appData.set(event.payload); + }, + ); + subscription.setUnlisten(unlisten); + subscription.setListening(true); } catch (error) { console.error(error); throw error; @@ -26,16 +29,7 @@ export async function initAppDataListener() { } export function stopAppDataListener() { - if (unlisten) { - unlisten(); - unlisten = null; - isListening = false; - } + subscription.stop(); } -// Handle HMR (Hot Module Replacement) cleanup -if (import.meta.hot) { - import.meta.hot.dispose(() => { - stopAppDataListener(); - }); -} +setupHmrCleanup(stopAppDataListener); diff --git a/src/events/cursor.ts b/src/events/cursor.ts index 937c55b..a1c1ba9 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -1,12 +1,18 @@ import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; import type { CursorPositions } from "../types/bindings/CursorPositions"; import type { CursorPosition } from "../types/bindings/CursorPosition"; import type { DollDto } from "../types/bindings/DollDto"; import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + createMultiListenerSubscription, + parseEventPayload, + removeFromStore, + setupHmrCleanup, +} from "./listener-utils"; -export let cursorPositionOnScreen = writable({ +export const cursorPositionOnScreen = writable({ raw: { x: 0, y: 0 }, mapped: { x: 0, y: 0 }, }); @@ -26,16 +32,12 @@ type FriendCursorData = { // The exported store will only expose the position part to consumers, // but internally we manage the full data. // Actually, it's easier if we just export the positions and manage state internally. -export let friendsCursorPositions = writable>( +export const friendsCursorPositions = writable>( {}, ); -export let friendsActiveDolls = writable>({}); +export const friendsActiveDolls = writable>({}); -let unlistenCursor: UnlistenFn | null = null; -let unlistenFriendCursor: UnlistenFn | null = null; -let unlistenFriendDisconnected: UnlistenFn | null = null; -let unlistenFriendActiveDollChanged: UnlistenFn | null = null; -let isListening = false; +const subscription = createMultiListenerSubscription(); // Internal state to track timestamps let friendCursorState: Record = {}; @@ -46,22 +48,21 @@ let friendCursorState: Record = {}; * but all windows can independently listen to the broadcast events. */ export async function initCursorTracking() { - if (isListening) { - return; - } + if (subscription.isListening()) return; try { // Listen to cursor position events (each window subscribes independently) - unlistenCursor = await listen( + const unlistenCursor = await listen( AppEvents.CursorPosition, (event) => { cursorPositionOnScreen.set(event.payload); }, ); + subscription.addUnlisten(unlistenCursor); // Listen to friend cursor position events - unlistenFriendCursor = await listen( - "friend-cursor-position", + const unlistenFriendCursor = await listen( + AppEvents.FriendCursorPosition, (event) => { // We now receive a clean object from Rust const data = event.payload; @@ -80,58 +81,44 @@ export async function initCursorTracking() { }); }, ); + subscription.addUnlisten(unlistenFriendCursor); // Listen to friend disconnected events - unlistenFriendDisconnected = await listen<[{ userId: string }]>( - "friend-disconnected", - (event) => { - let payload = event.payload; - if (typeof payload === "string") { - try { - payload = JSON.parse(payload); - } catch (e) { - console.error("Failed to parse friend disconnected payload:", e); - return; - } - } + const unlistenFriendDisconnected = await listen< + [{ userId: string }] | { userId: string } | string + >(AppEvents.FriendDisconnected, (event) => { + const payload = parseEventPayload< + [{ userId: string }] | { userId: string } + >(event.payload, "friend-disconnected"); + if (!payload) return; - const data = Array.isArray(payload) ? payload[0] : payload; + const data = Array.isArray(payload) ? payload[0] : payload; - // Remove from internal state - if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; - } + // Remove from internal state + if (friendCursorState[data.userId]) { + delete friendCursorState[data.userId]; + } - // Update svelte store - friendsCursorPositions.update((current) => { - const next = { ...current }; - delete next[data.userId]; - return next; - }); - }, - ); + // Update svelte store + friendsCursorPositions.update((current) => + removeFromStore(current, data.userId), + ); + }); + subscription.addUnlisten(unlistenFriendDisconnected); // Listen to friend active doll changed events - unlistenFriendActiveDollChanged = await listen< + const unlistenFriendActiveDollChanged = await listen< | string | { friendId: string; doll: DollDto | null; } - >("friend-active-doll-changed", (event) => { - let data = event.payload; - - if (typeof data === "string") { - try { - data = JSON.parse(data); - } catch (e) { - console.error( - "Failed to parse friend-active-doll-changed payload:", - e, - ); - return; - } - } + >(AppEvents.FriendActiveDollChanged, (event) => { + const data = parseEventPayload<{ + friendId: string; + doll: DollDto | null; + }>(event.payload, "friend-active-doll-changed"); + if (!data) return; // Cast to expected type after parsing const payload = data as { friendId: string; doll: DollDto | null }; @@ -149,11 +136,9 @@ export async function initCursorTracking() { }); // Also remove from cursor positions so the sprite disappears - friendsCursorPositions.update((current) => { - const next = { ...current }; - delete next[payload.friendId]; - return next; - }); + friendsCursorPositions.update((current) => + removeFromStore(current, payload.friendId), + ); } else { // Update or add the new doll configuration friendsActiveDolls.update((current) => { @@ -164,8 +149,9 @@ export async function initCursorTracking() { }); } }); + subscription.addUnlisten(unlistenFriendActiveDollChanged); - isListening = true; + subscription.setListening(true); } catch (err) { console.error("Failed to initialize cursor tracking:", err); throw err; @@ -177,28 +163,7 @@ export async function initCursorTracking() { * Note: This doesn't stop the Rust-side tracking, just stops this window from receiving events. */ export function stopCursorTracking() { - if (unlistenCursor) { - unlistenCursor(); - unlistenCursor = null; - } - if (unlistenFriendCursor) { - unlistenFriendCursor(); - unlistenFriendCursor = null; - } - if (unlistenFriendDisconnected) { - unlistenFriendDisconnected(); - unlistenFriendDisconnected = null; - } - if (unlistenFriendActiveDollChanged) { - unlistenFriendActiveDollChanged(); - unlistenFriendActiveDollChanged = null; - } - isListening = false; + subscription.stop(); } -// Handle HMR (Hot Module Replacement) cleanup -if (import.meta.hot) { - import.meta.hot.dispose(() => { - stopCursorTracking(); - }); -} +setupHmrCleanup(stopCursorTracking); diff --git a/src/events/interaction.ts b/src/events/interaction.ts index 14cfa4a..860046f 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -2,20 +2,27 @@ 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"; +import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + createMultiListenerSubscription, + setupHmrCleanup, +} from "./listener-utils"; -let unlistenReceived: (() => void) | undefined; -let unlistenFailed: (() => void) | undefined; +const subscription = createMultiListenerSubscription(); export async function initInteractionListeners() { - unlistenReceived = await listen( - "interaction-received", + if (subscription.isListening()) return; + + const unlistenReceived = await listen( + AppEvents.InteractionReceived, (event) => { addInteraction(event.payload); }, ); + subscription.addUnlisten(unlistenReceived); - unlistenFailed = await listen( - "interaction-delivery-failed", + const unlistenFailed = await listen( + AppEvents.InteractionDeliveryFailed, (event) => { console.error("Interaction delivery failed:", event.payload); // You might want to show a toast or alert here @@ -24,9 +31,12 @@ export async function initInteractionListeners() { ); }, ); + subscription.addUnlisten(unlistenFailed); + subscription.setListening(true); } export function stopInteractionListeners() { - if (unlistenReceived) unlistenReceived(); - if (unlistenFailed) unlistenFailed(); + subscription.stop(); } + +setupHmrCleanup(stopInteractionListeners); diff --git a/src/events/listener-utils.ts b/src/events/listener-utils.ts new file mode 100644 index 0000000..69efa41 --- /dev/null +++ b/src/events/listener-utils.ts @@ -0,0 +1,101 @@ +import type { UnlistenFn } from "@tauri-apps/api/event"; + +export type ListenerSubscription = { + stop: () => void; + isListening: () => boolean; + setListening: (value: boolean) => void; + setUnlisten: (unlisten: UnlistenFn | null) => void; +}; + +export type MultiListenerSubscription = { + stop: () => void; + isListening: () => boolean; + setListening: (value: boolean) => void; + addUnlisten: (unlisten: UnlistenFn | null) => void; +}; + +export function createListenerSubscription( + stopFn: () => void = () => {}, +): ListenerSubscription { + let unlisten: UnlistenFn | null = null; + let listening = false; + + return { + stop: () => { + if (unlisten) { + unlisten(); + unlisten = null; + } + listening = false; + stopFn(); + }, + isListening: () => listening, + setListening: (value) => { + listening = value; + }, + setUnlisten: (next) => { + unlisten = next; + }, + }; +} + +export function createMultiListenerSubscription( + stopFn: () => void = () => {}, +): MultiListenerSubscription { + let unlistens: UnlistenFn[] = []; + let listening = false; + + return { + stop: () => { + for (const unlisten of unlistens) { + unlisten(); + } + unlistens = []; + listening = false; + stopFn(); + }, + isListening: () => listening, + setListening: (value) => { + listening = value; + }, + addUnlisten: (unlisten) => { + if (unlisten) { + unlistens.push(unlisten); + } + }, + }; +} + +export function setupHmrCleanup(cleanup: () => void) { + if (import.meta.hot) { + import.meta.hot.dispose(() => { + cleanup(); + }); + } +} + +export function parseEventPayload( + payload: unknown, + errorLabel: string, +): T | null { + if (typeof payload === "string") { + try { + return JSON.parse(payload) as T; + } catch (error) { + console.error(`Failed to parse ${errorLabel} payload`, error); + return null; + } + } + + return payload as T; +} + +export function removeFromStore( + current: Record, + key: string, +): Record { + if (!(key in current)) return current; + const next = { ...current }; + delete next[key]; + return next; +} diff --git a/src/events/scene-interactive.ts b/src/events/scene-interactive.ts index 78aebd6..7fc1850 100644 --- a/src/events/scene-interactive.ts +++ b/src/events/scene-interactive.ts @@ -1,22 +1,26 @@ -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { createListenerSubscription, setupHmrCleanup } from "./listener-utils"; export const sceneInteractive = writable(false); -let unlisten: UnlistenFn | null = null; -let isListening = false; +const subscription = createListenerSubscription(); export async function initSceneInteractiveListener() { - if (isListening) return; + if (subscription.isListening()) return; try { // ensure initial default matches backend default sceneInteractive.set(false); - unlisten = await listen(AppEvents.SceneInteractive, (event) => { - sceneInteractive.set(Boolean(event.payload)); - }); - isListening = true; + const unlisten = await listen( + AppEvents.SceneInteractive, + (event) => { + sceneInteractive.set(Boolean(event.payload)); + }, + ); + subscription.setUnlisten(unlisten); + subscription.setListening(true); } catch (error) { console.error("Failed to initialize scene interactive listener:", error); throw error; @@ -24,15 +28,7 @@ export async function initSceneInteractiveListener() { } export function stopSceneInteractiveListener() { - if (unlisten) { - unlisten(); - unlisten = null; - isListening = false; - } + subscription.stop(); } -if (import.meta.hot) { - import.meta.hot.dispose(() => { - stopSceneInteractiveListener(); - }); -} +setupHmrCleanup(stopSceneInteractiveListener); diff --git a/src/events/user-status.ts b/src/events/user-status.ts index 6f071b7..ca3f049 100644 --- a/src/events/user-status.ts +++ b/src/events/user-status.ts @@ -1,6 +1,13 @@ -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; import type { PresenceStatus } from "../types/bindings/PresenceStatus"; +import { AppEvents } from "../types/bindings/AppEventsConstants"; +import { + createMultiListenerSubscription, + parseEventPayload, + removeFromStore, + setupHmrCleanup, +} from "./listener-utils"; export type UserStatus = { presenceStatus: PresenceStatus; @@ -9,75 +16,65 @@ export type UserStatus = { export const friendsUserStatuses = writable>({}); -let unlistenStatus: UnlistenFn | null = null; -let unlistenFriendDisconnected: UnlistenFn | null = null; -let isListening = false; +const subscription = createMultiListenerSubscription(); export async function initUserStatusListeners() { - if (isListening) return; + if (subscription.isListening()) return; try { - unlistenStatus = await listen("friend-user-status", (event) => { - let payload = event.payload as any; - if (typeof payload === "string") { - try { - payload = JSON.parse(payload); - } catch (error) { - console.error("Failed to parse friend-user-status payload", error); - return; - } - } + const unlistenStatus = await listen( + AppEvents.FriendUserStatus, + (event) => { + const payload = parseEventPayload<{ + userId?: string; + status?: UserStatus; + }>(event.payload, "friend-user-status"); + if (!payload) return; - const userId = payload?.userId as string | undefined; - const status = payload?.status as UserStatus | undefined; + const userId = payload.userId; + const status = payload.status; - if (!userId || !status) return; - if (!status.presenceStatus) return; + if (!userId || !status) return; + if (!status.presenceStatus) return; - // Validate that appMetadata has at least one valid name - const hasValidName = - (typeof status.presenceStatus.title === "string" && - status.presenceStatus.title.trim() !== "") || - (typeof status.presenceStatus.subtitle === "string" && - status.presenceStatus.subtitle.trim() !== ""); - if (!hasValidName) return; + // Validate that appMetadata has at least one valid name + const hasValidName = + (typeof status.presenceStatus.title === "string" && + status.presenceStatus.title.trim() !== "") || + (typeof status.presenceStatus.subtitle === "string" && + status.presenceStatus.subtitle.trim() !== ""); + if (!hasValidName) return; - if (status.state !== "idle" && status.state !== "resting") return; + if (status.state !== "idle" && status.state !== "resting") return; - friendsUserStatuses.update((current) => ({ - ...current, - [userId]: { - presenceStatus: status.presenceStatus, - state: status.state, - }, - })); - }); + friendsUserStatuses.update((current) => ({ + ...current, + [userId]: { + presenceStatus: status.presenceStatus, + state: status.state, + }, + })); + }, + ); + subscription.addUnlisten(unlistenStatus); - unlistenFriendDisconnected = await listen< + const unlistenFriendDisconnected = await listen< [{ userId: string }] | { userId: string } | string - >("friend-disconnected", (event) => { - let payload = event.payload as any; - if (typeof payload === "string") { - try { - payload = JSON.parse(payload); - } catch (error) { - console.error("Failed to parse friend-disconnected payload", error); - return; - } - } + >(AppEvents.FriendDisconnected, (event) => { + const payload = parseEventPayload< + [{ userId: string }] | { userId: string } + >(event.payload, "friend-disconnected"); + if (!payload) return; const data = Array.isArray(payload) ? payload[0] : payload; const userId = data?.userId as string | undefined; if (!userId) return; - friendsUserStatuses.update((current) => { - const next = { ...current }; - delete next[userId]; - return next; - }); + friendsUserStatuses.update((current) => removeFromStore(current, userId)); }); + subscription.addUnlisten(unlistenFriendDisconnected); - isListening = true; + subscription.setListening(true); } catch (error) { console.error("Failed to initialize user status listeners", error); throw error; @@ -85,19 +82,7 @@ export async function initUserStatusListeners() { } export function stopUserStatusListeners() { - if (unlistenStatus) { - unlistenStatus(); - unlistenStatus = null; - } - if (unlistenFriendDisconnected) { - unlistenFriendDisconnected(); - unlistenFriendDisconnected = null; - } - isListening = false; + subscription.stop(); } -if (import.meta.hot) { - import.meta.hot.dispose(() => { - stopUserStatusListeners(); - }); -} +setupHmrCleanup(stopUserStatusListeners); diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte index fe0233f..d5576c5 100644 --- a/src/routes/app-menu/+page.svelte +++ b/src/routes/app-menu/+page.svelte @@ -5,11 +5,12 @@ import YourDolls from "./tabs/your-dolls/index.svelte"; import { listen } from "@tauri-apps/api/event"; import { onMount } from "svelte"; + import { AppEvents } from "../../types/bindings/AppEventsConstants"; let showInteractionOverlay = false; onMount(() => { - const unlisten = listen("set-interaction-overlay", (event) => { + const unlisten = listen(AppEvents.SetInteractionOverlay, (event) => { showInteractionOverlay = event.payload as boolean; }); diff --git a/src/routes/app-menu/tabs/friends.svelte b/src/routes/app-menu/tabs/friends.svelte index 14a691f..6869fd8 100644 --- a/src/routes/app-menu/tabs/friends.svelte +++ b/src/routes/app-menu/tabs/friends.svelte @@ -3,6 +3,7 @@ import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import { appData } from "../../../events/app-data"; + import { AppEvents } from "../../../types/bindings/AppEventsConstants"; import type { FriendRequestResponseDto } from "../../../types/bindings/FriendRequestResponseDto.js"; import type { FriendshipResponseDto } from "../../../types/bindings/FriendshipResponseDto.js"; import type { UserBasicDto } from "../../../types/bindings/UserBasicDto.js"; @@ -50,26 +51,26 @@ refreshSent(); unlisteners.push( - await listen("friend-request-received", () => { + await listen(AppEvents.FriendRequestReceived, () => { refreshReceived(); }), ); unlisteners.push( - await listen("friend-request-accepted", () => { + await listen(AppEvents.FriendRequestAccepted, () => { refreshSent(); invoke("refresh_app_data"); }), ); unlisteners.push( - await listen("friend-request-denied", () => { + await listen(AppEvents.FriendRequestDenied, () => { refreshSent(); }), ); unlisteners.push( - await listen("unfriended", () => { + await listen(AppEvents.Unfriended, () => { invoke("refresh_app_data"); }), ); diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 0f28ca7..ea1e7be 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -13,9 +13,13 @@ import { invoke } from "@tauri-apps/api/core"; import DesktopPet from "./components/DesktopPet.svelte"; import FullscreenModal from "./components/FullscreenModal.svelte"; - import { receivedInteractions, clearInteraction } from "$lib/stores/interaction-store"; + import { + receivedInteractions, + clearInteraction, + } from "$lib/stores/interaction-store"; import { INTERACTION_TYPE_HEADPAT } from "$lib/constants/interaction"; import { listen } from "@tauri-apps/api/event"; + import { AppEvents } from "../../types/bindings/AppEventsConstants"; import { getSpriteSheetUrl } from "$lib/utils/sprite-utils"; import { onMount } from "svelte"; import type { PresenceStatus } from "../../types/bindings/PresenceStatus"; @@ -61,7 +65,9 @@ let userPetpetGif = ""; if (userDoll) { try { - const gifBase64 = await invoke("encode_pet_doll_gif_base64", { doll: userDoll }); + const gifBase64 = await invoke("encode_pet_doll_gif_base64", { + doll: userDoll, + }); userPetpetGif = `data:image/gif;base64,${gifBase64}`; } catch (e) { console.error("Failed to generate user petpet:", e); @@ -102,9 +108,14 @@ if (interaction.type === INTERACTION_TYPE_HEADPAT) { if (showFullscreenModal) { // Queue the headpat for later (deduplicate by replacing existing from same user) - const existingIndex = headpatQueue.findIndex((h) => h.userId === userId); + const existingIndex = headpatQueue.findIndex( + (h) => h.userId === userId, + ); if (existingIndex >= 0) { - headpatQueue[existingIndex] = { userId, content: interaction.content }; + headpatQueue[existingIndex] = { + userId, + content: interaction.content, + }; } else { headpatQueue.push({ userId, content: interaction.content }); } @@ -165,10 +176,12 @@ let presenceStatus: PresenceStatus | null = $state(null); onMount(() => { - const unlisten = listen("user-status-changed", (event) => { - console.log("event received"); - presenceStatus = event.payload.presenceStatus; - }); + const unlisten = listen( + AppEvents.UserStatusChanged, + (event) => { + presenceStatus = event.payload.presenceStatus; + }, + ); return () => { unlisten.then((u) => u()); diff --git a/src/types/bindings/AppEvents.ts b/src/types/bindings/AppEvents.ts index 7a2270b..2757428 100644 --- a/src/types/bindings/AppEvents.ts +++ b/src/types/bindings/AppEvents.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AppEvents = "cursor-position" | "scene-interactive" | "app-data-refreshed" | "set-interaction-overlay" | "edit-doll" | "create-doll" | "user-status-changed"; +export type AppEvents = "cursor-position" | "scene-interactive" | "app-data-refreshed" | "set-interaction-overlay" | "edit-doll" | "create-doll" | "user-status-changed" | "friend-cursor-position" | "friend-disconnected" | "friend-active-doll-changed" | "friend-user-status" | "interaction-received" | "interaction-delivery-failed" | "friend-request-received" | "friend-request-accepted" | "friend-request-denied" | "unfriended"; diff --git a/src/types/bindings/AppEventsConstants.ts b/src/types/bindings/AppEventsConstants.ts index cbfac87..cd046d2 100644 --- a/src/types/bindings/AppEventsConstants.ts +++ b/src/types/bindings/AppEventsConstants.ts @@ -9,6 +9,16 @@ export const AppEvents = { EditDoll: "edit-doll", CreateDoll: "create-doll", UserStatusChanged: "user-status-changed", + FriendCursorPosition: "friend-cursor-position", + FriendDisconnected: "friend-disconnected", + FriendActiveDollChanged: "friend-active-doll-changed", + FriendUserStatus: "friend-user-status", + InteractionReceived: "interaction-received", + InteractionDeliveryFailed: "interaction-delivery-failed", + FriendRequestReceived: "friend-request-received", + FriendRequestAccepted: "friend-request-accepted", + FriendRequestDenied: "friend-request-denied", + Unfriended: "unfriended", } as const; export type AppEvents = typeof AppEvents[keyof typeof AppEvents]; \ No newline at end of file