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