Monorepo for Tangled
0

Configure Feed

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

Update wilb.me.js

Wilhelm Berggren (Jul 14, 2026, 12:42 PM +0200) 2c323d6a 9ac30893

+23 -17
+23 -17
appview/pages/profile-fx/wilb.me.js
··· 3 3 // Swaps the punchcard for a small RSS-style feed of the profile owner's 4 4 // atproto records: Leaflet documents (both the original pub.leaflet.* and 5 5 // the newer site.standard.* lexicons) and Bluesky posts, merged and sorted 6 - // newest-first. Everything is fetched client-side from the public PDS; if 7 - // anything fails, the punchcard is simply left alone. 6 + // newest-first. The profile owner's DID is read off the page itself (the 7 + // followers span carries it as data-followers-did). Everything is fetched 8 + // client-side from the public PDS; if anything fails, the punchcard is 9 + // simply left alone. 8 10 9 - const DID = "did:plc:oxdlsmnvpk2riyyuvq5jtdkd"; 11 + const DID_SELECTOR = "#followers[data-followers-did]"; 10 12 const PLC_DIRECTORY = "https://plc.directory"; 11 13 12 14 const FEED_LIMIT = 12; // entries shown ··· 53 55 const xrpc = (pds, method, params) => 54 56 `${pds}/xrpc/${method}?${new URLSearchParams(params)}`; 55 57 56 - const resolveIdentity = async (signal) => { 57 - const doc = await getJson(`${PLC_DIRECTORY}/${DID}`, signal); 58 + const resolveIdentity = async (did, signal) => { 59 + const doc = await getJson(`${PLC_DIRECTORY}/${did}`, signal); 58 60 const svc = (doc.service || []).find( 59 61 (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer", 60 62 ); ··· 62 64 const aka = (doc.alsoKnownAs || [])[0] || ""; 63 65 return { 64 66 pds: svc.serviceEndpoint, 65 - handle: aka.startsWith("at://") ? aka.slice(5) : DID, 67 + handle: aka.startsWith("at://") ? aka.slice(5) : did, 66 68 }; 67 69 }; 68 70 69 - const listRecords = (pds, collection, limit, signal) => 71 + const listRecords = (pds, did, collection, limit, signal) => 70 72 getJson( 71 - xrpc(pds, "com.atproto.repo.listRecords", { repo: DID, collection, limit }), 73 + xrpc(pds, "com.atproto.repo.listRecords", { repo: did, collection, limit }), 72 74 signal, 73 75 ).then((r) => r.records || []); 74 76 ··· 92 94 return `https://pdsls.dev/at://${record.uri.slice(5)}`; // browse the raw record 93 95 }; 94 96 95 - const toEntry = (record, source, pubMap) => { 97 + const toEntry = (record, source, pubMap, did) => { 96 98 const v = record.value || {}; 97 99 const when = new Date(v.publishedAt || v.createdAt || 0); 98 100 if (source.kind === "doc") { ··· 111 113 label: source.label, 112 114 title: truncate(v.text) || "(media post)", 113 115 snippet: "", 114 - href: `https://bsky.app/profile/${DID}/post/${rkeyOf(record.uri)}`, 116 + href: `https://witchsky.app/profile/${did}/post/${rkeyOf(record.uri)}`, 115 117 }; 116 118 }; 117 119 118 - const loadFeed = async (signal) => { 119 - const identity = await resolveIdentity(signal); 120 + const loadFeed = async (did, signal) => { 121 + const identity = await resolveIdentity(did, signal); 120 122 const { pds } = identity; 121 123 122 124 // Only query collections that actually exist in the repo. 123 125 const info = await getJson( 124 - xrpc(pds, "com.atproto.repo.describeRepo", { repo: DID }), 126 + xrpc(pds, "com.atproto.repo.describeRepo", { repo: did }), 125 127 signal, 126 128 ); 127 129 const present = new Set(info.collections || []); ··· 138 140 ]; 139 141 const pubMap = new Map(); 140 142 const pubResults = await Promise.allSettled( 141 - pubCollections.map((c) => listRecords(pds, c, 50, signal)), 143 + pubCollections.map((c) => listRecords(pds, did, c, 50, signal)), 142 144 ); 143 145 pubResults.forEach((r) => { 144 146 if (r.status !== "fulfilled") return; ··· 150 152 151 153 // The records themselves; PDS returns newest-first by default. 152 154 const results = await Promise.allSettled( 153 - wanted.map((c) => listRecords(pds, c, PER_COLLECTION, signal)), 155 + wanted.map((c) => listRecords(pds, did, c, PER_COLLECTION, signal)), 154 156 ); 155 157 const entries = results.flatMap((r, i) => 156 158 r.status === "fulfilled" 157 159 ? r.value 158 - .map((rec) => toEntry(rec, SOURCES[wanted[i]], pubMap)) 160 + .map((rec) => toEntry(rec, SOURCES[wanted[i]], pubMap, did)) 159 161 .filter(Boolean) 160 162 : [], 161 163 ); ··· 283 285 // --- lifecycle --------------------------------------------------------------- 284 286 285 287 const attach = (grid) => { 288 + // The profile page carries the owner's DID on the followers count. 289 + const did = document.querySelector(DID_SELECTOR)?.dataset.followersDid; 290 + if (!did || !did.startsWith("did:")) return () => {}; 291 + 286 292 const ac = new AbortController(); 287 293 const dark = matchMedia(DARK_QUERY); 288 294 const reduced = matchMedia(REDUCED_QUERY).matches; ··· 309 315 // Re-render with the other palette when the color scheme flips. 310 316 dark.addEventListener("change", () => mount(false), { signal: ac.signal }); 311 317 312 - loadFeed(ac.signal) 318 + loadFeed(did, ac.signal) 313 319 .then((d) => { 314 320 if (ac.signal.aborted || d.entries.length === 0) return; 315 321 data = d;