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