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:
parent
4afe4649ec
commit
0e7f641dc1
25 changed files with 556 additions and 97 deletions
|
@ -1368,6 +1368,22 @@
|
||||||
"message": "Stop presenting",
|
"message": "Stop presenting",
|
||||||
"description": "Button tooltip label for stopping screen sharing"
|
"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": {
|
"calling__your-video-is-off": {
|
||||||
"message": "Your camera is off",
|
"message": "Your camera is off",
|
||||||
"description": "Label in the calling lobby indicating that 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": {
|
"calling__pre-call-info--will-notify-1": {
|
||||||
"message": "$person$ will be notified",
|
"message": "$person$ will be notified",
|
||||||
"description": "Shown in the calling lobby to describe who 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": {
|
"outgoingCallRinging": {
|
||||||
"message": "Ringing...",
|
"message": "Ringing...",
|
||||||
"description": "Shown in the call screen when placing an outgoing call that is now ringing"
|
"description": "Shown in the call screen when placing an outgoing call that is now ringing"
|
||||||
|
|
1
images/icons/v2/ring-28.svg
Normal file
1
images/icons/v2/ring-28.svg
Normal 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 |
|
@ -49,8 +49,6 @@ try {
|
||||||
window.GV2_MIGRATION_DISABLE_ADD = false;
|
window.GV2_MIGRATION_DISABLE_ADD = false;
|
||||||
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
window.GV2_MIGRATION_DISABLE_INVITE = false;
|
||||||
|
|
||||||
window.RING_WHEN_JOINING_GROUP_CALLS = false;
|
|
||||||
|
|
||||||
window.RETRY_DELAY = false;
|
window.RETRY_DELAY = false;
|
||||||
|
|
||||||
window.platform = process.platform;
|
window.platform = process.platform;
|
||||||
|
|
|
@ -229,6 +229,11 @@
|
||||||
text-shadow: 0 0 4px $color-black-alpha-40;
|
text-shadow: 0 0 4px $color-black-alpha-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin lonely-local-video-preview {
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Buttons
|
// --- Buttons
|
||||||
|
|
||||||
// Individual traits
|
// Individual traits
|
||||||
|
|
|
@ -5362,6 +5362,24 @@ button.module-image__border-overlay:focus {
|
||||||
.module-calling-button__container {
|
.module-calling-button__container {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
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 {
|
.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 {
|
&--presenting {
|
||||||
$icon: '../images/icons/v2/share-screen-26.svg';
|
$icon: '../images/icons/v2/share-screen-26.svg';
|
||||||
&--on {
|
&--on {
|
||||||
|
@ -5491,7 +5522,7 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
&--direct {
|
&--direct:not(&--call-not-started) {
|
||||||
.module-ongoing-call__header {
|
.module-ongoing-call__header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -5517,6 +5548,10 @@ button.module-image__border-overlay:focus {
|
||||||
letter-spacing: -0.0025em;
|
letter-spacing: -0.0025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__direct-call-ringing-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&__participants {
|
&__participants {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
@ -5710,6 +5745,11 @@ button.module-image__border-overlay:focus {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
video {
|
||||||
|
@include lonely-local-video-preview;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
|
|
||||||
.module-CallingLobby {
|
.module-CallingLobby {
|
||||||
&__local-preview {
|
&__local-preview {
|
||||||
|
@include lonely-local-video-preview;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
|
||||||
opacity: 0.6;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
|
@ -9,6 +9,7 @@ export type ConfigKeyType =
|
||||||
| 'desktop.announcementGroup'
|
| 'desktop.announcementGroup'
|
||||||
| 'desktop.clientExpiration'
|
| 'desktop.clientExpiration'
|
||||||
| 'desktop.disableGV1'
|
| 'desktop.disableGV1'
|
||||||
|
| 'desktop.groupCallOutboundRing'
|
||||||
| 'desktop.groupCalling'
|
| 'desktop.groupCalling'
|
||||||
| 'desktop.gv2'
|
| 'desktop.gv2'
|
||||||
| 'desktop.internalUser'
|
| 'desktop.internalUser'
|
||||||
|
|
|
@ -48,6 +48,7 @@ const getCommonActiveCallData = () => ({
|
||||||
hasLocalAudio: boolean('hasLocalAudio', true),
|
hasLocalAudio: boolean('hasLocalAudio', true),
|
||||||
hasLocalVideo: boolean('hasLocalVideo', false),
|
hasLocalVideo: boolean('hasLocalVideo', false),
|
||||||
isInSpeakerView: boolean('isInSpeakerView', false),
|
isInSpeakerView: boolean('isInSpeakerView', false),
|
||||||
|
outgoingRing: boolean('outgoingRing', true),
|
||||||
pip: boolean('pip', false),
|
pip: boolean('pip', false),
|
||||||
settingsDialogOpen: boolean('settingsDialogOpen', false),
|
settingsDialogOpen: boolean('settingsDialogOpen', false),
|
||||||
showParticipantsList: boolean('showParticipantsList', false),
|
showParticipantsList: boolean('showParticipantsList', false),
|
||||||
|
@ -67,7 +68,9 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
getPresentingSources: action('get-presenting-sources'),
|
getPresentingSources: action('get-presenting-sources'),
|
||||||
hangUp: action('hang-up'),
|
hangUp: action('hang-up'),
|
||||||
i18n,
|
i18n,
|
||||||
|
isGroupCallOutboundRingEnabled: true,
|
||||||
keyChangeOk: action('key-change-ok'),
|
keyChangeOk: action('key-change-ok'),
|
||||||
|
maxGroupCallRingSize: 16,
|
||||||
me: {
|
me: {
|
||||||
...getDefaultConversation({
|
...getDefaultConversation({
|
||||||
color: select(
|
color: select(
|
||||||
|
@ -90,6 +93,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
setPresenting: action('toggle-presenting'),
|
setPresenting: action('toggle-presenting'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
|
setOutgoingRing: action('set-outgoing-ring'),
|
||||||
startCall: action('start-call'),
|
startCall: action('start-call'),
|
||||||
stopRingtone: action('stop-ringtone'),
|
stopRingtone: action('stop-ringtone'),
|
||||||
toggleParticipants: action('toggle-participants'),
|
toggleParticipants: action('toggle-participants'),
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
CallEndedReason,
|
CallEndedReason,
|
||||||
CallMode,
|
CallMode,
|
||||||
CallState,
|
CallState,
|
||||||
|
GroupCallConnectionState,
|
||||||
GroupCallJoinState,
|
GroupCallJoinState,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
PresentedSource,
|
PresentedSource,
|
||||||
|
@ -41,6 +42,8 @@ import {
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
|
||||||
|
const GROUP_CALL_RING_DURATION = 60 * 1000;
|
||||||
|
|
||||||
type MeType = ConversationType & {
|
type MeType = ConversationType & {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
};
|
};
|
||||||
|
@ -77,6 +80,8 @@ export type PropsType = {
|
||||||
bounceAppIconStop: () => unknown;
|
bounceAppIconStop: () => unknown;
|
||||||
declineCall: (_: DeclineCallType) => void;
|
declineCall: (_: DeclineCallType) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isGroupCallOutboundRingEnabled: boolean;
|
||||||
|
maxGroupCallRingSize: number;
|
||||||
me: MeType;
|
me: MeType;
|
||||||
notifyForCall: (title: string, isVideoCall: boolean) => unknown;
|
notifyForCall: (title: string, isVideoCall: boolean) => unknown;
|
||||||
openSystemPreferencesAction: () => unknown;
|
openSystemPreferencesAction: () => unknown;
|
||||||
|
@ -85,6 +90,7 @@ export type PropsType = {
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
|
setOutgoingRing: (_: boolean) => void;
|
||||||
setPresenting: (_?: PresentedSource) => void;
|
setPresenting: (_?: PresentedSource) => void;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
stopRingtone: () => unknown;
|
stopRingtone: () => unknown;
|
||||||
|
@ -106,9 +112,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
hangUp,
|
hangUp,
|
||||||
i18n,
|
i18n,
|
||||||
|
isGroupCallOutboundRingEnabled,
|
||||||
keyChangeOk,
|
keyChangeOk,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
getPresentingSources,
|
getPresentingSources,
|
||||||
|
maxGroupCallRingSize,
|
||||||
me,
|
me,
|
||||||
openSystemPreferencesAction,
|
openSystemPreferencesAction,
|
||||||
renderDeviceSelection,
|
renderDeviceSelection,
|
||||||
|
@ -119,6 +127,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setPresenting,
|
setPresenting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
|
setOutgoingRing,
|
||||||
startCall,
|
startCall,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
togglePip,
|
togglePip,
|
||||||
|
@ -136,6 +145,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
presentingSourcesAvailable,
|
presentingSourcesAvailable,
|
||||||
settingsDialogOpen,
|
settingsDialogOpen,
|
||||||
showParticipantsList,
|
showParticipantsList,
|
||||||
|
outgoingRing,
|
||||||
} = activeCall;
|
} = activeCall;
|
||||||
|
|
||||||
const cancelActiveCall = useCallback(() => {
|
const cancelActiveCall = useCallback(() => {
|
||||||
|
@ -178,7 +188,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
let showCallLobby: boolean;
|
let showCallLobby: boolean;
|
||||||
let groupMembers:
|
let groupMembers:
|
||||||
| undefined
|
| undefined
|
||||||
| Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
|
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct: {
|
case CallMode.Direct: {
|
||||||
|
@ -222,14 +232,18 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
hasLocalVideo={hasLocalVideo}
|
hasLocalVideo={hasLocalVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupCall={activeCall.callMode === CallMode.Group}
|
isGroupCall={activeCall.callMode === CallMode.Group}
|
||||||
|
isGroupCallOutboundRingEnabled={isGroupCallOutboundRingEnabled}
|
||||||
isCallFull={isCallFull}
|
isCallFull={isCallFull}
|
||||||
|
maxGroupCallRingSize={maxGroupCallRingSize}
|
||||||
me={me}
|
me={me}
|
||||||
onCallCanceled={cancelActiveCall}
|
onCallCanceled={cancelActiveCall}
|
||||||
onJoinCall={joinActiveCall}
|
onJoinCall={joinActiveCall}
|
||||||
|
outgoingRing={outgoingRing}
|
||||||
peekedParticipants={peekedParticipants}
|
peekedParticipants={peekedParticipants}
|
||||||
setLocalPreview={setLocalPreview}
|
setLocalPreview={setLocalPreview}
|
||||||
setLocalAudio={setLocalAudio}
|
setLocalAudio={setLocalAudio}
|
||||||
setLocalVideo={setLocalVideo}
|
setLocalVideo={setLocalVideo}
|
||||||
|
setOutgoingRing={setOutgoingRing}
|
||||||
showParticipantsList={showParticipantsList}
|
showParticipantsList={showParticipantsList}
|
||||||
toggleParticipants={toggleParticipants}
|
toggleParticipants={toggleParticipants}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
|
@ -287,6 +301,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
activeCall={activeCall}
|
activeCall={activeCall}
|
||||||
getPresentingSources={getPresentingSources}
|
getPresentingSources={getPresentingSources}
|
||||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||||
|
groupMembers={groupMembers}
|
||||||
hangUp={hangUp}
|
hangUp={hangUp}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
joinedAt={joinedAt}
|
joinedAt={joinedAt}
|
||||||
|
@ -354,6 +369,7 @@ export const CallManager: React.FC<PropsType> = props => {
|
||||||
notifyForCall,
|
notifyForCall,
|
||||||
playRingtone,
|
playRingtone,
|
||||||
stopRingtone,
|
stopRingtone,
|
||||||
|
setOutgoingRing,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const shouldRing = getShouldRing(props);
|
const shouldRing = getShouldRing(props);
|
||||||
|
@ -369,6 +385,21 @@ export const CallManager: React.FC<PropsType> = props => {
|
||||||
return noop;
|
return noop;
|
||||||
}, [shouldRing, playRingtone, stopRingtone]);
|
}, [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) {
|
if (activeCall) {
|
||||||
// `props` should logically have an `activeCall` at this point, but TypeScript can't
|
// `props` should logically have an `activeCall` at this point, but TypeScript can't
|
||||||
// figure that out, so we pass it in again.
|
// figure that out, so we pass it in again.
|
||||||
|
@ -412,7 +443,14 @@ function getShouldRing({
|
||||||
activeCall.callState === CallState.Ringing
|
activeCall.callState === CallState.Ringing
|
||||||
);
|
);
|
||||||
case CallMode.Group:
|
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:
|
default:
|
||||||
throw missingCaseError(activeCall);
|
throw missingCaseError(activeCall);
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@ const createActiveCallProp = (
|
||||||
'isInSpeakerView',
|
'isInSpeakerView',
|
||||||
overrideProps.isInSpeakerView || false
|
overrideProps.isInSpeakerView || false
|
||||||
),
|
),
|
||||||
|
outgoingRing: true,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
@ -147,9 +148,11 @@ const createProps = (
|
||||||
i18n,
|
i18n,
|
||||||
me: {
|
me: {
|
||||||
color: AvatarColors[1],
|
color: AvatarColors[1],
|
||||||
|
id: '6146087e-f7ef-457e-9a8d-47df1fdd6b25',
|
||||||
name: 'Morty Smith',
|
name: 'Morty Smith',
|
||||||
profileName: 'Morty Smith',
|
profileName: 'Morty Smith',
|
||||||
title: 'Morty Smith',
|
title: 'Morty Smith',
|
||||||
|
uuid: '3c134598-eecb-42ab-9ad3-2b0873f771b2',
|
||||||
},
|
},
|
||||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { noop } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
|
@ -13,6 +19,7 @@ import {
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
import { Avatar } from './Avatar';
|
import { Avatar } from './Avatar';
|
||||||
import { CallingHeader } from './CallingHeader';
|
import { CallingHeader } from './CallingHeader';
|
||||||
|
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||||
import {
|
import {
|
||||||
|
@ -20,11 +27,13 @@ import {
|
||||||
CallMode,
|
CallMode,
|
||||||
CallState,
|
CallState,
|
||||||
GroupCallConnectionState,
|
GroupCallConnectionState,
|
||||||
|
GroupCallJoinState,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
PresentedSource,
|
PresentedSource,
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import { AvatarColors, AvatarColorType } from '../types/Colors';
|
import { AvatarColors, AvatarColorType } from '../types/Colors';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { CallingToastManager } from './CallingToastManager';
|
import { CallingToastManager } from './CallingToastManager';
|
||||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||||
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
||||||
|
@ -37,16 +46,19 @@ export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||||
getPresentingSources: () => void;
|
getPresentingSources: () => void;
|
||||||
|
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
hangUp: (_: HangUpType) => void;
|
hangUp: (_: HangUpType) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
me: {
|
me: {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color?: AvatarColorType;
|
color?: AvatarColorType;
|
||||||
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
uuid: string;
|
||||||
};
|
};
|
||||||
openSystemPreferencesAction: () => unknown;
|
openSystemPreferencesAction: () => unknown;
|
||||||
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
|
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
|
||||||
|
@ -67,6 +79,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
activeCall,
|
activeCall,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
getPresentingSources,
|
getPresentingSources,
|
||||||
|
groupMembers,
|
||||||
hangUp,
|
hangUp,
|
||||||
i18n,
|
i18n,
|
||||||
joinedAt,
|
joinedAt,
|
||||||
|
@ -198,36 +211,50 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let isRinging: boolean;
|
||||||
|
let hasCallStarted: boolean;
|
||||||
let headerMessage: string | undefined;
|
let headerMessage: string | undefined;
|
||||||
let headerTitle: string | undefined;
|
let headerTitle: string | undefined;
|
||||||
let isConnected: boolean;
|
let isConnected: boolean;
|
||||||
let participantCount: number;
|
let participantCount: number;
|
||||||
let remoteParticipantsElement: JSX.Element;
|
let remoteParticipantsElement: ReactNode;
|
||||||
|
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct:
|
case CallMode.Direct: {
|
||||||
headerMessage = renderHeaderMessage(
|
isRinging =
|
||||||
|
activeCall.callState === CallState.Prering ||
|
||||||
|
activeCall.callState === CallState.Ringing;
|
||||||
|
hasCallStarted = !isRinging;
|
||||||
|
headerMessage = renderDirectCallHeaderMessage(
|
||||||
i18n,
|
i18n,
|
||||||
activeCall.callState || CallState.Prering,
|
activeCall.callState || CallState.Prering,
|
||||||
acceptedDuration
|
acceptedDuration
|
||||||
);
|
);
|
||||||
headerTitle = conversation.title;
|
headerTitle = isRinging ? undefined : conversation.title;
|
||||||
isConnected = activeCall.callState === CallState.Accepted;
|
isConnected = activeCall.callState === CallState.Accepted;
|
||||||
participantCount = isConnected ? 2 : 0;
|
participantCount = isConnected ? 2 : 0;
|
||||||
remoteParticipantsElement = (
|
remoteParticipantsElement = hasCallStarted ? (
|
||||||
<DirectCallRemoteParticipant
|
<DirectCallRemoteParticipant
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
hasRemoteVideo={hasRemoteVideo}
|
hasRemoteVideo={hasRemoteVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
setRendererCanvas={setRendererCanvas}
|
setRendererCanvas={setRendererCanvas}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="module-ongoing-call__direct-call-ringing-spacer" />
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
|
isRinging =
|
||||||
|
activeCall.outgoingRing && !activeCall.remoteParticipants.length;
|
||||||
|
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
|
||||||
participantCount = activeCall.remoteParticipants.length + 1;
|
participantCount = activeCall.remoteParticipants.length + 1;
|
||||||
headerMessage = undefined;
|
headerMessage = undefined;
|
||||||
|
|
||||||
if (currentPresenter) {
|
if (isRinging) {
|
||||||
|
headerTitle = undefined;
|
||||||
|
} else if (currentPresenter) {
|
||||||
headerTitle = i18n('calling__presenting--person-ongoing', [
|
headerTitle = i18n('calling__presenting--person-ongoing', [
|
||||||
currentPresenter.title,
|
currentPresenter.title,
|
||||||
]);
|
]);
|
||||||
|
@ -301,7 +328,10 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
'module-calling__container',
|
'module-calling__container',
|
||||||
`module-ongoing-call__container--${getCallModeClassSuffix(
|
`module-ongoing-call__container--${getCallModeClassSuffix(
|
||||||
activeCall.callMode
|
activeCall.callMode
|
||||||
)}`
|
)}`,
|
||||||
|
`module-ongoing-call__container--${
|
||||||
|
hasCallStarted ? 'call-started' : 'call-not-started'
|
||||||
|
}`
|
||||||
)}
|
)}
|
||||||
onMouseMove={() => {
|
onMouseMove={() => {
|
||||||
setShowControls(true);
|
setShowControls(true);
|
||||||
|
@ -335,6 +365,15 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
toggleSpeakerView={toggleSpeakerView}
|
toggleSpeakerView={toggleSpeakerView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{isRinging && (
|
||||||
|
<CallingPreCallInfo
|
||||||
|
conversation={conversation}
|
||||||
|
groupMembers={groupMembers}
|
||||||
|
i18n={i18n}
|
||||||
|
me={me}
|
||||||
|
ringMode={RingMode.IsRinging}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{remoteParticipantsElement}
|
{remoteParticipantsElement}
|
||||||
{isSendingVideo && isLonelyInGroup ? (
|
{isSendingVideo && isLonelyInGroup ? (
|
||||||
<div className="module-ongoing-call__local-preview-fullsize">
|
<div className="module-ongoing-call__local-preview-fullsize">
|
||||||
|
@ -457,22 +496,18 @@ function getCallModeClassSuffix(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHeaderMessage(
|
function renderDirectCallHeaderMessage(
|
||||||
i18n: LocalizerType,
|
i18n: LocalizerType,
|
||||||
callState: CallState,
|
callState: CallState,
|
||||||
acceptedDuration: null | number
|
acceptedDuration: null | number
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
let message;
|
if (callState === CallState.Reconnecting) {
|
||||||
if (callState === CallState.Prering) {
|
return i18n('callReconnecting');
|
||||||
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)]);
|
|
||||||
}
|
}
|
||||||
return message;
|
if (callState === CallState.Accepted && acceptedDuration) {
|
||||||
|
return i18n('callDuration', [renderDuration(acceptedDuration)]);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDuration(ms: number): string {
|
function renderDuration(ms: number): string {
|
||||||
|
|
|
@ -16,6 +16,9 @@ export enum CallingButtonType {
|
||||||
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
|
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
|
||||||
PRESENTING_OFF = 'PRESENTING_OFF',
|
PRESENTING_OFF = 'PRESENTING_OFF',
|
||||||
PRESENTING_ON = 'PRESENTING_ON',
|
PRESENTING_ON = 'PRESENTING_ON',
|
||||||
|
RING_DISABLED = 'RING_DISABLED',
|
||||||
|
RING_OFF = 'RING_OFF',
|
||||||
|
RING_ON = 'RING_ON',
|
||||||
VIDEO_DISABLED = 'VIDEO_DISABLED',
|
VIDEO_DISABLED = 'VIDEO_DISABLED',
|
||||||
VIDEO_OFF = 'VIDEO_OFF',
|
VIDEO_OFF = 'VIDEO_OFF',
|
||||||
VIDEO_ON = 'VIDEO_ON',
|
VIDEO_ON = 'VIDEO_ON',
|
||||||
|
@ -24,6 +27,7 @@ export enum CallingButtonType {
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
buttonType: CallingButtonType;
|
buttonType: CallingButtonType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isVisible?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
tooltipDirection?: TooltipPlacement;
|
tooltipDirection?: TooltipPlacement;
|
||||||
};
|
};
|
||||||
|
@ -31,6 +35,7 @@ export type PropsType = {
|
||||||
export const CallingButton = ({
|
export const CallingButton = ({
|
||||||
buttonType,
|
buttonType,
|
||||||
i18n,
|
i18n,
|
||||||
|
isVisible = true,
|
||||||
onClick,
|
onClick,
|
||||||
tooltipDirection,
|
tooltipDirection,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
@ -70,6 +75,21 @@ export const CallingButton = ({
|
||||||
classNameSuffix = 'hangup';
|
classNameSuffix = 'hangup';
|
||||||
tooltipContent = i18n('calling__hangup');
|
tooltipContent = i18n('calling__hangup');
|
||||||
label = 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) {
|
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
||||||
classNameSuffix = 'presenting--disabled';
|
classNameSuffix = 'presenting--disabled';
|
||||||
tooltipContent = i18n('calling__button--presenting-disabled');
|
tooltipContent = i18n('calling__button--presenting-disabled');
|
||||||
|
@ -96,7 +116,12 @@ export const CallingButton = ({
|
||||||
direction={tooltipDirection}
|
direction={tooltipDirection}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
>
|
>
|
||||||
<div className="module-calling-button__container">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-calling-button__container',
|
||||||
|
!isVisible && 'module-calling-button__container--hidden'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
aria-label={tooltipContent}
|
aria-label={tooltipContent}
|
||||||
className={className}
|
className={className}
|
||||||
|
|
|
@ -42,9 +42,9 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||||
return {
|
return {
|
||||||
availableCameras: overrideProps.availableCameras || [camera],
|
availableCameras: overrideProps.availableCameras || [camera],
|
||||||
conversation,
|
conversation,
|
||||||
groupMembers: isGroupCall
|
groupMembers:
|
||||||
? times(3, () => getDefaultConversation())
|
overrideProps.groupMembers ||
|
||||||
: undefined,
|
(isGroupCall ? times(3, () => getDefaultConversation()) : undefined),
|
||||||
hasLocalAudio: boolean(
|
hasLocalAudio: boolean(
|
||||||
'hasLocalAudio',
|
'hasLocalAudio',
|
||||||
overrideProps.hasLocalAudio || false
|
overrideProps.hasLocalAudio || false
|
||||||
|
@ -55,17 +55,22 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||||
),
|
),
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall,
|
isGroupCall,
|
||||||
|
isGroupCallOutboundRingEnabled: true,
|
||||||
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
||||||
|
maxGroupCallRingSize: overrideProps.maxGroupCallRingSize || 16,
|
||||||
me: overrideProps.me || {
|
me: overrideProps.me || {
|
||||||
color: AvatarColors[0],
|
color: AvatarColors[0],
|
||||||
|
id: generateUuid(),
|
||||||
uuid: generateUuid(),
|
uuid: generateUuid(),
|
||||||
},
|
},
|
||||||
onCallCanceled: action('on-call-canceled'),
|
onCallCanceled: action('on-call-canceled'),
|
||||||
onJoinCall: action('on-join-call'),
|
onJoinCall: action('on-join-call'),
|
||||||
|
outgoingRing: boolean('outgoingRing', Boolean(overrideProps.outgoingRing)),
|
||||||
peekedParticipants: overrideProps.peekedParticipants || [],
|
peekedParticipants: overrideProps.peekedParticipants || [],
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
setLocalPreview: action('set-local-preview'),
|
setLocalPreview: action('set-local-preview'),
|
||||||
setLocalVideo: action('set-local-video'),
|
setLocalVideo: action('set-local-video'),
|
||||||
|
setOutgoingRing: action('set-outgoing-ring'),
|
||||||
showParticipantsList: boolean(
|
showParticipantsList: boolean(
|
||||||
'showParticipantsList',
|
'showParticipantsList',
|
||||||
Boolean(overrideProps.showParticipantsList)
|
Boolean(overrideProps.showParticipantsList)
|
||||||
|
@ -101,6 +106,7 @@ story.add('No Camera, local avatar', () => {
|
||||||
me: {
|
me: {
|
||||||
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
||||||
color: AvatarColors[0],
|
color: AvatarColors[0],
|
||||||
|
id: generateUuid(),
|
||||||
uuid: generateUuid(),
|
uuid: generateUuid(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -138,7 +144,10 @@ story.add('Group Call - 1 peeked participant (self)', () => {
|
||||||
const uuid = generateUuid();
|
const uuid = generateUuid();
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
isGroupCall: true,
|
||||||
me: { uuid },
|
me: {
|
||||||
|
id: generateUuid(),
|
||||||
|
uuid,
|
||||||
|
},
|
||||||
peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })],
|
peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })],
|
||||||
});
|
});
|
||||||
return <CallingLobby {...props} />;
|
return <CallingLobby {...props} />;
|
||||||
|
@ -175,3 +184,11 @@ story.add('Group Call - call full', () => {
|
||||||
});
|
});
|
||||||
return <CallingLobby {...props} />;
|
return <CallingLobby {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Group Call - 0 peeked participants, big group', () => {
|
||||||
|
const props = createProps({
|
||||||
|
isGroupCall: true,
|
||||||
|
groupMembers: times(100, () => getDefaultConversation()),
|
||||||
|
});
|
||||||
|
return <CallingLobby {...props} />;
|
||||||
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { CallingButton, CallingButtonType } from './CallingButton';
|
||||||
import { TooltipPlacement } from './Tooltip';
|
import { TooltipPlacement } from './Tooltip';
|
||||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||||
import { CallingHeader } from './CallingHeader';
|
import { CallingHeader } from './CallingHeader';
|
||||||
import { CallingPreCallInfo } from './CallingPreCallInfo';
|
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||||
import {
|
import {
|
||||||
CallingLobbyJoinButton,
|
CallingLobbyJoinButton,
|
||||||
CallingLobbyJoinButtonVariant,
|
CallingLobbyJoinButtonVariant,
|
||||||
|
@ -37,23 +37,28 @@ export type PropsType = {
|
||||||
| 'type'
|
| 'type'
|
||||||
| 'unblurredAvatarPath'
|
| 'unblurredAvatarPath'
|
||||||
>;
|
>;
|
||||||
groupMembers?: Array<Pick<ConversationType, 'title'>>;
|
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isGroupCall: boolean;
|
isGroupCall: boolean;
|
||||||
|
isGroupCallOutboundRingEnabled: boolean;
|
||||||
isCallFull?: boolean;
|
isCallFull?: boolean;
|
||||||
|
maxGroupCallRingSize: number;
|
||||||
me: {
|
me: {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
id: string;
|
||||||
color?: AvatarColorType;
|
color?: AvatarColorType;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
};
|
};
|
||||||
onCallCanceled: () => void;
|
onCallCanceled: () => void;
|
||||||
onJoinCall: () => void;
|
onJoinCall: () => void;
|
||||||
|
outgoingRing: boolean;
|
||||||
peekedParticipants: Array<ConversationType>;
|
peekedParticipants: Array<ConversationType>;
|
||||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||||
|
setOutgoingRing: (_: boolean) => void;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
toggleParticipants: () => void;
|
toggleParticipants: () => void;
|
||||||
toggleSettings: () => void;
|
toggleSettings: () => void;
|
||||||
|
@ -67,7 +72,9 @@ export const CallingLobby = ({
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall = false,
|
isGroupCall = false,
|
||||||
|
isGroupCallOutboundRingEnabled,
|
||||||
isCallFull = false,
|
isCallFull = false,
|
||||||
|
maxGroupCallRingSize,
|
||||||
me,
|
me,
|
||||||
onCallCanceled,
|
onCallCanceled,
|
||||||
onJoinCall,
|
onJoinCall,
|
||||||
|
@ -75,9 +82,11 @@ export const CallingLobby = ({
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
|
setOutgoingRing,
|
||||||
showParticipantsList,
|
showParticipantsList,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
|
outgoingRing,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
|
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
@ -91,6 +100,10 @@ export const CallingLobby = ({
|
||||||
setLocalVideo({ enabled: !hasLocalVideo });
|
setLocalVideo({ enabled: !hasLocalVideo });
|
||||||
}, [hasLocalVideo, setLocalVideo]);
|
}, [hasLocalVideo, setLocalVideo]);
|
||||||
|
|
||||||
|
const toggleOutgoingRing = React.useCallback((): void => {
|
||||||
|
setOutgoingRing(!outgoingRing);
|
||||||
|
}, [outgoingRing, setOutgoingRing]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLocalPreview({ element: localVideoRef });
|
setLocalPreview({ element: localVideoRef });
|
||||||
|
|
||||||
|
@ -132,10 +145,36 @@ export const CallingLobby = ({
|
||||||
: availableCameras.length === 0
|
: availableCameras.length === 0
|
||||||
? CallingButtonType.VIDEO_DISABLED
|
? CallingButtonType.VIDEO_DISABLED
|
||||||
: CallingButtonType.VIDEO_OFF;
|
: CallingButtonType.VIDEO_OFF;
|
||||||
|
|
||||||
const audioButtonType = hasLocalAudio
|
const audioButtonType = hasLocalAudio
|
||||||
? CallingButtonType.AUDIO_ON
|
? CallingButtonType.AUDIO_ON
|
||||||
: CallingButtonType.AUDIO_OFF;
|
: 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;
|
const canJoin = !isCallFull && !isCallConnecting;
|
||||||
|
|
||||||
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
|
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
|
||||||
|
@ -182,6 +221,7 @@ export const CallingLobby = ({
|
||||||
isCallFull={isCallFull}
|
isCallFull={isCallFull}
|
||||||
me={me}
|
me={me}
|
||||||
peekedParticipants={peekedParticipants}
|
peekedParticipants={peekedParticipants}
|
||||||
|
ringMode={preCallInfoRingMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -208,6 +248,13 @@ export const CallingLobby = ({
|
||||||
onClick={toggleAudio}
|
onClick={toggleAudio}
|
||||||
tooltipDirection={TooltipPlacement.Top}
|
tooltipDirection={TooltipPlacement.Top}
|
||||||
/>
|
/>
|
||||||
|
<CallingButton
|
||||||
|
buttonType={ringButtonType}
|
||||||
|
i18n={i18n}
|
||||||
|
isVisible={isRingButtonVisible}
|
||||||
|
onClick={toggleOutgoingRing}
|
||||||
|
tooltipDirection={TooltipPlacement.Top}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CallingLobbyJoinButton
|
<CallingLobbyJoinButton
|
||||||
|
|
|
@ -40,6 +40,7 @@ const getCommonActiveCallData = () => ({
|
||||||
hasLocalVideo: boolean('hasLocalVideo', false),
|
hasLocalVideo: boolean('hasLocalVideo', false),
|
||||||
isInSpeakerView: boolean('isInSpeakerView', false),
|
isInSpeakerView: boolean('isInSpeakerView', false),
|
||||||
joinedAt: Date.now(),
|
joinedAt: Date.now(),
|
||||||
|
outgoingRing: true,
|
||||||
pip: true,
|
pip: true,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
import { CallingPreCallInfo } from './CallingPreCallInfo';
|
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
const getDefaultGroupConversation = () =>
|
const getDefaultGroupConversation = () =>
|
||||||
|
@ -28,34 +28,28 @@ story.add('Direct conversation', () => (
|
||||||
conversation={getDefaultConversation()}
|
conversation={getDefaultConversation()}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
me={getDefaultConversation()}
|
me={getDefaultConversation()}
|
||||||
/>
|
ringMode={RingMode.WillRing}
|
||||||
));
|
|
||||||
|
|
||||||
story.add('Group conversation, empty group', () => (
|
|
||||||
<CallingPreCallInfo
|
|
||||||
conversation={getDefaultGroupConversation()}
|
|
||||||
groupMembers={[]}
|
|
||||||
i18n={i18n}
|
|
||||||
me={getDefaultConversation()}
|
|
||||||
peekedParticipants={[]}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
times(5, numberOfOtherPeople => {
|
times(5, numberOfOtherPeople => {
|
||||||
story.add(
|
[true, false].forEach(willRing => {
|
||||||
`Group conversation, group has ${numberOfOtherPeople} other member${
|
story.add(
|
||||||
numberOfOtherPeople === 1 ? '' : 's'
|
`Group conversation, group has ${numberOfOtherPeople} other member${
|
||||||
}`,
|
numberOfOtherPeople === 1 ? '' : 's'
|
||||||
() => (
|
}, will ${willRing ? 'ring' : 'notify'}`,
|
||||||
<CallingPreCallInfo
|
() => (
|
||||||
conversation={getDefaultGroupConversation()}
|
<CallingPreCallInfo
|
||||||
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
|
conversation={getDefaultGroupConversation()}
|
||||||
i18n={i18n}
|
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
|
||||||
me={getDefaultConversation()}
|
i18n={i18n}
|
||||||
peekedParticipants={[]}
|
me={getDefaultConversation()}
|
||||||
/>
|
peekedParticipants={[]}
|
||||||
)
|
ringMode={willRing ? RingMode.WillRing : RingMode.WillNotRing}
|
||||||
);
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
range(1, 5).forEach(numberOfOtherPeople => {
|
range(1, 5).forEach(numberOfOtherPeople => {
|
||||||
|
@ -70,6 +64,7 @@ range(1, 5).forEach(numberOfOtherPeople => {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
me={getDefaultConversation()}
|
me={getDefaultConversation()}
|
||||||
peekedParticipants={otherMembers.slice(0, numberOfOtherPeople)}
|
peekedParticipants={otherMembers.slice(0, numberOfOtherPeople)}
|
||||||
|
ringMode={RingMode.WillRing}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -84,6 +79,7 @@ story.add('Group conversation, you on an other device', () => {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
me={me}
|
me={me}
|
||||||
peekedParticipants={[me]}
|
peekedParticipants={[me]}
|
||||||
|
ringMode={RingMode.WillRing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -96,5 +92,6 @@ story.add('Group conversation, call is full', () => (
|
||||||
isCallFull
|
isCallFull
|
||||||
me={getDefaultConversation()}
|
me={getDefaultConversation()}
|
||||||
peekedParticipants={otherMembers}
|
peekedParticipants={otherMembers}
|
||||||
|
ringMode={RingMode.WillRing}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -9,6 +9,12 @@ import { Emojify } from './conversation/Emojify';
|
||||||
import { getParticipantName } from '../util/callingGetParticipantName';
|
import { getParticipantName } from '../util/callingGetParticipantName';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
|
||||||
|
export enum RingMode {
|
||||||
|
WillNotRing,
|
||||||
|
WillRing,
|
||||||
|
IsRinging,
|
||||||
|
}
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
conversation: Pick<
|
conversation: Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -25,10 +31,11 @@ type PropsType = {
|
||||||
| 'unblurredAvatarPath'
|
| 'unblurredAvatarPath'
|
||||||
>;
|
>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
me: Pick<ConversationType, 'uuid'>;
|
me: Pick<ConversationType, 'id' | 'uuid'>;
|
||||||
|
ringMode: RingMode;
|
||||||
|
|
||||||
// The following should only be set for group conversations.
|
// 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;
|
isCallFull?: boolean;
|
||||||
peekedParticipants?: Array<
|
peekedParticipants?: Array<
|
||||||
Pick<ConversationType, 'firstName' | 'title' | 'uuid'>
|
Pick<ConversationType, 'firstName' | 'title' | 'uuid'>
|
||||||
|
@ -42,9 +49,12 @@ export const CallingPreCallInfo: FunctionComponent<PropsType> = ({
|
||||||
isCallFull = false,
|
isCallFull = false,
|
||||||
me,
|
me,
|
||||||
peekedParticipants = [],
|
peekedParticipants = [],
|
||||||
|
ringMode,
|
||||||
}) => {
|
}) => {
|
||||||
let subtitle: string;
|
let subtitle: string;
|
||||||
if (isCallFull) {
|
if (ringMode === RingMode.IsRinging) {
|
||||||
|
subtitle = i18n('outgoingCallRinging');
|
||||||
|
} else if (isCallFull) {
|
||||||
subtitle = i18n('calling__call-is-full');
|
subtitle = i18n('calling__call-is-full');
|
||||||
} else if (peekedParticipants.length) {
|
} else if (peekedParticipants.length) {
|
||||||
// It should be rare to see yourself in this list, but it's possible if (1) you rejoin
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
} else if (conversation.type === 'direct') {
|
} else {
|
||||||
subtitle = i18n('calling__pre-call-info--will-ring-1', [
|
let memberNames: Array<string>;
|
||||||
getParticipantName(conversation),
|
switch (conversation.type) {
|
||||||
]);
|
case 'direct':
|
||||||
} else if (conversation.type === 'group') {
|
memberNames = [getParticipantName(conversation)];
|
||||||
const memberNames = groupMembers.map(getParticipantName);
|
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) {
|
switch (memberNames.length) {
|
||||||
case 0:
|
case 0:
|
||||||
subtitle = i18n('calling__pre-call-info--empty-group');
|
subtitle = i18n('calling__pre-call-info--empty-group');
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1: {
|
||||||
subtitle = i18n('calling__pre-call-info--will-notify-1', [
|
const i18nValues = [memberNames[0]];
|
||||||
memberNames[0],
|
subtitle = ring
|
||||||
]);
|
? i18n('calling__pre-call-info--will-ring-1', i18nValues)
|
||||||
|
: i18n('calling__pre-call-info--will-notify-1', i18nValues);
|
||||||
break;
|
break;
|
||||||
case 2:
|
}
|
||||||
subtitle = i18n('calling__pre-call-info--will-notify-2', {
|
case 2: {
|
||||||
|
const i18nValues = {
|
||||||
first: memberNames[0],
|
first: memberNames[0],
|
||||||
second: memberNames[1],
|
second: memberNames[1],
|
||||||
});
|
};
|
||||||
|
subtitle = ring
|
||||||
|
? i18n('calling__pre-call-info--will-ring-2', i18nValues)
|
||||||
|
: i18n('calling__pre-call-info--will-notify-2', i18nValues);
|
||||||
break;
|
break;
|
||||||
case 3:
|
}
|
||||||
subtitle = i18n('calling__pre-call-info--will-notify-3', {
|
case 3: {
|
||||||
|
const i18nValues = {
|
||||||
first: memberNames[0],
|
first: memberNames[0],
|
||||||
second: memberNames[1],
|
second: memberNames[1],
|
||||||
third: memberNames[2],
|
third: memberNames[2],
|
||||||
});
|
};
|
||||||
|
subtitle = ring
|
||||||
|
? i18n('calling__pre-call-info--will-ring-3', i18nValues)
|
||||||
|
: i18n('calling__pre-call-info--will-notify-3', i18nValues);
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
subtitle = i18n('calling__pre-call-info--will-notify-many', {
|
default: {
|
||||||
|
const i18nValues = {
|
||||||
first: memberNames[0],
|
first: memberNames[0],
|
||||||
second: memberNames[1],
|
second: memberNames[1],
|
||||||
others: String(memberNames.length - 2),
|
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;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw missingCaseError(conversation.type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -684,7 +684,8 @@ export class CallingClass {
|
||||||
public joinGroupCall(
|
public joinGroupCall(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
hasLocalAudio: boolean,
|
hasLocalAudio: boolean,
|
||||||
hasLocalVideo: boolean
|
hasLocalVideo: boolean,
|
||||||
|
shouldRing: boolean
|
||||||
): void {
|
): void {
|
||||||
this.attemptToGiveOurUuidToRingRtc();
|
this.attemptToGiveOurUuidToRingRtc();
|
||||||
|
|
||||||
|
@ -717,10 +718,7 @@ export class CallingClass {
|
||||||
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
|
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
|
||||||
this.videoCapturer.enableCaptureAndSend(groupCall);
|
this.videoCapturer.enableCaptureAndSend(groupCall);
|
||||||
|
|
||||||
// This is a temporary flag to help all client teams (Desktop, iOS, and Android)
|
if (shouldRing) {
|
||||||
// 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) {
|
|
||||||
groupCall.ringAll();
|
groupCall.ringAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ export type ActiveCallStateType = {
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
isInSpeakerView: boolean;
|
isInSpeakerView: boolean;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
|
outgoingRing: boolean;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
presentingSource?: PresentedSource;
|
presentingSource?: PresentedSource;
|
||||||
presentingSourcesAvailable?: Array<PresentableSource>;
|
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 RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
|
||||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_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 = 'calling/SET_PRESENTING';
|
||||||
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
const SET_PRESENTING_SOURCES = 'calling/SET_PRESENTING_SOURCES';
|
||||||
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
|
const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
|
||||||
|
@ -420,6 +422,11 @@ type SetPresentingSourcesActionType = {
|
||||||
payload: Array<PresentableSource>;
|
payload: Array<PresentableSource>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SetOutgoingRingActionType = {
|
||||||
|
type: 'calling/SET_OUTGOING_RING';
|
||||||
|
payload: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ShowCallLobbyActionType = {
|
type ShowCallLobbyActionType = {
|
||||||
type: 'calling/SHOW_CALL_LOBBY';
|
type: 'calling/SHOW_CALL_LOBBY';
|
||||||
payload: ShowCallLobbyType;
|
payload: ShowCallLobbyType;
|
||||||
|
@ -474,6 +481,7 @@ export type CallingActionType =
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
| SetPresentingSourcesActionType
|
| SetPresentingSourcesActionType
|
||||||
|
| SetOutgoingRingActionType
|
||||||
| ShowCallLobbyActionType
|
| ShowCallLobbyActionType
|
||||||
| StartDirectCallActionType
|
| StartDirectCallActionType
|
||||||
| ToggleNeedsScreenRecordingPermissionsActionType
|
| ToggleNeedsScreenRecordingPermissionsActionType
|
||||||
|
@ -502,7 +510,7 @@ function acceptCall(
|
||||||
await calling.acceptDirectCall(conversationId, asVideoCall);
|
await calling.acceptDirectCall(conversationId, asVideoCall);
|
||||||
break;
|
break;
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
calling.joinGroupCall(conversationId, true, asVideoCall);
|
calling.joinGroupCall(conversationId, true, asVideoCall, false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(call);
|
throw missingCaseError(call);
|
||||||
|
@ -1020,6 +1028,13 @@ function setPresenting(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setOutgoingRing(payload: boolean): SetOutgoingRingActionType {
|
||||||
|
return {
|
||||||
|
type: SET_OUTGOING_RING,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function startCallingLobby(
|
function startCallingLobby(
|
||||||
payload: StartCallingLobbyType
|
payload: StartCallingLobbyType
|
||||||
): ThunkAction<void, RootStateType, unknown, never> {
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
|
@ -1040,7 +1055,7 @@ function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
|
||||||
function startCall(
|
function startCall(
|
||||||
payload: StartCallType
|
payload: StartCallType
|
||||||
): ThunkAction<void, RootStateType, unknown, StartDirectCallActionType> {
|
): ThunkAction<void, RootStateType, unknown, StartDirectCallActionType> {
|
||||||
return dispatch => {
|
return (dispatch, getState) => {
|
||||||
switch (payload.callMode) {
|
switch (payload.callMode) {
|
||||||
case CallMode.Direct:
|
case CallMode.Direct:
|
||||||
calling.startOutgoingDirectCall(
|
calling.startOutgoingDirectCall(
|
||||||
|
@ -1053,15 +1068,20 @@ function startCall(
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case CallMode.Group:
|
case CallMode.Group: {
|
||||||
|
const outgoingRing = Boolean(
|
||||||
|
getState().calling.activeCallState?.outgoingRing
|
||||||
|
);
|
||||||
calling.joinGroupCall(
|
calling.joinGroupCall(
|
||||||
payload.conversationId,
|
payload.conversationId,
|
||||||
payload.hasLocalAudio,
|
payload.hasLocalAudio,
|
||||||
payload.hasLocalVideo
|
payload.hasLocalVideo,
|
||||||
|
outgoingRing
|
||||||
);
|
);
|
||||||
// The calling service should already be wired up to Redux so we don't need to
|
// The calling service should already be wired up to Redux so we don't need to
|
||||||
// dispatch anything here.
|
// dispatch anything here.
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(payload.callMode);
|
throw missingCaseError(payload.callMode);
|
||||||
}
|
}
|
||||||
|
@ -1126,6 +1146,7 @@ export const actions = {
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setPresenting,
|
setPresenting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
|
setOutgoingRing,
|
||||||
showCallLobby,
|
showCallLobby,
|
||||||
startCall,
|
startCall,
|
||||||
startCallingLobby,
|
startCallingLobby,
|
||||||
|
@ -1184,6 +1205,7 @@ export function reducer(
|
||||||
const { conversationId } = action.payload;
|
const { conversationId } = action.payload;
|
||||||
|
|
||||||
let call: DirectCallStateType | GroupCallStateType;
|
let call: DirectCallStateType | GroupCallStateType;
|
||||||
|
let outgoingRing: boolean;
|
||||||
switch (action.payload.callMode) {
|
switch (action.payload.callMode) {
|
||||||
case CallMode.Direct:
|
case CallMode.Direct:
|
||||||
call = {
|
call = {
|
||||||
|
@ -1192,6 +1214,7 @@ export function reducer(
|
||||||
isIncoming: false,
|
isIncoming: false,
|
||||||
isVideoCall: action.payload.hasLocalVideo,
|
isVideoCall: action.payload.hasLocalVideo,
|
||||||
};
|
};
|
||||||
|
outgoingRing = true;
|
||||||
break;
|
break;
|
||||||
case CallMode.Group: {
|
case CallMode.Group: {
|
||||||
// We expect to be in this state briefly. The Calling service should update the
|
// 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,
|
remoteParticipants: action.payload.remoteParticipants,
|
||||||
...getGroupCallRingState(existingCall),
|
...getGroupCallRingState(existingCall),
|
||||||
};
|
};
|
||||||
|
outgoingRing =
|
||||||
|
!call.peekInfo.uuids.length && !call.remoteParticipants.length;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -1232,6 +1257,7 @@ export function reducer(
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
outgoingRing,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1258,6 +1284,7 @@ export function reducer(
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
outgoingRing: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1279,6 +1306,7 @@ export function reducer(
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
outgoingRing: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1412,6 +1440,7 @@ export function reducer(
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
|
outgoingRing: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1513,6 +1542,18 @@ export function reducer(
|
||||||
: state.activeCallState;
|
: state.activeCallState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
newActiveCallState &&
|
||||||
|
newActiveCallState.outgoingRing &&
|
||||||
|
newActiveCallState.conversationId === conversationId &&
|
||||||
|
isAnybodyElseInGroupCall(newPeekInfo, ourUuid)
|
||||||
|
) {
|
||||||
|
newActiveCallState = {
|
||||||
|
...newActiveCallState,
|
||||||
|
outgoingRing: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let newRingState: GroupCallRingStateType;
|
let newRingState: GroupCallRingStateType;
|
||||||
if (joinState === GroupCallJoinState.NotJoined) {
|
if (joinState === GroupCallJoinState.NotJoined) {
|
||||||
newRingState = existingRingState;
|
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) {
|
if (action.type === TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS) {
|
||||||
const { activeCallState } = state;
|
const { activeCallState } = state;
|
||||||
if (!activeCallState) {
|
if (!activeCallState) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { getMe, getConversationSelector } from '../selectors/conversations';
|
||||||
import { getActiveCall } from '../ducks/calling';
|
import { getActiveCall } from '../ducks/calling';
|
||||||
import { ConversationType } from '../ducks/conversations';
|
import { ConversationType } from '../ducks/conversations';
|
||||||
import { getIncomingCall } from '../selectors/calling';
|
import { getIncomingCall } from '../selectors/calling';
|
||||||
|
import { getMaxGroupCallRingSize } from '../../groups/limits';
|
||||||
|
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
|
||||||
import {
|
import {
|
||||||
ActiveCallType,
|
ActiveCallType,
|
||||||
CallMode,
|
CallMode,
|
||||||
|
@ -111,6 +113,7 @@ const mapStateToActiveCallProp = (
|
||||||
hasLocalVideo: activeCallState.hasLocalVideo,
|
hasLocalVideo: activeCallState.hasLocalVideo,
|
||||||
isInSpeakerView: activeCallState.isInSpeakerView,
|
isInSpeakerView: activeCallState.isInSpeakerView,
|
||||||
joinedAt: activeCallState.joinedAt,
|
joinedAt: activeCallState.joinedAt,
|
||||||
|
outgoingRing: activeCallState.outgoingRing,
|
||||||
pip: activeCallState.pip,
|
pip: activeCallState.pip,
|
||||||
presentingSource: activeCallState.presentingSource,
|
presentingSource: activeCallState.presentingSource,
|
||||||
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
|
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
|
||||||
|
@ -292,7 +295,9 @@ const mapStateToProps = (state: StateType) => ({
|
||||||
availableCameras: state.calling.availableCameras,
|
availableCameras: state.calling.availableCameras,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
|
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
|
||||||
incomingCall: mapStateToIncomingCallProp(state),
|
incomingCall: mapStateToIncomingCallProp(state),
|
||||||
|
maxGroupCallRingSize: getMaxGroupCallRingSize(),
|
||||||
me: {
|
me: {
|
||||||
...getMe(state),
|
...getMe(state),
|
||||||
// `getMe` returns a `ConversationType` which might not have a UUID, at least
|
// `getMe` returns a `ConversationType` which might not have a UUID, at least
|
||||||
|
|
|
@ -46,6 +46,7 @@ describe('calling duck', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: true,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
},
|
},
|
||||||
|
@ -118,6 +119,7 @@ describe('calling duck', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: false,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
},
|
},
|
||||||
|
@ -423,6 +425,7 @@ describe('calling duck', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: false,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
});
|
});
|
||||||
|
@ -514,6 +517,7 @@ describe('calling duck', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: false,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
});
|
});
|
||||||
|
@ -1224,6 +1228,7 @@ describe('calling duck', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: false,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
});
|
});
|
||||||
|
@ -1264,6 +1269,62 @@ describe('calling duck', () => {
|
||||||
assert.isTrue(result.activeCallState?.hasLocalAudio);
|
assert.isTrue(result.activeCallState?.hasLocalAudio);
|
||||||
assert.isTrue(result.activeCallState?.hasLocalVideo);
|
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', () => {
|
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', () => {
|
describe('showCallLobby', () => {
|
||||||
const { showCallLobby } = actions;
|
const { showCallLobby } = actions;
|
||||||
|
|
||||||
|
@ -1548,6 +1627,7 @@ describe('calling duck', () => {
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
|
outgoingRing: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1610,6 +1690,7 @@ describe('calling duck', () => {
|
||||||
result.activeCallState?.conversationId,
|
result.activeCallState?.conversationId,
|
||||||
'fake-conversation-id'
|
'fake-conversation-id'
|
||||||
);
|
);
|
||||||
|
assert.isFalse(result.activeCallState?.outgoingRing);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('chooses fallback peek info if none is sent and there is no existing call', () => {
|
it('chooses fallback peek info if none is sent and there is no existing call', () => {
|
||||||
|
@ -1841,6 +1922,7 @@ describe('calling duck', () => {
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
|
outgoingRing: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ describe('state/selectors/calling', () => {
|
||||||
isInSpeakerView: false,
|
isInSpeakerView: false,
|
||||||
showParticipantsList: false,
|
showParticipantsList: false,
|
||||||
safetyNumberChangedUuids: [],
|
safetyNumberChangedUuids: [],
|
||||||
|
outgoingRing: true,
|
||||||
pip: false,
|
pip: false,
|
||||||
settingsDialogOpen: false,
|
settingsDialogOpen: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@ type ActiveCallBaseType = {
|
||||||
isInSpeakerView: boolean;
|
isInSpeakerView: boolean;
|
||||||
isSharingScreen?: boolean;
|
isSharingScreen?: boolean;
|
||||||
joinedAt?: number;
|
joinedAt?: number;
|
||||||
|
outgoingRing: boolean;
|
||||||
pip: boolean;
|
pip: boolean;
|
||||||
presentingSource?: PresentedSource;
|
presentingSource?: PresentedSource;
|
||||||
presentingSourcesAvailable?: Array<PresentableSource>;
|
presentingSourcesAvailable?: Array<PresentableSource>;
|
||||||
|
@ -60,7 +61,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
|
||||||
joinState: GroupCallJoinState;
|
joinState: GroupCallJoinState;
|
||||||
maxDevices: number;
|
maxDevices: number;
|
||||||
deviceCount: number;
|
deviceCount: number;
|
||||||
groupMembers: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
peekedParticipants: Array<ConversationType>;
|
peekedParticipants: Array<ConversationType>;
|
||||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||||
};
|
};
|
||||||
|
|
11
ts/util/isGroupCallOutboundRingEnabled.ts
Normal file
11
ts/util/isGroupCallOutboundRingEnabled.ts
Normal 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
1
ts/window.d.ts
vendored
|
@ -511,7 +511,6 @@ declare global {
|
||||||
GV2_ENABLE_STATE_PROCESSING: boolean;
|
GV2_ENABLE_STATE_PROCESSING: boolean;
|
||||||
GV2_MIGRATION_DISABLE_ADD: boolean;
|
GV2_MIGRATION_DISABLE_ADD: boolean;
|
||||||
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
||||||
RING_WHEN_JOINING_GROUP_CALLS: boolean;
|
|
||||||
|
|
||||||
RETRY_DELAY: boolean;
|
RETRY_DELAY: boolean;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue