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.

web: fix json breaking with persister

+23 -41
+2 -4
web/src/lib/auth.ts
··· 105 105 const rpc = new Client({ handler: oauthAgent }); 106 106 const did = oauthAgent.sub; 107 107 108 - // Fall back to the last-known handle (if any) so offline restores don't 109 - // show a raw DID in the header. Gets overwritten once Slingshot responds. 108 + // Cached handle covers offline restores; overwritten when Slingshot responds. 110 109 let handle = localStorage.getItem(CURRENT_HANDLE_KEY) ?? did; 111 110 let pdsUrl = ""; 112 111 try { ··· 115 114 pdsUrl = doc.pds ?? ""; 116 115 localStorage.setItem(CURRENT_HANDLE_KEY, handle); 117 116 } catch { 118 - // Network may be unavailable (e.g., just resumed from suspend). We'll 119 - // retry on the next tab focus via the visibilitychange listener below. 117 + // Offline; the visibilitychange listener below will retry on next focus. 120 118 } 121 119 122 120 currentAgent = rpc;
+2 -7
web/src/lib/bbsModeration.ts
··· 6 6 import { parseAtUri } from "./util"; 7 7 import { isBanRecord, isHideRecord } from "./recordGuards"; 8 8 9 + // Fields must be JSON-safe — this shape is persisted via localStorage. 9 10 export interface BBSModeration { 10 - bannedDids: Set<string>; 11 - hiddenUris: Set<string>; 12 11 /** DID → rkey of that user's ban record on the sysop's PDS. */ 13 12 banRkeys: Record<string, string>; 14 13 /** Post URI → rkey of its hide record on the sysop's PDS. */ ··· 24 23 listRecords(pdsUrl, did, HIDE).catch(() => []), 25 24 ]); 26 25 27 - const bannedDids = new Set<string>(); 28 26 const banRkeys: Record<string, string> = {}; 29 27 for (const record of banRecs) { 30 28 if (!isBanRecord(record)) continue; 31 - bannedDids.add(record.value.did); 32 29 banRkeys[record.value.did] = parseAtUri(record.uri).rkey; 33 30 } 34 31 35 - const hiddenUris = new Set<string>(); 36 32 const hideRkeys: Record<string, string> = {}; 37 33 for (const record of hideRecs) { 38 34 if (!isHideRecord(record)) continue; 39 - hiddenUris.add(record.value.uri); 40 35 hideRkeys[record.value.uri] = parseAtUri(record.uri).rkey; 41 36 } 42 37 43 - return { bannedDids, hiddenUris, banRkeys, hideRkeys }; 38 + return { banRkeys, hideRkeys }; 44 39 }
+4 -8
web/src/lib/queryPersister.ts
··· 1 1 import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; 2 2 3 - // Bump when cache shapes change (lexicon edits, query-key restructures) so 4 - // older clients discard incompatible cached data on next load instead of 5 - // deserializing into crashes. 6 - const BUSTER = "atbbs-v1"; 3 + // Bump on breaking cache-shape changes to invalidate older clients. 4 + const BUSTER = "atbbs-v2"; 7 5 const MAX_AGE = 24 * 60 * 60 * 1000; 8 6 9 7 const persister = createSyncStoragePersister({ ··· 16 14 buster: BUSTER, 17 15 maxAge: MAX_AGE, 18 16 dehydrateOptions: { 19 - // Skip fingerprinted thread-page entries. Their keys churn whenever a 20 - // reply is added or deleted, so persisting them just bloats localStorage 21 - // with old-fingerprint garbage. thread-refs is persisted and drives the 22 - // page rebuild on load. 17 + // thread-page keys are fingerprinted by reply rkeys, so persisting them 18 + // would accumulate stale entries. thread-refs drives page rebuild on load. 23 19 shouldDehydrateQuery: (query: { queryKey: readonly unknown[] }) => 24 20 query.queryKey[0] !== "thread-page", 25 21 },
+2 -4
web/src/lib/recordGuards.ts
··· 1 - // Type guards for narrowing raw ATRecord.value into typed lexicon records. 2 - // Each guard runs the schema's runtime check and, if it passes, narrows the 3 - // record so downstream code can access typed fields without `as unknown as ...` 4 - // casts. 1 + // Type guards that run the lexicon schema's runtime check and narrow the 2 + // record so callers can access typed fields directly. 5 3 6 4 import { is } from "@atcute/lexicons/validations"; 7 5 import { mainSchema as postSchema } from "../lexicons/types/xyz/atbbs/post";
+2 -3
web/src/lib/routes.ts
··· 1 - // Typed path builders for every internal URL. Centralizes encoding so handle 2 - // and slug (user-authored) always round-trip safely through the router. 3 - // DID and rkey are AT Proto formats with URL-safe character sets. 1 + // Internal URL builders. handle/slug are user-authored and encoded; 2 + // did/rkey are AT Proto formats with URL-safe character sets. 4 3 5 4 export const bbsUrl = (handle: string) => 6 5 `/bbs/${encodeURIComponent(handle)}`;
+2 -3
web/src/lib/threadCache.ts
··· 25 25 return refs.slice(start, start + REPLIES_PER_PAGE); 26 26 } 27 27 28 - // threadPageQuery's key is fingerprinted by reply rkeys, so adding or removing 29 - // a reply changes the cache key. We read the pre-change page data from the old 30 - // key and seed the new key explicitly. 28 + // threadPageQuery's key is fingerprinted by rkeys, so the key changes on 29 + // add/delete — seed the new key from the old one rather than using `prev`. 31 30 export function appendRefAndReply( 32 31 threadUri: string, 33 32 newRef: BacklinkRef,
+1 -4
web/src/lib/writes.ts
··· 63 63 } 64 64 } 65 65 66 - // Seed the per-record cache used by getRecord so immediate re-reads (e.g. a 67 - // refetch of profileQuery after putProfile) see the new value instead of the 68 - // pre-write cached entry. 66 + // Sync the per-record cache so re-reads via getRecord return the new value. 69 67 function syncRecordCache<V extends object>( 70 68 did: string, 71 69 collection: string, ··· 143 141 }, 144 142 }); 145 143 assertOk(resp, "deleteRecord"); 146 - // Drop the per-record cache entry from the cache 147 144 queryClient.removeQueries({ 148 145 queryKey: ["record", did, collection, rkey], 149 146 exact: true,
+2 -2
web/src/pages/Board.tsx
··· 76 76 ? allThreads 77 77 : allThreads.filter( 78 78 (t) => 79 - !moderation.bannedDids.has(t.did) && 80 - !moderation.hiddenUris.has(t.uri), 79 + !moderation.banRkeys[t.did] && 80 + !moderation.hideRkeys[t.uri], 81 81 ); 82 82 83 83 const [title, setTitle] = useState("");
+6 -6
web/src/pages/Thread.tsx
··· 63 63 const isSysop = !!(user && user.did === bbs.identity.did); 64 64 const threadHidden = 65 65 !isSysop && 66 - (moderation.bannedDids.has(thread.did) || 67 - moderation.hiddenUris.has(thread.uri)); 66 + (!!moderation.banRkeys[thread.did] || 67 + !!moderation.hideRkeys[thread.uri]); 68 68 const visibleReplies = isSysop 69 69 ? replies 70 70 : replies.filter( 71 71 (reply) => 72 - !moderation.bannedDids.has(reply.did) && 73 - !moderation.hiddenUris.has(reply.uri), 72 + !moderation.banRkeys[reply.did] && 73 + !moderation.hideRkeys[reply.uri], 74 74 ); 75 75 76 76 const [body, setBody] = useState(""); ··· 254 254 const parentHidden = 255 255 !!parentReply && 256 256 !isSysop && 257 - (moderation.bannedDids.has(parentReply.did) || 258 - moderation.hiddenUris.has(parentReply.uri)); 257 + (!!moderation.banRkeys[parentReply.did] || 258 + !!moderation.hideRkeys[parentReply.uri]); 259 259 return ( 260 260 <ReplyCard 261 261 key={reply.uri}