wip bsky client for the web & android
bbell.vt3e.cat
1import { defineStore } from 'pinia'
2import { shallowRef, reactive } from 'vue'
3import type { ComAtprotoRepoStrongRef } from '@atcute/atproto'
4import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky'
5import { ok } from '@atcute/client'
6
7import { useAuthStore } from './auth'
8
9type PostView = AppBskyFeedDefs.PostView
10
11export const usePostStore = defineStore('posts', () => {
12 const auth = useAuthStore()
13
14 const posts = shallowRef(new Map<string, PostView>())
15
16 function mergePost(post: PostView): PostView {
17 const existing = posts.value.get(post.uri)
18
19 if (existing) {
20 Object.assign(existing, post)
21 return existing
22 } else {
23 const reactivePost = reactive(post)
24 posts.value.set(post.uri, reactivePost)
25 return reactivePost
26 }
27 }
28
29 async function toggleLike(post: PostView) {
30 if (!auth.isAuthenticated) return
31 if (!auth.session) return
32
33 const uri = post.uri
34 const cid = post.cid
35
36 const originalLike = post.viewer?.like
37 const originalCount = post.likeCount || 0
38
39 if (!post.viewer) post.viewer = {}
40
41 if (originalLike) {
42 post.viewer.like = undefined
43 post.likeCount = originalCount - 1
44 } else {
45 post.viewer.like = 'at://did:plc:pending/like'
46 post.likeCount = originalCount + 1
47 }
48
49 try {
50 const rpc = auth.getRpc()
51 if (originalLike) {
52 await rpc.post('com.atproto.repo.deleteRecord', {
53 input: {
54 collection: 'app.bsky.feed.like',
55 repo: auth.session?.info.sub,
56 rkey: originalLike.split('/').pop()!,
57 },
58 })
59 } else {
60 const data = ok(
61 await rpc.post('com.atproto.repo.createRecord', {
62 input: {
63 collection: 'app.bsky.feed.like',
64 repo: auth.session?.info.sub,
65 record: {
66 $type: 'app.bsky.feed.like',
67 subject: { uri, cid },
68 createdAt: new Date().toISOString(),
69 },
70 },
71 }),
72 )
73
74 const storedPost = posts.value.get(uri)
75 if (storedPost && storedPost.viewer) {
76 storedPost.viewer.like = data.uri
77 }
78 }
79 } catch (err) {
80 console.error('Failed to toggle like', err)
81 const storedPost = posts.value.get(uri)
82 if (storedPost && storedPost.viewer) {
83 storedPost.viewer.like = originalLike
84 storedPost.likeCount = originalCount
85 }
86 }
87 }
88
89 async function toggleBookmark(post: PostView, silent = false) {
90 if (!auth.isAuthenticated) return
91 if (!auth.session) return
92
93 const uri = post.uri
94 const cid = post.cid
95
96 const originalBookmarked = post.viewer?.bookmarked
97 const originalCount = post.bookmarkCount || 0
98
99 if (!post.viewer) post.viewer = {}
100
101 if (originalBookmarked) {
102 post.viewer.bookmarked = false
103 post.bookmarkCount = originalCount - 1
104 } else {
105 post.viewer.bookmarked = true
106 post.bookmarkCount = originalCount + 1
107 }
108
109 try {
110 const rpc = auth.getRpc()
111 if (originalBookmarked) {
112 const res = await rpc.post('app.bsky.bookmark.deleteBookmark', {
113 input: {
114 uri: uri,
115 },
116 as: null,
117 })
118 } else {
119 const res = await rpc.post('app.bsky.bookmark.createBookmark', {
120 input: {
121 cid: cid,
122 uri: uri,
123 },
124 as: null,
125 })
126 }
127 } catch (err) {
128 console.error('Failed to toggle bookmark', err)
129 const storedPost = posts.value.get(uri)
130 if (storedPost && storedPost.viewer) {
131 storedPost.viewer.bookmarked = originalBookmarked
132 storedPost.bookmarkCount = originalCount
133 }
134 }
135 }
136
137 async function toggleRepost(post: PostView) {
138 if (!auth.isAuthenticated) return
139
140 const uri = post.uri
141 const cid = post.cid
142
143 const originalRepost = post.viewer?.repost
144 const originalCount = post.repostCount || 0
145
146 if (!post.viewer) post.viewer = {}
147
148 if (originalRepost) {
149 post.viewer.repost = undefined
150 post.repostCount = originalCount - 1
151 } else {
152 post.viewer.repost = 'at://did:plc:pending/repost'
153 post.repostCount = originalCount + 1
154 }
155
156 try {
157 const rpc = auth.getRpc()
158 if (originalRepost) {
159 await ok(
160 rpc.post('com.atproto.repo.deleteRecord', {
161 input: {
162 collection: 'app.bsky.feed.repost',
163 repo: auth.session!.info.sub,
164 rkey: originalRepost.split('/').pop()!,
165 },
166 }),
167 )
168 } else {
169 const data = ok(
170 await rpc.post('com.atproto.repo.createRecord', {
171 input: {
172 collection: 'app.bsky.feed.repost',
173 repo: auth.session!.info.sub,
174 record: {
175 $type: 'app.bsky.feed.repost',
176 subject: { uri, cid },
177 createdAt: new Date().toISOString(),
178 },
179 },
180 }),
181 )
182 const storedPost = posts.value.get(uri)
183 if (storedPost && storedPost.viewer) {
184 storedPost.viewer.repost = data.uri
185 }
186 }
187 } catch (err) {
188 console.error('Failed to toggle repost', err)
189 const storedPost = posts.value.get(uri)
190 if (storedPost && storedPost.viewer) {
191 storedPost.viewer.repost = originalRepost
192 storedPost.repostCount = originalCount
193 }
194 }
195 }
196
197 async function createPost(args: {
198 text: string
199 embeds?: AppBskyFeedPost.Main['embed']
200 reply?: {
201 parent: ComAtprotoRepoStrongRef.Main
202 root: ComAtprotoRepoStrongRef.Main
203 }
204 }) {
205 const { reply, text, embeds } = args
206 if (!auth.isAuthenticated || !auth.session) throw new Error('Not authenticated')
207
208 const rpc = auth.getRpc()
209 const record: AppBskyFeedPost.Main = {
210 $type: 'app.bsky.feed.post',
211 text,
212 createdAt: new Date().toISOString(),
213 embed: embeds,
214 reply: reply,
215 }
216
217 const data = await ok(
218 rpc.post('com.atproto.repo.createRecord', {
219 input: {
220 collection: 'app.bsky.feed.post',
221 repo: auth.session.info.sub,
222 record,
223 },
224 }),
225 )
226
227 return data
228 }
229
230 return {
231 posts,
232 mergePost,
233 createPost,
234
235 toggleLike,
236 toggleBookmark,
237 toggleRepost,
238 }
239})