From 2d144fbc9ab5253200c5fc9125799828954ba136 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 15 Jul 2026 17:55:56 +0100 Subject: [PATCH] bobbin: add listNetworkVouches --- bobbin/crates/knot-proxy/src/mirror.rs | 2 + bobbin/crates/resolver/src/legacy_upgrade.rs | 1 + bobbin/crates/xrpc/src/filter.rs | 23 ++- bobbin/crates/xrpc/src/lib.rs | 150 ++++++++++++++---- lexicons/graph/listNetworkVouches.json | 57 +++++++ .../lib/components/profile/ProfileCard.svelte | 3 +- .../lib/components/profile/VouchButton.svelte | 34 ++-- .../components/profile/tabs/VouchTab.svelte | 62 ++++++-- web/src/lib/oauth-client-metadata.json | 2 +- web/src/routes/[handle]/+layout.svelte | 41 ++++- web/src/routes/[handle]/+layout.ts | 28 ++-- web/src/routes/[handle]/+page.svelte | 8 +- web/src/routes/[handle]/+page.ts | 5 +- web/src/routes/timeline/+page.svelte | 4 +- 14 files changed, 319 insertions(+), 101 deletions(-) create mode 100644 lexicons/graph/listNetworkVouches.json diff --git a/bobbin/crates/knot-proxy/src/mirror.rs b/bobbin/crates/knot-proxy/src/mirror.rs index 96ac9234..648f85e9 100644 --- a/bobbin/crates/knot-proxy/src/mirror.rs +++ b/bobbin/crates/knot-proxy/src/mirror.rs @@ -27,6 +27,8 @@ pub enum MirrorProxyError { Host(#[from] KnotHostError), #[error("mirror http client: {0}")] Http(#[from] reqwest::Error), + #[error("mirror proxy: {0}")] + Proxy(#[from] crate::KnotProxyBuildError), #[error("mirror url must be a bare origin, got {0}")] NotAnOrigin(String), } diff --git a/bobbin/crates/resolver/src/legacy_upgrade.rs b/bobbin/crates/resolver/src/legacy_upgrade.rs index 51ba684b..fffc1540 100644 --- a/bobbin/crates/resolver/src/legacy_upgrade.rs +++ b/bobbin/crates/resolver/src/legacy_upgrade.rs @@ -382,6 +382,7 @@ async fn upgrade_pull( l.rounds }; Some(Pull { + blobs: None, created_at: l.created_at, body: l.body, dependent_on: l.dependent_on, diff --git a/bobbin/crates/xrpc/src/filter.rs b/bobbin/crates/xrpc/src/filter.rs index c8d0529b..2d64c9dd 100644 --- a/bobbin/crates/xrpc/src/filter.rs +++ b/bobbin/crates/xrpc/src/filter.rs @@ -1,4 +1,6 @@ -use bobbin_edge_index::{FilteredCount, IssueStateKind, PullStatusKind}; +use std::collections::HashSet; + +use bobbin_edge_index::{EdgeItem, FilteredCount, IssueStateKind, PullStatusKind}; use bobbin_types::ids::EdgeKey; use jacquard_common::DefaultStr; use jacquard_common::types::did::Did; @@ -153,3 +155,22 @@ fn count_by_author( ), } } + +pub struct VouchNetworkFilter { + network: HashSet, +} + +impl VouchNetworkFilter { + pub fn new(network: HashSet) -> Self { + Self { network } + } + + pub fn contains(&self, did: &str) -> bool { + self.network.contains(did) + } + + pub fn matches(&self, item: &EdgeItem) -> bool { + crate::source_authority_did(&item.uri) + .is_some_and(|did| self.network.contains(did.as_ref())) + } +} diff --git a/bobbin/crates/xrpc/src/lib.rs b/bobbin/crates/xrpc/src/lib.rs index 89dddec0..ccc8d08e 100644 --- a/bobbin/crates/xrpc/src/lib.rs +++ b/bobbin/crates/xrpc/src/lib.rs @@ -50,6 +50,7 @@ use bobbin_types::sh_tangled::feed::reaction::{Reaction, ReactionRecord}; use bobbin_types::sh_tangled::feed::star::{Star, StarRecord}; use bobbin_types::sh_tangled::git::ref_update::{RefUpdate, RefUpdateRecord}; use bobbin_types::sh_tangled::graph::follow::{Follow, FollowRecord}; +use bobbin_types::sh_tangled::graph::list_network_vouches::ListNetworkVouches; use bobbin_types::sh_tangled::graph::vouch::{Vouch, VouchRecord}; use bobbin_types::sh_tangled::knot::member::{ Member as KnotMember, MemberRecord as KnotMemberRecord, @@ -84,7 +85,7 @@ use bobbin_types::sh_tangled::string::{ }; use futures::Stream; use futures::stream::{self, StreamExt, TryStreamExt}; -use jacquard_axum::service_auth::{self, ServiceAuthConfig}; +use jacquard_axum::service_auth::{self, ExtractOptionalServiceAuth, ServiceAuthConfig}; use jacquard_common::types::did::Did; use jacquard_common::types::ident::AtIdentifier; use jacquard_common::types::nsid::Nsid; @@ -118,7 +119,9 @@ pub use backpressure::{ }; use client_address::X_FORWARDED_FOR; pub use client_address::{ClientAddress, SocketPeer}; -use filter::{CountFilter, IssueFilter, ListFilter, NoFilter, PullFilter, StateViewQuery}; +use filter::{ + CountFilter, IssueFilter, ListFilter, NoFilter, PullFilter, StateViewQuery, VouchNetworkFilter, +}; use trusted_proxies::TrustedProxies; const DEFAULT_LIMIT: u32 = 50; @@ -361,9 +364,11 @@ pub fn router(state: AppState) -> Router { "/xrpc/sh.tangled.publicKey.countKeys", get(count_public_keys), ) - .route("/xrpc/sh.tangled.graph.listVouches", get(list_vouches)) - .route("/xrpc/sh.tangled.graph.countVouches", get(count_vouches)) .route("/xrpc/sh.tangled.feed.getTimeline", get(feed::get_timeline)) + .route( + "/xrpc/sh.tangled.graph.listNetworkVouches", + get(list_network_vouches), + ) .route("/xrpc/sh.tangled.feed.listStarsBy", get(list_stars_by)) .route("/xrpc/sh.tangled.feed.countStarsBy", get(count_stars_by)) .route( @@ -379,11 +384,6 @@ pub fn router(state: AppState) -> Router { "/xrpc/sh.tangled.graph.countFollowsBy", get(count_follows_by), ) - .route("/xrpc/sh.tangled.graph.listVouchesBy", get(list_vouches_by)) - .route( - "/xrpc/sh.tangled.graph.countVouchesBy", - get(count_vouches_by), - ) .route( "/xrpc/sh.tangled.git.listRefUpdatesBy", get(list_ref_updates_by), @@ -2370,18 +2370,119 @@ async fn count_public_keys( count_for::(&state, q).map(Json) } -async fn list_vouches( - State(state): State, - XrpcQuery(q): XrpcQuery>, -) -> Result { - list_records::, _>(&state, q).await +fn vouch_network(state: &AppState, actor: &SubjectRef) -> std::collections::HashSet { + let vouch_by = nsid_static("sh.tangled.graph.vouch.by"); + state + .edges + .sources_for(&EdgeKey::new(vouch_by, actor.clone())) + .iter() + .filter_map(|uri| Did::::new_owned(uri.rkey()?.as_ref()).ok()) + .map(|did| did.as_ref().to_string()) + .collect() +} + +fn vouches_received( + state: &AppState, + actor: &SubjectRef, + network: &VouchNetworkFilter, +) -> Vec { + let vouch = nsid_static("sh.tangled.graph.vouch"); + state + .edges + .list( + &EdgeKey::new(vouch, actor.clone()), + PageCursor::Start, + PageLimit::new(PageLimit::MAX).unwrap(), + SortDir::Desc, + ) + .items + .into_iter() + .filter(|item| network.matches(item)) + .collect() +} + +fn vouches_sent(state: &AppState, actor: &SubjectRef) -> Vec { + let vouch_by = nsid_static("sh.tangled.graph.vouch.by"); + state + .edges + .list( + &EdgeKey::new(vouch_by, actor.clone()), + PageCursor::Start, + PageLimit::new(PageLimit::MAX).unwrap(), + SortDir::Desc, + ) + .items +} + +fn paginate_vouches( + candidates: impl Iterator, + cursor: Option<&str>, + limit: PageLimit, +) -> (Vec, Option) { + let mut seen = std::collections::HashSet::new(); + let mut combined: Vec = candidates + .filter(|item| seen.insert(item.uri.as_ref().to_string())) + .collect(); + combined.sort_by(|a, b| b.sort_micros.cmp(&a.sort_micros)); + + let start = match cursor.and_then(|s| s.parse::().ok()) { + None => 0, + Some(micros) => combined + .iter() + .position(|item| item.sort_micros < micros) + .unwrap_or(combined.len()), + }; + let limit = limit.get() as usize; + let has_more = combined.len() > start + limit; + let items: Vec = combined.into_iter().skip(start).take(limit).collect(); + let next = has_more + .then(|| items.last().map(|item| item.sort_micros.to_string())) + .flatten(); + (items, next) } -async fn count_vouches( +async fn list_network_vouches( State(state): State, - XrpcQuery(q): XrpcQuery, -) -> Result, XrpcError> { - count_for::(&state, q).map(Json) + ExtractOptionalServiceAuth(auth): ExtractOptionalServiceAuth, + XrpcQuery(q): XrpcQuery, +) -> Result { + let actor = SubjectRef::Did(q.actor.clone()); + let limit = parse_limit(q.limit.and_then(|n| u32::try_from(n).ok()))?; + let vouch_nsid = nsid_static("sh.tangled.graph.vouch"); + + let viewer = auth + .as_ref() + .map(|auth| SubjectRef::Did(auth.did().into_static())); + let network = VouchNetworkFilter::new( + viewer + .as_ref() + .map(|viewer| vouch_network(&state, viewer)) + .unwrap_or_default(), + ); + let received = vouches_received(&state, &actor, &network); + + let sent = if network.contains(q.actor.as_ref()) { + vouches_sent(&state, &actor) + } else { + Vec::new() + }; + + let (page_items, next_cursor) = + paginate_vouches(received.into_iter().chain(sent), q.cursor.as_deref(), limit); + + let permit = state.heavy_permit()?; + let views = hydrate_record_stream::>( + &state, + vouch_nsid, + page_items, + HitProvenance::Indexed, + ); + Ok(json_stream::>, _>( + "items", + views, + paged_tail(next_cursor, None), + permit, + )) } async fn list_stars_by( @@ -2423,19 +2524,6 @@ async fn count_follows_by( count_mirror::(&state, q).map(Json) } -async fn list_vouches_by( - State(state): State, - XrpcQuery(q): XrpcQuery>, -) -> Result { - list_mirror::, _>(&state, q).await -} -async fn count_vouches_by( - State(state): State, - XrpcQuery(q): XrpcQuery, -) -> Result, XrpcError> { - count_mirror::(&state, q).map(Json) -} - async fn list_ref_updates_by( State(state): State, XrpcQuery(q): XrpcQuery>, diff --git a/lexicons/graph/listNetworkVouches.json b/lexicons/graph/listNetworkVouches.json new file mode 100644 index 00000000..92f8473d --- /dev/null +++ b/lexicons/graph/listNetworkVouches.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "sh.tangled.graph.listNetworkVouches", + "defs": { + "main": { + "type": "query", + "description": "List vouches involving an actor as seen through a viewer's vouch network. Requires authentication.", + "parameters": { + "type": "params", + "required": ["actor"], + "properties": { + "actor": { + "type": "string", + "format": "did", + "description": "The DID of the profile whose network vouches to list." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "type": "ref", "ref": "#listItem" } + }, + "cursor": { "type": "string" } + } + } + } + }, + "listItem": { + "type": "object", + "required": ["uri", "value"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.graph.vouch record" + } + } + } + } +} diff --git a/web/src/lib/components/profile/ProfileCard.svelte b/web/src/lib/components/profile/ProfileCard.svelte index f57585ac..a3669af5 100644 --- a/web/src/lib/components/profile/ProfileCard.svelte +++ b/web/src/lib/components/profile/ProfileCard.svelte @@ -41,6 +41,7 @@ const auth = getAuth(); const isSelf = $derived(auth.currentDid === identity.did); + const signedIn = $derived(Boolean(auth.currentDid)); let editing = $state(false); let saved = $state(null); @@ -140,7 +141,7 @@ - {#if !isSelf || networkVouches.length > 0} + {#if (!isSelf && signedIn) || networkVouches.length > 0}
{#if !isSelf} - import { resolve } from "$app/paths"; import Button from "$lib/components/ui/Button.svelte"; import ShieldCheck from "$icon/shield-check"; import ShieldBan from "$icon/shield-ban"; @@ -105,27 +104,17 @@ }); -{#if !isSelf} - {#if !signedIn} - - {:else} - +{#if !isSelf && signedIn} +
- {/if} {/if} diff --git a/web/src/lib/components/profile/tabs/VouchTab.svelte b/web/src/lib/components/profile/tabs/VouchTab.svelte index 028008c7..fa963fb5 100644 --- a/web/src/lib/components/profile/tabs/VouchTab.svelte +++ b/web/src/lib/components/profile/tabs/VouchTab.svelte @@ -1,27 +1,63 @@ -
-
- {#each initial as vouch (vouch.uri)} - - {/each} -
+{#if !auth.agent} + +{:else if failed} + +{:else if vouches} +
+
+ {#each vouches as vouch (vouch.uri)} + + {/each} +
+{/if} diff --git a/web/src/lib/oauth-client-metadata.json b/web/src/lib/oauth-client-metadata.json index 1cd7fe51..a1bd3401 100644 --- a/web/src/lib/oauth-client-metadata.json +++ b/web/src/lib/oauth-client-metadata.json @@ -3,7 +3,7 @@ "client_name": "Tangled", "client_uri": "https://tangled.org", "redirect_uris": ["https://tangled.org/oauth/callback"], - "scope": "atproto repo:sh.tangled.actor.profile repo:sh.tangled.feed.comment repo:sh.tangled.feed.reaction repo:sh.tangled.feed.star repo:sh.tangled.graph.follow repo:sh.tangled.graph.vouch repo:sh.tangled.knot repo:sh.tangled.knot.member repo:sh.tangled.label.definition repo:sh.tangled.label.op repo:sh.tangled.publicKey repo:sh.tangled.repo repo:sh.tangled.repo.artifact repo:sh.tangled.repo.collaborator repo:sh.tangled.repo.issue repo:sh.tangled.repo.issue.comment repo:sh.tangled.repo.issue.state repo:sh.tangled.repo.pull repo:sh.tangled.repo.pull.comment repo:sh.tangled.repo.pull.status repo:sh.tangled.spindle repo:sh.tangled.spindle.member repo:sh.tangled.string blob:*/* rpc:sh.tangled.knot.addMember?aud=* rpc:sh.tangled.knot.removeMember?aud=* rpc:sh.tangled.ci.triggerPipeline?aud=* rpc:sh.tangled.ci.cancelPipeline?aud=* rpc:sh.tangled.repo.addCollaborator?aud=* rpc:sh.tangled.repo.addSecret?aud=* rpc:sh.tangled.repo.create?aud=* rpc:sh.tangled.repo.delete?aud=* rpc:sh.tangled.repo.deleteBranch?aud=* rpc:sh.tangled.repo.forkStatus?aud=* rpc:sh.tangled.repo.forkSync?aud=* rpc:sh.tangled.repo.hiddenRef?aud=* rpc:sh.tangled.repo.listSecrets?aud=* rpc:sh.tangled.repo.merge?aud=* rpc:sh.tangled.repo.mergeCheck?aud=* rpc:sh.tangled.repo.removeCollaborator?aud=* rpc:sh.tangled.repo.removeSecret?aud=* rpc:sh.tangled.repo.setDefaultBranch?aud=* rpc:org.tangled.temp.notification.getPreferences?aud=* rpc:org.tangled.temp.notification.updatePreferences?aud=* rpc:org.tangled.temp.site.getDomainClaim?aud=* rpc:org.tangled.temp.site.claimDomain?aud=* rpc:org.tangled.temp.site.releaseDomain?aud=* rpc:sh.tangled.git.keepCommit?aud=*", + "scope": "atproto repo:sh.tangled.actor.profile repo:sh.tangled.feed.comment repo:sh.tangled.feed.reaction repo:sh.tangled.feed.star repo:sh.tangled.graph.follow repo:sh.tangled.graph.vouch repo:sh.tangled.knot repo:sh.tangled.knot.member repo:sh.tangled.label.definition repo:sh.tangled.label.op repo:sh.tangled.publicKey repo:sh.tangled.repo repo:sh.tangled.repo.artifact repo:sh.tangled.repo.collaborator repo:sh.tangled.repo.issue repo:sh.tangled.repo.issue.comment repo:sh.tangled.repo.issue.state repo:sh.tangled.repo.pull repo:sh.tangled.repo.pull.comment repo:sh.tangled.repo.pull.status repo:sh.tangled.spindle repo:sh.tangled.spindle.member repo:sh.tangled.string blob:*/* rpc:sh.tangled.graph.listNetworkVouches?aud=* rpc:sh.tangled.knot.addMember?aud=* rpc:sh.tangled.knot.removeMember?aud=* rpc:sh.tangled.ci.triggerPipeline?aud=* rpc:sh.tangled.ci.cancelPipeline?aud=* rpc:sh.tangled.repo.addCollaborator?aud=* rpc:sh.tangled.repo.addSecret?aud=* rpc:sh.tangled.repo.create?aud=* rpc:sh.tangled.repo.delete?aud=* rpc:sh.tangled.repo.deleteBranch?aud=* rpc:sh.tangled.repo.forkStatus?aud=* rpc:sh.tangled.repo.forkSync?aud=* rpc:sh.tangled.repo.hiddenRef?aud=* rpc:sh.tangled.repo.listSecrets?aud=* rpc:sh.tangled.repo.merge?aud=* rpc:sh.tangled.repo.mergeCheck?aud=* rpc:sh.tangled.repo.removeCollaborator?aud=* rpc:sh.tangled.repo.removeSecret?aud=* rpc:sh.tangled.repo.setDefaultBranch?aud=* rpc:org.tangled.temp.notification.getPreferences?aud=* rpc:org.tangled.temp.notification.updatePreferences?aud=* rpc:org.tangled.temp.site.getDomainClaim?aud=* rpc:org.tangled.temp.site.claimDomain?aud=* rpc:org.tangled.temp.site.releaseDomain?aud=* rpc:sh.tangled.git.keepCommit?aud=*", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", diff --git a/web/src/routes/[handle]/+layout.svelte b/web/src/routes/[handle]/+layout.svelte index 259f0737..e23bf94f 100644 --- a/web/src/routes/[handle]/+layout.svelte +++ b/web/src/routes/[handle]/+layout.svelte @@ -6,6 +6,11 @@ import ProfileCard from "$lib/components/profile/ProfileCard.svelte"; import ProfileTabs from "$lib/components/profile/ProfileTabs.svelte"; import TabPanel from "$lib/components/ui/TabPanel.svelte"; + import { getAuth } from "$lib/auth.svelte"; + import { createBobbinClient } from "$lib/api/client"; + import { IdentityCache } from "$lib/api/identity"; + import { fetchVouchesPage } from "$lib/components/profile/pages"; + import type { NetworkVouch } from "$lib/components/profile/types"; let { children, data } = $props(); @@ -15,6 +20,40 @@ ); setContext(PROFILE_COUNTS_KEY, profileCounts); const counts = $derived(profileCounts.value); + + // network vouches are viewer-specific: bobbin derives the trust network from + // the authenticated viewer, so this is a service-auth'd call that can only run + // client-side (the oauth agent lives in the browser). anonymous viewers have no + // network and see nothing. identity resolution stays on an unauthed client since + // it's public and would otherwise need its own rpc scope. + const auth = getAuth(); + let networkVouches = $state([]); + + $effect(() => { + const agent = auth.agent; + const did = data.identity.did; + const viewerDid = auth.currentDid; + networkVouches = []; + if (!agent) return; + + let cancelled = false; + const bobbinUrl = data.publicConfig.bobbinUrl; + const ctx = createBobbinClient({ serviceUrl: bobbinUrl, agent }); + const cache = new IdentityCache(createBobbinClient({ serviceUrl: bobbinUrl })); + fetchVouchesPage(ctx, { did, cache }) + .then((page) => { + if (cancelled) return; + networkVouches = page.items + .filter((v) => v.direction === "incoming" && v.did !== viewerDid) + .map((v) => ({ did: v.did, handle: v.handle, kind: v.kind })); + }) + .catch(() => { + if (!cancelled) networkVouches = []; + }); + return () => { + cancelled = true; + }; + }); const tabParam = $derived(page.url.searchParams.get("tab") ?? "overview"); const activeTab = $derived( ["repos", "starred", "strings", "vouches"].includes(tabParam) ? tabParam : "overview" @@ -39,7 +78,7 @@ following={counts.following} viewerFollowRkey={data.viewerFollowRkey} viewerVouch={data.viewerVouch} - networkVouches={data.networkVouches} + {networkVouches} />
diff --git a/web/src/routes/[handle]/+layout.ts b/web/src/routes/[handle]/+layout.ts index 7dc10bc4..3445e9a6 100644 --- a/web/src/routes/[handle]/+layout.ts +++ b/web/src/routes/[handle]/+layout.ts @@ -15,8 +15,7 @@ import { toHttpError, httpStatusFor } from "$lib/api/load"; import { ClientResponseError } from "$lib/api/client"; import { rkeyFromUri } from "$lib/api/uri"; 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 { ProfileCounts, DirectVouch } from "$lib/components/profile/types"; import type { LayoutLoad } from "./$types"; const REPOS: LinkSource = "sh.tangled.repo:subject"; @@ -84,23 +83,15 @@ 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 own (drives the button state) and the - // rest of the network. the network list shows for everyone, including on your - // own profile; the viewer's own vouch only matters when it isn't your profile. - let viewerVouch: DirectVouch | null = null; - let networkVouches: NetworkVouch[] = []; - const [vouchPage, viewer] = await Promise.all([ - fetchVouchesPage(ctx, { did }).catch(() => null), + // the viewer's own vouch drives the button state. it's readable without auth + // via the viewer's own outgoing vouches, so it stays in the server load. the + // network-vouches list is viewer-scoped and needs a service-auth'd request + // (bobbin derives the network from the authenticated viewer), so it's fetched + // client-side in the layout component where the oauth agent is available. + const viewerVouch: DirectVouch | null = viewerDid && did !== 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 })); - } + ? await getViewerVouch(ctx, viewerDid, did).catch(() => null) + : null; const notJoined = !profile && Object.values(counts).every((n) => n === 0); @@ -110,7 +101,6 @@ export const load: LayoutLoad = async (event) => { counts, viewerFollowRkey, viewerVouch, - networkVouches, notJoined }; }; diff --git a/web/src/routes/[handle]/+page.svelte b/web/src/routes/[handle]/+page.svelte index 0606089e..f9f915c7 100644 --- a/web/src/routes/[handle]/+page.svelte +++ b/web/src/routes/[handle]/+page.svelte @@ -56,11 +56,7 @@ /> {/key} {:else if data.tab === "vouches"} - {#key data.vouches} - + {#key data.identity.did} + {/key} {/if} diff --git a/web/src/routes/[handle]/+page.ts b/web/src/routes/[handle]/+page.ts index 02d3f1f2..d9b7d778 100644 --- a/web/src/routes/[handle]/+page.ts +++ b/web/src/routes/[handle]/+page.ts @@ -6,7 +6,6 @@ import { fetchStringsPage, fetchStarredPage, fetchPeoplePage, - fetchVouchesPage, fetchPinned } from "$lib/components/profile/pages"; import type { PageLoad } from "./$types"; @@ -53,10 +52,10 @@ export const load: PageLoad = async (event) => { return { tab, people: page.items } as const; } case "vouches": { - const page = await fetchVouchesPage(ctx, { did }); + // the vouch network is viewer-scoped and needs a service-auth'd request, + // so it's fetched client-side in VouchTab. nothing to prefetch here. return { tab: "vouches" as const, - vouches: page.items, isSelf: viewerDid === did, profileHandle: handle }; diff --git a/web/src/routes/timeline/+page.svelte b/web/src/routes/timeline/+page.svelte index a045354d..c483d835 100644 --- a/web/src/routes/timeline/+page.svelte +++ b/web/src/routes/timeline/+page.svelte @@ -13,9 +13,9 @@ Timeline · Tangled -
+
-

+

-- 2.51.2