···22import { AtprotoBrowser } from '../../lib/atproto/atproto-browser';
33import { loadConfig } from '../../lib/config/site';
44import type { AtprotoRecord } from '../../lib/types/atproto';
55-import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob';
55+import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob-url';
667788interface Props {
-40
src/lib/atproto/blob.ts
···11-import { AtpAgent } from '@atproto/api'
22-import { loadConfig } from '../config/site'
33-44-export type BlobVariant = 'full' | 'avatar' | 'feed'
55-66-export function blobCdnUrl(did: string, cid: string, variant: BlobVariant = 'full'): string {
77- // Default to PDS sync getBlob endpoint; AppView/CDN variants can be swapped in later
88- const base = 'https://bsky.social/xrpc/com.atproto.sync.getBlob'
99- const params = new URLSearchParams({ did, cid })
1010- return `${base}?${params.toString()}`
1111-}
1212-1313-export async function fetchBlob(did: string, cid: string, agent?: AtpAgent): Promise<Blob> {
1414- const cfg = loadConfig()
1515- const atp = agent ?? new AtpAgent({ service: cfg.atproto.pdsUrl || 'https://bsky.social' })
1616- const res = await atp.com.atproto.sync.getBlob({ did, cid })
1717- // @atproto/api returns a Response-like object with data as ArrayBuffer
1818- const arrayBuf = await res.data.blob()
1919- return new Blob([arrayBuf])
2020-}
2121-2222-// Extract CID string from common embed image blob refs
2323-export function extractCidFromBlobRef(ref: unknown): string | null {
2424- // Formats we might encounter:
2525- // - string (cid)
2626- // - { $link: string }
2727- // - BlobRef object with toString()
2828- if (typeof ref === 'string') return ref
2929- if (ref && typeof ref === 'object') {
3030- const anyRef = ref as any
3131- if (typeof anyRef.$link === 'string') return anyRef.$link
3232- if (typeof anyRef.toString === 'function') {
3333- const s = anyRef.toString()
3434- if (s && typeof s === 'string') return s
3535- }
3636- }
3737- return null
3838-}
3939-4040-
-27
src/lib/atproto/record-types.ts
···11-import type { AppBskyFeedPost, AppBskyActorProfile } from '@atproto/api'
22-33-// Canonical repo record shape we use across the app
44-export interface RepoRecord<TValue = unknown> {
55- uri: string
66- cid: string
77- value: TValue
88- indexedAt: string
99- collection: string
1010- $type: string
1111-}
1212-1313-// First-party known record interfaces from @atproto/api
1414-export type BskyPostRecord = AppBskyFeedPost.Record
1515-export type BskyProfileRecord = AppBskyActorProfile.Record
1616-1717-// Discriminated helpers for known types
1818-export type KnownRecordByType =
1919- | { $type: 'app.bsky.feed.post'; record: BskyPostRecord }
2020- | { $type: 'app.bsky.actor.profile'; record: BskyProfileRecord }
2121-2222-// Fallback for custom or third-party records when we don't have a local lexicon yet
2323-export type UnknownRecordByType = { $type: string; record: unknown }
2424-2525-export type AnyRecordByType = KnownRecordByType | UnknownRecordByType
2626-2727-
-113
src/lib/components/discovered-registry.ts
···11-import type { DiscoveredTypes } from '../generated/discovered-types';
22-import type { AnyRecordByType } from '../atproto/record-types';
33-44-export type DiscoveredComponent<T extends string = DiscoveredTypes> = {
55- $type: T;
66- component: string;
77- props: Record<string, unknown>;
88-}
99-1010-export type ComponentRegistry = Record<string, { component: string; props?: Record<string, unknown> }>
1111-1212-export class DiscoveredComponentRegistry {
1313- private registry: ComponentRegistry = {};
1414- private discoveredTypes: DiscoveredTypes[] = [];
1515-1616- constructor() {
1717- this.initializeRegistry();
1818- }
1919-2020- // Initialize the registry with discovered types
2121- private initializeRegistry(): void {
2222- // This will be populated with discovered types
2323- // For now, we'll use a basic mapping
2424- this.registry = {
2525- 'app.bsky.feed.post': {
2626- component: 'BlueskyPost',
2727- props: { showAuthor: false, showTimestamp: true }
2828- },
2929- 'app.bsky.actor.profile': {
3030- component: 'ProfileDisplay',
3131- props: { showHandle: true }
3232- },
3333- 'social.grain.gallery': {
3434- component: 'GrainGalleryDisplay',
3535- props: { showCollections: true, columns: 3 }
3636- },
3737- 'grain.social.feed.gallery': {
3838- component: 'GrainGalleryDisplay',
3939- props: { showCollections: true, columns: 3 }
4040- }
4141- };
4242- }
4343-4444- // Register a component for a specific $type
4545- registerComponent($type: DiscoveredTypes, component: string, props?: Record<string, unknown>): void {
4646- this.registry[$type] = {
4747- component,
4848- props
4949- };
5050- }
5151-5252- // Get component info for a $type
5353- getComponent($type: DiscoveredTypes): { component: string; props?: Record<string, unknown> } | null {
5454- return this.registry[$type] || null;
5555- }
5656-5757- // Get all registered $types
5858- getRegisteredTypes(): DiscoveredTypes[] {
5959- return Object.keys(this.registry) as DiscoveredTypes[];
6060- }
6161-6262- // Check if a $type has a registered component
6363- hasComponent($type: DiscoveredTypes): boolean {
6464- return $type in this.registry;
6565- }
6666-6767- // Get component mapping for rendering
6868- getComponentMapping(): ComponentRegistry {
6969- return this.registry;
7070- }
7171-7272- // Update discovered types (called after build-time discovery)
7373- updateDiscoveredTypes(types: DiscoveredTypes[]): void {
7474- this.discoveredTypes = types;
7575-7676- // Auto-register components for discovered types that don't have explicit mappings
7777- for (const $type of types) {
7878- if (!this.hasComponent($type)) {
7979- // Auto-assign based on service/collection
8080- const component = this.autoAssignComponent($type);
8181- if (component) {
8282- this.registerComponent($type, component);
8383- }
8484- }
8585- }
8686- }
8787-8888- // Auto-assign component based on $type
8989- private autoAssignComponent($type: DiscoveredTypes): string | null {
9090- if ($type.includes('grain') || $type.includes('gallery')) {
9191- return 'GrainGalleryDisplay';
9292- }
9393- if ($type.includes('post') || $type.includes('feed')) {
9494- return 'BlueskyPost';
9595- }
9696- if ($type.includes('profile') || $type.includes('actor')) {
9797- return 'ProfileDisplay';
9898- }
9999- return 'GenericContentDisplay';
100100- }
101101-102102- // Get component info for rendering
103103- getComponentInfo<T extends DiscoveredTypes>($type: T): DiscoveredComponent<T> | null {
104104- const componentInfo = this.getComponent($type);
105105- if (!componentInfo) return null;
106106-107107- return {
108108- $type,
109109- component: componentInfo.component,
110110- props: componentInfo.props || {}
111111- };
112112- }
113113-}
+2-6
src/lib/types/atproto.ts
···11// Base ATproto record types
22-export interface AtprotoRecord {
33- uri: string;
44- cid: string;
55- value: any;
66- indexedAt: string;
77-}
22+import type { AtprotoRecord } from '../atproto/atproto-browser';
33+export type { AtprotoRecord };
8495// Bluesky post types with proper embed handling
106export interface BlueskyPost {