Skip to content

Instantly share code, notes, and snippets.

@santisbon
Forked from emilyliu7321/bluesky-comments.tsx
Created November 25, 2024 06:20
Show Gist options
  • Save santisbon/0c8068186f8b45f092162c0891b3d829 to your computer and use it in GitHub Desktop.
Save santisbon/0c8068186f8b45f092162c0891b3d829 to your computer and use it in GitHub Desktop.
Integrate Bluesky replies as your blog's comment section
"use client";
/* eslint-disable @next/next/no-img-element */
import Link from "next/link";
import { useState, useEffect } from 'react';
import {
AppBskyFeedDefs,
AppBskyFeedPost,
type AppBskyFeedGetPostThread,
} from "@atproto/api";
interface Props {
uri?: string;
}
type Reply = {
post: {
uri: string;
likeCount?: number;
repostCount?: number;
replyCount?: number,
};
};
type Thread = {
replies: Reply[];
post: {
likeCount?: number;
repostCount?: number;
replyCount?:number;
};
};
// Function to fetch the thread data
const fetchThreadData = async (uri, setThread, setError) => {
try {
const thread = await getPostThread(uri);
setThread(thread);
} catch (err) {
setError('Error loading comments');
}
};
export const CommentSection = ({ uri }) => {
if (!uri) return <div />;
const [, , did, _, rkey] = uri.split("/");
const postUrl = `https://bsky.app/profile/${did}/post/${rkey}`;
const [thread, setThread] = useState<Thread | null>(null);
const [error, setError] = useState<string | null>(null);
const [visibleCount, setVisibleCount] = useState(3);
useEffect(() => {
fetchThreadData(uri, setThread, setError);
}, [uri]);
if (error) {
return <p className="text-center">{error}</p>;
}
if (!thread) {
return <p className="text-center">Loading comments...</p>;
}
if (!thread.replies || thread.replies.length === 0) {
return <div />;
}
const showMore = () => {
setVisibleCount((prevCount) => prevCount + 5);
};
console.log("thread: ", thread.post);
const sortedReplies = thread.replies.sort(sortByLikes);
return (
<div>
<Link href={postUrl} target="_blank">
<p className="flex items-center hover:underline gap-2 text-lg">
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="pink" viewBox="0 0 24 24" stroke-width="1.5" stroke="pink" className="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 className="ml-1">{thread.post.likeCount ?? 0} likes</span>
</span>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="green" className="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 className="ml-1">{thread.post.repostCount ?? 0} reposts</span>
</span>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="#7FBADC" viewBox="0 0 24 24" stroke-width="1.5" stroke="#7FBADC" className="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 className="ml-1">{thread.post.replyCount ?? 0} replies</span>
</span>
</p>
</Link>
<h2 className="mt-6 text-xl font-bold">Comments</h2>
<p className="mt-2 text-sm">
Reply on Bluesky{" "}
<Link
href={postUrl}
className="underline"
target="_blank"
rel="noreferrer noopener"
>
here
</Link>{" "}
to join the conversation.
</p>
<hr className="mt-2" />
<div className="mt-2 space-y-8">
{sortedReplies.slice(0, visibleCount).map((reply) => {
if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
return <Comment key={reply.post.uri} comment={reply} />;
})}
{visibleCount < sortedReplies.length && (
<button onClick={showMore} className="mt-2 text-sm text-blue-500 underline">
Show more comments
</button>
)}
</div>
</div>
);
};
const Comment = ({ comment }: { comment: AppBskyFeedDefs.ThreadViewPost }) => {
const author = comment.post.author;
const avatarClassName = "h-4 w-4 shrink-0 rounded-full bg-gray-300";
if (!AppBskyFeedPost.isRecord(comment.post.record)) return null;
return (
<div className="my-4 text-sm">
<div className="flex max-w-xl flex-col gap-2">
<Link
className="flex flex-row items-center gap-2 hover:underline"
href={`https://bsky.app/profile/${author.did}`}
target="_blank"
rel="noreferrer noopener"
>
{author.avatar ? (
<img
src={comment.post.author.avatar}
alt="avatar"
className={avatarClassName}
/>
) : (
<div className={avatarClassName} />
)}
<p className="line-clamp-1">
{author.displayName ?? author.handle}{" "}
<span className="text-gray-500">@{author.handle}</span>
</p>
</Link>
<Link
href={`https://bsky.app/profile/${author.did}/post/${comment.post.uri.split("/").pop()}`}
target="_blank"
rel="noreferrer noopener"
>
<p>{comment.post.record.text}</p>
<Actions post={comment.post} />
</Link>
</div>
{comment.replies && comment.replies.length > 0 && (
<div className="border-l-2 border-neutral-600 pl-2">
{comment.replies.sort(sortByLikes).map((reply) => {
if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
return <Comment key={reply.post.uri} comment={reply} />;
})}
</div>
)}
</div>
);
};
const Actions = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
<div className="mt-2 flex w-full max-w-[150px] flex-row items-center justify-between opacity-60">
<div className="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" className="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 className="text-xs">{post.replyCount ?? 0}</p>
</div>
<div className="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" className="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 className="text-xs">{post.repostCount ?? 0}</p>
</div>
<div className="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" className="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 className="text-xs">{post.likeCount ?? 0}</p>
</div>
</div>
);
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);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment