Implement lower hand suggestion in group calls

This commit is contained in:
trevor-signal 2024-12-20 13:35:11 -05:00 committed by GitHub
parent 4312d03db0
commit f2d4f669fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 206 additions and 3 deletions

View file

@ -3699,6 +3699,14 @@
"messageformat": "Mic on",
"description": "Shown in a call when the user is muted and then unmutes their audio input using the Mute toggle button."
},
"icu:CallControls__LowerHandSuggestionToast": {
"messageformat": "Lower your hand?",
"description": "Shown in a call when the user has their hand raised but has been talking, next to a button to lower their hand."
},
"icu:CallControls__LowerHandSuggestionToast--button": {
"messageformat": "Lower",
"description": "Text of button that, when clicked, will lower your raised hand after you've been speaking"
},
"icu:CallControls__RingingToast--ringing-on": {
"messageformat": "Ringing on",
"description": "Shown in a group call lobby when call ringing is disabled, then the user enables ringing using the Ringing toggle button."

View file

@ -181,6 +181,7 @@ const getActiveCallForCallLink = (
pendingParticipants: overrideProps.pendingParticipants ?? [],
raisedHands: new Set<number>(),
remoteAudioLevels: new Map<number, number>(),
suggestLowerHand: false,
};
};
@ -232,6 +233,7 @@ export function OngoingGroupCall(): JSX.Element {
raisedHands: new Set<number>(),
remoteParticipants: [],
remoteAudioLevels: new Map<number, number>(),
suggestLowerHand: false,
},
})}
/>

View file

@ -74,6 +74,7 @@ type GroupCallOverrideProps = OverridePropsBase & {
raisedHands?: Set<number>;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
remoteAudioLevel?: number;
suggestLowerHand?: boolean;
};
const createActiveDirectCallProp = (
@ -153,6 +154,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
])
),
reactions: overrideProps.reactions || [],
suggestLowerHand: overrideProps.suggestLowerHand ?? false,
});
const createActiveCallProp = (
@ -799,6 +801,37 @@ export function GroupCallHandRaising(): JSX.Element {
return <CallScreen {...props} activeCall={activeCall} />;
}
export function GroupCallSuggestLowerHand(): JSX.Element {
const remoteParticipants = allRemoteParticipants.slice(0, 10);
const [props, setProps] = React.useState(
createProps({
callMode: CallMode.Group,
remoteParticipants,
raisedHands: new Set([LOCAL_DEMUX_ID]),
viewMode: CallViewMode.Sidebar,
suggestLowerHand: false,
})
);
React.useEffect(() => {
setTimeout(
() =>
setProps(
createProps({
callMode: CallMode.Group,
remoteParticipants,
viewMode: CallViewMode.Sidebar,
suggestLowerHand: true,
})
),
200
);
}, [remoteParticipants]);
return <CallScreen {...props} />;
}
// Every [frequency] ms, all hands are lowered and [random min to max] random hands
// are raised
function useHandRaiser(

View file

@ -855,6 +855,13 @@ export function CallScreen({
outgoingRing={undefined}
raisedHands={raisedHands}
renderRaisedHandsToast={renderRaisedHandsToast}
handleLowerHand={() => toggleRaiseHand(false)}
suggestLowerHand={
isGroupOrAdhocActiveCall(activeCall)
? activeCall.suggestLowerHand
: false
}
isHandRaised={localHandRaised}
i18n={i18n}
/>
{isCallLinkAdmin ? (

View file

@ -144,6 +144,7 @@ export function GroupCall(args: PropsType): JSX.Element {
raisedHands: new Set<number>(),
remoteParticipants: [],
remoteAudioLevels: new Map<number, number>(),
suggestLowerHand: false,
}}
/>
);

View file

@ -11,6 +11,7 @@ import { usePrevious } from '../hooks/usePrevious';
import { difference as setDifference } from '../util/setUtil';
import { isMoreRecentThan } from '../util/timestamp';
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
import { SECOND } from '../util/durations';
type PropsType = {
activeCall: ActiveCallType;
@ -251,6 +252,75 @@ function useRaisedHandsToast({
]);
}
function useLowerHandSuggestionToast({
suggestLowerHand,
handleLowerHand,
i18n,
isHandRaised,
}: {
suggestLowerHand: boolean | undefined;
i18n: LocalizerType;
handleLowerHand: (() => void) | undefined;
isHandRaised: boolean | undefined;
}): void {
const previousSuggestLowerHand = usePrevious(
suggestLowerHand,
suggestLowerHand
);
const { showToast, hideToast } = useCallingToasts();
const SUGGEST_LOWER_HAND_TOAST_KEY = 'SUGGEST_LOWER_HAND_TOAST_KEY';
useEffect(() => {
if (!handleLowerHand) {
return;
}
if (
previousSuggestLowerHand !== undefined &&
suggestLowerHand !== previousSuggestLowerHand
) {
if (suggestLowerHand && isHandRaised) {
showToast({
key: SUGGEST_LOWER_HAND_TOAST_KEY,
content: (
<div className="CallingRaisedHandsToast__Content">
<span className="CallingRaisedHandsToast__HandIcon" />
{i18n('icu:CallControls__LowerHandSuggestionToast')}
<button
className="CallingRaisedHandsToasts__Link"
type="button"
onClick={() => {
handleLowerHand();
hideToast(SUGGEST_LOWER_HAND_TOAST_KEY);
}}
>
{i18n('icu:CallControls__LowerHandSuggestionToast--button')}
</button>
</div>
),
dismissable: false,
autoClose: true,
lifetime: 10 * SECOND,
});
}
}
}, [
suggestLowerHand,
handleLowerHand,
previousSuggestLowerHand,
hideToast,
showToast,
SUGGEST_LOWER_HAND_TOAST_KEY,
isHandRaised,
i18n,
]);
useEffect(() => {
if (!isHandRaised) {
hideToast(SUGGEST_LOWER_HAND_TOAST_KEY);
}
}, [isHandRaised, hideToast]);
}
type CallingButtonToastsType = {
hasLocalAudio: boolean;
outgoingRing: boolean | undefined;
@ -258,6 +328,9 @@ type CallingButtonToastsType = {
renderRaisedHandsToast?: (
hands: Array<number>
) => JSX.Element | string | undefined;
suggestLowerHand?: boolean;
isHandRaised?: boolean;
handleLowerHand?: () => void;
i18n: LocalizerType;
};
@ -284,11 +357,20 @@ function CallingButtonToasts({
outgoingRing,
raisedHands,
renderRaisedHandsToast,
suggestLowerHand,
handleLowerHand,
isHandRaised,
i18n,
}: CallingButtonToastsType) {
useMutedToast({ hasLocalAudio, i18n });
useOutgoingRingToast({ outgoingRing, i18n });
useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });
useLowerHandSuggestionToast({
suggestLowerHand,
i18n,
handleLowerHand,
isHandRaised,
});
return null;
}

View file

@ -7,7 +7,6 @@ import type {
CallId,
DeviceId,
GroupCallObserver,
SpeechEvent,
PeekInfo,
UserId,
VideoFrameSource,
@ -39,6 +38,7 @@ import {
RingRTC,
RingUpdate,
GroupCallKind,
SpeechEvent,
} from '@signalapp/ringrtc';
import { uniqBy, noop, compact } from 'lodash';
@ -158,6 +158,7 @@ import { createIdenticon } from '../util/createIdenticon';
import { getColorForCallLink } from '../util/getColorForCallLink';
import { getUseRingrtcAdm } from '../util/ringrtc/ringrtcAdm';
import OS from '../util/os/osMain';
import { isLowerHandSuggestionEnabled } from '../util/isLowerHandSuggestionEnabled';
const { wasGroupCallRingPreviouslyCanceled } = DataReader;
const {
@ -207,10 +208,12 @@ type CallingReduxInterface = Pick<
| 'refreshIODevices'
| 'remoteSharingScreenChange'
| 'remoteVideoChange'
| 'sendGroupCallRaiseHand'
| 'startCallingLobby'
| 'startCallLinkLobby'
| 'startCallLinkLobbyByRoomId'
| 'peekNotConnectedGroupCall'
| 'setSuggestLowerHand'
> & {
areAnyCallsActiveOrRinging(): boolean;
};
@ -1479,8 +1482,22 @@ export class CallingClass {
endedReason,
});
},
onSpeechEvent: (_groupCall: GroupCall, _event: SpeechEvent) => {
// Implementation to come later
onSpeechEvent: (_groupCall: GroupCall, event: SpeechEvent) => {
if (!isLowerHandSuggestionEnabled()) {
return;
}
log.info('GroupCall#onSpeechEvent', event);
if (event === SpeechEvent.LowerHandSuggestion) {
this.reduxInterface?.setSuggestLowerHand(true);
} else if (event === SpeechEvent.StoppedSpeaking) {
this.reduxInterface?.setSuggestLowerHand(false);
} else {
log.error(
'GroupCall#onSpeechEvent, unknown speechEvent',
SpeechEvent,
Errors.toLogFormat(missingCaseError(event))
);
}
},
};
}

View file

@ -193,6 +193,7 @@ export type ActiveCallStateType = {
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
suggestLowerHand?: boolean;
reactions?: ActiveCallReactionsType;
};
export type WaitingCallStateType = ReadonlyDeep<{
@ -650,6 +651,7 @@ const SET_OUTGOING_RING = 'calling/SET_OUTGOING_RING';
const SET_PRESENTING = 'calling/SET_PRESENTING';
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
const SET_CAPTURER_BATON = 'calling/SET_CAPTURER_BATON';
const SUGGEST_LOWER_HAND = 'calling/SUGGEST_LOWER_HAND';
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
@ -915,6 +917,10 @@ type StartDirectCallActionType = ReadonlyDeep<{
type: 'calling/START_DIRECT_CALL';
payload: StartDirectCallType;
}>;
type SuggestLowerHandActionType = ReadonlyDeep<{
type: 'calling/SUGGEST_LOWER_HAND';
payload: { suggestLowerHand: boolean };
}>;
type ToggleNeedsScreenRecordingPermissionsActionType = ReadonlyDeep<{
type: 'calling/TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS';
@ -993,6 +999,7 @@ export type CallingActionType =
| TogglePipActionType
| SetPresentingFulfilledActionType
| ToggleSettingsActionType
| SuggestLowerHandActionType
| SwitchToPresentationViewActionType
| SwitchFromPresentationViewActionType
| WaitingForCallingLobbyActionType
@ -1622,6 +1629,15 @@ function hangUpActiveCall(
};
}
function setSuggestLowerHand(
suggestLowerHand: boolean
): SuggestLowerHandActionType {
return {
type: SUGGEST_LOWER_HAND,
payload: { suggestLowerHand },
};
}
function sendGroupCallRaiseHand(
payload: SendGroupCallRaiseHandType
): ThunkAction<void, RootStateType, unknown, SendGroupCallRaiseHandActionType> {
@ -2696,6 +2712,7 @@ export const actions = {
setLocalVideo,
setOutgoingRing,
setRendererCanvas,
setSuggestLowerHand,
startCall,
startCallLinkLobby,
startCallLinkLobbyByRoomId,
@ -4035,5 +4052,23 @@ export function reducer(
};
}
if (action.type === SUGGEST_LOWER_HAND) {
const { suggestLowerHand } = action.payload;
const { activeCallState } = state;
if (activeCallState?.state !== 'Active') {
log.warn('Cannot suggest lower hand when there is no active call');
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
suggestLowerHand,
},
};
}
return state;
}

View file

@ -333,6 +333,7 @@ const mapStateToActiveCallProp = (
raisedHands,
remoteParticipants,
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
suggestLowerHand: Boolean(activeCallState.suggestLowerHand),
} satisfies ActiveGroupCallType;
}
default:

View file

@ -95,6 +95,7 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
raisedHands: Set<number>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
remoteAudioLevels: Map<number, number>;
suggestLowerHand: boolean;
};
export type ActiveCallType = ActiveDirectCallType | ActiveGroupCallType;

View file

@ -0,0 +1,16 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as RemoteConfig from '../RemoteConfig';
import { isProduction } from './version';
export function isLowerHandSuggestionEnabled(): boolean {
if (
isProduction(window.getVersion()) ||
!RemoteConfig.isEnabled('desktop.internalUser')
) {
return false;
}
return true;
}