Bluesky app fork with some witchin' additions 馃挮
at readme-update 216 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 {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}