moved friend cursor data aggregation from frontend to backend
This commit is contained in:
@@ -25,10 +25,10 @@ use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, collect_commands
|
|||||||
|
|
||||||
use crate::services::app_events::{
|
use crate::services::app_events::{
|
||||||
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
||||||
FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted,
|
FriendCursorPositionsUpdated, FriendDisconnected,
|
||||||
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived,
|
||||||
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived,
|
||||||
SetInteractionOverlay, Unfriended, UserStatusChanged,
|
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
@@ -109,7 +109,7 @@ pub fn run() {
|
|||||||
EditDoll,
|
EditDoll,
|
||||||
CreateDoll,
|
CreateDoll,
|
||||||
UserStatusChanged,
|
UserStatusChanged,
|
||||||
FriendCursorPositionUpdated,
|
FriendCursorPositionsUpdated,
|
||||||
FriendDisconnected,
|
FriendDisconnected,
|
||||||
FriendActiveDollChanged,
|
FriendActiveDollChanged,
|
||||||
FriendUserStatusChanged,
|
FriendUserStatusChanged,
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||||
},
|
},
|
||||||
services::{cursor::CursorPositions, ws::OutgoingFriendCursorPayload},
|
services::{
|
||||||
|
cursor::CursorPositions, friend_cursor::FriendCursorPositionsDto,
|
||||||
|
ws::OutgoingFriendCursorPayload,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
@@ -47,6 +50,10 @@ pub struct UserStatusChanged(pub UserStatusPayload);
|
|||||||
#[tauri_specta(event_name = "friend-cursor-position")]
|
#[tauri_specta(event_name = "friend-cursor-position")]
|
||||||
pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload);
|
pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "friend-cursor-positions")]
|
||||||
|
pub struct FriendCursorPositionsUpdated(pub FriendCursorPositionsDto);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-disconnected")]
|
#[tauri_specta(event_name = "friend-disconnected")]
|
||||||
pub struct FriendDisconnected(pub FriendDisconnectedPayload);
|
pub struct FriendDisconnected(pub FriendDisconnectedPayload);
|
||||||
|
|||||||
147
src-tauri/src/services/friend_cursor.rs
Normal file
147
src-tauri/src/services/friend_cursor.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, LazyLock, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_app_handle, lock_r,
|
||||||
|
services::{app_events::FriendCursorPositionsUpdated, cursor::CursorPositions},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct FriendCursorPositionsDto(pub HashMap<String, CursorPositions>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FriendCursorProjection {
|
||||||
|
active_dolls: HashMap<String, bool>,
|
||||||
|
positions: HashMap<String, CursorPositions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static FRIEND_CURSOR_PROJECTION: LazyLock<Arc<RwLock<FriendCursorProjection>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(FriendCursorProjection::default())));
|
||||||
|
|
||||||
|
pub fn sync_from_app_data() {
|
||||||
|
let friends = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard.user_data.friends.clone().unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut projection = FRIEND_CURSOR_PROJECTION
|
||||||
|
.write()
|
||||||
|
.expect("friend cursor projection lock poisoned");
|
||||||
|
|
||||||
|
projection.active_dolls = friends
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|friendship| {
|
||||||
|
friendship.friend.map(|friend| {
|
||||||
|
let has_active_doll = friend.active_doll.is_some();
|
||||||
|
(friend.id, has_active_doll)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let active_dolls = projection.active_dolls.clone();
|
||||||
|
projection
|
||||||
|
.positions
|
||||||
|
.retain(|user_id, _| active_dolls.get(user_id) == Some(&true));
|
||||||
|
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
let mut projection = FRIEND_CURSOR_PROJECTION
|
||||||
|
.write()
|
||||||
|
.expect("friend cursor projection lock poisoned");
|
||||||
|
|
||||||
|
projection.active_dolls.clear();
|
||||||
|
projection.positions.clear();
|
||||||
|
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_position(user_id: String, position: CursorPositions) {
|
||||||
|
let mut projection = FRIEND_CURSOR_PROJECTION
|
||||||
|
.write()
|
||||||
|
.expect("friend cursor projection lock poisoned");
|
||||||
|
|
||||||
|
if !has_active_doll(&mut projection, &user_id) {
|
||||||
|
if projection.positions.remove(&user_id).is_some() {
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projection.positions.insert(user_id, position);
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_friend(user_id: &str) {
|
||||||
|
let mut projection = FRIEND_CURSOR_PROJECTION
|
||||||
|
.write()
|
||||||
|
.expect("friend cursor projection lock poisoned");
|
||||||
|
|
||||||
|
let removed_active_doll = projection.active_dolls.remove(user_id).is_some();
|
||||||
|
let removed_position = projection.positions.remove(user_id).is_some();
|
||||||
|
|
||||||
|
if removed_active_doll || removed_position {
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_doll(user_id: &str, has_active_doll: bool) {
|
||||||
|
let mut projection = FRIEND_CURSOR_PROJECTION
|
||||||
|
.write()
|
||||||
|
.expect("friend cursor projection lock poisoned");
|
||||||
|
|
||||||
|
projection
|
||||||
|
.active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
if !has_active_doll {
|
||||||
|
if projection.positions.remove(user_id).is_some() {
|
||||||
|
emit_snapshot(&projection.positions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_active_doll(projection: &mut FriendCursorProjection, user_id: &str) -> bool {
|
||||||
|
if let Some(has_active_doll) = projection.active_dolls.get(user_id) {
|
||||||
|
return *has_active_doll;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_active_doll = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.friends
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|friends| {
|
||||||
|
friends.iter().find_map(|friendship| {
|
||||||
|
let friend = friendship.friend.as_ref()?;
|
||||||
|
(friend.id == user_id).then_some(friend)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|friend| friend.active_doll.as_ref())
|
||||||
|
.is_some()
|
||||||
|
};
|
||||||
|
|
||||||
|
projection
|
||||||
|
.active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
has_active_doll
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_snapshot(positions: &HashMap<String, CursorPositions>) {
|
||||||
|
let payload = FriendCursorPositionsDto(positions.clone());
|
||||||
|
|
||||||
|
if let Err(err) = FriendCursorPositionsUpdated(payload).emit(get_app_handle()) {
|
||||||
|
tracing::warn!("Failed to emit friend cursor positions update: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mod auth;
|
|||||||
pub mod client_config_manager;
|
pub mod client_config_manager;
|
||||||
pub mod cursor;
|
pub mod cursor;
|
||||||
pub mod doll_editor;
|
pub mod doll_editor;
|
||||||
|
pub mod friend_cursor;
|
||||||
pub mod health_manager;
|
pub mod health_manager;
|
||||||
pub mod health_monitor;
|
pub mod health_monitor;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
|
|||||||
@@ -7,18 +7,16 @@ use crate::models::event_payloads::{
|
|||||||
UnfriendedPayload,
|
UnfriendedPayload,
|
||||||
};
|
};
|
||||||
use crate::services::app_events::{
|
use crate::services::app_events::{
|
||||||
FriendActiveDollChanged, FriendCursorPositionUpdated, FriendDisconnected,
|
FriendActiveDollChanged, FriendDisconnected, FriendRequestAccepted, FriendRequestDenied,
|
||||||
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
FriendRequestReceived, FriendUserStatusChanged, Unfriended,
|
||||||
Unfriended,
|
};
|
||||||
|
use crate::services::{
|
||||||
|
cursor::{normalized_to_absolute, CursorPositions},
|
||||||
|
friend_cursor,
|
||||||
};
|
};
|
||||||
use crate::services::cursor::{normalized_to_absolute, CursorPositions};
|
|
||||||
use crate::state::AppDataRefreshScope;
|
use crate::state::AppDataRefreshScope;
|
||||||
|
|
||||||
use super::{
|
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};
|
||||||
emitter, refresh,
|
|
||||||
types::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload},
|
|
||||||
utils,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Handler for friend-request-received event
|
/// Handler for friend-request-received event
|
||||||
pub fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
||||||
@@ -64,15 +62,13 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
|||||||
let mapped_pos = &friend_data.position;
|
let mapped_pos = &friend_data.position;
|
||||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||||
|
|
||||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
friend_cursor::update_position(
|
||||||
user_id: friend_data.user_id,
|
friend_data.user_id,
|
||||||
position: CursorPositions {
|
CursorPositions {
|
||||||
raw: raw_pos,
|
raw: raw_pos,
|
||||||
mapped: mapped_pos.clone(),
|
mapped: mapped_pos.clone(),
|
||||||
},
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
emitter::emit_to_frontend_typed(&FriendCursorPositionUpdated(outgoing_payload));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +77,7 @@ 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")
|
||||||
{
|
{
|
||||||
|
friend_cursor::remove_friend(&data.user_id);
|
||||||
emitter::emit_to_frontend_typed(&FriendDisconnected(data));
|
emitter::emit_to_frontend_typed(&FriendDisconnected(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +111,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
|
|||||||
payload,
|
payload,
|
||||||
"friend-active-doll-changed",
|
"friend-active-doll-changed",
|
||||||
) {
|
) {
|
||||||
|
friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some());
|
||||||
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
|
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
|
||||||
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r, lock_w,
|
get_app_handle, lock_r, lock_w,
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
services::app_events::AppDataRefreshed,
|
services::{app_events::AppDataRefreshed, friend_cursor},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
use std::{collections::HashSet, sync::LazyLock};
|
use std::{collections::HashSet, sync::LazyLock};
|
||||||
@@ -161,6 +161,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
Ok(friends) => {
|
Ok(friends) => {
|
||||||
let mut guard = lock_w!(crate::state::FDOLL);
|
let mut guard = lock_w!(crate::state::FDOLL);
|
||||||
guard.user_data.friends = Some(friends);
|
guard.user_data.friends = Some(friends);
|
||||||
|
drop(guard);
|
||||||
|
friend_cursor::sync_from_app_data();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to fetch friends list: {}", e);
|
warn!("Failed to fetch friends list: {}", e);
|
||||||
@@ -260,4 +262,6 @@ pub fn clear_app_data() {
|
|||||||
guard.user_data.dolls = None;
|
guard.user_data.dolls = None;
|
||||||
guard.user_data.user = None;
|
guard.user_data.user = None;
|
||||||
guard.user_data.friends = None;
|
guard.user_data.friends = None;
|
||||||
|
drop(guard);
|
||||||
|
friend_cursor::clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,32 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import {
|
import { events, type CursorPositions } from "$lib/bindings";
|
||||||
events,
|
import { createEventSource } from "./listener-utils";
|
||||||
type CursorPositions,
|
|
||||||
type DollDto,
|
|
||||||
type OutgoingFriendCursorPayload,
|
|
||||||
} from "$lib/bindings";
|
|
||||||
import { createEventSource, removeFromStore } from "./listener-utils";
|
|
||||||
|
|
||||||
type FriendCursorData = {
|
|
||||||
position: CursorPositions;
|
|
||||||
lastUpdated: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
export const friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
export const friendsActiveDolls = writable<Record<string, DollDto | null>>({});
|
|
||||||
|
// Here for now. Will extract into shared
|
||||||
|
// util when there's more similar cases.
|
||||||
|
function toCursorPositionsRecord(
|
||||||
|
payload: Partial<Record<string, CursorPositions>>,
|
||||||
|
): Record<string, CursorPositions> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(payload).filter(
|
||||||
|
(entry): entry is [string, CursorPositions] => {
|
||||||
|
return entry[1] !== undefined;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
start: startFriendCursorTracking,
|
start: startFriendCursorTracking,
|
||||||
stop: stopFriendCursorTracking,
|
stop: stopFriendCursorTracking,
|
||||||
} = createEventSource(async (addEventListener) => {
|
} = createEventSource(async (addEventListener) => {
|
||||||
let friendCursorState: Record<string, FriendCursorData> = {};
|
|
||||||
addEventListener(
|
addEventListener(
|
||||||
await events.friendCursorPositionUpdated.listen((event) => {
|
await events.friendCursorPositionsUpdated.listen((event) => {
|
||||||
const data: OutgoingFriendCursorPayload = event.payload;
|
friendsCursorPositions.set(toCursorPositionsRecord(event.payload));
|
||||||
|
|
||||||
friendCursorState[data.userId] = {
|
|
||||||
position: data.position,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
friendsCursorPositions.update((current) => {
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
[data.userId]: data.position,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventListener(
|
|
||||||
await events.friendDisconnected.listen((event) => {
|
|
||||||
const data = event.payload;
|
|
||||||
|
|
||||||
if (friendCursorState[data.userId]) {
|
|
||||||
delete friendCursorState[data.userId];
|
|
||||||
}
|
|
||||||
|
|
||||||
friendsCursorPositions.update((current) =>
|
|
||||||
removeFromStore(current, data.userId),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventListener(
|
|
||||||
await events.friendActiveDollChanged.listen((event) => {
|
|
||||||
const payload = event.payload;
|
|
||||||
|
|
||||||
if (!payload.doll) {
|
|
||||||
friendsActiveDolls.update((current) => {
|
|
||||||
const next = { ...current };
|
|
||||||
next[payload.friendId] = null;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
friendsCursorPositions.update((current) =>
|
|
||||||
removeFromStore(current, payload.friendId),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
friendsActiveDolls.update((current) => {
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
[payload.friendId]: payload.doll,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ cursorMoved: CursorMoved,
|
|||||||
editDoll: EditDoll,
|
editDoll: EditDoll,
|
||||||
friendActiveDollChanged: FriendActiveDollChanged,
|
friendActiveDollChanged: FriendActiveDollChanged,
|
||||||
friendCursorPositionUpdated: FriendCursorPositionUpdated,
|
friendCursorPositionUpdated: FriendCursorPositionUpdated,
|
||||||
|
friendCursorPositionsUpdated: FriendCursorPositionsUpdated,
|
||||||
friendDisconnected: FriendDisconnected,
|
friendDisconnected: FriendDisconnected,
|
||||||
friendRequestAccepted: FriendRequestAccepted,
|
friendRequestAccepted: FriendRequestAccepted,
|
||||||
friendRequestDenied: FriendRequestDenied,
|
friendRequestDenied: FriendRequestDenied,
|
||||||
@@ -149,6 +150,7 @@ cursorMoved: "cursor-moved",
|
|||||||
editDoll: "edit-doll",
|
editDoll: "edit-doll",
|
||||||
friendActiveDollChanged: "friend-active-doll-changed",
|
friendActiveDollChanged: "friend-active-doll-changed",
|
||||||
friendCursorPositionUpdated: "friend-cursor-position-updated",
|
friendCursorPositionUpdated: "friend-cursor-position-updated",
|
||||||
|
friendCursorPositionsUpdated: "friend-cursor-positions-updated",
|
||||||
friendDisconnected: "friend-disconnected",
|
friendDisconnected: "friend-disconnected",
|
||||||
friendRequestAccepted: "friend-request-accepted",
|
friendRequestAccepted: "friend-request-accepted",
|
||||||
friendRequestDenied: "friend-request-denied",
|
friendRequestDenied: "friend-request-denied",
|
||||||
@@ -183,6 +185,8 @@ export type EditDoll = string
|
|||||||
export type FriendActiveDollChanged = FriendActiveDollChangedPayload
|
export type FriendActiveDollChanged = FriendActiveDollChangedPayload
|
||||||
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
||||||
export type FriendCursorPositionUpdated = OutgoingFriendCursorPayload
|
export type FriendCursorPositionUpdated = OutgoingFriendCursorPayload
|
||||||
|
export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }>
|
||||||
|
export type FriendCursorPositionsUpdated = FriendCursorPositionsDto
|
||||||
export type FriendDisconnected = FriendDisconnectedPayload
|
export type FriendDisconnected = FriendDisconnectedPayload
|
||||||
export type FriendDisconnectedPayload = { userId: string }
|
export type FriendDisconnectedPayload = { userId: string }
|
||||||
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
||||||
|
|||||||
Reference in New Issue
Block a user