diff options
author | dan <dan.abramov@gmail.com> | 2024-10-08 09:02:58 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-07 17:02:58 -0700 |
commit | c06040cc209338fc37980648b31d4d64cc0c5c09 (patch) | |
tree | 766e41a310b03bed2e927f468114ca8d14602e5f | |
parent | dd8be2e939d2879e2bb23b2ccd843a034d19b8dd (diff) | |
download | voidsky-c06040cc209338fc37980648b31d4d64cc0c5c09.tar.zst |
Fetch link previews from RQ (#5608)
Co-authored-by: Mary <git@mary.my.id> Co-authored-by: Hailey <me@haileyok.com>
-rw-r--r-- | src/App.native.tsx | 61 | ||||
-rw-r--r-- | src/App.web.tsx | 57 | ||||
-rw-r--r-- | src/lib/api/index.ts | 47 | ||||
-rw-r--r-- | src/lib/api/resolve.ts | 49 | ||||
-rw-r--r-- | src/state/queries/resolve-link.ts | 70 | ||||
-rw-r--r-- | src/state/shell/composer/index.tsx | 23 | ||||
-rw-r--r-- | src/state/shell/index.tsx | 16 | ||||
-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 |
17 files changed, 490 insertions, 516 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index c6334379f..96b493af4 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -51,6 +51,7 @@ import { } from '#/state/session' import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' +import {Provider as ComposerProvider} from '#/state/shell/composer' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -125,35 +126,37 @@ function InnerApp() { // Resets the entire tree below when it changes: key={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}> - <StatsigProvider> - <MessagesProvider> - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - <LabelDefsProvider> - <ModerationOptsProvider> - <LoggedOutViewProvider> - <SelectedFeedProvider> - <HiddenRepliesProvider> - <UnreadNotifsProvider> - <BackgroundNotificationPreferencesProvider> - <MutedThreadsProvider> - <ProgressGuideProvider> - <GestureHandlerRootView - style={s.h100pct}> - <TestCtrls /> - <Shell /> - <NuxDialogs /> - </GestureHandlerRootView> - </ProgressGuideProvider> - </MutedThreadsProvider> - </BackgroundNotificationPreferencesProvider> - </UnreadNotifsProvider> - </HiddenRepliesProvider> - </SelectedFeedProvider> - </LoggedOutViewProvider> - </ModerationOptsProvider> - </LabelDefsProvider> - </MessagesProvider> - </StatsigProvider> + <ComposerProvider> + <StatsigProvider> + <MessagesProvider> + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + <LabelDefsProvider> + <ModerationOptsProvider> + <LoggedOutViewProvider> + <SelectedFeedProvider> + <HiddenRepliesProvider> + <UnreadNotifsProvider> + <BackgroundNotificationPreferencesProvider> + <MutedThreadsProvider> + <ProgressGuideProvider> + <GestureHandlerRootView + style={s.h100pct}> + <TestCtrls /> + <Shell /> + <NuxDialogs /> + </GestureHandlerRootView> + </ProgressGuideProvider> + </MutedThreadsProvider> + </BackgroundNotificationPreferencesProvider> + </UnreadNotifsProvider> + </HiddenRepliesProvider> + </SelectedFeedProvider> + </LoggedOutViewProvider> + </ModerationOptsProvider> + </LabelDefsProvider> + </MessagesProvider> + </StatsigProvider> + </ComposerProvider> </QueryProvider> </React.Fragment> </VideoVolumeProvider> diff --git a/src/App.web.tsx b/src/App.web.tsx index 1664812d0..0d500908f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -41,6 +41,7 @@ import { } from '#/state/session' import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' +import {Provider as ComposerProvider} from '#/state/shell/composer' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' @@ -116,33 +117,35 @@ function InnerApp() { // Resets the entire tree below when it changes: key={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}> - <StatsigProvider> - <MessagesProvider> - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - <LabelDefsProvider> - <ModerationOptsProvider> - <LoggedOutViewProvider> - <SelectedFeedProvider> - <HiddenRepliesProvider> - <UnreadNotifsProvider> - <BackgroundNotificationPreferencesProvider> - <MutedThreadsProvider> - <SafeAreaProvider> - <ProgressGuideProvider> - <Shell /> - <NuxDialogs /> - </ProgressGuideProvider> - </SafeAreaProvider> - </MutedThreadsProvider> - </BackgroundNotificationPreferencesProvider> - </UnreadNotifsProvider> - </HiddenRepliesProvider> - </SelectedFeedProvider> - </LoggedOutViewProvider> - </ModerationOptsProvider> - </LabelDefsProvider> - </MessagesProvider> - </StatsigProvider> + <ComposerProvider> + <StatsigProvider> + <MessagesProvider> + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + <LabelDefsProvider> + <ModerationOptsProvider> + <LoggedOutViewProvider> + <SelectedFeedProvider> + <HiddenRepliesProvider> + <UnreadNotifsProvider> + <BackgroundNotificationPreferencesProvider> + <MutedThreadsProvider> + <SafeAreaProvider> + <ProgressGuideProvider> + <Shell /> + <NuxDialogs /> + </ProgressGuideProvider> + </SafeAreaProvider> + </MutedThreadsProvider> + </BackgroundNotificationPreferencesProvider> + </UnreadNotifsProvider> + </HiddenRepliesProvider> + </SelectedFeedProvider> + </LoggedOutViewProvider> + </ModerationOptsProvider> + </LabelDefsProvider> + </MessagesProvider> + </StatsigProvider> + </ComposerProvider> </QueryProvider> <ToastContainer /> </React.Fragment> diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index e6e8eea3d..6edb111e6 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -12,6 +12,7 @@ import { ComAtprotoRepoStrongRef, RichText, } from '@atproto/api' +import {QueryClient} from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' @@ -19,6 +20,10 @@ import {logger} from '#/logger' import {ComposerImage, compressImage} from '#/state/gallery' import {writePostgateRecord} from '#/state/queries/postgate' import { + fetchResolveGifQuery, + fetchResolveLinkQuery, +} from '#/state/queries/resolve-link' +import { createThreadgateRecord, ThreadgateAllowUISetting, threadgateAllowUISettingToAllowRecordValue, @@ -27,7 +32,6 @@ import { import {ComposerState, EmbedDraft} from '#/view/com/composer/state/composer' import {createGIFDescription} from '../gif-alt-text' import {LinkMeta} from '../link-meta/link-meta' -import {resolveGif, resolveLink} from './resolve' import {uploadBlob} from './upload-blob' export {uploadBlob} @@ -51,7 +55,11 @@ interface PostOpts { langs?: string[] } -export async function post(agent: BskyAgent, opts: PostOpts) { +export async function post( + agent: BskyAgent, + queryClient: QueryClient, + opts: PostOpts, +) { let reply let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true}) @@ -64,6 +72,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) { const embed = await resolveEmbed( agent, + queryClient, opts.composerState, opts.onStateChange, ) @@ -178,6 +187,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) { async function resolveEmbed( agent: BskyAgent, + queryClient: QueryClient, draft: ComposerState, onStateChange: ((state: string) => void) | undefined, ): Promise< @@ -190,8 +200,8 @@ async function resolveEmbed( > { if (draft.embed.quote) { const [resolvedMedia, resolvedQuote] = await Promise.all([ - resolveMedia(agent, draft.embed, onStateChange), - resolveRecord(agent, draft.embed.quote.uri), + resolveMedia(agent, queryClient, draft.embed, onStateChange), + resolveRecord(agent, queryClient, draft.embed.quote.uri), ]) if (resolvedMedia) { return { @@ -208,12 +218,21 @@ async function resolveEmbed( record: resolvedQuote, } } - const resolvedMedia = await resolveMedia(agent, draft.embed, onStateChange) + const resolvedMedia = await resolveMedia( + agent, + queryClient, + draft.embed, + onStateChange, + ) if (resolvedMedia) { return resolvedMedia } if (draft.embed.link) { - const resolvedLink = await resolveLink(agent, draft.embed.link.uri) + const resolvedLink = await fetchResolveLinkQuery( + queryClient, + agent, + draft.embed.link.uri, + ) if (resolvedLink.type === 'record') { return { $type: 'app.bsky.embed.record', @@ -226,6 +245,7 @@ async function resolveEmbed( async function resolveMedia( agent: BskyAgent, + queryClient: QueryClient, embedDraft: EmbedDraft, onStateChange: ((state: string) => void) | undefined, ): Promise< @@ -286,7 +306,11 @@ async function resolveMedia( } if (embedDraft.media?.type === 'gif') { const gifDraft = embedDraft.media - const resolvedGif = await resolveGif(agent, gifDraft.gif) + const resolvedGif = await fetchResolveGifQuery( + queryClient, + agent, + gifDraft.gif, + ) let blob: BlobRef | undefined if (resolvedGif.thumb) { onStateChange?.('Uploading link thumbnail...') @@ -305,7 +329,11 @@ async function resolveMedia( } } if (embedDraft.link) { - const resolvedLink = await resolveLink(agent, embedDraft.link.uri) + const resolvedLink = await fetchResolveLinkQuery( + queryClient, + agent, + embedDraft.link.uri, + ) if (resolvedLink.type === 'external') { let blob: BlobRef | undefined if (resolvedLink.thumb) { @@ -330,9 +358,10 @@ async function resolveMedia( async function resolveRecord( agent: BskyAgent, + queryClient: QueryClient, uri: string, ): Promise<ComAtprotoRepoStrongRef.Main> { - const resolvedLink = await resolveLink(agent, uri) + const resolvedLink = await fetchResolveLinkQuery(queryClient, agent, uri) if (resolvedLink.type !== 'record') { throw Error('Expected uri to resolve to a record') } diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index a97a3f31c..4f409e100 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -1,4 +1,4 @@ -import {ComAtprotoRepoStrongRef} from '@atproto/api' +import {AppBskyActorDefs, ComAtprotoRepoStrongRef} from '@atproto/api' import {AtUri} from '@atproto/api' import {BskyAgent} from '@atproto/api' @@ -33,12 +33,32 @@ type ResolvedExternalLink = { thumb: ComposerImage | undefined } -type ResolvedRecord = { +type ResolvedPostRecord = { type: 'record' record: ComAtprotoRepoStrongRef.Main + kind: 'post' + meta: { + text: string + indexedAt: string + author: AppBskyActorDefs.ProfileViewBasic + } } -type ResolvedLink = ResolvedExternalLink | ResolvedRecord +type ResolvedOtherRecord = { + type: 'record' + record: ComAtprotoRepoStrongRef.Main + kind: 'other' + meta: { + // We should replace this with a hydrated record (e.g. feed, list, starter pack) + // and change the composer preview to use the actual post embed components: + title: string + } +} + +export type ResolvedLink = + | ResolvedExternalLink + | ResolvedPostRecord + | ResolvedOtherRecord export async function resolveLink( agent: BskyAgent, @@ -57,6 +77,8 @@ export async function resolveLink( cid: result.cid, uri: result.uri, }, + kind: 'post', + meta: result, } } if (isBskyCustomFeedUrl(uri)) { @@ -64,7 +86,12 @@ export async function resolveLink( const result = await getFeedAsEmbed(agent, fetchDid, uri) return { type: 'record', - record: result.embed!.record, // TODO: Fix types. + record: result.embed!.record, + kind: 'other', + meta: { + // TODO: Include hydrated content instead. + title: result.meta!.title!, + }, } } if (isBskyListUrl(uri)) { @@ -72,7 +99,12 @@ export async function resolveLink( const result = await getListAsEmbed(agent, fetchDid, uri) return { type: 'record', - record: result.embed!.record, // TODO: Fix types. + record: result.embed!.record, + kind: 'other', + meta: { + // TODO: Include hydrated content instead. + title: result.meta!.title!, + }, } } if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { @@ -80,7 +112,12 @@ export async function resolveLink( const result = await getStarterPackAsEmbed(agent, fetchDid, uri) return { type: 'record', - record: result.embed!.record, // TODO: Fix types. + record: result.embed!.record, + kind: 'other', + meta: { + // TODO: Include hydrated content instead. + title: result.meta!.title!, + }, } } return resolveExternal(agent, uri) diff --git a/src/state/queries/resolve-link.ts b/src/state/queries/resolve-link.ts new file mode 100644 index 000000000..5856cfb5f --- /dev/null +++ b/src/state/queries/resolve-link.ts @@ -0,0 +1,70 @@ +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {STALE} from '#/state/queries/index' +import {useAgent} from '../session' + +const RQKEY_LINK_ROOT = 'resolve-link' +export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url] + +const RQKEY_GIF_ROOT = 'resolve-gif' +export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url] + +import {BskyAgent} from '@atproto/api' + +import {ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve' +import {Gif} from './tenor' + +export function useResolveLinkQuery(url: string) { + const agent = useAgent() + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_LINK(url), + queryFn: async () => { + return await resolveLink(agent, url) + }, + }) +} +export function fetchResolveLinkQuery( + queryClient: QueryClient, + agent: BskyAgent, + url: string, +) { + return queryClient.fetchQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_LINK(url), + queryFn: async () => { + return await resolveLink(agent, url) + }, + }) +} +export function precacheResolveLinkQuery( + queryClient: QueryClient, + url: string, + resolvedLink: ResolvedLink, +) { + queryClient.setQueryData(RQKEY_LINK(url), resolvedLink) +} + +export function useResolveGifQuery(gif: Gif) { + const agent = useAgent() + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_GIF(gif.url), + queryFn: async () => { + return await resolveGif(agent, gif) + }, + }) +} +export function fetchResolveGifQuery( + queryClient: QueryClient, + agent: BskyAgent, + gif: Gif, +) { + return queryClient.fetchQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY_GIF(gif.url), + queryFn: async () => { + return await resolveGif(agent, gif) + }, + }) +} diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx index 770b0789e..096948506 100644 --- a/src/state/shell/composer/index.tsx +++ b/src/state/shell/composer/index.tsx @@ -7,9 +7,12 @@ import { } from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers' import {purgeTemporaryImageFiles} from '#/state/gallery' +import {precacheResolveLinkQuery} from '#/state/queries/resolve-link' import * as Toast from '#/view/com/util/Toast' export interface ComposerOptsPostRef { @@ -58,8 +61,28 @@ const controlsContext = React.createContext<ControlsContext>({ export function Provider({children}: React.PropsWithChildren<{}>) { const {_} = useLingui() const [state, setState] = React.useState<StateContext>() + const queryClient = useQueryClient() const openComposer = useNonReactiveCallback((opts: ComposerOpts) => { + if (opts.quote) { + const path = postUriToRelativePath(opts.quote.uri) + if (path) { + const appUrl = toBskyAppUrl(path) + precacheResolveLinkQuery(queryClient, appUrl, { + type: 'record', + kind: 'post', + record: { + cid: opts.quote.cid, + uri: opts.quote.uri, + }, + meta: { + author: opts.quote.author, + indexedAt: opts.quote.indexedAt, + text: opts.quote.text, + }, + }) + } + } const author = opts.replyTo?.author || opts.quote?.author const isBlocked = Boolean( author && diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index 07909c000..f61dc3c41 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -1,22 +1,22 @@ import React from 'react' -import {Provider as ShellLayoutProvder} from './shell-layout' + +import {Provider as ColorModeProvider} from './color-mode' import {Provider as DrawerOpenProvider} from './drawer-open' import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled' import {Provider as MinimalModeProvider} from './minimal-mode' -import {Provider as ColorModeProvider} from './color-mode' import {Provider as OnboardingProvider} from './onboarding' -import {Provider as ComposerProvider} from './composer' +import {Provider as ShellLayoutProvder} from './shell-layout' import {Provider as TickEveryMinuteProvider} from './tick-every-minute' +export {useSetThemePrefs, useThemePrefs} from './color-mode' +export {useComposerControls, useComposerState} from './composer' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export { useIsDrawerSwipeDisabled, useSetDrawerSwipeDisabled, } from './drawer-swipe-disabled' export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode' -export {useThemePrefs, useSetThemePrefs} from './color-mode' -export {useOnboardingState, useOnboardingDispatch} from './onboarding' -export {useComposerState, useComposerControls} from './composer' +export {useOnboardingDispatch, useOnboardingState} from './onboarding' export {useTickEveryMinute} from './tick-every-minute' export function Provider({children}: React.PropsWithChildren<{}>) { @@ -27,9 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { <MinimalModeProvider> <ColorModeProvider> <OnboardingProvider> - <ComposerProvider> - <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> - </ComposerProvider> + <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> </OnboardingProvider> </ColorModeProvider> </MinimalModeProvider> 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 { |