-
+
+
+ {isHandRaised && (
+
+ )}
+
+
>
)}
diff --git a/ts/services/calling.ts b/ts/services/calling.ts
index 484b34acef..a59433eac6 100644
--- a/ts/services/calling.ts
+++ b/ts/services/calling.ts
@@ -163,6 +163,7 @@ type CallingReduxInterface = Pick<
| 'callStateChange'
| 'cancelIncomingGroupCallRing'
| 'groupCallAudioLevelsChange'
+ | 'groupCallRaisedHandsChange'
| 'groupCallStateChange'
| 'outgoingCall'
| 'receiveGroupCallReactions'
@@ -847,8 +848,11 @@ export class CallingClass {
reactions,
});
},
- onRaisedHands: (_groupCall, _raisedHands) => {
- // TODO: Implement handling of raised hands.
+ onRaisedHands: (_groupCall, raisedHands) => {
+ this.reduxInterface?.groupCallRaisedHandsChange({
+ conversationId,
+ raisedHands,
+ });
},
onPeekChanged: groupCall => {
const localDeviceState = groupCall.getLocalDeviceState();
@@ -1153,6 +1157,14 @@ export class CallingClass {
groupCall.resendMediaKeys();
}
+ public sendGroupCallRaiseHand(conversationId: string, raise: boolean): void {
+ const groupCall = this.getGroupCall(conversationId);
+ if (!groupCall) {
+ throw new Error('Could not find matching call');
+ }
+ groupCall.raiseHand(raise);
+ }
+
public sendGroupCallReaction(conversationId: string, value: string): void {
const groupCall = this.getGroupCall(conversationId);
if (!groupCall) {
diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts
index 59004fe3c2..604011bd14 100644
--- a/ts/state/ducks/calling.ts
+++ b/ts/state/ducks/calling.ts
@@ -116,6 +116,7 @@ export type GroupCallStateType = {
localDemuxId: number | undefined;
joinState: GroupCallJoinState;
peekInfo?: GroupCallPeekInfoType;
+ raisedHands?: Array
;
remoteParticipants: Array;
remoteAudioLevels?: Map;
} & GroupCallRingStateType;
@@ -222,11 +223,15 @@ type IncomingGroupCallType = ReadonlyDeep<{
ringerAci: AciString;
}>;
+export type SendGroupCallRaiseHandType = ReadonlyDeep<{
+ conversationId: string;
+ raise: boolean;
+}>;
+
export type SendGroupCallReactionType = ReadonlyDeep<{
conversationId: string;
value: string;
}>;
-
type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
conversationId: string;
value: string;
@@ -445,6 +450,7 @@ const CHANGE_IO_DEVICE_FULFILLED = 'calling/CHANGE_IO_DEVICE_FULFILLED';
const CLOSE_NEED_PERMISSION_SCREEN = 'calling/CLOSE_NEED_PERMISSION_SCREEN';
const DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL';
const GROUP_CALL_AUDIO_LEVELS_CHANGE = 'calling/GROUP_CALL_AUDIO_LEVELS_CHANGE';
+const GROUP_CALL_RAISED_HANDS_CHANGE = 'calling/GROUP_CALL_RAISED_HANDS_CHANGE';
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
@@ -455,6 +461,7 @@ const MARK_CALL_TRUSTED = 'calling/MARK_CALL_TRUSTED';
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
const PEEK_GROUP_CALL_FULFILLED = 'calling/PEEK_GROUP_CALL_FULFILLED';
+const RAISE_HAND_GROUP_CALL = 'calling/RAISE_HAND_GROUP_CALL';
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
@@ -525,6 +532,16 @@ type GroupCallAudioLevelsChangeActionType = ReadonlyDeep<{
payload: GroupCallAudioLevelsChangeActionPayloadType;
}>;
+type GroupCallRaisedHandsChangeActionPayloadType = ReadonlyDeep<{
+ conversationId: string;
+ raisedHands: ReadonlyArray;
+}>;
+
+type GroupCallRaisedHandsChangeActionType = ReadonlyDeep<{
+ type: 'calling/GROUP_CALL_RAISED_HANDS_CHANGE';
+ payload: GroupCallRaisedHandsChangeActionPayloadType;
+}>;
+
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type GroupCallStateChangeActionType = {
type: 'calling/GROUP_CALL_STATE_CHANGE';
@@ -580,6 +597,11 @@ type KeyChangeOkActionType = ReadonlyDeep<{
payload: null;
}>;
+type SendGroupCallRaiseHandActionType = ReadonlyDeep<{
+ type: 'calling/RAISE_HAND_GROUP_CALL';
+ payload: SendGroupCallRaiseHandType;
+}>;
+
export type SendGroupCallReactionActionType = ReadonlyDeep<{
type: 'calling/SEND_GROUP_CALL_REACTION';
payload: SendGroupCallReactionLocalCopyType;
@@ -692,6 +714,7 @@ export type CallingActionType =
| ConversationRemovedActionType
| DeclineCallActionType
| GroupCallAudioLevelsChangeActionType
+ | GroupCallRaisedHandsChangeActionType
| GroupCallStateChangeActionType
| GroupCallReactionsReceivedActionType
| GroupCallReactionsExpiredActionType
@@ -950,6 +973,12 @@ function receiveGroupCallReactions(
};
}
+function groupCallRaisedHandsChange(
+ payload: GroupCallRaisedHandsChangeActionPayloadType
+): GroupCallRaisedHandsChangeActionType {
+ return { type: GROUP_CALL_RAISED_HANDS_CHANGE, payload };
+}
+
function groupCallStateChange(
payload: GroupCallStateChangeArgumentType
): ThunkAction {
@@ -1075,6 +1104,19 @@ function keyChangeOk(
};
}
+function sendGroupCallRaiseHand(
+ payload: SendGroupCallRaiseHandType
+): ThunkAction {
+ return dispatch => {
+ calling.sendGroupCallRaiseHand(payload.conversationId, payload.raise);
+
+ dispatch({
+ type: RAISE_HAND_GROUP_CALL,
+ payload,
+ });
+ };
+}
+
function sendGroupCallReaction(
payload: SendGroupCallReactionType
): ThunkAction<
@@ -1612,6 +1654,7 @@ export const actions = {
declineCall,
getPresentingSources,
groupCallAudioLevelsChange,
+ groupCallRaisedHandsChange,
groupCallStateChange,
hangUpActiveCall,
keyChangeOk,
@@ -1630,6 +1673,7 @@ export const actions = {
remoteSharingScreenChange,
remoteVideoChange,
returnToActiveCall,
+ sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
setIsCallActive,
@@ -2137,6 +2181,7 @@ export function reducer(
localDemuxId,
peekInfo: newPeekInfo,
remoteParticipants,
+ raisedHands: existingCall?.raisedHands ?? [],
...newRingState,
},
},
@@ -2267,6 +2312,29 @@ export function reducer(
};
}
+ if (action.type === GROUP_CALL_RAISED_HANDS_CHANGE) {
+ const { conversationId, raisedHands } = action.payload;
+
+ const { activeCallState } = state;
+ const existingCall = getGroupCall(conversationId, state);
+
+ if (
+ state.activeCallState?.conversationId !== conversationId ||
+ !activeCallState ||
+ !existingCall
+ ) {
+ return state;
+ }
+
+ return {
+ ...state,
+ callsByConversation: {
+ ...callsByConversation,
+ [conversationId]: { ...existingCall, raisedHands: [...raisedHands] },
+ },
+ };
+ }
+
if (action.type === REMOTE_SHARING_SCREEN_CHANGE) {
const { conversationId, isSharingScreen } = action.payload;
const call = getOwn(state.callsByConversation, conversationId);
diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx
index caadc879dc..0b038d397f 100644
--- a/ts/state/smart/CallManager.tsx
+++ b/ts/state/smart/CallManager.tsx
@@ -13,6 +13,7 @@ import { getActiveCall } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
import { getIncomingCall } from '../selectors/calling';
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
+import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
import type {
ActiveCallBaseType,
@@ -201,6 +202,8 @@ const mapStateToActiveCallProp = (
const remoteParticipants: Array = [];
const peekedParticipants: Array = [];
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
+ const { localDemuxId } = call;
+ const raisedHands: Set = new Set(call.raisedHands ?? []);
const { memberships = [] } = conversation;
@@ -243,6 +246,7 @@ const mapStateToActiveCallProp = (
demuxId: remoteParticipant.demuxId,
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
+ isHandRaised: raisedHands.has(remoteParticipant.demuxId),
presenting: remoteParticipant.presenting,
sharingScreen: remoteParticipant.sharingScreen,
speakerTime: remoteParticipant.speakerTime,
@@ -254,6 +258,17 @@ const mapStateToActiveCallProp = (
);
}
+ if (localDemuxId !== undefined) {
+ conversationsByDemuxId.set(localDemuxId, getMe(state));
+ }
+
+ // Filter raisedHands to ensure valid demuxIds.
+ raisedHands.forEach(demuxId => {
+ if (!conversationsByDemuxId.has(demuxId)) {
+ raisedHands.delete(demuxId);
+ }
+ });
+
for (
let i = 0;
i < activeCallState.safetyNumberChangedAcis.length;
@@ -293,9 +308,10 @@ const mapStateToActiveCallProp = (
groupMembers,
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
joinState: call.joinState,
- localDemuxId: call.localDemuxId,
+ localDemuxId,
maxDevices: peekInfo.maxDevices,
peekedParticipants,
+ raisedHands,
remoteParticipants,
remoteAudioLevels: call.remoteAudioLevels || new Map(),
} satisfies ActiveGroupCallType;
@@ -360,6 +376,7 @@ const mapStateToProps = (state: StateType) => {
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
+ isGroupCallRaiseHandEnabled: isGroupCallRaiseHandEnabled(),
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
incomingCall,
me: getMe(state),
diff --git a/ts/test-electron/state/ducks/calling_test.ts b/ts/test-electron/state/ducks/calling_test.ts
index b7ead4b0fe..b2a43c9e99 100644
--- a/ts/test-electron/state/ducks/calling_test.ts
+++ b/ts/test-electron/state/ducks/calling_test.ts
@@ -940,6 +940,7 @@ describe('calling duck', () => {
videoAspectRatio: 4 / 3,
},
],
+ raisedHands: [],
}
);
});
@@ -997,6 +998,7 @@ describe('calling duck', () => {
videoAspectRatio: 16 / 9,
},
],
+ raisedHands: [],
}
);
});
diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts
index 1cb236243b..b29da3b6e9 100644
--- a/ts/types/Calling.ts
+++ b/ts/types/Calling.ts
@@ -96,6 +96,7 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
groupMembers: Array>;
isConversationTooBigToRing: boolean;
peekedParticipants: Array;
+ raisedHands: Set;
remoteParticipants: Array;
remoteAudioLevels: Map;
};
@@ -158,6 +159,7 @@ export type GroupCallRemoteParticipantType = ConversationType & {
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
+ isHandRaised: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;
diff --git a/ts/util/isGroupCallRaiseHandEnabled.ts b/ts/util/isGroupCallRaiseHandEnabled.ts
new file mode 100644
index 0000000000..efe16dbe88
--- /dev/null
+++ b/ts/util/isGroupCallRaiseHandEnabled.ts
@@ -0,0 +1,8 @@
+// Copyright 2023 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import * as RemoteConfig from '../RemoteConfig';
+
+export function isGroupCallRaiseHandEnabled(): boolean {
+ return Boolean(RemoteConfig.isEnabled('desktop.internalUser'));
+}
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index a9a4fcf8ae..d2e526d528 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -3058,6 +3058,13 @@
"reasonCategory": "usageTrusted",
"updated": "2023-10-26T13:57:41.860Z"
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/CallingToastManager.tsx",
+ "line": " const raisedHandsInLastShownToastRef = useRef>(new Set());",
+ "reasonCategory": "usageTrusted",
+ "updated": "2023-12-05T22:11:41.559Z"
+ },
{
"rule": "React-useRef",
"path": "ts/components/CallsList.tsx",