feat: first draft at bsky replies

+5 -4
.zed/settings.json
···
"language_servers": ["deno", "..."],
"languages": {
"Vue.js": {
-
"language_servers": ["deno", "..."]
+
"formatter": { "language_server": { "name": "biome" } },
+
"language_servers": ["!deno", "biome", "..."]
},
"JavaScript": {
"formatter": { "language_server": { "name": "biome" } },
-
"language_servers": ["deno", "..."]
+
"language_servers": ["!deno", "..."]
},
"TypeScript": {
"formatter": { "language_server": { "name": "biome" } },
-
"language_servers": ["deno", "..."]
+
"language_servers": ["!deno", "..."]
},
"TSX": {
"formatter": { "language_server": { "name": "biome" } },
-
"language_servers": ["deno", "..."]
+
"language_servers": ["!deno", "..."]
},
"JSON": { "formatter": { "language_server": { "name": "biome" } } },
"JSONC": { "formatter": { "language_server": { "name": "biome" } } },
+52
app/components/BskyComments.vue
···
+
<script setup lang="ts">
+
import { getBskyReplies, type ReplyThread } from "~/util/atproto";
+
+
const props = defineProps({
+
cid: {
+
type: String,
+
required: true
+
}
+
});
+
const { cid } = toRefs(props);
+
+
const data = ref(await getBskyReplies(cid.value));
+
const err = ref("");
+
const post = ref();
+
+
if (data.value.$type === "app.bsky.feed.defs#blockedPost") {
+
err.value = "Post is blocked";
+
}
+
+
if (data.value.$type === "app.bsky.feed.defs#notFoundPost") {
+
err.value = "Post not found";
+
}
+
+
if (data.value.$type === "app.bsky.feed.defs#threadViewPost") {
+
post.value = data.value;
+
}
+
</script>
+
+
<template>
+
<h3>Join the conversation!</h3>
+
+
<div v-if="err">
+
<div>{{ err }}</div>
+
</div>
+
+
<div v-if="post">
+
<div v-if="post.post.replyCount === 0">
+
<div>No replies yet!</div>
+
</div>
+
+
<div v-else>
+
<p>{{post.post.replyCount}} replies</p>
+
+
<div v-for="reply in post.replies">
+
<a :href="`https://bsky.app/profile/${reply.post.author.handle}`" class="text-blue-500 hover:underline">
+
{{ reply.post.author.displayName }}
+
</a>
+
<div>{{ reply.post.record.text }}</div>
+
</div>
+
</div>
+
</div>
+
</template>
+4
app/pages/posts/[...slug].vue
···
<ShareActions :title="post.title" :description="post.description" :author="post.authors[0]?.name" />
+
<Suspense>
+
<BskyComments v-if="post.bskyCid" :cid="post.bskyCid" />
+
</Suspense>
+
</article>
<div v-else class="flex items-center justify-center">
+39
app/util/atproto.ts
···
+
import { Client, simpleFetchHandler } from "@atcute/client";
+
import type { AppBskyFeedDefs } from "@atcute/bluesky";
+
import type { ResourceUri } from "@atcute/lexicons";
+
+
import config from "@/../blog.config";
+
+
const handler = simpleFetchHandler({
+
service: "https://public.api.bsky.app"
+
});
+
const rpc = new Client({ handler });
+
+
export type ReplyThread =
+
| AppBskyFeedDefs.ThreadViewPost
+
| AppBskyFeedDefs.BlockedPost
+
| AppBskyFeedDefs.NotFoundPost;
+
+
export async function getBskyReplies(cid: string) {
+
// uri should be in format: at://did:plc:xxx/app.bsky.feed.post/xxxxx
+
const uri: ResourceUri = `at://${config.authorDid}/app.bsky.feed.post/${cid}`;
+
+
const { ok, data } = await rpc.get("app.bsky.feed.getPostThread", {
+
params: {
+
uri,
+
depth: 10
+
}
+
});
+
+
if (!ok) {
+
console.error(data);
+
console.error("Error fetching thread:", data.error);
+
return { $type: "app.bsky.feed.defs#notFoundPost" };
+
}
+
+
if (ok) {
+
return data.thread;
+
}
+
+
return { $type: "app.bsky.feed.defs#notFoundPost" };
+
}
+1
blog.config.ts
···
site: "https://finxol.io",
title: "finxol's blog",
author: "finxol",
+
authorDid: "did:plc:hpmpe3pzpdtxbmvhlwrevhju",
meta: [
{
name: "description",
+1 -1
content/posts/embracing-atproto-pt-2-tangled-knot.md
···
- atproto
- self-hosting
published: true
-
bskyCid: bafyreid4opjtllapzeyjgrsqcfrzyz2t6wjmxulmkhuh2wc6cyg5bre2su
+
bskyCid: 3lyzhrumfu22n
---
I recently set up my own atproto PDS, for use with Bluesky and all other atproto apps.
+2
globals.ts
···
sharingProviders: SharingProvider[];
title: string;
author: string;
+
authorDid: `did:plc:${string}`;
meta: {
name: string;
content: string;
···
: { bluesky: true, clipboard: true, native: true },
title: config.title ?? "My Blog",
author: config.author ?? "finxol",
+
authorDid: config.authorDid,
meta: config.meta ?? [
{ name: "description", content: "My blog description" }
],
+1
package.json
···
"dependencies": {
"@atcute/bluesky": "^3.2.10",
"@atcute/client": "^4.0.5",
+
"@atcute/lexicons": "^1.2.4",
"@nuxt/content": "^3.8.0",
"@nuxt/icon": "1.11.0",
"@nuxtjs/tailwindcss": "^6.14.0",
+3
pnpm-lock.yaml
···
'@atcute/client':
specifier: ^4.0.5
version: 4.0.5
+
'@atcute/lexicons':
+
specifier: ^1.2.4
+
version: 1.2.4
'@nuxt/content':
specifier: ^3.8.0
version: 3.8.0(@libsql/client@0.15.15)(better-sqlite3@12.4.1)(magicast@0.5.1)
+4 -1
tsconfig.json
···
{
// https://nuxt.com/docs/guide/concepts/typescript
-
"extends": "./.nuxt/tsconfig.json"
+
"extends": "./.nuxt/tsconfig.json",
+
"compilerOptions": {
+
"types": ["@atcute/bluesky"]
+
}
}