forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 ActivityIndicator,
4 type GestureResponderEvent,
5 Pressable,
6} from 'react-native'
7import {Image} from 'expo-image'
8import {type AppBskyEmbedExternal} from '@atproto/api'
9import {msg} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11
12import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
13import {useExternalEmbedsPrefs} from '#/state/preferences'
14import {atoms as a, useTheme} from '#/alf'
15import {useDialogControl} from '#/components/Dialog'
16import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
17import {Fill} from '#/components/Fill'
18import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
19import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
20
21export function ExternalGif({
22 link,
23 params,
24}: {
25 link: AppBskyEmbedExternal.ViewExternal
26 params: EmbedPlayerParams
27}) {
28 const t = useTheme()
29 const externalEmbedsPrefs = useExternalEmbedsPrefs()
30 const {_} = useLingui()
31 const consentDialogControl = useDialogControl()
32
33 // Tracking if the placer has been activated
34 const [isPlayerActive, setIsPlayerActive] = React.useState(false)
35 // Tracking whether the gif has been loaded yet
36 const [isPrefetched, setIsPrefetched] = React.useState(false)
37 // Tracking whether the image is animating
38 const [isAnimating, setIsAnimating] = React.useState(true)
39
40 // Used for controlling animation
41 const imageRef = React.useRef<Image>(null)
42
43 const load = React.useCallback(() => {
44 setIsPlayerActive(true)
45 Image.prefetch(params.playerUri).then(() => {
46 // Replace the image once it's fetched
47 setIsPrefetched(true)
48 })
49 }, [params.playerUri])
50
51 const onPlayPress = React.useCallback(
52 (event: GestureResponderEvent) => {
53 // Don't propagate on web
54 event.preventDefault()
55
56 // Show consent if this is the first load
57 if (externalEmbedsPrefs?.[params.source] === undefined) {
58 consentDialogControl.open()
59 return
60 }
61 // If the player isn't active, we want to activate it and prefetch the gif
62 if (!isPlayerActive) {
63 load()
64 return
65 }
66 // Control animation on native
67 setIsAnimating(prev => {
68 if (prev) {
69 if (IS_NATIVE) {
70 imageRef.current?.stopAnimating()
71 }
72 return false
73 } else {
74 if (IS_NATIVE) {
75 imageRef.current?.startAnimating()
76 }
77 return true
78 }
79 })
80 },
81 [
82 consentDialogControl,
83 externalEmbedsPrefs,
84 isPlayerActive,
85 load,
86 params.source,
87 ],
88 )
89
90 return (
91 <>
92 <EmbedConsentDialog
93 control={consentDialogControl}
94 source={params.source}
95 onAccept={load}
96 />
97
98 <Pressable
99 style={[
100 {height: 300},
101 a.w_full,
102 a.overflow_hidden,
103 {
104 borderBottomLeftRadius: 0,
105 borderBottomRightRadius: 0,
106 },
107 ]}
108 onPress={onPlayPress}
109 accessibilityRole="button"
110 accessibilityHint={_(msg`Plays the GIF`)}
111 accessibilityLabel={_(msg`Play ${link.title}`)}>
112 <Image
113 source={{
114 uri:
115 !isPrefetched || (IS_WEB && !isAnimating)
116 ? link.thumb
117 : params.playerUri,
118 }} // Web uses the thumb to control playback
119 style={{flex: 1}}
120 ref={imageRef}
121 autoplay={isAnimating}
122 contentFit="contain"
123 accessibilityIgnoresInvertColors
124 accessibilityLabel={link.title}
125 accessibilityHint={link.title}
126 cachePolicy={IS_IOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
127 />
128
129 {(!isPrefetched || !isAnimating) && (
130 <Fill style={[a.align_center, a.justify_center]}>
131 <Fill
132 style={[
133 t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
134 {
135 opacity: 0.3,
136 },
137 ]}
138 />
139
140 {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
141 <PlayButtonIcon />
142 ) : (
143 // Activity indicator while gif loads
144 <ActivityIndicator size="large" color="white" />
145 )}
146 </Fill>
147 )}
148 </Pressable>
149 </>
150 )
151}