Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

at master 127 lines 3.6 kB view raw
1/** Thread detail fetchers: root post, reply refs, and the hydrated 2 * reply records for one page of the thread. */ 3 4import { 5 getBacklinks, 6 getRecord, 7 getRecordsBatch, 8 resolveIdentitiesBatch, 9 resolveIdentity, 10 type BacklinkRef, 11} from "./atproto"; 12import { POST } from "./lexicon"; 13import { makeAtUri, parseAtUri } from "./util"; 14import { recordToReply } from "./replies"; 15import { isPostRecord } from "./recordGuards"; 16import type { Reply } from "../components/post/ReplyCard"; 17 18export interface ThreadRoot { 19 uri: string; 20 did: string; 21 rkey: string; 22 authorHandle: string; 23 authorPds: string; 24 title: string; 25 body: string; 26 createdAt: string; 27 boardSlug: string; 28 attachments?: { file: { ref: { $link: string } }; name: string }[]; 29} 30 31const MAX_REF_PAGES = 20; 32const REF_PAGE_SIZE = 100; 33 34/** Every reply ref for the thread, oldest-first. */ 35export async function fetchThreadRefs( 36 threadUri: string, 37): Promise<BacklinkRef[]> { 38 const collected: BacklinkRef[] = []; 39 let cursor: string | undefined; 40 for (let i = 0; i < MAX_REF_PAGES; i++) { 41 const page = await getBacklinks( 42 threadUri, 43 `${POST}:root`, 44 REF_PAGE_SIZE, 45 cursor, 46 ); 47 collected.push(...page.records); 48 if (!page.cursor) break; 49 cursor = page.cursor; 50 } 51 return collected.reverse(); 52} 53 54export async function fetchThreadRoot( 55 did: string, 56 tid: string, 57): Promise<ThreadRoot> { 58 const threadRecord = await getRecord(did, POST, tid); 59 if (!isPostRecord(threadRecord)) { 60 throw new Error("Invalid post record"); 61 } 62 const author = await resolveIdentity(did); 63 const postValue = threadRecord.value; 64 const boardSlug = parseAtUri(postValue.scope).rkey; 65 return { 66 uri: threadRecord.uri, 67 did, 68 rkey: tid, 69 authorHandle: author.handle, 70 authorPds: author.pds ?? "", 71 title: postValue.title ?? "", 72 body: postValue.body, 73 createdAt: postValue.createdAt, 74 boardSlug, 75 attachments: postValue.attachments as ThreadRoot["attachments"], 76 }; 77} 78 79export function threadUriFor(did: string, tid: string): string { 80 return makeAtUri(did, POST, tid); 81} 82 83export interface ReplyPage { 84 replies: Reply[]; 85 /** Lookup by URI for any reply referenced as a parent — includes both 86 * on-page replies and off-page parents fetched separately. */ 87 parentReplies: Record<string, Reply>; 88} 89 90export async function hydrateReplyPage( 91 pageRefs: BacklinkRef[], 92): Promise<ReplyPage> { 93 if (!pageRefs.length) return { replies: [], parentReplies: {} }; 94 95 const records = await getRecordsBatch(pageRefs); 96 const authors = await resolveIdentitiesBatch( 97 records.map((r) => parseAtUri(r.uri).did), 98 ); 99 const replies: Reply[] = records 100 .map((record) => recordToReply(record, authors)) 101 .filter((reply): reply is Reply => reply !== null) 102 .sort((a, b) => a.createdAt.localeCompare(b.createdAt)); 103 104 const parentReplies: Record<string, Reply> = {}; 105 for (const reply of replies) parentReplies[reply.uri] = reply; 106 107 const offPageParentUris = [ 108 ...new Set( 109 replies 110 .map((r) => r.parent) 111 .filter((uri): uri is string => !!uri && !parentReplies[uri]), 112 ), 113 ]; 114 if (offPageParentUris.length) { 115 const parentRefs = offPageParentUris.map((uri) => parseAtUri(uri)); 116 const parentRecords = await getRecordsBatch(parentRefs); 117 const parentAuthors = await resolveIdentitiesBatch( 118 parentRecords.map((r) => parseAtUri(r.uri).did), 119 ); 120 for (const record of parentRecords) { 121 const reply = recordToReply(record, parentAuthors); 122 if (reply) parentReplies[reply.uri] = reply; 123 } 124 } 125 126 return { replies, parentReplies }; 127}