From a842dfa9fdb882b38bb2779faab286c73503c158 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 5 Aug 2026 18:18:11 +0300 Subject: [PATCH] web/api: fetch own vouch state for vouch button Signed-off-by: oppiliappan --- web/src/lib/api/graph.ts | 21 ++++++++++++++++++++- web/src/lib/components/profile/types.ts | 13 +++++++++++++ web/src/routes/[handle]/+layout.ts | 23 ++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/web/src/lib/api/graph.ts b/web/src/lib/api/graph.ts index e73b71181..521db916b 100644 --- a/web/src/lib/api/graph.ts +++ b/web/src/lib/api/graph.ts @@ -8,11 +8,12 @@ import { createClient } from "$lib/auth/agent"; import { ClientResponseError, type BobbinContext } from "./client"; import { jsonGet } from "./_request"; import { httpStatusFor } from "./load"; -import { rkeyFromUri } from "./uri"; +import { didFromUri, rkeyFromUri } from "./uri"; import type { VouchRecord } from "./records"; import type * as ShTangledFeedGetStar from "./lexicons/types/sh/tangled/feed/getStar"; import type * as ShTangledGraphFollow from "./lexicons/types/sh/tangled/graph/follow"; import type * as ShTangledGraphGetFollow from "./lexicons/types/sh/tangled/graph/getFollow"; +import type * as ShTangledGraphListNetworkVouches from "./lexicons/types/sh/tangled/graph/listNetworkVouches"; export type FollowRecord = ShTangledGraphFollow.Main; export type { VouchRecord }; @@ -146,3 +147,21 @@ export const putVouch = ( export const deleteVouch = (agent: OAuthUserAgent, subject: string): Promise => deleteGenericRecord(agent, VOUCH_COLLECTION, subject); + +export const getViewerVouch = async ( + ctx: BobbinContext, + viewer: string, + subject: string +): Promise => { + const { items } = await jsonGet( + ctx, + "sh.tangled.graph.listNetworkVouches", + { actor: viewer as Did, limit: 100 } satisfies ShTangledGraphListNetworkVouches.$params + ); + const match = items.find( + (item) => didFromUri(item.uri) === viewer && rkeyFromUri(item.uri) === subject + ); + if (!match) return null; + const value = match.value as VouchRecord; + return { kind: value.kind === "denounce" ? "denounce" : "vouch", reason: value.reason }; +}; diff --git a/web/src/lib/components/profile/types.ts b/web/src/lib/components/profile/types.ts index 06de652e0..c40267d8e 100644 --- a/web/src/lib/components/profile/types.ts +++ b/web/src/lib/components/profile/types.ts @@ -65,6 +65,19 @@ export interface VouchData { createdAt: string; } +// the viewer's own vouch on a profile, driving the vouch button state +export interface DirectVouch { + kind: "vouch" | "denounce"; + reason?: string; +} + +// an indirect vouch/denounce on a profile from someone other than the viewer +export interface NetworkVouch { + did: string; + handle?: string; + kind: "vouch" | "denounce"; +} + export type StarData = | { kind: "repo"; uri: string; createdAt: string; repo: RepoCardData } | { kind: "string"; uri: string; createdAt: string; ownerHandle: string; rkey: string }; diff --git a/web/src/routes/[handle]/+layout.ts b/web/src/routes/[handle]/+layout.ts index e722fcdd6..6cdd35f5c 100644 --- a/web/src/routes/[handle]/+layout.ts +++ b/web/src/routes/[handle]/+layout.ts @@ -14,7 +14,9 @@ import { import { toHttpError, httpStatusFor } from "$lib/api/load"; import { ClientResponseError } from "$lib/api/client"; import { rkeyFromUri } from "$lib/api/uri"; -import type { ProfileCounts } from "$lib/components/profile/types"; +import { getViewerVouch } from "$lib/api/graph"; +import { fetchVouchesPage } from "$lib/components/profile/pages"; +import type { ProfileCounts, DirectVouch, NetworkVouch } from "$lib/components/profile/types"; import type { LayoutLoad } from "./$types"; const REPOS: LinkSource = "sh.tangled.repo:subject"; @@ -82,6 +84,23 @@ export const load: LayoutLoad = async (event) => { viewerDid && viewerDid !== did ? viewerUriOf(resolved.data, did, FOLLOWERS) : null; const viewerFollowRkey = viewerFollowUri ? rkeyFromUri(viewerFollowUri) : viewerFollowUri; + // vouches on this profile: the viewer's owndrives the button state) and the + // rest of the network + let viewerVouch: DirectVouch | null = null; + let networkVouches: NetworkVouch[] = []; + if (did !== viewerDid) { + const [vouchPage, viewer] = await Promise.all([ + fetchVouchesPage(ctx, { did }).catch(() => null), + viewerDid ? getViewerVouch(ctx, viewerDid, did).catch(() => null) : Promise.resolve(null) + ]); + viewerVouch = viewer; + if (vouchPage) { + networkVouches = vouchPage.items + .filter((v) => v.direction === "incoming" && v.did !== viewerDid) + .map((v) => ({ did: v.did, handle: v.handle, kind: v.kind })); + } + } + const notJoined = !profile && Object.values(counts).every((n) => n === 0); return { @@ -89,6 +108,8 @@ export const load: LayoutLoad = async (event) => { profile, counts, viewerFollowRkey, + viewerVouch, + networkVouches, notJoined }; }; -- 2.51.2