about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-09-04 18:52:41 +0100
committerGitHub <noreply@github.com>2024-09-04 10:52:41 -0700
commit5f5c14d044042e25b78bd173d245d5790843ff85 (patch)
tree76b4dabc0ef11172e07e71865643de0b94982a06 /src
parent82ca0b16b6385f727e1d305dc48256bc45a0086d (diff)
downloadvoidsky-5f5c14d044042e25b78bd173d245d5790843ff85.tar.zst
Replace `ImageHorzList` 🤮 with `MediaPreview` ✨ (#5143)
Diffstat (limited to 'src')
-rw-r--r--src/components/MediaPreview.tsx169
-rw-r--r--src/screens/Messages/Conversation/MessageInputEmbed.tsx15
-rw-r--r--src/view/com/notifications/FeedItem.tsx49
-rw-r--r--src/view/com/util/images/ImageHorzList.tsx61
4 files changed, 176 insertions, 118 deletions
diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx
new file mode 100644
index 000000000..17bae55b5
--- /dev/null
+++ b/src/components/MediaPreview.tsx
@@ -0,0 +1,169 @@
+import React from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {Image} from 'expo-image'
+import {
+  AppBskyEmbedExternal,
+  AppBskyEmbedImages,
+  AppBskyEmbedRecordWithMedia,
+  AppBskyEmbedVideo,
+} from '@atproto/api'
+import {Trans} from '@lingui/macro'
+
+import {parseTenorGif} from '#/lib/strings/embed-player'
+import {atoms as a} from '#/alf'
+import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
+import {Text} from '#/components/Typography'
+
+/**
+ * Streamlined MediaPreview component which just handles images, gifs, and videos
+ */
+export function Embed({
+  embed,
+  style,
+}: {
+  embed?:
+    | AppBskyEmbedImages.View
+    | AppBskyEmbedRecordWithMedia.View
+    | AppBskyEmbedExternal.View
+    | AppBskyEmbedVideo.View
+    | {[k: string]: unknown}
+  style?: StyleProp<ViewStyle>
+}) {
+  let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
+
+  if (AppBskyEmbedImages.isView(media)) {
+    return (
+      <Outer style={style}>
+        {media.images.map(image => (
+          <ImageItem
+            key={image.thumb}
+            thumbnail={image.thumb}
+            alt={image.alt}
+          />
+        ))}
+      </Outer>
+    )
+  } else if (AppBskyEmbedExternal.isView(embed) && embed.external.thumb) {
+    let url: URL | undefined
+    try {
+      url = new URL(embed.external.uri)
+    } catch {}
+    if (url) {
+      const {success} = parseTenorGif(url)
+      if (success) {
+        return (
+          <Outer style={style}>
+            <GifItem
+              thumbnail={embed.external.thumb}
+              alt={embed.external.title}
+            />
+          </Outer>
+        )
+      }
+    }
+  } else if (AppBskyEmbedVideo.isView(embed)) {
+    return (
+      <Outer style={style}>
+        <VideoItem thumbnail={embed.thumbnail} alt={embed.alt} />
+      </Outer>
+    )
+  }
+
+  return null
+}
+
+export function Outer({
+  children,
+  style,
+}: {
+  children?: React.ReactNode
+  style?: StyleProp<ViewStyle>
+}) {
+  return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View>
+}
+
+export function ImageItem({
+  thumbnail,
+  alt,
+  children,
+}: {
+  thumbnail: string
+  alt?: string
+  children?: React.ReactNode
+}) {
+  return (
+    <View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
+      <Image
+        key={thumbnail}
+        source={{uri: thumbnail}}
+        style={[a.flex_1, a.rounded_xs]}
+        contentFit="cover"
+        accessible={true}
+        accessibilityIgnoresInvertColors
+        accessibilityHint={alt}
+        accessibilityLabel=""
+      />
+      {children}
+    </View>
+  )
+}
+
+export function GifItem({thumbnail, alt}: {thumbnail: string; alt?: string}) {
+  return (
+    <ImageItem thumbnail={thumbnail} alt={alt}>
+      <View style={styles.altContainer}>
+        <Text style={styles.alt}>
+          <Trans>GIF</Trans>
+        </Text>
+      </View>
+    </ImageItem>
+  )
+}
+
+export function VideoItem({
+  thumbnail,
+  alt,
+}: {
+  thumbnail?: string
+  alt?: string
+}) {
+  if (!thumbnail) {
+    return (
+      <View
+        style={[
+          {backgroundColor: 'black'},
+          a.flex_1,
+          {aspectRatio: 1, maxWidth: 100},
+          a.justify_center,
+          a.align_center,
+        ]}>
+        <PlayIcon size="xl" fill="white" />
+      </View>
+    )
+  }
+  return (
+    <ImageItem thumbnail={thumbnail} alt={alt}>
+      <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
+        <PlayIcon size="xl" fill="white" />
+      </View>
+    </ImageItem>
+  )
+}
+
+const styles = StyleSheet.create({
+  altContainer: {
+    backgroundColor: 'rgba(0, 0, 0, 0.75)',
+    borderRadius: 6,
+    paddingHorizontal: 6,
+    paddingVertical: 3,
+    position: 'absolute',
+    right: 5,
+    bottom: 5,
+    zIndex: 2,
+  },
+  alt: {
+    color: 'white',
+    fontSize: 7,
+    fontWeight: 'bold',
+  },
+})
diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
index 4fdd31bcf..bf28ed4fe 100644
--- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx
+++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
@@ -1,8 +1,6 @@
 import React, {useCallback, useEffect, useMemo, useState} from 'react'
 import {LayoutAnimation, View} from 'react-native'
 import {
-  AppBskyEmbedImages,
-  AppBskyEmbedRecordWithMedia,
   AppBskyFeedPost,
   AppBskyRichtextFacet,
   AtUri,
@@ -22,12 +20,12 @@ import {
 } from '#/lib/strings/url-helpers'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePostQuery} from '#/state/queries/post'
-import {ImageHorzList} from '#/view/com/util/images/ImageHorzList'
 import {PostMeta} from '#/view/com/util/PostMeta'
 import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import {Loader} from '#/components/Loader'
+import * as MediaPreview from '#/components/MediaPreview'
 import {ContentHider} from '#/components/moderation/ContentHider'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {RichText} from '#/components/RichText'
@@ -160,13 +158,6 @@ export function MessageInputEmbed({
         return null
       }
 
-      const images = AppBskyEmbedImages.isView(post.embed)
-        ? post.embed.images
-        : AppBskyEmbedRecordWithMedia.isView(post.embed) &&
-          AppBskyEmbedImages.isView(post.embed.media)
-        ? post.embed.media.images
-        : undefined
-
       content = (
         <View
           style={[
@@ -202,9 +193,7 @@ export function MessageInputEmbed({
                 />
               </View>
             )}
-            {images && images?.length > 0 && (
-              <ImageHorzList images={images} style={a.mt_xs} />
-            )}
+            <MediaPreview.Embed embed={post.embed} style={a.mt_sm} />
           </ContentHider>
         </View>
       )
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 3e8f8d86d..b1cf3b485 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -8,9 +8,6 @@ import {
 } from 'react-native'
 import {
   AppBskyActorDefs,
-  AppBskyEmbedExternal,
-  AppBskyEmbedImages,
-  AppBskyEmbedRecordWithMedia,
   AppBskyFeedDefs,
   AppBskyFeedPost,
   AppBskyGraphFollow,
@@ -25,7 +22,6 @@ import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 
-import {parseTenorGif} from '#/lib/strings/embed-player'
 import {logger} from '#/logger'
 import {FeedNotification} from '#/state/queries/notifications/feed'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
@@ -52,11 +48,11 @@ import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/com
 import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
 import {StarterPack} from '#/components/icons/StarterPack'
 import {Link as NewLink} from '#/components/Link'
+import * as MediaPreview from '#/components/MediaPreview'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
 import {FeedSourceCard} from '../feeds/FeedSourceCard'
 import {Post} from '../post/Post'
-import {ImageHorzList} from '../util/images/ImageHorzList'
 import {Link, TextLink} from '../util/Link'
 import {formatCount} from '../util/numeric/format'
 import {Text} from '../util/text/Text'
@@ -593,49 +589,14 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
   const pal = usePalette('default')
   if (post && AppBskyFeedPost.isRecord(post?.record)) {
     const text = post.record.text
-    let images
-    let isGif = false
-
-    if (AppBskyEmbedImages.isView(post.embed)) {
-      images = post.embed.images
-    } else if (
-      AppBskyEmbedRecordWithMedia.isView(post.embed) &&
-      AppBskyEmbedImages.isView(post.embed.media)
-    ) {
-      images = post.embed.media.images
-    } else if (
-      AppBskyEmbedExternal.isView(post.embed) &&
-      post.embed.external.thumb
-    ) {
-      let url: URL | undefined
-      try {
-        url = new URL(post.embed.external.uri)
-      } catch {}
-      if (url) {
-        const {success} = parseTenorGif(url)
-        if (success) {
-          isGif = true
-          images = [
-            {
-              thumb: post.embed.external.thumb,
-              alt: post.embed.external.title,
-              fullsize: post.embed.external.thumb,
-            },
-          ]
-        }
-      }
-    }
 
     return (
       <>
         {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
-        {images && images.length > 0 && (
-          <ImageHorzList
-            images={images}
-            style={styles.additionalPostImages}
-            gif={isGif}
-          />
-        )}
+        <MediaPreview.Embed
+          embed={post.embed}
+          style={styles.additionalPostImages}
+        />
       </>
     )
   }
diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx
deleted file mode 100644
index bade2a444..000000000
--- a/src/view/com/util/images/ImageHorzList.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {Image} from 'expo-image'
-import {AppBskyEmbedImages} from '@atproto/api'
-import {Trans} from '@lingui/macro'
-
-import {atoms as a} from '#/alf'
-import {Text} from '#/components/Typography'
-
-interface Props {
-  images: AppBskyEmbedImages.ViewImage[]
-  style?: StyleProp<ViewStyle>
-  gif?: boolean
-}
-
-export function ImageHorzList({images, style, gif}: Props) {
-  return (
-    <View style={[a.flex_row, a.gap_xs, style]}>
-      {images.map(({thumb, alt}) => (
-        <View
-          key={thumb}
-          style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
-          <Image
-            key={thumb}
-            source={{uri: thumb}}
-            style={[a.flex_1, a.rounded_xs]}
-            accessible={true}
-            accessibilityIgnoresInvertColors
-            accessibilityHint={alt}
-            accessibilityLabel=""
-          />
-          {gif && (
-            <View style={styles.altContainer}>
-              <Text style={styles.alt}>
-                <Trans>GIF</Trans>
-              </Text>
-            </View>
-          )}
-        </View>
-      ))}
-    </View>
-  )
-}
-
-const styles = StyleSheet.create({
-  altContainer: {
-    backgroundColor: 'rgba(0, 0, 0, 0.75)',
-    borderRadius: 6,
-    paddingHorizontal: 6,
-    paddingVertical: 3,
-    position: 'absolute',
-    right: 5,
-    bottom: 5,
-    zIndex: 2,
-  },
-  alt: {
-    color: 'white',
-    fontSize: 7,
-    fontWeight: 'bold',
-  },
-})