An ATproto social media client -- with an independent Appview.
1import {useCallback} from 'react'
2import {View} from 'react-native'
3import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
4import {msg} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {isSafari, isTouchDevice} from '#/lib/browser'
8import {atoms as a} from '#/alf'
9import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
10import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
11import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
12import {ControlButton} from './ControlButton'
13
14export function VolumeControl({
15 muted,
16 changeMuted,
17 hovered,
18 onHover,
19 onEndHover,
20 drawFocus,
21}: {
22 muted: boolean
23 changeMuted: (muted: boolean | ((prev: boolean) => boolean)) => void
24 hovered: boolean
25 onHover: () => void
26 onEndHover: () => void
27 drawFocus: () => void
28}) {
29 const {_} = useLingui()
30 const [volume, setVolume] = useVideoVolumeState()
31
32 const onVolumeChange = useCallback(
33 (evt: React.ChangeEvent<HTMLInputElement>) => {
34 drawFocus()
35 const vol = sliderVolumeToVideoVolume(Number(evt.target.value))
36 setVolume(vol)
37 changeMuted(vol === 0)
38 },
39 [setVolume, drawFocus, changeMuted],
40 )
41
42 const sliderVolume = muted ? 0 : videoVolumeToSliderVolume(volume)
43
44 const isZeroVolume = volume === 0
45 const onPressMute = useCallback(() => {
46 drawFocus()
47 if (isZeroVolume) {
48 setVolume(1)
49 changeMuted(false)
50 } else {
51 changeMuted(prevMuted => !prevMuted)
52 }
53 }, [drawFocus, setVolume, isZeroVolume, changeMuted])
54
55 return (
56 <View
57 onPointerEnter={onHover}
58 onPointerLeave={onEndHover}
59 style={[a.relative]}>
60 {hovered && !isTouchDevice && (
61 <Animated.View
62 entering={FadeIn.duration(100)}
63 exiting={FadeOut.duration(100)}
64 style={[a.absolute, a.w_full, {height: 100, bottom: '100%'}]}>
65 <View
66 style={[
67 a.flex_1,
68 a.mb_xs,
69 a.px_2xs,
70 a.py_xs,
71 {backgroundColor: 'rgba(0, 0, 0, 0.6)'},
72 a.rounded_xs,
73 a.align_center,
74 ]}>
75 <input
76 type="range"
77 min={0}
78 max={100}
79 value={sliderVolume}
80 aria-label={_(msg`Volume`)}
81 style={
82 // Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h
83 isSafari ? {height: 92, minHeight: '100%'} : {height: '100%'}
84 }
85 onChange={onVolumeChange}
86 // @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn
87 orient="vertical"
88 />
89 </View>
90 </Animated.View>
91 )}
92 <ControlButton
93 active={muted || volume === 0}
94 activeLabel={_(msg({message: `Unmute`, context: 'video'}))}
95 inactiveLabel={_(msg({message: `Mute`, context: 'video'}))}
96 activeIcon={MuteIcon}
97 inactiveIcon={UnmuteIcon}
98 onPress={onPressMute}
99 />
100 </View>
101 )
102}
103
104function sliderVolumeToVideoVolume(value: number) {
105 return Math.pow(value / 100, 4)
106}
107
108function videoVolumeToSliderVolume(value: number) {
109 return Math.round(Math.pow(value, 1 / 4) * 100)
110}