Fix video thumbnails on native (#8774)

This commit is contained in:
Samuel Newman
2025-08-04 18:42:55 +03:00
committed by GitHub
parent b32568260f
commit 7b5d11eb80
4 changed files with 94 additions and 100 deletions
+1 -1
View File
@@ -142,7 +142,7 @@
"expo-file-system": "~18.1.10", "expo-file-system": "~18.1.10",
"expo-font": "~13.3.1", "expo-font": "~13.3.1",
"expo-haptics": "~14.1.4", "expo-haptics": "~14.1.4",
"expo-image": "~2.2.1", "expo-image": "^2.4.0",
"expo-image-crop-tool": "^0.1.8", "expo-image-crop-tool": "^0.1.8",
"expo-image-manipulator": "~13.1.7", "expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4", "expo-image-picker": "~16.1.4",
@@ -1,4 +1,4 @@
import React, {useRef} from 'react' import {useImperativeHandle, useRef, useState} from 'react'
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api' import {type AppBskyEmbedVideo} from '@atproto/api'
import {BlueskyVideoView} from '@haileyok/bluesky-video' import {BlueskyVideoView} from '@haileyok/bluesky-video'
@@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {TimeIndicator} from './TimeIndicator' import {TimeIndicator} from './TimeIndicator'
export const VideoEmbedInnerNative = React.forwardRef( export function VideoEmbedInnerNative({
function VideoEmbedInnerNative( ref,
{ embed,
embed, setStatus,
setStatus, setIsLoading,
setIsLoading, setIsActive,
setIsActive, }: {
}: { ref: React.Ref<{togglePlayback: () => void}>
embed: AppBskyEmbedVideo.View embed: AppBskyEmbedVideo.View
setStatus: (status: 'playing' | 'paused') => void setStatus: (status: 'playing' | 'paused') => void
setIsLoading: (isLoading: boolean) => void setIsLoading: (isLoading: boolean) => void
setIsActive: (isActive: boolean) => void setIsActive: (isActive: boolean) => void
}) {
const {_} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
const [isPlaying, setIsPlaying] = useState(false)
const [timeRemaining, setTimeRemaining] = useState(0)
const [error, setError] = useState<string>()
useImperativeHandle(ref, () => ({
togglePlayback: () => {
videoRef.current?.togglePlayback()
}, },
ref: React.Ref<{togglePlayback: () => void}>, }))
) {
const {_} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
const [isPlaying, setIsPlaying] = React.useState(false) if (error) {
const [timeRemaining, setTimeRemaining] = React.useState(0) throw new Error(error)
const [error, setError] = React.useState<string>() }
React.useImperativeHandle(ref, () => ({ return (
togglePlayback: () => { <View style={[a.flex_1, a.relative]}>
videoRef.current?.togglePlayback() <BlueskyVideoView
}, url={embed.playlist}
})) autoplay={!autoplayDisabled && !isWithinMessage}
beginMuted={autoplayDisabled ? false : muted}
if (error) { style={[a.rounded_sm]}
throw new Error(error) onActiveChange={e => {
} setIsActive(e.nativeEvent.isActive)
}}
return ( onLoadingChange={e => {
<View style={[a.flex_1, a.relative]}> setIsLoading(e.nativeEvent.isLoading)
<BlueskyVideoView }}
url={embed.playlist} onMutedChange={e => {
autoplay={!autoplayDisabled && !isWithinMessage} setMuted(e.nativeEvent.isMuted)
beginMuted={autoplayDisabled ? false : muted} }}
style={[a.rounded_sm]} onStatusChange={e => {
onActiveChange={e => { setStatus(e.nativeEvent.status)
setIsActive(e.nativeEvent.isActive) setIsPlaying(e.nativeEvent.status === 'playing')
}} }}
onLoadingChange={e => { onTimeRemainingChange={e => {
setIsLoading(e.nativeEvent.isLoading) setTimeRemaining(e.nativeEvent.timeRemaining)
}} }}
onMutedChange={e => { onError={e => {
setMuted(e.nativeEvent.isMuted) setError(e.nativeEvent.error)
}} }}
onStatusChange={e => { ref={videoRef}
setStatus(e.nativeEvent.status) accessibilityLabel={
setIsPlaying(e.nativeEvent.status === 'playing') embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}} }
onTimeRemainingChange={e => { accessibilityHint=""
setTimeRemaining(e.nativeEvent.timeRemaining) />
}} <VideoControls
onError={e => { enterFullscreen={() => {
setError(e.nativeEvent.error) videoRef.current?.enterFullscreen(true)
}} }}
ref={videoRef} toggleMuted={() => {
accessibilityLabel={ videoRef.current?.toggleMuted()
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) }}
} togglePlayback={() => {
accessibilityHint="" videoRef.current?.togglePlayback()
/> }}
<VideoControls isPlaying={isPlaying}
enterFullscreen={() => { timeRemaining={timeRemaining}
videoRef.current?.enterFullscreen(true) />
}} <MediaInsetBorder />
toggleMuted={() => { </View>
videoRef.current?.toggleMuted() )
}} }
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
<MediaInsetBorder />
</View>
)
},
)
function VideoControls({ function VideoControls({
enterFullscreen, enterFullscreen,
+9 -12
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useState} from 'react' import {useCallback, useRef, useState} from 'react'
import {ActivityIndicator, View} from 'react-native' import {ActivityIndicator, View} from 'react-native'
import {ImageBackground} from 'expo-image' import {ImageBackground} from 'expo-image'
import {type AppBskyEmbedVideo} from '@atproto/api' import {type AppBskyEmbedVideo} from '@atproto/api'
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
function InnerWrapper({embed}: Props) { function InnerWrapper({embed}: Props) {
const {_} = useLingui() const {_} = useLingui()
const ref = React.useRef<{togglePlayback: () => void}>(null) const ref = useRef<{togglePlayback: () => void}>(null)
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>( const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
'pending', 'pending',
) )
const [isLoading, setIsLoading] = React.useState(false) const [isLoading, setIsLoading] = useState(false)
const [isActive, setIsActive] = React.useState(false) const [isActive, setIsActive] = useState(false)
const showSpinner = useThrottledValue(isActive && isLoading, 100) const showSpinner = useThrottledValue(isActive && isLoading, 100)
const showOverlay = const showOverlay =
@@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) {
(status === 'paused' && !isActive) || (status === 'paused' && !isActive) ||
status === 'pending' status === 'pending'
React.useEffect(() => { if (!isActive && status !== 'pending') {
if (!isActive && status !== 'pending') { setStatus('pending')
setStatus('pending') }
}
}, [isActive, status])
return ( return (
<> <>
@@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) {
onPress={() => { onPress={() => {
ref.current?.togglePlayback() ref.current?.togglePlayback()
}} }}
label={_(msg`Play video`)} label={_(msg`Play video`)}>
color="secondary">
{showSpinner ? ( {showSpinner ? (
<View <View
style={[ style={[
+4 -4
View File
@@ -11301,10 +11301,10 @@ expo-image-picker@~16.1.4:
dependencies: dependencies:
expo-image-loader "~5.1.0" expo-image-loader "~5.1.0"
expo-image@~2.2.1: expo-image@^2.4.0:
version "2.2.1" version "2.4.0"
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.2.1.tgz#b4aa706a25f7e8902ac854a8da249caf4a90cd67" resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
integrity sha512-5ZSggMi0X2G9AN0aM+sdkCyyZ6YcWvGs9KYLYrRBVUN3ph6RBiu6mKGpaNN1TAscySRnH1eHbUE1H+Qeq7qm1g== integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
expo-json-utils@~0.15.0: expo-json-utils@~0.15.0:
version "0.15.0" version "0.15.0"