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