An ATproto social media client -- with an independent Appview.
at main 207 lines 6.0 kB view raw
1import {useImperativeHandle, useRef, useState} from 'react' 2import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' 3import {type AppBskyEmbedVideo} from '@atproto/api' 4import {BlueskyVideoView} from '@haileyok/bluesky-video' 5import {msg} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7 8import {HITSLOP_30} from '#/lib/constants' 9import {useAutoplayDisabled} from '#/state/preferences' 10import {atoms as a, useTheme} from '#/alf' 11import {useIsWithinMessage} from '#/components/dms/MessageContext' 12import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' 13import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause' 14import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' 15import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' 16import {MediaInsetBorder} from '#/components/MediaInsetBorder' 17import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' 18import {TimeIndicator} from './TimeIndicator' 19 20export function VideoEmbedInnerNative({ 21 ref, 22 embed, 23 setStatus, 24 setIsLoading, 25 setIsActive, 26}: { 27 ref: React.Ref<{togglePlayback: () => void}> 28 embed: AppBskyEmbedVideo.View 29 setStatus: (status: 'playing' | 'paused') => void 30 setIsLoading: (isLoading: boolean) => void 31 setIsActive: (isActive: boolean) => void 32}) { 33 const {_} = useLingui() 34 const videoRef = useRef<BlueskyVideoView>(null) 35 const autoplayDisabled = useAutoplayDisabled() 36 const isWithinMessage = useIsWithinMessage() 37 const [muted, setMuted] = useVideoMuteState() 38 39 const [isPlaying, setIsPlaying] = useState(false) 40 const [timeRemaining, setTimeRemaining] = useState(0) 41 const [error, setError] = useState<string>() 42 43 useImperativeHandle(ref, () => ({ 44 togglePlayback: () => { 45 videoRef.current?.togglePlayback() 46 }, 47 })) 48 49 if (error) { 50 throw new Error(error) 51 } 52 53 return ( 54 <View style={[a.flex_1, a.relative]}> 55 <BlueskyVideoView 56 url={embed.playlist} 57 autoplay={!autoplayDisabled && !isWithinMessage} 58 beginMuted={autoplayDisabled ? false : muted} 59 style={[a.rounded_sm]} 60 onActiveChange={e => { 61 setIsActive(e.nativeEvent.isActive) 62 }} 63 onLoadingChange={e => { 64 setIsLoading(e.nativeEvent.isLoading) 65 }} 66 onMutedChange={e => { 67 setMuted(e.nativeEvent.isMuted) 68 }} 69 onStatusChange={e => { 70 setStatus(e.nativeEvent.status) 71 setIsPlaying(e.nativeEvent.status === 'playing') 72 }} 73 onTimeRemainingChange={e => { 74 setTimeRemaining(e.nativeEvent.timeRemaining) 75 }} 76 onError={e => { 77 setError(e.nativeEvent.error) 78 }} 79 ref={videoRef} 80 accessibilityLabel={ 81 embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) 82 } 83 accessibilityHint="" 84 /> 85 <VideoControls 86 enterFullscreen={() => { 87 videoRef.current?.enterFullscreen(true) 88 }} 89 toggleMuted={() => { 90 videoRef.current?.toggleMuted() 91 }} 92 togglePlayback={() => { 93 videoRef.current?.togglePlayback() 94 }} 95 isPlaying={isPlaying} 96 timeRemaining={timeRemaining} 97 /> 98 <MediaInsetBorder /> 99 </View> 100 ) 101} 102 103function VideoControls({ 104 enterFullscreen, 105 toggleMuted, 106 togglePlayback, 107 timeRemaining, 108 isPlaying, 109}: { 110 enterFullscreen: () => void 111 toggleMuted: () => void 112 togglePlayback: () => void 113 timeRemaining: number 114 isPlaying: boolean 115}) { 116 const {_} = useLingui() 117 const t = useTheme() 118 const [muted] = useVideoMuteState() 119 120 // show countdown when: 121 // 1. timeRemaining is a number - was seeing NaNs 122 // 2. duration is greater than 0 - means metadata has loaded 123 // 3. we're less than 5 second into the video 124 const showTime = !isNaN(timeRemaining) 125 126 return ( 127 <View style={[a.absolute, a.inset_0]}> 128 <Pressable 129 onPress={enterFullscreen} 130 style={a.flex_1} 131 accessibilityLabel={_(msg`Video`)} 132 accessibilityHint={_(msg`Enters full screen`)} 133 accessibilityRole="button" 134 /> 135 <ControlButton 136 onPress={togglePlayback} 137 label={isPlaying ? _(msg`Pause`) : _(msg`Play`)} 138 accessibilityHint={_(msg`Plays or pauses the video`)} 139 style={{left: 6}}> 140 {isPlaying ? ( 141 <PauseIcon width={13} fill={t.palette.white} /> 142 ) : ( 143 <PlayIcon width={13} fill={t.palette.white} /> 144 )} 145 </ControlButton> 146 {showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />} 147 148 <ControlButton 149 onPress={toggleMuted} 150 label={ 151 muted 152 ? _(msg({message: `Unmute`, context: 'video'})) 153 : _(msg({message: `Mute`, context: 'video'})) 154 } 155 accessibilityHint={_(msg`Toggles the sound`)} 156 style={{right: 6}}> 157 {muted ? ( 158 <MuteIcon width={13} fill={t.palette.white} /> 159 ) : ( 160 <UnmuteIcon width={13} fill={t.palette.white} /> 161 )} 162 </ControlButton> 163 </View> 164 ) 165} 166 167function ControlButton({ 168 onPress, 169 children, 170 label, 171 accessibilityHint, 172 style, 173}: { 174 onPress: () => void 175 children: React.ReactNode 176 label: string 177 accessibilityHint: string 178 style?: StyleProp<ViewStyle> 179}) { 180 return ( 181 <View 182 style={[ 183 a.absolute, 184 a.rounded_full, 185 a.justify_center, 186 { 187 backgroundColor: 'rgba(0, 0, 0, 0.5)', 188 paddingHorizontal: 4, 189 paddingVertical: 4, 190 bottom: 6, 191 minHeight: 21, 192 minWidth: 21, 193 }, 194 style, 195 ]}> 196 <Pressable 197 onPress={onPress} 198 style={a.flex_1} 199 accessibilityLabel={label} 200 accessibilityHint={accessibilityHint} 201 accessibilityRole="button" 202 hitSlop={HITSLOP_30}> 203 {children} 204 </Pressable> 205 </View> 206 ) 207}