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",
|
||||
"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"
|
||||
|
|
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_INVITE = false;
|
||||
|
||||
window.RING_WHEN_JOINING_GROUP_CALLS = false;
|
||||
|
||||
window.RETRY_DELAY = false;
|
||||
|
||||
window.platform = process.platform;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,7 @@ export type ConfigKeyType =
|
|||
| 'desktop.announcementGroup'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.disableGV1'
|
||||
| 'desktop.groupCallOutboundRing'
|
||||
| 'desktop.groupCalling'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.internalUser'
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />;
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('state/selectors/calling', () => {
|
|||
isInSpeakerView: false,
|
||||
showParticipantsList: false,
|
||||
safetyNumberChangedUuids: [],
|
||||
outgoingRing: true,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
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_MIGRATION_DISABLE_ADD: boolean;
|
||||
GV2_MIGRATION_DISABLE_INVITE: boolean;
|
||||
RING_WHEN_JOINING_GROUP_CALLS: boolean;
|
||||
|
||||
RETRY_DELAY: boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue