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.

fmt

+34 -13
+6 -2
telnet/server.py
··· 58 58 ) -> str: 59 59 """Handle prompt + inactivity timeouts safely.""" 60 60 await write(writer, label) 61 - try: 61 + try: 62 62 data = await asyncio.wait_for(reader.readline(), timeout=READ_TIMEOUT) 63 63 except asyncio.TimeoutError: 64 64 await write(writer, "\r\n Connection inactive.\r\n") ··· 67 67 return "" 68 68 text = data.decode(errors="ignore").strip() 69 69 return re.sub(r"[^\x20-\x7e]", "", text) 70 + 70 71 71 72 async def show_bbs(writer, bbs): 72 73 await write(writer, f"\r\n {bbs.site.name}\r\n") ··· 86 87 await write(writer, f" Latest News: {bbs.news[0].title}\r\n\r\n") 87 88 88 89 await write(writer, "[#] open board [n] news [q] quit\r\n") 90 + 89 91 90 92 async def show_board(writer, board, threads, has_next): 91 93 await write(writer, f"\r\n {board.name}\r\n") ··· 96 98 else: 97 99 for i, t in enumerate(threads, 1): 98 100 date = format_datetime_utc(t.created_at) 99 - await write(writer, f" {i}. {t.title} · {t.author.handle} · {date}\r\n") 101 + await write( 102 + writer, f" {i}. {t.title} · {t.author.handle} · {date}\r\n" 103 + ) 100 104 101 105 cmds = ["[#] open thread"] 102 106 if has_next:
+5 -2
tui/screens/thread.py
··· 120 120 121 121 # Fetch any quoted replies not already known 122 122 missing = [ 123 - r.quote for r in result.replies 123 + r.quote 124 + for r in result.replies 124 125 if r.quote and r.quote not in self._replies_map 125 126 ] 126 127 for uri in missing: 127 128 try: 128 129 parsed = AtUri.parse(uri) 129 - rec = await get_record(client, parsed.did, parsed.collection, parsed.rkey) 130 + rec = await get_record( 131 + client, parsed.did, parsed.collection, parsed.rkey 132 + ) 130 133 author = await resolve_identity(client, parsed.did) 131 134 self._replies_map[uri] = reply_from_record(rec, author) 132 135 except Exception:
+4 -2
web/src/components/HandleInput.tsx
··· 13 13 14 14 // Props for HandleInput. Extends standard <input> props so callers can 15 15 // pass things like `required`, `disabled`, `id`, etc. 16 - interface HandleInputProps 17 - extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value"> { 16 + interface HandleInputProps extends Omit< 17 + InputHTMLAttributes<HTMLInputElement>, 18 + "onChange" | "value" 19 + > { 18 20 value: string; 19 21 onChange: (v: string) => void; 20 22 }
+4 -1
web/src/components/ReplyCard.tsx
··· 40 40 const isSysop = userDid === sysopDid; 41 41 42 42 return ( 43 - <div id={`reply-${reply.rkey}`} className="reply-card border border-neutral-800/50 rounded p-4"> 43 + <div 44 + id={`reply-${reply.rkey}`} 45 + className="reply-card border border-neutral-800/50 rounded p-4" 46 + > 44 47 <div className="flex items-baseline justify-between mb-2"> 45 48 <div className="flex items-baseline gap-2"> 46 49 <span className="text-neutral-300">{reply.handle}</span>
+8 -4
web/src/hooks/useThreadReplies.ts
··· 35 35 return index >= 0 ? Math.floor(index / REPLIES_PER_PAGE) + 1 : null; 36 36 } 37 37 38 - 39 38 function rkeyFromHash(): string | null { 40 39 const h = typeof window !== "undefined" ? window.location.hash : ""; 41 40 return h.startsWith("#reply-") ? h.slice(7) : null; ··· 118 117 return clampPage(fromHash ?? fromReply ?? fromUrl, allRefs.length); 119 118 }); 120 119 121 - const [initialScrollDone, setInitialScrollDone] = useState(!initialScrollRkey); 120 + const [initialScrollDone, setInitialScrollDone] = 121 + useState(!initialScrollRkey); 122 122 123 123 // Keep the URL in sync when the user changes page (e.g. via PageNav). 124 124 useEffect(() => { ··· 152 152 153 153 // Pending scroll target — set when navigating to a reply on another page. 154 154 // Cleared once the scroll completes. 155 - const [pendingScrollRkey, setPendingScrollRkey] = useState<string | null>(null); 155 + const [pendingScrollRkey, setPendingScrollRkey] = useState<string | null>( 156 + null, 157 + ); 156 158 157 159 const fetchVisiblePage = useCallback( 158 160 async (currentRefs: BacklinkRef[], currentPage: number) => { ··· 224 226 .map((i) => i.quote!) 225 227 .filter((uri) => !replyCache[uri]); 226 228 if (missingQuotes.length) { 227 - const quoteRefs = [...new Set(missingQuotes)].map((uri) => parseAtUri(uri)); 229 + const quoteRefs = [...new Set(missingQuotes)].map((uri) => 230 + parseAtUri(uri), 231 + ); 228 232 const quoteRecords = await getRecordsBatch(quoteRefs); 229 233 const quoteDids = quoteRecords.map((r) => parseAtUri(r.uri).did); 230 234 const quoteAuthors = await resolveIdentitiesBatch(quoteDids);
+4 -1
web/src/pages/Home.tsx
··· 87 87 88 88 <div className="border-t border-neutral-800 py-4"> 89 89 <h2 className="text-neutral-300 mb-4">Dial a BBS</h2> 90 - <form onSubmit={onSubmit} className="flex flex-col sm:flex-row gap-2 mb-6"> 90 + <form 91 + onSubmit={onSubmit} 92 + className="flex flex-col sm:flex-row gap-2 mb-6" 93 + > 91 94 <HandleInput 92 95 value={handle} 93 96 onChange={setHandle}
+3 -1
web/src/pages/Thread.tsx
··· 182 182 sysopDid={bbs.identity.did} 183 183 quoted={reply.quote ? replyCache[reply.quote] : undefined} 184 184 onQuote={() => setQuote({ uri: reply.uri, handle: reply.handle })} 185 - onQuoteClick={reply.quote ? () => scrollToReply(reply.quote!) : undefined} 185 + onQuoteClick={ 186 + reply.quote ? () => scrollToReply(reply.quote!) : undefined 187 + } 186 188 onDelete={() => onDeleteReply(reply)} 187 189 onBan={() => onBan(reply.did)} 188 190 onHide={() => onHide(reply.uri)}