Implement lower hand suggestion in group calls
This commit is contained in:
parent
4312d03db0
commit
f2d4f669fe
11 changed files with 206 additions and 3 deletions
|
@ -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."
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -144,6 +144,7 @@ export function GroupCall(args: PropsType): JSX.Element {
|
|||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
suggestLowerHand: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -333,6 +333,7 @@ const mapStateToActiveCallProp = (
|
|||
raisedHands,
|
||||
remoteParticipants,
|
||||
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
|
||||
suggestLowerHand: Boolean(activeCallState.suggestLowerHand),
|
||||
} satisfies ActiveGroupCallType;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -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;
|
||||
|
|
16
ts/util/isLowerHandSuggestionEnabled.ts
Normal file
16
ts/util/isLowerHandSuggestionEnabled.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue