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