about summary refs log tree commit diff
diff options
context:
space:
mode:
authorOllie H <renahlee@outlook.com>2023-05-01 11:59:17 -0700
committerGitHub <noreply@github.com>2023-05-01 13:59:17 -0500
commitdbb3c5c15524c517291356a4918d043348906aad (patch)
tree8b7a38c2d5c56c34b43dcbddccf640e8c9a52ad3
parent0ec98c77ef65ff74e83b314d8eed9ef9b07d47d3 (diff)
downloadvoidsky-dbb3c5c15524c517291356a4918d043348906aad.tar.zst
Image alt text view modal (#551)
* Image alt text view modal

* Minor style tweaks

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
-rw-r--r--src/state/models/ui/shell.ts8
-rw-r--r--src/view/com/modals/AltImageRead.tsx75
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/util/images/Gallery.tsx76
-rw-r--r--src/view/com/util/images/ImageLayoutGrid.tsx204
-rw-r--r--src/view/com/util/post-embeds/index.tsx94
7 files changed, 272 insertions, 192 deletions
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 797d53f81..98e98ef8e 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -47,6 +47,11 @@ export interface AltTextImageModal {
   onAltTextSet: (altText?: string) => void
 }
 
+export interface AltTextImageReadModal {
+  name: 'alt-text-image-read'
+  altText: string
+}
+
 export interface DeleteAccountModal {
   name: 'delete-account'
 }
@@ -93,8 +98,9 @@ export type Modal =
   | ReportAccountModal
   | ReportPostModal
 
-  // Posting
+  // Posts
   | AltTextImageModal
+  | AltTextImageReadModal
   | CropImageModal
   | ServerInputModal
   | RepostModal
diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx
new file mode 100644
index 000000000..e7b4797ee
--- /dev/null
+++ b/src/view/com/modals/AltImageRead.tsx
@@ -0,0 +1,75 @@
+import React, {useCallback} from 'react'
+import {StyleSheet, View} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {gradients, s} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {TouchableOpacity} from 'react-native-gesture-handler'
+import LinearGradient from 'react-native-linear-gradient'
+import {useStores} from 'state/index'
+import {isDesktopWeb} from 'platform/detection'
+
+export const snapPoints = ['70%']
+
+interface Props {
+  altText: string
+}
+
+export function Component({altText}: Props) {
+  const pal = usePalette('default')
+  const store = useStores()
+
+  const onPress = useCallback(() => {
+    store.shell.closeModal()
+  }, [store])
+
+  return (
+    <View
+      testID="altTextImageModal"
+      style={[pal.view, styles.container, s.flex1]}>
+      <Text style={[styles.title, pal.text]}>Image description</Text>
+      <View style={[styles.text, pal.viewLight]}>
+        <Text style={pal.text}>{altText}</Text>
+      </View>
+      <TouchableOpacity testID="altTextImageSaveBtn" onPress={onPress}>
+        <LinearGradient
+          colors={[gradients.blueLight.start, gradients.blueLight.end]}
+          start={{x: 0, y: 0}}
+          end={{x: 1, y: 1}}
+          style={[styles.button]}>
+          <Text type="button-lg" style={[s.white, s.bold]}>
+            Done
+          </Text>
+        </LinearGradient>
+      </TouchableOpacity>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    gap: 18,
+    paddingVertical: isDesktopWeb ? 0 : 18,
+    paddingHorizontal: isDesktopWeb ? 0 : 12,
+    height: '100%',
+    width: '100%',
+  },
+  title: {
+    textAlign: 'center',
+    fontWeight: 'bold',
+    fontSize: 24,
+  },
+  text: {
+    borderRadius: 5,
+    marginVertical: 18,
+    paddingHorizontal: 18,
+    paddingVertical: 16,
+  },
+  button: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    width: '100%',
+    borderRadius: 32,
+    padding: 10,
+  },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index df7d7f042..2e053e3ad 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -13,6 +13,7 @@ import * as ServerInputModal from './ServerInput'
 import * as ReportPostModal from './ReportPost'
 import * as RepostModal from './Repost'
 import * as AltImageModal from './AltImage'
+import * as AltImageReadModal from './AltImageRead'
 import * as ReportAccountModal from './ReportAccount'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as ChangeHandleModal from './ChangeHandle'
@@ -74,6 +75,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
   } else if (activeModal?.name === 'alt-text-image') {
     snapPoints = AltImageModal.snapPoints
     element = <AltImageModal.Component {...activeModal} />
+  } else if (activeModal?.name === 'alt-text-image-read') {
+    snapPoints = AltImageReadModal.snapPoints
+    element = <AltImageReadModal.Component {...activeModal} />
   } else if (activeModal?.name === 'change-handle') {
     snapPoints = ChangeHandleModal.snapPoints
     element = <ChangeHandleModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 07d5168ed..de748b3a8 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -15,6 +15,7 @@ import * as DeleteAccountModal from './DeleteAccount'
 import * as RepostModal from './Repost'
 import * as CropImageModal from './crop-image/CropImage.web'
 import * as AltTextImageModal from './AltImage'
+import * as AltTextImageReadModal from './AltImageRead'
 import * as ChangeHandleModal from './ChangeHandle'
 import * as WaitlistModal from './Waitlist'
 import * as InviteCodesModal from './InviteCodes'
@@ -84,6 +85,8 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <ContentFilteringSettingsModal.Component />
   } else if (modal.name === 'alt-text-image') {
     element = <AltTextImageModal.Component {...modal} />
+  } else if (modal.name === 'alt-text-image-read') {
+    element = <AltTextImageReadModal.Component {...modal} />
   } else {
     return null
   }
diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx
new file mode 100644
index 000000000..78ced0668
--- /dev/null
+++ b/src/view/com/util/images/Gallery.tsx
@@ -0,0 +1,76 @@
+import {AppBskyEmbedImages} from '@atproto/api'
+import React, {ComponentProps, FC, useCallback} from 'react'
+import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
+import {Image} from 'expo-image'
+import {useStores} from 'state/index'
+
+type EventFunction = (index: number) => void
+
+interface GalleryItemProps {
+  images: AppBskyEmbedImages.ViewImage[]
+  index: number
+  onPress?: EventFunction
+  onLongPress?: EventFunction
+  onPressIn?: EventFunction
+  imageStyle: ComponentProps<typeof Image>['style']
+}
+
+const DELAY_PRESS_IN = 500
+
+export const GalleryItem: FC<GalleryItemProps> = ({
+  images,
+  index,
+  imageStyle,
+  onPress,
+  onPressIn,
+  onLongPress,
+}) => {
+  const image = images[index]
+  const store = useStores()
+
+  const onPressAltText = useCallback(() => {
+    store.shell.openModal({
+      name: 'alt-text-image-read',
+      altText: image.alt,
+    })
+  }, [image.alt, store.shell])
+
+  return (
+    <View>
+      <TouchableOpacity
+        delayPressIn={DELAY_PRESS_IN}
+        onPress={() => onPress?.(index)}
+        onPressIn={() => onPressIn?.(index)}
+        onLongPress={() => onLongPress?.(index)}>
+        <Image
+          source={{uri: image.thumb}}
+          style={imageStyle}
+          accessible={true}
+          accessibilityLabel={image.alt}
+        />
+      </TouchableOpacity>
+      {image.alt === '' ? null : (
+        <Pressable onPress={onPressAltText}>
+          <Text style={styles.alt}>ALT</Text>
+        </Pressable>
+      )}
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  alt: {
+    backgroundColor: 'rgba(0, 0, 0, 0.75)',
+    borderRadius: 6,
+    color: 'white',
+    fontSize: 12,
+    fontWeight: 'bold',
+    letterSpacing: 1,
+    paddingHorizontal: 10,
+    paddingVertical: 3,
+    position: 'absolute',
+    left: 10,
+    top: -26,
+    width: 46,
+  },
+})
diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx
index 51bb04fe9..4c0901304 100644
--- a/src/view/com/util/images/ImageLayoutGrid.tsx
+++ b/src/view/com/util/images/ImageLayoutGrid.tsx
@@ -3,15 +3,13 @@ import {
   LayoutChangeEvent,
   StyleProp,
   StyleSheet,
-  TouchableOpacity,
   View,
   ViewStyle,
 } from 'react-native'
-import {Image, ImageStyle} from 'expo-image'
+import {ImageStyle} from 'expo-image'
 import {Dimensions} from 'lib/media/types'
 import {AppBskyEmbedImages} from '@atproto/api'
-
-export const DELAY_PRESS_IN = 500
+import {GalleryItem} from './Gallery'
 
 interface ImageLayoutGridProps {
   images: AppBskyEmbedImages.ViewImage[]
@@ -21,32 +19,21 @@ interface ImageLayoutGridProps {
   style?: StyleProp<ViewStyle>
 }
 
-export function ImageLayoutGrid({
-  images,
-  onPress,
-  onLongPress,
-  onPressIn,
-  style,
-}: ImageLayoutGridProps) {
+export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) {
   const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
 
   const onLayout = (evt: LayoutChangeEvent) => {
+    const {width, height} = evt.nativeEvent.layout
     setContainerInfo({
-      width: evt.nativeEvent.layout.width,
-      height: evt.nativeEvent.layout.height,
+      width,
+      height,
     })
   }
 
   return (
     <View style={style} onLayout={onLayout}>
       {containerInfo ? (
-        <ImageLayoutGridInner
-          images={images}
-          onPress={onPress}
-          onPressIn={onPressIn}
-          onLongPress={onLongPress}
-          containerInfo={containerInfo}
-        />
+        <ImageLayoutGridInner {...props} containerInfo={containerInfo} />
       ) : undefined}
     </View>
   )
@@ -61,13 +48,10 @@ interface ImageLayoutGridInnerProps {
 }
 
 function ImageLayoutGridInner({
-  images,
-  onPress,
-  onLongPress,
-  onPressIn,
   containerInfo,
+  ...props
 }: ImageLayoutGridInnerProps) {
-  const count = images.length
+  const count = props.images.length
   const size1 = useMemo<ImageStyle>(() => {
     if (count === 3) {
       const size = (containerInfo.width - 10) / 3
@@ -87,149 +71,43 @@ function ImageLayoutGridInner({
     }
   }, [count, containerInfo])
 
-  if (count === 2) {
-    return (
-      <View style={styles.flexRow}>
-        <TouchableOpacity
-          delayPressIn={DELAY_PRESS_IN}
-          onPress={() => onPress?.(0)}
-          onPressIn={() => onPressIn?.(0)}
-          onLongPress={() => onLongPress?.(0)}>
-          <Image
-            source={{uri: images[0].thumb}}
-            style={size1}
-            accessible={true}
-            accessibilityLabel={images[0].alt}
-          />
-        </TouchableOpacity>
-        <View style={styles.wSpace} />
-        <TouchableOpacity
-          delayPressIn={DELAY_PRESS_IN}
-          onPress={() => onPress?.(1)}
-          onPressIn={() => onPressIn?.(1)}
-          onLongPress={() => onLongPress?.(1)}>
-          <Image
-            source={{uri: images[1].thumb}}
-            style={size1}
-            accessible={true}
-            accessibilityLabel={images[1].alt}
-          />
-        </TouchableOpacity>
-      </View>
-    )
-  }
-  if (count === 3) {
-    return (
-      <View style={styles.flexRow}>
-        <TouchableOpacity
-          delayPressIn={DELAY_PRESS_IN}
-          onPress={() => onPress?.(0)}
-          onPressIn={() => onPressIn?.(0)}
-          onLongPress={() => onLongPress?.(0)}>
-          <Image
-            source={{uri: images[0].thumb}}
-            style={size2}
-            accessible={true}
-            accessibilityLabel={images[0].alt}
-          />
-        </TouchableOpacity>
-        <View style={styles.wSpace} />
-        <View>
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(1)}
-            onPressIn={() => onPressIn?.(1)}
-            onLongPress={() => onLongPress?.(1)}>
-            <Image
-              source={{uri: images[1].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[1].alt}
-            />
-          </TouchableOpacity>
-          <View style={styles.hSpace} />
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(2)}
-            onPressIn={() => onPressIn?.(2)}
-            onLongPress={() => onLongPress?.(2)}>
-            <Image
-              source={{uri: images[2].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[2].alt}
-            />
-          </TouchableOpacity>
+  switch (count) {
+    case 2:
+      return (
+        <View style={styles.flexRow}>
+          <GalleryItem index={0} {...props} imageStyle={size1} />
+          <GalleryItem index={1} {...props} imageStyle={size1} />
         </View>
-      </View>
-    )
-  }
-  if (count === 4) {
-    return (
-      <View style={styles.flexRow}>
-        <View>
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(0)}
-            onPressIn={() => onPressIn?.(0)}
-            onLongPress={() => onLongPress?.(0)}>
-            <Image
-              source={{uri: images[0].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[0].alt}
-            />
-          </TouchableOpacity>
-          <View style={styles.hSpace} />
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(2)}
-            onPressIn={() => onPressIn?.(2)}
-            onLongPress={() => onLongPress?.(2)}>
-            <Image
-              source={{uri: images[2].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[2].alt}
-            />
-          </TouchableOpacity>
+      )
+    case 3:
+      return (
+        <View style={styles.flexRow}>
+          <GalleryItem index={0} {...props} imageStyle={size2} />
+          <View style={styles.flexColumn}>
+            <GalleryItem index={1} {...props} imageStyle={size1} />
+            <GalleryItem index={2} {...props} imageStyle={size1} />
+          </View>
         </View>
-        <View style={styles.wSpace} />
-        <View>
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(1)}
-            onPressIn={() => onPressIn?.(1)}
-            onLongPress={() => onLongPress?.(1)}>
-            <Image
-              source={{uri: images[1].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[1].alt}
-            />
-          </TouchableOpacity>
-          <View style={styles.hSpace} />
-          <TouchableOpacity
-            delayPressIn={DELAY_PRESS_IN}
-            onPress={() => onPress?.(3)}
-            onPressIn={() => onPressIn?.(3)}
-            onLongPress={() => onLongPress?.(3)}>
-            <Image
-              source={{uri: images[3].thumb}}
-              style={size1}
-              accessible={true}
-              accessibilityLabel={images[3].alt}
-            />
-          </TouchableOpacity>
+      )
+    case 4:
+      return (
+        <View style={styles.flexRow}>
+          <View style={styles.flexColumn}>
+            <GalleryItem index={0} {...props} imageStyle={size1} />
+            <GalleryItem index={2} {...props} imageStyle={size1} />
+          </View>
+          <View style={styles.flexColumn}>
+            <GalleryItem index={1} {...props} imageStyle={size1} />
+            <GalleryItem index={3} {...props} imageStyle={size1} />
+          </View>
         </View>
-      </View>
-    )
+      )
+    default:
+      return null
   }
-  return <View />
 }
 
 const styles = StyleSheet.create({
-  flexRow: {flexDirection: 'row'},
-  wSpace: {width: 5},
-  hSpace: {height: 5},
+  flexRow: {flexDirection: 'row', gap: 5},
+  flexColumn: {flexDirection: 'column', gap: 5},
 })
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index f37fba342..6a7759840 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -1,10 +1,12 @@
-import React from 'react'
+import React, {useCallback} from 'react'
 import {
   StyleSheet,
   StyleProp,
   View,
   ViewStyle,
   Image as RNImage,
+  Pressable,
+  Text,
 } from 'react-native'
 import {
   AppBskyEmbedImages,
@@ -14,7 +16,6 @@ import {
   AppBskyFeedPost,
 } from '@atproto/api'
 import {Link} from '../Link'
-import {AutoSizedImage} from '../images/AutoSizedImage'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
 import {ImagesLightbox} from 'state/models/ui/shell'
 import {useStores} from 'state/index'
@@ -24,6 +25,7 @@ import {YoutubeEmbed} from './YoutubeEmbed'
 import {ExternalLinkEmbed} from './ExternalLinkEmbed'
 import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import QuoteEmbed from './QuoteEmbed'
+import {AutoSizedImage} from '../images/AutoSizedImage'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -42,6 +44,16 @@ export function PostEmbeds({
   const pal = usePalette('default')
   const store = useStores()
 
+  const onPressAltText = useCallback(
+    (alt: string) => {
+      store.shell.openModal({
+        name: 'alt-text-image-read',
+        altText: alt,
+      })
+    },
+    [store.shell],
+  )
+
   if (
     AppBskyEmbedRecordWithMedia.isView(embed) &&
     AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
@@ -88,7 +100,9 @@ export function PostEmbeds({
   }
 
   if (AppBskyEmbedImages.isView(embed)) {
-    if (embed.images.length > 0) {
+    const {images} = embed
+
+    if (images.length > 0) {
       const uris = embed.images.map(img => img.fullsize)
       const openLightbox = (index: number) => {
         store.shell.openLightbox(new ImagesLightbox(uris, index))
@@ -107,32 +121,42 @@ export function PostEmbeds({
         })
       }
 
-      switch (embed.images.length) {
-        case 1:
-          return (
-            <View style={[styles.imagesContainer, style]}>
-              <AutoSizedImage
-                alt={embed.images[0].alt}
-                uri={embed.images[0].thumb}
-                onPress={() => openLightbox(0)}
-                onLongPress={() => onLongPress(0)}
-                onPressIn={() => onPressIn(0)}
-                style={styles.singleImage}
-              />
-            </View>
-          )
-        default:
-          return (
-            <View style={[styles.imagesContainer, style]}>
-              <ImageLayoutGrid
-                images={embed.images}
-                onPress={openLightbox}
-                onLongPress={onLongPress}
-                onPressIn={onPressIn}
-              />
-            </View>
-          )
+      if (images.length === 1) {
+        const {alt, thumb} = images[0]
+        return (
+          <View style={[styles.imagesContainer, style]}>
+            <AutoSizedImage
+              alt={alt}
+              uri={thumb}
+              onPress={() => openLightbox(0)}
+              onLongPress={() => onLongPress(0)}
+              onPressIn={() => onPressIn(0)}
+              style={styles.singleImage}>
+              {alt === '' ? null : (
+                <Pressable
+                  onPress={() => {
+                    onPressAltText(alt)
+                  }}>
+                  <Text style={styles.alt}>ALT</Text>
+                </Pressable>
+              )}
+            </AutoSizedImage>
+          </View>
+        )
       }
+
+      return (
+        <View style={[styles.imagesContainer, style]}>
+          <ImageLayoutGrid
+            images={embed.images}
+            onPress={openLightbox}
+            onLongPress={onLongPress}
+            onPressIn={onPressIn}
+            style={embed.images.length === 1 ? styles.singleImage : undefined}
+          />
+        </View>
+      )
+      // }
     }
   }
 
@@ -172,4 +196,18 @@ const styles = StyleSheet.create({
     borderRadius: 8,
     marginTop: 4,
   },
+  alt: {
+    backgroundColor: 'rgba(0, 0, 0, 0.75)',
+    borderRadius: 6,
+    color: 'white',
+    fontSize: 12,
+    fontWeight: 'bold',
+    letterSpacing: 1,
+    paddingHorizontal: 10,
+    paddingVertical: 3,
+    position: 'absolute',
+    left: 10,
+    top: -26,
+    width: 46,
+  },
 })