From 6620d0ee5a66a06ed03a6cdfac65caa01dc35eec Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Feb 2026 11:13:56 -0600 Subject: [PATCH] Move queries into liveNow --- src/components/live/EditLiveDialog.tsx | 7 +- src/components/live/GoLiveDialog.tsx | 3 +- src/components/live/queries.ts | 217 ------------------------- src/features/liveNow/index.tsx | 216 +++++++++++++++++++++++- 4 files changed, 220 insertions(+), 223 deletions(-) delete mode 100644 src/components/live/queries.ts diff --git a/src/components/live/EditLiveDialog.tsx b/src/components/live/EditLiveDialog.tsx index 7f42e2ca1c..280765eabd 100644 --- a/src/components/live/EditLiveDialog.tsx +++ b/src/components/live/EditLiveDialog.tsx @@ -20,13 +20,14 @@ import * as TextField from '#/components/forms/TextField' import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -import {displayDuration, useDebouncedValue} from '#/features/liveNow' -import {LinkPreview} from './LinkPreview' import { + displayDuration, + useDebouncedValue, useLiveLinkMetaQuery, useRemoveLiveStatusMutation, useUpsertLiveStatusMutation, -} from './queries' +} from '#/features/liveNow' +import {LinkPreview} from './LinkPreview' export function EditLiveDialog({ control, diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx index eb5276a7ab..9bfe9d6013 100644 --- a/src/components/live/GoLiveDialog.tsx +++ b/src/components/live/GoLiveDialog.tsx @@ -20,11 +20,12 @@ import { displayDuration, getLiveServiceNames, useDebouncedValue, + useLiveLinkMetaQuery, useLiveNowConfig, + useUpsertLiveStatusMutation, } from '#/features/liveNow' import type * as bsky from '#/types/bsky' import {LinkPreview} from './LinkPreview' -import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries' export function GoLiveDialog({ control, diff --git a/src/components/live/queries.ts b/src/components/live/queries.ts deleted file mode 100644 index 735c1a5211..0000000000 --- a/src/components/live/queries.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { - type $Typed, - type AppBskyActorStatus, - type AppBskyEmbedExternal, - ComAtprotoRepoPutRecord, -} from '@atproto/api' -import {retry} from '@atproto/common-web' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' - -import {uploadBlob} from '#/lib/api' -import {imageToThumb} from '#/lib/api/resolve' -import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' -import {updateProfileShadow} from '#/state/cache/profile-shadow' -import {useAgent, useSession} from '#/state/session' -import * as Toast from '#/view/com/util/Toast' -import {useDialogContext} from '#/components/Dialog' -import {useAnalytics} from '#/analytics' -import {getLiveServiceNames, useLiveNowConfig} from '#/features/liveNow' - -export function useLiveLinkMetaQuery(url: string | null) { - const liveNowConfig = useLiveNowConfig() - const {_} = useLingui() - - const agent = useAgent() - return useQuery({ - enabled: !!url, - queryKey: ['link-meta', url], - queryFn: async () => { - if (!url) return undefined - const urlp = new URL(url) - if (!liveNowConfig.currentAccountAllowedHosts.has(urlp.hostname)) { - const {formatted} = getLiveServiceNames( - liveNowConfig.currentAccountAllowedHosts, - ) - throw new Error( - _( - msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`, - ), - ) - } - - return await getLinkMeta(agent, url) - }, - }) -} - -export function useUpsertLiveStatusMutation( - duration: number, - linkMeta: LinkMeta | null | undefined, - createdAt?: string, -) { - const ax = useAnalytics() - const {currentAccount} = useSession() - const agent = useAgent() - const queryClient = useQueryClient() - const control = useDialogContext() - const {_} = useLingui() - - return useMutation({ - mutationFn: async () => { - if (!currentAccount) throw new Error('Not logged in') - - let embed: $Typed | undefined - - if (linkMeta) { - let thumb - - if (linkMeta.image) { - try { - const img = await imageToThumb(linkMeta.image) - if (img) { - const blob = await uploadBlob( - agent, - img.source.path, - img.source.mime, - ) - thumb = blob.data.blob - } - } catch (e: any) { - ax.logger.error(`Failed to upload thumbnail for live status`, { - url: linkMeta.url, - image: linkMeta.image, - safeMessage: e, - }) - } - } - - embed = { - $type: 'app.bsky.embed.external', - external: { - $type: 'app.bsky.embed.external#external', - title: linkMeta.title ?? '', - description: linkMeta.description ?? '', - uri: linkMeta.url, - thumb, - }, - } - } - - const record = { - $type: 'app.bsky.actor.status', - createdAt: createdAt ?? new Date().toISOString(), - status: 'app.bsky.actor.status#live', - durationMinutes: duration, - embed, - } satisfies AppBskyActorStatus.Record - - const upsert = async () => { - const repo = currentAccount.did - const collection = 'app.bsky.actor.status' - - const existing = await agent.com.atproto.repo - .getRecord({repo, collection, rkey: 'self'}) - .catch(_e => undefined) - - await agent.com.atproto.repo.putRecord({ - repo, - collection, - rkey: 'self', - record, - swapRecord: existing?.data.cid || null, - }) - } - - await retry(upsert, { - maxRetries: 5, - retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError, - }) - - return { - record, - image: linkMeta?.image, - } - }, - onError: (e: any) => { - ax.logger.error(`Failed to upsert live status`, { - url: linkMeta?.url, - image: linkMeta?.image, - safeMessage: e, - }) - }, - onSuccess: ({record, image}) => { - if (createdAt) { - ax.metric('live:edit', {duration: record.durationMinutes}) - } else { - ax.metric('live:create', {duration: record.durationMinutes}) - } - - Toast.show(_(msg`You are now live!`)) - control.close(() => { - if (!currentAccount) return - - const expiresAt = new Date(record.createdAt) - expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes) - - updateProfileShadow(queryClient, currentAccount.did, { - status: { - $type: 'app.bsky.actor.defs#statusView', - status: 'app.bsky.actor.status#live', - isActive: true, - expiresAt: expiresAt.toISOString(), - embed: - record.embed && image - ? { - $type: 'app.bsky.embed.external#view', - external: { - ...record.embed.external, - $type: 'app.bsky.embed.external#viewExternal', - thumb: image, - }, - } - : undefined, - record, - }, - }) - }) - }, - }) -} - -export function useRemoveLiveStatusMutation() { - const ax = useAnalytics() - const {currentAccount} = useSession() - const agent = useAgent() - const queryClient = useQueryClient() - const control = useDialogContext() - const {_} = useLingui() - - return useMutation({ - mutationFn: async () => { - if (!currentAccount) throw new Error('Not logged in') - - await agent.app.bsky.actor.status.delete({ - repo: currentAccount.did, - rkey: 'self', - }) - }, - onError: (e: any) => { - ax.logger.error(`Failed to remove live status`, { - safeMessage: e, - }) - }, - onSuccess: () => { - ax.metric('live:remove', {}) - Toast.show(_(msg`You are no longer live`)) - control.close(() => { - if (!currentAccount) return - - updateProfileShadow(queryClient, currentAccount.did, { - status: undefined, - }) - }) - }, - }) -} diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index c6106a4bb1..c30ce78fa7 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -2,16 +2,31 @@ import {useMemo} from 'react' import { type $Typed, type AppBskyActorDefs, + type AppBskyActorStatus, AppBskyEmbedExternal, AtUri, + ComAtprotoRepoPutRecord, } from '@atproto/api' +import {retry} from '@atproto/common-web' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {isAfter, parseISO} from 'date-fns' +import {uploadBlob} from '#/lib/api' +import {imageToThumb} from '#/lib/api/resolve' +import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {useAppConfig} from '#/state/appConfig' -import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' -import {useSession} from '#/state/session' +import { + updateProfileShadow, + useMaybeProfileShadow, +} from '#/state/cache/profile-shadow' +import {useAgent, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' +import * as Toast from '#/view/com/util/Toast' +import {useDialogContext} from '#/components/Dialog' import {useAnalytics} from '#/analytics' +import {getLiveServiceNames} from '#/features/liveNow/utils' import type * as bsky from '#/types/bsky' export * from '#/features/liveNow/utils' @@ -150,3 +165,200 @@ export function isStatusValidForViewers( return false } } + +export function useLiveLinkMetaQuery(url: string | null) { + const liveNowConfig = useLiveNowConfig() + const {_} = useLingui() + + const agent = useAgent() + return useQuery({ + enabled: !!url, + queryKey: ['link-meta', url], + queryFn: async () => { + if (!url) return undefined + const urlp = new URL(url) + if (!liveNowConfig.currentAccountAllowedHosts.has(urlp.hostname)) { + const {formatted} = getLiveServiceNames( + liveNowConfig.currentAccountAllowedHosts, + ) + throw new Error( + _( + msg`This service is not supported while the Live feature is in beta. Allowed services: ${formatted}.`, + ), + ) + } + + return await getLinkMeta(agent, url) + }, + }) +} + +export function useUpsertLiveStatusMutation( + duration: number, + linkMeta: LinkMeta | null | undefined, + createdAt?: string, +) { + const ax = useAnalytics() + const {currentAccount} = useSession() + const agent = useAgent() + const queryClient = useQueryClient() + const control = useDialogContext() + const {_} = useLingui() + + return useMutation({ + mutationFn: async () => { + if (!currentAccount) throw new Error('Not logged in') + + let embed: $Typed | undefined + + if (linkMeta) { + let thumb + + if (linkMeta.image) { + try { + const img = await imageToThumb(linkMeta.image) + if (img) { + const blob = await uploadBlob( + agent, + img.source.path, + img.source.mime, + ) + thumb = blob.data.blob + } + } catch (e: any) { + ax.logger.error(`Failed to upload thumbnail for live status`, { + url: linkMeta.url, + image: linkMeta.image, + safeMessage: e, + }) + } + } + + embed = { + $type: 'app.bsky.embed.external', + external: { + $type: 'app.bsky.embed.external#external', + title: linkMeta.title ?? '', + description: linkMeta.description ?? '', + uri: linkMeta.url, + thumb, + }, + } + } + + const record = { + $type: 'app.bsky.actor.status', + createdAt: createdAt ?? new Date().toISOString(), + status: 'app.bsky.actor.status#live', + durationMinutes: duration, + embed, + } satisfies AppBskyActorStatus.Record + + const upsert = async () => { + const repo = currentAccount.did + const collection = 'app.bsky.actor.status' + + const existing = await agent.com.atproto.repo + .getRecord({repo, collection, rkey: 'self'}) + .catch(_e => undefined) + + await agent.com.atproto.repo.putRecord({ + repo, + collection, + rkey: 'self', + record, + swapRecord: existing?.data.cid || null, + }) + } + + await retry(upsert, { + maxRetries: 5, + retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError, + }) + + return { + record, + image: linkMeta?.image, + } + }, + onError: (e: any) => { + ax.logger.error(`Failed to upsert live status`, { + url: linkMeta?.url, + image: linkMeta?.image, + safeMessage: e, + }) + }, + onSuccess: ({record, image}) => { + if (createdAt) { + ax.metric('live:edit', {duration: record.durationMinutes}) + } else { + ax.metric('live:create', {duration: record.durationMinutes}) + } + + Toast.show(_(msg`You are now live!`)) + control.close(() => { + if (!currentAccount) return + + const expiresAt = new Date(record.createdAt) + expiresAt.setMinutes(expiresAt.getMinutes() + record.durationMinutes) + + updateProfileShadow(queryClient, currentAccount.did, { + status: { + $type: 'app.bsky.actor.defs#statusView', + status: 'app.bsky.actor.status#live', + isActive: true, + expiresAt: expiresAt.toISOString(), + embed: + record.embed && image + ? { + $type: 'app.bsky.embed.external#view', + external: { + ...record.embed.external, + $type: 'app.bsky.embed.external#viewExternal', + thumb: image, + }, + } + : undefined, + record, + }, + }) + }) + }, + }) +} + +export function useRemoveLiveStatusMutation() { + const ax = useAnalytics() + const {currentAccount} = useSession() + const agent = useAgent() + const queryClient = useQueryClient() + const control = useDialogContext() + const {_} = useLingui() + + return useMutation({ + mutationFn: async () => { + if (!currentAccount) throw new Error('Not logged in') + + await agent.app.bsky.actor.status.delete({ + repo: currentAccount.did, + rkey: 'self', + }) + }, + onError: (e: any) => { + ax.logger.error(`Failed to remove live status`, { + safeMessage: e, + }) + }, + onSuccess: () => { + ax.metric('live:remove', {}) + Toast.show(_(msg`You are no longer live`)) + control.close(() => { + if (!currentAccount) return + + updateProfileShadow(queryClient, currentAccount.did, { + status: undefined, + }) + }) + }, + }) +}