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