From df934afefa90c9e68438588307ab16d5cb133920 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Tue, 16 Dec 2025 01:56:46 +0800 Subject: [PATCH] =?UTF-8?q?friends=20cursor=20broadcast=20system=20refined?= =?UTF-8?q?=20=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/services/ws.rs | 24 ++++++++++++---- src/events/cursor.ts | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index 1fb051c..df261bf 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -18,6 +18,7 @@ impl WS_EVENT { pub const FRIEND_REQUEST_DENIED: &str = "friend-request-denied"; pub const UNFRIENDED: &str = "unfriended"; pub const FRIEND_CURSOR_POSITION: &str = "friend-cursor-position"; + pub const FRIEND_DISCONNECTED: &str = "friend-disconnected"; } fn on_friend_request_received(payload: Payload, _socket: RawClient) { @@ -28,7 +29,7 @@ fn on_friend_request_received(payload: Payload, _socket: RawClient) { .emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str) .unwrap(); } - _ => todo!(), + _ => error!("Received unexpected payload format for friend request received"), } } @@ -40,7 +41,7 @@ fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { .emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) .unwrap(); } - _ => todo!(), + _ => error!("Received unexpected payload format for friend request accepted"), } } @@ -52,7 +53,7 @@ fn on_friend_request_denied(payload: Payload, _socket: RawClient) { .emit(WS_EVENT::FRIEND_REQUEST_DENIED, str) .unwrap(); } - _ => todo!(), + _ => error!("Received unexpected payload format for friend request denied"), } } @@ -62,7 +63,7 @@ fn on_unfriended(payload: Payload, _socket: RawClient) { println!("Received unfriended: {:?}", str); get_app_handle().emit(WS_EVENT::UNFRIENDED, str).unwrap(); } - _ => todo!(), + _ => error!("Received unexpected payload format for unfriended"), } } @@ -73,7 +74,19 @@ fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { .emit(WS_EVENT::FRIEND_CURSOR_POSITION, str) .unwrap(); } - _ => todo!(), + _ => error!("Received unexpected payload format for friend cursor position"), + } +} + +fn on_friend_disconnected(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(str) => { + println!("Received friend disconnected: {:?}", str); + get_app_handle() + .emit(WS_EVENT::FRIEND_DISCONNECTED, str) + .unwrap(); + } + _ => error!("Received unexpected payload format for friend disconnected"), } } @@ -145,6 +158,7 @@ pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::C .on(WS_EVENT::FRIEND_REQUEST_DENIED, on_friend_request_denied) .on(WS_EVENT::UNFRIENDED, on_unfriended) .on(WS_EVENT::FRIEND_CURSOR_POSITION, on_friend_cursor_position) + .on(WS_EVENT::FRIEND_DISCONNECTED, on_friend_disconnected) .auth(json!({ "token": token })) .connect() }) diff --git a/src/events/cursor.ts b/src/events/cursor.ts index c5ca7fd..6145d11 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -14,14 +14,28 @@ export type FriendCursorPosition = { position: CursorPosition; }; +// Map of userId -> { position: CursorPosition, lastUpdated: number } +// We store the timestamp to detect stale cursors +type FriendCursorData = { + position: CursorPosition; + lastUpdated: number; +}; + +// 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>( {}, ); let unlistenCursor: UnlistenFn | null = null; let unlistenFriendCursor: UnlistenFn | null = null; +let unlistenFriendDisconnected: UnlistenFn | null = null; let isListening = false; +// Internal state to track timestamps +let friendCursorState: Record = {}; + /** * Initialize cursor tracking for this window. * Can be called from multiple windows - only the first call starts tracking on the Rust side, @@ -67,6 +81,12 @@ export async function initCursorTracking() { // Since we only send one argument { userId, position }, it's the first element const data = Array.isArray(payload) ? payload[0] : payload; + // Update internal state with timestamp + friendCursorState[data.userId] = { + position: data.position, + lastUpdated: Date.now(), + }; + friendsCursorPositions.update((current) => { return { ...current, @@ -76,6 +96,36 @@ export async function initCursorTracking() { }, ); + // 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("[Cursor] Failed to parse friend disconnected payload:", e); + return; + } + } + + const data = Array.isArray(payload) ? payload[0] : payload; + + // 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; + }); + } + ); + isListening = true; } catch (err) { console.error("[Cursor] Failed to initialize cursor tracking:", err); @@ -96,6 +146,10 @@ export function stopCursorTracking() { unlistenFriendCursor(); unlistenFriendCursor = null; } + if (unlistenFriendDisconnected) { + unlistenFriendDisconnected(); + unlistenFriendDisconnected = null; + } isListening = false; }