diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/statsig/gates.ts | 1 | ||||
-rw-r--r-- | src/lib/strings/embed-player.ts | 46 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 10 | ||||
-rw-r--r-- | src/view/com/composer/ExternalEmbed.tsx | 70 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/ExternalLinkEmbed.tsx | 150 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/GifEmbed.tsx | 140 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 52 |
7 files changed, 320 insertions, 149 deletions
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index c41083afb..84183c1d9 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -4,7 +4,6 @@ export type Gate = | 'disable_min_shell_on_foregrounding_v2' | 'disable_poll_on_discover_v2' | 'hide_vertical_scroll_indicators' - | 'new_gif_player' | 'show_follow_back_label_v2' | 'start_session_with_following_v2' | 'use_new_suggestions_endpoint' diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index bbc58a206..b1fc75b8b 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -1,4 +1,4 @@ -import {Dimensions} from 'react-native' +import {Dimensions, Platform} from 'react-native' import {isWeb} from 'platform/detection' const {height: SCREEN_HEIGHT} = Dimensions.get('window') @@ -255,16 +255,6 @@ export function parseEmbedPlayerFromUrl( if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') { const [_, gifs, nameAndId] = urlp.pathname.split('/') - const h = urlp.searchParams.get('hh') - const w = urlp.searchParams.get('ww') - let dimensions - if (h && w) { - dimensions = { - height: Number(h), - width: Number(w), - } - } - /* * nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name) * We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can @@ -281,10 +271,7 @@ export function parseEmbedPlayerFromUrl( isGif: true, hideDetails: true, metaUri: `https://giphy.com/gifs/${gifId}`, - playerUri: `https://i.giphy.com/media/${gifId}/${ - dimensions ? '200.mp4' : '200.webp' - }`, - dimensions, + playerUri: `https://i.giphy.com/media/${gifId}/200.webp`, } } } @@ -350,21 +337,34 @@ export function parseEmbedPlayerFromUrl( } } - if (urlp.hostname === 'tenor.com' || urlp.hostname === 'www.tenor.com') { - const [_, pathOrIntl, pathOrFilename, intlFilename] = - urlp.pathname.split('/') - const isIntl = pathOrFilename === 'view' - const filename = isIntl ? intlFilename : pathOrFilename + if (urlp.hostname === 'media.tenor.com') { + let [_, id, filename] = urlp.pathname.split('/') - if ((pathOrIntl === 'view' || pathOrFilename === 'view') && filename) { - const includesExt = filename.split('.').pop() === 'gif' + const h = urlp.searchParams.get('hh') + const w = urlp.searchParams.get('ww') + let dimensions + if (h && w) { + dimensions = { + height: Number(h), + width: Number(w), + } + } + + if (id && filename && dimensions && id.includes('AAAAC')) { + if (Platform.OS === 'web') { + id = id.replace('AAAAC', 'AAAP3') + filename = filename.replace('.gif', '.webm') + } else { + id = id.replace('AAAAC', 'AAAAM') + } return { type: 'tenor_gif', source: 'tenor', isGif: true, hideDetails: true, - playerUri: `${url}${!includesExt ? '.gif' : ''}`, + playerUri: `https://t.gifs.bsky.app/${id}/${filename}`, + dimensions, } } } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 93e2dc6b5..8d14c16e2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -121,6 +121,7 @@ export const ComposePost = observer(function ComposePost({ initQuote, ) const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) + const [extGif, setExtGif] = useState<Gif>() const [labels, setLabels] = useState<string[]>([]) const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([]) const gallery = useMemo( @@ -318,7 +319,7 @@ export const ComposePost = observer(function ComposePost({ const onSelectGif = useCallback( (gif: Gif) => { setExtLink({ - uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[0]}&ww=${gif.media_formats.gif.dims[1]}`, + 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, @@ -328,6 +329,7 @@ export const ComposePost = observer(function ComposePost({ description: `ALT: ${gif.content_description}`, }, }) + setExtGif(gif) }, [setExtLink], ) @@ -473,7 +475,11 @@ export const ComposePost = observer(function ComposePost({ {gallery.isEmpty && extLink && ( <ExternalEmbed link={extLink} - onRemove={() => setExtLink(undefined)} + gif={extGif} + onRemove={() => { + setExtLink(undefined) + setExtGif(undefined) + }} /> )} {quote ? ( diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 3c2bf762d..321e29b30 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -1,11 +1,12 @@ import React from 'react' -import {TouchableOpacity, View} from 'react-native' +import {StyleProp, TouchableOpacity, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {ExternalEmbedDraft} from 'lib/api/index' import {s} from 'lib/styles' +import {Gif} from 'state/queries/tenor' import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed' import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' @@ -14,9 +15,11 @@ import {Text} from '#/components/Typography' export const ExternalEmbed = ({ link, onRemove, + gif, }: { link?: ExternalEmbedDraft onRemove: () => void + gif?: Gif }) => { const t = useTheme() const {_} = useLingui() @@ -34,45 +37,38 @@ export const ExternalEmbed = ({ 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 + return ( - <View - style={[ - a.border, - a.rounded_sm, - a.mt_2xl, - a.mb_xl, - a.overflow_hidden, - t.atoms.border_contrast_medium, - ]}> + <View style={[a.mb_xl, a.overflow_hidden, t.atoms.border_contrast_medium]}> {link.isLoading ? ( - <View - style={[ - a.align_center, - a.justify_center, - a.py_5xl, - t.atoms.bg_contrast_25, - ]}> + <Container style={loadingStyle}> <Loader size="xl" /> - </View> + </Container> ) : link.meta?.error ? ( - <View - style={[a.justify_center, a.p_md, a.gap_xs, t.atoms.bg_contrast_25]}> + <Container style={[a.align_start, a.p_md, a.gap_xs]}> <Text numberOfLines={1} style={t.atoms.text_contrast_high}> {link.uri} </Text> <Text numberOfLines={2} style={[{color: t.palette.negative_400}]}> - {link.meta.error} + {link.meta?.error} </Text> - </View> + </Container> ) : linkInfo ? ( - <View style={{pointerEvents: 'none'}}> + <View style={{pointerEvents: !gif ? 'none' : 'auto'}}> <ExternalLinkEmbed link={linkInfo} /> </View> ) : null} <TouchableOpacity style={{ position: 'absolute', - top: 10, + top: 16, right: 10, height: 36, width: 36, @@ -91,3 +87,29 @@ export const ExternalEmbed = ({ </View> ) } + +function Container({ + style, + children, +}: { + style?: StyleProp<ViewStyle> + children: React.ReactNode +}) { + const t = useTheme() + return ( + <View + style={[ + a.mt_sm, + a.rounded_sm, + a.border, + a.align_center, + a.justify_center, + a.py_5xl, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_medium, + style, + ]}> + {children} + </View> + ) +} diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index ff7c643f6..1fe75c44e 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -1,27 +1,32 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' +import React, {useCallback} from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedExternal} from '@atproto/api' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useGate} from 'lib/statsig/statsig' +import {shareUrl} from 'lib/sharing' import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' import {toNiceDomain} from 'lib/strings/url-helpers' +import {isNative} from 'platform/detection' import {useExternalEmbedsPrefs} from 'state/preferences' +import {Link} from 'view/com/util/Link' import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' +import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' +import {atoms as a, useTheme} from '#/alf' import {Text} from '../text/Text' export const ExternalLinkEmbed = ({ link, + style, }: { link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp<ViewStyle> }) => { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const externalEmbedPrefs = useExternalEmbedsPrefs() - const gate = useGate() const embedPlayerParams = React.useMemo(() => { const params = parseEmbedPlayerFromUrl(link.uri) @@ -30,71 +35,96 @@ export const ExternalLinkEmbed = ({ return params } }, [link.uri, externalEmbedPrefs]) - const isCompatibleGiphy = - embedPlayerParams?.source === 'giphy' && - embedPlayerParams.dimensions && - gate('new_gif_player') + + if (embedPlayerParams?.source === 'tenor') { + return <GifEmbed params={embedPlayerParams} link={link} /> + } return ( - <View style={styles.container}> - {link.thumb && !embedPlayerParams ? ( - <Image - style={{aspectRatio: 1.91}} - source={{uri: link.thumb}} - accessibilityIgnoresInvertColors - /> - ) : undefined} - {isCompatibleGiphy ? ( - <View /> - ) : embedPlayerParams?.isGif ? ( - <ExternalGifEmbed link={link} params={embedPlayerParams} /> - ) : embedPlayerParams ? ( - <ExternalPlayer link={link} params={embedPlayerParams} /> - ) : undefined} - <View style={[styles.info, {paddingHorizontal: isMobile ? 10 : 14}]}> - {!isCompatibleGiphy && ( + <View style={[a.flex_col, a.rounded_sm, a.overflow_hidden, a.mt_sm]}> + <LinkWrapper link={link} style={style}> + {link.thumb && !embedPlayerParams ? ( + <Image + style={{ + aspectRatio: 1.91, + borderTopRightRadius: 6, + borderTopLeftRadius: 6, + }} + source={{uri: link.thumb}} + accessibilityIgnoresInvertColors + /> + ) : undefined} + {embedPlayerParams?.isGif ? ( + <ExternalGifEmbed link={link} params={embedPlayerParams} /> + ) : embedPlayerParams ? ( + <ExternalPlayer link={link} params={embedPlayerParams} /> + ) : undefined} + <View + style={[ + a.flex_1, + a.py_sm, + { + paddingHorizontal: isMobile ? 10 : 14, + }, + ]}> <Text type="sm" numberOfLines={1} - style={[pal.textLight, styles.extUri]}> + style={[pal.textLight, {marginVertical: 2}]}> {toNiceDomain(link.uri)} </Text> - )} - {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( - <Text type="lg-bold" numberOfLines={3} style={[pal.text]}> - {link.title || link.uri} - </Text> - )} - {link.description && !embedPlayerParams?.hideDetails ? ( - <Text - type="md" - numberOfLines={link.thumb ? 2 : 4} - style={[pal.text, styles.extDescription]}> - {link.description} - </Text> - ) : undefined} - </View> + {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( + <Text type="lg-bold" numberOfLines={3} style={[pal.text]}> + {link.title || link.uri} + </Text> + )} + {link.description ? ( + <Text + type="md" + numberOfLines={link.thumb ? 2 : 4} + style={[pal.text, a.mt_xs]}> + {link.description} + </Text> + ) : undefined} + </View> + </LinkWrapper> </View> ) } -const styles = StyleSheet.create({ - container: { - flexDirection: 'column', - borderRadius: 6, - overflow: 'hidden', - }, - info: { - width: '100%', - bottom: 0, - paddingTop: 8, - paddingBottom: 10, - }, - extUri: { - marginTop: 2, - }, - extDescription: { - marginTop: 4, - }, -}) +function LinkWrapper({ + link, + style, + children, +}: { + link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp<ViewStyle> + children: React.ReactNode +}) { + const t = useTheme() + + const onShareExternal = useCallback(() => { + if (link.uri && isNative) { + shareUrl(link.uri) + } + }, [link.uri]) + + return ( + <Link + asAnchor + anchorNoUnderline + href={link.uri} + style={[ + a.flex_1, + a.border, + a.rounded_sm, + t.atoms.border_contrast_medium, + style, + ]} + hoverStyle={t.atoms.border_contrast_high} + onLongPress={onShareExternal}> + {children} + </Link> + ) +} diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx new file mode 100644 index 000000000..32bd75df0 --- /dev/null +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -0,0 +1,140 @@ +import React from 'react' +import {Pressable, View} from 'react-native' +import {AppBskyEmbedExternal} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {EmbedPlayerParams} from 'lib/strings/embed-player' +import {useAutoplayDisabled} from 'state/preferences' +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' +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`Play or pause 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 + : !isPlaying + ? 'rgba(0, 0, 0, 0.3)' + : undefined, + }, + ]} + onPress={onPress}> + {!isLoaded ? ( + <View> + <View style={[a.align_center, a.justify_center]}> + <Loader size="xl" /> + </View> + </View> + ) : !isPlaying ? ( + <View + style={[ + a.rounded_full, + a.align_center, + a.justify_center, + { + backgroundColor: t.palette.primary_500, + width: 60, + height: 60, + }, + ]}> + <FontAwesomeIcon + icon="play" + size={42} + color="white" + style={{marginLeft: 8}} + /> + </View> + ) : undefined} + </Pressable> + ) +} + +export function GifEmbed({ + params, + link, +}: { + params: EmbedPlayerParams + link: AppBskyEmbedExternal.ViewExternal +}) { + 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_sm, a.overflow_hidden, a.mt_sm]}> + <View + style={[ + a.rounded_sm, + a.overflow_hidden, + { + aspectRatio: params.dimensions!.width / params.dimensions!.height, + }, + ]}> + <PlaybackControls + onPress={onPress} + isPlaying={playerState.isPlaying} + isLoaded={playerState.isLoaded} + /> + <GifView + source={params.playerUri} + placeholderSource={link.thumb} + style={[a.flex_1, a.rounded_sm]} + autoplay={!autoplayDisabled} + onPlayerStateChange={onPlayerStateChange} + ref={playerRef} + accessibilityHint={_(msg`Animated GIF`)} + accessibilityLabel={link.description.replace('ALT: ', '')} + /> + </View> + </View> + ) +} diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 47091fbb0..7ea5b55cf 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,34 +1,32 @@ -import React, {useCallback} from 'react' +import React from 'react' import { - StyleSheet, + InteractionManager, StyleProp, + StyleSheet, + Text, View, ViewStyle, - Text, - InteractionManager, } from 'react-native' import {Image} from 'expo-image' import { - AppBskyEmbedImages, AppBskyEmbedExternal, + AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyGraphDefs, ModerationDecision, } from '@atproto/api' -import {Link} from '../Link' -import {ImageLayoutGrid} from '../images/ImageLayoutGrid' -import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' + +import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' import {usePalette} from 'lib/hooks/usePalette' -import {ExternalLinkEmbed} from './ExternalLinkEmbed' -import {MaybeQuoteEmbed} from './QuoteEmbed' -import {AutoSizedImage} from '../images/AutoSizedImage' -import {ListEmbed} from './ListEmbed' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {ContentHider} from '../../../../components/moderation/ContentHider' -import {isNative} from '#/platform/detection' -import {shareUrl} from '#/lib/sharing' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {ImageLayoutGrid} from '../images/ImageLayoutGrid' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' +import {ListEmbed} from './ListEmbed' +import {MaybeQuoteEmbed} from './QuoteEmbed' type Embed = | AppBskyEmbedRecord.View @@ -49,16 +47,6 @@ export function PostEmbeds({ const pal = usePalette('default') const {openLightbox} = useLightboxControls() - const externalUri = AppBskyEmbedExternal.isView(embed) - ? embed.external.uri - : null - - const onShareExternal = useCallback(() => { - if (externalUri && isNative) { - shareUrl(externalUri) - } - }, [externalUri]) - // quote post with media // = if (AppBskyEmbedRecordWithMedia.isView(embed)) { @@ -161,18 +149,9 @@ export function PostEmbeds({ // = if (AppBskyEmbedExternal.isView(embed)) { const link = embed.external - return ( <ContentHider modui={moderation?.ui('contentMedia')}> - <Link - asAnchor - anchorNoUnderline - href={link.uri} - style={[styles.extOuter, pal.view, pal.borderDark, style]} - hoverStyle={{borderColor: pal.colors.borderLinkHover}} - onLongPress={onShareExternal}> - <ExternalLinkEmbed link={link} /> - </Link> + <ExternalLinkEmbed link={link} style={style} /> </ContentHider> ) } @@ -187,11 +166,6 @@ const styles = StyleSheet.create({ singleImage: { borderRadius: 8, }, - extOuter: { - borderWidth: 1, - borderRadius: 8, - marginTop: 4, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, |