diff --git a/js/bluesky-thread.js b/js/bluesky-thread.js new file mode 100644 index 0000000..edcc866 --- /dev/null +++ b/js/bluesky-thread.js @@ -0,0 +1,69 @@ +/** + * 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; + } + }, + })); +});