resolve live data on initial load
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use device_query::{DeviceEvents, DeviceEventsHandler};
|
||||
use device_query::{DeviceEvents, DeviceEventsHandler, DeviceQuery, DeviceState};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -63,6 +63,38 @@ impl CursorState {
|
||||
.map(|positions| positions.clone())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, user_id: &str) -> Result<Option<CursorPositions>, String> {
|
||||
self.0
|
||||
.read()
|
||||
.map(|positions| positions.get(user_id).cloned())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn current_position(handle: &AppHandle) -> Result<CursorPositions, String> {
|
||||
let monitor = handle
|
||||
.primary_monitor()
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Primary monitor is unavailable")?;
|
||||
let position = DeviceState::new().get_mouse().coords;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64,
|
||||
y: position.1 as f64,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64 * monitor.scale_factor(),
|
||||
y: position.1 as f64 * monitor.scale_factor(),
|
||||
};
|
||||
|
||||
Ok(CursorPositions {
|
||||
mapped: transform_cursor_pos(&raw, true, &monitor),
|
||||
raw,
|
||||
})
|
||||
}
|
||||
|
||||
// Was private, but for some reason LSP
|
||||
@@ -95,6 +127,11 @@ pub fn init(app: &AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
match current_position(app) {
|
||||
Ok(positions) => update_cursor_position(app, positions),
|
||||
Err(error) => eprintln!("Failed to resolve current cursor position: {error}"),
|
||||
}
|
||||
|
||||
let (stop_tx, stop_rx) = watch::channel(false);
|
||||
|
||||
println!("Spawning cursor tracking task");
|
||||
|
||||
@@ -23,10 +23,10 @@ async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>>
|
||||
scene_configuration::init(handle).await?;
|
||||
keypair::init(handle).await?;
|
||||
app.manage(interactions::InteractionState::default());
|
||||
network::init(handle).await?;
|
||||
app.manage(ufa::ForegroundAppState::default());
|
||||
app.manage(cursor::CursorState::default());
|
||||
app.manage(puppet::PuppetStateStore::default());
|
||||
network::init(handle).await?;
|
||||
ufa::init(handle);
|
||||
cursor::init(handle);
|
||||
puppet::init(handle)?;
|
||||
@@ -79,6 +79,7 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
network::list_statuses,
|
||||
network::list_friend_statuses,
|
||||
network::resolve_friend_display_name,
|
||||
live_data::list_live_data,
|
||||
skins::resolve_skin,
|
||||
images::pick_and_send_image,
|
||||
images::send_image_bytes,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::cursor::CursorPositions;
|
||||
use crate::ufa::AppMeta;
|
||||
@@ -10,6 +14,43 @@ pub enum LiveData {
|
||||
ForegroundApp { meta: AppMeta },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveDataSnapshot {
|
||||
pub cursor_positions: HashMap<String, CursorPositions>,
|
||||
pub foreground_apps: HashMap<String, AppMeta>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn list_live_data(handle: AppHandle) -> Result<LiveDataSnapshot, String> {
|
||||
Ok(LiveDataSnapshot {
|
||||
cursor_positions: handle.state::<crate::cursor::CursorState>().snapshot()?,
|
||||
foreground_apps: handle
|
||||
.state::<crate::ufa::ForegroundAppState>()
|
||||
.snapshot()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn publish_current(handle: &AppHandle) -> Result<(), String> {
|
||||
let user_id = handle
|
||||
.state::<crate::keypair::AppKeypair>()
|
||||
.public_key()
|
||||
.to_owned();
|
||||
let network = handle.state::<crate::network::Network>();
|
||||
|
||||
if let Some(positions) = handle.state::<crate::cursor::CursorState>().get(&user_id)? {
|
||||
network.send_live_data(LiveData::Cursor { positions });
|
||||
}
|
||||
if let Some(meta) = handle
|
||||
.state::<crate::ufa::ForegroundAppState>()
|
||||
.get(&user_id)?
|
||||
{
|
||||
network.send_live_data(LiveData::ForegroundApp { meta });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -261,12 +261,13 @@ impl Network {
|
||||
.filter(|(id, connection)| desired.get(*id) != Some(&connection.remote))
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
let mut presence_changes = Vec::new();
|
||||
|
||||
for id in stale {
|
||||
if let Some(connection) = connections.remove(&id) {
|
||||
connection.task.abort();
|
||||
remove_status(&self.statuses, &id, connection.generation);
|
||||
apply_friend_presence_change(handle, self.friend_presence.remove(&id));
|
||||
presence_changes.push(self.friend_presence.remove(&id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +318,9 @@ impl Network {
|
||||
}
|
||||
|
||||
drop(connections);
|
||||
for change in presence_changes {
|
||||
apply_friend_presence_change(handle, change);
|
||||
}
|
||||
emit_statuses(handle, &self.statuses)
|
||||
}
|
||||
}
|
||||
@@ -337,8 +341,10 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>>
|
||||
keypair,
|
||||
next_generation: AtomicU64::new(1),
|
||||
};
|
||||
network.sync_remotes(handle, remotes::all(&database).await?)?;
|
||||
handle.manage(network);
|
||||
handle
|
||||
.state::<Network>()
|
||||
.sync_remotes(handle, remotes::all(&database).await?)?;
|
||||
|
||||
let listener_handle = handle.clone();
|
||||
RemotesChanged::listen(handle, move |event| {
|
||||
@@ -411,6 +417,17 @@ fn apply_friend_presence_change(
|
||||
match result {
|
||||
Ok(Some(change)) => {
|
||||
crate::cursor::remove_positions(handle, &change.went_offline);
|
||||
if let Err(error) = handle
|
||||
.state::<crate::ufa::ForegroundAppState>()
|
||||
.remove(&change.went_offline)
|
||||
{
|
||||
eprintln!("failed to remove offline foreground apps: {error}");
|
||||
}
|
||||
if !change.came_online.is_empty()
|
||||
&& let Err(error) = crate::live_data::publish_current(handle)
|
||||
{
|
||||
eprintln!("failed to publish current live data: {error}");
|
||||
}
|
||||
if let Err(error) = (FriendStatusesChanged {
|
||||
friend_ids: change.online,
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::Mutex;
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct Change {
|
||||
pub(super) online: Vec<String>,
|
||||
pub(super) came_online: Vec<String>,
|
||||
pub(super) went_offline: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -88,11 +89,14 @@ fn diff(before: HashSet<String>, by_remote: &HashMap<String, HashSet<String>>) -
|
||||
return None;
|
||||
}
|
||||
let mut went_offline = before.difference(&after).cloned().collect::<Vec<_>>();
|
||||
let mut came_online = after.difference(&before).cloned().collect::<Vec<_>>();
|
||||
let mut online = after.into_iter().collect::<Vec<_>>();
|
||||
came_online.sort_unstable();
|
||||
went_offline.sort_unstable();
|
||||
online.sort_unstable();
|
||||
Some(Change {
|
||||
online,
|
||||
came_online,
|
||||
went_offline,
|
||||
})
|
||||
}
|
||||
@@ -110,6 +114,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
Some(Change {
|
||||
online: vec!["friend".to_owned()],
|
||||
came_online: vec!["friend".to_owned()],
|
||||
went_offline: Vec::new(),
|
||||
})
|
||||
);
|
||||
@@ -124,6 +129,7 @@ mod tests {
|
||||
presence.remove("remote-b").unwrap(),
|
||||
Some(Change {
|
||||
online: Vec::new(),
|
||||
came_online: Vec::new(),
|
||||
went_offline: vec!["friend".to_owned()],
|
||||
})
|
||||
);
|
||||
@@ -136,14 +142,12 @@ mod tests {
|
||||
.replace("remote", vec!["kept".to_owned(), "removed".to_owned()])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
presence
|
||||
.update("remote", "added".to_owned(), true)
|
||||
.unwrap()
|
||||
.expect("friend came online")
|
||||
.online,
|
||||
["added", "kept", "removed"]
|
||||
);
|
||||
let change = presence
|
||||
.update("remote", "added".to_owned(), true)
|
||||
.unwrap()
|
||||
.expect("friend came online");
|
||||
assert_eq!(change.online, ["added", "kept", "removed"]);
|
||||
assert_eq!(change.came_online, ["added"]);
|
||||
assert_eq!(
|
||||
presence
|
||||
.update("remote", "added".to_owned(), false)
|
||||
|
||||
+101
-15
@@ -2,6 +2,7 @@
|
||||
UFA: User Focused App
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_specta::Event;
|
||||
@@ -27,7 +28,47 @@ pub struct FriendForegroundAppChanged {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ForegroundAppState(RwLock<AppMeta>);
|
||||
pub struct ForegroundAppState(RwLock<HashMap<String, AppMeta>>);
|
||||
|
||||
impl ForegroundAppState {
|
||||
fn update(&self, user_id: String, meta: AppMeta) -> Result<(), String> {
|
||||
self.0
|
||||
.write()
|
||||
.map_err(|error| error.to_string())?
|
||||
.insert(user_id, meta);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&self, user_ids: &[String]) -> Result<(), String> {
|
||||
let mut apps = self.0.write().map_err(|error| error.to_string())?;
|
||||
apps.retain(|user_id, _| !user_ids.contains(user_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Result<HashMap<String, AppMeta>, String> {
|
||||
self.0
|
||||
.read()
|
||||
.map(|apps| apps.clone())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, user_id: &str) -> Result<Option<AppMeta>, String> {
|
||||
self.0
|
||||
.read()
|
||||
.map(|apps| apps.get(user_id).cloned())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn current_app() -> AppMeta {
|
||||
#[cfg(target_os = "macos")]
|
||||
let meta = macos::get_active_app_metadata_macos();
|
||||
#[cfg(target_os = "windows")]
|
||||
let meta = windows::get_active_app_metadata_windows(None);
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
let meta = AppMeta::default();
|
||||
meta
|
||||
}
|
||||
|
||||
/// Listens for changes in the active (foreground) application and calls the provided callback with metadata.
|
||||
/// The implementation varies by platform: macOS uses NSWorkspace notifications, Windows uses WinEventHook.
|
||||
@@ -66,26 +107,71 @@ where
|
||||
/// and emits events to the Tauri app on changes.
|
||||
/// Used for app to emit user foreground app to peers.
|
||||
pub fn init(handle: &AppHandle) {
|
||||
update_local_app(handle, current_app());
|
||||
let handle = handle.clone();
|
||||
|
||||
init_listener(move |meta: AppMeta| {
|
||||
let state = handle.state::<ForegroundAppState>();
|
||||
let mut current = state.0.write().expect("Foreground App lock failed");
|
||||
*current = meta.clone();
|
||||
drop(current);
|
||||
|
||||
if let Err(error) = (ForegroundAppChanged { meta: meta.clone() }).emit(&handle) {
|
||||
eprintln!("Failed to emit foreground app change: {error}");
|
||||
}
|
||||
|
||||
handle
|
||||
.state::<crate::network::Network>()
|
||||
.send_live_data(crate::live_data::LiveData::ForegroundApp { meta });
|
||||
});
|
||||
init_listener(move |meta: AppMeta| update_local_app(&handle, meta));
|
||||
}
|
||||
|
||||
pub(crate) fn emit_friend_app(handle: &AppHandle, friend_id: String, meta: AppMeta) {
|
||||
if let Err(error) = handle
|
||||
.state::<ForegroundAppState>()
|
||||
.update(friend_id.clone(), meta.clone())
|
||||
{
|
||||
eprintln!("Failed to cache friend foreground app: {error}");
|
||||
return;
|
||||
}
|
||||
if let Err(error) = (FriendForegroundAppChanged { friend_id, meta }).emit(handle) {
|
||||
eprintln!("Failed to emit friend foreground app change: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn update_local_app(handle: &AppHandle, meta: AppMeta) {
|
||||
let user_id = handle
|
||||
.state::<crate::keypair::AppKeypair>()
|
||||
.public_key()
|
||||
.to_owned();
|
||||
if let Err(error) = handle
|
||||
.state::<ForegroundAppState>()
|
||||
.update(user_id, meta.clone())
|
||||
{
|
||||
eprintln!("Failed to cache foreground app: {error}");
|
||||
return;
|
||||
}
|
||||
if let Err(error) = (ForegroundAppChanged { meta: meta.clone() }).emit(handle) {
|
||||
eprintln!("Failed to emit foreground app change: {error}");
|
||||
}
|
||||
handle
|
||||
.state::<crate::network::Network>()
|
||||
.send_live_data(crate::live_data::LiveData::ForegroundApp { meta });
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn app(name: &str) -> AppMeta {
|
||||
AppMeta {
|
||||
local: Some(name.to_owned()),
|
||||
unlocal: None,
|
||||
ico: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_app_state_snapshots_and_removes_users() {
|
||||
let state = ForegroundAppState::default();
|
||||
state.update("local".to_owned(), app("Local")).unwrap();
|
||||
state.update("friend".to_owned(), app("Friend")).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get("friend").unwrap().unwrap().local.as_deref(),
|
||||
Some("Friend")
|
||||
);
|
||||
state.remove(&["friend".to_owned()]).unwrap();
|
||||
|
||||
let snapshot = state.snapshot().unwrap();
|
||||
assert_eq!(snapshot.len(), 1);
|
||||
assert_eq!(snapshot["local"].local.as_deref(), Some("Local"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ async listFriendStatuses() : Promise<string[]> {
|
||||
async resolveFriendDisplayName(userId: string) : Promise<string | null> {
|
||||
return await TAURI_INVOKE("resolve_friend_display_name", { userId });
|
||||
},
|
||||
async listLiveData() : Promise<LiveDataSnapshot> {
|
||||
return await TAURI_INVOKE("list_live_data");
|
||||
},
|
||||
async resolveSkin(userId: string, skinHash: string) : Promise<string | null> {
|
||||
return await TAURI_INVOKE("resolve_skin", { userId, skinHash });
|
||||
},
|
||||
@@ -142,6 +145,7 @@ export type FriendInteractionReceived = { interactionId: string; friendId: strin
|
||||
export type FriendStatusesChanged = { friendIds: string[] }
|
||||
export type FriendsChanged = { friends: Friend[] }
|
||||
export type InteractionContent = { type: "text"; text: string } | { type: "wave" } | { type: "image"; mediaType: string; data: string }
|
||||
export type LiveDataSnapshot = { cursorPositions: Partial<{ [key in string]: CursorPositions }>; foregroundApps: Partial<{ [key in string]: AppMeta }> }
|
||||
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
|
||||
export type ProfileChanged = { profile: User }
|
||||
export type PuppetMovementMode = "free" | "bottom"
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { commands, events } from "$lib/bindings";
|
||||
import { removeFriendForegroundApps } from "./live-metadata";
|
||||
import { retainOnlineForegroundApps } from "./live-metadata";
|
||||
|
||||
export const onlineFriendIds = writable<Set<string>>(new Set());
|
||||
export const friendStatusesListenerError = writable("");
|
||||
|
||||
export async function initFriendStatusesListener() {
|
||||
let current = new Set<string>();
|
||||
|
||||
const apply = (friendIds: string[]) => {
|
||||
const next = new Set(friendIds);
|
||||
const wentOffline = [...current].filter((friendId) => !next.has(friendId));
|
||||
current = next;
|
||||
onlineFriendIds.set(next);
|
||||
|
||||
removeFriendForegroundApps(wentOffline);
|
||||
retainOnlineForegroundApps(next);
|
||||
};
|
||||
|
||||
const unlisten = await events.friendStatusesChanged.listen((event) => {
|
||||
|
||||
@@ -10,17 +10,29 @@ import { initSceneConfigurationListener } from "./scene-configuration";
|
||||
|
||||
type Unlisten = () => void;
|
||||
|
||||
async function initPresenceListeners(): Promise<Unlisten> {
|
||||
const unlistenLiveMetadata = await initLiveMetadataListeners();
|
||||
try {
|
||||
const unlistenFriendStatuses = await initFriendStatusesListener();
|
||||
return () => {
|
||||
unlistenFriendStatuses();
|
||||
unlistenLiveMetadata();
|
||||
};
|
||||
} catch (error) {
|
||||
unlistenLiveMetadata();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function initAppListeners(): Unlisten {
|
||||
let disposed = false;
|
||||
let unlisteners: Unlisten[] = [];
|
||||
|
||||
Promise.allSettled([
|
||||
initFriendsListener(),
|
||||
initRemotesListener(),
|
||||
initProfileListener(),
|
||||
initConnectionStatusesListener(),
|
||||
initFriendStatusesListener(),
|
||||
initLiveMetadataListeners(),
|
||||
initPresenceListeners(),
|
||||
initInteractionListener(),
|
||||
initPuppetStatesListener(),
|
||||
initSceneConfigurationListener(),
|
||||
|
||||
@@ -19,20 +19,24 @@ export const liveMetadata = writable<LiveMetadata>({
|
||||
});
|
||||
export const liveMetadataListenerError = writable("");
|
||||
|
||||
export function removeFriendForegroundApps(friendIds: string[]) {
|
||||
if (friendIds.length === 0) return;
|
||||
const removed = new Set(friendIds);
|
||||
export function retainOnlineForegroundApps(onlineFriendIds: Set<string>) {
|
||||
liveMetadata.update((metadata) => ({
|
||||
...metadata,
|
||||
foregroundApps: new Map(
|
||||
[...metadata.foregroundApps].filter(([userId]) => !removed.has(userId)),
|
||||
[...metadata.foregroundApps].filter(
|
||||
([userId]) =>
|
||||
userId === metadata.localId || onlineFriendIds.has(userId),
|
||||
),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function initLiveMetadataListeners() {
|
||||
let localId = "";
|
||||
let initializing = true;
|
||||
let pendingCursorPositions: LiveMetadata["cursorPositions"] | null = null;
|
||||
let pendingLocalForegroundApp: AppMeta | null = null;
|
||||
const pendingFriendForegroundApps = new Map<string, AppMeta>();
|
||||
|
||||
const updateForegroundApp = (userId: string, meta: AppMeta) => {
|
||||
liveMetadata.update((current) => ({
|
||||
@@ -41,34 +45,76 @@ export async function initLiveMetadataListeners() {
|
||||
}));
|
||||
};
|
||||
|
||||
const applyPendingUpdates = () => {
|
||||
const cursorPositions = pendingCursorPositions;
|
||||
if (cursorPositions) {
|
||||
liveMetadata.update((current) => ({ ...current, cursorPositions }));
|
||||
pendingCursorPositions = null;
|
||||
}
|
||||
if (localId && pendingLocalForegroundApp) {
|
||||
updateForegroundApp(localId, pendingLocalForegroundApp);
|
||||
pendingLocalForegroundApp = null;
|
||||
}
|
||||
pendingFriendForegroundApps.forEach((meta, friendId) => {
|
||||
updateForegroundApp(friendId, meta);
|
||||
});
|
||||
pendingFriendForegroundApps.clear();
|
||||
};
|
||||
|
||||
const unlisteners = await Promise.all([
|
||||
events.cursorPositionChanged.listen((event) => {
|
||||
if (initializing) {
|
||||
pendingCursorPositions = event.payload.positions;
|
||||
return;
|
||||
}
|
||||
liveMetadata.update((current) => ({
|
||||
...current,
|
||||
cursorPositions: event.payload.positions,
|
||||
}));
|
||||
}),
|
||||
events.foregroundAppChanged.listen((event) => {
|
||||
if (localId) {
|
||||
if (!initializing && localId) {
|
||||
updateForegroundApp(localId, event.payload.meta);
|
||||
} else {
|
||||
pendingLocalForegroundApp = event.payload.meta;
|
||||
}
|
||||
}),
|
||||
events.friendForegroundAppChanged.listen((event) => {
|
||||
updateForegroundApp(event.payload.friendId, event.payload.meta);
|
||||
if (initializing) {
|
||||
pendingFriendForegroundApps.set(
|
||||
event.payload.friendId,
|
||||
event.payload.meta,
|
||||
);
|
||||
} else {
|
||||
updateForegroundApp(event.payload.friendId, event.payload.meta);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
try {
|
||||
localId = await commands.getPublicKey();
|
||||
liveMetadata.update((current) => ({ ...current, localId }));
|
||||
|
||||
if (pendingLocalForegroundApp) {
|
||||
updateForegroundApp(localId, pendingLocalForegroundApp);
|
||||
pendingLocalForegroundApp = null;
|
||||
}
|
||||
const [snapshot] = await Promise.all([
|
||||
commands.listLiveData(),
|
||||
commands.getPublicKey().then((resolvedLocalId) => {
|
||||
localId = resolvedLocalId;
|
||||
}),
|
||||
]);
|
||||
const foregroundApps = new Map<string, AppMeta>();
|
||||
Object.entries(snapshot.foregroundApps).forEach(([userId, meta]) => {
|
||||
if (meta) foregroundApps.set(userId, meta);
|
||||
});
|
||||
liveMetadata.set({
|
||||
localId,
|
||||
cursorPositions: snapshot.cursorPositions,
|
||||
foregroundApps,
|
||||
});
|
||||
initializing = false;
|
||||
applyPendingUpdates();
|
||||
} catch (error) {
|
||||
initializing = false;
|
||||
if (localId) {
|
||||
liveMetadata.update((current) => ({ ...current, localId }));
|
||||
}
|
||||
applyPendingUpdates();
|
||||
liveMetadataListenerError.set(String(error));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user