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