Bluesky app fork with some witchin' additions 馃挮
at main 218 lines 6.3 kB view raw
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}