Call link admin key fix and in-call approve, deny, remove
This commit is contained in:
		
					parent
					
						
							
								5df8924197
							
						
					
				
			
			
				commit
				
					
						8ec585d54c
					
				
			
		
					 20 changed files with 599 additions and 43 deletions
				
			
		|  | @ -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" | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										34
									
								
								stylesheets/components/CallingPendingParticipants.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								stylesheets/components/CallingPendingParticipants.scss
									
										
									
									
									
										Normal 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); | ||||
| } | ||||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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>(), | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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], | ||||
|  |  | |||
|  | @ -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"> | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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> | ||||
|             ) | ||||
|           )} | ||||
|  |  | |||
							
								
								
									
										101
									
								
								ts/components/CallingPendingParticipants.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								ts/components/CallingPendingParticipants.tsx
									
										
									
									
									
										Normal 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> | ||||
|   ); | ||||
| } | ||||
|  | @ -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>(), | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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, | ||||
|       }, | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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, | ||||
|             }, | ||||
|  |  | |||
|  | @ -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>; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 ayumi-signal
				ayumi-signal