consolidation & handling of data from backend pt 2

This commit is contained in:
2026-03-08 23:04:49 +08:00
parent 2aa1d5f92f
commit 2dcc202540
13 changed files with 217 additions and 102 deletions

View File

@@ -1,34 +1,34 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DollColorSchemeDto { pub struct DollColorSchemeDto {
pub outline: String, pub outline: String,
pub body: String, pub body: String,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DollConfigurationDto { pub struct DollConfigurationDto {
pub color_scheme: DollColorSchemeDto, pub color_scheme: DollColorSchemeDto,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CreateDollDto { pub struct CreateDollDto {
pub name: String, pub name: String,
pub configuration: Option<DollConfigurationDto>, pub configuration: Option<DollConfigurationDto>,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UpdateDollDto { pub struct UpdateDollDto {
pub name: Option<String>, pub name: Option<String>,
pub configuration: Option<DollConfigurationDto>, pub configuration: Option<DollConfigurationDto>,
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)] #[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DollDto { pub struct DollDto {
pub id: String, pub id: String,

View File

@@ -5,14 +5,14 @@ use super::dolls::DollDto;
use super::friends::UserBasicDto; use super::friends::UserBasicDto;
use crate::services::presence_modules::models::PresenceStatus; use crate::services::presence_modules::models::PresenceStatus;
#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum UserStatusState { pub enum UserStatusState {
Idle, Idle,
Resting, Resting,
} }
#[derive(Clone, Serialize, Deserialize, Debug, Type)] #[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UserStatusPayload { pub struct UserStatusPayload {
pub presence_status: PresenceStatus, pub presence_status: PresenceStatus,

View File

@@ -15,14 +15,14 @@ use crate::{
}; };
use tauri_specta::Event as _; use tauri_specta::Event as _;
#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CursorPosition { pub struct CursorPosition {
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CursorPositions { pub struct CursorPositions {
pub raw: CursorPosition, pub raw: CursorPosition,

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Clone, Serialize, Deserialize, Type, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PresenceStatus { pub struct PresenceStatus {
pub title: Option<String>, pub title: Option<String>,

View File

@@ -1,6 +1,8 @@
use crate::{ use crate::{
get_app_handle, lock_r, lock_w, models::event_payloads::UserStatusPayload, get_app_handle, lock_r, lock_w,
services::app_events::PresenceStateUpdated, state::FDOLL, models::event_payloads::UserStatusPayload,
services::app_events::PresenceStateUpdated,
state::{get_known_friend_ids, FDOLL},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
@@ -22,44 +24,44 @@ pub fn get_presence_state_snapshot() -> PresenceStateSnapshot {
} }
} }
pub fn set_current_presence(status: UserStatusPayload) { pub fn set_current_presence(status: UserStatusPayload) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
if guard.presence.current.as_ref() == Some(&status) {
return false;
}
guard.presence.current = Some(status); guard.presence.current = Some(status);
true
} }
pub fn set_friend_presence(friend_id: String, status: UserStatusPayload) { pub fn set_friend_presence(friend_id: String, status: UserStatusPayload) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
if guard.presence.friends.get(&friend_id) == Some(&status) {
return false;
}
guard.presence.friends.insert(friend_id, status); guard.presence.friends.insert(friend_id, status);
true
} }
pub fn remove_friend_presence(friend_id: &str) { pub fn remove_friend_presence(friend_id: &str) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
guard.presence.friends.remove(friend_id); guard.presence.friends.remove(friend_id).is_some()
} }
pub fn clear_missing_friends_from_presence_state() { pub fn clear_missing_friends_from_presence_state() -> bool {
let friend_ids = { let friend_ids = get_known_friend_ids();
let guard = lock_r!(FDOLL);
guard
.user_data
.friends
.as_ref()
.map(|friends| {
friends
.iter()
.filter_map(|friendship| {
friendship.friend.as_ref().map(|friend| friend.id.clone())
})
.collect::<std::collections::HashSet<_>>()
})
.unwrap_or_default()
};
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
let initial_count = guard.presence.friends.len();
guard guard
.presence .presence
.friends .friends
.retain(|friend_id, _| friend_ids.contains(friend_id)); .retain(|friend_id, _| friend_ids.contains(friend_id));
guard.presence.friends.len() != initial_count
} }
pub fn emit_presence_state_updated() { pub fn emit_presence_state_updated() {

View File

@@ -2,7 +2,7 @@ use crate::{
get_app_handle, lock_r, lock_w, get_app_handle, lock_r, lock_w,
models::{dolls::DollDto, scene::SceneFriendNeko}, models::{dolls::DollDto, scene::SceneFriendNeko},
services::{app_events::SceneFriendsUpdated, cursor::CursorPositions}, services::{app_events::SceneFriendsUpdated, cursor::CursorPositions},
state::FDOLL, state::{get_known_friend_ids, FDOLL},
}; };
use tauri_specta::Event as _; use tauri_specta::Event as _;
use tracing::error; use tracing::error;
@@ -34,44 +34,49 @@ pub fn get_scene_friends_snapshot() -> Vec<SceneFriendNeko> {
.collect() .collect()
} }
pub fn set_friend_cursor_position(friend_id: String, position: CursorPositions) { pub fn set_friend_cursor_position(friend_id: String, position: CursorPositions) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
if guard.friend_scene.cursor_positions.get(&friend_id) == Some(&position) {
return false;
}
guard guard
.friend_scene .friend_scene
.cursor_positions .cursor_positions
.insert(friend_id, position); .insert(friend_id, position);
true
} }
pub fn set_friend_active_doll(friend_id: String, doll: Option<DollDto>) { pub fn set_friend_active_doll(friend_id: String, doll: Option<DollDto>) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
if guard.friend_scene.active_dolls.get(&friend_id) == Some(&doll) {
return false;
}
guard.friend_scene.active_dolls.insert(friend_id, doll); guard.friend_scene.active_dolls.insert(friend_id, doll);
true
} }
pub fn remove_friend(friend_id: &str) { pub fn remove_friend(friend_id: &str) -> bool {
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
guard.friend_scene.cursor_positions.remove(friend_id); let removed_cursor = guard
guard.friend_scene.active_dolls.remove(friend_id); .friend_scene
.cursor_positions
.remove(friend_id)
.is_some();
let removed_doll = guard.friend_scene.active_dolls.remove(friend_id).is_some();
removed_cursor || removed_doll
} }
pub fn clear_missing_friends_from_runtime_state() { pub fn clear_missing_friends_from_runtime_state() -> bool {
let friend_ids = { let friend_ids = get_known_friend_ids();
let guard = lock_r!(FDOLL);
guard
.user_data
.friends
.as_ref()
.map(|friends| {
friends
.iter()
.filter_map(|friendship| {
friendship.friend.as_ref().map(|friend| friend.id.clone())
})
.collect::<std::collections::HashSet<_>>()
})
.unwrap_or_default()
};
let mut guard = lock_w!(FDOLL); let mut guard = lock_w!(FDOLL);
let initial_cursor_count = guard.friend_scene.cursor_positions.len();
let initial_active_doll_count = guard.friend_scene.active_dolls.len();
guard guard
.friend_scene .friend_scene
.cursor_positions .cursor_positions
@@ -80,6 +85,9 @@ pub fn clear_missing_friends_from_runtime_state() {
.friend_scene .friend_scene
.active_dolls .active_dolls
.retain(|friend_id, _| friend_ids.contains(friend_id)); .retain(|friend_id, _| friend_ids.contains(friend_id));
guard.friend_scene.cursor_positions.len() != initial_cursor_count
|| guard.friend_scene.active_dolls.len() != initial_active_doll_count
} }
pub fn emit_scene_friends_updated() { pub fn emit_scene_friends_updated() {

View File

@@ -75,28 +75,34 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
}, },
}; };
scene_friends::set_friend_cursor_position( let scene_friends_changed = scene_friends::set_friend_cursor_position(
outgoing_payload.user_id.clone(), outgoing_payload.user_id.clone(),
outgoing_payload.position.clone(), outgoing_payload.position.clone(),
); );
emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload)); emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload));
if scene_friends_changed {
scene_friends::emit_scene_friends_updated(); scene_friends::emit_scene_friends_updated();
} }
} }
}
/// Handler for friend-disconnected event /// Handler for friend-disconnected event
pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected") utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected")
{ {
scene_friends::remove_friend(&data.user_id); let scene_friends_changed = scene_friends::remove_friend(&data.user_id);
presence_state::remove_friend_presence(&data.user_id); let presence_changed = presence_state::remove_friend_presence(&data.user_id);
emitter::emit_to_frontend_typed(&FriendDisconnected(data)); emitter::emit_to_frontend_typed(&FriendDisconnected(data));
if scene_friends_changed {
scene_friends::emit_scene_friends_updated(); scene_friends::emit_scene_friends_updated();
}
if presence_changed {
presence_state::emit_presence_state_updated(); presence_state::emit_presence_state_updated();
} }
} }
}
/// Handler for friend-doll-created event /// Handler for friend-doll-created event
pub fn on_friend_doll_created(payload: Payload, _socket: RawClient) { pub fn on_friend_doll_created(payload: Payload, _socket: RawClient) {
@@ -129,7 +135,6 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
) { ) {
scene_friends::set_friend_active_doll(data.friend_id.clone(), data.doll.clone()); scene_friends::set_friend_active_doll(data.friend_id.clone(), data.doll.clone());
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
scene_friends::emit_scene_friends_updated();
refresh::refresh_app_data(AppDataRefreshScope::Friends); refresh::refresh_app_data(AppDataRefreshScope::Friends);
} }
} }
@@ -139,8 +144,11 @@ pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status") utils::extract_and_parse::<FriendUserStatusPayload>(payload, "friend-user-status")
{ {
let presence_changed =
presence_state::set_friend_presence(data.user_id.clone(), data.status.clone()); presence_state::set_friend_presence(data.user_id.clone(), data.status.clone());
emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data)); emitter::emit_to_frontend_typed(&FriendUserStatusChanged(data));
if presence_changed {
presence_state::emit_presence_state_updated(); presence_state::emit_presence_state_updated();
} }
} }
}

View File

@@ -29,8 +29,9 @@ pub async fn report_user_status(status: UserStatusPayload) {
warn!("Failed to emit user-status-changed event: {e}"); warn!("Failed to emit user-status-changed event: {e}");
} }
presence_state::set_current_presence(status.clone()); if presence_state::set_current_presence(status.clone()) {
presence_state::emit_presence_state_updated(); presence_state::emit_presence_state_updated();
}
// Schedule new report after 500ms // Schedule new report after 500ms
let handle = async_runtime::spawn(async move { let handle = async_runtime::spawn(async move {

View File

@@ -1,5 +1,6 @@
// in app-core/src/state.rs // in app-core/src/state.rs
use crate::{ use crate::{
lock_r,
lock_w, lock_w,
models::{app_data::UserData, dolls::DollDto}, models::{app_data::UserData, dolls::DollDto},
services::{ services::{
@@ -7,7 +8,7 @@ use crate::{
presence_modules::models::ModuleMetadata, presence_modules::models::ModuleMetadata,
}, },
}; };
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, LazyLock, RwLock}; use std::sync::{Arc, LazyLock, RwLock};
use tauri::tray::TrayIcon; use tauri::tray::TrayIcon;
use tracing::info; use tracing::info;
@@ -55,6 +56,22 @@ pub struct AppState {
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> = pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
LazyLock::new(|| Arc::new(RwLock::new(AppState::default()))); LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
pub fn get_known_friend_ids() -> HashSet<String> {
let guard = lock_r!(FDOLL);
guard
.user_data
.friends
.as_ref()
.map(|friends| {
friends
.iter()
.filter_map(|friendship| friendship.friend.as_ref().map(|friend| friend.id.clone()))
.collect()
})
.unwrap_or_default()
}
/// Populate app state with initial /// Populate app state with initial
/// values and necesary client instances. /// values and necesary client instances.
pub fn init_app_state() { pub fn init_app_state() {

View File

@@ -211,8 +211,14 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
let app_data_clone = guard.user_data.clone(); let app_data_clone = guard.user_data.clone();
drop(guard); // Drop lock before emitting to prevent potential deadlocks drop(guard); // Drop lock before emitting to prevent potential deadlocks
let refreshes_friend_runtime = matches!(
scope,
AppDataRefreshScope::All | AppDataRefreshScope::Friends
);
if refreshes_friend_runtime {
scene_friends::clear_missing_friends_from_runtime_state(); scene_friends::clear_missing_friends_from_runtime_state();
presence_state::clear_missing_friends_from_presence_state(); presence_state::clear_missing_friends_from_presence_state();
}
if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) { if let Err(e) = AppDataRefreshed(app_data_clone).emit(get_app_handle()) {
warn!("Failed to emit app-data-refreshed event: {}", e); warn!("Failed to emit app-data-refreshed event: {}", e);
@@ -229,9 +235,13 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
.show(|_| {}); .show(|_| {});
} }
if refreshes_friend_runtime {
scene_friends::emit_scene_friends_updated(); scene_friends::emit_scene_friends_updated();
}
if refreshes_friend_runtime {
presence_state::emit_presence_state_updated(); presence_state::emit_presence_state_updated();
} }
}
Ok(()) Ok(())
} }

View File

@@ -1,15 +1,11 @@
import type { DollDto } from "$lib/bindings"; import type { DollColorSchemeDto, DollDto } from "$lib/bindings";
import type { RecolorOptions } from "$lib/utils/sprite-utils";
export function getSpriteOptions( export function getDollColorScheme(
doll: DollDto | null | undefined, doll: DollDto | null | undefined,
): RecolorOptions | undefined { ): DollColorSchemeDto | undefined {
if (!doll) { if (!doll) {
return undefined; return undefined;
} }
return { return doll.configuration.colorScheme;
bodyColor: doll.configuration.colorScheme.body,
outlineColor: doll.configuration.colorScheme.outline,
};
} }

View File

@@ -1,10 +1,10 @@
import { commands, type DollColorSchemeDto } from "$lib/bindings"; import { commands, type DollColorSchemeDto } from "$lib/bindings";
import onekoGif from "../../assets/oneko/oneko.gif"; import onekoGif from "../../assets/oneko/oneko.gif";
export interface RecolorOptions { const spriteSheetUrlCache = new Map<string, Promise<string>>();
bodyColor: string;
outlineColor: string; function getSpriteCacheKey(options: DollColorSchemeDto): string {
applyTexture?: boolean; return `${options.body}:${options.outline}`;
} }
export async function getSpriteSheetUrl( export async function getSpriteSheetUrl(
@@ -14,15 +14,26 @@ export async function getSpriteSheetUrl(
return onekoGif; return onekoGif;
} }
try { const cacheKey = getSpriteCacheKey(options);
const result = await commands.recolorGifBase64( const cachedSpriteSheet = spriteSheetUrlCache.get(cacheKey);
if (cachedSpriteSheet) {
return cachedSpriteSheet;
}
const spriteSheetPromise = commands
.recolorGifBase64(
options.body, options.body,
options.outline, options.outline,
true, // default texture to true at this stage, maybe one day open up more customization options true, // default texture to true at this stage, maybe one day open up more customization options
); )
return `data:image/gif;base64,${result}`; .then((result) => `data:image/gif;base64,${result}`)
} catch (e) { .catch((e) => {
console.error("Failed to recolor sprite:", e); console.error("Failed to recolor sprite:", e);
spriteSheetUrlCache.delete(cacheKey);
return onekoGif; return onekoGif;
} });
spriteSheetUrlCache.set(cacheKey, spriteSheetPromise);
return spriteSheetPromise;
} }

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { UnlistenFn } from "@tauri-apps/api/event";
import { onMount } from "svelte";
import { cursorPositionOnScreen } from "../../events/cursor"; import { cursorPositionOnScreen } from "../../events/cursor";
import { sceneFriends } from "../../events/friend-cursor"; import { sceneFriends } from "../../events/friend-cursor";
import { appData } from "../../events/app-data"; import { appData } from "../../events/app-data";
@@ -8,17 +10,21 @@
currentPresenceState, currentPresenceState,
} from "../../events/user-status"; } from "../../events/user-status";
import { commands, events, type SceneFriendNeko } from "$lib/bindings"; import { commands, events, type SceneFriendNeko } from "$lib/bindings";
import { getDollColorScheme } from "$lib/scene";
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils"; import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
import DebugBar from "./components/debug-bar.svelte"; import DebugBar from "./components/debug-bar.svelte";
import Neko from "./components/neko/neko.svelte"; import Neko from "./components/neko/neko.svelte";
let userSpriteUrl = $state(""); let userSpriteUrl = $state("");
let friendSpriteUrls = $state<Record<string, string>>({}); let friendSpriteUrls = $state<Record<string, string>>({});
let latestFriendSpritesRequest = 0;
let cachedFriendSpriteUrls: Record<string, string> = {};
let cachedFriendSpriteKeys: Record<string, string> = {};
async function fetchUserSprite() { async function fetchUserSprite() {
try { try {
const doll = await commands.getUserActiveDoll(); const doll = await commands.getUserActiveDoll();
const url = await getSpriteSheetUrl(doll?.configuration.colorScheme); const url = await getSpriteSheetUrl(getDollColorScheme(doll));
userSpriteUrl = url; userSpriteUrl = url;
} catch { } catch {
const url = await getSpriteSheetUrl(); const url = await getSpriteSheetUrl();
@@ -26,31 +32,87 @@
} }
} }
$effect(() => { function getFriendSpriteKey(friend: SceneFriendNeko): string {
fetchUserSprite(); const colorScheme = getDollColorScheme(friend.activeDoll);
events.userActiveDollUpdated.listen(() => { if (!colorScheme) {
fetchUserSprite(); return "default";
}
return `${colorScheme.body}:${colorScheme.outline}`;
}
onMount(() => {
void fetchUserSprite();
let isDisposed = false;
let unlisten: UnlistenFn | undefined;
void (async () => {
const stopListening = await events.userActiveDollUpdated.listen(() => {
void fetchUserSprite();
}); });
if (isDisposed) {
stopListening();
return;
}
unlisten = stopListening;
})();
return () => {
isDisposed = true;
unlisten?.();
};
}); });
$effect(() => { $effect(() => {
const friends = $sceneFriends; const friends = $sceneFriends;
const requestId = ++latestFriendSpritesRequest;
if (friends.length === 0) { if (friends.length === 0) {
friendSpriteUrls = {}; friendSpriteUrls = {};
cachedFriendSpriteUrls = {};
cachedFriendSpriteKeys = {};
return; return;
} }
Promise.all( void Promise.all(
friends.map(async (friend: SceneFriendNeko) => { friends.map(async (friend: SceneFriendNeko) => {
const spriteKey = getFriendSpriteKey(friend);
const existingSpriteUrl = cachedFriendSpriteUrls[friend.id];
if (existingSpriteUrl && cachedFriendSpriteKeys[friend.id] === spriteKey) {
return [friend.id, spriteKey, existingSpriteUrl] as const;
}
return [ return [
friend.id, friend.id,
await getSpriteSheetUrl(friend.activeDoll.configuration.colorScheme), spriteKey,
await getSpriteSheetUrl(getDollColorScheme(friend.activeDoll)),
] as const; ] as const;
}), }),
).then((entries) => { ).then((entries) => {
friendSpriteUrls = Object.fromEntries(entries); if (requestId !== latestFriendSpritesRequest) {
return;
}
cachedFriendSpriteUrls = Object.fromEntries(
entries.map(
([friendId, , spriteUrl]: readonly [string, string, string]) => [
friendId,
spriteUrl,
],
),
);
cachedFriendSpriteKeys = Object.fromEntries(
entries.map(([friendId, spriteKey]: readonly [string, string, string]) => [
friendId,
spriteKey,
]),
);
friendSpriteUrls = cachedFriendSpriteUrls;
}); });
}); });
</script> </script>
@@ -81,7 +143,7 @@
cursorPosition={$cursorPositionOnScreen} cursorPosition={$cursorPositionOnScreen}
presenceStatus={$currentPresenceState?.presenceStatus ?? null} presenceStatus={$currentPresenceState?.presenceStatus ?? null}
friendsCursorPositions={Object.fromEntries( friendsCursorPositions={Object.fromEntries(
$sceneFriends.map((friend) => [friend.id, friend.position]), $sceneFriends.map((friend: SceneFriendNeko) => [friend.id, friend.position]),
)} )}
friends={$appData?.friends ?? []} friends={$appData?.friends ?? []}
friendsPresenceStates={$friendsPresenceStates} friendsPresenceStates={$friendsPresenceStates}