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