fix(desktop): land live presence updates for not-yet-cached pubkeys (#947)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-10 16:58:12 +00:00
committed by GitHub
co-authored by Brain
parent 5c0af0bc93
commit 34c8bdab1a
3 changed files with 135 additions and 23 deletions
+19 -22
View File
@@ -4,6 +4,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { relayClient } from "@/shared/api/relayClient";
import { getPresence } from "@/shared/api/tauri";
import { normalizePubkey } from "@/shared/lib/pubkey";
import {
mergePresenceUpdate,
parseLivePresenceEvent,
presenceQueryWantsPubkey,
} from "@/features/presence/lib/presence";
import type { PresenceLookup, PresenceStatus } from "@/shared/api/types";
const PRESENCE_HEARTBEAT_INTERVAL_MS = 30_000;
@@ -103,25 +108,18 @@ export function usePresenceSubscription() {
let isCancelled = false;
let retryTimer: ReturnType<typeof setTimeout> | null = null;
function handlePresenceEvent(event: {
pubkey: string;
content: string;
tags?: string[][];
}) {
function handlePresenceEvent(event: { pubkey: string; content: string }) {
if (isCancelled) return;
const status = event.content;
if (status !== "online" && status !== "away" && status !== "offline")
return;
const pubkey = (
event.tags?.find((t) => t[0] === "p")?.[1] ?? event.pubkey
).toLowerCase();
const parsed = parseLivePresenceEvent(event);
if (!parsed) return;
const { pubkey, status } = parsed;
queryClient.setQueriesData<PresenceLookup>(
{ queryKey: ["presence"] },
(old) => {
if (!old || !(pubkey in old)) return old;
if (old[pubkey] === status) return old;
return { ...old, [pubkey]: status };
{
queryKey: ["presence"],
predicate: (query) =>
presenceQueryWantsPubkey(query.queryKey, pubkey),
},
(old) => mergePresenceUpdate(old, pubkey, status),
);
}
@@ -176,14 +174,13 @@ export function useSetPresenceMutation(pubkey?: string) {
},
onSuccess: ({ status }) => {
if (normalizedPubkey.length === 0) return;
// Update all cached presence queries containing this pubkey.
queryClient.setQueriesData<PresenceLookup>(
{ queryKey: ["presence"] },
(old) => {
if (!old || !(normalizedPubkey in old)) return old;
if (old[normalizedPubkey] === status) return old;
return { ...old, [normalizedPubkey]: status };
{
queryKey: ["presence"],
predicate: (query) =>
presenceQueryWantsPubkey(query.queryKey, normalizedPubkey),
},
(old) => mergePresenceUpdate(old, normalizedPubkey, status),
);
},
});
@@ -0,0 +1,79 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
mergePresenceUpdate,
parseLivePresenceEvent,
presenceQueryWantsPubkey,
} from "./presence.ts";
const WILL = "8e39cba681211b3782d0e4483e9343719b9b7be66515252da5491f26421896b1";
const OTHER =
"44b8e82baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
test("merge adds an absent pubkey going online (the core bug)", () => {
const old = {};
const next = mergePresenceUpdate(old, WILL, "online");
assert.deepEqual(next, { [WILL]: "online" });
});
test("merge updates an existing pubkey", () => {
const next = mergePresenceUpdate({ [WILL]: "offline" }, WILL, "online");
assert.deepEqual(next, { [WILL]: "online" });
});
test("merge returns same reference when status is unchanged", () => {
const old = { [WILL]: "online" };
assert.equal(mergePresenceUpdate(old, WILL, "online"), old);
});
test("merge leaves other pubkeys untouched", () => {
const next = mergePresenceUpdate({ [OTHER]: "away" }, WILL, "online");
assert.deepEqual(next, { [OTHER]: "away", [WILL]: "online" });
});
test("merge is a no-op on an undefined cache", () => {
assert.equal(mergePresenceUpdate(undefined, WILL, "online"), undefined);
});
test("query wants a pubkey it requested", () => {
assert.equal(presenceQueryWantsPubkey(["presence", WILL, OTHER], WILL), true);
});
test("query does not want a pubkey it did not request", () => {
assert.equal(presenceQueryWantsPubkey(["presence", OTHER], WILL), false);
});
test("bare presence key (no pubkeys) wants nothing", () => {
assert.equal(presenceQueryWantsPubkey(["presence"], WILL), false);
});
test("live event keys off the author, not a p tag", () => {
const event = { pubkey: OTHER, content: "online", tags: [["p", WILL]] };
assert.deepEqual(parseLivePresenceEvent(event), {
pubkey: OTHER,
status: "online",
});
});
test("spoof attempt cannot mark a victim: foreign p tag is ignored", () => {
const event = { pubkey: OTHER, content: "offline", tags: [["p", WILL]] };
const parsed = parseLivePresenceEvent(event);
assert.notEqual(parsed.pubkey, WILL);
assert.equal(parsed.pubkey, OTHER);
});
test("live event with unknown status is rejected", () => {
assert.equal(
parseLivePresenceEvent({ pubkey: WILL, content: "lurking" }),
null,
);
});
test("live event lowercases the author pubkey", () => {
const parsed = parseLivePresenceEvent({
pubkey: WILL.toUpperCase(),
content: "away",
});
assert.equal(parsed.pubkey, WILL);
});
+37 -1
View File
@@ -1,4 +1,40 @@
import type { PresenceStatus } from "@/shared/api/types";
import type { PresenceLookup, PresenceStatus } from "@/shared/api/types";
// Live kind:20001 events are self-signed by their author; the subject is
// always the event author. A p tag is NOT trusted here — a client could forge
// one to spoof another user. The relay-signed REST/seed path is the only place
// a p-tag subject is trusted. Returns null for unknown statuses.
export function parseLivePresenceEvent(event: {
pubkey: string;
content: string;
}): { pubkey: string; status: PresenceStatus } | null {
const status = event.content;
if (status !== "online" && status !== "away" && status !== "offline") {
return null;
}
return { pubkey: event.pubkey.toLowerCase(), status };
}
// Presence query keys are ["presence", ...normalizedSortedPubkeys]; a query
// "wants" an update only for a pubkey it actually requested.
export function presenceQueryWantsPubkey(
queryKey: readonly unknown[],
pubkey: string,
): boolean {
return queryKey.length > 1 && queryKey.includes(pubkey);
}
// get_presence omits offline/unknown pubkeys, so a live online event often
// targets a pubkey absent from the lookup — merge it in rather than dropping it.
export function mergePresenceUpdate(
old: PresenceLookup | undefined,
pubkey: string,
status: PresenceStatus,
): PresenceLookup | undefined {
if (!old) return old;
if (old[pubkey] === status) return old;
return { ...old, [pubkey]: status };
}
export function getPresenceLabel(status: PresenceStatus) {
switch (status) {