Let users ring members when starting a group call

Co-Authored-By: Josh Perez <60019601+josh-signal@users.noreply.github.com>
This commit is contained in:
Evan Hahn 2021-08-25 16:42:51 -05:00 committed by GitHub
parent 4afe4649ec
commit 0e7f641dc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 556 additions and 97 deletions

View File

@ -1368,6 +1368,22 @@
"message": "Stop presenting",
"description": "Button tooltip label for stopping screen sharing"
},
"calling__button--ring__label": {
"message": "Ring",
"description": "Label under the ring button"
},
"calling__button--ring__disabled-because-group-is-too-large": {
"message": "Group is too large to ring the participants.",
"description": "Button tooltip label when you can't ring because the group is too large"
},
"calling__button--ring__off": {
"message": "Notify, don't ring",
"description": "Button tooltip label for turning ringing off"
},
"calling__button--ring__on": {
"message": "Enable ringing",
"description": "Button tooltip label for turning ringing on"
},
"calling__your-video-is-off": {
"message": "Your camera is off",
"description": "Label in the calling lobby indicating that your camera is off"
@ -1450,6 +1466,56 @@
}
}
},
"calling__pre-call-info--will-ring-2": {
"message": "Signal will ring $first$ and $second$",
"description": "Shown in the calling lobby to describe who will be rang",
"placeholders": {
"first": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
}
}
},
"calling__pre-call-info--will-ring-3": {
"message": "Signal will ring $first$, $second$, and $third$",
"description": "Shown in the calling lobby to describe who will be rang",
"placeholders": {
"first": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
},
"third": {
"content": "$3",
"example": "April"
}
}
},
"calling__pre-call-info--will-ring-many": {
"message": "Signal will ring $first$, $second$, and $others$ others",
"description": "Shown in the calling lobby to describe who will be rang",
"placeholders": {
"person": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
},
"others": {
"content": "$3",
"example": "5"
}
}
},
"calling__pre-call-info--will-notify-1": {
"message": "$person$ will be notified",
"description": "Shown in the calling lobby to describe who will be notified",
@ -3490,10 +3556,6 @@
}
}
},
"outgoingCallPrering": {
"message": "Calling...",
"description": "Shown in the call screen when placing an outgoing call that isn't ringing yet"
},
"outgoingCallRinging": {
"message": "Ringing...",
"description": "Shown in the call screen when placing an outgoing call that is now ringing"

View File

@ -0,0 +1 @@
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg"><path d="m25.29 19.84a2.19 2.19 0 0 1 -2.29 2.16h-18a2.19 2.19 0 0 1 -2.25-2.11c0-1.31.91-1.89 1.9-2.33 2-.89 2.43-3.35 2.85-6 .5-3.39 1.19-7.56 6.5-7.56s6 4.22 6.54 7.61c.42 2.6.81 5.06 2.85 6 .99.39 1.9.92 1.9 2.23zm-1.64-8.16a.86.86 0 0 0 .84-.9c-.13-3.84-1.64-7.11-4.14-9a.88.88 0 1 0 -1.05 1.42c2.07 1.55 3.33 4.34 3.44 7.64a.88.88 0 0 0 .88.84zm-18.44-.84c.11-3.3 1.37-6.09 3.44-7.64a.88.88 0 1 0 -1.05-1.4c-2.5 1.87-4 5.14-4.14 9a.86.86 0 0 0 .84.9.88.88 0 0 0 .91-.86zm6.21 12.66a.5.5 0 0 0 -.44.73 3.46 3.46 0 0 0 6 0 .5.5 0 0 0 -.44-.73z"/></svg>

After

Width:  |  Height:  |  Size: 616 B

View File

@ -49,8 +49,6 @@ try {
window.GV2_MIGRATION_DISABLE_ADD = false;
window.GV2_MIGRATION_DISABLE_INVITE = false;
window.RING_WHEN_JOINING_GROUP_CALLS = false;
window.RETRY_DELAY = false;
window.platform = process.platform;

View File

@ -229,6 +229,11 @@
text-shadow: 0 0 4px $color-black-alpha-40;
}
@mixin lonely-local-video-preview {
object-fit: cover;
opacity: 0.6;
}
// --- Buttons
// Individual traits

View File

@ -5362,6 +5362,24 @@ button.module-image__border-overlay:focus {
.module-calling-button__container {
display: inline-flex;
flex-direction: column;
margin-left: 0;
transition: margin-left 0.3s ease-out, opacity 0.3s ease-out;
@media (prefers-reduced-motion) {
transition: none;
}
&--hidden {
margin-left: -100px;
opacity: 0;
pointer-events: none;
// The container could be wider than 100px depending on the label. Hiding the label
// ensures that the above `margin-left` will completely hide the button.
.module-calling-button__label {
display: none;
}
}
}
.module-calling-button__icon {
@ -5429,6 +5447,19 @@ button.module-image__border-overlay:focus {
);
}
&--ring {
$icon: '../images/icons/v2/ring-28.svg';
&--on {
@include calling-button-icon-on($icon);
}
&--off {
@include calling-button-icon-off($icon);
}
&--disabled {
@include calling-button-icon-disabled($icon);
}
}
&--presenting {
$icon: '../images/icons/v2/share-screen-26.svg';
&--on {
@ -5491,7 +5522,7 @@ button.module-image__border-overlay:focus {
}
&__container {
&--direct {
&--direct:not(&--call-not-started) {
.module-ongoing-call__header {
position: absolute;
}
@ -5517,6 +5548,10 @@ button.module-image__border-overlay:focus {
letter-spacing: -0.0025em;
}
&__direct-call-ringing-spacer {
flex: 1;
}
&__participants {
display: flex;
flex: 1 1 0;
@ -5710,6 +5745,11 @@ button.module-image__border-overlay:focus {
position: absolute;
top: 0;
width: 100%;
z-index: -1;
video {
@include lonely-local-video-preview;
}
}
&__footer {

View File

@ -3,9 +3,8 @@
.module-CallingLobby {
&__local-preview {
@include lonely-local-video-preview;
height: 100%;
object-fit: cover;
opacity: 0.6;
position: absolute;
width: 100%;
z-index: -1;

View File

@ -9,6 +9,7 @@ export type ConfigKeyType =
| 'desktop.announcementGroup'
| 'desktop.clientExpiration'
| 'desktop.disableGV1'
| 'desktop.groupCallOutboundRing'
| 'desktop.groupCalling'
| 'desktop.gv2'
| 'desktop.internalUser'

View File

@ -48,6 +48,7 @@ const getCommonActiveCallData = () => ({
hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false),
isInSpeakerView: boolean('isInSpeakerView', false),
outgoingRing: boolean('outgoingRing', true),
pip: boolean('pip', false),
settingsDialogOpen: boolean('settingsDialogOpen', false),
showParticipantsList: boolean('showParticipantsList', false),
@ -67,7 +68,9 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
getPresentingSources: action('get-presenting-sources'),
hangUp: action('hang-up'),
i18n,
isGroupCallOutboundRingEnabled: true,
keyChangeOk: action('key-change-ok'),
maxGroupCallRingSize: 16,
me: {
...getDefaultConversation({
color: select(
@ -90,6 +93,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
setLocalVideo: action('set-local-video'),
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
setOutgoingRing: action('set-outgoing-ring'),
startCall: action('start-call'),
stopRingtone: action('stop-ringtone'),
toggleParticipants: action('toggle-participants'),

View File

@ -19,6 +19,7 @@ import {
CallEndedReason,
CallMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
GroupCallVideoRequest,
PresentedSource,
@ -41,6 +42,8 @@ import {
import { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
const GROUP_CALL_RING_DURATION = 60 * 1000;
type MeType = ConversationType & {
uuid: string;
};
@ -77,6 +80,8 @@ export type PropsType = {
bounceAppIconStop: () => unknown;
declineCall: (_: DeclineCallType) => void;
i18n: LocalizerType;
isGroupCallOutboundRingEnabled: boolean;
maxGroupCallRingSize: number;
me: MeType;
notifyForCall: (title: string, isVideoCall: boolean) => unknown;
openSystemPreferencesAction: () => unknown;
@ -85,6 +90,7 @@ export type PropsType = {
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setOutgoingRing: (_: boolean) => void;
setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void;
stopRingtone: () => unknown;
@ -106,9 +112,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
closeNeedPermissionScreen,
hangUp,
i18n,
isGroupCallOutboundRingEnabled,
keyChangeOk,
getGroupCallVideoFrameSource,
getPresentingSources,
maxGroupCallRingSize,
me,
openSystemPreferencesAction,
renderDeviceSelection,
@ -119,6 +127,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
setLocalVideo,
setPresenting,
setRendererCanvas,
setOutgoingRing,
startCall,
toggleParticipants,
togglePip,
@ -136,6 +145,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
presentingSourcesAvailable,
settingsDialogOpen,
showParticipantsList,
outgoingRing,
} = activeCall;
const cancelActiveCall = useCallback(() => {
@ -178,7 +188,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
let showCallLobby: boolean;
let groupMembers:
| undefined
| Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
switch (activeCall.callMode) {
case CallMode.Direct: {
@ -222,14 +232,18 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
hasLocalVideo={hasLocalVideo}
i18n={i18n}
isGroupCall={activeCall.callMode === CallMode.Group}
isGroupCallOutboundRingEnabled={isGroupCallOutboundRingEnabled}
isCallFull={isCallFull}
maxGroupCallRingSize={maxGroupCallRingSize}
me={me}
onCallCanceled={cancelActiveCall}
onJoinCall={joinActiveCall}
outgoingRing={outgoingRing}
peekedParticipants={peekedParticipants}
setLocalPreview={setLocalPreview}
setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings}
@ -287,6 +301,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
activeCall={activeCall}
getPresentingSources={getPresentingSources}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
groupMembers={groupMembers}
hangUp={hangUp}
i18n={i18n}
joinedAt={joinedAt}
@ -354,6 +369,7 @@ export const CallManager: React.FC<PropsType> = props => {
notifyForCall,
playRingtone,
stopRingtone,
setOutgoingRing,
} = props;
const shouldRing = getShouldRing(props);
@ -369,6 +385,21 @@ export const CallManager: React.FC<PropsType> = props => {
return noop;
}, [shouldRing, playRingtone, stopRingtone]);
const hasActiveCall = Boolean(activeCall);
const isGroupCall = activeCall?.callMode === CallMode.Group;
useEffect(() => {
if (!hasActiveCall || !isGroupCall) {
return noop;
}
const timeout = setTimeout(() => {
setOutgoingRing(false);
}, GROUP_CALL_RING_DURATION);
return () => {
clearTimeout(timeout);
};
}, [hasActiveCall, setOutgoingRing, isGroupCall]);
if (activeCall) {
// `props` should logically have an `activeCall` at this point, but TypeScript can't
// figure that out, so we pass it in again.
@ -412,7 +443,14 @@ function getShouldRing({
activeCall.callState === CallState.Ringing
);
case CallMode.Group:
return false;
return (
activeCall.outgoingRing &&
(activeCall.connectionState === GroupCallConnectionState.Connecting ||
activeCall.connectionState === GroupCallConnectionState.Connected) &&
activeCall.joinState !== GroupCallJoinState.NotJoined &&
!activeCall.remoteParticipants.length &&
(activeCall.conversation.sortedGroupMembers || []).length >= 2
);
default:
throw missingCaseError(activeCall);
}

View File

@ -120,6 +120,7 @@ const createActiveCallProp = (
'isInSpeakerView',
overrideProps.isInSpeakerView || false
),
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
@ -147,9 +148,11 @@ const createProps = (
i18n,
me: {
color: AvatarColors[1],
id: '6146087e-f7ef-457e-9a8d-47df1fdd6b25',
name: 'Morty Smith',
profileName: 'Morty Smith',
title: 'Morty Smith',
uuid: '3c134598-eecb-42ab-9ad3-2b0873f771b2',
},
openSystemPreferencesAction: action('open-system-preferences-action'),
setGroupCallVideoRequest: action('set-group-call-video-request'),

View File

@ -1,7 +1,13 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useRef, useEffect, useCallback } from 'react';
import React, {
ReactNode,
useState,
useRef,
useEffect,
useCallback,
} from 'react';
import { noop } from 'lodash';
import classNames from 'classnames';
import {
@ -13,6 +19,7 @@ import {
} from '../state/ducks/calling';
import { Avatar } from './Avatar';
import { CallingHeader } from './CallingHeader';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
import { CallingButton, CallingButtonType } from './CallingButton';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import {
@ -20,11 +27,13 @@ import {
CallMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
GroupCallVideoRequest,
PresentedSource,
VideoFrameSource,
} from '../types/Calling';
import { AvatarColors, AvatarColorType } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations';
import { CallingToastManager } from './CallingToastManager';
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
@ -37,16 +46,19 @@ export type PropsType = {
activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
getPresentingSources: () => void;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
hangUp: (_: HangUpType) => void;
i18n: LocalizerType;
joinedAt?: number;
me: {
avatarPath?: string;
color?: AvatarColorType;
id: string;
name?: string;
phoneNumber?: string;
profileName?: string;
title: string;
uuid: string;
};
openSystemPreferencesAction: () => unknown;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
@ -67,6 +79,7 @@ export const CallScreen: React.FC<PropsType> = ({
activeCall,
getGroupCallVideoFrameSource,
getPresentingSources,
groupMembers,
hangUp,
i18n,
joinedAt,
@ -198,36 +211,50 @@ export const CallScreen: React.FC<PropsType> = ({
remoteParticipant => remoteParticipant.hasRemoteVideo
);
let isRinging: boolean;
let hasCallStarted: boolean;
let headerMessage: string | undefined;
let headerTitle: string | undefined;
let isConnected: boolean;
let participantCount: number;
let remoteParticipantsElement: JSX.Element;
let remoteParticipantsElement: ReactNode;
switch (activeCall.callMode) {
case CallMode.Direct:
headerMessage = renderHeaderMessage(
case CallMode.Direct: {
isRinging =
activeCall.callState === CallState.Prering ||
activeCall.callState === CallState.Ringing;
hasCallStarted = !isRinging;
headerMessage = renderDirectCallHeaderMessage(
i18n,
activeCall.callState || CallState.Prering,
acceptedDuration
);
headerTitle = conversation.title;
headerTitle = isRinging ? undefined : conversation.title;
isConnected = activeCall.callState === CallState.Accepted;
participantCount = isConnected ? 2 : 0;
remoteParticipantsElement = (
remoteParticipantsElement = hasCallStarted ? (
<DirectCallRemoteParticipant
conversation={conversation}
hasRemoteVideo={hasRemoteVideo}
i18n={i18n}
setRendererCanvas={setRendererCanvas}
/>
) : (
<div className="module-ongoing-call__direct-call-ringing-spacer" />
);
break;
}
case CallMode.Group:
isRinging =
activeCall.outgoingRing && !activeCall.remoteParticipants.length;
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
participantCount = activeCall.remoteParticipants.length + 1;
headerMessage = undefined;
if (currentPresenter) {
if (isRinging) {
headerTitle = undefined;
} else if (currentPresenter) {
headerTitle = i18n('calling__presenting--person-ongoing', [
currentPresenter.title,
]);
@ -301,7 +328,10 @@ export const CallScreen: React.FC<PropsType> = ({
'module-calling__container',
`module-ongoing-call__container--${getCallModeClassSuffix(
activeCall.callMode
)}`
)}`,
`module-ongoing-call__container--${
hasCallStarted ? 'call-started' : 'call-not-started'
}`
)}
onMouseMove={() => {
setShowControls(true);
@ -335,6 +365,15 @@ export const CallScreen: React.FC<PropsType> = ({
toggleSpeakerView={toggleSpeakerView}
/>
</div>
{isRinging && (
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
me={me}
ringMode={RingMode.IsRinging}
/>
)}
{remoteParticipantsElement}
{isSendingVideo && isLonelyInGroup ? (
<div className="module-ongoing-call__local-preview-fullsize">
@ -457,22 +496,18 @@ function getCallModeClassSuffix(
}
}
function renderHeaderMessage(
function renderDirectCallHeaderMessage(
i18n: LocalizerType,
callState: CallState,
acceptedDuration: null | number
): string | undefined {
let message;
if (callState === CallState.Prering) {
message = i18n('outgoingCallPrering');
} else if (callState === CallState.Ringing) {
message = i18n('outgoingCallRinging');
} else if (callState === CallState.Reconnecting) {
message = i18n('callReconnecting');
} else if (callState === CallState.Accepted && acceptedDuration) {
message = i18n('callDuration', [renderDuration(acceptedDuration)]);
if (callState === CallState.Reconnecting) {
return i18n('callReconnecting');
}
return message;
if (callState === CallState.Accepted && acceptedDuration) {
return i18n('callDuration', [renderDuration(acceptedDuration)]);
}
return undefined;
}
function renderDuration(ms: number): string {

View File

@ -16,6 +16,9 @@ export enum CallingButtonType {
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
PRESENTING_OFF = 'PRESENTING_OFF',
PRESENTING_ON = 'PRESENTING_ON',
RING_DISABLED = 'RING_DISABLED',
RING_OFF = 'RING_OFF',
RING_ON = 'RING_ON',
VIDEO_DISABLED = 'VIDEO_DISABLED',
VIDEO_OFF = 'VIDEO_OFF',
VIDEO_ON = 'VIDEO_ON',
@ -24,6 +27,7 @@ export enum CallingButtonType {
export type PropsType = {
buttonType: CallingButtonType;
i18n: LocalizerType;
isVisible?: boolean;
onClick: () => void;
tooltipDirection?: TooltipPlacement;
};
@ -31,6 +35,7 @@ export type PropsType = {
export const CallingButton = ({
buttonType,
i18n,
isVisible = true,
onClick,
tooltipDirection,
}: PropsType): JSX.Element => {
@ -70,6 +75,21 @@ export const CallingButton = ({
classNameSuffix = 'hangup';
tooltipContent = i18n('calling__hangup');
label = i18n('calling__hangup');
} else if (buttonType === CallingButtonType.RING_DISABLED) {
classNameSuffix = 'ring--disabled';
disabled = true;
tooltipContent = i18n(
'calling__button--ring__disabled-because-group-is-too-large'
);
label = i18n('calling__button--ring__label');
} else if (buttonType === CallingButtonType.RING_OFF) {
classNameSuffix = 'ring--off';
tooltipContent = i18n('calling__button--ring__on');
label = i18n('calling__button--ring__label');
} else if (buttonType === CallingButtonType.RING_ON) {
classNameSuffix = 'ring--on';
tooltipContent = i18n('calling__button--ring__off');
label = i18n('calling__button--ring__label');
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
classNameSuffix = 'presenting--disabled';
tooltipContent = i18n('calling__button--presenting-disabled');
@ -96,7 +116,12 @@ export const CallingButton = ({
direction={tooltipDirection}
theme={Theme.Dark}
>
<div className="module-calling-button__container">
<div
className={classNames(
'module-calling-button__container',
!isVisible && 'module-calling-button__container--hidden'
)}
>
<button
aria-label={tooltipContent}
className={className}

View File

@ -42,9 +42,9 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
return {
availableCameras: overrideProps.availableCameras || [camera],
conversation,
groupMembers: isGroupCall
? times(3, () => getDefaultConversation())
: undefined,
groupMembers:
overrideProps.groupMembers ||
(isGroupCall ? times(3, () => getDefaultConversation()) : undefined),
hasLocalAudio: boolean(
'hasLocalAudio',
overrideProps.hasLocalAudio || false
@ -55,17 +55,22 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
),
i18n,
isGroupCall,
isGroupCallOutboundRingEnabled: true,
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
maxGroupCallRingSize: overrideProps.maxGroupCallRingSize || 16,
me: overrideProps.me || {
color: AvatarColors[0],
id: generateUuid(),
uuid: generateUuid(),
},
onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'),
outgoingRing: boolean('outgoingRing', Boolean(overrideProps.outgoingRing)),
peekedParticipants: overrideProps.peekedParticipants || [],
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
setOutgoingRing: action('set-outgoing-ring'),
showParticipantsList: boolean(
'showParticipantsList',
Boolean(overrideProps.showParticipantsList)
@ -101,6 +106,7 @@ story.add('No Camera, local avatar', () => {
me: {
avatarPath: '/fixtures/kitten-4-112-112.jpg',
color: AvatarColors[0],
id: generateUuid(),
uuid: generateUuid(),
},
});
@ -138,7 +144,10 @@ story.add('Group Call - 1 peeked participant (self)', () => {
const uuid = generateUuid();
const props = createProps({
isGroupCall: true,
me: { uuid },
me: {
id: generateUuid(),
uuid,
},
peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })],
});
return <CallingLobby {...props} />;
@ -175,3 +184,11 @@ story.add('Group Call - call full', () => {
});
return <CallingLobby {...props} />;
});
story.add('Group Call - 0 peeked participants, big group', () => {
const props = createProps({
isGroupCall: true,
groupMembers: times(100, () => getDefaultConversation()),
});
return <CallingLobby {...props} />;
});

View File

@ -12,7 +12,7 @@ import { CallingButton, CallingButtonType } from './CallingButton';
import { TooltipPlacement } from './Tooltip';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { CallingHeader } from './CallingHeader';
import { CallingPreCallInfo } from './CallingPreCallInfo';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
import {
CallingLobbyJoinButton,
CallingLobbyJoinButtonVariant,
@ -37,23 +37,28 @@ export type PropsType = {
| 'type'
| 'unblurredAvatarPath'
>;
groupMembers?: Array<Pick<ConversationType, 'title'>>;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
i18n: LocalizerType;
isGroupCall: boolean;
isGroupCallOutboundRingEnabled: boolean;
isCallFull?: boolean;
maxGroupCallRingSize: number;
me: {
avatarPath?: string;
id: string;
color?: AvatarColorType;
uuid: string;
};
onCallCanceled: () => void;
onJoinCall: () => void;
outgoingRing: boolean;
peekedParticipants: Array<ConversationType>;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setOutgoingRing: (_: boolean) => void;
showParticipantsList: boolean;
toggleParticipants: () => void;
toggleSettings: () => void;
@ -67,7 +72,9 @@ export const CallingLobby = ({
hasLocalVideo,
i18n,
isGroupCall = false,
isGroupCallOutboundRingEnabled,
isCallFull = false,
maxGroupCallRingSize,
me,
onCallCanceled,
onJoinCall,
@ -75,9 +82,11 @@ export const CallingLobby = ({
setLocalAudio,
setLocalPreview,
setLocalVideo,
setOutgoingRing,
showParticipantsList,
toggleParticipants,
toggleSettings,
outgoingRing,
}: PropsType): JSX.Element => {
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
@ -91,6 +100,10 @@ export const CallingLobby = ({
setLocalVideo({ enabled: !hasLocalVideo });
}, [hasLocalVideo, setLocalVideo]);
const toggleOutgoingRing = React.useCallback((): void => {
setOutgoingRing(!outgoingRing);
}, [outgoingRing, setOutgoingRing]);
React.useEffect(() => {
setLocalPreview({ element: localVideoRef });
@ -132,10 +145,36 @@ export const CallingLobby = ({
: availableCameras.length === 0
? CallingButtonType.VIDEO_DISABLED
: CallingButtonType.VIDEO_OFF;
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
const isRingButtonVisible: boolean =
isGroupCall &&
isGroupCallOutboundRingEnabled &&
peekedParticipants.length === 0 &&
(groupMembers || []).length > 1;
const preCallInfoRingMode: RingMode =
isGroupCall && !outgoingRing ? RingMode.WillNotRing : RingMode.WillRing;
let ringButtonType:
| CallingButtonType.RING_DISABLED
| CallingButtonType.RING_ON
| CallingButtonType.RING_OFF;
if (isRingButtonVisible) {
if ((groupMembers || []).length > maxGroupCallRingSize) {
ringButtonType = CallingButtonType.RING_DISABLED;
} else if (outgoingRing) {
ringButtonType = CallingButtonType.RING_ON;
} else {
ringButtonType = CallingButtonType.RING_OFF;
}
} else {
ringButtonType = CallingButtonType.RING_DISABLED;
}
const canJoin = !isCallFull && !isCallConnecting;
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
@ -182,6 +221,7 @@ export const CallingLobby = ({
isCallFull={isCallFull}
me={me}
peekedParticipants={peekedParticipants}
ringMode={preCallInfoRingMode}
/>
<div
@ -208,6 +248,13 @@ export const CallingLobby = ({
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={ringButtonType}
i18n={i18n}
isVisible={isRingButtonVisible}
onClick={toggleOutgoingRing}
tooltipDirection={TooltipPlacement.Top}
/>
</div>
<CallingLobbyJoinButton

View File

@ -40,6 +40,7 @@ const getCommonActiveCallData = () => ({
hasLocalVideo: boolean('hasLocalVideo', false),
isInSpeakerView: boolean('isInSpeakerView', false),
joinedAt: Date.now(),
outgoingRing: true,
pip: true,
settingsDialogOpen: false,
showParticipantsList: false,

View File

@ -8,7 +8,7 @@ import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { CallingPreCallInfo } from './CallingPreCallInfo';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
const i18n = setupI18n('en', enMessages);
const getDefaultGroupConversation = () =>
@ -28,34 +28,28 @@ story.add('Direct conversation', () => (
conversation={getDefaultConversation()}
i18n={i18n}
me={getDefaultConversation()}
/>
));
story.add('Group conversation, empty group', () => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={[]}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={[]}
ringMode={RingMode.WillRing}
/>
));
times(5, numberOfOtherPeople => {
story.add(
`Group conversation, group has ${numberOfOtherPeople} other member${
numberOfOtherPeople === 1 ? '' : 's'
}`,
() => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={[]}
/>
)
);
[true, false].forEach(willRing => {
story.add(
`Group conversation, group has ${numberOfOtherPeople} other member${
numberOfOtherPeople === 1 ? '' : 's'
}, will ${willRing ? 'ring' : 'notify'}`,
() => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={[]}
ringMode={willRing ? RingMode.WillRing : RingMode.WillNotRing}
/>
)
);
});
});
range(1, 5).forEach(numberOfOtherPeople => {
@ -70,6 +64,7 @@ range(1, 5).forEach(numberOfOtherPeople => {
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={otherMembers.slice(0, numberOfOtherPeople)}
ringMode={RingMode.WillRing}
/>
)
);
@ -84,6 +79,7 @@ story.add('Group conversation, you on an other device', () => {
i18n={i18n}
me={me}
peekedParticipants={[me]}
ringMode={RingMode.WillRing}
/>
);
});
@ -96,5 +92,6 @@ story.add('Group conversation, call is full', () => (
isCallFull
me={getDefaultConversation()}
peekedParticipants={otherMembers}
ringMode={RingMode.WillRing}
/>
));

View File

@ -9,6 +9,12 @@ import { Emojify } from './conversation/Emojify';
import { getParticipantName } from '../util/callingGetParticipantName';
import { missingCaseError } from '../util/missingCaseError';
export enum RingMode {
WillNotRing,
WillRing,
IsRinging,
}
type PropsType = {
conversation: Pick<
ConversationType,
@ -25,10 +31,11 @@ type PropsType = {
| 'unblurredAvatarPath'
>;
i18n: LocalizerType;
me: Pick<ConversationType, 'uuid'>;
me: Pick<ConversationType, 'id' | 'uuid'>;
ringMode: RingMode;
// The following should only be set for group conversations.
groupMembers?: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
isCallFull?: boolean;
peekedParticipants?: Array<
Pick<ConversationType, 'firstName' | 'title' | 'uuid'>
@ -42,9 +49,12 @@ export const CallingPreCallInfo: FunctionComponent<PropsType> = ({
isCallFull = false,
me,
peekedParticipants = [],
ringMode,
}) => {
let subtitle: string;
if (isCallFull) {
if (ringMode === RingMode.IsRinging) {
subtitle = i18n('outgoingCallRinging');
} else if (isCallFull) {
subtitle = i18n('calling__call-is-full');
} else if (peekedParticipants.length) {
// It should be rare to see yourself in this list, but it's possible if (1) you rejoin
@ -86,45 +96,67 @@ export const CallingPreCallInfo: FunctionComponent<PropsType> = ({
});
break;
}
} else if (conversation.type === 'direct') {
subtitle = i18n('calling__pre-call-info--will-ring-1', [
getParticipantName(conversation),
]);
} else if (conversation.type === 'group') {
const memberNames = groupMembers.map(getParticipantName);
} else {
let memberNames: Array<string>;
switch (conversation.type) {
case 'direct':
memberNames = [getParticipantName(conversation)];
break;
case 'group':
memberNames = groupMembers
.filter(member => member.id !== me.id)
.map(getParticipantName);
break;
default:
throw missingCaseError(conversation.type);
}
const ring = ringMode === RingMode.WillRing;
switch (memberNames.length) {
case 0:
subtitle = i18n('calling__pre-call-info--empty-group');
break;
case 1:
subtitle = i18n('calling__pre-call-info--will-notify-1', [
memberNames[0],
]);
case 1: {
const i18nValues = [memberNames[0]];
subtitle = ring
? i18n('calling__pre-call-info--will-ring-1', i18nValues)
: i18n('calling__pre-call-info--will-notify-1', i18nValues);
break;
case 2:
subtitle = i18n('calling__pre-call-info--will-notify-2', {
}
case 2: {
const i18nValues = {
first: memberNames[0],
second: memberNames[1],
});
};
subtitle = ring
? i18n('calling__pre-call-info--will-ring-2', i18nValues)
: i18n('calling__pre-call-info--will-notify-2', i18nValues);
break;
case 3:
subtitle = i18n('calling__pre-call-info--will-notify-3', {
}
case 3: {
const i18nValues = {
first: memberNames[0],
second: memberNames[1],
third: memberNames[2],
});
};
subtitle = ring
? i18n('calling__pre-call-info--will-ring-3', i18nValues)
: i18n('calling__pre-call-info--will-notify-3', i18nValues);
break;
default:
subtitle = i18n('calling__pre-call-info--will-notify-many', {
}
default: {
const i18nValues = {
first: memberNames[0],
second: memberNames[1],
others: String(memberNames.length - 2),
});
};
subtitle = ring
? i18n('calling__pre-call-info--will-ring-many', i18nValues)
: i18n('calling__pre-call-info--will-notify-many', i18nValues);
break;
}
}
} else {
throw missingCaseError(conversation.type);
}
return (

View File

@ -684,7 +684,8 @@ export class CallingClass {
public joinGroupCall(
conversationId: string,
hasLocalAudio: boolean,
hasLocalVideo: boolean
hasLocalVideo: boolean,
shouldRing: boolean
): void {
this.attemptToGiveOurUuidToRingRtc();
@ -717,10 +718,7 @@ export class CallingClass {
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
this.videoCapturer.enableCaptureAndSend(groupCall);
// This is a temporary flag to help all client teams (Desktop, iOS, and Android)
// debug. Soon, this will be exposed in the UI (see DESKTOP-2113).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (window.RING_WHEN_JOINING_GROUP_CALLS) {
if (shouldRing) {
groupCall.ringAll();
}

View File

@ -88,6 +88,7 @@ export type ActiveCallStateType = {
hasLocalVideo: boolean;
isInSpeakerView: boolean;
joinedAt?: number;
outgoingRing: boolean;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
@ -286,6 +287,7 @@ const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
const SET_OUTGOING_RING = 'calling/SET_OUTGOING_RING';
const SET_PRESENTING = 'calling/SET_PRESENTING';
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
@ -420,6 +422,11 @@ type SetPresentingSourcesActionType = {
payload: Array<PresentableSource>;
};
type SetOutgoingRingActionType = {
type: 'calling/SET_OUTGOING_RING';
payload: boolean;
};
type ShowCallLobbyActionType = {
type: 'calling/SHOW_CALL_LOBBY';
payload: ShowCallLobbyType;
@ -474,6 +481,7 @@ export type CallingActionType =
| SetLocalAudioActionType
| SetLocalVideoFulfilledActionType
| SetPresentingSourcesActionType
| SetOutgoingRingActionType
| ShowCallLobbyActionType
| StartDirectCallActionType
| ToggleNeedsScreenRecordingPermissionsActionType
@ -502,7 +510,7 @@ function acceptCall(
await calling.acceptDirectCall(conversationId, asVideoCall);
break;
case CallMode.Group:
calling.joinGroupCall(conversationId, true, asVideoCall);
calling.joinGroupCall(conversationId, true, asVideoCall, false);
break;
default:
throw missingCaseError(call);
@ -1020,6 +1028,13 @@ function setPresenting(
};
}
function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
return {
type: SET_OUTGOING_RING,
payload,
};
}
function startCallingLobby(
payload: StartCallingLobbyType
): ThunkAction<void, RootStateType, unknown, never> {
@ -1040,7 +1055,7 @@ function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
function startCall(
payload: StartCallType
): ThunkAction<void, RootStateType, unknown, StartDirectCallActionType> {
return dispatch => {
return (dispatch, getState) => {
switch (payload.callMode) {
case CallMode.Direct:
calling.startOutgoingDirectCall(
@ -1053,15 +1068,20 @@ function startCall(
payload,
});
break;
case CallMode.Group:
case CallMode.Group: {
const outgoingRing = Boolean(
getState().calling.activeCallState?.outgoingRing
);
calling.joinGroupCall(
payload.conversationId,
payload.hasLocalAudio,
payload.hasLocalVideo
payload.hasLocalVideo,
outgoingRing
);
// The calling service should already be wired up to Redux so we don't need to
// dispatch anything here.
break;
}
default:
throw missingCaseError(payload.callMode);
}
@ -1126,6 +1146,7 @@ export const actions = {
setLocalVideo,
setPresenting,
setRendererCanvas,
setOutgoingRing,
showCallLobby,
startCall,
startCallingLobby,
@ -1184,6 +1205,7 @@ export function reducer(
const { conversationId } = action.payload;
let call: DirectCallStateType | GroupCallStateType;
let outgoingRing: boolean;
switch (action.payload.callMode) {
case CallMode.Direct:
call = {
@ -1192,6 +1214,7 @@ export function reducer(
isIncoming: false,
isVideoCall: action.payload.hasLocalVideo,
};
outgoingRing = true;
break;
case CallMode.Group: {
// We expect to be in this state briefly. The Calling service should update the
@ -1211,6 +1234,8 @@ export function reducer(
remoteParticipants: action.payload.remoteParticipants,
...getGroupCallRingState(existingCall),
};
outgoingRing =
!call.peekInfo.uuids.length && !call.remoteParticipants.length;
break;
}
default:
@ -1232,6 +1257,7 @@ export function reducer(
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing,
},
};
}
@ -1258,6 +1284,7 @@ export function reducer(
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing: true,
},
};
}
@ -1279,6 +1306,7 @@ export function reducer(
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing: false,
},
};
}
@ -1412,6 +1440,7 @@ export function reducer(
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing: true,
},
};
}
@ -1513,6 +1542,18 @@ export function reducer(
: state.activeCallState;
}
if (
newActiveCallState &&
newActiveCallState.outgoingRing &&
newActiveCallState.conversationId === conversationId &&
isAnybodyElseInGroupCall(newPeekInfo, ourUuid)
) {
newActiveCallState = {
...newActiveCallState,
outgoingRing: false,
};
}
let newRingState: GroupCallRingStateType;
if (joinState === GroupCallJoinState.NotJoined) {
newRingState = existingRingState;
@ -1801,6 +1842,22 @@ export function reducer(
};
}
if (action.type === SET_OUTGOING_RING) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn('Cannot set outgoing ring when there is no active call');
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
outgoingRing: action.payload,
},
};
}
if (action.type === TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS) {
const { activeCallState } = state;
if (!activeCallState) {

View File

@ -12,6 +12,8 @@ import { getMe, getConversationSelector } from '../selectors/conversations';
import { getActiveCall } from '../ducks/calling';
import { ConversationType } from '../ducks/conversations';
import { getIncomingCall } from '../selectors/calling';
import { getMaxGroupCallRingSize } from '../../groups/limits';
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
import {
ActiveCallType,
CallMode,
@ -111,6 +113,7 @@ const mapStateToActiveCallProp = (
hasLocalVideo: activeCallState.hasLocalVideo,
isInSpeakerView: activeCallState.isInSpeakerView,
joinedAt: activeCallState.joinedAt,
outgoingRing: activeCallState.outgoingRing,
pip: activeCallState.pip,
presentingSource: activeCallState.presentingSource,
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
@ -292,7 +295,9 @@ const mapStateToProps = (state: StateType) => ({
availableCameras: state.calling.availableCameras,
getGroupCallVideoFrameSource,
i18n: getIntl(state),
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
incomingCall: mapStateToIncomingCallProp(state),
maxGroupCallRingSize: getMaxGroupCallRingSize(),
me: {
...getMe(state),
// `getMe` returns a `ConversationType` which might not have a UUID, at least

View File

@ -46,6 +46,7 @@ describe('calling duck', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
},
@ -118,6 +119,7 @@ describe('calling duck', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
pip: false,
settingsDialogOpen: false,
},
@ -423,6 +425,7 @@ describe('calling duck', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
pip: false,
settingsDialogOpen: false,
});
@ -514,6 +517,7 @@ describe('calling duck', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
pip: false,
settingsDialogOpen: false,
});
@ -1224,6 +1228,7 @@ describe('calling duck', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: false,
pip: false,
settingsDialogOpen: false,
});
@ -1264,6 +1269,62 @@ describe('calling duck', () => {
assert.isTrue(result.activeCallState?.hasLocalAudio);
assert.isTrue(result.activeCallState?.hasLocalVideo);
});
it("doesn't stop ringing if nobody is in the call", () => {
const state = {
...stateWithActiveGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
outgoingRing: true,
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
peekInfo: {
uuids: [],
maxDevices: 16,
deviceCount: 0,
},
remoteParticipants: [],
})
);
assert.isTrue(result.activeCallState?.outgoingRing);
});
it('stops ringing if someone enters the call', () => {
const state = {
...stateWithActiveGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
outgoingRing: true,
},
};
const result = reducer(
state,
getAction({
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
hasLocalAudio: true,
hasLocalVideo: true,
peekInfo: {
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [],
})
);
assert.isFalse(result.activeCallState?.outgoingRing);
});
});
describe('peekNotConnectedGroupCall', () => {
@ -1519,6 +1580,24 @@ describe('calling duck', () => {
});
});
describe('setOutgoingRing', () => {
const { setOutgoingRing } = actions;
it('enables a desire to ring', () => {
const action = setOutgoingRing(true);
const result = reducer(stateWithActiveGroupCall, action);
assert.isTrue(result.activeCallState?.outgoingRing);
});
it('disables a desire to ring', () => {
const action = setOutgoingRing(false);
const result = reducer(stateWithActiveDirectCall, action);
assert.isFalse(result.activeCallState?.outgoingRing);
});
});
describe('showCallLobby', () => {
const { showCallLobby } = actions;
@ -1548,6 +1627,7 @@ describe('calling duck', () => {
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
});
});
@ -1610,6 +1690,7 @@ describe('calling duck', () => {
result.activeCallState?.conversationId,
'fake-conversation-id'
);
assert.isFalse(result.activeCallState?.outgoingRing);
});
it('chooses fallback peek info if none is sent and there is no existing call', () => {
@ -1841,6 +1922,7 @@ describe('calling duck', () => {
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
outgoingRing: true,
});
});

View File

@ -54,6 +54,7 @@ describe('state/selectors/calling', () => {
isInSpeakerView: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
outgoingRing: true,
pip: false,
settingsDialogOpen: false,
},

View File

@ -30,6 +30,7 @@ type ActiveCallBaseType = {
isInSpeakerView: boolean;
isSharingScreen?: boolean;
joinedAt?: number;
outgoingRing: boolean;
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
@ -60,7 +61,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
joinState: GroupCallJoinState;
maxDevices: number;
deviceCount: number;
groupMembers: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
peekedParticipants: Array<ConversationType>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
};

View File

@ -0,0 +1,11 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as RemoteConfig from '../RemoteConfig';
export function isGroupCallOutboundRingEnabled(): boolean {
return Boolean(
RemoteConfig.isEnabled('desktop.internalUser') ||
RemoteConfig.isEnabled('desktop.groupCallOutboundRing')
);
}

1
ts/window.d.ts vendored
View File

@ -511,7 +511,6 @@ declare global {
GV2_ENABLE_STATE_PROCESSING: boolean;
GV2_MIGRATION_DISABLE_ADD: boolean;
GV2_MIGRATION_DISABLE_INVITE: boolean;
RING_WHEN_JOINING_GROUP_CALLS: boolean;
RETRY_DELAY: boolean;
}