mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 Pressable,
4 type StyleProp,
5 StyleSheet,
6 TouchableOpacity,
7 View,
8 type ViewStyle,
9} from 'react-native'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {HITSLOP_20} from '#/lib/constants'
14import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
15import {isWeb} from '#/platform/detection'
16import {useAutoplayDisabled} from '#/state/preferences'
17import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
18import {atoms as a, useTheme} from '#/alf'
19import {Fill} from '#/components/Fill'
20import {Loader} from '#/components/Loader'
21import * as Prompt from '#/components/Prompt'
22import {Text} from '#/components/Typography'
23import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
24import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
25import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
26
27function PlaybackControls({
28 onPress,
29 isPlaying,
30 isLoaded,
31}: {
32 onPress: () => void
33 isPlaying: boolean
34 isLoaded: boolean
35}) {
36 const {_} = useLingui()
37 const t = useTheme()
38
39 return (
40 <Pressable
41 accessibilityRole="button"
42 accessibilityHint={_(msg`Plays or pauses the GIF`)}
43 accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
44 style={[
45 a.absolute,
46 a.align_center,
47 a.justify_center,
48 !isLoaded && a.border,
49 t.atoms.border_contrast_medium,
50 a.inset_0,
51 a.w_full,
52 a.h_full,
53 {
54 zIndex: 2,
55 backgroundColor: !isLoaded
56 ? t.atoms.bg_contrast_25.backgroundColor
57 : undefined,
58 },
59 ]}
60 onPress={onPress}>
61 {!isLoaded ? (
62 <View>
63 <View style={[a.align_center, a.justify_center]}>
64 <Loader size="xl" />
65 </View>
66 </View>
67 ) : !isPlaying ? (
68 <PlayButtonIcon />
69 ) : undefined}
70 </Pressable>
71 )
72}
73
74export function GifEmbed({
75 params,
76 thumb,
77 altText,
78 isPreferredAltText,
79 hideAlt,
80 style = {width: '100%'},
81}: {
82 params: EmbedPlayerParams
83 thumb: string | undefined
84 altText: string
85 isPreferredAltText: boolean
86 hideAlt?: boolean
87 style?: StyleProp<ViewStyle>
88}) {
89 const t = useTheme()
90 const {_} = useLingui()
91 const autoplayDisabled = useAutoplayDisabled()
92
93 const playerRef = React.useRef<GifView>(null)
94
95 const [playerState, setPlayerState] = React.useState<{
96 isPlaying: boolean
97 isLoaded: boolean
98 }>({
99 isPlaying: !autoplayDisabled,
100 isLoaded: false,
101 })
102
103 const onPlayerStateChange = React.useCallback(
104 (e: GifViewStateChangeEvent) => {
105 setPlayerState(e.nativeEvent)
106 },
107 [],
108 )
109
110 const onPress = React.useCallback(() => {
111 playerRef.current?.toggleAsync()
112 }, [])
113
114 return (
115 <View
116 style={[
117 a.rounded_md,
118 a.overflow_hidden,
119 a.border,
120 t.atoms.border_contrast_low,
121 {aspectRatio: params.dimensions!.width / params.dimensions!.height},
122 style,
123 ]}>
124 <View
125 style={[
126 a.absolute,
127 /*
128 * Aspect ratio was being clipped weirdly on web -esb
129 */
130 {
131 top: -2,
132 bottom: -2,
133 left: -2,
134 right: -2,
135 },
136 ]}>
137 <PlaybackControls
138 onPress={onPress}
139 isPlaying={playerState.isPlaying}
140 isLoaded={playerState.isLoaded}
141 />
142 <GifView
143 source={params.playerUri}
144 placeholderSource={thumb}
145 style={[a.flex_1]}
146 autoplay={!autoplayDisabled}
147 onPlayerStateChange={onPlayerStateChange}
148 ref={playerRef}
149 accessibilityHint={_(msg`Animated GIF`)}
150 accessibilityLabel={altText}
151 />
152 {!playerState.isPlaying && (
153 <Fill
154 style={[
155 t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
156 {
157 opacity: 0.3,
158 },
159 ]}
160 />
161 )}
162 {!hideAlt && isPreferredAltText && <AltText text={altText} />}
163 </View>
164 </View>
165 )
166}
167
168function AltText({text}: {text: string}) {
169 const control = Prompt.usePromptControl()
170 const largeAltBadge = useLargeAltBadgeEnabled()
171
172 const {_} = useLingui()
173 return (
174 <>
175 <TouchableOpacity
176 testID="altTextButton"
177 accessibilityRole="button"
178 accessibilityLabel={_(msg`Show alt text`)}
179 accessibilityHint=""
180 hitSlop={HITSLOP_20}
181 onPress={control.open}
182 style={styles.altContainer}>
183 <Text
184 style={[styles.alt, largeAltBadge && a.text_xs]}
185 accessible={false}>
186 <Trans>ALT</Trans>
187 </Text>
188 </TouchableOpacity>
189 <Prompt.Outer control={control}>
190 <Prompt.TitleText>
191 <Trans>Alt Text</Trans>
192 </Prompt.TitleText>
193 <Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
194 <Prompt.Actions>
195 <Prompt.Action
196 onPress={() => control.close()}
197 cta={_(msg`Close`)}
198 color="secondary"
199 />
200 </Prompt.Actions>
201 </Prompt.Outer>
202 </>
203 )
204}
205
206const styles = StyleSheet.create({
207 altContainer: {
208 backgroundColor: 'rgba(0, 0, 0, 0.75)',
209 borderRadius: 6,
210 paddingHorizontal: isWeb ? 8 : 6,
211 paddingVertical: isWeb ? 6 : 3,
212 position: 'absolute',
213 // Related to margin/gap hack. This keeps the alt label in the same position
214 // on all platforms
215 right: isWeb ? 8 : 5,
216 bottom: isWeb ? 8 : 5,
217 zIndex: 2,
218 },
219 alt: {
220 color: 'white',
221 fontSize: isWeb ? 10 : 7,
222 fontWeight: '600',
223 },
224})