/** * Client-side Bluesky thread fetcher (Alpine.js) * Fetches a Bluesky post thread via the public AT Protocol API * and renders replies in a nested conversation view. */ document.addEventListener("alpine:init", () => { Alpine.data("bskyThread", (bskyUrl) => ({ replies: [], loading: true, async init() { try { const match = bskyUrl.match(/bsky\.app\/profile\/([^/?#]+)\/post\/([^/?#]+)/); if (!match) { this.loading = false; return; } const [, actor, rkey] = match; const atUri = `at://${actor}/app.bsky.feed.post/${rkey}`; const res = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(atUri)}&depth=10` ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); this.replies = this._flatten(data.thread, 0); } catch { // Fail silently — section stays hidden if loading stays true... no, set empty this.replies = []; } finally { this.loading = false; } }, _flatten(node, depth) { const out = []; for (const r of (node.replies || [])) { if (r.$type !== "app.bsky.feed.defs#threadViewPost") continue; const p = r.post; const rkey = p.uri.split("/").pop(); out.push({ depth: Math.min(depth, 3), uri: p.uri, url: `https://bsky.app/profile/${p.author.handle}/post/${rkey}`, author: { name: p.author.displayName || p.author.handle, handle: p.author.handle, avatar: p.author.avatar || "", url: `https://bsky.app/profile/${p.author.handle}`, }, text: p.record.text, published: p.record.createdAt, likeCount: p.likeCount || 0, }); out.push(...this._flatten(r, depth + 1)); } return out; }, formatDate(dateStr) { try { return new Date(dateStr).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric", }); } catch { return dateStr; } }, })); });