182 lines
5.0 KiB
TypeScript
182 lines
5.0 KiB
TypeScript
import type { GroupCallConnectionState } from '../../../lib/secret-sauce';
|
|
import type { FC } from '../../../lib/teact/teact';
|
|
import React, {
|
|
memo, useEffect, useMemo, useRef, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { vibrateShort } from '../../../util/vibrate';
|
|
import usePrevious from '../../../hooks/usePrevious';
|
|
import { selectActiveGroupCall, selectGroupCallParticipant } from '../../../global/selectors/calls';
|
|
import useLang from '../../../hooks/useLang';
|
|
|
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
|
|
|
import './MicrophoneButton.scss';
|
|
|
|
const CONNECTION_STATE_DEFAULT = 'discarded';
|
|
|
|
type StateProps = {
|
|
connectionState?: GroupCallConnectionState;
|
|
hasRequestedToSpeak: boolean;
|
|
isMuted?: boolean;
|
|
canSelfUnmute?: boolean;
|
|
noAudioStream: boolean;
|
|
};
|
|
|
|
const REQUEST_TO_SPEAK_THROTTLE = 3000;
|
|
const HOLD_TO_SPEAK_TIME = 200;
|
|
const ICON_SIZE = 48;
|
|
|
|
const MicrophoneButton: FC<StateProps> = ({
|
|
noAudioStream,
|
|
canSelfUnmute,
|
|
isMuted,
|
|
hasRequestedToSpeak,
|
|
connectionState,
|
|
}) => {
|
|
const {
|
|
toggleGroupCallMute,
|
|
requestToSpeak,
|
|
playGroupCallSound,
|
|
} = getActions();
|
|
|
|
const lang = useLang();
|
|
const muteMouseDownState = useRef('up');
|
|
|
|
const [isRequestingToSpeak, setIsRequestingToSpeak] = useState(false);
|
|
const isConnecting = connectionState !== 'connected';
|
|
const shouldRaiseHand = !canSelfUnmute && isMuted;
|
|
const prevShouldRaiseHand = usePrevious(shouldRaiseHand);
|
|
|
|
useEffect(() => {
|
|
if (prevShouldRaiseHand && !shouldRaiseHand) {
|
|
playGroupCallSound({ sound: 'allowTalk' });
|
|
}
|
|
}, [playGroupCallSound, prevShouldRaiseHand, shouldRaiseHand]);
|
|
|
|
// Voice mini
|
|
// unmuted -> muted [69, 99]
|
|
// muted -> unmuted [36, 69]
|
|
// raise -> muted [0, 36]
|
|
// muted -> raise [99, 136]
|
|
// unmuted -> raise [136, 172]
|
|
// TODO should probably move to other component
|
|
const playSegment: [number, number] = useMemo(() => {
|
|
if (isRequestingToSpeak) {
|
|
const r = Math.floor(Math.random() * 100);
|
|
return (r < 32 ? [0, 120]
|
|
: (r < 64 ? [120, 240]
|
|
: (r < 97 ? [240, 420]
|
|
: [420, 540]
|
|
)
|
|
)
|
|
);
|
|
}
|
|
if (!prevShouldRaiseHand && shouldRaiseHand) {
|
|
return noAudioStream ? [99, 135] : [136, 172];
|
|
}
|
|
if (prevShouldRaiseHand && !shouldRaiseHand) {
|
|
return [0, 36];
|
|
}
|
|
if (!shouldRaiseHand) {
|
|
return noAudioStream ? [69, 99] : [36, 69];
|
|
}
|
|
return [0, 0];
|
|
}, [prevShouldRaiseHand, isRequestingToSpeak, noAudioStream, shouldRaiseHand]);
|
|
|
|
const animatedIconName = isRequestingToSpeak ? 'HandFilled' : 'VoiceMini';
|
|
|
|
const toggleMute = () => {
|
|
vibrateShort();
|
|
toggleGroupCallMute();
|
|
};
|
|
|
|
const handleMouseDownMute = () => {
|
|
if (shouldRaiseHand) {
|
|
if (isRequestingToSpeak) return;
|
|
vibrateShort();
|
|
requestToSpeak();
|
|
setIsRequestingToSpeak(true);
|
|
setTimeout(() => {
|
|
setIsRequestingToSpeak(false);
|
|
}, REQUEST_TO_SPEAK_THROTTLE);
|
|
return;
|
|
}
|
|
muteMouseDownState.current = 'down';
|
|
if (noAudioStream) {
|
|
setTimeout(() => {
|
|
if (muteMouseDownState.current === 'down') {
|
|
muteMouseDownState.current = 'hold';
|
|
toggleMute();
|
|
}
|
|
}, HOLD_TO_SPEAK_TIME);
|
|
}
|
|
};
|
|
|
|
const handleMouseUpMute = () => {
|
|
if (shouldRaiseHand) {
|
|
return;
|
|
}
|
|
toggleMute();
|
|
muteMouseDownState.current = 'up';
|
|
};
|
|
|
|
const buttonText = useMemo(() => {
|
|
return lang(
|
|
hasRequestedToSpeak ? 'VoipMutedTapedForSpeak' : (
|
|
shouldRaiseHand ? 'VoipMutedByAdmin' : (
|
|
noAudioStream ? 'VoipUnmute' : 'VoipTapToMute'
|
|
)
|
|
),
|
|
);
|
|
}, [hasRequestedToSpeak, noAudioStream, lang, shouldRaiseHand]);
|
|
|
|
return (
|
|
<div className="button-wrapper microphone-wrapper">
|
|
<button
|
|
className={buildClassName(
|
|
'MicrophoneButton',
|
|
noAudioStream && 'crossed',
|
|
canSelfUnmute && 'can-self-unmute',
|
|
isConnecting && 'is-connecting',
|
|
shouldRaiseHand && 'muted-by-admin',
|
|
)}
|
|
onMouseDown={handleMouseDownMute}
|
|
onMouseUp={handleMouseUpMute}
|
|
>
|
|
<AnimatedIcon
|
|
name={animatedIconName}
|
|
size={ICON_SIZE}
|
|
playSegment={playSegment}
|
|
/>
|
|
</button>
|
|
<div className="button-text">
|
|
{buttonText}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal(
|
|
(global): StateProps => {
|
|
const groupCall = selectActiveGroupCall(global);
|
|
|
|
const { connectionState } = groupCall || {};
|
|
const meParticipant = groupCall && selectGroupCallParticipant(global, groupCall.id, global.currentUserId!);
|
|
|
|
const {
|
|
raiseHandRating, hasAudioStream, canSelfUnmute, isMuted,
|
|
} = meParticipant || {};
|
|
|
|
return {
|
|
connectionState: connectionState || CONNECTION_STATE_DEFAULT,
|
|
hasRequestedToSpeak: Boolean(raiseHandRating),
|
|
noAudioStream: !hasAudioStream,
|
|
canSelfUnmute,
|
|
isMuted,
|
|
};
|
|
},
|
|
)(MicrophoneButton));
|