Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
at main 156 lines 4.7 kB view raw
1import {useCallback, useRef, useState} from 'react' 2import {ActivityIndicator, View} from 'react-native' 3import {ImageBackground} from 'expo-image' 4import {type AppBskyEmbedVideo} from '@atproto/api' 5import {msg} from '@lingui/core/macro' 6import {useLingui} from '@lingui/react' 7import {Trans} from '@lingui/react/macro' 8 9import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 10import {atoms as a, platform} from '#/alf' 11import {Button} from '#/components/Button' 12import {useThrottledValue} from '#/components/hooks/useThrottledValue' 13import {ConstrainedImage} from '#/components/images/AutoSizedImage' 14import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 15import {GifPresentationControls} from './GifPresentationControls' 16import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' 17import * as VideoFallback from './VideoEmbedInner/VideoFallback' 18 19interface Props { 20 embed: AppBskyEmbedVideo.View 21} 22 23export function VideoEmbed({embed}: Props) { 24 const [key, setKey] = useState(0) 25 26 const renderError = useCallback( 27 (error: unknown) => ( 28 <VideoError error={error} retry={() => setKey(key + 1)} /> 29 ), 30 [key], 31 ) 32 33 let aspectRatio: number | undefined 34 const dims = embed.aspectRatio 35 if (dims) { 36 aspectRatio = dims.width / dims.height 37 if (Number.isNaN(aspectRatio)) { 38 aspectRatio = undefined 39 } 40 } 41 42 let constrained: number | undefined 43 if (aspectRatio !== undefined) { 44 const ratio = 1 / 2 // max of 1:2 ratio in feeds 45 constrained = Math.max(aspectRatio, ratio) 46 } 47 48 const contents = ( 49 <ErrorBoundary renderError={renderError} key={key}> 50 <InnerWrapper embed={embed} /> 51 </ErrorBoundary> 52 ) 53 54 return ( 55 <View style={[a.pt_xs]}> 56 <ConstrainedImage 57 aspectRatio={constrained || 1} 58 // slightly smaller max height than images 59 // images use 16 / 9, for reference 60 minMobileAspectRatio={14 / 9}> 61 {contents} 62 </ConstrainedImage> 63 </View> 64 ) 65} 66 67function InnerWrapper({embed}: Props) { 68 const {_} = useLingui() 69 const ref = useRef<{togglePlayback: () => void}>(null) 70 71 const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>( 72 'pending', 73 ) 74 const [isLoading, setIsLoading] = useState(false) 75 const [isActive, setIsActive] = useState(false) 76 const showSpinner = useThrottledValue(isActive && isLoading, 100) 77 78 const showOverlay = 79 !isActive || 80 isLoading || 81 (status === 'paused' && !isActive) || 82 status === 'pending' 83 84 if (!isActive && status !== 'pending') { 85 setStatus('pending') 86 } 87 88 return ( 89 <> 90 <VideoEmbedInnerNative 91 embed={embed} 92 setStatus={setStatus} 93 setIsLoading={setIsLoading} 94 setIsActive={setIsActive} 95 ref={ref} 96 /> 97 <ImageBackground 98 source={{uri: embed.thumbnail}} 99 accessibilityIgnoresInvertColors 100 style={[ 101 a.absolute, 102 a.inset_0, 103 { 104 backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here, 105 // the play button won't show up on the first render on android 🥴😮‍💨 106 }, 107 platform({ 108 android: {display: showOverlay ? 'flex' : 'none'}, 109 ios: {zIndex: showOverlay ? 1 : -1}, 110 }), 111 ]} 112 cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android 113 > 114 {showOverlay && 115 (embed.presentation === 'gif' ? ( 116 <GifPresentationControls 117 isPlaying={false} 118 isLoading={showSpinner} 119 onPress={() => { 120 ref.current?.togglePlayback() 121 }} 122 altText={embed.alt} 123 /> 124 ) : ( 125 <Button 126 style={[a.flex_1, a.align_center, a.justify_center]} 127 onPress={() => { 128 ref.current?.togglePlayback() 129 }} 130 label={_(msg`Play video`)}> 131 {showSpinner ? ( 132 <View style={[a.align_center, a.justify_center]}> 133 <ActivityIndicator size="large" color="white" /> 134 </View> 135 ) : ( 136 <PlayButtonIcon /> 137 )} 138 </Button> 139 ))} 140 </ImageBackground> 141 </> 142 ) 143} 144 145function VideoError({retry}: {error: unknown; retry: () => void}) { 146 return ( 147 <VideoFallback.Container> 148 <VideoFallback.Text> 149 <Trans> 150 An error occurred while loading the video. Please try again later. 151 </Trans> 152 </VideoFallback.Text> 153 <VideoFallback.RetryButton onPress={retry} /> 154 </VideoFallback.Container> 155 ) 156}