Highlight speaker on group calls
This commit is contained in:
parent
3d4248e070
commit
23cbd2c8b3
6 changed files with 80 additions and 25 deletions
|
@ -3972,6 +3972,21 @@ button.module-image__border-overlay:focus {
|
|||
transform: translate(0, 0);
|
||||
transition: transform 200ms linear, width 200ms linear, height 200ms linear;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0 solid transparent;
|
||||
border-radius: 5px;
|
||||
transition: border-width 200ms, border-color 200ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
&--speaking:after {
|
||||
border-width: 3px;
|
||||
border-color: $color-white;
|
||||
}
|
||||
|
||||
&__remote-video {
|
||||
// The background-color is seen while the video loads.
|
||||
background-color: $color-gray-75;
|
||||
|
|
|
@ -40,11 +40,15 @@ import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPerm
|
|||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
|
||||
import { CallingAudioIndicator } from './CallingAudioIndicator';
|
||||
import {
|
||||
CallingAudioIndicator,
|
||||
SPEAKING_LINGER_MS,
|
||||
} from './CallingAudioIndicator';
|
||||
import {
|
||||
useActiveCallShortcuts,
|
||||
useKeyboardShortcuts,
|
||||
} from '../hooks/useKeyboardShortcuts';
|
||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -155,6 +159,11 @@ export function CallScreen({
|
|||
showParticipantsList,
|
||||
} = activeCall;
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(
|
||||
localAudioLevel > 0,
|
||||
SPEAKING_LINGER_MS
|
||||
);
|
||||
|
||||
useActivateSpeakerViewOnPresenting({
|
||||
remoteParticipants,
|
||||
switchToPresentationView,
|
||||
|
@ -536,6 +545,7 @@ export function CallScreen({
|
|||
<CallingAudioIndicator
|
||||
hasAudio={hasLocalAudio}
|
||||
audioLevel={localAudioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,8 +4,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import { CallingAudioIndicator } from './CallingAudioIndicator';
|
||||
import {
|
||||
CallingAudioIndicator,
|
||||
SPEAKING_LINGER_MS,
|
||||
} from './CallingAudioIndicator';
|
||||
import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants';
|
||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
|
||||
export default {
|
||||
title: 'Components/CallingAudioIndicator',
|
||||
|
@ -24,10 +28,13 @@ export function Extreme(): JSX.Element {
|
|||
};
|
||||
}, [audioLevel, setAudioLevel]);
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(audioLevel > 0, SPEAKING_LINGER_MS);
|
||||
|
||||
return (
|
||||
<CallingAudioIndicator
|
||||
hasAudio={boolean('hasAudio', true)}
|
||||
audioLevel={audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -45,10 +52,13 @@ export function Random(): JSX.Element {
|
|||
};
|
||||
}, [audioLevel, setAudioLevel]);
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(audioLevel > 0, SPEAKING_LINGER_MS);
|
||||
|
||||
return (
|
||||
<CallingAudioIndicator
|
||||
hasAudio={boolean('hasAudio', true)}
|
||||
audioLevel={audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,16 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
|
||||
import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
const SPEAKING_LINGER_MS = 500;
|
||||
|
||||
export const SPEAKING_LINGER_MS = 500;
|
||||
const BASE_CLASS_NAME = 'CallingAudioIndicator';
|
||||
const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`;
|
||||
|
||||
|
@ -104,23 +102,12 @@ function Bars({ audioLevel }: { audioLevel: number }): ReactElement {
|
|||
export function CallingAudioIndicator({
|
||||
hasAudio,
|
||||
audioLevel,
|
||||
}: Readonly<{ hasAudio: boolean; audioLevel: number }>): ReactElement {
|
||||
const [shouldShowSpeaking, setShouldShowSpeaking] = useState(audioLevel > 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioLevel > 0) {
|
||||
setShouldShowSpeaking(true);
|
||||
} else if (shouldShowSpeaking) {
|
||||
const timeout = setTimeout(() => {
|
||||
setShouldShowSpeaking(false);
|
||||
}, SPEAKING_LINGER_MS);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
return noop;
|
||||
}, [audioLevel, shouldShowSpeaking]);
|
||||
|
||||
shouldShowSpeaking,
|
||||
}: Readonly<{
|
||||
hasAudio: boolean;
|
||||
audioLevel: number;
|
||||
shouldShowSpeaking: boolean;
|
||||
}>): ReactElement {
|
||||
if (!hasAudio) {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -16,13 +16,17 @@ import type { GroupCallRemoteParticipantType } from '../types/Calling';
|
|||
import type { LocalizerType } from '../types/Util';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { CallingAudioIndicator } from './CallingAudioIndicator';
|
||||
import {
|
||||
CallingAudioIndicator,
|
||||
SPEAKING_LINGER_MS,
|
||||
} from './CallingAudioIndicator';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { Intl } from './Intl';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
|
||||
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
|
||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
|
||||
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;
|
||||
const MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES = 60000;
|
||||
|
@ -79,6 +83,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
videoAspectRatio,
|
||||
} = props.remoteParticipant;
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(
|
||||
!props.isInPip ? props.audioLevel > 0 : false,
|
||||
SPEAKING_LINGER_MS
|
||||
);
|
||||
|
||||
const [hasReceivedVideoRecently, setHasReceivedVideoRecently] =
|
||||
useState(false);
|
||||
const [isWide, setIsWide] = useState<boolean>(
|
||||
|
@ -266,7 +275,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
)}
|
||||
|
||||
<div
|
||||
className="module-ongoing-call__group-call-remote-participant"
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant',
|
||||
isSpeaking &&
|
||||
'module-ongoing-call__group-call-remote-participant--speaking'
|
||||
)}
|
||||
ref={intersectionRef}
|
||||
style={containerStyles}
|
||||
>
|
||||
|
@ -283,6 +296,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
<CallingAudioIndicator
|
||||
hasAudio={hasRemoteAudio}
|
||||
audioLevel={props.audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
19
ts/hooks/useValueAtFixedRate.ts
Normal file
19
ts/hooks/useValueAtFixedRate.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useValueAtFixedRate<T>(value: T, rate: number): T {
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setCurrentValue(value);
|
||||
}, rate);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [value, rate]);
|
||||
|
||||
return currentValue;
|
||||
}
|
Loading…
Reference in a new issue