Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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