From c33c3b7d1e44037d3370da0ac60926293bf8a158 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 6 May 2024 17:28:38 +0100 Subject: [PATCH] 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 --- src/components/Prompt.tsx | 4 +- src/components/dialogs/MutedWords.tsx | 4 +- src/view/com/composer/Composer.tsx | 46 ++++- src/view/com/composer/ExternalEmbed.tsx | 9 +- src/view/com/composer/GifAltText.tsx | 177 ++++++++++++++++++ src/view/com/composer/photos/Gallery.tsx | 67 ++++--- src/view/com/util/images/Gallery.tsx | 5 +- .../util/post-embeds/ExternalLinkEmbed.tsx | 4 +- src/view/com/util/post-embeds/GifEmbed.tsx | 75 +++++++- 9 files changed, 344 insertions(+), 47 deletions(-) create mode 100644 src/view/com/composer/GifAltText.tsx diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 0a171674..327bbbaa 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -43,7 +43,9 @@ export function Outer({ + style={[ + gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> {children} diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 0eced11e..53426342 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -37,12 +37,12 @@ export function MutedWordsDialog() { return ( - + ) } -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 0ac4ac56..f472bb2e 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 ( {gallery.isEmpty && extLink && ( - { - setExtLink(undefined) - setExtGif(undefined) - }} - /> + + { + setExtLink(undefined) + setExtGif(undefined) + }} + /> + + )} {quote ? ( diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 321e29b3..b81065e9 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -46,7 +46,12 @@ export const ExternalEmbed = ({ : undefined return ( - + {link.isLoading ? ( @@ -62,7 +67,7 @@ export const ExternalEmbed = ({ ) : linkInfo ? ( - + ) : null} 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 ( + <> + + {link.description ? ( + + ) : ( + + )} + + ALT + + + + + + + + + + + ) +} + +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 ( + + + + + + Descriptive alt text + + + + setAltText(enforceLen(text, MAX_ALT_TEXT)) + } + value={altText} + multiline + numberOfLines={3} + autoFocus + /> + + + + + {/* below the text input to force tab order */} + + + Add ALT text + + + + + + + + + ) +} diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 69c8debb..7ff1b7b9 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]}> - - ALT - {image.altText.length > 0 ? ( - ) : undefined} + ) : ( + + )} + + ALT + ))} - - - - - - - Alt text describes images for blind and low-vision users, and helps - give context to everyone. - - - + ) : null }) +export function AltTextReminder() { + const t = useTheme() + return ( + + + + + + + Alt text describes images for blind and low-vision users, and helps + give context to everyone. + + + + ) +} + 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 7de3b093..f6d2c7a1 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 1fe75c44..b84c04b8 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 + hideAlt?: boolean }) => { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() @@ -37,7 +39,7 @@ export const ExternalLinkEmbed = ({ }, [link.uri, externalEmbedPrefs]) if (embedPlayerParams?.source === 'tenor') { - return + return } return ( diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index 5d21ce06..dde6efe3 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 ( - + + + {!hideAlt && link.description.startsWith('Alt text: ') && ( + + )} ) } + +function AltText({text}: {text: string}) { + const control = Prompt.usePromptControl() + + const {_} = useLingui() + return ( + <> + + + ALT + + + + + + Alt Text + + {text} + + + + + + ) +} + +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', + }, +})