forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {TouchableOpacity, View} from 'react-native'
3import {msg, Plural, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5
6import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
7import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
8import {
9 type EmbedPlayerParams,
10 parseEmbedPlayerFromUrl,
11} from '#/lib/strings/embed-player'
12import {useResolveGifQuery} from '#/state/queries/resolve-link'
13import {type Gif} from '#/state/queries/tenor'
14import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
15import {atoms as a, useTheme} from '#/alf'
16import {Button, ButtonText} from '#/components/Button'
17import * as Dialog from '#/components/Dialog'
18import {type DialogControlProps} from '#/components/Dialog'
19import * as TextField from '#/components/forms/TextField'
20import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
21import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
22import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
23import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
24import {Text} from '#/components/Typography'
25import {IS_ANDROID} from '#/env'
26import {AltTextReminder} from './photos/Gallery'
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 <AltTextReminder />
105
106 <Dialog.Outer
107 control={control}
108 onClose={() => {
109 onSubmit(altTextDraft)
110 }}>
111 <Dialog.Handle />
112 <AltTextInner
113 vendorAltText={vendorAltText}
114 altText={altTextDraft}
115 onChange={setAltTextDraft}
116 thumb={thumb}
117 control={control}
118 params={params}
119 />
120 </Dialog.Outer>
121 </>
122 )
123}
124
125function AltTextInner({
126 vendorAltText,
127 altText,
128 onChange,
129 control,
130 params,
131 thumb,
132}: {
133 vendorAltText: string
134 altText: string
135 onChange: (text: string) => void
136 control: DialogControlProps
137 params: EmbedPlayerParams
138 thumb: string | undefined
139}) {
140 const t = useTheme()
141 const {_, i18n} = useLingui()
142
143 return (
144 <Dialog.ScrollableInner label={_(msg`Add alt text`)}>
145 <View style={a.flex_col_reverse}>
146 <View style={[a.mt_md, a.gap_md]}>
147 <View style={[a.gap_sm]}>
148 <View style={[a.relative]}>
149 <TextField.LabelText>
150 <Trans>Descriptive alt text</Trans>
151 </TextField.LabelText>
152 <TextField.Root>
153 <Dialog.Input
154 label={_(msg`Alt text`)}
155 placeholder={vendorAltText}
156 onChangeText={onChange}
157 defaultValue={altText}
158 multiline
159 numberOfLines={3}
160 autoFocus
161 onKeyPress={({nativeEvent}) => {
162 if (nativeEvent.key === 'Escape') {
163 control.close()
164 }
165 }}
166 />
167 </TextField.Root>
168 </View>
169
170 {altText.length > MAX_ALT_TEXT && (
171 <View style={[a.pb_sm, a.flex_row, a.gap_xs]}>
172 <CircleInfo fill={t.palette.negative_500} />
173 <Text
174 style={[
175 a.italic,
176 a.leading_snug,
177 t.atoms.text_contrast_medium,
178 ]}>
179 <Trans>
180 Alt text will be truncated.{' '}
181 <Plural
182 value={MAX_ALT_TEXT}
183 other={`Limit: ${i18n.number(MAX_ALT_TEXT)} characters.`}
184 />
185 </Trans>
186 </Text>
187 </View>
188 )}
189 </View>
190
191 <AltTextCounterWrapper altText={altText}>
192 <Button
193 label={_(msg`Save`)}
194 size="large"
195 color="primary"
196 variant="solid"
197 onPress={() => {
198 control.close()
199 }}
200 style={[a.flex_grow]}>
201 <ButtonText>
202 <Trans>Save</Trans>
203 </ButtonText>
204 </Button>
205 </AltTextCounterWrapper>
206 </View>
207 {/* below the text input to force tab order */}
208 <View>
209 <Text
210 style={[a.text_2xl, a.font_semi_bold, a.leading_tight, a.pb_sm]}>
211 <Trans>Add alt text</Trans>
212 </Text>
213 <View style={[a.align_center]}>
214 <GifEmbed
215 thumb={thumb}
216 altText={altText}
217 isPreferredAltText={true}
218 params={params}
219 hideAlt
220 style={[{height: 225}]}
221 />
222 </View>
223 </View>
224 </View>
225 <Dialog.Close />
226 {/* Maybe fix this later -h */}
227 {IS_ANDROID ? <View style={{height: 300}} /> : null}
228 </Dialog.ScrollableInner>
229 )
230}