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 {BlueskyVideoView} from '@bsky.app/video'
import {type CompressedVideo} from '#/lib/media/video/types' import {type CompressedVideo} from '#/lib/media/video/types'
import {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences' import {useAutoplayDisabled} from '#/state/preferences'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' 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 {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
@@ -23,58 +23,63 @@ export function VideoPreview({
isActivePost: boolean isActivePost: boolean
clear: () => void clear: () => void
}) { }) {
const t = useTheme()
const playerRef = useRef<BlueskyVideoView>(null) const playerRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled() const autoplayDisabled = useAutoplayDisabled()
let aspectRatio = asset.width / asset.height
if (isNaN(aspectRatio)) { let aspectRatio: number | undefined
aspectRatio = 16 / 9 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 ( return (
<View <View style={[a.pt_xs]}>
style={[ <ConstrainedImage
a.w_full, aspectRatio={constrained || 1}
a.rounded_sm, minMobileAspectRatio={14 / 9}>
{aspectRatio}, <View style={[a.flex_1, {backgroundColor: 'black'}]}>
a.overflow_hidden, <View style={[a.absolute, a.inset_0]}>
a.border, <VideoTranscodeBackdrop uri={asset.uri} />
t.atoms.border_contrast_low, </View>
{backgroundColor: 'black'}, {isActivePost && (
]}> <>
<View style={[a.absolute, a.inset_0]}> {video.mimeType === 'image/gif' ? (
<VideoTranscodeBackdrop uri={asset.uri} /> <Image
</View> style={[a.flex_1]}
{isActivePost && ( autoplay={!autoplayDisabled}
<> source={{uri: video.uri}}
{video.mimeType === 'image/gif' ? ( accessibilityIgnoresInvertColors
<Image cachePolicy="none"
style={[a.flex_1]} contentFit="contain"
autoplay={!autoplayDisabled} />
source={{uri: video.uri}} ) : (
accessibilityIgnoresInvertColors <BlueskyVideoView
cachePolicy="none" url={video.uri}
/> autoplay={!autoplayDisabled}
) : ( beginMuted={true}
<BlueskyVideoView forceTakeover={true}
url={video.uri} ref={playerRef}
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> </View>
)} </ConstrainedImage>
</View> </View>
) )
} }
@@ -4,22 +4,21 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {type CompressedVideo} from '#/lib/media/video/types' import {type CompressedVideo} from '#/lib/media/video/types'
import {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences' import {useAutoplayDisabled} from '#/state/preferences'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn' import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
export function VideoPreview({ export function VideoPreview({
asset, asset,
video, video,
clear, clear,
}: { }: {
asset: ImagePickerAsset asset: ImagePickerAsset
video: CompressedVideo video: CompressedVideo
isActivePost: boolean
clear: () => void clear: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
@@ -27,56 +26,65 @@ export function VideoPreview({
// it's not possible using an img tag -sfn // it's not possible using an img tag -sfn
const autoplayDisabled = useAutoplayDisabled() const autoplayDisabled = useAutoplayDisabled()
let aspectRatio = asset.width / asset.height let aspectRatio: number | undefined
if (asset.width && asset.height) {
if (isNaN(aspectRatio)) { const raw = asset.width / asset.height
aspectRatio = 16 / 9 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 ( return (
<View <View style={[a.pt_xs]}>
style={[ <ConstrainedImage
a.w_full, aspectRatio={constrained || 1}
a.rounded_sm, minMobileAspectRatio={14 / 9}>
{aspectRatio}, <View style={[a.flex_1, {backgroundColor: 'black'}]}>
a.overflow_hidden, {video.mimeType === 'image/gif' ? (
{backgroundColor: 'black'}, <img
a.relative, src={video.uri}
]}> style={{width: '100%', height: '100%', objectFit: 'contain'}}
<ExternalEmbedRemoveBtn onRemove={clear} /> alt="GIF"
{video.mimeType === 'image/gif' ? ( />
<img ) : (
src={video.uri} <>
style={{width: '100%', height: '100%', objectFit: 'cover'}} <video
alt="GIF" src={video.uri}
/> style={{width: '100%', height: '100%', objectFit: 'contain'}}
) : ( autoPlay={!autoplayDisabled}
<> loop
<video muted
src={video.uri} playsInline
style={{width: '100%', height: '100%', objectFit: 'cover'}} onError={err => {
autoPlay={!autoplayDisabled} console.error('Error loading video', err)
loop Toast.show(_(msg`Could not process your video`), {
muted type: 'error',
playsInline })
onError={err => { clear()
console.error('Error loading video', err) }}
Toast.show(_(msg`Could not process your video`), { />
type: 'error', {autoplayDisabled && (
}) <View
clear() style={[
}} a.absolute,
/> a.inset_0,
{autoplayDisabled && ( a.justify_center,
<View a.align_center,
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> ]}>
<PlayButtonIcon /> <PlayButtonIcon />
</View> </View>
)}
</>
)} )}
</> <ExternalEmbedRemoveBtn onRemove={clear} />
)} </View>
</ConstrainedImage>
</View> </View>
) )
} }
@@ -3,8 +3,8 @@ import {View} from 'react-native'
import ProgressPie from 'react-native-progress/Pie' import ProgressPie from 'react-native-progress/Pie'
import {type ImagePickerAsset} from 'expo-image-picker' import {type ImagePickerAsset} from 'expo-image-picker'
import {clamp} from '#/lib/numbers'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn' import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
@@ -22,42 +22,53 @@ export function VideoTranscodeProgress({
if (IS_WEB) return null if (IS_WEB) return null
let aspectRatio = asset.width / asset.height let aspectRatio: number | undefined
if (asset.width && asset.height) {
if (isNaN(aspectRatio)) { const raw = asset.width / asset.height
aspectRatio = 16 / 9 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 ( return (
<View <View style={[a.pt_xs]}>
style={[ <ConstrainedImage
a.w_full, aspectRatio={constrained || 1}
t.atoms.bg_contrast_50, minMobileAspectRatio={14 / 9}>
a.rounded_md, <View
a.overflow_hidden, style={[
{aspectRatio}, a.flex_1,
]}> t.atoms.bg_contrast_50,
<VideoTranscodeBackdrop uri={asset.uri} /> a.rounded_md,
<View a.overflow_hidden,
style={[ ]}>
a.flex_1, <VideoTranscodeBackdrop uri={asset.uri} />
a.align_center, <View
a.justify_center, style={[
a.gap_lg, a.flex_1,
a.absolute, a.align_center,
a.inset_0, a.justify_center,
]}> a.gap_lg,
<ProgressPie a.absolute,
size={48} a.inset_0,
borderWidth={3} ]}>
borderColor={t.atoms.text.color} <ProgressPie
color={t.atoms.text.color} size={48}
progress={progress} borderWidth={3}
/> borderColor={t.atoms.text.color}
</View> color={t.atoms.text.color}
<ExternalEmbedRemoveBtn onRemove={clear} /> progress={progress}
/>
</View>
<ExternalEmbedRemoveBtn onRemove={clear} />
</View>
</ConstrainedImage>
</View> </View>
) )
} }