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