diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 176 | ||||
-rw-r--r-- | src/view/com/composer/ExternalEmbed.tsx | 119 | ||||
-rw-r--r-- | src/view/com/composer/GifAltText.tsx | 95 | ||||
-rw-r--r-- | src/view/com/composer/state/composer.ts | 3 | ||||
-rw-r--r-- | src/view/com/composer/useExternalLinkFetch.e2e.ts | 47 | ||||
-rw-r--r-- | src/view/com/composer/useExternalLinkFetch.ts | 187 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/ExternalLinkEmbed.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/GifEmbed.tsx | 21 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/QuoteEmbed.tsx | 19 |
10 files changed, 247 insertions, 436 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index a1c4e7656..ecafea500 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -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) }} /> )} diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index f61d410df..d7dc32f14 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -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 ? ( + )} + <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> + ) : error ? ( <Container style={[a.align_start, a.p_md, a.gap_xs]}> <Text numberOfLines={1} style={t.atoms.text_contrast_high}> - {link.uri} + {uri} </Text> <Text numberOfLines={2} style={[{color: t.palette.negative_400}]}> - {link.meta?.error} + {cleanError(error)} </Text> </Container> - ) : linkInfo ? ( - <View style={{pointerEvents: !gif ? 'none' : 'auto'}}> - <ExternalLinkEmbed link={linkInfo} hideAlt /> - </View> - ) : null} + ) : ( + <Container> + <Loader size="xl" /> + </Container> + )} <ExternalEmbedRemoveBtn onRemove={onRemove} /> </View> ) diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index 90d20d94f..01778c381 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -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})]} diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 62d1bff49..6156d3cfa 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -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, diff --git a/src/view/com/composer/useExternalLinkFetch.e2e.ts b/src/view/com/composer/useExternalLinkFetch.e2e.ts deleted file mode 100644 index 257a3e8e5..000000000 --- a/src/view/com/composer/useExternalLinkFetch.e2e.ts +++ /dev/null @@ -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} -} diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts deleted file mode 100644 index 60afadefe..000000000 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ /dev/null @@ -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} -} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 1cad5e091..8f93538c6 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -18,6 +18,8 @@ import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' import {POST_CTRL_HITSLOP} from '#/lib/constants' +import {CountWheel} from '#/lib/custom-animations/CountWheel' +import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' @@ -35,8 +37,6 @@ import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide' -import {CountWheel} from 'lib/custom-animations/CountWheel' -import {AnimatedLikeIcon} from 'lib/custom-animations/LikeIcon' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index 98332c33b..eb03385d0 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -5,6 +5,7 @@ import {AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {shareUrl} from '#/lib/sharing' @@ -55,7 +56,16 @@ export const ExternalLinkEmbed = ({ }, [link.uri, externalEmbedPrefs]) if (embedPlayerParams?.source === 'tenor') { - return <GifEmbed params={embedPlayerParams} link={link} hideAlt={hideAlt} /> + const parsedAlt = parseAltFromGIFDescription(link.description) + return ( + <GifEmbed + params={embedPlayerParams} + thumb={link.thumb} + altText={parsedAlt.alt} + isPreferredAltText={parsedAlt.isPreferred} + hideAlt={hideAlt} + /> + ) } return ( diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index a1af6ab26..fc66278c9 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -7,12 +7,10 @@ import { View, ViewStyle, } from 'react-native' -import {AppBskyEmbedExternal} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {HITSLOP_20} from '#/lib/constants' -import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import {EmbedPlayerParams} from '#/lib/strings/embed-player' import {isWeb} from '#/platform/detection' import {useAutoplayDisabled} from '#/state/preferences' @@ -77,12 +75,16 @@ function PlaybackControls({ export function GifEmbed({ params, - link, + thumb, + altText, + isPreferredAltText, hideAlt, style = {width: '100%'}, }: { params: EmbedPlayerParams - link: AppBskyEmbedExternal.ViewExternal + thumb: string | undefined + altText: string + isPreferredAltText: boolean hideAlt?: boolean style?: StyleProp<ViewStyle> }) { @@ -111,11 +113,6 @@ export function GifEmbed({ playerRef.current?.toggleAsync() }, []) - const parsedAlt = React.useMemo( - () => parseAltFromGIFDescription(link.description), - [link], - ) - return ( <View style={[a.rounded_md, a.overflow_hidden, a.mt_sm, style]}> <View @@ -131,13 +128,13 @@ export function GifEmbed({ /> <GifView source={params.playerUri} - placeholderSource={link.thumb} + placeholderSource={thumb} style={[a.flex_1, a.rounded_md]} autoplay={!autoplayDisabled} onPlayerStateChange={onPlayerStateChange} ref={playerRef} accessibilityHint={_(msg`Animated GIF`)} - accessibilityLabel={parsedAlt.alt} + accessibilityLabel={altText} /> {!playerState.isPlaying && ( <Fill @@ -150,7 +147,7 @@ export function GifEmbed({ /> )} <MediaInsetBorder /> - {!hideAlt && parsedAlt.isPreferred && <AltText text={parsedAlt.alt} />} + {!hideAlt && isPreferredAltText && <AltText text={altText} />} </View> </View> ) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 3b8152c8b..c44ec3b84 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -31,6 +31,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {s} from '#/lib/styles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {precacheProfile} from '#/state/queries/profile' +import {useResolveLinkQuery} from '#/state/queries/resolve-link' import {useSession} from '#/state/session' import {ComposerOptsQuote} from '#/state/shell/composer' import {atoms as a, useTheme} from '#/alf' @@ -286,6 +287,24 @@ export function QuoteX({onRemove}: {onRemove: () => void}) { ) } +export function LazyQuoteEmbed({uri}: {uri: string}) { + const {data} = useResolveLinkQuery(uri) + if (!data || data.type !== 'record' || data.kind !== 'post') { + return null + } + return ( + <QuoteEmbed + quote={{ + cid: data.record.cid, + uri: data.record.uri, + author: data.meta.author, + indexedAt: data.meta.indexedAt, + text: data.meta.text, + }} + /> + ) +} + function viewRecordToPostView( viewRecord: AppBskyEmbedRecord.ViewRecord, ): AppBskyFeedDefs.PostView { |