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} 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 return (
73 <>
74 <TouchableOpacity
75 testID="altTextButton"
76 accessibilityRole="button"
77 accessibilityLabel={_(msg`Add alt text`)}
78 accessibilityHint=""
79 hitSlop={HITSLOP_10}
80 onPress={control.open}
81 style={[
82 a.absolute,
83 {top: 8, left: 8},
84 {borderRadius: 6},
85 a.pl_xs,
86 a.pr_sm,
87 a.py_2xs,
88 a.flex_row,
89 a.gap_xs,
90 a.align_center,
91 {backgroundColor: 'rgba(0, 0, 0, 0.75)'},
92 ]}>
93 {altText ? (
94 <Check size="xs" fill={t.palette.white} style={a.ml_xs} />
95 ) : (
96 <Plus size="sm" fill={t.palette.white} />
97 )}
98 <Text
99 style={[a.font_semi_bold, {color: t.palette.white}]}
100 accessible={false}>
101 <Trans>ALT</Trans>
102 </Text>
103 </TouchableOpacity>
104
105 <AltTextReminder />
106
107 <Dialog.Outer
108 control={control}
109 onClose={() => {
110 onSubmit(altTextDraft)
111 }}>
112 <Dialog.Handle />
113 <AltTextInner
114 vendorAltText={vendorAltText}
115 altText={altTextDraft}
116 onChange={setAltTextDraft}
117 thumb={thumb}
118 control={control}
119 params={params}
120 />
121 </Dialog.Outer>
122 </>
123 )
124}
125
126function AltTextInner({
127 vendorAltText,
128 altText,
129 onChange,
130 control,
131 params,
132 thumb,
133}: {
134 vendorAltText: string
135 altText: string
136 onChange: (text: string) => void
137 control: DialogControlProps
138 params: EmbedPlayerParams
139 thumb: string | undefined
140}) {
141 const t = useTheme()
142 const {_, i18n} = useLingui()
143
144 return (
145 <Dialog.ScrollableInner label={_(msg`Add alt text`)}>
146 <View style={a.flex_col_reverse}>
147 <View style={[a.mt_md, a.gap_md]}>
148 <View style={[a.gap_sm]}>
149 <View style={[a.relative]}>
150 <TextField.LabelText>
151 <Trans>Descriptive alt text</Trans>
152 </TextField.LabelText>
153 <TextField.Root>
154 <Dialog.Input
155 label={_(msg`Alt text`)}
156 placeholder={vendorAltText}
157 onChangeText={onChange}
158 defaultValue={altText}
159 multiline
160 numberOfLines={3}
161 autoFocus
162 onKeyPress={({nativeEvent}) => {
163 if (nativeEvent.key === 'Escape') {
164 control.close()
165 }
166 }}
167 />
168 </TextField.Root>
169 </View>
170
171 {altText.length > MAX_ALT_TEXT && (
172 <View style={[a.pb_sm, a.flex_row, a.gap_xs]}>
173 <CircleInfo fill={t.palette.negative_500} />
174 <Text
175 style={[
176 a.italic,
177 a.leading_snug,
178 t.atoms.text_contrast_medium,
179 ]}>
180 <Trans>
181 Alt text will be truncated.{' '}
182 <Plural
183 value={MAX_ALT_TEXT}
184 other={`Limit: ${i18n.number(MAX_ALT_TEXT)} characters.`}
185 />
186 </Trans>
187 </Text>
188 </View>
189 )}
190 </View>
191
192 <AltTextCounterWrapper altText={altText}>
193 <Button
194 label={_(msg`Save`)}
195 size="large"
196 color="primary"
197 variant="solid"
198 onPress={() => {
199 control.close()
200 }}
201 style={[a.flex_grow]}>
202 <ButtonText>
203 <Trans>Save</Trans>
204 </ButtonText>
205 </Button>
206 </AltTextCounterWrapper>
207 </View>
208 {/* below the text input to force tab order */}
209 <View>
210 <Text
211 style={[a.text_2xl, a.font_semi_bold, a.leading_tight, a.pb_sm]}>
212 <Trans>Add alt text</Trans>
213 </Text>
214 <View style={[a.align_center]}>
215 <GifEmbed
216 thumb={thumb}
217 altText={altText}
218 isPreferredAltText={true}
219 params={params}
220 hideAlt
221 style={[{height: 225}]}
222 />
223 </View>
224 </View>
225 </View>
226 <Dialog.Close />
227 {/* Maybe fix this later -h */}
228 {IS_ANDROID ? <View style={{height: 300}} /> : null}
229 </Dialog.ScrollableInner>
230 )
231}