diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-05-06 17:28:38 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-06 17:28:38 +0100 |
commit | c33c3b7d1e44037d3370da0ac60926293bf8a158 (patch) | |
tree | 08c6c852549dd7a9486ffa12a2d1492314afd452 | |
parent | ae7626ce6ed08059c161345046b6037313fc2505 (diff) | |
download | voidsky-c33c3b7d1e44037d3370da0ac60926293bf8a158.tar.zst |
Alt text for gifs (#3876)
* add alt text dialog * multiline alt text input * add pressable alt text badge * rename `ALT: ` to `Alt text: ` to avoid including old bad ones * reuse alt text reminder * reuse alt text reminder in gallery * add alt text reminder in the dialog itself * autofocus text input * reorder components to fix tab order * fix close btn position
-rw-r--r-- | src/components/Prompt.tsx | 4 | ||||
-rw-r--r-- | src/components/dialogs/MutedWords.tsx | 4 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 46 | ||||
-rw-r--r-- | src/view/com/composer/ExternalEmbed.tsx | 9 | ||||
-rw-r--r-- | src/view/com/composer/GifAltText.tsx | 177 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 67 | ||||
-rw-r--r-- | src/view/com/util/images/Gallery.tsx | 5 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/ExternalLinkEmbed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/GifEmbed.tsx | 75 |
9 files changed, 344 insertions, 47 deletions
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 0a171674d..327bbbaa2 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -43,7 +43,9 @@ export function Outer({ <Dialog.ScrollableInner accessibilityLabelledBy={titleId} accessibilityDescribedBy={descriptionId} - style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> + style={[ + gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> {children} </Dialog.ScrollableInner> </Context.Provider> diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 0eced11e3..534263422 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -37,12 +37,12 @@ export function MutedWordsDialog() { return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <MutedWordsInner control={control} /> + <MutedWordsInner /> </Dialog.Outer> ) } -function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) { +function MutedWordsInner() { const t = useTheme() const {_} = useLingui() const {gtMobile} = useBreakpoints() diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 0ac4ac56e..f472bb2e2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -59,6 +59,7 @@ import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {CharProgress} from './char-progress/CharProgress' import {ExternalEmbed} from './ExternalEmbed' +import {GifAltText} from './GifAltText' import {LabelsBtn} from './labels/LabelsBtn' import {Gallery} from './photos/Gallery' import {OpenCameraBtn} from './photos/OpenCameraBtn' @@ -327,7 +328,7 @@ export const ComposePost = observer(function ComposePost({ image: gif.media_formats.preview.url, likelyType: LikelyType.HTML, title: gif.content_description, - description: `ALT: ${gif.content_description}`, + description: '', }, }) setExtGif(gif) @@ -335,6 +336,26 @@ export const ComposePost = observer(function ComposePost({ [setExtLink], ) + const handleChangeGifAltText = useCallback( + (altText: string) => { + setExtLink(ext => + ext && ext.meta + ? { + ...ext, + meta: { + ...ext?.meta, + description: + altText.trim().length === 0 + ? '' + : `Alt text: ${altText.trim()}`, + }, + } + : ext, + ) + }, + [setExtLink], + ) + return ( <KeyboardAvoidingView testID="composePostView" @@ -474,14 +495,21 @@ export const ComposePost = observer(function ComposePost({ <Gallery gallery={gallery} /> {gallery.isEmpty && extLink && ( - <ExternalEmbed - link={extLink} - gif={extGif} - onRemove={() => { - setExtLink(undefined) - setExtGif(undefined) - }} - /> + <View style={a.relative}> + <ExternalEmbed + link={extLink} + gif={extGif} + onRemove={() => { + setExtLink(undefined) + setExtGif(undefined) + }} + /> + <GifAltText + link={extLink} + gif={extGif} + onSubmit={handleChangeGifAltText} + /> + </View> )} {quote ? ( <View style={[s.mt5, isWeb && s.mb10]}> diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 321e29b30..b81065e99 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -46,7 +46,12 @@ export const ExternalEmbed = ({ : undefined return ( - <View style={[a.mb_xl, a.overflow_hidden, t.atoms.border_contrast_medium]}> + <View + style={[ + !gif && a.mb_xl, + a.overflow_hidden, + t.atoms.border_contrast_medium, + ]}> {link.isLoading ? ( <Container style={loadingStyle}> <Loader size="xl" /> @@ -62,7 +67,7 @@ export const ExternalEmbed = ({ </Container> ) : linkInfo ? ( <View style={{pointerEvents: !gif ? 'none' : 'auto'}}> - <ExternalLinkEmbed link={linkInfo} /> + <ExternalLinkEmbed link={linkInfo} hideAlt /> </View> ) : null} <TouchableOpacity diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx new file mode 100644 index 000000000..9e41a328f --- /dev/null +++ b/src/view/com/composer/GifAltText.tsx @@ -0,0 +1,177 @@ +import React, {useCallback, useState} from 'react' +import {TouchableOpacity, View} from 'react-native' +import {AppBskyEmbedExternal} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {ExternalEmbedDraft} from '#/lib/api' +import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants' +import { + EmbedPlayerParams, + parseEmbedPlayerFromUrl, +} from '#/lib/strings/embed-player' +import {enforceLen} from '#/lib/strings/helpers' +import {isAndroid} from '#/platform/detection' +import {Gif} from '#/state/queries/tenor' +import {atoms as a, native, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Text} from '#/components/Typography' +import {GifEmbed} from '../util/post-embeds/GifEmbed' +import {AltTextReminder} from './photos/Gallery' + +export function GifAltText({ + link: linkProp, + gif, + onSubmit, +}: { + link: ExternalEmbedDraft + gif?: Gif + onSubmit: (alt: string) => void +}) { + const control = Dialog.useDialogControl() + const {_} = useLingui() + const t = useTheme() + + const {link, params} = React.useMemo(() => { + return { + link: { + title: linkProp.meta?.title ?? linkProp.uri, + uri: linkProp.uri, + description: linkProp.meta?.description ?? '', + thumb: linkProp.localThumb?.path, + }, + params: parseEmbedPlayerFromUrl(linkProp.uri), + } + }, [linkProp]) + + const onPressSubmit = useCallback( + (alt: string) => { + control.close(() => { + onSubmit(alt) + }) + }, + [onSubmit, control], + ) + + if (!gif || !params) return null + + return ( + <> + <TouchableOpacity + testID="altTextButton" + accessibilityRole="button" + accessibilityLabel={_(msg`Add alt text`)} + accessibilityHint="" + hitSlop={HITSLOP_10} + onPress={control.open} + style={[ + a.absolute, + {top: 20, left: 12}, + {borderRadius: 6}, + a.pl_xs, + a.pr_sm, + a.py_2xs, + a.flex_row, + a.gap_xs, + a.align_center, + {backgroundColor: 'rgba(0, 0, 0, 0.75)'}, + ]}> + {link.description ? ( + <Check size="xs" fill={t.palette.white} style={a.ml_xs} /> + ) : ( + <Plus size="sm" fill={t.palette.white} /> + )} + <Text + style={[a.font_bold, {color: t.palette.white}]} + accessible={false}> + <Trans>ALT</Trans> + </Text> + </TouchableOpacity> + + <AltTextReminder /> + + <Dialog.Outer + control={control} + nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> + <Dialog.Handle /> + <AltTextInner + onSubmit={onPressSubmit} + link={link} + params={params} + initalValue={link.description.replace('Alt text: ', '')} + key={link.uri} + /> + </Dialog.Outer> + </> + ) +} + +function AltTextInner({ + onSubmit, + link, + params, + initalValue, +}: { + onSubmit: (text: string) => void + link: AppBskyEmbedExternal.ViewExternal + params: EmbedPlayerParams + initalValue: string +}) { + const {_} = useLingui() + const [altText, setAltText] = useState(initalValue) + + const onPressSubmit = useCallback(() => { + onSubmit(altText) + }, [onSubmit, altText]) + + return ( + <Dialog.ScrollableInner label={_(msg`Add alt text`)}> + <View style={a.flex_col_reverse}> + <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`)} + placeholder={link.title} + onChangeText={text => + setAltText(enforceLen(text, MAX_ALT_TEXT)) + } + value={altText} + multiline + numberOfLines={3} + autoFocus + /> + </TextField.Root> + </View> + <Button + label={_(msg`Save`)} + size="medium" + color="primary" + variant="solid" + onPress={onPressSubmit}> + <ButtonText> + <Trans>Save</Trans> + </ButtonText> + </Button> + </View> + {/* below the text input to force tab order */} + <View> + <Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}> + <Trans>Add ALT text</Trans> + </Text> + <View style={[a.w_full, a.align_center, native({maxHeight: 200})]}> + <GifEmbed link={link} params={params} hideAlt /> + </View> + </View> + </View> + <Dialog.Close /> + </Dialog.ScrollableInner> + ) +} diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 69c8debb0..7ff1b7b9a 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -1,19 +1,20 @@ import React, {useState} from 'react' import {ImageStyle, Keyboard, LayoutChangeEvent} from 'react-native' -import {GalleryModel} from 'state/models/media/gallery' -import {observer} from 'mobx-react-lite' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {s, colors} from 'lib/styles' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' -import {Text} from 'view/com/util/text/Text' -import {Dimensions} from 'lib/media/types' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {Trans, msg} from '@lingui/macro' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {observer} from 'mobx-react-lite' + 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 {GalleryModel} from 'state/models/media/gallery' +import {Text} from 'view/com/util/text/Text' +import {useTheme} from '#/alf' const IMAGE_GAP = 8 @@ -49,10 +50,10 @@ const GalleryInner = observer(function GalleryImpl({ gallery, containerInfo, }: GalleryInnerProps) { - const pal = usePalette('default') const {_} = useLingui() const {isMobile} = useWebMediaQueries() const {openModal} = useModalControls() + const t = useTheme() let side: number @@ -126,16 +127,22 @@ const GalleryInner = observer(function GalleryImpl({ }) }} style={[styles.altTextControl, altTextControlStyle]}> - <Text style={styles.altTextControlLabel} accessible={false}> - <Trans>ALT</Trans> - </Text> {image.altText.length > 0 ? ( <FontAwesomeIcon icon="check" size={10} - style={{color: colors.green3}} + style={{color: t.palette.white}} + /> + ) : ( + <FontAwesomeIcon + icon="plus" + size={10} + style={{color: t.palette.white}} /> - ) : undefined} + )} + <Text style={styles.altTextControlLabel} accessible={false}> + <Trans>ALT</Trans> + </Text> </TouchableOpacity> <View style={imageControlsStyle}> <TouchableOpacity @@ -201,21 +208,28 @@ const GalleryInner = observer(function GalleryImpl({ </View> ))} </View> - <View style={[styles.reminder]}> - <View style={[styles.infoIcon, pal.viewLight]}> - <FontAwesomeIcon icon="info" size={12} color={pal.colors.text} /> - </View> - <Text type="sm" style={[pal.textLight, s.flex1]}> - <Trans> - Alt text describes images for blind and low-vision users, and helps - give context to everyone. - </Trans> - </Text> - </View> + <AltTextReminder /> </> ) : null }) +export function AltTextReminder() { + const t = useTheme() + return ( + <View style={[styles.reminder]}> + <View style={[styles.infoIcon, t.atoms.bg_contrast_25]}> + <FontAwesomeIcon icon="info" size={12} color={t.atoms.text.color} /> + </View> + <Text type="sm" style={[t.atoms.text_contrast_medium, s.flex1]}> + <Trans> + Alt text describes images for blind and low-vision users, and helps + give context to everyone. + </Trans> + </Text> + </View> + ) +} + const styles = StyleSheet.create({ gallery: { flex: 1, @@ -244,6 +258,7 @@ const styles = StyleSheet.create({ paddingVertical: 3, flexDirection: 'row', alignItems: 'center', + gap: 4, }, altTextControlLabel: { color: 'white', diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 7de3b093a..f6d2c7a1b 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,9 +1,10 @@ -import {AppBskyEmbedImages} from '@atproto/api' import React, {ComponentProps, FC} from 'react' -import {StyleSheet, Text, Pressable, View} from 'react-native' +import {Pressable, StyleSheet, Text, View} from 'react-native' import {Image} from 'expo-image' +import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' + import {isWeb} from 'platform/detection' type EventFunction = (index: number) => void diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index 1fe75c44e..b84c04b83 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -20,9 +20,11 @@ import {Text} from '../text/Text' export const ExternalLinkEmbed = ({ link, style, + hideAlt, }: { link: AppBskyEmbedExternal.ViewExternal style?: StyleProp<ViewStyle> + hideAlt?: boolean }) => { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() @@ -37,7 +39,7 @@ export const ExternalLinkEmbed = ({ }, [link.uri, externalEmbedPrefs]) if (embedPlayerParams?.source === 'tenor') { - return <GifEmbed params={embedPlayerParams} link={link} /> + return <GifEmbed params={embedPlayerParams} link={link} hideAlt={hideAlt} /> } return ( diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index 5d21ce064..dde6efe34 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -1,14 +1,18 @@ import React from 'react' -import {Pressable, View} from 'react-native' +import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native' import {AppBskyEmbedExternal} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {HITSLOP_10} from '#/lib/constants' +import {isWeb} from '#/platform/detection' import {EmbedPlayerParams} from 'lib/strings/embed-player' import {useAutoplayDisabled} from 'state/preferences' import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' import {GifView} from '../../../../../modules/expo-bluesky-gif-view' import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' @@ -82,9 +86,11 @@ function PlaybackControls({ export function GifEmbed({ params, link, + hideAlt, }: { params: EmbedPlayerParams link: AppBskyEmbedExternal.ViewExternal + hideAlt?: boolean }) { const {_} = useLingui() const autoplayDisabled = useAutoplayDisabled() @@ -111,7 +117,8 @@ export function GifEmbed({ }, []) return ( - <View style={[a.rounded_sm, a.overflow_hidden, a.mt_sm]}> + <View + style={[a.rounded_sm, a.overflow_hidden, a.mt_sm, {maxWidth: '100%'}]}> <View style={[ a.rounded_sm, @@ -133,9 +140,69 @@ export function GifEmbed({ onPlayerStateChange={onPlayerStateChange} ref={playerRef} accessibilityHint={_(msg`Animated GIF`)} - accessibilityLabel={link.description.replace('ALT: ', '')} + accessibilityLabel={link.description.replace('Alt text: ', '')} /> + + {!hideAlt && link.description.startsWith('Alt text: ') && ( + <AltText text={link.description.replace('Alt text: ', '')} /> + )} </View> </View> ) } + +function AltText({text}: {text: string}) { + const control = Prompt.usePromptControl() + + const {_} = useLingui() + return ( + <> + <TouchableOpacity + testID="altTextButton" + accessibilityRole="button" + accessibilityLabel={_(msg`Show alt text`)} + accessibilityHint="" + hitSlop={HITSLOP_10} + onPress={control.open} + style={styles.altContainer}> + <Text style={styles.alt} accessible={false}> + <Trans>ALT</Trans> + </Text> + </TouchableOpacity> + + <Prompt.Outer control={control}> + <Prompt.TitleText> + <Trans>Alt Text</Trans> + </Prompt.TitleText> + <Prompt.DescriptionText>{text}</Prompt.DescriptionText> + <Prompt.Actions> + <Prompt.Action + onPress={control.close} + cta={_(msg`Close`)} + color="secondary" + /> + </Prompt.Actions> + </Prompt.Outer> + </> + ) +} + +const styles = StyleSheet.create({ + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + // Related to margin/gap hack. This keeps the alt label in the same position + // on all platforms + left: isWeb ? 8 : 5, + bottom: isWeb ? 8 : 5, + zIndex: 2, + }, + alt: { + color: 'white', + fontSize: 10, + fontWeight: 'bold', + }, +}) |