Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 6982eb4fb4d44105dc8b44e898d452d4e5d32c82 223 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 {logger} from '#/logger' 16import {updateProfileShadow} from '#/state/cache/profile-shadow' 17import {useLiveNowConfig} from '#/state/service-config' 18import {useAgent, useSession} from '#/state/session' 19import {pdsAgent} from '#/state/session/agent' 20import * as Toast from '#/view/com/util/Toast' 21import {useDialogContext} from '#/components/Dialog' 22import {getLiveServiceNames} from '#/components/live/utils' 23 24export function useLiveLinkMetaQuery(url: string | null) { 25 const liveNowConfig = useLiveNowConfig() 26 const {_} = useLingui() 27 28 const agent = useAgent() 29 return useQuery({ 30 enabled: !!url, 31 queryKey: ['link-meta', url], 32 queryFn: async () => { 33 if (!url) return undefined 34 const urlp = new URL(url) 35 if (!liveNowConfig.allowedDomains.has(urlp.hostname)) { 36 const {formatted} = getLiveServiceNames(liveNowConfig.allowedDomains) 37 throw new Error( 38 _( 39 msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`, 40 ), 41 ) 42 } 43 44 return await getLinkMeta(agent, url) 45 }, 46 }) 47} 48 49export function useUpsertLiveStatusMutation( 50 duration: number, 51 linkMeta: LinkMeta | null | undefined, 52 createdAt?: string, 53) { 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 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 pdsAgent(agent) 114 .com.atproto.repo.getRecord({repo, collection, rkey: 'self'}) 115 .catch(_e => undefined) 116 117 await pdsAgent(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 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 logger.metric( 146 'live:edit', 147 {duration: record.durationMinutes}, 148 {statsig: true}, 149 ) 150 } else { 151 logger.metric( 152 'live:create', 153 {duration: record.durationMinutes}, 154 {statsig: true}, 155 ) 156 } 157 158 Toast.show(_(msg`You are now live!`)) 159 control.close(() => { 160 if (!currentAccount) return 161 162 const expiresAt = new Date(record.createdAt) 163 expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes) 164 165 updateProfileShadow(queryClient, currentAccount.did, { 166 status: { 167 $type: 'app.bsky.actor.defs#statusView', 168 status: 'app.bsky.actor.status#live', 169 isActive: true, 170 expiresAt: expiresAt.toISOString(), 171 embed: 172 record.embed && image 173 ? { 174 $type: 'app.bsky.embed.external#view', 175 external: { 176 ...record.embed.external, 177 $type: 'app.bsky.embed.external#viewExternal', 178 thumb: image, 179 }, 180 } 181 : undefined, 182 record, 183 }, 184 }) 185 }) 186 }, 187 }) 188} 189 190export function useRemoveLiveStatusMutation() { 191 const {currentAccount} = useSession() 192 const agent = useAgent() 193 const queryClient = useQueryClient() 194 const control = useDialogContext() 195 const {_} = useLingui() 196 197 return useMutation({ 198 mutationFn: async () => { 199 if (!currentAccount) throw new Error('Not logged in') 200 201 await agent.app.bsky.actor.status.delete({ 202 repo: currentAccount.did, 203 rkey: 'self', 204 }) 205 }, 206 onError: (e: any) => { 207 logger.error(`Failed to remove live status`, { 208 safeMessage: e, 209 }) 210 }, 211 onSuccess: () => { 212 logger.metric('live:remove', {}, {statsig: true}) 213 Toast.show(_(msg`You are no longer live`)) 214 control.close(() => { 215 if (!currentAccount) return 216 217 updateProfileShadow(queryClient, currentAccount.did, { 218 status: undefined, 219 }) 220 }) 221 }, 222 }) 223}