From 481658f849ad19bd3397ddff31447a5a601875cf Mon Sep 17 00:00:00 2001 From: scanash00 Date: Fri, 12 Jun 2026 08:54:43 -0800 Subject: [PATCH] microcosm: community CDN + viewer-state hydration - Default reconstructed image URLs to community cdn.blueat.net instead of cdn.bsky.app (configurable via EXPO_PUBLIC_IMG_CDN_URL); verified same scheme - Reconstruct PostView.viewer like/repost state from Constellation did-filtered backlinks (record rkey -> at-uri), threaded through hydratePost via agent.did --- MICROCOSM_PLAN.md | 7 +++- src/env/common.ts | 10 +++++ src/lib/microcosm/hydrate.ts | 72 ++++++++++++++++++++++++++++++++---- src/state/queries/post.ts | 2 +- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/MICROCOSM_PLAN.md b/MICROCOSM_PLAN.md index b7e56ba..7486f48 100644 --- a/MICROCOSM_PLAN.md +++ b/MICROCOSM_PLAN.md @@ -126,9 +126,14 @@ Source strings map directly to bsky interactions: verified the feed_thumbnail/feed_fullsize CDN URLs resolve (200 JPEG). - ✅ `hydrateProfileBasic(did)` → `ProfileViewBasic` (post author embedding). +- ✅ viewer state (did *I* like/repost): reconstructed from Constellation + did-filtered backlinks -> like/repost record rkey -> at-uri. Wired into + `hydratePost` via `agent.did`. Verified live. +- ✅ Image CDN swapped from Bluesky's `cdn.bsky.app` to the community-run + `cdn.blueat.net` (configurable via `IMG_CDN_URL`); same URL scheme, verified. + Remaining for Phase 3 (follow-ups, non-blocking): - quote-record + video embeds (need a record fetch / video service) -- viewer state (did *I* like/repost — Constellation `did` filter on my DID) - thread reply-tree assembly (replaces app.bsky.unspecced.getPostThreadV2) Original Phase 3 scope notes: diff --git a/src/env/common.ts b/src/env/common.ts index cc2ba3b..942d942 100644 --- a/src/env/common.ts +++ b/src/env/common.ts @@ -114,3 +114,13 @@ export const SPACEDUST_URL: string = * identify your project and a contact. */ export const MICROCOSM_USER_AGENT = 'fih (microcosm client; scan@scanash.com)' + +/** + * Image CDN base used when reconstructing avatar/banner/feed image URLs from + * raw blob refs (during client-side hydration). Defaults to the community-run + * `cdn.blueat.net` mirror rather than Bluesky's first-party `cdn.bsky.app`, so + * the app doesn't depend on Bluesky CDN infrastructure. Same URL scheme: + * `/img//plain//@jpeg`. + */ +export const IMG_CDN_URL: string = + process.env.EXPO_PUBLIC_IMG_CDN_URL || 'https://cdn.blueat.net' diff --git a/src/lib/microcosm/hydrate.ts b/src/lib/microcosm/hydrate.ts index a9f6710..6e5c059 100644 --- a/src/lib/microcosm/hydrate.ts +++ b/src/lib/microcosm/hydrate.ts @@ -22,15 +22,18 @@ import { import {constellation} from '#/lib/microcosm' import {getRecordByUri, resolveMiniDoc} from '#/lib/microcosm/slingshot' +import {IMG_CDN_URL} from '#/env' /** - * Bluesky image CDN base. Avatars/banners are blob refs in the raw record; the - * AppView rewrites them to CDN URLs. We reconstruct the same URLs. + * Image CDN base. Avatars/banners/feed images are blob refs in the raw record; + * the AppView rewrites them to CDN URLs and we reconstruct the same scheme + * (`/img//plain//@jpeg`). Defaults to the community-run + * `cdn.blueat.net` (see `IMG_CDN_URL`) instead of Bluesky's `cdn.bsky.app`. * - * NOTE: this is still the bsky CDN. A fully host-independent build would instead - * proxy blobs from the author's PDS (`/xrpc/com.atproto.sync.getBlob`). + * NOTE: a fully host-independent build would proxy blobs directly from the + * author's PDS (`/xrpc/com.atproto.sync.getBlob`). */ -const IMG_CDN = 'https://cdn.bsky.app/img' +const IMG_CDN = `${IMG_CDN_URL}/img` type BlobRef = {ref?: {$link?: string}; $link?: string} @@ -151,17 +154,66 @@ export async function hydrateProfile( } /** - * Hydrate a `PostView` for a post at-uri from microcosm. - * Embeds and viewer state are not reconstructed here (follow-up work). + * Reconstruct the viewer's interaction state for a post: whether the viewer has + * a like / repost record pointing at it, and the at-uri of that record (which + * the UI needs to undo the interaction). Found by filtering Constellation + * backlinks to the viewer's own DID. + */ +async function hydrateViewerState( + postUri: string, + viewerDid: string, + signal?: AbortSignal, +): Promise { + const [likeRes, repostRes] = await Promise.all([ + constellation + .getBacklinks( + { + subject: postUri, + source: constellation.Sources.likes, + did: [viewerDid], + limit: 1, + }, + signal, + ) + .catch(() => undefined), + constellation + .getBacklinks( + { + subject: postUri, + source: constellation.Sources.reposts, + did: [viewerDid], + limit: 1, + }, + signal, + ) + .catch(() => undefined), + ]) + + const likeRec = likeRes?.records?.[0] + const repostRec = repostRes?.records?.[0] + return { + like: likeRec + ? `at://${likeRec.did}/${likeRec.collection}/${likeRec.rkey}` + : undefined, + repost: repostRec + ? `at://${repostRec.did}/${repostRec.collection}/${repostRec.rkey}` + : undefined, + } +} + +/** + * Hydrate a `PostView` for a post at-uri from microcosm. If `viewerDid` is + * provided, the viewer's like/repost state is reconstructed too. */ export async function hydratePost( postUri: string, + viewerDid?: string, signal?: AbortSignal, ): Promise { const parsed = parseAtUri(postUri) if (!parsed) return undefined - const [recordRes, author, likes, reposts, replies, quotes] = + const [recordRes, author, likes, reposts, replies, quotes, viewer] = await Promise.all([ getRecordByUri(postUri, undefined, signal).catch(() => undefined), hydrateProfileBasic(parsed.did, signal).catch(() => undefined), @@ -169,6 +221,9 @@ export async function hydratePost( constellation.repostCount(postUri, signal).catch(() => undefined), constellation.replyCount(postUri, signal).catch(() => undefined), constellation.quoteCount(postUri, signal).catch(() => undefined), + viewerDid + ? hydrateViewerState(postUri, viewerDid, signal).catch(() => undefined) + : Promise.resolve(undefined), ]) if (!recordRes || !author) return undefined @@ -187,6 +242,7 @@ export async function hydratePost( replyCount: replies?.total, quoteCount: quotes?.total, indexedAt: record?.createdAt ?? new Date().toISOString(), + viewer, } } diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 52feca3..af5b75e 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -39,7 +39,7 @@ export function usePostQuery(uri: string | undefined) { if (MICROCOSM_ENABLED) { try { - const view = await hydratePost(urip.toString(), signal) + const view = await hydratePost(urip.toString(), agent.did, signal) if (view) return view } catch (e) { logger.debug('microcosm hydratePost failed, falling back', { -- 2.51.2