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