From a82402cda545a94345ae12be366bb054baec503f Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 7 May 2024 22:05:40 +0700 Subject: [PATCH] feat: alf'd image alt text dialog --- src/view/com/composer/photos/Gallery.tsx | 320 ++++++++++-------- .../composer/photos/ImageAltTextDialog.tsx | 124 +++++++ 2 files changed, 294 insertions(+), 150 deletions(-) create mode 100644 src/view/com/composer/photos/ImageAltTextDialog.tsx diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 886dc193e3..250b549258 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ImageStyle, Keyboard, LayoutChangeEvent} from 'react-native' +import {ImageStyle, LayoutChangeEvent, ViewStyle} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -7,13 +7,14 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {ComposerImage, cropImage} from '#/state/gallery' -import {useModalControls} from '#/state/modals' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {Dimensions} from 'lib/media/types' import {colors, s} from 'lib/styles' import {isNative} from 'platform/detection' import {Text} from 'view/com/util/text/Text' import {useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {ImageAltTextDialog} from './ImageAltTextDialog' const IMAGE_GAP = 8 @@ -48,165 +49,65 @@ interface GalleryInnerProps extends GalleryProps { } const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => { - const {_} = useLingui() const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - const t = useTheme() - let side: number + const {altTextControlStyle, imageControlsStyle, imageStyle} = + React.useMemo(() => { + const side = + images.length === 1 + ? 250 + : (containerInfo.width - IMAGE_GAP * (images.length - 1)) / + images.length - if (images.length === 1) { - side = 250 - } else { - side = - (containerInfo.width - IMAGE_GAP * (images.length - 1)) / images.length - } + const isOverflow = isMobile && images.length > 2 - const imageStyle = { - height: side, - width: side, - } - - const isOverflow = isMobile && images.length > 2 - - const altTextControlStyle = isOverflow - ? {left: 4, bottom: 4} - : !isMobile && images.length < 3 - ? {left: 8, top: 8} - : {left: 4, top: 4} - - const imageControlsStyle = { - display: 'flex' as const, - flexDirection: 'row' as const, - position: 'absolute' as const, - ...(isOverflow - ? {top: 4, right: 4, gap: 4} - : !isMobile && images.length < 3 - ? {top: 8, right: 8, gap: 8} - : {top: 4, right: 4, gap: 4}), - zIndex: 1, - } + return { + altTextControlStyle: isOverflow + ? {left: 4, bottom: 4} + : !isMobile && images.length < 3 + ? {left: 8, top: 8} + : {left: 4, top: 4}, + imageControlsStyle: { + display: 'flex' as const, + flexDirection: 'row' as const, + position: 'absolute' as const, + ...(isOverflow + ? {top: 4, right: 4, gap: 4} + : !isMobile && images.length < 3 + ? {top: 8, right: 8, gap: 8} + : {top: 4, right: 4, gap: 4}), + zIndex: 1, + }, + imageStyle: { + height: side, + width: side, + }, + } + }, [images.length, containerInfo, isMobile]) return images.length !== 0 ? ( <> {images.map((image, index) => { - const onRemove = () => { - const next = images.slice() - next.splice(index, 1) - - onChange(next) - } - - const onAltText = () => { - Keyboard.dismiss() - openModal({ - name: 'alt-text-image', - image, - onChange(next) { - onChange( - images.map(i => (i.source === image.source ? next : i)), - ) - }, - }) - } - - const onEdit = () => { - if (isNative) { - cropImage(image).then(next => { - if (next === image) { - return - } - - onChange( - images.map(i => (i.source === image.source ? next : i)), - ) - }) - } else { - // openModal({ - // name: 'edit-image', - // image, - // gallery, - // }) - } - } - return ( - - - {image.alt.length !== 0 ? ( - - ) : ( - - )} - - ALT - - - - {isNative && ( - - - - )} - - - - - + { + onChange( + images.map(i => (i.source === image.source ? next : i)), + ) + }} + onRemove={() => { + const next = images.slice() + next.splice(index, 1) - - + onChange(next) + }} + /> ) })} @@ -215,6 +116,125 @@ const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => { ) : null } +type GalleryItemProps = { + image: ComposerImage + altTextControlStyle?: ViewStyle + imageControlsStyle?: ViewStyle + imageStyle?: ViewStyle + onChange: (next: ComposerImage) => void + onRemove: () => void +} + +const GalleryItem = ({ + image, + altTextControlStyle, + imageControlsStyle, + imageStyle, + onChange, + onRemove, +}: GalleryItemProps): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + const altTextControl = Dialog.useDialogControl() + + const onEdit = () => { + if (isNative) { + cropImage(image).then(next => { + onChange(next) + }) + } else { + // openModal({ + // name: 'edit-image', + // image, + // gallery, + // }) + } + } + + return ( + + + {image.alt.length !== 0 ? ( + + ) : ( + + )} + + ALT + + + + {isNative && ( + + + + )} + + + + + + + + + + + ) +} + export function AltTextReminder() { const t = useTheme() return ( diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx new file mode 100644 index 0000000000..b029d8ceb6 --- /dev/null +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import {ImageStyle, useWindowDimensions, View} from 'react-native' +import {Image} from 'expo-image' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from '#/platform/detection' +import {ComposerImage} from '#/state/gallery' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Text} from '#/components/Typography' + +type Props = { + control: Dialog.DialogOuterProps['control'] + image: ComposerImage + onChange: (next: ComposerImage) => void +} + +export const ImageAltTextDialog = (props: Props): React.ReactNode => { + return ( + + + + + + ) +} + +const ImageAltTextInner = ({ + control, + image, + onChange, +}: Props): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + const windim = useWindowDimensions() + + const [altText, setAltText] = React.useState(image.alt) + const trimmed = React.useMemo(() => altText.trim(), [altText]) + + const onPressSubmit = React.useCallback(() => { + control.close() + onChange({...image, alt: trimmed}) + }, [control, image, trimmed, onChange]) + + const imageStyle = React.useMemo(() => { + const maxWidth = isWeb ? 450 : windim.width + const source = image.transformed ?? image.source + + if (source.height > source.width) { + return { + resizeMode: 'contain', + width: '100%', + aspectRatio: 1, + borderRadius: 8, + } + } + return { + width: '100%', + height: (maxWidth / source.width) * source.height, + borderRadius: 8, + } + }, [image, windim]) + + return ( + + + + + + + + Descriptive alt text + + + setAltText(text)} + value={altText} + multiline + numberOfLines={3} + autoFocus + /> + + + + + + {/* below the text input to force tab order */} + + + Add ALT text + + + + + + + + + ) +}