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.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 ax = useAnalytics()
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 ax.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 agent.com.atproto.repo
114 .getRecord({repo, collection, rkey: 'self'})
115 .catch(_e => undefined)
116
117 await 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 ax.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 ax.metric('live:edit', {duration: record.durationMinutes})
146 } else {
147 ax.metric('live:create', {duration: record.durationMinutes})
148 }
149
150 Toast.show(_(msg`You are now live!`))
151 control.close(() => {
152 if (!currentAccount) return
153
154 const expiresAt = new Date(record.createdAt)
155 expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes)
156
157 updateProfileShadow(queryClient, currentAccount.did, {
158 status: {
159 $type: 'app.bsky.actor.defs#statusView',
160 status: 'app.bsky.actor.status#live',
161 isActive: true,
162 expiresAt: expiresAt.toISOString(),
163 embed:
164 record.embed && image
165 ? {
166 $type: 'app.bsky.embed.external#view',
167 external: {
168 ...record.embed.external,
169 $type: 'app.bsky.embed.external#viewExternal',
170 thumb: image,
171 },
172 }
173 : undefined,
174 record,
175 },
176 })
177 })
178 },
179 })
180}
181
182export function useRemoveLiveStatusMutation() {
183 const ax = useAnalytics()
184 const {currentAccount} = useSession()
185 const agent = useAgent()
186 const queryClient = useQueryClient()
187 const control = useDialogContext()
188 const {_} = useLingui()
189
190 return useMutation({
191 mutationFn: async () => {
192 if (!currentAccount) throw new Error('Not logged in')
193
194 await agent.app.bsky.actor.status.delete({
195 repo: currentAccount.did,
196 rkey: 'self',
197 })
198 },
199 onError: (e: any) => {
200 ax.logger.error(`Failed to remove live status`, {
201 safeMessage: e,
202 })
203 },
204 onSuccess: () => {
205 ax.metric('live:remove', {})
206 Toast.show(_(msg`You are no longer live`))
207 control.close(() => {
208 if (!currentAccount) return
209
210 updateProfileShadow(queryClient, currentAccount.did, {
211 status: undefined,
212 })
213 })
214 },
215 })
216}