Call link admin key fix and in-call approve, deny, remove

This commit is contained in:
ayumi-signal 2024-04-30 09:36:34 -07:00 committed by GitHub
parent 5df8924197
commit 8ec585d54c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 599 additions and 43 deletions

View file

@ -1685,6 +1685,18 @@
"messageformat": "More options",
"description": "Tooltip label for button in the calling screen that opens a menu with other call actions such as React or Raise Hand."
},
"icu:CallingPendingParticipants__ApproveUser": {
"messageformat": "Approve join request",
"description": "Tooltip label for check mark button to approve a user's request to join a call."
},
"icu:CallingPendingParticipants__DenyUser": {
"messageformat": "Deny join request",
"description": "Tooltip label for check mark button to deny a user's request to join a call."
},
"icu:CallingPendingParticipants__RequestsToJoin": {
"messageformat": "{count, plural, one {# request} other {# requests}} to join the call",
"description": "Shown in the call pending join request list to describe how many people are requesting to join"
},
"icu:CallingRaisedHandsList__Title": {
"messageformat": "Raised hands · {count, plural, one {# person} other {# people}}",
"description": "Shown in the call raised hands list to describe how many people have active raised hands"
@ -3654,6 +3666,10 @@
"messageformat": "Copy link",
"description": "Menu item in the in-call info popup for call link calls. The action is to add the call link to the clipboard."
},
"icu:CallingAdhocCallInfo__RemoveClient": {
"messageformat": "Remove this person from the call",
"description": "Button in the in-call info popup for call link calls showing all participants. The action is to remove the participant from the call."
},
"icu:callingDeviceSelection__label--video": {
"messageformat": "Video",
"description": "Label for video input selector"

View file

@ -4511,6 +4511,13 @@ button.module-image__border-overlay:focus {
$color-white
);
}
&__remove {
@include color-svg(
'../images/icons/v3/minus/minus-circle-compact.svg',
$color-white
);
}
}
.module-call-need-permission-screen {

View file

@ -81,3 +81,11 @@
margin-inline: 10px;
border: 1px solid $color-gray-65;
}
.CallingAdhocCallInfo__RemoveClient {
@include button-reset;
width: 16px;
height: 16px;
margin-inline: 8px;
background: $color-white;
}

View file

@ -0,0 +1,34 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.CallingPendingParticipants {
width: 420px;
height: auto;
padding-block-end: 2px;
margin-block-start: auto;
margin-block-end: 36px;
margin-inline-start: auto;
margin-inline-end: auto;
}
.CallingPendingParticipants__PendingActionButton {
padding-inline: 0;
margin-inline-end: 16px;
}
.CallingPendingParticipants__PendingActionButton:last-child {
margin-inline-end: 8px;
}
.CallingPendingParticipants__PendingActionButtonIcon {
width: 20px;
height: 20px;
}
.CallingPendingParticipants__PendingActionButtonIcon--Approve {
@include color-svg('../images/icons/v3/check/check.svg', $color-white);
}
.CallingPendingParticipants__PendingActionButtonIcon--Deny {
@include color-svg('../images/icons/v3/x/x.svg', $color-white);
}

View file

@ -45,6 +45,7 @@
@import './components/CallControls.scss';
@import './components/CallSettingsButton.scss';
@import './components/CallingLobby.scss';
@import './components/CallingPendingParticipants.scss';
@import './components/CallingPreCallInfo.scss';
@import './components/CallingScreenSharingController.scss';
@import './components/CallingSelectPresentingSourcesModal.scss';

View file

@ -59,12 +59,14 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
...storyProps,
availableCameras: [],
acceptCall: action('accept-call'),
approveUser: action('approve-user'),
bounceAppIconStart: action('bounce-app-icon-start'),
bounceAppIconStop: action('bounce-app-icon-stop'),
cancelCall: action('cancel-call'),
changeCallView: action('change-call-view'),
closeNeedPermissionScreen: action('close-need-permission-screen'),
declineCall: action('decline-call'),
denyUser: action('deny-user'),
getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
fakeGetGroupCallVideoFrameSource(demuxId),
getPresentingSources: action('get-presenting-sources'),
@ -84,6 +86,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
notifyForCall: action('notify-for-call'),
openSystemPreferencesAction: action('open-system-preferences-action'),
playRingtone: action('play-ringtone'),
removeClient: action('remove-client'),
renderDeviceSelection: () => <div />,
renderEmojiPicker: () => <>EmojiPicker</>,
renderReactionPicker: () => <div />,
@ -156,6 +159,7 @@ export function OngoingGroupCall(): JSX.Element {
groupMembers: [],
isConversationTooBigToRing: false,
peekedParticipants: [],
pendingParticipants: [],
raisedHands: new Set<number>(),
remoteParticipants: [],
remoteAudioLevels: new Map<number, number>(),

View file

@ -31,6 +31,8 @@ import type {
CancelCallType,
DeclineCallType,
GroupCallParticipantInfoType,
PendingUserActionPayloadType,
RemoveClientType,
SendGroupCallRaiseHandType,
SendGroupCallReactionType,
SetGroupCallVideoRequestType,
@ -95,9 +97,11 @@ export type PropsType = {
startCall: (payload: StartCallType) => void;
toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void;
approveUser: (payload: PendingUserActionPayloadType) => void;
bounceAppIconStart: () => unknown;
bounceAppIconStop: () => unknown;
declineCall: (_: DeclineCallType) => void;
denyUser: (payload: PendingUserActionPayloadType) => void;
hasInitialLoadCompleted: boolean;
i18n: LocalizerType;
isGroupCallRaiseHandEnabled: boolean;
@ -109,6 +113,7 @@ export type PropsType = {
) => unknown;
openSystemPreferencesAction: () => unknown;
playRingtone: () => unknown;
removeClient: (payload: RemoveClientType) => void;
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
@ -151,11 +156,13 @@ type ActiveCallManagerPropsType = {
function ActiveCallManager({
activeCall,
approveUser,
availableCameras,
callLink,
cancelCall,
changeCallView,
closeNeedPermissionScreen,
denyUser,
hangUpActiveCall,
i18n,
isGroupCallRaiseHandEnabled,
@ -166,6 +173,7 @@ function ActiveCallManager({
renderDeviceSelection,
renderEmojiPicker,
renderReactionPicker,
removeClient,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
@ -258,6 +266,7 @@ function ActiveCallManager({
let isConvoTooBigToRing = false;
let isAdhocAdminApprovalRequired = false;
let isAdhocJoinRequestPending = false;
let isCallLinkAdmin = false;
switch (activeCall.callMode) {
case CallMode.Direct: {
@ -292,6 +301,7 @@ function ActiveCallManager({
isAdhocJoinRequestPending =
isAdhocAdminApprovalRequired &&
activeCall.joinState === GroupCallJoinState.Pending;
isCallLinkAdmin = Boolean(callLink?.adminKey);
break;
}
default:
@ -352,10 +362,12 @@ function ActiveCallManager({
<CallingAdhocCallInfo
callLink={callLink}
i18n={i18n}
isCallLinkAdmin={isCallLinkAdmin}
ourServiceId={me.serviceId}
participants={peekedParticipants}
onClose={toggleParticipants}
onCopyCallLink={onCopyCallLink}
removeClient={removeClient}
/>
) : (
<CallingParticipantsList
@ -388,6 +400,7 @@ function ActiveCallManager({
hasRemoteVideo: hasLocalVideo,
isHandRaised,
presenting: Boolean(activeCall.presentingSource),
demuxId: activeCall.localDemuxId,
},
]
: [];
@ -396,12 +409,15 @@ function ActiveCallManager({
<>
<CallScreen
activeCall={activeCall}
approveUser={approveUser}
changeCallView={changeCallView}
denyUser={denyUser}
getPresentingSources={getPresentingSources}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
groupMembers={groupMembers}
hangUpActiveCall={hangUpActiveCall}
i18n={i18n}
isCallLinkAdmin={isCallLinkAdmin}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
me={me}
openSystemPreferencesAction={openSystemPreferencesAction}
@ -438,10 +454,12 @@ function ActiveCallManager({
<CallingAdhocCallInfo
callLink={callLink}
i18n={i18n}
isCallLinkAdmin={isCallLinkAdmin}
ourServiceId={me.serviceId}
participants={groupCallParticipantsForParticipantsList}
onClose={toggleParticipants}
onCopyCallLink={onCopyCallLink}
removeClient={removeClient}
/>
) : (
<CallingParticipantsList
@ -458,6 +476,7 @@ function ActiveCallManager({
export function CallManager({
acceptCall,
activeCall,
approveUser,
availableCameras,
bounceAppIconStart,
bounceAppIconStop,
@ -466,6 +485,7 @@ export function CallManager({
changeCallView,
closeNeedPermissionScreen,
declineCall,
denyUser,
getGroupCallVideoFrameSource,
getPresentingSources,
hangUpActiveCall,
@ -479,6 +499,7 @@ export function CallManager({
openSystemPreferencesAction,
pauseVoiceNotePlayer,
playRingtone,
removeClient,
renderDeviceSelection,
renderEmojiPicker,
renderReactionPicker,
@ -552,10 +573,12 @@ export function CallManager({
<ActiveCallManager
activeCall={activeCall}
availableCameras={availableCameras}
approveUser={approveUser}
callLink={callLink}
cancelCall={cancelCall}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
denyUser={denyUser}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall}
@ -564,6 +587,7 @@ export function CallManager({
me={me}
openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
removeClient={removeClient}
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}

View file

@ -67,6 +67,7 @@ type GroupCallOverrideProps = OverridePropsBase & {
callMode: CallMode.Group;
connectionState?: GroupCallConnectionState;
peekedParticipants?: Array<ConversationType>;
pendingParticipants?: Array<ConversationType>;
raisedHands?: Set<number>;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
remoteAudioLevel?: number;
@ -135,6 +136,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
isConversationTooBigToRing: false,
peekedParticipants:
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
pendingParticipants: overrideProps.pendingParticipants || [],
raisedHands:
overrideProps.raisedHands ||
getRaisedHands(overrideProps) ||
@ -181,11 +183,14 @@ const createProps = (
}
): PropsType => ({
activeCall: createActiveCallProp(overrideProps),
approveUser: action('approve-user'),
changeCallView: action('change-call-view'),
denyUser: action('deny-user'),
getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource,
getPresentingSources: action('get-presenting-sources'),
hangUpActiveCall: action('hang-up'),
i18n,
isCallLinkAdmin: true,
isGroupCallRaiseHandEnabled: true,
me: getDefaultConversation({
color: AvatarColors[1],

View file

@ -8,6 +8,7 @@ import classNames from 'classnames';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import type {
ActiveCallStateType,
PendingUserActionPayloadType,
SendGroupCallRaiseHandType,
SendGroupCallReactionType,
SetLocalAudioType,
@ -88,14 +89,18 @@ import {
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
import { assertDev } from '../util/assert';
import { emojiToData } from './emoji/lib';
import { CallingPendingParticipants } from './CallingPendingParticipants';
export type PropsType = {
activeCall: ActiveCallType;
approveUser: (payload: PendingUserActionPayloadType) => void;
denyUser: (payload: PendingUserActionPayloadType) => void;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
getPresentingSources: () => void;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
hangUpActiveCall: (reason: string) => void;
i18n: LocalizerType;
isCallLinkAdmin: boolean;
isGroupCallRaiseHandEnabled: boolean;
me: ConversationType;
openSystemPreferencesAction: () => unknown;
@ -178,12 +183,15 @@ function CallDuration({
export function CallScreen({
activeCall,
approveUser,
changeCallView,
denyUser,
getGroupCallVideoFrameSource,
getPresentingSources,
groupMembers,
hangUpActiveCall,
i18n,
isCallLinkAdmin,
isGroupCallRaiseHandEnabled,
me,
openSystemPreferencesAction,
@ -396,6 +404,11 @@ export function CallScreen({
throw missingCaseError(activeCall);
}
const pendingParticipants =
activeCall.callMode === CallMode.Adhoc && isCallLinkAdmin
? activeCall.pendingParticipants
: [];
let lonelyInCallNode: ReactNode;
let localPreviewNode: ReactNode;
@ -811,6 +824,15 @@ export function CallScreen({
renderRaisedHandsToast={renderRaisedHandsToast}
i18n={i18n}
/>
{pendingParticipants.length ? (
<CallingPendingParticipants
i18n={i18n}
ourServiceId={me.serviceId}
participants={pendingParticipants}
approveUser={approveUser}
denyUser={denyUser}
/>
) : null}
{/* We render the local preview first and set the footer flex direction to row-reverse
to ensure the preview is visible at low viewport widths. */}
<div className="module-ongoing-call__footer">

View file

@ -61,10 +61,12 @@ function getCallLink(overrideProps: Partial<CallLinkType> = {}): CallLinkType {
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
callLink: getCallLink(overrideProps.callLink || {}),
i18n,
isCallLinkAdmin: overrideProps.isCallLinkAdmin || false,
ourServiceId: generateAci(),
participants: overrideProps.participants || [],
onClose: action('on-close'),
onCopyCallLink: action('on-copy-call-link'),
removeClient: overrideProps.removeClient || action('remove-client'),
});
export default {

View file

@ -16,29 +16,35 @@ import { sortByTitle } from '../util/sortByTitle';
import type { ConversationType } from '../state/ducks/conversations';
import { ModalHost } from './ModalHost';
import { isInSystemContacts } from '../util/isInSystemContacts';
import type { RemoveClientType } from '../state/ducks/calling';
type ParticipantType = ConversationType & {
hasRemoteAudio?: boolean;
hasRemoteVideo?: boolean;
isHandRaised?: boolean;
presenting?: boolean;
demuxId?: number;
};
export type PropsType = {
readonly callLink: CallLinkType;
readonly i18n: LocalizerType;
readonly isCallLinkAdmin: boolean;
readonly ourServiceId: ServiceIdString | undefined;
readonly participants: Array<ParticipantType>;
readonly onClose: () => void;
readonly onCopyCallLink: () => void;
readonly removeClient: ((payload: RemoveClientType) => void) | null;
};
export function CallingAdhocCallInfo({
i18n,
isCallLinkAdmin,
ourServiceId,
participants,
onClose,
onCopyCallLink,
removeClient,
}: PropsType): JSX.Element | null {
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
() => sortByTitle(participants),
@ -137,6 +143,26 @@ export function CallingAdhocCallInfo({
'module-calling-participants-list__muted--audio'
)}
/>
{isCallLinkAdmin &&
removeClient &&
participant.demuxId &&
!(ourServiceId && participant.serviceId === ourServiceId) ? (
<button
aria-label={i18n('icu:CallingAdhocCallInfo__RemoveClient')}
className={classNames(
'CallingAdhocCallInfo__RemoveClient',
'module-calling-participants-list__status-icon',
'module-calling-participants-list__remove'
)}
onClick={() => {
if (!participant.demuxId) {
return;
}
removeClient({ demuxId: participant.demuxId });
}}
type="button"
/>
) : null}
</li>
)
)}

View file

@ -0,0 +1,101 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable react/no-array-index-key */
import React from 'react';
import { Avatar, AvatarSize } from './Avatar';
import { ContactName } from './conversation/ContactName';
import { InContactsIcon } from './InContactsIcon';
import type { LocalizerType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import { isInSystemContacts } from '../util/isInSystemContacts';
import type { PendingUserActionPayloadType } from '../state/ducks/calling';
import type { ServiceIdString } from '../types/ServiceId';
import { Button, ButtonVariant } from './Button';
export type PropsType = {
readonly i18n: LocalizerType;
readonly ourServiceId: ServiceIdString | undefined;
readonly participants: Array<ConversationType>;
readonly approveUser: (payload: PendingUserActionPayloadType) => void;
readonly denyUser: (payload: PendingUserActionPayloadType) => void;
};
export function CallingPendingParticipants({
i18n,
ourServiceId,
participants,
approveUser,
denyUser,
}: PropsType): JSX.Element | null {
return (
<div className="CallingPendingParticipants module-calling-participants-list">
<div className="module-calling-participants-list__header">
<div className="module-calling-participants-list__title">
{i18n('icu:CallingPendingParticipants__RequestsToJoin', {
count: participants.length,
})}
</div>
</div>
<ul className="module-calling-participants-list__list">
{participants.map((participant: ConversationType, index: number) => (
<li className="module-calling-participants-list__contact" key={index}>
<div className="module-calling-participants-list__avatar-and-name">
<Avatar
acceptedMessageRequest={participant.acceptedMessageRequest}
avatarPath={participant.avatarPath}
badge={undefined}
color={participant.color}
conversationType="direct"
i18n={i18n}
isMe={participant.isMe}
profileName={participant.profileName}
title={participant.title}
sharedGroupNames={participant.sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
/>
{ourServiceId && participant.serviceId === ourServiceId ? (
<span className="module-calling-participants-list__name">
{i18n('icu:you')}
</span>
) : (
<>
<ContactName
module="module-calling-participants-list__name"
title={participant.title}
/>
{isInSystemContacts(participant) ? (
<span>
{' '}
<InContactsIcon
className="module-calling-participants-list__contact-icon"
i18n={i18n}
/>
</span>
) : null}
</>
)}
</div>
<Button
aria-label={i18n('icu:CallingPendingParticipants__DenyUser')}
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
onClick={() => denyUser({ serviceId: participant.serviceId })}
variant={ButtonVariant.Destructive}
>
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Deny" />
</Button>
<Button
aria-label={i18n('icu:CallingPendingParticipants__ApproveUser')}
className="CallingPendingParticipants__PendingActionButton CallingButton__icon"
onClick={() => approveUser({ serviceId: participant.serviceId })}
variant={ButtonVariant.Calling}
>
<span className="CallingPendingParticipants__PendingActionButtonIcon CallingPendingParticipants__PendingActionButtonIcon--Approve" />
</Button>
</li>
))}
</ul>
</div>
);
}

View file

@ -139,6 +139,7 @@ export function GroupCall(args: PropsType): JSX.Element {
maxDevices: 5,
deviceCount: 0,
peekedParticipants: [],
pendingParticipants: [],
raisedHands: new Set<number>(),
remoteParticipants: [],
remoteAudioLevels: new Map<number, number>(),

View file

@ -40,7 +40,7 @@ import {
RingRTC,
RingUpdate,
} from '@signalapp/ringrtc';
import { uniqBy, noop } from 'lodash';
import { uniqBy, noop, compact } from 'lodash';
import Long from 'long';
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
@ -125,11 +125,13 @@ import {
} from '../util/callDisposition';
import { isNormalNumber } from '../util/isNormalNumber';
import { LocalCallEvent } from '../types/CallDisposition';
import { isServiceIdString, type ServiceIdString } from '../types/ServiceId';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import { isServiceIdString } from '../types/ServiceId';
import { isInSystemContacts } from '../util/isInSystemContacts';
import {
getRoomIdFromRootKey,
getCallLinkAuthCredentialPresentation,
toAdminKeyBytes,
} from '../util/callLinks';
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
import { conversationJobQueue } from '../jobs/conversationJobQueue';
@ -580,10 +582,12 @@ export class CallingClass {
async startCallLinkLobby({
callLinkRootKey,
adminPasskey,
hasLocalAudio,
hasLocalVideo = true,
}: Readonly<{
callLinkRootKey: CallLinkRootKey;
adminPasskey: Buffer | undefined;
hasLocalAudio: boolean;
hasLocalVideo?: boolean;
}>): Promise<
@ -610,7 +614,7 @@ export class CallingClass {
roomId,
authCredentialPresentation,
callLinkRootKey,
adminPasskey: undefined,
adminPasskey,
});
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
@ -1210,11 +1214,13 @@ export class CallingClass {
public async joinCallLinkCall({
roomId,
rootKey,
adminKey,
hasLocalAudio,
hasLocalVideo,
}: {
roomId: string;
rootKey: string;
adminKey: string | undefined;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
}): Promise<void> {
@ -1228,13 +1234,16 @@ export class CallingClass {
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
const authCredentialPresentation =
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
const adminPasskey = adminKey
? Buffer.from(toAdminKeyBytes(adminKey))
: undefined;
// RingRTC reuses the same type GroupCall between Adhoc and Group calls.
const groupCall = this.connectCallLinkCall({
roomId,
authCredentialPresentation,
callLinkRootKey,
adminPasskey: undefined,
adminPasskey,
});
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
@ -1267,6 +1276,33 @@ export class CallingClass {
groupCall.setGroupMembers(this.getGroupCallMembers(conversationId));
}
public approveUser(conversationId: string, aci: AciString): void {
const groupCall = this.getGroupCall(conversationId);
if (!groupCall) {
throw new Error('Could not find matching call');
}
groupCall.approveUser(Buffer.from(uuidToBytes(aci)));
}
public denyUser(conversationId: string, aci: AciString): void {
const groupCall = this.getGroupCall(conversationId);
if (!groupCall) {
throw new Error('Could not find matching call');
}
groupCall.denyUser(Buffer.from(uuidToBytes(aci)));
}
public removeClient(conversationId: string, demuxId: number): void {
const groupCall = this.getGroupCall(conversationId);
if (!groupCall) {
throw new Error('Could not find matching call');
}
groupCall.removeClient(demuxId);
}
// See the comment in types/Calling.ts to explain why we have to do this conversion.
private convertRingRtcConnectionState(
connectionState: ConnectionState
@ -1301,6 +1337,18 @@ export class CallingClass {
}
}
private formatUserId(userId: Buffer): AciString | null {
const uuid = bytesToUuid(userId);
if (uuid && isAciString(uuid)) {
return uuid;
}
log.error(
'Calling.formatUserId: could not convert participant UUID Uint8Array to string'
);
return null;
}
public formatGroupCallPeekInfoForRedux(
peekInfo: PeekInfo
): GroupCallPeekInfoType {
@ -1308,17 +1356,10 @@ export class CallingClass {
return {
acis: peekInfo.devices.map(peekDeviceInfo => {
if (peekDeviceInfo.userId) {
const uuid = bytesToUuid(peekDeviceInfo.userId);
const uuid = this.formatUserId(peekDeviceInfo.userId);
if (uuid) {
assertDev(
isAciString(uuid),
'peeked participant uuid must be an ACI'
);
return uuid;
}
log.error(
'Calling.formatGroupCallPeekInfoForRedux: could not convert peek UUID Uint8Array to string; using fallback UUID'
);
} else {
log.error(
'Calling.formatGroupCallPeekInfoForRedux: device had no user ID; using fallback UUID'
@ -1329,6 +1370,9 @@ export class CallingClass {
'formatGrouPCallPeekInfoForRedux'
);
}),
pendingAcis: compact(
peekInfo.pendingUsers.map(userId => this.formatUserId(userId))
),
creatorAci:
creatorAci !== undefined
? normalizeAci(

View file

@ -49,11 +49,12 @@ import { requestCameraPermissions } from '../../util/callingPermissions';
import {
CALL_LINK_DEFAULT_STATE,
getRoomIdFromRootKey,
toAdminKeyBytes,
} from '../../util/callLinks';
import { sendCallLinkUpdateSync } from '../../util/sendCallLinkUpdateSync';
import { sleep } from '../../util/sleep';
import { LatestQueue } from '../../util/LatestQueue';
import type { AciString } from '../../types/ServiceId';
import type { AciString, ServiceIdString } from '../../types/ServiceId';
import type {
ConversationChangedActionType,
ConversationRemovedActionType,
@ -81,11 +82,13 @@ import { SHOW_ERROR_MODAL } from './globalModals';
import { ButtonVariant } from '../../components/Button';
import { getConversationIdForLogging } from '../../util/idForLogging';
import dataInterface from '../../sql/Client';
import { isAciString } from '../../util/isAciString';
// State
export type GroupCallPeekInfoType = ReadonlyDeep<{
acis: Array<AciString>;
pendingAcis: Array<AciString>;
creatorAci?: AciString;
eraId?: string;
maxDevices: number;
@ -250,7 +253,7 @@ type HangUpActionPayloadType = ReadonlyDeep<{
conversationId: string;
}>;
type HandleCallLinkUpdateType = ReadonlyDeep<{
export type HandleCallLinkUpdateType = ReadonlyDeep<{
rootKey: string;
adminKey: string | null;
}>;
@ -309,6 +312,10 @@ type RemoteSharingScreenChangeType = ReadonlyDeep<{
isSharingScreen: boolean;
}>;
export type RemoveClientType = ReadonlyDeep<{
demuxId: number;
}>;
export type SetLocalAudioType = ReadonlyDeep<{
enabled: boolean;
}>;
@ -558,10 +565,12 @@ const doGroupCallPeek = ({
// Actions
const ACCEPT_CALL_PENDING = 'calling/ACCEPT_CALL_PENDING';
const APPROVE_USER = 'calling/APPROVE_USER';
const CANCEL_CALL = 'calling/CANCEL_CALL';
const CANCEL_INCOMING_GROUP_CALL_RING =
'calling/CANCEL_INCOMING_GROUP_CALL_RING';
const CHANGE_CALL_VIEW = 'calling/CHANGE_CALL_VIEW';
const DENY_USER = 'calling/DENY_USER';
const START_CALLING_LOBBY = 'calling/START_CALLING_LOBBY';
const START_CALL_LINK_LOBBY = 'calling/START_CALL_LINK_LOBBY';
const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED';
@ -584,6 +593,7 @@ 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';
const REMOVE_CLIENT = 'calling/REMOVE_CLIENT';
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
const SEND_GROUP_CALL_REACTION = 'calling/SEND_GROUP_CALL_REACTION';
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
@ -605,6 +615,10 @@ type AcceptCallPendingActionType = ReadonlyDeep<{
payload: AcceptCallType;
}>;
type ApproveUserActionType = ReadonlyDeep<{
type: 'calling/APPROVE_USER';
}>;
type CancelCallActionType = ReadonlyDeep<{
type: 'calling/CANCEL_CALL';
}>;
@ -614,6 +628,10 @@ type CancelIncomingGroupCallRingActionType = ReadonlyDeep<{
payload: CancelIncomingGroupCallRingType;
}>;
type DenyUserActionType = ReadonlyDeep<{
type: 'calling/DENY_USER';
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type StartCallingLobbyActionType = {
type: 'calling/START_CALLING_LOBBY';
@ -751,6 +769,10 @@ export type PeekGroupCallFulfilledActionType = ReadonlyDeep<{
};
}>;
export type PendingUserActionPayloadType = ReadonlyDeep<{
serviceId: ServiceIdString | undefined;
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type RefreshIODevicesActionType = {
type: 'calling/REFRESH_IO_DEVICES';
@ -767,6 +789,10 @@ type RemoteVideoChangeActionType = ReadonlyDeep<{
payload: RemoteVideoChangeType;
}>;
type RemoveClientActionType = ReadonlyDeep<{
type: 'calling/REMOVE_CLIENT';
}>;
type ReturnToActiveCallActionType = ReadonlyDeep<{
type: 'calling/RETURN_TO_ACTIVE_CALL';
}>;
@ -833,10 +859,12 @@ type SwitchFromPresentationViewActionType = ReadonlyDeep<{
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type CallingActionType =
| ApproveUserActionType
| AcceptCallPendingActionType
| CancelCallActionType
| CancelIncomingGroupCallRingActionType
| ChangeCallViewActionType
| DenyUserActionType
| StartCallingLobbyActionType
| StartCallLinkLobbyActionType
| CallStateChangeFulfilledActionType
@ -860,6 +888,7 @@ export type CallingActionType =
| RefreshIODevicesActionType
| RemoteSharingScreenChangeActionType
| RemoteVideoChangeActionType
| RemoveClientActionType
| ReturnToActiveCallActionType
| SendGroupCallReactionActionType
| SetLocalAudioActionType
@ -911,6 +940,68 @@ function acceptCall(
};
}
function approveUser(
payload: PendingUserActionPayloadType
): ThunkAction<void, RootStateType, unknown, ApproveUserActionType> {
return (dispatch, getState) => {
const activeCall = getActiveCall(getState().calling);
if (!activeCall || !isGroupOrAdhocCallMode(activeCall.callMode)) {
log.warn(
'approveUser: Trying to approve pending user without active group or adhoc call'
);
return;
}
if (!isAciString(payload.serviceId)) {
log.warn(
'approveUser: Trying to approve pending user without valid aci serviceid'
);
return;
}
calling.approveUser(activeCall.conversationId, payload.serviceId);
dispatch({ type: APPROVE_USER });
};
}
function denyUser(
payload: PendingUserActionPayloadType
): ThunkAction<void, RootStateType, unknown, DenyUserActionType> {
return (dispatch, getState) => {
const activeCall = getActiveCall(getState().calling);
if (!activeCall || !isGroupOrAdhocCallMode(activeCall.callMode)) {
log.warn(
'approveUser: Trying to approve pending user without active group or adhoc call'
);
return;
}
if (!isAciString(payload.serviceId)) {
log.warn(
'approveUser: Trying to approve pending user without valid aci serviceid'
);
return;
}
calling.denyUser(activeCall.conversationId, payload.serviceId);
dispatch({ type: DENY_USER });
};
}
function removeClient(
payload: RemoveClientType
): ThunkAction<void, RootStateType, unknown, RemoveClientActionType> {
return (dispatch, getState) => {
const activeCall = getActiveCall(getState().calling);
if (!activeCall || !isGroupOrAdhocCallMode(activeCall.callMode)) {
log.warn(
'approveUser: Trying to approve pending user without active group or adhoc call'
);
return;
}
calling.removeClient(activeCall.conversationId, payload.demuxId);
dispatch({ type: REMOVE_CLIENT });
};
}
function callStateChange(
payload: CallStateChangeType
): ThunkAction<
@ -1869,8 +1960,13 @@ const _startCallLinkLobby = async ({
groupCall?.remoteParticipants.length ||
0;
const { adminKey } = getOwn(state.calling.callLinks, roomId) ?? {};
const adminPasskey = adminKey
? Buffer.from(toAdminKeyBytes(adminKey))
: undefined;
const callLobbyData = await calling.startCallLinkLobby({
callLinkRootKey,
adminPasskey,
hasLocalAudio: groupCallDeviceCount < 8,
});
if (!callLobbyData) {
@ -2003,6 +2099,7 @@ function startCall(
await calling.joinCallLinkCall({
roomId: conversationId,
rootKey: callLink.rootKey,
adminKey: callLink.adminKey ?? undefined,
hasLocalAudio,
hasLocalVideo,
});
@ -2061,6 +2158,7 @@ function switchFromPresentationView(): SwitchFromPresentationViewActionType {
}
export const actions = {
acceptCall,
approveUser,
callStateChange,
cancelCall,
cancelIncomingGroupCallRing,
@ -2068,6 +2166,7 @@ export const actions = {
changeIODevice,
closeNeedPermissionScreen,
declineCall,
denyUser,
getPresentingSources,
groupCallAudioLevelsChange,
groupCallEnded,
@ -2089,6 +2188,7 @@ export const actions = {
refreshIODevices,
remoteSharingScreenChange,
remoteVideoChange,
removeClient,
returnToActiveCall,
sendGroupCallRaiseHand,
sendGroupCallReaction,
@ -2237,6 +2337,7 @@ export function reducer(
peekInfo: peekInfo ||
existingCall?.peekInfo || {
acis: remoteParticipants.map(({ aci }) => aci),
pendingAcis: [],
maxDevices: Infinity,
deviceCount: remoteParticipants.length,
},
@ -2286,8 +2387,10 @@ export function reducer(
...callLinks,
[conversationId]: {
...action.payload.callLinkState,
rootKey: action.payload.callLinkRootKey,
adminKey: null,
rootKey:
callLinks[conversationId]?.rootKey ??
action.payload.callLinkRootKey,
adminKey: callLinks[conversationId]?.adminKey,
},
}
: callLinks,
@ -2478,6 +2581,7 @@ export function reducer(
localDemuxId: undefined,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: Infinity,
deviceCount: 0,
},
@ -2676,6 +2780,7 @@ export function reducer(
const newPeekInfo = peekInfo ||
existingCall?.peekInfo || {
acis: remoteParticipants.map(({ aci }) => aci),
pendingAcis: [],
maxDevices: Infinity,
deviceCount: remoteParticipants.length,
};
@ -2755,6 +2860,7 @@ export function reducer(
localDemuxId: undefined,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: Infinity,
deviceCount: 0,
},

View file

@ -211,6 +211,7 @@ const mapStateToActiveCallProp = (
const groupMembers: Array<ConversationType> = [];
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<ConversationType> = [];
const pendingParticipants: Array<ConversationType> = [];
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
const { localDemuxId } = call;
const raisedHands: Set<number> = new Set(call.raisedHands ?? []);
@ -224,6 +225,7 @@ const mapStateToActiveCallProp = (
deviceCount: 0,
maxDevices: Infinity,
acis: [],
pendingAcis: [],
},
} = call;
@ -294,6 +296,20 @@ const mapStateToActiveCallProp = (
peekedParticipants.push(peekedConversation);
}
for (let i = 0; i < peekInfo.pendingAcis.length; i += 1) {
const aci = peekInfo.pendingAcis[i];
// In call links, pending users may be unknown until they share profile keys.
// conversationSelectorByAci should create conversations for new contacts.
const pendingConversation = conversationSelectorByAci(aci);
if (!pendingConversation) {
log.error('Pending participant has no corresponding conversation');
continue;
}
pendingParticipants.push(pendingConversation);
}
return {
...baseResult,
callMode: call.callMode,
@ -306,6 +322,7 @@ const mapStateToActiveCallProp = (
localDemuxId,
maxDevices: peekInfo.maxDevices,
peekedParticipants,
pendingParticipants,
raisedHands,
remoteParticipants,
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
@ -407,6 +424,8 @@ export const SmartCallManager = memo(function SmartCallManager() {
: false;
const {
approveUser,
denyUser,
changeCallView,
closeNeedPermissionScreen,
getPresentingSources,
@ -416,6 +435,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
acceptCall,
declineCall,
openSystemPreferencesAction,
removeClient,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
@ -440,6 +460,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
<CallManager
acceptCall={acceptCall}
activeCall={activeCall}
approveUser={approveUser}
availableCameras={availableCameras}
bounceAppIconStart={bounceAppIconStart}
bounceAppIconStop={bounceAppIconStop}
@ -448,6 +469,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
declineCall={declineCall}
denyUser={denyUser}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall}
@ -461,6 +483,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
playRingtone={playRingtone}
removeClient={removeClient}
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}

View file

@ -1,7 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
import type { CallingConversationType } from '../../types/Calling';
import type { CallLinkType } from '../../types/CallLink';
import { CallLinkRestrictions } from '../../types/CallLink';
import { MONTH } from '../../util/durations/constants';
@ -26,6 +26,11 @@ export const FAKE_CALL_LINK_WITH_ADMIN_KEY: CallLinkType = {
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg',
};
export function getCallLinkState(callLink: CallLinkType): CallLinkStateType {
const { name, restrictions, expiration, revoked } = callLink;
return { name, restrictions, expiration, revoked };
}
export function getDefaultCallLinkConversation(
callLinkOverrideProps: Partial<CallLinkType> = {}
): CallingConversationType {

View file

@ -10,12 +10,15 @@ import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import type {
ActiveCallStateType,
CallingActionType,
CallingStateType,
DirectCallStateType,
GroupCallReactionsReceivedActionType,
GroupCallStateChangeActionType,
GroupCallStateType,
HandleCallLinkUpdateType,
SendGroupCallReactionActionType,
StartCallLinkLobbyType,
} from '../../../state/ducks/calling';
import {
actions,
@ -36,8 +39,11 @@ import {
import { generateAci } from '../../../types/ServiceId';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import type { UnwrapPromise } from '../../../types/Util';
import { CallLinkRestrictions } from '../../../types/CallLink';
import { FAKE_CALL_LINK } from '../../../test-both/helpers/fakeCallLink';
import {
FAKE_CALL_LINK,
FAKE_CALL_LINK_WITH_ADMIN_KEY,
getCallLinkState,
} from '../../../test-both/helpers/fakeCallLink';
const ACI_1 = generateAci();
const NOW = new Date('2020-01-23T04:56:00.000');
@ -109,6 +115,7 @@ describe('calling duck', () => {
localDemuxId: 1,
peekInfo: {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -902,6 +909,7 @@ describe('calling duck', () => {
hasLocalVideo: false,
peekInfo: {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -932,6 +940,7 @@ describe('calling duck', () => {
localDemuxId: 1,
peekInfo: {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -967,6 +976,7 @@ describe('calling duck', () => {
hasLocalVideo: false,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -995,6 +1005,7 @@ describe('calling duck', () => {
localDemuxId: 1,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1041,6 +1052,7 @@ describe('calling duck', () => {
hasLocalVideo: false,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1095,6 +1107,7 @@ describe('calling duck', () => {
hasLocalVideo: false,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1136,6 +1149,7 @@ describe('calling duck', () => {
hasLocalVideo: false,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1170,6 +1184,7 @@ describe('calling duck', () => {
hasLocalVideo: true,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1216,6 +1231,7 @@ describe('calling duck', () => {
hasLocalVideo: true,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1262,6 +1278,7 @@ describe('calling duck', () => {
hasLocalVideo: true,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: 16,
deviceCount: 0,
},
@ -1292,6 +1309,7 @@ describe('calling duck', () => {
hasLocalVideo: true,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
maxDevices: 16,
deviceCount: 1,
},
@ -1304,42 +1322,42 @@ describe('calling duck', () => {
});
describe('handleCallLinkUpdate', () => {
const { roomId, rootKey, expiration } = FAKE_CALL_LINK;
const {
roomId,
name,
restrictions,
expiration,
revoked,
rootKey,
adminKey,
} = FAKE_CALL_LINK;
beforeEach(function (this: Mocha.Context) {
this.callingServiceReadCallLink = this.sandbox
.stub(callingService, 'readCallLink')
.resolves({
callLinkState: {
name: 'Signal Call',
restrictions: CallLinkRestrictions.None,
expiration,
revoked: false,
},
callLinkState: getCallLinkState(FAKE_CALL_LINK),
errorStatusCode: undefined,
});
});
it('reads the call link from calling service', async function (this: Mocha.Context) {
const doAction = async (
payload: HandleCallLinkUpdateType
): Promise<{ dispatch: sinon.SinonSpy }> => {
const { handleCallLinkUpdate } = actions;
const dispatch = sinon.spy();
await handleCallLinkUpdate({ rootKey, adminKey: null })(
dispatch,
getEmptyRootState,
null
);
await handleCallLinkUpdate(payload)(dispatch, getEmptyRootState, null);
return { dispatch };
};
it('reads the call link from calling service', async function (this: Mocha.Context) {
await doAction({ rootKey, adminKey: null });
sinon.assert.calledOnce(this.callingServiceReadCallLink);
});
it('dispatches HANDLE_CALL_LINK_UPDATE', async () => {
const { handleCallLinkUpdate } = actions;
const dispatch = sinon.spy();
await handleCallLinkUpdate({ rootKey, adminKey: null })(
dispatch,
getEmptyRootState,
null
);
const { dispatch } = await doAction({ rootKey, adminKey: null });
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
@ -1347,16 +1365,115 @@ describe('calling duck', () => {
payload: {
roomId,
callLinkDetails: {
name: 'Signal Call',
restrictions: CallLinkRestrictions.None,
name,
restrictions,
expiration,
revoked: false,
revoked,
rootKey,
adminKey: null,
adminKey,
},
},
});
});
it('can save adminKey', async () => {
const { dispatch } = await doAction({ rootKey, adminKey: 'banana' });
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/HANDLE_CALL_LINK_UPDATE',
payload: {
roomId,
callLinkDetails: {
name,
restrictions,
expiration,
revoked,
rootKey,
adminKey: 'banana',
},
},
});
});
});
describe('startCallLinkLobby', () => {
const callLobbyData = {
callMode: CallMode.Adhoc,
connectionState: GroupCallConnectionState.NotConnected,
hasLocalAudio: true,
hasLocalVideo: true,
joinState: GroupCallJoinState.NotJoined,
peekInfo: [],
remoteParticipants: [],
};
const callLinkState = getCallLinkState(FAKE_CALL_LINK);
const getStateWithAdminKey = (): RootStateType => ({
...getEmptyRootState(),
calling: {
...getEmptyState(),
callLinks: {
[FAKE_CALL_LINK_WITH_ADMIN_KEY.roomId]:
FAKE_CALL_LINK_WITH_ADMIN_KEY,
},
},
});
beforeEach(function (this: Mocha.Context) {
this.callingServiceReadCallLink = this.sandbox
.stub(callingService, 'readCallLink')
.resolves({
callLinkState,
errorStatusCode: undefined,
});
this.callingServiceStartCallLinkLobby = this.sandbox
.stub(callingService, 'startCallLinkLobby')
.resolves(callLobbyData);
});
const doAction = async (
payload: StartCallLinkLobbyType
): Promise<{ dispatch: sinon.SinonSpy }> => {
const { startCallLinkLobby } = actions;
const dispatch = sinon.spy();
await startCallLinkLobby(payload)(dispatch, getEmptyRootState, null);
return { dispatch };
};
it('reads the link and dispatches START_CALL_LINK_LOBBY', async function (this: Mocha.Context) {
const { roomId, rootKey } = FAKE_CALL_LINK;
const { dispatch } = await doAction({ rootKey });
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'calling/START_CALL_LINK_LOBBY',
payload: {
...callLobbyData,
callLinkState,
callLinkRootKey: rootKey,
conversationId: roomId,
isConversationTooBigToRing: false,
},
});
});
it('preserves adminKey', () => {
const { startCallLinkLobby } = actions;
const { roomId, rootKey, adminKey } = FAKE_CALL_LINK_WITH_ADMIN_KEY;
const dispatch = sinon.spy();
const result = reducer(
getStateWithAdminKey().calling,
startCallLinkLobby({
rootKey,
})(
dispatch,
getStateWithAdminKey,
null
) as unknown as Readonly<CallingActionType>
);
assert.equal(result.callLinks[roomId]?.adminKey, adminKey);
});
});
describe('peekNotConnectedGroupCall', () => {
@ -1503,6 +1620,7 @@ describe('calling duck', () => {
localDemuxId: undefined,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: Infinity,
deviceCount: 0,
},
@ -1956,6 +2074,7 @@ describe('calling duck', () => {
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -1983,6 +2102,7 @@ describe('calling duck', () => {
localDemuxId: undefined,
peekInfo: {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -2022,6 +2142,7 @@ describe('calling duck', () => {
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
acis: [],
pendingAcis: [],
maxDevices: Infinity,
deviceCount: 0,
});
@ -2053,6 +2174,7 @@ describe('calling duck', () => {
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
acis: [creatorAci],
pendingAcis: [],
creatorAci,
eraId: 'xyz',
maxDevices: 16,
@ -2081,6 +2203,7 @@ describe('calling duck', () => {
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
acis: [differentCreatorAci],
pendingAcis: [],
creatorAci: differentCreatorAci,
eraId: 'abc',
maxDevices: 5,
@ -2103,6 +2226,7 @@ describe('calling duck', () => {
const call = result.callsByConversation['fake-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
acis: [differentCreatorAci],
pendingAcis: [],
creatorAci: differentCreatorAci,
eraId: 'abc',
maxDevices: 5,

View file

@ -100,6 +100,7 @@ describe('state/selectors/calling', () => {
localDemuxId: undefined,
peekInfo: {
acis: [ACI_1],
pendingAcis: [],
creatorAci: ACI_1,
maxDevices: Infinity,
deviceCount: 1,
@ -180,6 +181,7 @@ describe('state/selectors/calling', () => {
...incomingGroupCall,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: Infinity,
deviceCount: 1,
},

View file

@ -97,6 +97,7 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
isConversationTooBigToRing: boolean;
peekedParticipants: Array<ConversationType>;
pendingParticipants: Array<ConversationType>;
raisedHands: Set<number>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
remoteAudioLevels: Map<number, number>;