forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 type $Typed,
3 type AppBskyActorStatus,
4 type AppBskyEmbedExternal,
5 ComAtprotoRepoPutRecord,
6} from '@atproto/api'
7import {retry} from '@atproto/common-web'
8import {msg} from '@lingui/macro'
9import {useLingui} from '@lingui/react'
10import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
11
12import {uploadBlob} from '#/lib/api'
13import {imageToThumb} from '#/lib/api/resolve'
14import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
15import {logger} from '#/logger'
16import {updateProfileShadow} from '#/state/cache/profile-shadow'
17import {useLiveNowConfig} from '#/state/service-config'
18import {useAgent, useSession} from '#/state/session'
19import {pdsAgent} from '#/state/session/agent'
20import * as Toast from '#/view/com/util/Toast'
21import {useDialogContext} from '#/components/Dialog'
22import {getLiveServiceNames} from '#/components/live/utils'
23
24export function useLiveLinkMetaQuery(url: string | null) {
25 const liveNowConfig = useLiveNowConfig()
26 const {_} = useLingui()
27
28 const agent = useAgent()
29 return useQuery({
30 enabled: !!url,
31 queryKey: ['link-meta', url],
32 queryFn: async () => {
33 if (!url) return undefined
34 const urlp = new URL(url)
35 if (!liveNowConfig.allowedDomains.has(urlp.hostname)) {
36 const {formatted} = getLiveServiceNames(liveNowConfig.allowedDomains)
37 throw new Error(
38 _(
39 msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`,
40 ),
41 )
42 }
43
44 return await getLinkMeta(agent, url)
45 },
46 })
47}
48
49export function useUpsertLiveStatusMutation(
50 duration: number,
51 linkMeta: LinkMeta | null | undefined,
52 createdAt?: string,
53) {
54 const {currentAccount} = useSession()
55 const agent = useAgent()
56 const queryClient = useQueryClient()
57 const control = useDialogContext()
58 const {_} = useLingui()
59
60 return useMutation({
61 mutationFn: async () => {
62 if (!currentAccount) throw new Error('Not logged in')
63
64 let embed: $Typed<AppBskyEmbedExternal.Main> | undefined
65
66 if (linkMeta) {
67 let thumb
68
69 if (linkMeta.image) {
70 try {
71 const img = await imageToThumb(linkMeta.image)
72 if (img) {
73 const blob = await uploadBlob(
74 agent,
75 img.source.path,
76 img.source.mime,
77 )
78 thumb = blob.data.blob
79 }
80 } catch (e: any) {
81 logger.error(`Failed to upload thumbnail for live status`, {
82 url: linkMeta.url,
83 image: linkMeta.image,
84 safeMessage: e,
85 })
86 }
87 }
88
89 embed = {
90 $type: 'app.bsky.embed.external',
91 external: {
92 $type: 'app.bsky.embed.external#external',
93 title: linkMeta.title ?? '',
94 description: linkMeta.description ?? '',
95 uri: linkMeta.url,
96 thumb,
97 },
98 }
99 }
100
101 const record = {
102 $type: 'app.bsky.actor.status',
103 createdAt: createdAt ?? new Date().toISOString(),
104 status: 'app.bsky.actor.status#live',
105 durationMinutes: duration,
106 embed,
107 } satisfies AppBskyActorStatus.Record
108
109 const upsert = async () => {
110 const repo = currentAccount.did
111 const collection = 'app.bsky.actor.status'
112
113 const existing = await pdsAgent(agent)
114 .com.atproto.repo.getRecord({repo, collection, rkey: 'self'})
115 .catch(_e => undefined)
116
117 await pdsAgent(agent).com.atproto.repo.putRecord({
118 repo,
119 collection,
120 rkey: 'self',
121 record,
122 swapRecord: existing?.data.cid || null,
123 })
124 }
125
126 await retry(upsert, {
127 maxRetries: 5,
128 retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError,
129 })
130
131 return {
132 record,
133 image: linkMeta?.image,
134 }
135 },
136 onError: (e: any) => {
137 logger.error(`Failed to upsert live status`, {
138 url: linkMeta?.url,
139 image: linkMeta?.image,
140 safeMessage: e,
141 })
142 },
143 onSuccess: ({record, image}) => {
144 if (createdAt) {
145 logger.metric(
146 'live:edit',
147 {duration: record.durationMinutes},
148 {statsig: true},
149 )
150 } else {
151 logger.metric(
152 'live:create',
153 {duration: record.durationMinutes},
154 {statsig: true},
155 )
156 }
157
158 Toast.show(_(msg`You are now live!`))
159 control.close(() => {
160 if (!currentAccount) return
161
162 const expiresAt = new Date(record.createdAt)
163 expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes)
164
165 updateProfileShadow(queryClient, currentAccount.did, {
166 status: {
167 $type: 'app.bsky.actor.defs#statusView',
168 status: 'app.bsky.actor.status#live',
169 isActive: true,
170 expiresAt: expiresAt.toISOString(),
171 embed:
172 record.embed && image
173 ? {
174 $type: 'app.bsky.embed.external#view',
175 external: {
176 ...record.embed.external,
177 $type: 'app.bsky.embed.external#viewExternal',
178 thumb: image,
179 },
180 }
181 : undefined,
182 record,
183 },
184 })
185 })
186 },
187 })
188}
189
190export function useRemoveLiveStatusMutation() {
191 const {currentAccount} = useSession()
192 const agent = useAgent()
193 const queryClient = useQueryClient()
194 const control = useDialogContext()
195 const {_} = useLingui()
196
197 return useMutation({
198 mutationFn: async () => {
199 if (!currentAccount) throw new Error('Not logged in')
200
201 await agent.app.bsky.actor.status.delete({
202 repo: currentAccount.did,
203 rkey: 'self',
204 })
205 },
206 onError: (e: any) => {
207 logger.error(`Failed to remove live status`, {
208 safeMessage: e,
209 })
210 },
211 onSuccess: () => {
212 logger.metric('live:remove', {}, {statsig: true})
213 Toast.show(_(msg`You are no longer live`))
214 control.close(() => {
215 if (!currentAccount) return
216
217 updateProfileShadow(queryClient, currentAccount.did, {
218 status: undefined,
219 })
220 })
221 },
222 })
223}