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/Gif.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/Gif.tsx')
-rw-r--r-- | src/components/Post/Embed/ExternalEmbed/Gif.tsx | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/src/components/Post/Embed/ExternalEmbed/Gif.tsx b/src/components/Post/Embed/ExternalEmbed/Gif.tsx new file mode 100644 index 000000000..a839294f1 --- /dev/null +++ b/src/components/Post/Embed/ExternalEmbed/Gif.tsx @@ -0,0 +1,224 @@ +import React from 'react' +import { + Pressable, + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {HITSLOP_20} from '#/lib/constants' +import {EmbedPlayerParams} from '#/lib/strings/embed-player' +import {isWeb} from '#/platform/detection' +import {useAutoplayDisabled} from '#/state/preferences' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {atoms as a, useTheme} from '#/alf' +import {Fill} from '#/components/Fill' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' +import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' +import {GifView} from '../../../../../modules/expo-bluesky-gif-view' +import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' + +function PlaybackControls({ + onPress, + isPlaying, + isLoaded, +}: { + onPress: () => void + isPlaying: boolean + isLoaded: boolean +}) { + const {_} = useLingui() + const t = useTheme() + + return ( + <Pressable + accessibilityRole="button" + accessibilityHint={_(msg`Plays or pauses the GIF`)} + accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)} + style={[ + a.absolute, + a.align_center, + a.justify_center, + !isLoaded && a.border, + t.atoms.border_contrast_medium, + a.inset_0, + a.w_full, + a.h_full, + { + zIndex: 2, + backgroundColor: !isLoaded + ? t.atoms.bg_contrast_25.backgroundColor + : undefined, + }, + ]} + onPress={onPress}> + {!isLoaded ? ( + <View> + <View style={[a.align_center, a.justify_center]}> + <Loader size="xl" /> + </View> + </View> + ) : !isPlaying ? ( + <PlayButtonIcon /> + ) : undefined} + </Pressable> + ) +} + +export function GifEmbed({ + params, + thumb, + altText, + isPreferredAltText, + hideAlt, + style = {width: '100%'}, +}: { + params: EmbedPlayerParams + thumb: string | undefined + altText: string + isPreferredAltText: boolean + hideAlt?: boolean + style?: StyleProp<ViewStyle> +}) { + const t = useTheme() + const {_} = useLingui() + const autoplayDisabled = useAutoplayDisabled() + + const playerRef = React.useRef<GifView>(null) + + const [playerState, setPlayerState] = React.useState<{ + isPlaying: boolean + isLoaded: boolean + }>({ + isPlaying: !autoplayDisabled, + isLoaded: false, + }) + + const onPlayerStateChange = React.useCallback( + (e: GifViewStateChangeEvent) => { + setPlayerState(e.nativeEvent) + }, + [], + ) + + const onPress = React.useCallback(() => { + playerRef.current?.toggleAsync() + }, []) + + return ( + <View + style={[ + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + {aspectRatio: params.dimensions!.width / params.dimensions!.height}, + style, + ]}> + <View + style={[ + a.absolute, + /* + * Aspect ratio was being clipped weirdly on web -esb + */ + { + top: -2, + bottom: -2, + left: -2, + right: -2, + }, + ]}> + <PlaybackControls + onPress={onPress} + isPlaying={playerState.isPlaying} + isLoaded={playerState.isLoaded} + /> + <GifView + source={params.playerUri} + placeholderSource={thumb} + style={[a.flex_1]} + autoplay={!autoplayDisabled} + onPlayerStateChange={onPlayerStateChange} + ref={playerRef} + accessibilityHint={_(msg`Animated GIF`)} + accessibilityLabel={altText} + /> + {!playerState.isPlaying && ( + <Fill + style={[ + t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, + { + opacity: 0.3, + }, + ]} + /> + )} + {!hideAlt && isPreferredAltText && <AltText text={altText} />} + </View> + </View> + ) +} + +function AltText({text}: {text: string}) { + const control = Prompt.usePromptControl() + const largeAltBadge = useLargeAltBadgeEnabled() + + const {_} = useLingui() + return ( + <> + <TouchableOpacity + testID="altTextButton" + accessibilityRole="button" + accessibilityLabel={_(msg`Show alt text`)} + accessibilityHint="" + hitSlop={HITSLOP_20} + onPress={control.open} + style={styles.altContainer}> + <Text + style={[styles.alt, largeAltBadge && a.text_xs]} + accessible={false}> + <Trans>ALT</Trans> + </Text> + </TouchableOpacity> + <Prompt.Outer control={control}> + <Prompt.TitleText> + <Trans>Alt Text</Trans> + </Prompt.TitleText> + <Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText> + <Prompt.Actions> + <Prompt.Action + onPress={() => control.close()} + cta={_(msg`Close`)} + color="secondary" + /> + </Prompt.Actions> + </Prompt.Outer> + </> + ) +} + +const styles = StyleSheet.create({ + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: isWeb ? 8 : 6, + paddingVertical: isWeb ? 6 : 3, + position: 'absolute', + // Related to margin/gap hack. This keeps the alt label in the same position + // on all platforms + right: isWeb ? 8 : 5, + bottom: isWeb ? 8 : 5, + zIndex: 2, + }, + alt: { + color: 'white', + fontSize: isWeb ? 10 : 7, + fontWeight: '600', + }, +}) |