Move queries into liveNow
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AppBskyEmbedExternal.Main> | 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,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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<AppBskyEmbedExternal.Main> | 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,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user