Add alt text support and rework image layout (#503)

* Add alt text support and rework image layout

* Add additional BottomSheet implementation to account for nested Composer modal

* Use mobile gallery layout on mobile web

* Missing key

* Fix lint

* Move altimage modal into the standard modal system

* Fix overflow wrapping of images

* Fixes to the alt-image modal

* Remove unnecessary switch

* Restore old imagelayoutgrid code

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ollie Hsieh 2023-04-21 14:20:06 -07:00 committed by GitHub
parent 0f5735b616
commit f0706dbe9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 412 additions and 132 deletions

View file

@ -9,29 +9,33 @@ import {
import {Image} from 'expo-image'
import {clamp} from 'lib/numbers'
import {useStores} from 'state/index'
import {Dim} from 'lib/media/manip'
import {Dimensions} from 'lib/media/types'
export const DELAY_PRESS_IN = 500
const MIN_ASPECT_RATIO = 0.33 // 1/3
const MAX_ASPECT_RATIO = 5 // 5/1
export function AutoSizedImage({
uri,
onPress,
onLongPress,
onPressIn,
style,
children = null,
}: {
interface Props {
alt?: string
uri: string
onPress?: () => void
onLongPress?: () => void
onPressIn?: () => void
style?: StyleProp<ViewStyle>
children?: React.ReactNode
}) {
}
export function AutoSizedImage({
alt,
uri,
onPress,
onLongPress,
onPressIn,
style,
children = null,
}: Props) {
const store = useStores()
const [dim, setDim] = React.useState<Dim | undefined>(
const [dim, setDim] = React.useState<Dimensions | undefined>(
store.imageSizes.get(uri),
)
const [aspectRatio, setAspectRatio] = React.useState<number>(
@ -59,20 +63,31 @@ export function AutoSizedImage({
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
style={[styles.container, style]}>
<Image style={[styles.image, {aspectRatio}]} source={uri} />
<Image
style={[styles.image, {aspectRatio}]}
source={uri}
accessible={true} // Must set for `accessibilityLabel` to work
accessibilityLabel={alt}
/>
{children}
</TouchableOpacity>
)
}
return (
<View style={[styles.container, style]}>
<Image style={[styles.image, {aspectRatio}]} source={{uri}} />
<Image
style={[styles.image, {aspectRatio}]}
source={{uri}}
accessible={true} // Must set for `accessibilityLabel` to work
accessibilityLabel={alt}
/>
{children}
</View>
)
}
function calc(dim: Dim) {
function calc(dim: Dimensions) {
if (dim.width === 0 || dim.height === 0) {
return 1
}

View file

@ -7,21 +7,25 @@ import {
ViewStyle,
} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api'
export function ImageHorzList({
uris,
onPress,
style,
}: {
uris: string[]
interface Props {
images: AppBskyEmbedImages.ViewImage[]
onPress?: (index: number) => void
style?: StyleProp<ViewStyle>
}) {
}
export function ImageHorzList({images, onPress, style}: Props) {
return (
<View style={[styles.flexRow, style]}>
{uris.map((uri, i) => (
{images.map(({thumb, alt}, i) => (
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
<Image source={{uri}} style={styles.image} />
<Image
source={{uri: thumb}}
style={styles.image}
accessible={true}
accessibilityLabel={alt}
/>
</TouchableWithoutFeedback>
))}
</View>

View file

@ -9,26 +9,25 @@ import {
} from 'react-native'
import {Image, ImageStyle} from 'expo-image'
import {Dimensions} from 'lib/media/types'
import {AppBskyEmbedImages} from '@atproto/api'
export const DELAY_PRESS_IN = 500
export type ImageLayoutGridType = number
export function ImageLayoutGrid({
type,
uris,
onPress,
onLongPress,
onPressIn,
style,
}: {
type: ImageLayoutGridType
uris: string[]
interface ImageLayoutGridProps {
images: AppBskyEmbedImages.ViewImage[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
style?: StyleProp<ViewStyle>
}) {
}
export function ImageLayoutGrid({
images,
onPress,
onLongPress,
onPressIn,
style,
}: ImageLayoutGridProps) {
const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
const onLayout = (evt: LayoutChangeEvent) => {
@ -42,8 +41,7 @@ export function ImageLayoutGrid({
<View style={style} onLayout={onLayout}>
{containerInfo ? (
<ImageLayoutGridInner
type={type}
uris={uris}
images={images}
onPress={onPress}
onPressIn={onPressIn}
onLongPress={onLongPress}
@ -54,41 +52,42 @@ export function ImageLayoutGrid({
)
}
function ImageLayoutGridInner({
type,
uris,
onPress,
onLongPress,
onPressIn,
containerInfo,
}: {
type: ImageLayoutGridType
uris: string[]
interface ImageLayoutGridInnerProps {
images: AppBskyEmbedImages.ViewImage[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
containerInfo: Dimensions
}) {
}
function ImageLayoutGridInner({
images,
onPress,
onLongPress,
onPressIn,
containerInfo,
}: ImageLayoutGridInnerProps) {
const count = images.length
const size1 = useMemo<ImageStyle>(() => {
if (type === 3) {
if (count === 3) {
const size = (containerInfo.width - 10) / 3
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [type, containerInfo])
}, [count, containerInfo])
const size2 = React.useMemo<ImageStyle>(() => {
if (type === 3) {
if (count === 3) {
const size = ((containerInfo.width - 10) / 3) * 2 + 5
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [type, containerInfo])
}, [count, containerInfo])
if (type === 2) {
if (count === 2) {
return (
<View style={styles.flexRow}>
<TouchableOpacity
@ -96,7 +95,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size1} />
<Image
source={{uri: images[0].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[0].alt}
/>
</TouchableOpacity>
<View style={styles.wSpace} />
<TouchableOpacity
@ -104,12 +108,17 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<Image
source={{uri: images[1].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[1].alt}
/>
</TouchableOpacity>
</View>
)
}
if (type === 3) {
if (count === 3) {
return (
<View style={styles.flexRow}>
<TouchableOpacity
@ -117,7 +126,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size2} />
<Image
source={{uri: images[0].thumb}}
style={size2}
accessible={true}
accessibilityLabel={images[0].alt}
/>
</TouchableOpacity>
<View style={styles.wSpace} />
<View>
@ -126,7 +140,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<Image
source={{uri: images[1].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[1].alt}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@ -134,13 +153,18 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} />
<Image
source={{uri: images[2].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[2].alt}
/>
</TouchableOpacity>
</View>
</View>
)
}
if (type === 4) {
if (count === 4) {
return (
<View style={styles.flexRow}>
<View>
@ -149,7 +173,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress?.(0)}>
<Image source={{uri: uris[0]}} style={size1} />
<Image
source={{uri: images[0].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[0].alt}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@ -157,7 +186,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress?.(2)}>
<Image source={{uri: uris[2]}} style={size1} />
<Image
source={{uri: images[2].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[2].alt}
/>
</TouchableOpacity>
</View>
<View style={styles.wSpace} />
@ -167,7 +201,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress?.(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<Image
source={{uri: images[1].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[1].alt}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@ -175,7 +214,12 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(3)}
onPressIn={() => onPressIn?.(3)}
onLongPress={() => onLongPress?.(3)}>
<Image source={{uri: uris[3]}} style={size1} />
<Image
source={{uri: images[3].thumb}}
style={size1}
accessible={true}
accessibilityLabel={images[3].alt}
/>
</TouchableOpacity>
</View>
</View>

View file

@ -112,6 +112,7 @@ export function PostEmbeds({
return (
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
alt={embed.images[0].alt}
uri={embed.images[0].thumb}
onPress={() => openLightbox(0)}
onLongPress={() => onLongPress(0)}
@ -124,8 +125,7 @@ export function PostEmbeds({
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
type={embed.images.length}
uris={embed.images.map(img => img.thumb)}
images={embed.images}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}