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

Configure Feed

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

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