Fetch link previews from RQ (#5608)
Co-authored-by: Mary <git@mary.my.id> Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
@@ -46,19 +46,15 @@ import {RichText} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import * as apilib from '#/lib/api/index'
|
||||
import {until} from '#/lib/async/until'
|
||||
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||
import {
|
||||
createGIFDescription,
|
||||
parseAltFromGIFDescription,
|
||||
} from '#/lib/gif-alt-text'
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {LikelyType} from '#/lib/link-meta/link-meta'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
||||
@@ -87,8 +83,11 @@ import {useComposerControls} from '#/state/shell/composer'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo'
|
||||
import {ExternalEmbed} from '#/view/com/composer/ExternalEmbed'
|
||||
import {GifAltText} from '#/view/com/composer/GifAltText'
|
||||
import {
|
||||
ExternalEmbedGif,
|
||||
ExternalEmbedLink,
|
||||
} from '#/view/com/composer/ExternalEmbed'
|
||||
import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
|
||||
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
||||
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
||||
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
|
||||
@@ -100,12 +99,11 @@ import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLa
|
||||
// due to linting false positives
|
||||
import {TextInput, TextInputRef} from '#/view/com/composer/text-input/TextInput'
|
||||
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn'
|
||||
import {useExternalLinkFetch} from '#/view/com/composer/useExternalLinkFetch'
|
||||
import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn'
|
||||
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
|
||||
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
||||
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
|
||||
import {QuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
|
||||
import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
@@ -117,13 +115,15 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {composerReducer, createComposerState} from './state/composer'
|
||||
import {
|
||||
composerReducer,
|
||||
createComposerState,
|
||||
MAX_IMAGES,
|
||||
} from './state/composer'
|
||||
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export const ComposePost = ({
|
||||
replyTo,
|
||||
onPost,
|
||||
quote: initQuote,
|
||||
quoteCount,
|
||||
quoteCount: initQuoteCount,
|
||||
mention: initMention,
|
||||
openEmojiPicker,
|
||||
text: initText,
|
||||
@@ -147,6 +147,7 @@ export const ComposePost = ({
|
||||
}) => {
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const currentDid = currentAccount!.did
|
||||
const {data: currentProfile} = useProfileQuery({did: currentDid})
|
||||
const {isModalActive} = useModals()
|
||||
@@ -183,9 +184,6 @@ export const ComposePost = ({
|
||||
const graphemeLength = useMemo(() => {
|
||||
return shortenLinks(richtext).graphemeLength
|
||||
}, [richtext])
|
||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
||||
initQuote,
|
||||
)
|
||||
|
||||
// TODO: Move more state here.
|
||||
const [composerState, dispatch] = useReducer(
|
||||
@@ -246,8 +244,6 @@ export const ComposePost = ({
|
||||
|
||||
const [publishOnUpload, setPublishOnUpload] = useState(false)
|
||||
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote, setError})
|
||||
const [extGif, setExtGif] = useState<Gif>()
|
||||
const [labels, setLabels] = useState<string[]>([])
|
||||
const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] =
|
||||
useState<ThreadgateAllowUISetting[]>(
|
||||
@@ -255,10 +251,24 @@ export const ComposePost = ({
|
||||
)
|
||||
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
|
||||
|
||||
let quote: string | undefined
|
||||
if (composerState.embed.quote) {
|
||||
quote = composerState.embed.quote.uri
|
||||
}
|
||||
let images = NO_IMAGES
|
||||
if (composerState.embed.media?.type === 'images') {
|
||||
images = composerState.embed.media.images
|
||||
}
|
||||
let extGif: Gif | undefined
|
||||
let extGifAlt: string | undefined
|
||||
if (composerState.embed.media?.type === 'gif') {
|
||||
extGif = composerState.embed.media.gif
|
||||
extGifAlt = composerState.embed.media.alt
|
||||
}
|
||||
let extLink: string | undefined
|
||||
if (composerState.embed.link) {
|
||||
extLink = composerState.embed.link.uri
|
||||
}
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
closeComposer()
|
||||
@@ -335,14 +345,9 @@ export const ComposePost = ({
|
||||
}
|
||||
}, [onEscape, isModalActive])
|
||||
|
||||
const onNewLink = useCallback(
|
||||
(uri: string) => {
|
||||
dispatch({type: 'embed_add_uri', uri})
|
||||
if (extLink != null) return
|
||||
setExtLink({uri, isLoading: true})
|
||||
},
|
||||
[extLink, setExtLink],
|
||||
)
|
||||
const onNewLink = useCallback((uri: string) => {
|
||||
dispatch({type: 'embed_add_uri', uri})
|
||||
}, [])
|
||||
|
||||
const onImageAdd = useCallback(
|
||||
(next: ComposerImage[]) => {
|
||||
@@ -371,14 +376,10 @@ export const ComposePost = ({
|
||||
|
||||
if (images.some(img => img.alt === '')) return true
|
||||
|
||||
if (extGif) {
|
||||
if (!extLink?.meta?.description) return true
|
||||
if (extGif && !extGifAlt) return true
|
||||
|
||||
const parsedAlt = parseAltFromGIFDescription(extLink.meta.description)
|
||||
if (!parsedAlt.isPreferred) return true
|
||||
}
|
||||
return false
|
||||
}, [images, extLink, extGif, requireAltTextEnabled])
|
||||
}, [images, extGifAlt, extGif, requireAltTextEnabled])
|
||||
|
||||
const onPressPublish = React.useCallback(
|
||||
async (finishedUploading?: boolean) => {
|
||||
@@ -411,17 +412,13 @@ export const ComposePost = ({
|
||||
setError(_(msg`Did you want to say anything?`))
|
||||
return
|
||||
}
|
||||
if (extLink?.isLoading) {
|
||||
setError(_(msg`Please wait for your link card to finish loading`))
|
||||
return
|
||||
}
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
let postUri
|
||||
try {
|
||||
postUri = (
|
||||
await apilib.post(agent, {
|
||||
await apilib.post(agent, queryClient, {
|
||||
composerState, // TODO: move more state here.
|
||||
rawText: richtext.text,
|
||||
replyTo: replyTo?.uri,
|
||||
@@ -449,13 +446,6 @@ export const ComposePost = ({
|
||||
hasImages: images.length > 0,
|
||||
})
|
||||
|
||||
if (extLink) {
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: true,
|
||||
localThumb: undefined,
|
||||
} as apilib.ExternalEmbedDraft)
|
||||
}
|
||||
let err = cleanError(e.message)
|
||||
if (err.includes('not locate record')) {
|
||||
err = _(
|
||||
@@ -481,13 +471,13 @@ export const ComposePost = ({
|
||||
emitPostCreated()
|
||||
}
|
||||
setLangPrefs.savePostLanguageToHistory()
|
||||
if (quote) {
|
||||
if (initQuote && initQuoteCount !== undefined) {
|
||||
// We want to wait for the quote count to update before we call `onPost`, which will refetch data
|
||||
whenAppViewReady(agent, quote.uri, res => {
|
||||
whenAppViewReady(agent, initQuote.uri, res => {
|
||||
const thread = res.data.thread
|
||||
if (
|
||||
AppBskyFeedDefs.isThreadViewPost(thread) &&
|
||||
thread.post.quoteCount !== quoteCount
|
||||
thread.post.quoteCount !== initQuoteCount
|
||||
) {
|
||||
onPost?.(postUri)
|
||||
return true
|
||||
@@ -519,14 +509,15 @@ export const ComposePost = ({
|
||||
onPost,
|
||||
postgate,
|
||||
quote,
|
||||
quoteCount,
|
||||
initQuote,
|
||||
initQuoteCount,
|
||||
replyTo,
|
||||
richtext.text,
|
||||
setExtLink,
|
||||
setLangPrefs,
|
||||
threadgateAllowUISettings,
|
||||
videoState.asset,
|
||||
videoState.status,
|
||||
queryClient,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -549,11 +540,9 @@ export const ComposePost = ({
|
||||
|
||||
const canSelectImages =
|
||||
images.length < MAX_IMAGES &&
|
||||
!extLink &&
|
||||
videoState.status === 'idle' &&
|
||||
!videoState.video
|
||||
const hasMedia =
|
||||
images.length > 0 || Boolean(extLink) || Boolean(videoState.video)
|
||||
const hasMedia = images.length > 0 || Boolean(videoState.video)
|
||||
|
||||
const onEmojiButtonPress = useCallback(() => {
|
||||
openEmojiPicker?.(textInput.current?.getCursorPosition())
|
||||
@@ -563,45 +552,13 @@ export const ComposePost = ({
|
||||
textInput.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
dispatch({type: 'embed_add_gif', gif})
|
||||
setExtLink({
|
||||
uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
|
||||
isLoading: true,
|
||||
meta: {
|
||||
url: gif.media_formats.gif.url,
|
||||
image: gif.media_formats.preview.url,
|
||||
likelyType: LikelyType.HTML,
|
||||
title: gif.content_description,
|
||||
description: createGIFDescription(gif.content_description),
|
||||
},
|
||||
})
|
||||
setExtGif(gif)
|
||||
},
|
||||
[setExtLink],
|
||||
)
|
||||
const onSelectGif = useCallback((gif: Gif) => {
|
||||
dispatch({type: 'embed_add_gif', gif})
|
||||
}, [])
|
||||
|
||||
const handleChangeGifAltText = useCallback(
|
||||
(altText: string) => {
|
||||
dispatch({type: 'embed_update_gif', alt: altText})
|
||||
setExtLink(ext =>
|
||||
ext && ext.meta
|
||||
? {
|
||||
...ext,
|
||||
meta: {
|
||||
...ext.meta,
|
||||
description: createGIFDescription(
|
||||
ext.meta.title ?? '',
|
||||
altText,
|
||||
),
|
||||
},
|
||||
}
|
||||
: ext,
|
||||
)
|
||||
},
|
||||
[setExtLink],
|
||||
)
|
||||
const handleChangeGifAltText = useCallback((altText: string) => {
|
||||
dispatch({type: 'embed_update_gif', alt: altText})
|
||||
}, [])
|
||||
|
||||
const {
|
||||
scrollHandler,
|
||||
@@ -660,7 +617,7 @@ export const ComposePost = ({
|
||||
<LabelsBtn
|
||||
labels={labels}
|
||||
onChange={setLabels}
|
||||
hasMedia={hasMedia}
|
||||
hasMedia={hasMedia || Boolean(extLink)}
|
||||
/>
|
||||
{canPost ? (
|
||||
<Button
|
||||
@@ -759,29 +716,35 @@ export const ComposePost = ({
|
||||
dispatch={dispatch}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
link={extLink}
|
||||
|
||||
{extGif && (
|
||||
<View style={a.relative} key={extGif.url}>
|
||||
<ExternalEmbedGif
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
if (extGif) {
|
||||
dispatch({type: 'embed_remove_gif'})
|
||||
} else {
|
||||
dispatch({type: 'embed_remove_link'})
|
||||
}
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
dispatch({type: 'embed_remove_gif'})
|
||||
}}
|
||||
/>
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
<GifAltTextDialog
|
||||
gif={extGif}
|
||||
altText={extGifAlt ?? ''}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
Portal={Portal.Portal}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!composerState.embed.media && extLink && (
|
||||
<View style={a.relative} key={extLink}>
|
||||
<ExternalEmbedLink
|
||||
uri={extLink}
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_link'})
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<LayoutAnimationConfig skipExiting>
|
||||
{hasVideo && (
|
||||
<Animated.View
|
||||
@@ -835,13 +798,12 @@ export const ComposePost = ({
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
<LazyQuoteEmbed uri={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
{!initQuote && (
|
||||
<QuoteX
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_quote'})
|
||||
setQuote(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,71 +1,112 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {ExternalEmbedDraft} from 'lib/api/index'
|
||||
import {Gif} from 'state/queries/tenor'
|
||||
import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn'
|
||||
import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {
|
||||
useResolveGifQuery,
|
||||
useResolveLinkQuery,
|
||||
} from '#/state/queries/resolve-link'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
|
||||
import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
export const ExternalEmbedGif = ({
|
||||
onRemove,
|
||||
gif,
|
||||
}: {
|
||||
link?: ExternalEmbedDraft
|
||||
onRemove: () => void
|
||||
gif?: Gif
|
||||
gif: Gif
|
||||
}) => {
|
||||
const t = useTheme()
|
||||
|
||||
const {data, error} = useResolveGifQuery(gif)
|
||||
const linkInfo = React.useMemo(
|
||||
() =>
|
||||
link && {
|
||||
title: link.meta?.title ?? link.uri,
|
||||
uri: link.uri,
|
||||
description: link.meta?.description ?? '',
|
||||
thumb: link.localThumb?.source.path,
|
||||
data && {
|
||||
title: data.title ?? data.uri,
|
||||
uri: data.uri,
|
||||
description: data.description ?? '',
|
||||
thumb: data.thumb?.source.path,
|
||||
},
|
||||
[link],
|
||||
[data],
|
||||
)
|
||||
|
||||
if (!link) return null
|
||||
|
||||
const loadingStyle: ViewStyle | undefined = gif
|
||||
? {
|
||||
aspectRatio:
|
||||
gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
|
||||
width: '100%',
|
||||
}
|
||||
: undefined
|
||||
const loadingStyle: ViewStyle = {
|
||||
aspectRatio: gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
|
||||
width: '100%',
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
!gif && a.mb_xl,
|
||||
a.overflow_hidden,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
{link.isLoading ? (
|
||||
<View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
||||
{linkInfo ? (
|
||||
<View style={{pointerEvents: 'auto'}}>
|
||||
<ExternalLinkEmbed link={linkInfo} hideAlt />
|
||||
</View>
|
||||
) : error ? (
|
||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||
<Text numberOfLines={1} style={t.atoms.text_contrast_high}>
|
||||
{gif.url}
|
||||
</Text>
|
||||
<Text numberOfLines={2} style={[{color: t.palette.negative_400}]}>
|
||||
{cleanError(error)}
|
||||
</Text>
|
||||
</Container>
|
||||
) : (
|
||||
<Container style={loadingStyle}>
|
||||
<Loader size="xl" />
|
||||
</Container>
|
||||
) : link.meta?.error ? (
|
||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||
<Text numberOfLines={1} style={t.atoms.text_contrast_high}>
|
||||
{link.uri}
|
||||
</Text>
|
||||
<Text numberOfLines={2} style={[{color: t.palette.negative_400}]}>
|
||||
{link.meta?.error}
|
||||
</Text>
|
||||
</Container>
|
||||
) : linkInfo ? (
|
||||
<View style={{pointerEvents: !gif ? 'none' : 'auto'}}>
|
||||
)}
|
||||
<ExternalEmbedRemoveBtn onRemove={onRemove} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const ExternalEmbedLink = ({
|
||||
uri,
|
||||
onRemove,
|
||||
}: {
|
||||
uri: string
|
||||
onRemove: () => void
|
||||
}) => {
|
||||
const t = useTheme()
|
||||
const {data, error} = useResolveLinkQuery(uri)
|
||||
const linkInfo = React.useMemo(
|
||||
() =>
|
||||
data && {
|
||||
title:
|
||||
data.type === 'external'
|
||||
? data.title
|
||||
: data.kind === 'other'
|
||||
? data.meta.title
|
||||
: uri,
|
||||
uri,
|
||||
description: data.type === 'external' ? data.description : '',
|
||||
thumb: data.type === 'external' ? data.thumb?.source.path : undefined,
|
||||
},
|
||||
[data, uri],
|
||||
)
|
||||
return (
|
||||
<View style={[a.mb_xl, a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
||||
{linkInfo ? (
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<ExternalLinkEmbed link={linkInfo} hideAlt />
|
||||
</View>
|
||||
) : null}
|
||||
) : error ? (
|
||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||
<Text numberOfLines={1} style={t.atoms.text_contrast_high}>
|
||||
{uri}
|
||||
</Text>
|
||||
<Text numberOfLines={2} style={[{color: t.palette.negative_400}]}>
|
||||
{cleanError(error)}
|
||||
</Text>
|
||||
</Container>
|
||||
) : (
|
||||
<Container>
|
||||
<Loader size="xl" />
|
||||
</Container>
|
||||
)}
|
||||
<ExternalEmbedRemoveBtn onRemove={onRemove} />
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, {useState} from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ExternalEmbedDraft} from '#/lib/api'
|
||||
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
||||
import {
|
||||
@@ -12,6 +10,7 @@ import {
|
||||
parseEmbedPlayerFromUrl,
|
||||
} from '#/lib/strings/embed-player'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useResolveGifQuery} from '#/state/queries/resolve-link'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
@@ -27,38 +26,54 @@ import {Text} from '#/components/Typography'
|
||||
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
||||
import {AltTextReminder} from './photos/Gallery'
|
||||
|
||||
export function GifAltText({
|
||||
link: linkProp,
|
||||
export function GifAltTextDialog({
|
||||
gif,
|
||||
altText,
|
||||
onSubmit,
|
||||
Portal,
|
||||
}: {
|
||||
link: ExternalEmbedDraft
|
||||
gif?: Gif
|
||||
gif: Gif
|
||||
altText: string
|
||||
onSubmit: (alt: string) => void
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const {data} = useResolveGifQuery(gif)
|
||||
const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt
|
||||
const params = data ? parseEmbedPlayerFromUrl(data.uri) : undefined
|
||||
if (!data || !params) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<GifAltTextDialogLoaded
|
||||
altText={altText}
|
||||
vendorAltText={vendorAltText}
|
||||
thumb={data.thumb?.source.path}
|
||||
params={params}
|
||||
onSubmit={onSubmit}
|
||||
Portal={Portal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function GifAltTextDialogLoaded({
|
||||
vendorAltText,
|
||||
altText,
|
||||
onSubmit,
|
||||
params,
|
||||
thumb,
|
||||
Portal,
|
||||
}: {
|
||||
vendorAltText: string
|
||||
altText: string
|
||||
onSubmit: (alt: string) => void
|
||||
params: EmbedPlayerParams
|
||||
thumb: string | undefined
|
||||
Portal: PortalComponent
|
||||
}) {
|
||||
const control = Dialog.useDialogControl()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const {link, params} = React.useMemo(() => {
|
||||
return {
|
||||
link: {
|
||||
title: linkProp.meta?.title ?? linkProp.uri,
|
||||
uri: linkProp.uri,
|
||||
description: linkProp.meta?.description ?? '',
|
||||
thumb: linkProp.localThumb?.source.path,
|
||||
},
|
||||
params: parseEmbedPlayerFromUrl(linkProp.uri),
|
||||
}
|
||||
}, [linkProp])
|
||||
|
||||
const parsedAlt = parseAltFromGIFDescription(link.description)
|
||||
const [altText, setAltText] = useState(parsedAlt.alt)
|
||||
|
||||
if (!gif || !params) return null
|
||||
|
||||
const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
@@ -80,7 +95,7 @@ export function GifAltText({
|
||||
a.align_center,
|
||||
{backgroundColor: 'rgba(0, 0, 0, 0.75)'},
|
||||
]}>
|
||||
{parsedAlt.isPreferred ? (
|
||||
{altText ? (
|
||||
<Check size="xs" fill={t.palette.white} style={a.ml_xs} />
|
||||
) : (
|
||||
<Plus size="sm" fill={t.palette.white} />
|
||||
@@ -97,17 +112,17 @@ export function GifAltText({
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={() => {
|
||||
onSubmit(altText)
|
||||
onSubmit(altTextDraft)
|
||||
}}
|
||||
Portal={Portal}>
|
||||
<Dialog.Handle />
|
||||
<AltTextInner
|
||||
altText={altText}
|
||||
setAltText={setAltText}
|
||||
vendorAltText={vendorAltText}
|
||||
altText={altTextDraft}
|
||||
onChange={setAltTextDraft}
|
||||
thumb={thumb}
|
||||
control={control}
|
||||
link={link}
|
||||
params={params}
|
||||
key={link.uri}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
@@ -115,17 +130,19 @@ export function GifAltText({
|
||||
}
|
||||
|
||||
function AltTextInner({
|
||||
vendorAltText,
|
||||
altText,
|
||||
setAltText,
|
||||
onChange,
|
||||
control,
|
||||
link,
|
||||
params,
|
||||
thumb,
|
||||
}: {
|
||||
vendorAltText: string
|
||||
altText: string
|
||||
setAltText: (text: string) => void
|
||||
onChange: (text: string) => void
|
||||
control: DialogControlProps
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
thumb: string | undefined
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
@@ -142,10 +159,8 @@ function AltTextInner({
|
||||
<TextField.Root>
|
||||
<Dialog.Input
|
||||
label={_(msg`Alt text`)}
|
||||
placeholder={link.title}
|
||||
onChangeText={text => {
|
||||
setAltText(text)
|
||||
}}
|
||||
placeholder={vendorAltText}
|
||||
onChangeText={onChange}
|
||||
defaultValue={altText}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
@@ -200,7 +215,9 @@ function AltTextInner({
|
||||
</Text>
|
||||
<View style={[a.align_center]}>
|
||||
<GifEmbed
|
||||
link={link}
|
||||
thumb={thumb}
|
||||
altText={altText}
|
||||
isPreferredAltText={true}
|
||||
params={params}
|
||||
hideAlt
|
||||
style={[native({maxHeight: 225})]}
|
||||
|
||||
@@ -64,7 +64,7 @@ export type ComposerAction =
|
||||
| {type: 'embed_update_gif'; alt: string}
|
||||
| {type: 'embed_remove_gif'}
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
export const MAX_IMAGES = 4
|
||||
|
||||
export function composerReducer(
|
||||
state: ComposerState,
|
||||
@@ -317,7 +317,6 @@ export function createComposerState({
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Other initial content.
|
||||
return {
|
||||
embed: {
|
||||
quote,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
|
||||
export function useExternalLinkFetch({}: {
|
||||
setQuote: (opts: ComposerOpts['quote']) => void
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let aborted = false
|
||||
const cleanup = () => {
|
||||
aborted = true
|
||||
}
|
||||
if (!extLink) {
|
||||
return cleanup
|
||||
}
|
||||
if (!extLink.meta) {
|
||||
getLinkMeta(agent, extLink.uri).then(meta => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
uri: extLink.uri,
|
||||
isLoading: !!meta.image,
|
||||
meta,
|
||||
})
|
||||
})
|
||||
return cleanup
|
||||
}
|
||||
if (extLink.isLoading) {
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: false, // done
|
||||
})
|
||||
}
|
||||
return cleanup
|
||||
}, [extLink, agent])
|
||||
|
||||
return {extLink, setExtLink}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import * as apilib from '#/lib/api/index'
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {
|
||||
EmbeddingDisabledError,
|
||||
getFeedAsEmbed,
|
||||
getListAsEmbed,
|
||||
getPostAsQuote,
|
||||
getStarterPackAsEmbed,
|
||||
} from '#/lib/link-meta/bsky'
|
||||
import {getLinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
|
||||
import {downloadAndResize} from '#/lib/media/manip'
|
||||
import {
|
||||
isBskyCustomFeedUrl,
|
||||
isBskyListUrl,
|
||||
isBskyPostUrl,
|
||||
isBskyStarterPackUrl,
|
||||
isBskyStartUrl,
|
||||
isShortLink,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {createComposerImage} from '#/state/gallery'
|
||||
import {useFetchDid} from '#/state/queries/handle'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
|
||||
export function useExternalLinkFetch({
|
||||
setQuote,
|
||||
setError,
|
||||
}: {
|
||||
setQuote: (opts: ComposerOpts['quote']) => void
|
||||
setError: (err: string) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
|
||||
undefined,
|
||||
)
|
||||
const getPost = useGetPost()
|
||||
const fetchDid = useFetchDid()
|
||||
const agent = useAgent()
|
||||
|
||||
useEffect(() => {
|
||||
let aborted = false
|
||||
const cleanup = () => {
|
||||
aborted = true
|
||||
}
|
||||
if (!extLink) {
|
||||
return cleanup
|
||||
}
|
||||
if (!extLink.meta) {
|
||||
if (isBskyPostUrl(extLink.uri)) {
|
||||
getPostAsQuote(getPost, extLink.uri).then(
|
||||
newQuote => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setQuote(newQuote)
|
||||
setExtLink(undefined)
|
||||
},
|
||||
err => {
|
||||
if (err instanceof EmbeddingDisabledError) {
|
||||
setError(_(msg`This post's author has disabled quote posts.`))
|
||||
} else {
|
||||
logger.error('Failed to fetch post for quote embedding', {
|
||||
message: err.toString(),
|
||||
})
|
||||
}
|
||||
setExtLink(undefined)
|
||||
},
|
||||
)
|
||||
} else if (isBskyCustomFeedUrl(extLink.uri)) {
|
||||
getFeedAsEmbed(agent, fetchDid, extLink.uri).then(
|
||||
({embed, meta}) => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
uri: extLink.uri,
|
||||
isLoading: false,
|
||||
meta,
|
||||
embed,
|
||||
})
|
||||
},
|
||||
err => {
|
||||
logger.error('Failed to fetch feed for embedding', {message: err})
|
||||
setExtLink(undefined)
|
||||
},
|
||||
)
|
||||
} else if (isBskyListUrl(extLink.uri)) {
|
||||
getListAsEmbed(agent, fetchDid, extLink.uri).then(
|
||||
({embed, meta}) => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
uri: extLink.uri,
|
||||
isLoading: false,
|
||||
meta,
|
||||
embed,
|
||||
})
|
||||
},
|
||||
err => {
|
||||
logger.error('Failed to fetch list for embedding', {message: err})
|
||||
setExtLink(undefined)
|
||||
},
|
||||
)
|
||||
} else if (
|
||||
isBskyStartUrl(extLink.uri) ||
|
||||
isBskyStarterPackUrl(extLink.uri)
|
||||
) {
|
||||
getStarterPackAsEmbed(agent, fetchDid, extLink.uri).then(
|
||||
({embed, meta}) => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
uri: extLink.uri,
|
||||
isLoading: false,
|
||||
meta,
|
||||
embed,
|
||||
})
|
||||
},
|
||||
)
|
||||
} else if (isShortLink(extLink.uri)) {
|
||||
if (isShortLink(extLink.uri)) {
|
||||
resolveShortLink(extLink.uri).then(res => {
|
||||
if (res && res !== extLink.uri) {
|
||||
setExtLink({
|
||||
uri: res,
|
||||
isLoading: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
getLinkMeta(agent, extLink.uri).then(meta => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
uri: extLink.uri,
|
||||
isLoading: !!meta.image,
|
||||
meta,
|
||||
})
|
||||
})
|
||||
}
|
||||
return cleanup
|
||||
}
|
||||
if (extLink.isLoading && extLink.meta?.image && !extLink.localThumb) {
|
||||
downloadAndResize({
|
||||
uri: extLink.meta.image,
|
||||
width: POST_IMG_MAX.width,
|
||||
height: POST_IMG_MAX.height,
|
||||
mode: 'contain',
|
||||
maxSize: POST_IMG_MAX.size,
|
||||
timeout: 15e3,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.then(thumb => (thumb ? createComposerImage(thumb) : undefined))
|
||||
.then(thumb => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: false, // done
|
||||
localThumb: thumb,
|
||||
})
|
||||
})
|
||||
return cleanup
|
||||
}
|
||||
if (extLink.isLoading) {
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: false, // done
|
||||
})
|
||||
}
|
||||
return cleanup
|
||||
}, [_, extLink, setQuote, getPost, fetchDid, agent, setError])
|
||||
|
||||
return {extLink, setExtLink}
|
||||
}
|
||||
Reference in New Issue
Block a user