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