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/PostEmbeds.tsx180
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx112
2 files changed, 243 insertions, 49 deletions
diff --git a/src/view/com/util/PostEmbeds.tsx b/src/view/com/util/PostEmbeds.tsx
index 1591c658a..ea15dc9ca 100644
--- a/src/view/com/util/PostEmbeds.tsx
+++ b/src/view/com/util/PostEmbeds.tsx
@@ -1,88 +1,170 @@
 import React, {useEffect, useState} from 'react'
 import {
   ActivityIndicator,
+  Image,
+  ImageStyle,
   StyleSheet,
   StyleProp,
   Text,
+  TouchableWithoutFeedback,
   View,
   ViewStyle,
 } from 'react-native'
-import {Entity} from '../../../third-party/api/src/client/types/app/bsky/feed/post'
+import {
+  Record as PostRecord,
+  Entity,
+} from '../../../third-party/api/src/client/types/app/bsky/feed/post'
+import * as AppBskyEmbedImages from '../../../third-party/api/src/client/types/app/bsky/embed/images'
+import * as AppBskyEmbedExternal from '../../../third-party/api/src/client/types/app/bsky/embed/external'
 import {Link} from '../util/Link'
 import {LinkMeta, getLikelyType, LikelyType} from '../../../lib/link-meta'
 import {colors} from '../../lib/styles'
-import {useStores} from '../../../state'
+import {AutoSizedImage} from './images/AutoSizedImage'
+
+type Embed =
+  | AppBskyEmbedImages.Presented
+  | AppBskyEmbedExternal.Presented
+  | {$type: string; [k: string]: unknown}
 
 export function PostEmbeds({
-  entities,
+  embed,
   style,
 }: {
-  entities?: Entity[]
+  embed?: Embed
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
-  const [linkMeta, setLinkMeta] = useState<LinkMeta | undefined>(undefined)
-  const link = entities?.find(
-    ent =>
-      ent.type === 'link' && getLikelyType(ent.value || '') === LikelyType.HTML,
-  )
-
-  useEffect(() => {
-    let aborted = false
-    store.linkMetas.getLinkMeta(link?.value || '').then(linkMeta => {
-      if (!aborted) {
-        setLinkMeta(linkMeta)
+  if (embed?.$type === 'app.bsky.embed.images#presented') {
+    const imgEmbed = embed as AppBskyEmbedImages.Presented
+    if (imgEmbed.images.length > 0) {
+      const Thumb = ({i, style}: {i: number; style: StyleProp<ImageStyle>}) => (
+        <AutoSizedImage
+          style={style}
+          uri={imgEmbed.images[i].thumb}
+          fullSizeUri={imgEmbed.images[i].fullsize}
+        />
+      )
+      if (imgEmbed.images.length === 4) {
+        return (
+          <View style={styles.imagesContainer}>
+            <View style={styles.imagePair}>
+              <Thumb i={0} style={styles.imagePairItem} />
+              <View style={styles.imagesWidthSpacer} />
+              <Thumb i={1} style={styles.imagePairItem} />
+            </View>
+            <View style={styles.imagesHeightSpacer} />
+            <View style={styles.imagePair}>
+              <Thumb i={2} style={styles.imagePairItem} />
+              <View style={styles.imagesWidthSpacer} />
+              <Thumb i={3} style={styles.imagePairItem} />
+            </View>
+          </View>
+        )
+      } else if (imgEmbed.images.length === 3) {
+        return (
+          <View style={styles.imagesContainer}>
+            <View style={styles.imageWide}>
+              <Thumb i={0} style={styles.imageWideItem} />
+            </View>
+            <View style={styles.imagesHeightSpacer} />
+            <View style={styles.imagePair}>
+              <Thumb i={1} style={styles.imagePairItem} />
+              <View style={styles.imagesWidthSpacer} />
+              <Thumb i={2} style={styles.imagePairItem} />
+            </View>
+          </View>
+        )
+      } else if (imgEmbed.images.length === 2) {
+        return (
+          <View style={styles.imagesContainer}>
+            <View style={styles.imagePair}>
+              <Thumb i={0} style={styles.imagePairItem} />
+              <View style={styles.imagesWidthSpacer} />
+              <Thumb i={1} style={styles.imagePairItem} />
+            </View>
+          </View>
+        )
+      } else {
+        return (
+          <View style={styles.imagesContainer}>
+            <View style={styles.imageBig}>
+              <Thumb i={0} style={styles.imageBigItem} />
+            </View>
+          </View>
+        )
       }
-    })
-
-    return () => {
-      aborted = true
     }
-  }, [link])
-
-  if (!link) {
-    return <View />
   }
-
-  return (
-    <Link style={[styles.outer, style]} href={link.value}>
-      {linkMeta ? (
-        <>
-          <Text numberOfLines={1} style={styles.title}>
-            {linkMeta.title || linkMeta.url}
-          </Text>
-          <Text numberOfLines={1} style={styles.url}>
-            {linkMeta.url}
+  if (embed?.$type === 'app.bsky.embed.external#presented') {
+    const externalEmbed = embed as AppBskyEmbedExternal.Presented
+    const link = externalEmbed.external
+    return (
+      <Link style={[styles.extOuter, style]} href={link.uri}>
+        {link.thumb ? (
+          <AutoSizedImage style={style} uri={link.thumb} />
+        ) : undefined}
+        <Text numberOfLines={1} style={styles.extTitle}>
+          {link.title || link.uri}
+        </Text>
+        <Text numberOfLines={1} style={styles.extUrl}>
+          {link.uri}
+        </Text>
+        {link.description ? (
+          <Text numberOfLines={2} style={styles.extDescription}>
+            {link.description}
           </Text>
-          {linkMeta.description ? (
-            <Text numberOfLines={2} style={styles.description}>
-              {linkMeta.description}
-            </Text>
-          ) : undefined}
-        </>
-      ) : (
-        <ActivityIndicator />
-      )}
-    </Link>
-  )
+        ) : undefined}
+      </Link>
+    )
+  }
+  return <View />
 }
 
 const styles = StyleSheet.create({
-  outer: {
+  imagesContainer: {
+    marginBottom: 20,
+  },
+  imagesWidthSpacer: {
+    width: 5,
+  },
+  imagesHeightSpacer: {
+    height: 5,
+  },
+  imagePair: {
+    flexDirection: 'row',
+  },
+  imagePairItem: {
+    resizeMode: 'contain',
+    flex: 1,
+    borderRadius: 4,
+  },
+  imageWide: {},
+  imageWideItem: {
+    resizeMode: 'contain',
+    borderRadius: 4,
+  },
+  imageBig: {},
+  imageBigItem: {
+    borderRadius: 4,
+  },
+
+  extOuter: {
     borderWidth: 1,
     borderColor: colors.gray2,
     borderRadius: 8,
     padding: 10,
   },
-  title: {
+  extImage: {
+    // TODO
+  },
+  extTitle: {
     fontSize: 16,
     fontWeight: 'bold',
   },
-  description: {
+  extDescription: {
     marginTop: 4,
     fontSize: 15,
   },
-  url: {
+  extUrl: {
     color: colors.gray4,
   },
 })
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
new file mode 100644
index 000000000..fedc94321
--- /dev/null
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -0,0 +1,112 @@
+import React, {useState, useEffect, useMemo} from 'react'
+import {
+  Image,
+  ImageStyle,
+  LayoutChangeEvent,
+  StyleProp,
+  StyleSheet,
+  Text,
+  TouchableWithoutFeedback,
+  View,
+} from 'react-native'
+import {ImageLightbox} from '../../../../state/models/shell-ui'
+import {useStores} from '../../../../state'
+import {colors} from '../../../lib/styles'
+
+const MAX_HEIGHT = 300
+
+interface Dim {
+  width: number
+  height: number
+}
+
+export function AutoSizedImage({
+  uri,
+  fullSizeUri,
+  style,
+}: {
+  uri: string
+  fullSizeUri?: string
+  style: StyleProp<ImageStyle>
+}) {
+  const store = useStores()
+  const [error, setError] = useState<string | undefined>()
+  const [imgInfo, setImgInfo] = useState<Dim | undefined>()
+  const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
+  const calculatedStyle = useMemo(() => {
+    if (imgInfo && containerInfo) {
+      // imgInfo.height / imgInfo.width = x / containerInfo.width
+      // x = imgInfo.height / imgInfo.width * containerInfo.width
+      return {
+        height: Math.min(
+          MAX_HEIGHT,
+          (imgInfo.height / imgInfo.width) * containerInfo.width,
+        ),
+      }
+    }
+    return undefined
+  }, [imgInfo, containerInfo])
+
+  useEffect(() => {
+    Image.getSize(
+      uri,
+      (width: number, height: number) => {
+        setImgInfo({width, height})
+      },
+      (error: any) => {
+        setError(String(error))
+      },
+    )
+  }, [uri])
+
+  const onLayout = (evt: LayoutChangeEvent) => {
+    setContainerInfo({
+      width: evt.nativeEvent.layout.width,
+      height: evt.nativeEvent.layout.height,
+    })
+  }
+
+  const onPressImage = () => {
+    if (fullSizeUri) {
+      store.shell.openLightbox(new ImageLightbox(fullSizeUri))
+    }
+  }
+
+  return (
+    <View style={style}>
+      <TouchableWithoutFeedback onPress={onPressImage}>
+        {error ? (
+          <View style={[styles.container, styles.errorContainer]}>
+            <Text style={styles.error}>{error}</Text>
+          </View>
+        ) : calculatedStyle ? (
+          <View style={styles.container}>
+            <Image style={calculatedStyle} source={{uri}} />
+          </View>
+        ) : (
+          <View style={[style, styles.placeholder]} onLayout={onLayout} />
+        )}
+      </TouchableWithoutFeedback>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  placeholder: {
+    width: '100%',
+    aspectRatio: 1,
+    backgroundColor: colors.gray1,
+  },
+  errorContainer: {
+    backgroundColor: colors.red1,
+    paddingHorizontal: 8,
+    paddingVertical: 4,
+  },
+  container: {
+    borderRadius: 8,
+    overflow: 'hidden',
+  },
+  error: {
+    color: colors.red5,
+  },
+})