diff --git a/web/src/lib/auth.ts b/web/src/lib/auth.ts index 9ca01f3..f971140 100644 --- a/web/src/lib/auth.ts +++ b/web/src/lib/auth.ts @@ -105,8 +105,7 @@ async function setSignedIn(oauthAgent: OAuthUserAgent) { const rpc = new Client({ handler: oauthAgent }); const did = oauthAgent.sub; - // Fall back to the last-known handle (if any) so offline restores don't - // show a raw DID in the header. Gets overwritten once Slingshot responds. + // Cached handle covers offline restores; overwritten when Slingshot responds. let handle = localStorage.getItem(CURRENT_HANDLE_KEY) ?? did; let pdsUrl = ""; try { @@ -115,8 +114,7 @@ async function setSignedIn(oauthAgent: OAuthUserAgent) { pdsUrl = doc.pds ?? ""; localStorage.setItem(CURRENT_HANDLE_KEY, handle); } catch { - // Network may be unavailable (e.g., just resumed from suspend). We'll - // retry on the next tab focus via the visibilitychange listener below. + // Offline; the visibilitychange listener below will retry on next focus. } currentAgent = rpc; diff --git a/web/src/lib/bbsModeration.ts b/web/src/lib/bbsModeration.ts index d9103ef..31c19b8 100644 --- a/web/src/lib/bbsModeration.ts +++ b/web/src/lib/bbsModeration.ts @@ -6,9 +6,8 @@ import { BAN, HIDE } from "./lexicon"; import { parseAtUri } from "./util"; import { isBanRecord, isHideRecord } from "./recordGuards"; +// Fields must be JSON-safe — this shape is persisted via localStorage. export interface BBSModeration { - bannedDids: Set; - hiddenUris: Set; /** DID → rkey of that user's ban record on the sysop's PDS. */ banRkeys: Record; /** Post URI → rkey of its hide record on the sysop's PDS. */ @@ -24,21 +23,17 @@ export async function fetchBBSModeration( listRecords(pdsUrl, did, HIDE).catch(() => []), ]); - const bannedDids = new Set(); const banRkeys: Record = {}; for (const record of banRecs) { if (!isBanRecord(record)) continue; - bannedDids.add(record.value.did); banRkeys[record.value.did] = parseAtUri(record.uri).rkey; } - const hiddenUris = new Set(); const hideRkeys: Record = {}; for (const record of hideRecs) { if (!isHideRecord(record)) continue; - hiddenUris.add(record.value.uri); hideRkeys[record.value.uri] = parseAtUri(record.uri).rkey; } - return { bannedDids, hiddenUris, banRkeys, hideRkeys }; + return { banRkeys, hideRkeys }; } diff --git a/web/src/lib/queryPersister.ts b/web/src/lib/queryPersister.ts index 8b29766..a0bb0f0 100644 --- a/web/src/lib/queryPersister.ts +++ b/web/src/lib/queryPersister.ts @@ -1,9 +1,7 @@ import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; -// Bump when cache shapes change (lexicon edits, query-key restructures) so -// older clients discard incompatible cached data on next load instead of -// deserializing into crashes. -const BUSTER = "atbbs-v1"; +// Bump on breaking cache-shape changes to invalidate older clients. +const BUSTER = "atbbs-v2"; const MAX_AGE = 24 * 60 * 60 * 1000; const persister = createSyncStoragePersister({ @@ -16,10 +14,8 @@ export const persistOptions = { buster: BUSTER, maxAge: MAX_AGE, dehydrateOptions: { - // Skip fingerprinted thread-page entries. Their keys churn whenever a - // reply is added or deleted, so persisting them just bloats localStorage - // with old-fingerprint garbage. thread-refs is persisted and drives the - // page rebuild on load. + // thread-page keys are fingerprinted by reply rkeys, so persisting them + // would accumulate stale entries. thread-refs drives page rebuild on load. shouldDehydrateQuery: (query: { queryKey: readonly unknown[] }) => query.queryKey[0] !== "thread-page", }, diff --git a/web/src/lib/recordGuards.ts b/web/src/lib/recordGuards.ts index 461851a..3c7b30c 100644 --- a/web/src/lib/recordGuards.ts +++ b/web/src/lib/recordGuards.ts @@ -1,7 +1,5 @@ -// Type guards for narrowing raw ATRecord.value into typed lexicon records. -// Each guard runs the schema's runtime check and, if it passes, narrows the -// record so downstream code can access typed fields without `as unknown as ...` -// casts. +// Type guards that run the lexicon schema's runtime check and narrow the +// record so callers can access typed fields directly. import { is } from "@atcute/lexicons/validations"; import { mainSchema as postSchema } from "../lexicons/types/xyz/atbbs/post"; diff --git a/web/src/lib/routes.ts b/web/src/lib/routes.ts index 27b72b0..f85d9a7 100644 --- a/web/src/lib/routes.ts +++ b/web/src/lib/routes.ts @@ -1,6 +1,5 @@ -// Typed path builders for every internal URL. Centralizes encoding so handle -// and slug (user-authored) always round-trip safely through the router. -// DID and rkey are AT Proto formats with URL-safe character sets. +// Internal URL builders. handle/slug are user-authored and encoded; +// did/rkey are AT Proto formats with URL-safe character sets. export const bbsUrl = (handle: string) => `/bbs/${encodeURIComponent(handle)}`; diff --git a/web/src/lib/threadCache.ts b/web/src/lib/threadCache.ts index 09a2483..97e56c8 100644 --- a/web/src/lib/threadCache.ts +++ b/web/src/lib/threadCache.ts @@ -25,9 +25,8 @@ function pageSlice(refs: BacklinkRef[], page: number): BacklinkRef[] { return refs.slice(start, start + REPLIES_PER_PAGE); } -// threadPageQuery's key is fingerprinted by reply rkeys, so adding or removing -// a reply changes the cache key. We read the pre-change page data from the old -// key and seed the new key explicitly. +// threadPageQuery's key is fingerprinted by rkeys, so the key changes on +// add/delete — seed the new key from the old one rather than using `prev`. export function appendRefAndReply( threadUri: string, newRef: BacklinkRef, diff --git a/web/src/lib/writes.ts b/web/src/lib/writes.ts index 315a1d8..6a153de 100644 --- a/web/src/lib/writes.ts +++ b/web/src/lib/writes.ts @@ -63,9 +63,7 @@ function assertOk( } } -// Seed the per-record cache used by getRecord so immediate re-reads (e.g. a -// refetch of profileQuery after putProfile) see the new value instead of the -// pre-write cached entry. +// Sync the per-record cache so re-reads via getRecord return the new value. function syncRecordCache( did: string, collection: string, @@ -143,7 +141,6 @@ export async function deleteRecord( }, }); assertOk(resp, "deleteRecord"); - // Drop the per-record cache entry from the cache queryClient.removeQueries({ queryKey: ["record", did, collection, rkey], exact: true, diff --git a/web/src/pages/Board.tsx b/web/src/pages/Board.tsx index 060c1a1..c8b93dc 100644 --- a/web/src/pages/Board.tsx +++ b/web/src/pages/Board.tsx @@ -76,8 +76,8 @@ export default function BoardPage() { ? allThreads : allThreads.filter( (t) => - !moderation.bannedDids.has(t.did) && - !moderation.hiddenUris.has(t.uri), + !moderation.banRkeys[t.did] && + !moderation.hideRkeys[t.uri], ); const [title, setTitle] = useState(""); diff --git a/web/src/pages/Thread.tsx b/web/src/pages/Thread.tsx index f54db65..d389a58 100644 --- a/web/src/pages/Thread.tsx +++ b/web/src/pages/Thread.tsx @@ -63,14 +63,14 @@ export default function ThreadPage() { const isSysop = !!(user && user.did === bbs.identity.did); const threadHidden = !isSysop && - (moderation.bannedDids.has(thread.did) || - moderation.hiddenUris.has(thread.uri)); + (!!moderation.banRkeys[thread.did] || + !!moderation.hideRkeys[thread.uri]); const visibleReplies = isSysop ? replies : replies.filter( (reply) => - !moderation.bannedDids.has(reply.did) && - !moderation.hiddenUris.has(reply.uri), + !moderation.banRkeys[reply.did] && + !moderation.hideRkeys[reply.uri], ); const [body, setBody] = useState(""); @@ -254,8 +254,8 @@ export default function ThreadPage() { const parentHidden = !!parentReply && !isSysop && - (moderation.bannedDids.has(parentReply.did) || - moderation.hiddenUris.has(parentReply.uri)); + (!!moderation.banRkeys[parentReply.did] || + !!moderation.hideRkeys[parentReply.uri]); return (