Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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/core/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 <Pressable
98 style={[
99 {height: 300},
100 a.w_full,
101 a.overflow_hidden,
102 {
103 borderBottomLeftRadius: 0,
104 borderBottomRightRadius: 0,
105 },
106 ]}
107 onPress={onPlayPress}
108 accessibilityRole="button"
109 accessibilityHint={_(msg`Plays the GIF`)}
110 accessibilityLabel={_(msg`Play ${link.title}`)}>
111 <Image
112 source={{
113 uri:
114 !isPrefetched || (IS_WEB && !isAnimating)
115 ? link.thumb
116 : params.playerUri,
117 }} // Web uses the thumb to control playback
118 style={{flex: 1}}
119 ref={imageRef}
120 autoplay={isAnimating}
121 contentFit="contain"
122 accessibilityIgnoresInvertColors
123 accessibilityLabel={link.title}
124 accessibilityHint={link.title}
125 cachePolicy={IS_IOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
126 />
127
128 {(!isPrefetched || !isAnimating) && (
129 <Fill style={[a.align_center, a.justify_center]}>
130 <Fill
131 style={[
132 t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
133 {
134 opacity: 0.3,
135 },
136 ]}
137 />
138
139 {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
140 <PlayButtonIcon />
141 ) : (
142 // Activity indicator while gif loads
143 <ActivityIndicator size="large" color="white" />
144 )}
145 </Fill>
146 )}
147 </Pressable>
148 </>
149 )
150}