diff options
Diffstat (limited to 'src/view/com/composer')
-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 |
4 files changed, 262 insertions, 37 deletions
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', |