Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 150 lines 4.4 kB view raw
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}