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/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}