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