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>
zio/stable
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

@ -64,7 +64,7 @@
"expo-build-properties": "~0.5.1",
"expo-camera": "~13.2.1",
"expo-dev-client": "~2.1.1",
"expo-image": "~1.0.0",
"expo-image": "^1.2.1",
"expo-image-picker": "~14.1.1",
"expo-localization": "~14.1.1",
"expo-media-library": "~15.2.3",

View File

@ -10,15 +10,15 @@ import {
import {AtUri} from '@atproto/api'
import {RootStoreModel} from 'state/models/root-store'
import {isNetworkError} from 'lib/strings/errors'
import {Image} from 'lib/media/types'
import {LinkMeta} from '../link-meta/link-meta'
import {isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
export interface ExternalEmbedDraft {
uri: string
isLoading: boolean
meta?: LinkMeta
localThumb?: Image
localThumb?: ImageModel
}
export async function resolveName(store: RootStoreModel, didOrHandle: string) {
@ -61,7 +61,7 @@ interface PostOpts {
cid: string
}
extLink?: ExternalEmbedDraft
images?: string[]
images?: ImageModel[]
knownHandles?: Set<string>
onStateChange?: (state: string) => void
}
@ -109,10 +109,11 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
const images: AppBskyEmbedImages.Image[] = []
for (const image of opts.images) {
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
const res = await uploadBlob(store, image, 'image/jpeg')
const path = image.compressed?.path ?? image.path
const res = await uploadBlob(store, path, 'image/jpeg')
images.push({
image: res.data.blob,
alt: '', // TODO supply alt text
alt: image.altText ?? '',
})
}

View File

@ -4,6 +4,10 @@ export const FEEDBACK_FORM_URL =
export const MAX_DISPLAY_NAME = 64
export const MAX_DESCRIPTION = 256
// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
// but adding buffer room to account for languages like German
export const MAX_ALT_TEXT = 120
export const PROD_TEAM_HANDLES = [
'jay.bsky.social',
'pfrazee.com',

View File

@ -0,0 +1,16 @@
import {RootStoreModel} from 'state/index'
export async function openAltTextModal(store: RootStoreModel): Promise<string> {
return new Promise((resolve, reject) => {
store.shell.openModal({
name: 'alt-text-image',
onAltTextSet: (altText?: string) => {
if (altText) {
resolve(altText)
} else {
reject(new Error('Canceled'))
}
},
})
})
}

View File

@ -65,6 +65,10 @@ export class GalleryModel {
})
}
setAltText(image: ImageModel) {
image.setAltText()
}
crop(image: ImageModel) {
image.crop()
}

View File

@ -5,6 +5,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {openCropper} from 'lib/media/picker'
import {POST_IMG_MAX} from 'lib/constants'
import {scaleDownDimensions} from 'lib/media/util'
import {openAltTextModal} from 'lib/media/alt-text'
// TODO: EXIF embed
// Cases to consider: ExternalEmbed
@ -14,6 +15,7 @@ export class ImageModel implements RNImage {
width: number
height: number
size: number
altText?: string = undefined
cropped?: RNImage = undefined
compressed?: RNImage = undefined
scaledWidth: number = POST_IMG_MAX.width
@ -41,6 +43,18 @@ export class ImageModel implements RNImage {
this.scaledHeight = height
}
async setAltText() {
try {
const altText = await openAltTextModal(this.rootStore)
runInAction(() => {
this.altText = altText
})
} catch (err) {
this.rootStore.log.error('Failed to set alt text', err)
}
}
async crop() {
try {
const cropped = await openCropper(this.rootStore, {

View File

@ -3,7 +3,7 @@ import {RootStoreModel} from '../root-store'
import {makeAutoObservable} from 'mobx'
import {ProfileModel} from '../content/profile'
import {isObj, hasProp} from 'lib/type-guards'
import {Image} from 'lib/media/types'
import {Image as RNImage} from 'react-native-image-crop-picker'
export interface ConfirmModal {
name: 'confirm'
@ -38,7 +38,12 @@ export interface ReportAccountModal {
export interface CropImageModal {
name: 'crop-image'
uri: string
onSelect: (img?: Image) => void
onSelect: (img?: RNImage) => void
}
export interface AltTextImageModal {
name: 'alt-text-image'
onAltTextSet: (altText?: string) => void
}
export interface DeleteAccountModal {
@ -70,18 +75,30 @@ export interface ContentFilteringSettingsModal {
}
export type Modal =
| ConfirmModal
| EditProfileModal
| ServerInputModal
| ReportPostModal
| ReportAccountModal
| CropImageModal
| DeleteAccountModal
| RepostModal
// Account
| ChangeHandleModal
| DeleteAccountModal
| EditProfileModal
// Curation
| ContentFilteringSettingsModal
// Reporting
| ReportAccountModal
| ReportPostModal
// Posting
| AltTextImageModal
| CropImageModal
| ServerInputModal
| RepostModal
// Bluesky access
| WaitlistModal
| InviteCodesModal
| ContentFilteringSettingsModal
// Generic
| ConfirmModal
interface LightboxModel {}

View File

@ -142,7 +142,7 @@ export const ComposePost = observer(function ComposePost({
await apilib.post(store, {
rawText: rt.text,
replyTo: replyTo?.uri,
images: gallery.paths,
images: gallery.images,
quote: quote,
extLink: extLink,
onStateChange: setProcessingState,

View File

@ -1,4 +1,5 @@
import React, {useCallback} from 'react'
import {ImageStyle, Keyboard} from 'react-native'
import {GalleryModel} from 'state/models/media/gallery'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@ -6,6 +7,8 @@ import {colors} from 'lib/styles'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {ImageModel} from 'state/models/media/image'
import {Image} from 'expo-image'
import {Text} from 'view/com/util/text/Text'
import {isDesktopWeb} from 'platform/detection'
interface Props {
gallery: GalleryModel
@ -13,17 +16,28 @@ interface Props {
export const Gallery = observer(function ({gallery}: Props) {
const getImageStyle = useCallback(() => {
switch (gallery.size) {
case 1:
return styles.image250
case 2:
return styles.image175
default:
return styles.image85
let side: number
if (gallery.size === 1) {
side = 250
} else {
side = (isDesktopWeb ? 560 : 350) / gallery.size
}
return {
height: side,
width: side,
}
}, [gallery])
const imageStyle = getImageStyle()
const handleAddImageAltText = useCallback(
(image: ImageModel) => {
Keyboard.dismiss()
gallery.setAltText(image)
},
[gallery],
)
const handleRemovePhoto = useCallback(
(image: ImageModel) => {
gallery.remove(image)
@ -38,14 +52,68 @@ export const Gallery = observer(function ({gallery}: Props) {
[gallery],
)
const isOverflow = !isDesktopWeb && gallery.size > 2
const imageControlLabelStyle = {
borderRadius: 5,
paddingHorizontal: 10,
position: 'absolute' as const,
width: 46,
zIndex: 1,
...(isOverflow
? {
left: 4,
bottom: 4,
}
: isDesktopWeb && gallery.size < 3
? {
left: 8,
top: 8,
}
: {
left: 4,
top: 4,
}),
}
const imageControlsSubgroupStyle = {
display: 'flex' as const,
flexDirection: 'row' as const,
position: 'absolute' as const,
...(isOverflow
? {
top: 4,
right: 4,
gap: 4,
}
: isDesktopWeb && gallery.size < 3
? {
top: 8,
right: 8,
gap: 8,
}
: {
top: 4,
right: 4,
gap: 4,
}),
zIndex: 1,
}
return !gallery.isEmpty ? (
<View testID="selectedPhotosView" style={styles.gallery}>
{gallery.images.map(image =>
image.compressed !== undefined ? (
<View
key={`selected-image-${image.path}`}
style={[styles.imageContainer, imageStyle]}>
<View style={styles.imageControls}>
<View key={`selected-image-${image.path}`} style={[imageStyle]}>
<TouchableOpacity
testID="altTextButton"
onPress={() => {
handleAddImageAltText(image)
}}
style={[styles.imageControl, imageControlLabelStyle]}>
<Text style={styles.imageControlTextContent}>ALT</Text>
</TouchableOpacity>
<View style={imageControlsSubgroupStyle}>
<TouchableOpacity
testID="cropPhotoButton"
onPress={() => {
@ -72,7 +140,7 @@ export const Gallery = observer(function ({gallery}: Props) {
<Image
testID="selectedPhotoImage"
style={[styles.image, imageStyle]}
style={[styles.image, imageStyle] as ImageStyle}
source={{
uri: image.compressed.path,
}}
@ -88,36 +156,13 @@ const styles = StyleSheet.create({
gallery: {
flex: 1,
flexDirection: 'row',
gap: 8,
marginTop: 16,
},
imageContainer: {
margin: 2,
},
image: {
resizeMode: 'cover',
borderRadius: 8,
},
image250: {
width: 250,
height: 250,
},
image175: {
width: 175,
height: 175,
},
image85: {
width: 85,
height: 85,
},
imageControls: {
position: 'absolute',
display: 'flex',
flexDirection: 'row',
gap: 4,
top: 8,
right: 8,
zIndex: 1,
},
imageControl: {
width: 24,
height: 24,
@ -127,4 +172,10 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
imageControlTextContent: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
letterSpacing: 1,
},
})

View File

@ -0,0 +1,106 @@
import React, {useCallback, useState} from 'react'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from './util'
import {gradients, s} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers'
import {MAX_ALT_TEXT} from 'lib/constants'
import {useTheme} from 'lib/ThemeContext'
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 = [330]
interface Props {
onAltTextSet: (altText?: string | undefined) => void
}
export function Component({onAltTextSet}: Props) {
const pal = usePalette('default')
const store = useStores()
const theme = useTheme()
const [altText, setAltText] = useState('')
const onPressSave = useCallback(() => {
onAltTextSet(altText)
store.shell.closeModal()
}, [store, altText, onAltTextSet])
const onPressCancel = () => {
store.shell.closeModal()
}
return (
<View testID="altTextImageModal" style={[pal.view, styles.container]}>
<Text style={[styles.title, pal.text]}>Add alt text</Text>
<TextInput
testID="altTextImageInput"
style={[styles.textArea, pal.border, pal.text]}
keyboardAppearance={theme.colorScheme}
multiline
value={altText}
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
/>
<View style={styles.buttonControls}>
<TouchableOpacity testID="altTextImageSaveBtn" onPress={onPressSave}>
<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]}>
Save
</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
testID="altTextImageCancelBtn"
onPress={onPressCancel}>
<View style={[styles.button]}>
<Text type="button-lg" style={[pal.textLight]}>
Cancel
</Text>
</View>
</TouchableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
gap: 18,
bottom: 0,
paddingVertical: 18,
paddingHorizontal: isDesktopWeb ? 0 : 12,
width: '100%',
},
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
},
textArea: {
borderWidth: 1,
borderRadius: 6,
paddingTop: 10,
paddingHorizontal: 12,
fontSize: 16,
height: 100,
textAlignVertical: 'top',
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 10,
},
buttonControls: {
gap: 8,
},
})

View File

@ -1,5 +1,5 @@
import React, {useRef, useEffect} from 'react'
import {StyleSheet, View} from 'react-native'
import {StyleSheet} from 'react-native'
import {observer} from 'mobx-react-lite'
import BottomSheet from '@gorhom/bottom-sheet'
import {useStores} from 'state/index'
@ -11,6 +11,7 @@ import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
import * as ReportPostModal from './ReportPost'
import * as RepostModal from './Repost'
import * as AltImageModal from './AltImage'
import * as ReportAccountModal from './ReportAccount'
import * as DeleteAccountModal from './DeleteAccount'
import * as ChangeHandleModal from './ChangeHandle'
@ -68,6 +69,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'repost') {
snapPoints = RepostModal.snapPoints
element = <RepostModal.Component {...activeModal} />
} else if (activeModal?.name === 'alt-text-image') {
snapPoints = AltImageModal.snapPoints
element = <AltImageModal.Component {...activeModal} />
} else if (activeModal?.name === 'change-handle') {
snapPoints = ChangeHandleModal.snapPoints
element = <ChangeHandleModal.Component {...activeModal} />
@ -81,7 +85,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
snapPoints = ContentFilteringSettingsModal.snapPoints
element = <ContentFilteringSettingsModal.Component />
} else {
return <View />
return null
}
return (

View File

@ -14,6 +14,7 @@ import * as ReportAccountModal from './ReportAccount'
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 ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
@ -78,6 +79,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <InviteCodesModal.Component />
} else if (modal.name === 'content-filtering-settings') {
element = <ContentFilteringSettingsModal.Component />
} else if (modal.name === 'alt-text-image') {
element = <AltTextImageModal.Component {...modal} />
} else {
return null
}

View File

@ -369,10 +369,7 @@ function AdditionalPostText({
<>
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
{images && images?.length > 0 && (
<ImageHorzList
uris={images?.map(img => img.thumb)}
style={styles.additionalPostImages}
/>
<ImageHorzList images={images} style={styles.additionalPostImages} />
)}
</>
)

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}

View File

@ -54,7 +54,6 @@ const ShellInner = observer(() => {
</Drawer>
</ErrorBoundary>
</View>
<ModalsContainer />
<Lightbox />
<Composer
active={store.shell.isComposerActive}
@ -64,6 +63,7 @@ const ShellInner = observer(() => {
onPost={store.shell.composerOpts?.onPost}
quote={store.shell.composerOpts?.quote}
/>
<ModalsContainer />
</>
)
})

View File

@ -8364,10 +8364,10 @@ expo-image-picker@~14.1.1:
dependencies:
expo-image-loader "~4.1.0"
expo-image@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.0.0.tgz#a3670d20815d99e2527307a33761c9b0088823b1"
integrity sha512-A1amVExKhBa/eRXuceauYtPkf9izeje5AbxEWL09tgK91rf3GSIZXM5PSDGlIM0s7dpCV+Iet2jhwcFUfWaZrw==
expo-image@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.2.1.tgz#3f377cb3142de2107903f4e4f88a7f44785dee18"
integrity sha512-pYZFN0ctuIBA+sqUiw70rHQQ04WDyEcF549ObArdj0MNgSUCBJMFmu/jrWDmxOpEMF40lfLVIZKigJT7Bw+GYA==
expo-json-utils@~0.5.0:
version "0.5.1"