-
-
Save rudiv/246afc9cffcf3be749da0341e4acbf03 to your computer and use it in GitHub Desktop.
Integrate Bluesky replies as your blog's comment section in Svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts" module> | |
import { AppBskyFeedDefs, AppBskyFeedPost, type AppBskyFeedGetPostThread } from "@atproto/api"; | |
import type { ThreadViewPost } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; | |
export type CommentsProps = { | |
did: string; | |
threadId: string; | |
}; | |
export type Reply = { | |
post: { | |
uri: string; | |
likeCount?: number; | |
repostCount?: number; | |
replyCount?: number; | |
}; | |
}; | |
export type Thread = { | |
replies: Reply[]; | |
post: { | |
likeCount?: number; | |
repostCount?: number; | |
replyCount?: number; | |
}; | |
}; | |
</script> | |
<script lang="ts"> | |
let { did, threadId }: CommentsProps = $props(); | |
let postUrl = `https://bsky.app/profile/${did}/post/${threadId}`; | |
const getPostThread = async (uri: string) => { | |
const params = new URLSearchParams({ uri }); | |
const res = await fetch( | |
"https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?" + params.toString(), | |
{ | |
method: "GET", | |
headers: { | |
Accept: "application/json" | |
}, | |
cache: "no-store" | |
} | |
); | |
if (!res.ok) { | |
console.error(await res.text()); | |
throw new Error("Failed to fetch post thread"); | |
} | |
const data = (await res.json()) as AppBskyFeedGetPostThread.OutputSchema; | |
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) { | |
throw new Error("Could not find thread"); | |
} | |
return data.thread; | |
}; | |
const sortByLikes = (a: unknown, b: unknown) => { | |
if (!AppBskyFeedDefs.isThreadViewPost(a) || !AppBskyFeedDefs.isThreadViewPost(b)) { | |
return 0; | |
} | |
return (b.post.likeCount ?? 0) - (a.post.likeCount ?? 0); | |
}; | |
let visibleCount = $state(3); | |
const loader: Promise<ThreadViewPost> = getPostThread( | |
`at://${did}/app.bsky.feed.post/${threadId}` | |
); | |
</script> | |
{#await loader} | |
Loading... | |
{:then thread} | |
{@const sortedReplies = thread.replies?.sort(sortByLikes) ?? []} | |
<a href={postUrl} target="bsky" class="flex items-center gap-3"> | |
<span class="flex items-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="pink" viewBox="0 0 24 24" stroke-width="1.5" stroke="pink" class="size-5" color="pink"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" /> | |
</svg> | |
<span class="ml-1">{thread.post.likeCount ?? 0} likes</span> | |
</span> | |
<span class="flex items-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="green" class="size-5"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" /> | |
</svg> | |
<span class="ml-1">{thread.post.repostCount ?? 0} reposts</span> | |
</span> | |
<span class="flex items-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="#7FBADC" viewBox="0 0 24 24" stroke-width="1.5" stroke="#7FBADC" class="size-5"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z" /> | |
</svg> | |
<span class="ml-1">{thread.post.replyCount ?? 0} replies</span> | |
</span> | |
</a> | |
<h2 class="mt-6 text-xl font-bold">Comments</h2> | |
<p class="mt-2 text-sm"> | |
Reply on Bluesky{" "} | |
<a href={postUrl} class="underline" target="_blank" rel="noreferrer noopener"> here </a> | |
to join the conversation. | |
</p> | |
<hr class="mt-2" /> | |
<div class="mt-2 space-y-8"> | |
{#each sortedReplies.slice(0, visibleCount) as reply} | |
{#if AppBskyFeedDefs.isThreadViewPost(reply) && AppBskyFeedPost.isRecord(reply.post.record)} | |
{@render renderComment(reply)} | |
{/if} | |
{/each} | |
{#if visibleCount < sortedReplies.length} | |
<button onclick={() => (visibleCount += 5)} class="mt-2 text-sm text-blue-500 underline"> | |
Show more comments | |
</button> | |
{/if} | |
</div> | |
{:catch error} | |
<div class="rounded-md border border-red-200 bg-red-50 p-4">Something went wrong {error}</div> | |
{/await} | |
{#snippet renderComment(comment: AppBskyFeedDefs.ThreadViewPost)} | |
{@const author = comment.post.author} | |
<div class="my-4 text-sm"> | |
<div class="flex max-w-xl flex-col gap-2"> | |
<a | |
class="flex flex-row items-center gap-2 hover:underline" | |
href={`https://bsky.app/profile/${author.did}`} | |
target="_blank" | |
rel="noreferrer noopener"> | |
{#if author.avatar} | |
<img src={comment.post.author.avatar} alt="avatar" class="h-4 w-4 shrink-0 rounded-full bg-gray-300" /> | |
{:else} | |
<div class="h-4 w-4 shrink-0 rounded-full bg-gray-300"></div> | |
{/if} | |
<p class="line-clamp-1"> | |
{author.displayName ?? author.handle} | |
<span class="text-gray-500">@{author.handle}</span> | |
</p> | |
</a> | |
<a | |
href={`https://bsky.app/profile/${author.did}/post/${comment.post.uri.split("/").pop()}`} | |
target="_blank" | |
rel="noreferrer noopener"> | |
<p>{comment.post.record.text!}</p> | |
<div | |
class="mt-2 flex w-full max-w-[150px] flex-row items-center justify-between opacity-60"> | |
<div class="flex flex-row items-center gap-1.5"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z" /> | |
</svg> | |
<p class="text-xs">{comment.post.replyCount ?? 0}</p> | |
</div> | |
<div class="flex flex-row items-center gap-1.5"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" /> | |
</svg> | |
<p class="text-xs">{comment.post.repostCount ?? 0}</p> | |
</div> | |
<div class="flex flex-row items-center gap-1.5"> | |
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" /> | |
</svg> | |
<p class="text-xs">{comment.post.likeCount ?? 0}</p> | |
</div> | |
</div> | |
</a> | |
</div> | |
{#if comment.replies && comment.replies.length > 0} | |
{@const sortedReplies = comment.replies.sort(sortByLikes)} | |
<div class="border-l-2 border-neutral-600 pl-2"> | |
{#each sortedReplies as reply} | |
{#if AppBskyFeedDefs.isThreadViewPost(reply) && AppBskyFeedPost.isRecord(reply.post.record)} | |
{@render renderComment(reply)} | |
{/if} | |
{/each} | |
</div> | |
{/if} | |
</div> | |
{/snippet} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment