A personal website powered by Astro and ATProto
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

simplified atproto api

+3 -187
+1 -1
src/components/content/ContentFeed.astro
··· 2 2 import { AtprotoBrowser } from '../../lib/atproto/atproto-browser'; 3 3 import { loadConfig } from '../../lib/config/site'; 4 4 import type { AtprotoRecord } from '../../lib/types/atproto'; 5 - import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob'; 5 + import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob-url'; 6 6 7 7 8 8 interface Props {
-40
src/lib/atproto/blob.ts
··· 1 - import { AtpAgent } from '@atproto/api' 2 - import { loadConfig } from '../config/site' 3 - 4 - export type BlobVariant = 'full' | 'avatar' | 'feed' 5 - 6 - export function blobCdnUrl(did: string, cid: string, variant: BlobVariant = 'full'): string { 7 - // Default to PDS sync getBlob endpoint; AppView/CDN variants can be swapped in later 8 - const base = 'https://bsky.social/xrpc/com.atproto.sync.getBlob' 9 - const params = new URLSearchParams({ did, cid }) 10 - return `${base}?${params.toString()}` 11 - } 12 - 13 - export async function fetchBlob(did: string, cid: string, agent?: AtpAgent): Promise<Blob> { 14 - const cfg = loadConfig() 15 - const atp = agent ?? new AtpAgent({ service: cfg.atproto.pdsUrl || 'https://bsky.social' }) 16 - const res = await atp.com.atproto.sync.getBlob({ did, cid }) 17 - // @atproto/api returns a Response-like object with data as ArrayBuffer 18 - const arrayBuf = await res.data.blob() 19 - return new Blob([arrayBuf]) 20 - } 21 - 22 - // Extract CID string from common embed image blob refs 23 - export function extractCidFromBlobRef(ref: unknown): string | null { 24 - // Formats we might encounter: 25 - // - string (cid) 26 - // - { $link: string } 27 - // - BlobRef object with toString() 28 - if (typeof ref === 'string') return ref 29 - if (ref && typeof ref === 'object') { 30 - const anyRef = ref as any 31 - if (typeof anyRef.$link === 'string') return anyRef.$link 32 - if (typeof anyRef.toString === 'function') { 33 - const s = anyRef.toString() 34 - if (s && typeof s === 'string') return s 35 - } 36 - } 37 - return null 38 - } 39 - 40 -
-27
src/lib/atproto/record-types.ts
··· 1 - import type { AppBskyFeedPost, AppBskyActorProfile } from '@atproto/api' 2 - 3 - // Canonical repo record shape we use across the app 4 - export interface RepoRecord<TValue = unknown> { 5 - uri: string 6 - cid: string 7 - value: TValue 8 - indexedAt: string 9 - collection: string 10 - $type: string 11 - } 12 - 13 - // First-party known record interfaces from @atproto/api 14 - export type BskyPostRecord = AppBskyFeedPost.Record 15 - export type BskyProfileRecord = AppBskyActorProfile.Record 16 - 17 - // Discriminated helpers for known types 18 - export type KnownRecordByType = 19 - | { $type: 'app.bsky.feed.post'; record: BskyPostRecord } 20 - | { $type: 'app.bsky.actor.profile'; record: BskyProfileRecord } 21 - 22 - // Fallback for custom or third-party records when we don't have a local lexicon yet 23 - export type UnknownRecordByType = { $type: string; record: unknown } 24 - 25 - export type AnyRecordByType = KnownRecordByType | UnknownRecordByType 26 - 27 -
-113
src/lib/components/discovered-registry.ts
··· 1 - import type { DiscoveredTypes } from '../generated/discovered-types'; 2 - import type { AnyRecordByType } from '../atproto/record-types'; 3 - 4 - export type DiscoveredComponent<T extends string = DiscoveredTypes> = { 5 - $type: T; 6 - component: string; 7 - props: Record<string, unknown>; 8 - } 9 - 10 - export type ComponentRegistry = Record<string, { component: string; props?: Record<string, unknown> }> 11 - 12 - export class DiscoveredComponentRegistry { 13 - private registry: ComponentRegistry = {}; 14 - private discoveredTypes: DiscoveredTypes[] = []; 15 - 16 - constructor() { 17 - this.initializeRegistry(); 18 - } 19 - 20 - // Initialize the registry with discovered types 21 - private initializeRegistry(): void { 22 - // This will be populated with discovered types 23 - // For now, we'll use a basic mapping 24 - this.registry = { 25 - 'app.bsky.feed.post': { 26 - component: 'BlueskyPost', 27 - props: { showAuthor: false, showTimestamp: true } 28 - }, 29 - 'app.bsky.actor.profile': { 30 - component: 'ProfileDisplay', 31 - props: { showHandle: true } 32 - }, 33 - 'social.grain.gallery': { 34 - component: 'GrainGalleryDisplay', 35 - props: { showCollections: true, columns: 3 } 36 - }, 37 - 'grain.social.feed.gallery': { 38 - component: 'GrainGalleryDisplay', 39 - props: { showCollections: true, columns: 3 } 40 - } 41 - }; 42 - } 43 - 44 - // Register a component for a specific $type 45 - registerComponent($type: DiscoveredTypes, component: string, props?: Record<string, unknown>): void { 46 - this.registry[$type] = { 47 - component, 48 - props 49 - }; 50 - } 51 - 52 - // Get component info for a $type 53 - getComponent($type: DiscoveredTypes): { component: string; props?: Record<string, unknown> } | null { 54 - return this.registry[$type] || null; 55 - } 56 - 57 - // Get all registered $types 58 - getRegisteredTypes(): DiscoveredTypes[] { 59 - return Object.keys(this.registry) as DiscoveredTypes[]; 60 - } 61 - 62 - // Check if a $type has a registered component 63 - hasComponent($type: DiscoveredTypes): boolean { 64 - return $type in this.registry; 65 - } 66 - 67 - // Get component mapping for rendering 68 - getComponentMapping(): ComponentRegistry { 69 - return this.registry; 70 - } 71 - 72 - // Update discovered types (called after build-time discovery) 73 - updateDiscoveredTypes(types: DiscoveredTypes[]): void { 74 - this.discoveredTypes = types; 75 - 76 - // Auto-register components for discovered types that don't have explicit mappings 77 - for (const $type of types) { 78 - if (!this.hasComponent($type)) { 79 - // Auto-assign based on service/collection 80 - const component = this.autoAssignComponent($type); 81 - if (component) { 82 - this.registerComponent($type, component); 83 - } 84 - } 85 - } 86 - } 87 - 88 - // Auto-assign component based on $type 89 - private autoAssignComponent($type: DiscoveredTypes): string | null { 90 - if ($type.includes('grain') || $type.includes('gallery')) { 91 - return 'GrainGalleryDisplay'; 92 - } 93 - if ($type.includes('post') || $type.includes('feed')) { 94 - return 'BlueskyPost'; 95 - } 96 - if ($type.includes('profile') || $type.includes('actor')) { 97 - return 'ProfileDisplay'; 98 - } 99 - return 'GenericContentDisplay'; 100 - } 101 - 102 - // Get component info for rendering 103 - getComponentInfo<T extends DiscoveredTypes>($type: T): DiscoveredComponent<T> | null { 104 - const componentInfo = this.getComponent($type); 105 - if (!componentInfo) return null; 106 - 107 - return { 108 - $type, 109 - component: componentInfo.component, 110 - props: componentInfo.props || {} 111 - }; 112 - } 113 - }
+2 -6
src/lib/types/atproto.ts
··· 1 1 // Base ATproto record types 2 - export interface AtprotoRecord { 3 - uri: string; 4 - cid: string; 5 - value: any; 6 - indexedAt: string; 7 - } 2 + import type { AtprotoRecord } from '../atproto/atproto-browser'; 3 + export type { AtprotoRecord }; 8 4 9 5 // Bluesky post types with proper embed handling 10 6 export interface BlueskyPost {