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