mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at remove-hackfix 253 lines 7.4 kB view raw
1import { 2 createContext, 3 useCallback, 4 useContext, 5 useEffect, 6 useRef, 7 useState, 8} from 'react' 9import {View} from 'react-native' 10import {type AppBskyEmbedVideo} from '@atproto/api' 11import {msg} from '@lingui/macro' 12import {useLingui} from '@lingui/react' 13 14import {isFirefox} from '#/lib/browser' 15import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 16import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' 17import {atoms as a, useTheme} from '#/alf' 18import {useIsWithinMessage} from '#/components/dms/MessageContext' 19import {useFullscreen} from '#/components/hooks/useFullscreen' 20import { 21 HLSUnsupportedError, 22 VideoEmbedInnerWeb, 23 VideoNotFoundError, 24} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb' 25import {useActiveVideoWeb} from './ActiveVideoWebContext' 26import * as VideoFallback from './VideoEmbedInner/VideoFallback' 27 28export function VideoEmbed({ 29 embed, 30 crop, 31}: { 32 embed: AppBskyEmbedVideo.View 33 crop?: 'none' | 'square' | 'constrained' 34}) { 35 const t = useTheme() 36 const ref = useRef<HTMLDivElement>(null) 37 const {active, setActive, sendPosition, currentActiveView} = 38 useActiveVideoWeb() 39 const [onScreen, setOnScreen] = useState(false) 40 const [isFullscreen] = useFullscreen() 41 const lastKnownTime = useRef<number | undefined>(undefined) 42 43 useEffect(() => { 44 if (!ref.current) return 45 if (isFullscreen && !isFirefox) return 46 const observer = new IntersectionObserver( 47 entries => { 48 const entry = entries[0] 49 if (!entry) return 50 setOnScreen(entry.isIntersecting) 51 sendPosition( 52 entry.boundingClientRect.y + entry.boundingClientRect.height / 2, 53 ) 54 }, 55 {threshold: 0.5}, 56 ) 57 observer.observe(ref.current) 58 return () => observer.disconnect() 59 }, [sendPosition, isFullscreen]) 60 61 const [key, setKey] = useState(0) 62 const renderError = useCallback( 63 (error: unknown) => ( 64 <VideoError error={error} retry={() => setKey(key + 1)} /> 65 ), 66 [key], 67 ) 68 69 let aspectRatio: number | undefined 70 const dims = embed.aspectRatio 71 if (dims) { 72 aspectRatio = dims.width / dims.height 73 if (Number.isNaN(aspectRatio)) { 74 aspectRatio = undefined 75 } 76 } 77 78 let constrained: number | undefined 79 let max: number | undefined 80 if (aspectRatio !== undefined) { 81 const ratio = 1 / 2 // max of 1:2 ratio in feeds 82 constrained = Math.max(aspectRatio, ratio) 83 max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread 84 } 85 const cropDisabled = crop === 'none' 86 87 const contents = ( 88 <div 89 ref={ref} 90 style={{ 91 display: 'flex', 92 flex: 1, 93 cursor: 'default', 94 backgroundImage: `url(${embed.thumbnail})`, 95 backgroundSize: 'cover', 96 }} 97 onClick={evt => evt.stopPropagation()}> 98 <ErrorBoundary renderError={renderError} key={key}> 99 <OnlyNearScreen> 100 <VideoEmbedInnerWeb 101 embed={embed} 102 active={active} 103 setActive={setActive} 104 onScreen={onScreen} 105 lastKnownTime={lastKnownTime} 106 /> 107 </OnlyNearScreen> 108 </ErrorBoundary> 109 </div> 110 ) 111 112 return ( 113 <View style={[a.pt_xs]}> 114 <ViewportObserver 115 sendPosition={sendPosition} 116 isAnyViewActive={currentActiveView !== null}> 117 {cropDisabled ? ( 118 <View 119 style={[ 120 a.w_full, 121 a.overflow_hidden, 122 {aspectRatio: max ?? 1}, 123 a.rounded_md, 124 a.overflow_hidden, 125 t.atoms.bg_contrast_25, 126 ]}> 127 {contents} 128 </View> 129 ) : ( 130 <ConstrainedImage 131 fullBleed={crop === 'square'} 132 aspectRatio={constrained || 1} 133 // slightly smaller max height than images 134 // images use 16 / 9, for reference 135 minMobileAspectRatio={14 / 9}> 136 {contents} 137 </ConstrainedImage> 138 )} 139 </ViewportObserver> 140 </View> 141 ) 142} 143 144const NearScreenContext = createContext(false) 145NearScreenContext.displayName = 'VideoNearScreenContext' 146 147/** 148 * Renders a 100vh tall div and watches it with an IntersectionObserver to 149 * send the position of the div when it's near the screen. 150 * 151 * IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container. 152 */ 153function ViewportObserver({ 154 children, 155 sendPosition, 156 isAnyViewActive, 157}: { 158 children: React.ReactNode 159 sendPosition: (position: number) => void 160 isAnyViewActive: boolean 161}) { 162 const ref = useRef<HTMLDivElement>(null) 163 const [nearScreen, setNearScreen] = useState(false) 164 const [isFullscreen] = useFullscreen() 165 const isWithinMessage = useIsWithinMessage() 166 167 // Send position when scrolling. This is done with an IntersectionObserver 168 // observing a div of 100vh height 169 useEffect(() => { 170 if (!ref.current) return 171 if (isFullscreen && !isFirefox) return 172 const observer = new IntersectionObserver( 173 entries => { 174 const entry = entries[0] 175 if (!entry) return 176 const position = 177 entry.boundingClientRect.y + entry.boundingClientRect.height / 2 178 sendPosition(position) 179 setNearScreen(entry.isIntersecting) 180 }, 181 {threshold: Array.from({length: 101}, (_, i) => i / 100)}, 182 ) 183 observer.observe(ref.current) 184 return () => observer.disconnect() 185 }, [sendPosition, isFullscreen]) 186 187 // In case scrolling hasn't started yet, send up the position 188 useEffect(() => { 189 if (ref.current && !isAnyViewActive) { 190 const rect = ref.current.getBoundingClientRect() 191 const position = rect.y + rect.height / 2 192 sendPosition(position) 193 } 194 }, [isAnyViewActive, sendPosition]) 195 196 return ( 197 <View style={[a.flex_1, a.flex_row]}> 198 <NearScreenContext.Provider value={nearScreen}> 199 {children} 200 </NearScreenContext.Provider> 201 <div 202 ref={ref} 203 style={{ 204 // Don't escape bounds when in a message 205 ...(isWithinMessage 206 ? {top: 0, height: '100%'} 207 : {top: 'calc(50% - 50vh)', height: '100vh'}), 208 position: 'absolute', 209 left: '50%', 210 width: 1, 211 pointerEvents: 'none', 212 }} 213 /> 214 </View> 215 ) 216} 217 218/** 219 * Awkward data flow here, but we need to hide the video when it's not near the screen. 220 * But also, ViewportObserver _must_ not be within a `overflow: hidden` container. 221 * So we put it at the top level of the component tree here, then hide the children of 222 * the auto-resizing container. 223 */ 224export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => { 225 const nearScreen = useContext(NearScreenContext) 226 227 return nearScreen ? children : null 228} 229 230function VideoError({error, retry}: {error: unknown; retry: () => void}) { 231 const {_} = useLingui() 232 233 let showRetryButton = true 234 let text = null 235 236 if (error instanceof VideoNotFoundError) { 237 text = _(msg`Video not found.`) 238 } else if (error instanceof HLSUnsupportedError) { 239 showRetryButton = false 240 text = _( 241 msg`Your browser does not support the video format. Please try a different browser.`, 242 ) 243 } else { 244 text = _(msg`An error occurred while loading the video. Please try again.`) 245 } 246 247 return ( 248 <VideoFallback.Container> 249 <VideoFallback.Text>{text}</VideoFallback.Text> 250 {showRetryButton && <VideoFallback.RetryButton onPress={retry} />} 251 </VideoFallback.Container> 252 ) 253}