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:
parent
0f5735b616
commit
f0706dbe9f
19 changed files with 412 additions and 132 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue