diff options
author | Mary <148872143+mary-ext@users.noreply.github.com> | 2024-09-24 23:21:06 +0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-25 01:21:06 +0900 |
commit | ed512d6dc5390555232bb4ac3f96f477751c33b1 (patch) | |
tree | 4ef4f2eaf1ea6d0d2da9ccb67a857deb40ecd76f /src | |
parent | 8ea89469ef1a7988a7b3d05716da55e9da680c35 (diff) | |
download | voidsky-ed512d6dc5390555232bb4ac3f96f477751c33b1.tar.zst |
Revamp edit image alt text dialog (#5461)
* revamp alt dialog * readd the limit check don't trim with enforceLen, it ruins copy-pasting long text and it's overall annoying behavior * Update src/view/com/composer/photos/ImageAltTextDialog.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/state/modals/index.tsx | 8 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 14 | ||||
-rw-r--r-- | src/view/com/composer/photos/ImageAltTextDialog.tsx | 121 | ||||
-rw-r--r-- | src/view/com/modals/AltImage.tsx | 189 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 9 |
6 files changed, 136 insertions, 211 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 467853a25..9bc96cf5e 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -3,7 +3,6 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {ComposerImage} from '../gallery' export interface EditProfileModal { name: 'edit-profile' @@ -43,12 +42,6 @@ export interface CropImageModal { onSelect: (img?: RNImage) => void } -export interface AltTextImageModal { - name: 'alt-text-image' - image: ComposerImage - onChange: (next: ComposerImage) => void -} - export interface DeleteAccountModal { name: 'delete-account' } @@ -131,7 +124,6 @@ export type Modal = | ListAddRemoveUsersModal // Posts - | AltTextImageModal | CropImageModal | SelfLabelModal diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 775413e81..83c1e3c80 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -18,9 +18,10 @@ import {Dimensions} from '#/lib/media/types' import {colors, s} from '#/lib/styles' import {isNative} from '#/platform/detection' import {ComposerImage, cropImage} from '#/state/gallery' -import {useModalControls} from '#/state/modals' 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 @@ -141,7 +142,8 @@ const GalleryItem = ({ }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() - const {openModal} = useModalControls() + + const altTextControl = Dialog.useDialogControl() const onImageEdit = () => { if (isNative) { @@ -153,7 +155,7 @@ const GalleryItem = ({ const onAltTextEdit = () => { Keyboard.dismiss() - openModal({name: 'alt-text-image', image, onChange}) + altTextControl.open() } return ( @@ -229,6 +231,12 @@ const GalleryItem = ({ accessible={true} accessibilityIgnoresInvertColors /> + + <ImageAltTextDialog + control={altTextControl} + image={image} + onChange={onChange} + /> </View> ) } diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx new file mode 100644 index 000000000..123e1066a --- /dev/null +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -0,0 +1,121 @@ +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 {MAX_ALT_TEXT} from '#/lib/constants' +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 ( + <Dialog.Outer control={props.control}> + <Dialog.Handle /> + + <ImageAltTextInner {...props} /> + </Dialog.Outer> + ) +} + +const ImageAltTextInner = ({ + control, + image, + onChange, +}: Props): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + const windim = useWindowDimensions() + + const [altText, setAltText] = React.useState(image.alt) + + const onPressSubmit = React.useCallback(() => { + control.close() + onChange({...image, alt: altText.trim()}) + }, [control, image, altText, onChange]) + + const imageStyle = React.useMemo<ImageStyle>(() => { + 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 ( + <Dialog.ScrollableInner label={_(msg`Add alt text`)}> + <Dialog.Close /> + + <View> + <Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}> + <Trans>Add alt text</Trans> + </Text> + + <View style={[t.atoms.bg_contrast_50, a.rounded_sm, a.overflow_hidden]}> + <Image + style={imageStyle} + source={{ + uri: (image.transformed ?? image.source).path, + }} + contentFit="contain" + accessible={true} + accessibilityIgnoresInvertColors + enableLiveTextInteraction + /> + </View> + </View> + + <View style={[a.mt_md, a.gap_md]}> + <View> + <TextField.LabelText> + <Trans>Descriptive alt text</Trans> + </TextField.LabelText> + <TextField.Root> + <Dialog.Input + label={_(msg`Alt text`)} + onChangeText={text => setAltText(text)} + value={altText} + multiline + numberOfLines={3} + autoFocus + /> + </TextField.Root> + </View> + <Button + label={_(msg`Save`)} + disabled={altText.length > MAX_ALT_TEXT || altText === image.alt} + size="large" + color="primary" + variant="solid" + onPress={onPressSubmit}> + <ButtonText> + <Trans>Save</Trans> + </ButtonText> + </Button> + </View> + </Dialog.ScrollableInner> + ) +} diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx deleted file mode 100644 index c711f73a5..000000000 --- a/src/view/com/modals/AltImage.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react' -import { - ImageStyle, - ScrollView as RNScrollView, - StyleSheet, - TextInput as RNTextInput, - TouchableOpacity, - useWindowDimensions, - View, -} from 'react-native' -import {Image} from 'expo-image' -import {LinearGradient} from 'expo-linear-gradient' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {ComposerImage} from '#/state/gallery' -import {useModalControls} from '#/state/modals' -import {MAX_ALT_TEXT} from 'lib/constants' -import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' -import {usePalette} from 'lib/hooks/usePalette' -import {enforceLen} from 'lib/strings/helpers' -import {gradients, s} from 'lib/styles' -import {useTheme} from 'lib/ThemeContext' -import {isAndroid, isWeb} from 'platform/detection' -import {Text} from '../util/text/Text' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['100%'] - -interface Props { - image: ComposerImage - onChange: (next: ComposerImage) => void -} - -export function Component({image, onChange}: Props) { - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - const [altText, setAltText] = useState(image.alt) - const windim = useWindowDimensions() - const {closeModal} = useModalControls() - const inputRef = React.useRef<RNTextInput>(null) - const scrollViewRef = React.useRef<RNScrollView>(null) - const keyboardShown = useIsKeyboardVisible() - - // Autofocus hack when we open the modal. We have to wait for the animation to complete first - React.useEffect(() => { - if (isAndroid) return - setTimeout(() => { - inputRef.current?.focus() - }, 500) - }, []) - - // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll - // (especially on android, it acts weird) - React.useEffect(() => { - if (keyboardShown[0]) { - scrollViewRef.current?.scrollToEnd() - } - }, [keyboardShown]) - - const imageStyles = useMemo<ImageStyle>(() => { - const maxWidth = isWeb ? 450 : windim.width - const media = image.transformed ?? image.source - if (media.height > media.width) { - return { - resizeMode: 'contain', - width: '100%', - aspectRatio: 1, - borderRadius: 8, - } - } - return { - width: '100%', - height: (maxWidth / media.width) * media.height, - borderRadius: 8, - } - }, [image, windim]) - - const onUpdate = useCallback( - (v: string) => { - v = enforceLen(v, MAX_ALT_TEXT) - setAltText(v) - }, - [setAltText], - ) - - const onPressSave = useCallback(() => { - onChange({ - ...image, - alt: altText, - }) - - closeModal() - }, [closeModal, image, altText, onChange]) - - return ( - <ScrollView - testID="altTextImageModal" - style={[pal.view, styles.scrollContainer]} - keyboardShouldPersistTaps="always" - ref={scrollViewRef} - nativeID="imageAltText"> - <View style={styles.scrollInner}> - <View style={[pal.viewLight, styles.imageContainer]}> - <Image - testID="selectedPhotoImage" - style={imageStyles} - source={{uri: (image.transformed ?? image.source).path}} - contentFit="contain" - accessible={true} - accessibilityIgnoresInvertColors - enableLiveTextInteraction - /> - </View> - <TextInput - testID="altTextImageInput" - style={[styles.textArea, pal.border, pal.text]} - keyboardAppearance={theme.colorScheme} - multiline - placeholder={_(msg`Add alt text`)} - placeholderTextColor={pal.colors.textLight} - value={altText} - onChangeText={onUpdate} - accessibilityLabel={_(msg`Image alt text`)} - accessibilityHint="" - accessibilityLabelledBy="imageAltText" - // @ts-ignore This is fine, type is weird on the BottomSheetTextInput - ref={inputRef} - /> - <View style={styles.buttonControls}> - <TouchableOpacity - testID="altTextImageSaveBtn" - onPress={onPressSave} - accessibilityLabel={_(msg`Save alt text`)} - accessibilityHint="" - accessibilityRole="button"> - <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]}> - <Trans>Done</Trans> - </Text> - </LinearGradient> - </TouchableOpacity> - </View> - </View> - </ScrollView> - ) -} - -const styles = StyleSheet.create({ - scrollContainer: { - flex: 1, - height: '100%', - paddingHorizontal: isWeb ? 0 : 12, - paddingVertical: isWeb ? 0 : 24, - }, - scrollInner: { - gap: 12, - paddingTop: isWeb ? 0 : 12, - }, - imageContainer: { - borderRadius: 8, - }, - 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, - paddingBottom: isWeb ? 0 : 50, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index fd881ebc4..90e93821c 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -3,12 +3,11 @@ import {StyleSheet} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' import BottomSheet from '@discord/bottom-sheet/src' +import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import * as AddAppPassword from './AddAppPasswords' -import * as AltImageModal from './AltImage' import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' @@ -74,9 +73,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'self-label') { snapPoints = SelfLabelModal.snapPoints element = <SelfLabelModal.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} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index fe24695d2..c1024751f 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -2,13 +2,12 @@ import React from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {usePalette} from '#/lib/hooks/usePalette' import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import type {Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import * as AddAppPassword from './AddAppPasswords' -import * as AltTextImageModal from './AltImage' import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' @@ -53,7 +52,7 @@ function Modal({modal}: {modal: ModalIface}) { } const onPressMask = () => { - if (modal.name === 'crop-image' || modal.name === 'alt-text-image') { + if (modal.name === 'crop-image') { return // dont close on mask presses during crop } closeModal() @@ -88,8 +87,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <ContentLanguagesSettingsModal.Component /> } else if (modal.name === 'post-languages-settings') { element = <PostLanguagesSettingsModal.Component /> - } else if (modal.name === 'alt-text-image') { - element = <AltTextImageModal.Component {...modal} /> } else if (modal.name === 'verify-email') { element = <VerifyEmailModal.Component {...modal} /> } else if (modal.name === 'change-email') { |