bluesky client without react native baggage written in sveltekit
1<script lang="ts">
2 import RichText from './RichText.svelte';
3 import Avatar from './Avatar.svelte';
4 import { getClient } from '$lib/atproto';
5 import { getUserContext } from '$lib/context';
6 import type { PostView } from '@atcute/bluesky/types/app/feed/defs';
7
8 let { post }: { post: PostView } = $props();
9 const user = getUserContext();
10 let liked = $state(!!post.viewer?.like);
11 let reposted = $state(!!post.viewer?.repost);
12 let likeCount = $derived(post.viewer?.like ? (post.likeCount ?? 0) - 1 : post.likeCount);
13 let repostCount = $derived(post.viewer?.repost ? (post.repostCount ?? 0) - 1 : post.repostCount);
14 const isAuthenticated = !!user.profile?.did;
15
16 async function likePost() {
17 liked = true;
18 const client = await getClient();
19
20 if (!user.profile?.did) {
21 liked = false;
22 throw new Error('you must be authenticated to do this action');
23 }
24
25 const { ok } = await client.post('com.atproto.repo.createRecord', {
26 input: {
27 collection: 'app.bsky.feed.like',
28 record: {
29 $type: 'app.bsky.feed.like',
30 createdAt: new Date().toISOString(),
31 subject: {
32 cid: post.cid,
33 uri: post.uri
34 }
35 },
36 repo: user.profile.did
37 }
38 });
39
40 if (!ok) {
41 liked = false;
42 throw new Error('failed to like the post');
43 }
44 }
45
46 async function unlikePost() {
47 liked = false;
48 const client = await getClient();
49
50 if (!user.profile?.did) {
51 liked = true;
52 throw new Error('you must be authenticated to do this action (how did you even like this)');
53 }
54 const rkey = post.uri.split('/').at(-1);
55 if (!rkey) {
56 liked = true;
57 throw new Error("couldn't properly extract rkey");
58 }
59 const { ok } = await client.post('com.atproto.repo.deleteRecord', {
60 input: {
61 collection: 'app.bsky.feed.like',
62 rkey,
63 repo: user.profile.did
64 }
65 });
66
67 if (!ok) {
68 liked = true;
69 throw new Error('failed to unlike the post');
70 }
71 console.log('liked post!');
72 }
73</script>
74
75<article
76 class="flex border-x border-b border-post-border bg-body-background pt-2 pr-4 pb-2 pl-2.5 hover:bg-item-hover"
77>
78 <div class="mr-2.5 ml-2 shrink-0">
79 <Avatar user={post.author} />
80 </div>
81 <div>
82 <div class="mb-1">
83 <a href="#">
84 <b>{post.author.displayName || post.author.handle}</b>
85 <span class="text-secondary-text">@{post.author.handle}</span>
86 </a>
87 </div>
88 <RichText text={post.record.text} facets={post.record.facets} />
89 {#if post.embed}
90 {#if post.embed.$type === 'app.bsky.embed.images#view'}
91 {#each post.embed.images as image (image.thumb)}
92 <img class="aspect-[1.23151 / 1] my-2 rounded-xl" src={image.thumb} alt={image.alt} />
93 {/each}
94 {/if}
95 {/if}
96 <div class="mt-0.5 flex w-full">
97 <div class="flex w-[320px] max-w-[320px] justify-between">
98 <div class="grow">
99 <button class="flex items-center gap-1 py-1.25 pr-1.25"
100 ><span class="text-4.5 icon-[boxicons--message-reply] h-4.5 w-4.5"></span>
101 {post.replyCount}</button
102 >
103 </div>
104 {#if reposted}
105 <div class="flex grow items-center gap-1">
106 <span class="text-4.5 icon-[mdi--repost] h-4.5 w-4.5 text-green-500"></span>
107 {(repostCount ?? 0) + 1}
108 </div>
109 {:else}
110 <div class="flex grow items-center gap-1">
111 <span class="text-4.5 icon-[mdi--repost] h-4.5 w-4.5"></span>
112 {repostCount}
113 </div>
114 {/if}
115 {#if !isAuthenticated}
116 <div class="flex grow items-center gap-1">
117 <span class="text-4.5 icon-[icon-park-solid--like] h-4.5 w-4.5 text-gray-400"></span>
118 <span aria-hidden={true}>{likeCount}</span>
119 </div>
120 {:else if liked}
121 <button
122 aria-label={`Unlike this post, {likeCount+1} likes`}
123 aria-pressed={true}
124 class="flex grow items-center gap-1 hover:cursor-pointer"
125 onclick={unlikePost}
126 >
127 <span class="text-4.5 icon-[icon-park-solid--like] h-4.5 w-4.5 text-red-500"></span>
128 <span aria-hidden={true}>{(likeCount ?? 0) + 1}</span>
129 </button>
130 {:else}
131 <button
132 aria-label={`Like this post, {likeCount} likes`}
133 aria-pressed={false}
134 class="flex grow items-center gap-1 hover:cursor-pointer"
135 onclick={likePost}
136 >
137 <span class="text-4.5 icon-[icon-park-outline--like] h-4.5 w-4.5"></span>
138 <span aria-hidden={true}>{likeCount}</span>
139 </button>
140 {/if}
141 </div>
142 </div>
143 </div>
144</article>