Align composer video preview with in-feed embed (#10447)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-05-08 18:37:07 +01:00
committed by GitHub
parent 92199774d5
commit 92fdb6630d
3 changed files with 150 additions and 126 deletions
+50 -45
View File
@@ -5,10 +5,10 @@ import {type ImagePickerAsset} from 'expo-image-picker'
import {BlueskyVideoView} from '@bsky.app/video'
import {type CompressedVideo} from '#/lib/media/video/types'
import {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
@@ -23,58 +23,63 @@ export function VideoPreview({
isActivePost: boolean
clear: () => void
}) {
const t = useTheme()
const playerRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
let aspectRatio = asset.width / asset.height
if (isNaN(aspectRatio)) {
aspectRatio = 16 / 9
let aspectRatio: number | undefined
if (asset.width && asset.height) {
const raw = asset.width / asset.height
if (!Number.isNaN(raw)) {
aspectRatio = raw
}
}
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
let constrained: number | undefined
if (aspectRatio !== undefined) {
const ratio = 1 / 2 // max of 1:2 ratio in feeds
constrained = Math.max(aspectRatio, ratio)
}
return (
<View
style={[
a.w_full,
a.rounded_sm,
{aspectRatio},
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
{backgroundColor: 'black'},
]}>
<View style={[a.absolute, a.inset_0]}>
<VideoTranscodeBackdrop uri={asset.uri} />
</View>
{isActivePost && (
<>
{video.mimeType === 'image/gif' ? (
<Image
style={[a.flex_1]}
autoplay={!autoplayDisabled}
source={{uri: video.uri}}
accessibilityIgnoresInvertColors
cachePolicy="none"
/>
) : (
<BlueskyVideoView
url={video.uri}
autoplay={!autoplayDisabled}
beginMuted={true}
forceTakeover={true}
ref={playerRef}
/>
<View style={[a.pt_xs]}>
<ConstrainedImage
aspectRatio={constrained || 1}
minMobileAspectRatio={14 / 9}>
<View style={[a.flex_1, {backgroundColor: 'black'}]}>
<View style={[a.absolute, a.inset_0]}>
<VideoTranscodeBackdrop uri={asset.uri} />
</View>
{isActivePost && (
<>
{video.mimeType === 'image/gif' ? (
<Image
style={[a.flex_1]}
autoplay={!autoplayDisabled}
source={{uri: video.uri}}
accessibilityIgnoresInvertColors
cachePolicy="none"
contentFit="contain"
/>
) : (
<BlueskyVideoView
url={video.uri}
autoplay={!autoplayDisabled}
beginMuted={true}
forceTakeover={true}
ref={playerRef}
/>
)}
</>
)}
<ExternalEmbedRemoveBtn onRemove={clear} />
{autoplayDisabled && (
<View
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
<PlayButtonIcon />
</View>
)}
</>
)}
<ExternalEmbedRemoveBtn onRemove={clear} />
{autoplayDisabled && (
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
<PlayButtonIcon />
</View>
)}
</ConstrainedImage>
</View>
)
}
@@ -4,22 +4,21 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {type CompressedVideo} from '#/lib/media/video/types'
import {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
import {atoms as a} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import * as Toast from '#/components/Toast'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
export function VideoPreview({
asset,
video,
clear,
}: {
asset: ImagePickerAsset
video: CompressedVideo
isActivePost: boolean
clear: () => void
}) {
const {_} = useLingui()
@@ -27,56 +26,65 @@ export function VideoPreview({
// it's not possible using an img tag -sfn
const autoplayDisabled = useAutoplayDisabled()
let aspectRatio = asset.width / asset.height
if (isNaN(aspectRatio)) {
aspectRatio = 16 / 9
let aspectRatio: number | undefined
if (asset.width && asset.height) {
const raw = asset.width / asset.height
if (!Number.isNaN(raw)) {
aspectRatio = raw
}
}
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
let constrained: number | undefined
if (aspectRatio !== undefined) {
const ratio = 1 / 2 // max of 1:2 ratio in feeds
constrained = Math.max(aspectRatio, ratio)
}
return (
<View
style={[
a.w_full,
a.rounded_sm,
{aspectRatio},
a.overflow_hidden,
{backgroundColor: 'black'},
a.relative,
]}>
<ExternalEmbedRemoveBtn onRemove={clear} />
{video.mimeType === 'image/gif' ? (
<img
src={video.uri}
style={{width: '100%', height: '100%', objectFit: 'cover'}}
alt="GIF"
/>
) : (
<>
<video
src={video.uri}
style={{width: '100%', height: '100%', objectFit: 'cover'}}
autoPlay={!autoplayDisabled}
loop
muted
playsInline
onError={err => {
console.error('Error loading video', err)
Toast.show(_(msg`Could not process your video`), {
type: 'error',
})
clear()
}}
/>
{autoplayDisabled && (
<View
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
<PlayButtonIcon />
</View>
<View style={[a.pt_xs]}>
<ConstrainedImage
aspectRatio={constrained || 1}
minMobileAspectRatio={14 / 9}>
<View style={[a.flex_1, {backgroundColor: 'black'}]}>
{video.mimeType === 'image/gif' ? (
<img
src={video.uri}
style={{width: '100%', height: '100%', objectFit: 'contain'}}
alt="GIF"
/>
) : (
<>
<video
src={video.uri}
style={{width: '100%', height: '100%', objectFit: 'contain'}}
autoPlay={!autoplayDisabled}
loop
muted
playsInline
onError={err => {
console.error('Error loading video', err)
Toast.show(_(msg`Could not process your video`), {
type: 'error',
})
clear()
}}
/>
{autoplayDisabled && (
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.align_center,
]}>
<PlayButtonIcon />
</View>
)}
</>
)}
</>
)}
<ExternalEmbedRemoveBtn onRemove={clear} />
</View>
</ConstrainedImage>
</View>
)
}
@@ -3,8 +3,8 @@ import {View} from 'react-native'
import ProgressPie from 'react-native-progress/Pie'
import {type ImagePickerAsset} from 'expo-image-picker'
import {clamp} from '#/lib/numbers'
import {atoms as a, useTheme} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {IS_WEB} from '#/env'
import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
@@ -22,42 +22,53 @@ export function VideoTranscodeProgress({
if (IS_WEB) return null
let aspectRatio = asset.width / asset.height
if (isNaN(aspectRatio)) {
aspectRatio = 16 / 9
let aspectRatio: number | undefined
if (asset.width && asset.height) {
const raw = asset.width / asset.height
if (!Number.isNaN(raw)) {
aspectRatio = raw
}
}
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
let constrained: number | undefined
if (aspectRatio !== undefined) {
const ratio = 1 / 2 // max of 1:2 ratio in feeds
constrained = Math.max(aspectRatio, ratio)
}
return (
<View
style={[
a.w_full,
t.atoms.bg_contrast_50,
a.rounded_md,
a.overflow_hidden,
{aspectRatio},
]}>
<VideoTranscodeBackdrop uri={asset.uri} />
<View
style={[
a.flex_1,
a.align_center,
a.justify_center,
a.gap_lg,
a.absolute,
a.inset_0,
]}>
<ProgressPie
size={48}
borderWidth={3}
borderColor={t.atoms.text.color}
color={t.atoms.text.color}
progress={progress}
/>
</View>
<ExternalEmbedRemoveBtn onRemove={clear} />
<View style={[a.pt_xs]}>
<ConstrainedImage
aspectRatio={constrained || 1}
minMobileAspectRatio={14 / 9}>
<View
style={[
a.flex_1,
t.atoms.bg_contrast_50,
a.rounded_md,
a.overflow_hidden,
]}>
<VideoTranscodeBackdrop uri={asset.uri} />
<View
style={[
a.flex_1,
a.align_center,
a.justify_center,
a.gap_lg,
a.absolute,
a.inset_0,
]}>
<ProgressPie
size={48}
borderWidth={3}
borderColor={t.atoms.text.color}
color={t.atoms.text.color}
progress={progress}
/>
</View>
<ExternalEmbedRemoveBtn onRemove={clear} />
</View>
</ConstrainedImage>
</View>
)
}