Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites atproto.eu
community atproto
Something went wrong. Try again.
5.0 kB · 132 lines
Astro
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133---// Follow via the signed-in agent (writes app.bsky.graph.follow to the user's own PDS).// Buttons carry data-did. Signed out, every button shows "Follow" and opens the profile.// Signed in, the button is hidden on your own card and for accounts you already follow, showing// "Follow" only where you don't follow yet. Follow state comes from your OWN follow records (a// plain repo read — no AppView/rpc scope). That scan is slow for heavy-follow accounts, so we// cache it in localStorage: repeat loads apply the cache instantly (no flash), then refresh in the// background. On the first load (no cache) buttons stay hidden until the scan finishes — no// show-then-vanish flash. Following hides that button and updates the cache.---
<script> import { handleActionError } from '../lib/auth-actions'; import { ensureWriteScope } from '../lib/auth-step-up';
const SELECTOR = '.follow-btn'; const cacheKey = (did: string) => 'atn-follows:' + did;
function buttons(): HTMLElement[] { return Array.from(document.querySelectorAll(SELECTOR)); }
function setFollow(b: HTMLElement) { // Localized label comes from data-follow-label (set per-locale on each button); fall back to English. b.textContent = (b as HTMLElement).dataset.followLabel || 'Follow'; b.setAttribute('aria-pressed', 'false'); }
// Reveal only the buttons for accounts you don't follow (and not your own card); hide the rest. function apply(btns: HTMLElement[], followed: Set<string>, self: string) { for (const b of btns) { b.hidden = b.dataset.did === self || (!!b.dataset.did && followed.has(b.dataset.did)); if (!b.hidden) setFollow(b); } }
function readCache(did: string): Set<string> | null { try { const v = JSON.parse(localStorage.getItem(cacheKey(did)) || 'null'); return Array.isArray(v) ? new Set(v) : null; } catch { return null; } }
// Every DID the viewer follows, read straight from their own repo (public records; no rpc scope). async function followedDids(agent: any, did: string): Promise<Set<string>> { const followed = new Set<string>(); let cursor: string | undefined; do { const res = await agent.com.atproto.repo.listRecords({ repo: did, collection: 'app.bsky.graph.follow', limit: 100, cursor, }); for (const r of res.data.records) { const subj = (r.value as any)?.subject; if (typeof subj === 'string') followed.add(subj); } cursor = res.data.cursor; } while (cursor); return followed; }
async function refresh() { const atn = (window as any).atn; const btns = buttons(); if (!atn?.agent) { // Signed out: every button is a plain "Follow" that opens the profile. btns.forEach((b) => { b.hidden = false; setFollow(b); }); return; } // Optimistic: apply the cached follow set instantly so repeat loads don't flash. Without a // cache, leave buttons hidden (their default) until the scan finishes — no show-then-hide. const cached = readCache(atn.did); if (cached) apply(btns, cached, atn.did);
try { const followed = await followedDids(atn.agent, atn.did); localStorage.setItem(cacheKey(atn.did), JSON.stringify([...followed])); apply(btns, followed, atn.did); } catch { // Scan failed: if we had nothing cached, reveal all as "Follow" so buttons aren't stuck hidden. if (!cached) { for (const b of btns) { b.hidden = b.dataset.did === atn.did; if (!b.hidden) setFollow(b); } } } }
document.addEventListener('click', async (e) => { const btn = (e.target as HTMLElement)?.closest(SELECTOR) as HTMLElement | null; if (!btn) return; const atn = (window as any).atn; if (!atn?.agent) { if (btn.dataset.profileUrl) window.open(btn.dataset.profileUrl, '_blank', 'noopener'); return; } if (!(await ensureWriteScope('follow this account'))) return; btn.setAttribute('disabled', ''); try { await atn.agent.com.atproto.repo.createRecord({ repo: atn.did, collection: 'app.bsky.graph.follow', record: { $type: 'app.bsky.graph.follow', subject: btn.dataset.did, createdAt: new Date().toISOString(), }, }); btn.hidden = true; // following → hide the button // Keep the cache in sync so it stays hidden on the next load. const set = readCache(atn.did) ?? new Set<string>(); if (btn.dataset.did) set.add(btn.dataset.did); localStorage.setItem(cacheKey(atn.did), JSON.stringify([...set])); } catch (err: any) { await handleActionError(err, 'Follow'); } finally { btn.removeAttribute('disabled'); } });
document.addEventListener('atn:signedin', refresh); document.addEventListener('atn:signedout', refresh); if ((window as any).atn) refresh();</script>