Personal blog finxol.io
blog

feat: show reply replies, max depth 2

finxol.io e61c1bea e4c3a778

verified
+95 -7
+35 -7
app/components/BskyComments.vue
··· 31 31 <div class="md:w-[80%] mx-auto mt-16"> 32 32 <div class="flex items-baseline gap-4"> 33 33 <h3 class="font-bold text-xl">Join the conversation!</h3> 34 - <p class="text-gray-500 text-sm"> 34 + <p class="text-gray-500 text-sm" title="Replies"> 35 35 <Icon name="ri:reply-line" class="-mb-[2px] mr-1" /> 36 36 {{post.post.replyCount}} 37 37 </p> 38 - <p class="text-gray-500 text-sm"> 38 + <p class="text-gray-500 text-sm" title="Likes"> 39 39 <Icon name="ri:heart-3-line" class="-mb-[2px] mr-1" /> 40 40 <span> 41 41 {{post.post.likeCount}} 42 42 </span> 43 43 </p> 44 - <p class="text-gray-500 text-sm"> 44 + <p class="text-gray-500 text-sm" title="Bookmarks"> 45 45 <Icon name="ri:bookmark-line" class="-mb-[2px] mr-1" /> 46 46 {{post.post.bookmarkCount}} 47 47 </p> ··· 61 61 </div> 62 62 63 63 <div v-else v-for="reply in post.replies" class="mt-6"> 64 - <a :href="`https://bsky.app/profile/${reply.post.author.handle}`" class="flex items-center gap-2 text-blue-500 hover:underline w-fit"> 64 + <BskyPost :post="reply" :depth="0" /> 65 + 66 + <!-- <a :href="`https://bsky.app/profile/${reply.post.author.handle}`" class="flex items-center gap-2 text-blue-500 hover:underline w-fit"> 65 67 <img :src="reply.post.author.avatar" :alt="reply.post.author.displayName" class="size-8 rounded-full" /> 66 68 <span> 67 69 {{ reply.post.author.displayName }} ··· 69 71 </a> 70 72 <div class="ml-10">{{ reply.post.record.text }}</div> 71 73 <div class="flex items-baseline gap-4 ml-10 mt-2"> 72 - <p class="text-gray-500 text-sm"> 74 + <p class="text-gray-500 text-sm" title="Replies"> 73 75 <Icon name="ri:reply-line" class="-mb-[2px] mr-1" /> 74 76 {{reply.post.replyCount}} 75 77 </p> 76 - <p class="text-gray-500 text-sm"> 78 + <p class="text-gray-500 text-sm" title="Likes"> 77 79 <Icon name="ri:heart-3-line" class="-mb-[2px] mr-1" /> 78 80 <span> 79 81 {{reply.post.likeCount}} 80 82 </span> 81 83 </p> 82 - <p class="text-gray-500 text-sm"> 84 + <p class="text-gray-500 text-sm" title="Bookmarks"> 83 85 <Icon name="ri:bookmark-line" class="-mb-[2px] mr-1" /> 84 86 {{reply.post.bookmarkCount}} 85 87 </p> 86 88 </div> 89 + 90 + <div v-for="rep in reply.replies" class="mt-6 ml-10"> 91 + <a :href="`https://bsky.app/profile/${rep.post.author.handle}`" class="flex items-center gap-2 text-blue-500 hover:underline w-fit"> 92 + <img :src="rep.post.author.avatar" :alt="rep.post.author.displayName" class="size-8 rounded-full" /> 93 + <span> 94 + {{ rep.post.author.displayName }} 95 + </span> 96 + </a> 97 + <div class="ml-10">{{ rep.post.record.text }}</div> 98 + <div class="flex items-baseline gap-4 ml-10 mt-2"> 99 + <p class="text-gray-500 text-sm" title="Replies"> 100 + <Icon name="ri:reply-line" class="-mb-[2px] mr-1" /> 101 + {{rep.post.replyCount}} 102 + </p> 103 + <p class="text-gray-500 text-sm" title="Likes"> 104 + <Icon name="ri:heart-3-line" class="-mb-[2px] mr-1" /> 105 + <span> 106 + {{rep.post.likeCount}} 107 + </span> 108 + </p> 109 + <p class="text-gray-500 text-sm" title="Bookmarks"> 110 + <Icon name="ri:bookmark-line" class="-mb-[2px] mr-1" /> 111 + {{rep.post.bookmarkCount}} 112 + </p> 113 + </div> 114 + </div> --> 87 115 </div> 88 116 </div> 89 117 </div>
+52
app/components/BskyPost.vue
··· 1 + <script setup lang="ts"> 2 + import type { AppBskyFeedDefs } from "@atcute/bluesky"; 3 + import { extractPostId } from "~/util/atproto"; 4 + 5 + const props = defineProps<{ 6 + post: AppBskyFeedDefs.ThreadViewPost; 7 + depth: number; 8 + }>(); 9 + const { post, depth } = toRefs(props); 10 + 11 + const MAX_DEPTH = 2; // Max number of replies to a reply 12 + </script> 13 + 14 + <template> 15 + <div v-if="post && depth <= MAX_DEPTH" :class="['mt-6', depth > 0 ? 'ml-10' : '']"> 16 + <a :href="`https://bsky.app/profile/${post.post.author.handle}`" class="flex items-center gap-2 text-blue-500 hover:underline w-fit"> 17 + <img :src="post.post.author.avatar" :alt="post.post.author.displayName" class="size-8 rounded-full" /> 18 + <span> 19 + {{ post.post.author.displayName }} 20 + </span> 21 + </a> 22 + <div class="ml-10">{{ post.post.record.text }}</div> 23 + <div class="flex items-baseline gap-4 ml-10 mt-2"> 24 + <p class="text-gray-500 text-sm" title="Replies"> 25 + <Icon name="ri:reply-line" class="-mb-[2px] mr-1" /> 26 + {{post.post.replyCount}} 27 + </p> 28 + <p class="text-gray-500 text-sm" title="Likes"> 29 + <Icon name="ri:heart-3-line" class="-mb-[2px] mr-1" /> 30 + <span> 31 + {{post.post.likeCount}} 32 + </span> 33 + </p> 34 + <p class="text-gray-500 text-sm" title="Bookmarks"> 35 + <Icon name="ri:bookmark-line" class="-mb-[2px] mr-1" /> 36 + {{post.post.bookmarkCount}} 37 + </p> 38 + </div> 39 + 40 + <div v-if="post.replies"> 41 + <div v-if="depth === MAX_DEPTH"> 42 + <a :href="`https://bsky.app/profile/${post.post.author.handle}/post/${extractPostId(post.post.uri)}`" class="text-gray-500 text-sm flex items-center gap-2 mt-4 ml-10"> 43 + View more replies on Bluesky 44 + <Icon name='ri:arrow-drop-right-line' /> 45 + </a> 46 + </div> 47 + <div v-for="reply in post.replies"> 48 + <BskyPost v-if="reply.$type === 'app.bsky.feed.defs#threadViewPost'" :post="reply" :depth="depth + 1" /> 49 + </div> 50 + </div> 51 + </div> 52 + </template>
+8
app/util/atproto.ts
··· 36 36 37 37 return { $type: "app.bsky.feed.defs#notFoundPost" }; 38 38 } 39 + 40 + export function extractPostId(uri: ResourceUri) { 41 + if (uri.includes("app.bsky.feed.post")) { 42 + const parts = uri.split("/"); 43 + return parts.at(-1); 44 + } 45 + return ""; 46 + }