Bluesky app fork with some witchin' additions 💫
at feat/tealfm 150 lines 4.4 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 {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 9import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 10import {atoms as a} 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 {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 enableSquareButtons = useEnableSquareButtons() 71 72 const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>( 73 'pending', 74 ) 75 const [isLoading, setIsLoading] = useState(false) 76 const [isActive, setIsActive] = useState(false) 77 const showSpinner = useThrottledValue(isActive && isLoading, 100) 78 79 const showOverlay = 80 !isActive || 81 isLoading || 82 (status === 'paused' && !isActive) || 83 status === 'pending' 84 85 if (!isActive && status !== 'pending') { 86 setStatus('pending') 87 } 88 89 return ( 90 <> 91 <VideoEmbedInnerNative 92 embed={embed} 93 setStatus={setStatus} 94 setIsLoading={setIsLoading} 95 setIsActive={setIsActive} 96 ref={ref} 97 /> 98 <ImageBackground 99 source={{uri: embed.thumbnail}} 100 accessibilityIgnoresInvertColors 101 style={[ 102 a.absolute, 103 a.inset_0, 104 { 105 backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here, 106 // the play button won't show up on the first render on android 🥴😮‍💨 107 display: showOverlay ? 'flex' : 'none', 108 }, 109 ]} 110 cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android 111 > 112 {showOverlay && ( 113 <Button 114 style={[a.flex_1, a.align_center, a.justify_center]} 115 onPress={() => { 116 ref.current?.togglePlayback() 117 }} 118 label={_(msg`Play video`)}> 119 {showSpinner ? ( 120 <View 121 style={[ 122 enableSquareButtons ? a.rounded_sm : a.rounded_full, 123 a.p_xs, 124 a.align_center, 125 a.justify_center, 126 ]}> 127 <ActivityIndicator size="large" color="white" /> 128 </View> 129 ) : ( 130 <PlayButtonIcon /> 131 )} 132 </Button> 133 )} 134 </ImageBackground> 135 </> 136 ) 137} 138 139function VideoError({retry}: {error: unknown; retry: () => void}) { 140 return ( 141 <VideoFallback.Container> 142 <VideoFallback.Text> 143 <Trans> 144 An error occurred while loading the video. Please try again later. 145 </Trans> 146 </VideoFallback.Text> 147 <VideoFallback.RetryButton onPress={retry} /> 148 </VideoFallback.Container> 149 ) 150}