diff options
author | Eric Bailey <git@esb.lol> | 2025-06-13 12:05:41 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-13 12:05:41 -0500 |
commit | 45f0f7eefecae1922c2f30d4e7760d2b93b1ae56 (patch) | |
tree | a2fd6917867f18fe334b54dd3289775c2930bc85 /src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx | |
parent | ba0f5a9bdef5bd0447ded23cab1af222b65511cc (diff) | |
download | voidsky-45f0f7eefecae1922c2f30d4e7760d2b93b1ae56.tar.zst |
Port post embeds to new arch (#7408)
* Direct port of embeds to new arch (cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6) * Re-org * Split out ListEmbed and FeedEmbed * Split out ImageEmbed * DRY up a bit * Port over ExternalLinkEmbed * Port over Player and Gif embeds * Migrate ComposerReplyTo * Replace other usages of old post-embeds * Migrate view contexts * Copy pasta VideoEmbed * Copy pasta GifEmbed * Swap in new file location * Clean up * Fix up native * Add back in correct moderation on List and Feed embeds * Format * Prettier * delete old video utils * move bandwidth-estimate.ts * Remove log * Add LazyQuoteEmbed for composer use * Clean up unused things * Remove remaining items * Prettier * Fix imports * Handle nested quotes same as prod * Add back silenced error handling * Fix lint --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx')
-rw-r--r-- | src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx | 147 |
1 files changed, 147 insertions, 0 deletions
diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx new file mode 100644 index 000000000..8a12f0374 --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx @@ -0,0 +1,147 @@ +import React from 'react' +import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native' +import {Image} from 'expo-image' +import {AppBskyEmbedExternal} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {EmbedPlayerParams} from '#/lib/strings/embed-player' +import {isIOS, isNative, isWeb} from '#/platform/detection' +import {useExternalEmbedsPrefs} from '#/state/preferences' +import {atoms as a, useTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' +import {Fill} from '#/components/Fill' +import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' + +export function ExternalGif({ + link, + params, +}: { + link: AppBskyEmbedExternal.ViewExternal + params: EmbedPlayerParams +}) { + const t = useTheme() + const externalEmbedsPrefs = useExternalEmbedsPrefs() + const {_} = useLingui() + const consentDialogControl = useDialogControl() + + // Tracking if the placer has been activated + const [isPlayerActive, setIsPlayerActive] = React.useState(false) + // Tracking whether the gif has been loaded yet + const [isPrefetched, setIsPrefetched] = React.useState(false) + // Tracking whether the image is animating + const [isAnimating, setIsAnimating] = React.useState(true) + + // Used for controlling animation + const imageRef = React.useRef<Image>(null) + + const load = React.useCallback(() => { + setIsPlayerActive(true) + Image.prefetch(params.playerUri).then(() => { + // Replace the image once it's fetched + setIsPrefetched(true) + }) + }, [params.playerUri]) + + const onPlayPress = React.useCallback( + (event: GestureResponderEvent) => { + // Don't propagate on web + event.preventDefault() + + // Show consent if this is the first load + if (externalEmbedsPrefs?.[params.source] === undefined) { + consentDialogControl.open() + return + } + // If the player isn't active, we want to activate it and prefetch the gif + if (!isPlayerActive) { + load() + return + } + // Control animation on native + setIsAnimating(prev => { + if (prev) { + if (isNative) { + imageRef.current?.stopAnimating() + } + return false + } else { + if (isNative) { + imageRef.current?.startAnimating() + } + return true + } + }) + }, + [ + consentDialogControl, + externalEmbedsPrefs, + isPlayerActive, + load, + params.source, + ], + ) + + return ( + <> + <EmbedConsentDialog + control={consentDialogControl} + source={params.source} + onAccept={load} + /> + + <Pressable + style={[ + {height: 300}, + a.w_full, + a.overflow_hidden, + { + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + }, + ]} + onPress={onPlayPress} + accessibilityRole="button" + accessibilityHint={_(msg`Plays the GIF`)} + accessibilityLabel={_(msg`Play ${link.title}`)}> + <Image + source={{ + uri: + !isPrefetched || (isWeb && !isAnimating) + ? link.thumb + : params.playerUri, + }} // Web uses the thumb to control playback + style={{flex: 1}} + ref={imageRef} + autoplay={isAnimating} + contentFit="contain" + accessibilityIgnoresInvertColors + accessibilityLabel={link.title} + accessibilityHint={link.title} + cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios + /> + + {(!isPrefetched || !isAnimating) && ( + <Fill style={[a.align_center, a.justify_center]}> + <Fill + style={[ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.3, + }, + ]} + /> + + {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active + <PlayButtonIcon /> + ) : ( + // Activity indicator while gif loads + <ActivityIndicator size="large" color="white" /> + )} + </Fill> + )} + </Pressable> + </> + ) +} |