friends connection status system

This commit is contained in:
2026-08-26 21:44:53 +08:00
parent 74a5268420
commit 5916de9a91
15 changed files with 722 additions and 56 deletions
+6
View File
@@ -44,6 +44,9 @@ async getPublicKey() : Promise<string> {
async listStatuses() : Promise<ConnectionStatus[]> {
return await TAURI_INVOKE("list_statuses");
},
async listFriendStatuses() : Promise<string[]> {
return await TAURI_INVOKE("list_friend_statuses");
},
async pickAndSendImage(recipientId: string) : Promise<boolean> {
return await TAURI_INVOKE("pick_and_send_image", { recipientId });
},
@@ -66,6 +69,7 @@ cursorPositionChanged: CursorPositionChanged,
foregroundAppChanged: ForegroundAppChanged,
friendForegroundAppChanged: FriendForegroundAppChanged,
friendInteractionReceived: FriendInteractionReceived,
friendStatusesChanged: FriendStatusesChanged,
friendsChanged: FriendsChanged,
networkStatusChanged: NetworkStatusChanged,
profileChanged: ProfileChanged,
@@ -75,6 +79,7 @@ cursorPositionChanged: "cursor-position-changed",
foregroundAppChanged: "foreground-app-changed",
friendForegroundAppChanged: "friend-foreground-app-changed",
friendInteractionReceived: "friend-interaction-received",
friendStatusesChanged: "friend-statuses-changed",
friendsChanged: "friends-changed",
networkStatusChanged: "network-status-changed",
profileChanged: "profile-changed",
@@ -107,6 +112,7 @@ mapped: CursorPosition }
export type ForegroundAppChanged = { meta: AppMeta }
export type FriendForegroundAppChanged = { friendId: string; meta: AppMeta }
export type FriendInteractionReceived = { interactionId: string; friendId: string; content: InteractionContent }
export type FriendStatusesChanged = { friendIds: string[] }
export type FriendsChanged = { friends: User[] }
export type InteractionContent = { type: "text"; text: string } | { type: "wave" } | { type: "image"; mediaType: string; data: string }
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
import {
friendStatusesListenerError,
onlineFriendIds,
} from "$lib/listeners/friend-statuses";
import { friends, friendsListenerError } from "$lib/listeners/friends";
</script>
<section>
<h1>Friend statuses</h1>
{#if $friendsListenerError || $friendStatusesListenerError}
<p aria-live="polite">
{$friendsListenerError || $friendStatusesListenerError}
</p>
{:else if $friends.length === 0}
<p>No friends configured.</p>
{:else}
<ul aria-live="polite">
{#each $friends as friend (friend.id)}
{@const online = $onlineFriendIds.has(friend.id)}
<li>
<strong>{friend.displayName}</strong>: {online ? "Online" : "Offline"}
<small>({friend.id})</small>
</li>
{/each}
</ul>
{/if}
</section>
+31
View File
@@ -0,0 +1,31 @@
import { writable } from "svelte/store";
import { commands, events } from "$lib/bindings";
import { removeFriendForegroundApps } 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);
};
const unlisten = await events.friendStatusesChanged.listen((event) => {
apply(event.payload.friendIds);
});
try {
apply(await commands.listFriendStatuses());
} catch (error) {
friendStatusesListenerError.set(String(error));
}
return unlisten;
}
+2
View File
@@ -1,4 +1,5 @@
import { initConnectionStatusesListener } from "./connection-status";
import { initFriendStatusesListener } from "./friend-statuses";
import { initFriendsListener } from "./friends";
import { initInteractionListener } from "./interactions";
import { initLiveMetadataListeners } from "./live-metadata";
@@ -16,6 +17,7 @@ export function initAppListeners(): Unlisten {
initRemotesListener(),
initProfileListener(),
initConnectionStatusesListener(),
initFriendStatusesListener(),
initLiveMetadataListeners(),
initInteractionListener(),
])
+11
View File
@@ -19,6 +19,17 @@ export const liveMetadata = writable<LiveMetadata>({
});
export const liveMetadataListenerError = writable("");
export function removeFriendForegroundApps(friendIds: string[]) {
if (friendIds.length === 0) return;
const removed = new Set(friendIds);
liveMetadata.update((metadata) => ({
...metadata,
foregroundApps: new Map(
[...metadata.foregroundApps].filter(([userId]) => !removed.has(userId)),
),
}));
}
export async function initLiveMetadataListeners() {
let localId = "";
let pendingLocalForegroundApp: AppMeta | null = null;
+4
View File
@@ -1,5 +1,6 @@
<script>
import FriendsList from "$lib/components/friends-list.svelte";
import FriendStatuses from "$lib/components/friend-statuses.svelte";
import ConnectionStatus from "$lib/components/connection-status.svelte";
import Profile from "$lib/components/profile.svelte";
import RemotesList from "$lib/components/remotes-list.svelte";
@@ -13,6 +14,9 @@
<div>
<FriendsList />
</div>
<div>
<FriendStatuses />
</div>
<div>
<RemotesList />
</div>
+14 -1
View File
@@ -2,6 +2,7 @@
import { onMount } from "svelte";
import Popover from "$lib/components/popover.svelte";
import { friends } from "$lib/listeners/friends";
import { onlineFriendIds } from "$lib/listeners/friend-statuses";
import { incomingInteraction } from "$lib/listeners/interactions";
import { liveMetadata } from "$lib/listeners/live-metadata";
import { profile } from "$lib/listeners/profile";
@@ -28,6 +29,18 @@
return () => window.clearTimeout(timer);
});
$effect(() => {
if (
selectedUserId &&
selectedUserId !== $liveMetadata.localId &&
!$onlineFriendIds.has(selectedUserId)
) {
selectedUserId = null;
selectedUserX = null;
lockedPopoverUserId = null;
}
});
function displayName(userId: string) {
return $profile?.id === userId
? $profile.displayName
@@ -65,7 +78,7 @@
{@const popoverId = `scene-user-${index}`}
{@const interaction =
$incomingInteraction?.friendId === userId ? $incomingInteraction : null}
{#if cursor}
{#if cursor && (userId === $liveMetadata.localId || $onlineFriendIds.has(userId))}
{@const renderedX =
selectedUserId === userId ? selectedUserX : cursor.mapped.x}
<div