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…
Reference in a new issue