Add desktop presence UI (#35)

This commit is contained in:
Wes
2026-03-11 15:26:01 -07:00
committed by GitHub
parent 397564b434
commit d94e3fbf77
18 changed files with 1072 additions and 149 deletions
+45 -21
View File
@@ -5,7 +5,7 @@ import {
getRelayWsUrl,
signRelayEvent,
} from "@/shared/api/tauri";
import type { RelayEvent } from "@/shared/api/types";
import type { PresenceStatus, RelayEvent } from "@/shared/api/types";
type RelaySubscriptionFilter = {
kinds: number[];
@@ -125,29 +125,27 @@ class RelayClient {
tags: [["h", channelId]],
});
return new Promise<RelayEvent>((resolve, reject) => {
const timeout = window.setTimeout(() => {
this.pendingEvents.delete(event.id);
reject(new Error("Timed out while sending the message."));
}, 8_000);
return this.publishEvent(
event,
"Timed out while sending the message.",
"Failed to send the message.",
);
}
this.pendingEvents.set(event.id, {
event,
resolve,
reject,
timeout,
});
async sendPresence(status: PresenceStatus) {
await this.ensureConnected();
void this.sendRaw(["EVENT", event]).catch((error) => {
window.clearTimeout(timeout);
this.pendingEvents.delete(event.id);
reject(
error instanceof Error
? error
: new Error("Failed to send the message."),
);
});
const event = await signRelayEvent({
kind: 20001,
content: status,
tags: [],
});
return this.publishEvent(
event,
"Timed out while updating presence.",
"Failed to update presence.",
);
}
async subscribeToChannel(
@@ -260,6 +258,32 @@ class RelayClient {
await this.sendRaw(["CLOSE", subId]);
}
private publishEvent(
event: RelayEvent,
timeoutMessage: string,
sendErrorMessage: string,
) {
return new Promise<RelayEvent>((resolve, reject) => {
const timeout = window.setTimeout(() => {
this.pendingEvents.delete(event.id);
reject(new Error(timeoutMessage));
}, 8_000);
this.pendingEvents.set(event.id, {
event,
resolve,
reject,
timeout,
});
void this.sendRaw(["EVENT", event]).catch((error) => {
window.clearTimeout(timeout);
this.pendingEvents.delete(event.id);
reject(error instanceof Error ? error : new Error(sendErrorMessage));
});
});
}
private async handleWsMessage(message: unknown) {
if (
typeof message === "object" &&
+36
View File
@@ -11,10 +11,13 @@ import type {
GetHomeFeedInput,
HomeFeedResponse,
Identity,
PresenceLookup,
PresenceStatus,
Profile,
RelayEvent,
SearchMessagesInput,
SearchMessagesResponse,
SetPresenceResult,
SetChannelPurposeInput,
SetChannelTopicInput,
UpdateProfileInput,
@@ -46,6 +49,13 @@ type RawUsersBatchResponse = {
missing: string[];
};
type RawPresenceLookup = Record<string, PresenceStatus>;
type RawSetPresenceResult = {
status: PresenceStatus;
ttl_seconds: number;
};
type RawChannel = {
id: string;
name: string;
@@ -268,6 +278,32 @@ export async function getUsersBatch(
};
}
export async function getPresence(pubkeys: string[]): Promise<PresenceLookup> {
const response = await invoke<RawPresenceLookup>("get_presence", {
pubkeys,
});
return Object.fromEntries(
Object.entries(response).map(([pubkey, status]) => [
pubkey.toLowerCase(),
status,
]),
);
}
export async function setPresence(
status: PresenceStatus,
): Promise<SetPresenceResult> {
const response = await invoke<RawSetPresenceResult>("set_presence", {
status,
});
return {
status: response.status,
ttlSeconds: response.ttl_seconds,
};
}
export function getRelayWsUrl(): Promise<string> {
return invoke<string>("get_relay_ws_url");
}
+9
View File
@@ -104,6 +104,15 @@ export type UpdateProfileInput = {
nip05Handle?: string;
};
export type PresenceStatus = "online" | "away" | "offline";
export type PresenceLookup = Record<string, PresenceStatus>;
export type SetPresenceResult = {
status: PresenceStatus;
ttlSeconds: number;
};
export type RelayEvent = {
id: string;
pubkey: string;