about summary refs log tree commit diff
path: root/src/view/com/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util')
-rw-r--r--src/view/com/util/post-embeds/ExternalLinkEmbed.tsx150
-rw-r--r--src/view/com/util/post-embeds/GifEmbed.tsx140
-rw-r--r--src/view/com/util/post-embeds/index.tsx52
3 files changed, 243 insertions, 99 deletions
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,