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:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user