Skip to content

Instantly share code, notes, and snippets.

@rudiv
Forked from emilyliu7321/bluesky-comments.tsx
Last active December 2, 2024 10:22
Show Gist options
  • Save rudiv/246afc9cffcf3be749da0341e4acbf03 to your computer and use it in GitHub Desktop.
Save rudiv/246afc9cffcf3be749da0341e4acbf03 to your computer and use it in GitHub Desktop.
Integrate Bluesky replies as your blog's comment section in Svelte
<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