diff options
author | Ollie Hsieh <renahlee@outlook.com> | 2023-04-21 14:20:06 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-21 16:20:06 -0500 |
commit | f0706dbe9ffb758d2aa1f75c51cfa0c61cc84482 (patch) | |
tree | 40c644c4c256154660be85c9c583028ebaaedaef /src | |
parent | 0f5735b616e3565c1c739e4c8007f4ea4aedba92 (diff) | |
download | voidsky-f0706dbe9ffb758d2aa1f75c51cfa0c61cc84482.tar.zst |
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/api/index.ts | 11 | ||||
-rw-r--r-- | src/lib/constants.ts | 4 | ||||
-rw-r--r-- | src/lib/media/alt-text.ts | 16 | ||||
-rw-r--r-- | src/state/models/media/gallery.ts | 4 | ||||
-rw-r--r-- | src/state/models/media/image.ts | 14 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 33 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 2 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 123 | ||||
-rw-r--r-- | src/view/com/modals/AltImage.tsx | 106 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/notifications/FeedItem.tsx | 5 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 41 | ||||
-rw-r--r-- | src/view/com/util/images/ImageHorzList.tsx | 22 | ||||
-rw-r--r-- | src/view/com/util/images/ImageLayoutGrid.tsx | 122 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 4 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 2 |
17 files changed, 400 insertions, 120 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 1b12f29c5..3877b3ef7 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -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 ?? '', }) } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d49d8c75c..12bdc5543 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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', diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts new file mode 100644 index 000000000..9f9f907bf --- /dev/null +++ b/src/lib/media/alt-text.ts @@ -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')) + } + }, + }) + }) +} diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts index fbe6c92a0..97b1ac1d8 100644 --- a/src/state/models/media/gallery.ts +++ b/src/state/models/media/gallery.ts @@ -65,6 +65,10 @@ export class GalleryModel { }) } + setAltText(image: ImageModel) { + image.setAltText() + } + crop(image: ImageModel) { image.crop() } diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index 584bf90cc..3585bb083 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -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, { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 47cc0aa82..b717fe05c 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -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 + // Account + | ChangeHandleModal + | DeleteAccountModal | EditProfileModal - | ServerInputModal - | ReportPostModal + + // Curation + | ContentFilteringSettingsModal + + // Reporting | ReportAccountModal + | ReportPostModal + + // Posting + | AltTextImageModal | CropImageModal - | DeleteAccountModal + | ServerInputModal | RepostModal - | ChangeHandleModal + + // Bluesky access | WaitlistModal | InviteCodesModal - | ContentFilteringSettingsModal + + // Generic + | ConfirmModal interface LightboxModel {} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 08f977f79..275001309 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -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, diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index f4dfc88fa..98f0824fd 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -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, + }, }) diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx new file mode 100644 index 000000000..987df1462 --- /dev/null +++ b/src/view/com/modals/AltImage.tsx @@ -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, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 3f10ec836..a83cdfdae 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -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 ( diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 25fed69a4..1effee69b 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -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 } diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index b05111ffc..02dea4204 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -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} /> )} </> ) diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 17e3e809b..8c31f5614 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -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 +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, -}: { - uri: string - onPress?: () => void - onLongPress?: () => void - onPressIn?: () => void - style?: StyleProp<ViewStyle> - children?: React.ReactNode -}) { +}: 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 } diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 40f1948d6..5c232e0b4 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -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> diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index f4fe59522..51bb04fe9 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -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 +interface ImageLayoutGridProps { + images: AppBskyEmbedImages.ViewImage[] + onPress?: (index: number) => void + onLongPress?: (index: number) => void + onPressIn?: (index: number) => void + style?: StyleProp<ViewStyle> +} export function ImageLayoutGrid({ - type, - uris, + images, onPress, onLongPress, onPressIn, style, -}: { - type: ImageLayoutGridType - uris: string[] - onPress?: (index: number) => void - onLongPress?: (index: number) => void - onPressIn?: (index: number) => void - style?: StyleProp<ViewStyle> -}) { +}: 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({ ) } +interface ImageLayoutGridInnerProps { + images: AppBskyEmbedImages.ViewImage[] + onPress?: (index: number) => void + onLongPress?: (index: number) => void + onPressIn?: (index: number) => void + containerInfo: Dimensions +} + function ImageLayoutGridInner({ - type, - uris, + images, onPress, onLongPress, onPressIn, containerInfo, -}: { - type: ImageLayoutGridType - uris: string[] - onPress?: (index: number) => void - onLongPress?: (index: number) => void - onPressIn?: (index: number) => void - containerInfo: Dimensions -}) { +}: 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> diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index c15986b76..f37fba342 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -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} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index eab050fd0..e0abec777 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -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 /> </> ) }) |