[Live] Add warning if link is missing image (#8393)
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react'
|
import {createContext, useContext} from 'react'
|
||||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button as BaseButton, ButtonProps} from '#/components/Button'
|
import {Button as BaseButton, type ButtonProps} from '#/components/Button'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||||
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import {Text as BaseText, TextProps} from '#/components/Typography'
|
import {Text as BaseText, type TextProps} from '#/components/Typography'
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
warning: {
|
warning: {
|
||||||
@@ -20,13 +20,13 @@ type Context = {
|
|||||||
type: 'info' | 'tip' | 'warning' | 'error'
|
type: 'info' | 'tip' | 'warning' | 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Context = React.createContext<Context>({
|
const Context = createContext<Context>({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Icon() {
|
export function Icon() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {type} = React.useContext(Context)
|
const {type} = useContext(Context)
|
||||||
const Icon = {
|
const Icon = {
|
||||||
info: InfoIcon,
|
info: InfoIcon,
|
||||||
tip: TipIcon,
|
tip: TipIcon,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useMemo, useState} from 'react'
|
import {useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {
|
import {
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
AppBskyActorStatus,
|
AppBskyActorStatus,
|
||||||
@@ -8,28 +7,23 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
|
||||||
import {differenceInMinutes} from 'date-fns'
|
import {differenceInMinutes} from 'date-fns'
|
||||||
|
|
||||||
import {getLinkMeta} from '#/lib/link-meta/link-meta'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
|
||||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||||
import {useAgent} from '#/state/session'
|
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|
||||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
|
||||||
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {LinkPreview} from './LinkPreview'
|
||||||
import {
|
import {
|
||||||
|
useLiveLinkMetaQuery,
|
||||||
useRemoveLiveStatusMutation,
|
useRemoveLiveStatusMutation,
|
||||||
useUpsertLiveStatusMutation,
|
useUpsertLiveStatusMutation,
|
||||||
} from './queries'
|
} from './queries'
|
||||||
@@ -62,10 +56,9 @@ function DialogInner({
|
|||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const agent = useAgent()
|
|
||||||
const [liveLink, setLiveLink] = useState(embed.external.uri)
|
const [liveLink, setLiveLink] = useState(embed.external.uri)
|
||||||
const [liveLinkError, setLiveLinkError] = useState('')
|
const [liveLinkError, setLiveLinkError] = useState('')
|
||||||
const [imageLoadError, setImageLoadError] = useState(false)
|
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
|
|
||||||
const liveLinkUrl = definitelyUrl(liveLink)
|
const liveLinkUrl = definitelyUrl(liveLink)
|
||||||
@@ -78,14 +71,7 @@ function DialogInner({
|
|||||||
isSuccess: hasValidLinkMeta,
|
isSuccess: hasValidLinkMeta,
|
||||||
isLoading: linkMetaLoading,
|
isLoading: linkMetaLoading,
|
||||||
error: linkMetaError,
|
error: linkMetaError,
|
||||||
} = useQuery({
|
} = useLiveLinkMetaQuery(debouncedUrl)
|
||||||
enabled: !!debouncedUrl,
|
|
||||||
queryKey: ['link-meta', debouncedUrl],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!debouncedUrl) return null
|
|
||||||
return getLinkMeta(agent, debouncedUrl)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const record = useMemo(() => {
|
const record = useMemo(() => {
|
||||||
if (!AppBskyActorStatus.isRecord(status.record)) return null
|
if (!AppBskyActorStatus.isRecord(status.record)) return null
|
||||||
@@ -208,85 +194,7 @@ function DialogInner({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(linkMeta || linkMetaLoading) && (
|
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
t.atoms.bg,
|
|
||||||
a.flex_row,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.align_stretch,
|
|
||||||
]}>
|
|
||||||
{(!linkMeta || linkMeta.image) && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{minHeight: 64, width: 114},
|
|
||||||
a.justify_center,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
{linkMeta?.image && (
|
|
||||||
<Image
|
|
||||||
source={linkMeta.image}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
transition={200}
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
contentFit="cover"
|
|
||||||
onLoad={() => setImageLoadError(false)}
|
|
||||||
onError={() => setImageLoadError(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{linkMeta && imageLoadError && (
|
|
||||||
<CircleXIcon
|
|
||||||
style={[t.atoms.text_contrast_low]}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.justify_center,
|
|
||||||
a.py_sm,
|
|
||||||
a.gap_xs,
|
|
||||||
a.px_md,
|
|
||||||
]}>
|
|
||||||
{linkMeta ? (
|
|
||||||
<>
|
|
||||||
<Text
|
|
||||||
numberOfLines={2}
|
|
||||||
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
|
||||||
{linkMeta.title || linkMeta.url}
|
|
||||||
</Text>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<GlobeIcon
|
|
||||||
size="xs"
|
|
||||||
style={[t.atoms.text_contrast_low]}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
{toNiceDomain(linkMeta.url)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LoadingPlaceholder height={16} width={128} />
|
|
||||||
<LoadingPlaceholder height={12} width={72} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{goLiveError && (
|
{goLiveError && (
|
||||||
|
|||||||
@@ -1,33 +1,25 @@
|
|||||||
import {useCallback, useState} from 'react'
|
import {useCallback, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {getLinkMeta} from '#/lib/link-meta/link-meta'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
|
||||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useLiveNowConfig} from '#/state/service-config'
|
|
||||||
import {useAgent, useSession} from '#/state/session'
|
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|
||||||
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
|
||||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
|
||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import * as Select from '#/components/Select'
|
import * as Select from '#/components/Select'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {useUpsertLiveStatusMutation} from './queries'
|
import {LinkPreview} from './LinkPreview'
|
||||||
|
import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
|
||||||
import {displayDuration, useDebouncedValue} from './utils'
|
import {displayDuration, useDebouncedValue} from './utils'
|
||||||
|
|
||||||
export function GoLiveDialog({
|
export function GoLiveDialog({
|
||||||
@@ -52,17 +44,11 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const agent = useAgent()
|
|
||||||
const [liveLink, setLiveLink] = useState('')
|
const [liveLink, setLiveLink] = useState('')
|
||||||
const [liveLinkError, setLiveLinkError] = useState('')
|
const [liveLinkError, setLiveLinkError] = useState('')
|
||||||
const [imageLoadError, setImageLoadError] = useState(false)
|
|
||||||
const [duration, setDuration] = useState(60)
|
const [duration, setDuration] = useState(60)
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
const liveNowConfig = useLiveNowConfig()
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
|
|
||||||
const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
|
|
||||||
|
|
||||||
const time = useCallback(
|
const time = useCallback(
|
||||||
(offset: number) => {
|
(offset: number) => {
|
||||||
@@ -90,21 +76,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
isSuccess: hasValidLinkMeta,
|
isSuccess: hasValidLinkMeta,
|
||||||
isLoading: linkMetaLoading,
|
isLoading: linkMetaLoading,
|
||||||
error: linkMetaError,
|
error: linkMetaError,
|
||||||
} = useQuery({
|
} = useLiveLinkMetaQuery(debouncedUrl)
|
||||||
enabled: !!debouncedUrl,
|
|
||||||
queryKey: ['link-meta', debouncedUrl],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!debouncedUrl) return null
|
|
||||||
if (!config) throw new Error(_(msg`You are not allowed to go live`))
|
|
||||||
|
|
||||||
const urlp = new URL(debouncedUrl)
|
|
||||||
if (!config.domains.includes(urlp.hostname)) {
|
|
||||||
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
|
|
||||||
}
|
|
||||||
|
|
||||||
return getLinkMeta(agent, debouncedUrl)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: goLive,
|
mutate: goLive,
|
||||||
@@ -193,85 +165,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(linkMeta || linkMetaLoading) && (
|
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
t.atoms.bg,
|
|
||||||
a.flex_row,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.align_stretch,
|
|
||||||
]}>
|
|
||||||
{(!linkMeta || linkMeta.image) && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{minHeight: 64, width: 114},
|
|
||||||
a.justify_center,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
{linkMeta?.image && (
|
|
||||||
<Image
|
|
||||||
source={linkMeta.image}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
transition={200}
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
contentFit="cover"
|
|
||||||
onLoad={() => setImageLoadError(false)}
|
|
||||||
onError={() => setImageLoadError(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{linkMeta && imageLoadError && (
|
|
||||||
<CircleXIcon
|
|
||||||
style={[t.atoms.text_contrast_low]}
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.justify_center,
|
|
||||||
a.py_sm,
|
|
||||||
a.gap_xs,
|
|
||||||
a.px_md,
|
|
||||||
]}>
|
|
||||||
{linkMeta ? (
|
|
||||||
<>
|
|
||||||
<Text
|
|
||||||
numberOfLines={2}
|
|
||||||
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
|
||||||
{linkMeta.title || linkMeta.url}
|
|
||||||
</Text>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<GlobeIcon
|
|
||||||
size="xs"
|
|
||||||
style={[t.atoms.text_contrast_low]}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
{toNiceDomain(linkMeta.url)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LoadingPlaceholder height={16} width={128} />
|
|
||||||
<LoadingPlaceholder height={12} width={72} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{hasLink && (
|
{hasLink && (
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||||
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||||
|
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function LinkPreview({
|
||||||
|
linkMeta,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
linkMeta?: LinkMeta
|
||||||
|
loading: boolean
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const [imageLoadError, setImageLoadError] = useState(false)
|
||||||
|
|
||||||
|
if (!linkMeta && !loading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.flex_row,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.align_stretch,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{minHeight: 64, width: 114},
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
]}>
|
||||||
|
{linkMeta?.image && (
|
||||||
|
<Image
|
||||||
|
source={linkMeta.image}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
transition={200}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
contentFit="cover"
|
||||||
|
onLoad={() => setImageLoadError(false)}
|
||||||
|
onError={() => setImageLoadError(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{linkMeta && (!linkMeta.image || imageLoadError) && (
|
||||||
|
<>
|
||||||
|
<ImageIcon style={[t.atoms.text_contrast_low]} />
|
||||||
|
<Text style={[t.atoms.text_contrast_low, a.text_xs, a.text_center]}>
|
||||||
|
<Trans>No image</Trans>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_1, a.justify_center, a.py_sm, a.gap_xs, a.px_md]}>
|
||||||
|
{linkMeta ? (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
numberOfLines={2}
|
||||||
|
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
||||||
|
{linkMeta.title || linkMeta.url}
|
||||||
|
</Text>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||||
|
<GlobeIcon size="xs" style={[t.atoms.text_contrast_low]} />
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{toNiceDomain(linkMeta.url)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LoadingPlaceholder height={16} width={128} />
|
||||||
|
<LoadingPlaceholder height={12} width={72} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,17 +7,43 @@ import {
|
|||||||
import {retry} from '@atproto/common-web'
|
import {retry} from '@atproto/common-web'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {uploadBlob} from '#/lib/api'
|
import {uploadBlob} from '#/lib/api'
|
||||||
import {imageToThumb} from '#/lib/api/resolve'
|
import {imageToThumb} from '#/lib/api/resolve'
|
||||||
import {type LinkMeta} from '#/lib/link-meta/link-meta'
|
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
|
|
||||||
|
export function useLiveLinkMetaQuery(url: string | null) {
|
||||||
|
const liveNowConfig = useLiveNowConfig()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const agent = useAgent()
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!url,
|
||||||
|
queryKey: ['link-meta', url],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!url) return undefined
|
||||||
|
const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
|
||||||
|
|
||||||
|
if (!config) throw new Error(_(msg`You are not allowed to go live`))
|
||||||
|
|
||||||
|
const urlp = new URL(url)
|
||||||
|
if (!config.domains.includes(urlp.hostname)) {
|
||||||
|
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getLinkMeta(agent, url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpsertLiveStatusMutation(
|
export function useUpsertLiveStatusMutation(
|
||||||
duration: number,
|
duration: number,
|
||||||
linkMeta: LinkMeta | null | undefined,
|
linkMeta: LinkMeta | null | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user