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