Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Mary
surfdude29
and committed by
GitHub
ed512d6d 8ea89469

+136 -211
-8
src/state/modals/index.tsx
··· 3 3 import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' 4 4 5 5 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 6 - import {ComposerImage} from '../gallery' 7 6 8 7 export interface EditProfileModal { 9 8 name: 'edit-profile' ··· 41 40 uri: string 42 41 dimensions?: {width: number; height: number} 43 42 onSelect: (img?: RNImage) => void 44 - } 45 - 46 - export interface AltTextImageModal { 47 - name: 'alt-text-image' 48 - image: ComposerImage 49 - onChange: (next: ComposerImage) => void 50 43 } 51 44 52 45 export interface DeleteAccountModal { ··· 131 124 | ListAddRemoveUsersModal 132 125 133 126 // Posts 134 - | AltTextImageModal 135 127 | CropImageModal 136 128 | SelfLabelModal 137 129
+11 -3
src/view/com/composer/photos/Gallery.tsx
··· 18 18 import {colors, s} from '#/lib/styles' 19 19 import {isNative} from '#/platform/detection' 20 20 import {ComposerImage, cropImage} from '#/state/gallery' 21 - import {useModalControls} from '#/state/modals' 22 21 import {Text} from '#/view/com/util/text/Text' 23 22 import {useTheme} from '#/alf' 23 + import * as Dialog from '#/components/Dialog' 24 + import {ImageAltTextDialog} from './ImageAltTextDialog' 24 25 25 26 const IMAGE_GAP = 8 26 27 ··· 141 142 }: GalleryItemProps): React.ReactNode => { 142 143 const {_} = useLingui() 143 144 const t = useTheme() 144 - const {openModal} = useModalControls() 145 + 146 + const altTextControl = Dialog.useDialogControl() 145 147 146 148 const onImageEdit = () => { 147 149 if (isNative) { ··· 153 155 154 156 const onAltTextEdit = () => { 155 157 Keyboard.dismiss() 156 - openModal({name: 'alt-text-image', image, onChange}) 158 + altTextControl.open() 157 159 } 158 160 159 161 return ( ··· 228 230 }} 229 231 accessible={true} 230 232 accessibilityIgnoresInvertColors 233 + /> 234 + 235 + <ImageAltTextDialog 236 + control={altTextControl} 237 + image={image} 238 + onChange={onChange} 231 239 /> 232 240 </View> 233 241 )
+121
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 1 + import React from 'react' 2 + import {ImageStyle, useWindowDimensions, View} from 'react-native' 3 + import {Image} from 'expo-image' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {MAX_ALT_TEXT} from '#/lib/constants' 8 + import {isWeb} from '#/platform/detection' 9 + import {ComposerImage} from '#/state/gallery' 10 + import {atoms as a, useTheme} from '#/alf' 11 + import {Button, ButtonText} from '#/components/Button' 12 + import * as Dialog from '#/components/Dialog' 13 + import * as TextField from '#/components/forms/TextField' 14 + import {Text} from '#/components/Typography' 15 + 16 + type Props = { 17 + control: Dialog.DialogOuterProps['control'] 18 + image: ComposerImage 19 + onChange: (next: ComposerImage) => void 20 + } 21 + 22 + export const ImageAltTextDialog = (props: Props): React.ReactNode => { 23 + return ( 24 + <Dialog.Outer control={props.control}> 25 + <Dialog.Handle /> 26 + 27 + <ImageAltTextInner {...props} /> 28 + </Dialog.Outer> 29 + ) 30 + } 31 + 32 + const ImageAltTextInner = ({ 33 + control, 34 + image, 35 + onChange, 36 + }: Props): React.ReactNode => { 37 + const {_} = useLingui() 38 + const t = useTheme() 39 + 40 + const windim = useWindowDimensions() 41 + 42 + const [altText, setAltText] = React.useState(image.alt) 43 + 44 + const onPressSubmit = React.useCallback(() => { 45 + control.close() 46 + onChange({...image, alt: altText.trim()}) 47 + }, [control, image, altText, onChange]) 48 + 49 + const imageStyle = React.useMemo<ImageStyle>(() => { 50 + const maxWidth = isWeb ? 450 : windim.width 51 + const source = image.transformed ?? image.source 52 + 53 + if (source.height > source.width) { 54 + return { 55 + resizeMode: 'contain', 56 + width: '100%', 57 + aspectRatio: 1, 58 + borderRadius: 8, 59 + } 60 + } 61 + return { 62 + width: '100%', 63 + height: (maxWidth / source.width) * source.height, 64 + borderRadius: 8, 65 + } 66 + }, [image, windim]) 67 + 68 + return ( 69 + <Dialog.ScrollableInner label={_(msg`Add alt text`)}> 70 + <Dialog.Close /> 71 + 72 + <View> 73 + <Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}> 74 + <Trans>Add alt text</Trans> 75 + </Text> 76 + 77 + <View style={[t.atoms.bg_contrast_50, a.rounded_sm, a.overflow_hidden]}> 78 + <Image 79 + style={imageStyle} 80 + source={{ 81 + uri: (image.transformed ?? image.source).path, 82 + }} 83 + contentFit="contain" 84 + accessible={true} 85 + accessibilityIgnoresInvertColors 86 + enableLiveTextInteraction 87 + /> 88 + </View> 89 + </View> 90 + 91 + <View style={[a.mt_md, a.gap_md]}> 92 + <View> 93 + <TextField.LabelText> 94 + <Trans>Descriptive alt text</Trans> 95 + </TextField.LabelText> 96 + <TextField.Root> 97 + <Dialog.Input 98 + label={_(msg`Alt text`)} 99 + onChangeText={text => setAltText(text)} 100 + value={altText} 101 + multiline 102 + numberOfLines={3} 103 + autoFocus 104 + /> 105 + </TextField.Root> 106 + </View> 107 + <Button 108 + label={_(msg`Save`)} 109 + disabled={altText.length > MAX_ALT_TEXT || altText === image.alt} 110 + size="large" 111 + color="primary" 112 + variant="solid" 113 + onPress={onPressSubmit}> 114 + <ButtonText> 115 + <Trans>Save</Trans> 116 + </ButtonText> 117 + </Button> 118 + </View> 119 + </Dialog.ScrollableInner> 120 + ) 121 + }
-189
src/view/com/modals/AltImage.tsx
··· 1 - import React, {useCallback, useMemo, useState} from 'react' 2 - import { 3 - ImageStyle, 4 - ScrollView as RNScrollView, 5 - StyleSheet, 6 - TextInput as RNTextInput, 7 - TouchableOpacity, 8 - useWindowDimensions, 9 - View, 10 - } from 'react-native' 11 - import {Image} from 'expo-image' 12 - import {LinearGradient} from 'expo-linear-gradient' 13 - import {msg, Trans} from '@lingui/macro' 14 - import {useLingui} from '@lingui/react' 15 - 16 - import {ComposerImage} from '#/state/gallery' 17 - import {useModalControls} from '#/state/modals' 18 - import {MAX_ALT_TEXT} from 'lib/constants' 19 - import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' 20 - import {usePalette} from 'lib/hooks/usePalette' 21 - import {enforceLen} from 'lib/strings/helpers' 22 - import {gradients, s} from 'lib/styles' 23 - import {useTheme} from 'lib/ThemeContext' 24 - import {isAndroid, isWeb} from 'platform/detection' 25 - import {Text} from '../util/text/Text' 26 - import {ScrollView, TextInput} from './util' 27 - 28 - export const snapPoints = ['100%'] 29 - 30 - interface Props { 31 - image: ComposerImage 32 - onChange: (next: ComposerImage) => void 33 - } 34 - 35 - export function Component({image, onChange}: Props) { 36 - const pal = usePalette('default') 37 - const theme = useTheme() 38 - const {_} = useLingui() 39 - const [altText, setAltText] = useState(image.alt) 40 - const windim = useWindowDimensions() 41 - const {closeModal} = useModalControls() 42 - const inputRef = React.useRef<RNTextInput>(null) 43 - const scrollViewRef = React.useRef<RNScrollView>(null) 44 - const keyboardShown = useIsKeyboardVisible() 45 - 46 - // Autofocus hack when we open the modal. We have to wait for the animation to complete first 47 - React.useEffect(() => { 48 - if (isAndroid) return 49 - setTimeout(() => { 50 - inputRef.current?.focus() 51 - }, 500) 52 - }, []) 53 - 54 - // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll 55 - // (especially on android, it acts weird) 56 - React.useEffect(() => { 57 - if (keyboardShown[0]) { 58 - scrollViewRef.current?.scrollToEnd() 59 - } 60 - }, [keyboardShown]) 61 - 62 - const imageStyles = useMemo<ImageStyle>(() => { 63 - const maxWidth = isWeb ? 450 : windim.width 64 - const media = image.transformed ?? image.source 65 - if (media.height > media.width) { 66 - return { 67 - resizeMode: 'contain', 68 - width: '100%', 69 - aspectRatio: 1, 70 - borderRadius: 8, 71 - } 72 - } 73 - return { 74 - width: '100%', 75 - height: (maxWidth / media.width) * media.height, 76 - borderRadius: 8, 77 - } 78 - }, [image, windim]) 79 - 80 - const onUpdate = useCallback( 81 - (v: string) => { 82 - v = enforceLen(v, MAX_ALT_TEXT) 83 - setAltText(v) 84 - }, 85 - [setAltText], 86 - ) 87 - 88 - const onPressSave = useCallback(() => { 89 - onChange({ 90 - ...image, 91 - alt: altText, 92 - }) 93 - 94 - closeModal() 95 - }, [closeModal, image, altText, onChange]) 96 - 97 - return ( 98 - <ScrollView 99 - testID="altTextImageModal" 100 - style={[pal.view, styles.scrollContainer]} 101 - keyboardShouldPersistTaps="always" 102 - ref={scrollViewRef} 103 - nativeID="imageAltText"> 104 - <View style={styles.scrollInner}> 105 - <View style={[pal.viewLight, styles.imageContainer]}> 106 - <Image 107 - testID="selectedPhotoImage" 108 - style={imageStyles} 109 - source={{uri: (image.transformed ?? image.source).path}} 110 - contentFit="contain" 111 - accessible={true} 112 - accessibilityIgnoresInvertColors 113 - enableLiveTextInteraction 114 - /> 115 - </View> 116 - <TextInput 117 - testID="altTextImageInput" 118 - style={[styles.textArea, pal.border, pal.text]} 119 - keyboardAppearance={theme.colorScheme} 120 - multiline 121 - placeholder={_(msg`Add alt text`)} 122 - placeholderTextColor={pal.colors.textLight} 123 - value={altText} 124 - onChangeText={onUpdate} 125 - accessibilityLabel={_(msg`Image alt text`)} 126 - accessibilityHint="" 127 - accessibilityLabelledBy="imageAltText" 128 - // @ts-ignore This is fine, type is weird on the BottomSheetTextInput 129 - ref={inputRef} 130 - /> 131 - <View style={styles.buttonControls}> 132 - <TouchableOpacity 133 - testID="altTextImageSaveBtn" 134 - onPress={onPressSave} 135 - accessibilityLabel={_(msg`Save alt text`)} 136 - accessibilityHint="" 137 - accessibilityRole="button"> 138 - <LinearGradient 139 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 140 - start={{x: 0, y: 0}} 141 - end={{x: 1, y: 1}} 142 - style={[styles.button]}> 143 - <Text type="button-lg" style={[s.white, s.bold]}> 144 - <Trans>Done</Trans> 145 - </Text> 146 - </LinearGradient> 147 - </TouchableOpacity> 148 - </View> 149 - </View> 150 - </ScrollView> 151 - ) 152 - } 153 - 154 - const styles = StyleSheet.create({ 155 - scrollContainer: { 156 - flex: 1, 157 - height: '100%', 158 - paddingHorizontal: isWeb ? 0 : 12, 159 - paddingVertical: isWeb ? 0 : 24, 160 - }, 161 - scrollInner: { 162 - gap: 12, 163 - paddingTop: isWeb ? 0 : 12, 164 - }, 165 - imageContainer: { 166 - borderRadius: 8, 167 - }, 168 - textArea: { 169 - borderWidth: 1, 170 - borderRadius: 6, 171 - paddingTop: 10, 172 - paddingHorizontal: 12, 173 - fontSize: 16, 174 - height: 100, 175 - textAlignVertical: 'top', 176 - }, 177 - button: { 178 - flexDirection: 'row', 179 - alignItems: 'center', 180 - justifyContent: 'center', 181 - width: '100%', 182 - borderRadius: 32, 183 - padding: 10, 184 - }, 185 - buttonControls: { 186 - gap: 8, 187 - paddingBottom: isWeb ? 0 : 50, 188 - }, 189 - })
+1 -5
src/view/com/modals/Modal.tsx
··· 3 3 import {SafeAreaView} from 'react-native-safe-area-context' 4 4 import BottomSheet from '@discord/bottom-sheet/src' 5 5 6 + import {usePalette} from '#/lib/hooks/usePalette' 6 7 import {useModalControls, useModals} from '#/state/modals' 7 - import {usePalette} from 'lib/hooks/usePalette' 8 8 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 9 9 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 10 10 import * as AddAppPassword from './AddAppPasswords' 11 - import * as AltImageModal from './AltImage' 12 11 import * as ChangeEmailModal from './ChangeEmail' 13 12 import * as ChangeHandleModal from './ChangeHandle' 14 13 import * as ChangePasswordModal from './ChangePassword' ··· 74 73 } else if (activeModal?.name === 'self-label') { 75 74 snapPoints = SelfLabelModal.snapPoints 76 75 element = <SelfLabelModal.Component {...activeModal} /> 77 - } else if (activeModal?.name === 'alt-text-image') { 78 - snapPoints = AltImageModal.snapPoints 79 - element = <AltImageModal.Component {...activeModal} /> 80 76 } else if (activeModal?.name === 'change-handle') { 81 77 snapPoints = ChangeHandleModal.snapPoints 82 78 element = <ChangeHandleModal.Component {...activeModal} />
+3 -6
src/view/com/modals/Modal.web.tsx
··· 2 2 import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 3 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 4 5 + import {usePalette} from '#/lib/hooks/usePalette' 5 6 import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 7 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 6 8 import type {Modal as ModalIface} from '#/state/modals' 7 9 import {useModalControls, useModals} from '#/state/modals' 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 10 import * as AddAppPassword from './AddAppPasswords' 11 - import * as AltTextImageModal from './AltImage' 12 11 import * as ChangeEmailModal from './ChangeEmail' 13 12 import * as ChangeHandleModal from './ChangeHandle' 14 13 import * as ChangePasswordModal from './ChangePassword' ··· 53 52 } 54 53 55 54 const onPressMask = () => { 56 - if (modal.name === 'crop-image' || modal.name === 'alt-text-image') { 55 + if (modal.name === 'crop-image') { 57 56 return // dont close on mask presses during crop 58 57 } 59 58 closeModal() ··· 88 87 element = <ContentLanguagesSettingsModal.Component /> 89 88 } else if (modal.name === 'post-languages-settings') { 90 89 element = <PostLanguagesSettingsModal.Component /> 91 - } else if (modal.name === 'alt-text-image') { 92 - element = <AltTextImageModal.Component {...modal} /> 93 90 } else if (modal.name === 'verify-email') { 94 91 element = <VerifyEmailModal.Component {...modal} /> 95 92 } else if (modal.name === 'change-email') {