measure card width, keep thumbnail as video bg

This commit is contained in:
vineyardbovines
2026-07-16 14:48:47 -04:00
parent 35ee7d5426
commit 6cac311d80
@@ -6,7 +6,7 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api' import {type AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -16,7 +16,6 @@ import {atoms as a, useTheme} from '#/alf'
import {useIsWithinMessage} from '#/components/dms/MessageContext' import {useIsWithinMessage} from '#/components/dms/MessageContext'
import {useFullscreen} from '#/components/hooks/useFullscreen' import {useFullscreen} from '#/components/hooks/useFullscreen'
import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {CENTER_COLUMN_WIDTH} from '#/components/Layout'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import { import {
HLSUnsupportedError, HLSUnsupportedError,
@@ -97,16 +96,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
constrained = Math.max(aspectRatio, ratio) constrained = Math.max(aspectRatio, ratio)
} }
const {width: windowWidth} = useWindowDimensions() const [containerWidth, setContainerWidth] = useState(0)
/*
* Estimated rather than measured with onLayout - a measurement lands after
* first paint, which flashed a full-width card for a frame before snapping
* narrow. The post column is capped at CENTER_COLUMN_WIDTH, and feed
* padding plus the avatar gutter inset the embed by roughly 100px.
*/
const estimatedContainerWidth =
Math.min(windowWidth, CENTER_COLUMN_WIDTH) - 100
/* /*
* Portrait videos render at their own ratio instead of pillarboxed, but * Portrait videos render at their own ratio instead of pillarboxed, but
@@ -115,7 +105,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
* ratio can't be fit, so both keep the full-width pillarbox - a narrow * ratio can't be fit, so both keep the full-width pillarbox - a narrow
* card with black slices down the sides looks broken (see #9371). * card with black slices down the sides looks broken (see #9371).
*/ */
const cardWidth = estimatedContainerWidth * Math.min(aspectRatio ?? 1, 1) const cardWidth = containerWidth * Math.min(aspectRatio ?? 1, 1)
const fullBleed = const fullBleed =
aspectRatio === undefined || aspectRatio === undefined ||
aspectRatio < 1 / 2 || aspectRatio < 1 / 2 ||
@@ -130,34 +120,42 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
cursor: 'default', cursor: 'default',
position: 'relative', position: 'relative',
backgroundColor: t.palette.black, backgroundColor: t.palette.black,
backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}} }}
onClick={evt => evt.stopPropagation()}> onClick={evt => evt.stopPropagation()}>
{/* blurred backdrop fills the bars when the video is boxed */}
{fullBleed && embed.thumbnail && ( {fullBleed && embed.thumbnail && (
<div <>
aria-hidden {/* blurred backdrop fills the bars when the video is boxed */}
style={{ <div
position: 'absolute', aria-hidden
inset: 0, style={{
backgroundImage: `url(${embed.thumbnail})`, position: 'absolute',
backgroundSize: 'cover', inset: 0,
backgroundPosition: 'center', backgroundImage: `url(${embed.thumbnail})`,
filter: 'blur(32px)', backgroundSize: 'cover',
// hide the transparent fade the blur creates at the edges backgroundPosition: 'center',
transform: 'scale(1.2)', filter: 'blur(32px)',
}} // hide the transparent fade the blur creates at the edges
/> transform: 'scale(1.2)',
}}
/>
{/* redraw the sharp thumbnail above the blur */}
<div
aria-hidden
style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
</>
)} )}
<div
style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
<ErrorBoundary renderError={renderError} key={key}> <ErrorBoundary renderError={renderError} key={key}>
<OnlyNearScreen> <OnlyNearScreen>
<VideoEmbedInnerWeb <VideoEmbedInnerWeb
@@ -173,7 +171,9 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
) )
return ( return (
<View style={[a.pt_xs]}> <View
style={[a.pt_xs]}
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}>
<ViewportObserver <ViewportObserver
sendPosition={isGif ? noop : sendPosition} sendPosition={isGif ? noop : sendPosition}
isAnyViewActive={currentActiveView !== null}> isAnyViewActive={currentActiveView !== null}>