this repo has no description
1import {useState} from 'react'
2import {TouchableOpacity, View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Plural, Trans} from '@lingui/react/macro'
6
7import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
8import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
9import {
10 type EmbedPlayerParams,
11 parseEmbedPlayerFromUrl,
12} from '#/lib/strings/embed-player'
13import {useResolveGifQuery} from '#/state/queries/resolve-link'
14import {type Gif} from '#/state/queries/tenor'
15import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
16import {atoms as a, useTheme} from '#/alf'
17import {Admonition} from '#/components/Admonition'
18import {Button, ButtonText} from '#/components/Button'
19import * as Dialog from '#/components/Dialog'
20import {type DialogControlProps} from '#/components/Dialog'
21import * as TextField from '#/components/forms/TextField'
22import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
23import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
24import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
25import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
26import {Text} from '#/components/Typography'
27
28export function GifAltTextDialog({
29 gif,
30 altText,
31 onSubmit,
32}: {
33 gif: Gif
34 altText: string
35 onSubmit: (alt: string) => void
36}) {
37 const {data} = useResolveGifQuery(gif)
38 const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt
39 const params = data ? parseEmbedPlayerFromUrl(data.uri) : undefined
40 if (!data || !params) {
41 return null
42 }
43 return (
44 <GifAltTextDialogLoaded
45 altText={altText}
46 vendorAltText={vendorAltText}
47 thumb={data.thumb?.source.path}
48 params={params}
49 onSubmit={onSubmit}
50 />
51 )
52}
53
54export function GifAltTextDialogLoaded({
55 vendorAltText,
56 altText,
57 onSubmit,
58 params,
59 thumb,
60}: {
61 vendorAltText: string
62 altText: string
63 onSubmit: (alt: string) => void
64 params: EmbedPlayerParams
65 thumb: string | undefined
66}) {
67 const control = Dialog.useDialogControl()
68 const {_} = useLingui()
69 const t = useTheme()
70 const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
71 return (
72 <>
73 <TouchableOpacity
74 testID="altTextButton"
75 accessibilityRole="button"
76 accessibilityLabel={_(msg`Add alt text`)}
77 accessibilityHint=""
78 hitSlop={HITSLOP_10}
79 onPress={control.open}
80 style={[
81 a.absolute,
82 {top: 8, left: 8},
83 {borderRadius: 6},
84 a.pl_xs,
85 a.pr_sm,
86 a.py_2xs,
87 a.flex_row,
88 a.gap_xs,
89 a.align_center,
90 {backgroundColor: 'rgba(0, 0, 0, 0.75)'},
91 ]}>
92 {altText ? (
93 <Check size="xs" fill={t.palette.white} style={a.ml_xs} />
94 ) : (
95 <Plus size="sm" fill={t.palette.white} />
96 )}
97 <Text
98 style={[a.font_semi_bold, {color: t.palette.white}]}
99 accessible={false}>
100 <Trans>ALT</Trans>
101 </Text>
102 </TouchableOpacity>
103
104 <Admonition type="tip" style={[a.mt_sm]}>
105 <Trans>
106 Alt text describes images for blind and low-vision users, and helps
107 give context to everyone.
108 </Trans>
109 </Admonition>
110
111 <Dialog.Outer
112 control={control}
113 onClose={() => {
114 onSubmit(altTextDraft)
115 }}
116 nativeOptions={{fullHeight: true}}>
117 <Dialog.Handle />
118 <AltTextInner
119 vendorAltText={vendorAltText}
120 altText={altTextDraft}
121 onChange={setAltTextDraft}
122 thumb={thumb}
123 control={control}
124 params={params}
125 />
126 </Dialog.Outer>
127 </>
128 )
129}
130
131function AltTextInner({
132 vendorAltText,
133 altText,
134 onChange,
135 control,
136 params,
137 thumb,
138}: {
139 vendorAltText: string
140 altText: string
141 onChange: (text: string) => void
142 control: DialogControlProps
143 params: EmbedPlayerParams
144 thumb: string | undefined
145}) {
146 const t = useTheme()
147 const {_, i18n} = useLingui()
148
149 return (
150 <Dialog.ScrollableInner label={_(msg`Add alt text`)}>
151 <View style={a.flex_col_reverse}>
152 <View style={[a.mt_md, a.gap_md]}>
153 <View style={[a.gap_sm]}>
154 <View style={[a.relative]}>
155 <TextField.LabelText>
156 <Trans>Descriptive alt text</Trans>
157 </TextField.LabelText>
158 <TextField.Root>
159 <Dialog.Input
160 label={_(msg`Alt text`)}
161 placeholder={vendorAltText}
162 onChangeText={onChange}
163 defaultValue={altText}
164 multiline
165 numberOfLines={3}
166 autoFocus
167 onKeyPress={({nativeEvent}) => {
168 if (nativeEvent.key === 'Escape') {
169 control.close()
170 }
171 }}
172 />
173 </TextField.Root>
174 </View>
175
176 {altText.length > MAX_ALT_TEXT && (
177 <View style={[a.pb_sm, a.flex_row, a.gap_xs]}>
178 <CircleInfo fill={t.palette.negative_500} />
179 <Text
180 style={[
181 a.italic,
182 a.leading_snug,
183 t.atoms.text_contrast_medium,
184 ]}>
185 <Trans>
186 Alt text will be truncated.{' '}
187 <Plural
188 value={MAX_ALT_TEXT}
189 other={`Limit: ${i18n.number(MAX_ALT_TEXT)} characters.`}
190 />
191 </Trans>
192 </Text>
193 </View>
194 )}
195 </View>
196
197 <AltTextCounterWrapper altText={altText}>
198 <Button
199 label={_(msg`Save`)}
200 size="large"
201 color="primary"
202 variant="solid"
203 onPress={() => {
204 control.close()
205 }}
206 style={[a.flex_grow]}>
207 <ButtonText>
208 <Trans>Save</Trans>
209 </ButtonText>
210 </Button>
211 </AltTextCounterWrapper>
212 </View>
213 {/* below the text input to force tab order */}
214 <View>
215 <Text
216 style={[a.text_2xl, a.font_semi_bold, a.leading_tight, a.pb_sm]}>
217 <Trans>Add alt text</Trans>
218 </Text>
219 <View style={[a.align_center]}>
220 <GifEmbed
221 thumb={thumb}
222 altText={altText}
223 isPreferredAltText={true}
224 params={params}
225 hideAlt
226 style={[{height: 225}]}
227 />
228 </View>
229 </View>
230 </View>
231 <Dialog.Close />
232 </Dialog.ScrollableInner>
233 )
234}