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>
This commit is contained in:
Mary
2024-09-24 23:21:06 +07:00
committed by GitHub
parent 8ea89469ef
commit ed512d6dc5
6 changed files with 136 additions and 211 deletions
-8
View File
@@ -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
+11 -3
View File
@@ -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>
)
}
@@ -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>
)
}
-189
View File
@@ -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,
},
})
+1 -5
View File
@@ -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} />
+3 -6
View File
@@ -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') {