Group calling enhancements
This commit is contained in:
parent
72e4ec95ce
commit
1f0c091e13
27 changed files with 1038 additions and 451 deletions
|
@ -1183,17 +1183,85 @@
|
|||
"description": "Button tooltip label when the microphone is disabled"
|
||||
},
|
||||
"calling__button--audio-off": {
|
||||
"message": "Turn off microphone",
|
||||
"message": "Mute mic",
|
||||
"description": "Button tooltip label for turning off the microphone"
|
||||
},
|
||||
"calling__button--audio-on": {
|
||||
"message": "Turn on microphone",
|
||||
"message": "Unmute mic",
|
||||
"description": "Button tooltip label for turning on the microphone"
|
||||
},
|
||||
"calling__your-video-is-off": {
|
||||
"message": "Your video is off",
|
||||
"description": "Label in the calling lobby indicating that your camera is off"
|
||||
},
|
||||
"calling__lobby-summary--zero": {
|
||||
"message": "No one else is here",
|
||||
"description": "Shown in the calling lobby to describe who is in the call"
|
||||
},
|
||||
"calling__lobby-summary--single": {
|
||||
"message": "$first$ is in this call",
|
||||
"description": "Shown in the calling lobby to describe who is in the call",
|
||||
"placeholders": {
|
||||
"first": {
|
||||
"content": "$1",
|
||||
"example": "Sam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calling__lobby-summary--double": {
|
||||
"message": "$first$ and $second$ are in this call",
|
||||
"description": "Shown in the calling lobby to describe who is in the call",
|
||||
"placeholders": {
|
||||
"first": {
|
||||
"content": "$1",
|
||||
"example": "Sam"
|
||||
},
|
||||
"second": {
|
||||
"content": "$2",
|
||||
"example": "Cayce"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calling__lobby-summary--triple": {
|
||||
"message": "$first$, $second$, and $third$ are in this call",
|
||||
"description": "Shown in the calling lobby to describe who is in the call",
|
||||
"placeholders": {
|
||||
"first": {
|
||||
"content": "$1",
|
||||
"example": "Sam"
|
||||
},
|
||||
"second": {
|
||||
"content": "$2",
|
||||
"example": "Cayce"
|
||||
},
|
||||
"third": {
|
||||
"content": "$3",
|
||||
"example": "April"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calling__lobby-summary--many": {
|
||||
"message": "$first$, $second$, and $others$ others are in this call",
|
||||
"description": "Shown in the calling lobby to describe who is in the call",
|
||||
"placeholders": {
|
||||
"first": {
|
||||
"content": "$1",
|
||||
"example": "Sam"
|
||||
},
|
||||
"second": {
|
||||
"content": "$2",
|
||||
"example": "Cayce"
|
||||
},
|
||||
"others": {
|
||||
"content": "$3",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calling__in-this-call--zero": {
|
||||
"message": "No one else is here",
|
||||
"description": "Shown in the participants list to describe how many people are in the call"
|
||||
},
|
||||
"calling__in-this-call--one": {
|
||||
"message": "In this call · 1 person",
|
||||
"description": "Shown in the participants list to describe how many people are in the call"
|
||||
|
@ -2984,15 +3052,25 @@
|
|||
"description": "Title for device selection settings"
|
||||
},
|
||||
"calling__participants": {
|
||||
"message": "Participants",
|
||||
"description": "Title for participants list toggle"
|
||||
"message": "$people$ in call",
|
||||
"description": "Title for participants list toggle",
|
||||
"placeholders": {
|
||||
"people": {
|
||||
"content": "$1",
|
||||
"example": "16"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calling__pip": {
|
||||
"message": "Picture-in-picture",
|
||||
"calling__pip--on": {
|
||||
"message": "Minimize call",
|
||||
"description": "Title for picture-in-picture toggle"
|
||||
},
|
||||
"calling__pip--off": {
|
||||
"message": "Fullscreen call",
|
||||
"description": "Title for picture-in-picture toggle"
|
||||
},
|
||||
"calling__hangup": {
|
||||
"message": "Hang Up",
|
||||
"message": "Leave call",
|
||||
"description": "Title for hang up button"
|
||||
},
|
||||
"callingDeviceSelection__label--video": {
|
||||
|
|
|
@ -5872,14 +5872,16 @@ button.module-image__border-overlay:focus {
|
|||
padding-top: 24px;
|
||||
text-align: center;
|
||||
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
&--header-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.009em;
|
||||
line-height: 21px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6239,6 +6241,8 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
&__header {
|
||||
background: linear-gradient($color-black-alpha-40, transparent);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__header-message {
|
||||
|
@ -6373,6 +6377,9 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
.module-calling-lobby {
|
||||
&__actions {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
|
@ -6411,6 +6418,12 @@ button.module-image__border-overlay:focus {
|
|||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
color: $color-white;
|
||||
margin-bottom: 36px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-calling-pip {
|
||||
|
@ -9728,6 +9741,7 @@ button.module-image__border-overlay:focus {
|
|||
border-radius: 4px;
|
||||
border: none;
|
||||
color: $color-white;
|
||||
line-height: 24px;
|
||||
outline: none;
|
||||
padding: 7px 14px;
|
||||
|
||||
|
@ -9744,6 +9758,7 @@ button.module-image__border-overlay:focus {
|
|||
border-radius: 4px;
|
||||
border: none;
|
||||
color: $color-white;
|
||||
line-height: 24px;
|
||||
outline: none;
|
||||
padding: 7px 14px;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as React from 'react';
|
|||
import { noop } from 'lodash';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, select, text } from '@storybook/addon-knobs';
|
||||
|
||||
import { CallManager, PropsType } from './CallManager';
|
||||
import {
|
||||
|
@ -15,26 +16,47 @@ import {
|
|||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { Colors, ColorType } from '../types/Colors';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const conversation = {
|
||||
const getConversation = () => ({
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
color: select('Callee color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Callee Title', 'Rick Sanchez'),
|
||||
name: text('Callee Name', 'Rick Sanchez'),
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct' as ConversationTypeType,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
const getCallState = () => ({
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: boolean('hasLocalAudio', true),
|
||||
hasLocalVideo: boolean('hasLocalVideo', false),
|
||||
pip: boolean('pip', false),
|
||||
settingsDialogOpen: boolean('settingsDialogOpen', false),
|
||||
showParticipantsList: boolean('showParticipantsList', false),
|
||||
});
|
||||
|
||||
const getIncomingCallState = (extraProps = {}) => ({
|
||||
...extraProps,
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: boolean('isVideoCall', true),
|
||||
hasRemoteVideo: true,
|
||||
});
|
||||
|
||||
const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||
...storyProps,
|
||||
availableCameras: [],
|
||||
acceptCall: action('accept-call'),
|
||||
cancelCall: action('cancel-call'),
|
||||
|
@ -54,8 +76,8 @@ const defaultProps = {
|
|||
hangUp: action('hang-up'),
|
||||
i18n,
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Morty Smith',
|
||||
color: select('Caller color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Caller Title', 'Morty Smith'),
|
||||
},
|
||||
renderDeviceSelection: () => <div />,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
|
@ -66,16 +88,15 @@ const defaultProps = {
|
|||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
});
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Call Manager (no call)',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ongoing direct call)',
|
||||
props: {
|
||||
const story = storiesOf('Components/CallManager', module);
|
||||
|
||||
story.add('No Call', () => <CallManager {...createProps()} />);
|
||||
|
||||
story.add('Ongoing Direct Call', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
|
@ -85,22 +106,17 @@ const permutations = [
|
|||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ongoing group call)',
|
||||
props: {
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Ongoing Group Call', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
|
@ -109,70 +125,36 @@ const permutations = [
|
|||
joinState: GroupCallJoinState.Joined,
|
||||
remoteParticipants: [],
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ringing)',
|
||||
props: {
|
||||
incomingCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
conversation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (call request needed)',
|
||||
props: {
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ended,
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('Components/CallManager', module).add('Iterations', () => {
|
||||
return permutations.map(
|
||||
({ props, title }: { props: Partial<PropsType>; title: string }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<CallManager {...defaultProps} {...props} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
});
|
||||
story.add('Ringing', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
incomingCall: {
|
||||
call: getIncomingCallState(),
|
||||
conversation: getConversation(),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Call Request Needed', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: getIncomingCallState({
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
}),
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
|
||||
import { CallingLobby } from './CallingLobby';
|
||||
import { CallScreen } from './CallScreen';
|
||||
import { CallingLobby } from './CallingLobby';
|
||||
import { CallingParticipantsList } from './CallingParticipantsList';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import {
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
CallEndedReason,
|
||||
CanvasVideoRenderer,
|
||||
VideoFrameSource,
|
||||
GroupCallJoinState,
|
||||
GroupCallRemoteParticipantType,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
|
@ -35,9 +37,10 @@ import { ColorType } from '../types/Colors';
|
|||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
interface ActiveCallType {
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
activeCallState: ActiveCallStateType;
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
groupCallParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
}
|
||||
|
||||
export interface PropsType {
|
||||
|
@ -101,13 +104,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
togglePip,
|
||||
toggleSettings,
|
||||
}) => {
|
||||
const { call, activeCallState, conversation } = activeCall;
|
||||
const {
|
||||
joinedAt,
|
||||
call,
|
||||
activeCallState,
|
||||
conversation,
|
||||
groupCallParticipants,
|
||||
} = activeCall;
|
||||
const {
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
settingsDialogOpen,
|
||||
joinedAt,
|
||||
pip,
|
||||
settingsDialogOpen,
|
||||
showParticipantsList,
|
||||
} = activeCallState;
|
||||
|
||||
const cancelActiveCall = useCallback(() => {
|
||||
|
@ -160,6 +169,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
}
|
||||
|
||||
if (showCallLobby) {
|
||||
const participantNames = groupCallParticipants.map(participant =>
|
||||
participant.isSelf
|
||||
? i18n('you')
|
||||
: participant.firstName || participant.title
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<CallingLobby
|
||||
|
@ -168,12 +182,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
// TODO: Set this to `true` for group calls. We can get away with this for
|
||||
// now because it only affects rendering. See DESKTOP-888 and DESKTOP-889.
|
||||
isGroupCall={false}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
me={me}
|
||||
onCallCanceled={cancelActiveCall}
|
||||
onJoinCall={joinActiveCall}
|
||||
participantNames={participantNames}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
|
@ -181,20 +194,26 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Group calls should also support the PiP. See DESKTOP-886.
|
||||
if (pip && call.callMode === CallMode.Direct) {
|
||||
const hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
|
||||
if (pip) {
|
||||
return (
|
||||
<CallingPip
|
||||
call={call}
|
||||
conversation={conversation}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||
hangUp={hangUp}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
|
@ -220,10 +239,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
setRendererCanvas={setRendererCanvas}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
stickyControls={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,40 +8,68 @@ import { boolean, select } from '@storybook/addon-knobs';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallMode, CallState } from '../types/Calling';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { Colors } from '../types/Colors';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
GroupCallParticipantInfoType,
|
||||
} from '../state/ducks/calling';
|
||||
import { CallScreen, PropsType } from './CallScreen';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (
|
||||
function getGroupCallState(
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>
|
||||
): GroupCallStateType {
|
||||
return {
|
||||
callMode: CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: 2,
|
||||
joinState: 2,
|
||||
remoteParticipants,
|
||||
};
|
||||
}
|
||||
|
||||
function getDirectCallState(
|
||||
overrideProps: {
|
||||
callState?: CallState;
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
} = {}
|
||||
): PropsType => ({
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
): DirectCallStateType {
|
||||
return {
|
||||
callMode: CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: select(
|
||||
'callState',
|
||||
CallState,
|
||||
overrideProps.callState || CallState.Accepted
|
||||
),
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: boolean(
|
||||
'hasRemoteVideo',
|
||||
overrideProps.hasRemoteVideo || false
|
||||
Boolean(overrideProps.hasRemoteVideo)
|
||||
),
|
||||
},
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (
|
||||
overrideProps: {
|
||||
callState?: CallState;
|
||||
callTypeState?: DirectCallStateType | GroupCallStateType;
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
remoteParticipants?: Array<GroupCallParticipantInfoType>;
|
||||
} = {}
|
||||
): PropsType => ({
|
||||
call: overrideProps.callTypeState || getDirectCallState(overrideProps),
|
||||
conversation: {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
color: Colors[0],
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
|
@ -67,7 +95,7 @@ const createProps = (
|
|||
i18n,
|
||||
joinedAt: Date.now(),
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
color: Colors[1],
|
||||
name: 'Morty Smith',
|
||||
profileName: 'Morty Smith',
|
||||
title: 'Morty Smith',
|
||||
|
@ -76,6 +104,8 @@ const createProps = (
|
|||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
stickyControls: boolean('stickyControls', false),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
|
@ -87,19 +117,43 @@ story.add('Default', () => {
|
|||
});
|
||||
|
||||
story.add('Pre-Ring', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Prering })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Prering,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Ringing', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ringing })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Ringing,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Reconnecting', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Reconnecting })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Reconnecting,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Ended', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ended })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Ended,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('hasLocalAudio', () => {
|
||||
|
@ -113,3 +167,53 @@ story.add('hasLocalVideo', () => {
|
|||
story.add('hasRemoteVideo', () => {
|
||||
return <CallScreen {...createProps({ hasRemoteVideo: true })} />;
|
||||
});
|
||||
|
||||
story.add('Group call - 1', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState([
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
]),
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group call - Many', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState([
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
conversationId: '456',
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: true,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
conversationId: '789',
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
]),
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import {
|
||||
|
@ -52,6 +53,8 @@ export type PropsType = {
|
|||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
stickyControls: boolean;
|
||||
toggleParticipants: () => void;
|
||||
togglePip: () => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
@ -71,6 +74,8 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
setLocalVideo,
|
||||
setLocalPreview,
|
||||
setRendererCanvas,
|
||||
stickyControls,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}) => {
|
||||
|
@ -110,14 +115,14 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
}, [joinedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showControls) {
|
||||
if (!showControls || stickyControls) {
|
||||
return noop;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 5000);
|
||||
return clearInterval.bind(null, timer);
|
||||
}, [showControls]);
|
||||
}, [showControls, stickyControls]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -146,13 +151,13 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
|
||||
let hasRemoteVideo: boolean;
|
||||
let isConnected: boolean;
|
||||
let remoteParticipants: JSX.Element;
|
||||
let remoteParticipantsElement: JSX.Element;
|
||||
|
||||
switch (call.callMode) {
|
||||
case CallMode.Direct:
|
||||
hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
isConnected = call.callState === CallState.Accepted;
|
||||
remoteParticipants = (
|
||||
remoteParticipantsElement = (
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
|
@ -166,7 +171,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||
);
|
||||
isConnected = call.connectionState === GroupCallConnectionState.Connected;
|
||||
remoteParticipants = (
|
||||
remoteParticipantsElement = (
|
||||
<GroupCallRemoteParticipants
|
||||
remoteParticipants={call.remoteParticipants}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
|
@ -194,6 +199,9 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
!showControls && !isAudioOnly && isConnected,
|
||||
});
|
||||
|
||||
const remoteParticipants =
|
||||
call.callMode === CallMode.Group ? call.remoteParticipants.length : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -208,40 +216,33 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
role="group"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling__header',
|
||||
'module-ongoing-call__header',
|
||||
controlsFadeClass
|
||||
)}
|
||||
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
||||
>
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversation.title}
|
||||
</div>
|
||||
{call.callMode === CallMode.Direct &&
|
||||
renderHeaderMessage(
|
||||
i18n,
|
||||
call.callState || CallState.Prering,
|
||||
acceptedDuration
|
||||
)}
|
||||
<div className="module-calling-tools">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-tools__button module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
{/* TODO: Group calls should also support the PiP. See DESKTOP-886. */}
|
||||
{call.callMode === CallMode.Direct && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip')}
|
||||
className="module-calling-tools__button module-calling-button__pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CallingHeader
|
||||
canPip
|
||||
conversationTitle={
|
||||
<>
|
||||
{call.callMode === CallMode.Group &&
|
||||
!call.remoteParticipants.length
|
||||
? i18n('calling__in-this-call--zero')
|
||||
: conversation.title}
|
||||
{call.callMode === CallMode.Direct &&
|
||||
renderHeaderMessage(
|
||||
i18n,
|
||||
call.callState || CallState.Prering,
|
||||
acceptedDuration
|
||||
)}
|
||||
</>
|
||||
}
|
||||
i18n={i18n}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
remoteParticipants={remoteParticipants}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
</div>
|
||||
{remoteParticipants}
|
||||
{remoteParticipantsElement}
|
||||
<div className="module-ongoing-call__footer">
|
||||
{/* This layout-only element is not ideal.
|
||||
See the comment in _modules.css for more. */}
|
||||
|
|
54
ts/components/CallingHeader.stories.tsx
Normal file
54
ts/components/CallingHeader.stories.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, number } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallingHeader, PropsType } from './CallingHeader';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
canPip: boolean('canPip', Boolean(overrideProps.canPip)),
|
||||
conversationTitle: overrideProps.conversationTitle || 'With Someone',
|
||||
i18n,
|
||||
isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)),
|
||||
remoteParticipants: number(
|
||||
'remoteParticipants',
|
||||
overrideProps.remoteParticipants || 0
|
||||
),
|
||||
toggleParticipants: () => action('toggle-participants'),
|
||||
togglePip: () => action('toggle-pip'),
|
||||
toggleSettings: () => action('toggle-settings'),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingHeader', module);
|
||||
|
||||
story.add('Default', () => <CallingHeader {...createProps()} />);
|
||||
|
||||
story.add('Has Pip', () => (
|
||||
<CallingHeader {...createProps({ canPip: true })} />
|
||||
));
|
||||
|
||||
story.add('With Participants', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
canPip: true,
|
||||
isGroupCall: true,
|
||||
remoteParticipants: 10,
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Long Title', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
conversationTitle:
|
||||
'What do I got to, what do I got to do to wake you up? To shake you up, to break the structure up?',
|
||||
})}
|
||||
/>
|
||||
));
|
89
ts/components/CallingHeader.tsx
Normal file
89
ts/components/CallingHeader.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
canPip?: boolean;
|
||||
conversationTitle: JSX.Element | string;
|
||||
i18n: LocalizerType;
|
||||
isGroupCall?: boolean;
|
||||
remoteParticipants?: number;
|
||||
toggleParticipants?: () => void;
|
||||
togglePip?: () => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
||||
export const CallingHeader = ({
|
||||
canPip = false,
|
||||
conversationTitle,
|
||||
i18n,
|
||||
isGroupCall = false,
|
||||
remoteParticipants,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element => (
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversationTitle}
|
||||
</div>
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__participants', [
|
||||
String(remoteParticipants),
|
||||
])}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__participants', [
|
||||
String(remoteParticipants),
|
||||
])}
|
||||
className="module-calling-button__participants"
|
||||
onClick={toggleParticipants}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('callingDeviceSelection__settings')}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{canPip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__pip--on')}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip--on')}
|
||||
className="module-calling-button__pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -35,6 +35,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
me: overrideProps.me || { color: 'ultramarine' as ColorType },
|
||||
onCallCanceled: action('on-call-canceled'),
|
||||
onJoinCall: action('on-join-call'),
|
||||
participantNames: overrideProps.participantNames || [],
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
|
@ -81,7 +82,36 @@ story.add('Local Video', () => {
|
|||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call', () => {
|
||||
const props = createProps({ isGroupCall: true });
|
||||
story.add('Group Call - 0', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: [] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 1', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: ['Sam'] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 2', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 3', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 4', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
|
|
@ -13,8 +13,10 @@ import {
|
|||
TooltipDirection,
|
||||
} from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -31,6 +33,7 @@ export type PropsType = {
|
|||
};
|
||||
onCallCanceled: () => void;
|
||||
onJoinCall: () => void;
|
||||
participantNames: Array<string>;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
|
@ -48,6 +51,7 @@ export const CallingLobby = ({
|
|||
me,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
participantNames,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
|
@ -97,6 +101,8 @@ export const CallingLobby = ({
|
|||
};
|
||||
}, [toggleVideo, toggleAudio]);
|
||||
|
||||
const [isCallConnecting, setIsCallConnecting] = React.useState(false);
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
|
@ -109,27 +115,15 @@ export const CallingLobby = ({
|
|||
|
||||
return (
|
||||
<div className="module-calling__container">
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__participants')}
|
||||
className="module-calling-tools__button module-calling-button__participants"
|
||||
onClick={toggleParticipants}
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-tools__button module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CallingHeader
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
remoteParticipants={participantNames.length}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
|
||||
<div className="module-calling-lobby__video">
|
||||
{hasLocalVideo && availableCameras.length > 0 ? (
|
||||
<video ref={localVideoRef} autoPlay />
|
||||
|
@ -160,6 +154,32 @@ export const CallingLobby = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isGroupCall ? (
|
||||
<div className="module-calling-lobby__info">
|
||||
{participantNames.length === 0 &&
|
||||
i18n('calling__lobby-summary--zero')}
|
||||
{participantNames.length === 1 &&
|
||||
i18n('calling__lobby-summary--single', participantNames)}
|
||||
{participantNames.length === 2 &&
|
||||
i18n('calling__lobby-summary--double', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
})}
|
||||
{participantNames.length === 3 &&
|
||||
i18n('calling__lobby-summary--triple', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
third: participantNames[2],
|
||||
})}
|
||||
{participantNames.length > 3 &&
|
||||
i18n('calling__lobby-summary--many', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
others: String(participantNames.length - 2),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="module-calling-lobby__actions">
|
||||
<button
|
||||
className="module-button__gray module-calling-lobby__button"
|
||||
|
@ -169,14 +189,29 @@ export const CallingLobby = ({
|
|||
>
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
onClick={onJoinCall}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{isGroupCall ? i18n('calling__join') : i18n('calling__start')}
|
||||
</button>
|
||||
{isCallConnecting && (
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
disabled
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<Spinner svgSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!isCallConnecting && (
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{isGroupCall ? i18n('calling__join') : i18n('calling__start')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,61 +6,76 @@ import { storiesOf } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
|
||||
import { Colors } from '../types/Colors';
|
||||
import { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const participant = {
|
||||
title: 'Bardock',
|
||||
};
|
||||
function createParticipant(
|
||||
participantProps: Partial<GroupCallRemoteParticipantType>
|
||||
): GroupCallRemoteParticipantType {
|
||||
const randomColor = Math.floor(Math.random() * Colors.length - 1);
|
||||
return {
|
||||
avatarPath: participantProps.avatarPath,
|
||||
color: Colors[randomColor],
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isSelf: Boolean(participantProps.isSelf),
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
onClose: action('on-close'),
|
||||
participants: overrideProps.participants || [participant],
|
||||
participants: overrideProps.participants || [],
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingParticipantsList', module);
|
||||
|
||||
story.add('Default', () => {
|
||||
story.add('No one', () => {
|
||||
const props = createProps();
|
||||
return <CallingParticipantsList {...props} />;
|
||||
});
|
||||
|
||||
story.add('Solo Call', () => {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
createParticipant({
|
||||
title: 'Bardock',
|
||||
}),
|
||||
],
|
||||
});
|
||||
return <CallingParticipantsList {...props} />;
|
||||
});
|
||||
|
||||
story.add('Many Participants', () => {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
{
|
||||
color: 'blue',
|
||||
profileName: 'Son Goku',
|
||||
createParticipant({
|
||||
isSelf: true,
|
||||
title: 'Son Goku',
|
||||
audioMuted: true,
|
||||
videoMuted: true,
|
||||
},
|
||||
{
|
||||
color: 'deep_orange',
|
||||
profileName: 'Rage Trunks',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
title: 'Rage Trunks',
|
||||
},
|
||||
{
|
||||
color: 'indigo',
|
||||
profileName: 'Prince Vegeta',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
title: 'Prince Vegeta',
|
||||
videoMuted: true,
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
profileName: 'Goku Black',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
title: 'Goku Black',
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
profileName: 'Supreme Kai Zamasu',
|
||||
}),
|
||||
createParticipant({
|
||||
title: 'Supreme Kai Zamasu',
|
||||
audioMuted: true,
|
||||
videoMuted: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
return <CallingParticipantsList {...props} />;
|
||||
|
|
|
@ -6,23 +6,14 @@
|
|||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Avatar } from './Avatar';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
type ParticipantType = {
|
||||
audioMuted?: boolean;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
videoMuted?: boolean;
|
||||
};
|
||||
import { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
|
||||
export type PropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClose: () => void;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
readonly participants: Array<GroupCallRemoteParticipantType>;
|
||||
};
|
||||
|
||||
export const CallingParticipantsList = React.memo(
|
||||
|
@ -52,11 +43,12 @@ export const CallingParticipantsList = React.memo(
|
|||
<div className="module-calling-participants-list">
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{participants.length > 1
|
||||
? i18n('calling__in-this-call--many', [
|
||||
String(participants.length),
|
||||
])
|
||||
: i18n('calling__in-this-call--one')}
|
||||
{!participants.length && i18n('calling__in-this-call--zero')}
|
||||
{participants.length === 1 && i18n('calling__in-this-call--one')}
|
||||
{participants.length > 1 &&
|
||||
i18n('calling__in-this-call--many', [
|
||||
String(participants.length),
|
||||
])}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -67,37 +59,45 @@ export const CallingParticipantsList = React.memo(
|
|||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{participants.map((participant: ParticipantType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
avatarPath={participant.avatarPath}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
size={32}
|
||||
/>
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{participant.audioMuted ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
{participant.videoMuted ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{participants.map(
|
||||
(participant: GroupCallRemoteParticipantType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
avatarPath={participant.avatarPath}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
size={32}
|
||||
/>
|
||||
{participant.isSelf ? (
|
||||
<span className="module-calling-participants-list__name">
|
||||
{i18n('you')}
|
||||
</span>
|
||||
) : (
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!participant.hasRemoteAudio ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
{!participant.hasRemoteVideo ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
|
@ -2,12 +2,20 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { CallingPip, PropsType } from './CallingPip';
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -21,16 +29,29 @@ const conversation = {
|
|||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct' as ConversationTypeType,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
const defaultCall = {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
call: overrideProps.call || defaultCall,
|
||||
conversation: overrideProps.conversation || conversation,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
createCanvasVideoRenderer: noop as any,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
getGroupCallVideoFrameSource: noop as any,
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
hasRemoteVideo: boolean(
|
||||
'hasRemoteVideo',
|
||||
overrideProps.hasRemoteVideo || false
|
||||
),
|
||||
i18n,
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
|
@ -63,3 +84,16 @@ story.add('Contact (no color)', () => {
|
|||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call', () => {
|
||||
const props = createProps({
|
||||
call: {
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
remoteParticipants: [],
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
|
|
@ -2,69 +2,26 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { CanvasVideoRenderer, VideoFrameSource } from '../types/Calling';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
HangUpType,
|
||||
SetLocalPreviewType,
|
||||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
function renderAvatar(
|
||||
{
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
}: {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
},
|
||||
i18n: LocalizerType
|
||||
): JSX.Element {
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={52}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hangUp: (_: HangUpType) => void;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
|
@ -77,10 +34,12 @@ const PIP_DEFAULT_Y = 56;
|
|||
const PIP_PADDING = 8;
|
||||
|
||||
export const CallingPip = ({
|
||||
call,
|
||||
conversation,
|
||||
createCanvasVideoRenderer,
|
||||
getGroupCallVideoFrameSource,
|
||||
hangUp,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
setLocalPreview,
|
||||
setRendererCanvas,
|
||||
|
@ -88,7 +47,6 @@ export const CallingPip = ({
|
|||
}: PropsType): JSX.Element | null => {
|
||||
const videoContainerRef = React.useRef(null);
|
||||
const localVideoRef = React.useRef(null);
|
||||
const remoteVideoRef = React.useRef(null);
|
||||
|
||||
const [dragState, setDragState] = React.useState({
|
||||
offsetX: 0,
|
||||
|
@ -103,8 +61,7 @@ export const CallingPip = ({
|
|||
|
||||
React.useEffect(() => {
|
||||
setLocalPreview({ element: localVideoRef });
|
||||
setRendererCanvas({ element: remoteVideoRef });
|
||||
}, [setLocalPreview, setRendererCanvas]);
|
||||
}, [setLocalPreview]);
|
||||
|
||||
const handleMouseMove = React.useCallback(
|
||||
(ev: MouseEvent) => {
|
||||
|
@ -211,14 +168,14 @@ export const CallingPip = ({
|
|||
transition: dragState.isDragging ? 'none' : 'transform ease-out 300ms',
|
||||
}}
|
||||
>
|
||||
{hasRemoteVideo ? (
|
||||
<canvas
|
||||
className="module-calling-pip__video--remote"
|
||||
ref={remoteVideoRef}
|
||||
/>
|
||||
) : (
|
||||
renderAvatar(conversation, i18n)
|
||||
)}
|
||||
<CallingPipRemoteVideo
|
||||
call={call}
|
||||
conversation={conversation}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
{hasLocalVideo ? (
|
||||
<video
|
||||
className="module-calling-pip__video--local"
|
||||
|
@ -237,10 +194,18 @@ export const CallingPip = ({
|
|||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip')}
|
||||
aria-label={i18n('calling__pip--off')}
|
||||
className="module-calling-pip__button--pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
>
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__pip--off')}
|
||||
hoverDelay={0}
|
||||
>
|
||||
<div />
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
105
ts/components/CallingPipRemoteVideo.tsx
Normal file
105
ts/components/CallingPipRemoteVideo.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
CallMode,
|
||||
CanvasVideoRenderer,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
|
||||
export interface PropsType {
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
i18n: LocalizerType;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
}
|
||||
|
||||
export const CallingPipRemoteVideo = ({
|
||||
call,
|
||||
conversation,
|
||||
createCanvasVideoRenderer,
|
||||
getGroupCallVideoFrameSource,
|
||||
i18n,
|
||||
setRendererCanvas,
|
||||
}: PropsType): JSX.Element => {
|
||||
if (call.callMode === CallMode.Direct) {
|
||||
if (!call.hasRemoteVideo) {
|
||||
const {
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
} = conversation;
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={52}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={call.hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (call.callMode === CallMode.Group) {
|
||||
const speaker = call.remoteParticipants[0];
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<GroupCallRemoteParticipant
|
||||
key={speaker.demuxId}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
demuxId={speaker.demuxId}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
hasRemoteVideo={speaker.hasRemoteVideo}
|
||||
height="100%"
|
||||
left={0}
|
||||
top={0}
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('CallingRemoteVideo: Unknown Call Mode');
|
||||
};
|
|
@ -13,10 +13,10 @@ interface PropsType {
|
|||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
height: number;
|
||||
height: number | string;
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
width: number | string;
|
||||
}
|
||||
|
||||
export const GroupCallRemoteParticipant: React.FC<PropsType> = ({
|
||||
|
|
|
@ -5,7 +5,7 @@ import React, { useState, useMemo } from 'react';
|
|||
import Measure from 'react-measure';
|
||||
import { takeWhile, chunk, maxBy, flatten } from 'lodash';
|
||||
import { CanvasVideoRenderer, VideoFrameSource } from '../types/Calling';
|
||||
import { GroupCallRemoteParticipantType } from '../state/ducks/calling';
|
||||
import { GroupCallParticipantInfoType } from '../state/ducks/calling';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
|
||||
const MIN_RENDERED_HEIGHT = 10;
|
||||
|
@ -17,14 +17,14 @@ interface Dimensions {
|
|||
}
|
||||
|
||||
interface GridArrangement {
|
||||
rows: Array<Array<GroupCallRemoteParticipantType>>;
|
||||
rows: Array<Array<GroupCallParticipantInfoType>>;
|
||||
scalar: number;
|
||||
}
|
||||
|
||||
interface PropsType {
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
|
||||
remoteParticipants: ReadonlyArray<GroupCallParticipantInfoType>;
|
||||
}
|
||||
|
||||
// This component lays out group call remote participants. It uses a custom layout
|
||||
|
@ -84,7 +84,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
|||
//
|
||||
// This is primarily memoized for clarity, not performance. We only need the result,
|
||||
// not any of the "intermediate" values.
|
||||
const visibleParticipants: Array<GroupCallRemoteParticipantType> = useMemo(() => {
|
||||
const visibleParticipants: Array<GroupCallParticipantInfoType> = useMemo(() => {
|
||||
// Imagine that we laid out all of the rows end-to-end. That's the maximum total
|
||||
// width. So if there were 5 rows and the container was 100px wide, then we can't
|
||||
// possibly fit more than 500px of participants.
|
||||
|
@ -233,7 +233,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
|||
};
|
||||
|
||||
function totalRemoteParticipantWidthAtMinHeight(
|
||||
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>
|
||||
remoteParticipants: ReadonlyArray<GroupCallParticipantInfoType>
|
||||
): number {
|
||||
return remoteParticipants.reduce(
|
||||
(result, { videoAspectRatio }) =>
|
||||
|
|
|
@ -3959,7 +3959,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return this.get('name') || window.i18n('unknownGroup');
|
||||
}
|
||||
|
||||
getProfileName(): string | null {
|
||||
getProfileName(): string | undefined {
|
||||
if (this.isPrivate()) {
|
||||
return Util.combineNames(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
@ -3967,7 +3967,8 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
this.get('profileFamilyName')
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getNumber(): string {
|
||||
|
|
|
@ -489,6 +489,8 @@ export class CallingClass {
|
|||
? GroupCallJoinState.NotJoined
|
||||
: this.convertRingRtcJoinState(localDeviceState.joinState);
|
||||
|
||||
const ourId = window.ConversationController.getOurConversationId();
|
||||
|
||||
return {
|
||||
connectionState: this.convertRingRtcConnectionState(
|
||||
localDeviceState.connectionState
|
||||
|
@ -496,16 +498,28 @@ export class CallingClass {
|
|||
joinState,
|
||||
hasLocalAudio: !localDeviceState.audioMuted,
|
||||
hasLocalVideo: !localDeviceState.videoMuted,
|
||||
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => ({
|
||||
demuxId: remoteDeviceState.demuxId,
|
||||
userId: arrayBufferToUuid(remoteDeviceState.userId) || '',
|
||||
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
||||
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
||||
// If RingRTC doesn't send us an aspect ratio, we make a guess.
|
||||
videoAspectRatio:
|
||||
remoteDeviceState.videoAspectRatio ||
|
||||
(remoteDeviceState.videoMuted ? 1 : 4 / 3),
|
||||
})),
|
||||
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => {
|
||||
const uuid = arrayBufferToUuid(remoteDeviceState.userId);
|
||||
|
||||
const id = window.ConversationController.ensureContactIds({ uuid });
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
'Calling.formatGroupCallForRedux: no conversation found'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
conversationId: id,
|
||||
demuxId: remoteDeviceState.demuxId,
|
||||
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
||||
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
||||
isSelf: id === ourId,
|
||||
// If RingRTC doesn't send us an aspect ratio, we make a guess.
|
||||
videoAspectRatio:
|
||||
remoteDeviceState.videoAspectRatio ||
|
||||
(remoteDeviceState.videoMuted ? 1 : 4 / 3),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import { notify } from '../../services/notify';
|
|||
import { calling } from '../../services/calling';
|
||||
import { StateType as RootStateType } from '../reducer';
|
||||
import {
|
||||
CallingDeviceType,
|
||||
CallMode,
|
||||
CallState,
|
||||
CallingDeviceType,
|
||||
ChangeIODevicePayloadType,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
|
@ -27,6 +27,15 @@ import {
|
|||
|
||||
// State
|
||||
|
||||
export interface GroupCallParticipantInfoType {
|
||||
conversationId: string;
|
||||
demuxId: number;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
isSelf: boolean;
|
||||
videoAspectRatio: number;
|
||||
}
|
||||
|
||||
export interface DirectCallStateType {
|
||||
callMode: CallMode.Direct;
|
||||
conversationId: string;
|
||||
|
@ -37,20 +46,12 @@ export interface DirectCallStateType {
|
|||
hasRemoteVideo?: boolean;
|
||||
}
|
||||
|
||||
export interface GroupCallRemoteParticipantType {
|
||||
demuxId: number;
|
||||
userId: string;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
videoAspectRatio: number;
|
||||
}
|
||||
|
||||
export interface GroupCallStateType {
|
||||
callMode: CallMode.Group;
|
||||
conversationId: string;
|
||||
connectionState: GroupCallConnectionState;
|
||||
joinState: GroupCallJoinState;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
}
|
||||
|
||||
export interface ActiveCallStateType {
|
||||
|
@ -58,7 +59,7 @@ export interface ActiveCallStateType {
|
|||
joinedAt?: number;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
participantsList: boolean;
|
||||
showParticipantsList: boolean;
|
||||
pip: boolean;
|
||||
settingsDialogOpen: boolean;
|
||||
}
|
||||
|
@ -99,7 +100,7 @@ export type GroupCallStateChangeType = {
|
|||
joinState: GroupCallJoinState;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
};
|
||||
|
||||
export type HangUpType = {
|
||||
|
@ -148,7 +149,7 @@ export type ShowCallLobbyType =
|
|||
joinState: GroupCallJoinState;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
};
|
||||
|
||||
export type SetLocalPreviewType = {
|
||||
|
@ -706,7 +707,7 @@ export function reducer(
|
|||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: action.payload.hasLocalAudio,
|
||||
hasLocalVideo: action.payload.hasLocalVideo,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -730,7 +731,7 @@ export function reducer(
|
|||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: action.payload.hasLocalAudio,
|
||||
hasLocalVideo: action.payload.hasLocalVideo,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -749,7 +750,7 @@ export function reducer(
|
|||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: action.payload.asVideoCall,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -813,7 +814,7 @@ export function reducer(
|
|||
conversationId: action.payload.conversationId,
|
||||
hasLocalAudio: action.payload.hasLocalAudio,
|
||||
hasLocalVideo: action.payload.hasLocalVideo,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -1028,7 +1029,7 @@ export function reducer(
|
|||
...state,
|
||||
activeCallState: {
|
||||
...activeCallState,
|
||||
participantsList: !activeCallState.participantsList,
|
||||
showParticipantsList: !activeCallState.showParticipantsList,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { CallManager } from '../../components/CallManager';
|
||||
import { calling as callingService } from '../../services/calling';
|
||||
import { getMe, getConversationSelector } from '../selectors/conversations';
|
||||
import { getActiveCall } from '../ducks/calling';
|
||||
import { getActiveCall, GroupCallParticipantInfoType } from '../ducks/calling';
|
||||
import { getIncomingCall } from '../selectors/calling';
|
||||
import { CallMode, GroupCallRemoteParticipantType } from '../../types/Calling';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
@ -42,18 +43,47 @@ const mapStateToActiveCallProp = (state: StateType) => {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const conversation = getConversationSelector(state)(
|
||||
activeCallState.conversationId
|
||||
);
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const conversation = conversationSelector(activeCallState.conversationId);
|
||||
if (!conversation) {
|
||||
window.log.error('The active call has no corresponding conversation');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const groupCallParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||
if (call && call.callMode === CallMode.Group) {
|
||||
call.remoteParticipants.forEach(
|
||||
(remoteParticipant: GroupCallParticipantInfoType) => {
|
||||
const remoteConversation = conversationSelector(
|
||||
remoteParticipant.conversationId
|
||||
);
|
||||
|
||||
if (!remoteConversation) {
|
||||
window.log.error(
|
||||
'Remote participant has no corresponding conversation'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
groupCallParticipants.push({
|
||||
avatarPath: remoteConversation.avatarPath,
|
||||
color: remoteConversation.color,
|
||||
firstName: remoteConversation.firstName,
|
||||
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
||||
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
||||
isSelf: remoteParticipant.isSelf,
|
||||
profileName: remoteConversation.profileName,
|
||||
title: remoteConversation.title,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
call,
|
||||
activeCallState,
|
||||
call,
|
||||
conversation,
|
||||
groupCallParticipants,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -71,10 +71,11 @@ describe('calling duck', () => {
|
|||
joinState: GroupCallJoinState.NotJoined,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 123,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
|
@ -88,7 +89,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-group-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
@ -180,7 +181,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
|
@ -296,10 +297,11 @@ describe('calling duck', () => {
|
|||
hasLocalVideo: false,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 123,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
|
@ -315,10 +317,11 @@ describe('calling duck', () => {
|
|||
joinState: GroupCallJoinState.Joining,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 123,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
|
@ -337,10 +340,11 @@ describe('calling duck', () => {
|
|||
hasLocalVideo: false,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 456,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -356,10 +360,11 @@ describe('calling duck', () => {
|
|||
joinState: GroupCallJoinState.Joined,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 456,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -378,10 +383,11 @@ describe('calling duck', () => {
|
|||
hasLocalVideo: false,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 456,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -402,10 +408,11 @@ describe('calling duck', () => {
|
|||
hasLocalVideo: true,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 456,
|
||||
userId: '6d174bc4-2ea1-45b6-9099-c46fc87ce72f',
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -416,7 +423,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-group-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
|
@ -433,10 +440,11 @@ describe('calling duck', () => {
|
|||
hasLocalVideo: true,
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 456,
|
||||
userId: 'aead696f-4373-4e51-b9c2-1bb4d1adccf0',
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -559,7 +567,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
|
@ -638,7 +646,7 @@ describe('calling duck', () => {
|
|||
conversationId: 'fake-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
});
|
||||
|
@ -688,9 +696,9 @@ describe('calling duck', () => {
|
|||
toggleParticipants()
|
||||
);
|
||||
|
||||
assert.isTrue(afterOneToggle.activeCallState?.participantsList);
|
||||
assert.isFalse(afterTwoToggles.activeCallState?.participantsList);
|
||||
assert.isTrue(afterThreeToggles.activeCallState?.participantsList);
|
||||
assert.isTrue(afterOneToggle.activeCallState?.showParticipantsList);
|
||||
assert.isFalse(afterTwoToggles.activeCallState?.showParticipantsList);
|
||||
assert.isTrue(afterThreeToggles.activeCallState?.showParticipantsList);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('state/selectors/calling', () => {
|
|||
conversationId: 'fake-direct-call-conversation-id',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
showParticipantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
|
|
|
@ -6,8 +6,8 @@ import { assert } from 'chai';
|
|||
import { combineNames } from '../../util/combineNames';
|
||||
|
||||
describe('combineNames', () => {
|
||||
it('returns null if no names provided', () => {
|
||||
assert.strictEqual(combineNames('', ''), null);
|
||||
it('returns undefined if no names provided', () => {
|
||||
assert.strictEqual(combineNames('', ''), undefined);
|
||||
});
|
||||
|
||||
it('returns first name only if family name not provided', () => {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ColorType } from './Colors';
|
||||
|
||||
export enum CallMode {
|
||||
None,
|
||||
Direct,
|
||||
Group,
|
||||
None = 'None',
|
||||
Direct = 'Direct',
|
||||
Group = 'Group',
|
||||
}
|
||||
|
||||
// Ideally, we would import many of these directly from RingRTC. But because Storybook
|
||||
|
@ -56,6 +58,17 @@ export enum GroupCallJoinState {
|
|||
Joined = 2,
|
||||
}
|
||||
|
||||
export interface GroupCallRemoteParticipantType {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
firstName?: string;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
isSelf: boolean;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// Should match RingRTC's CanvasVideoRenderer
|
||||
interface Ref<T> {
|
||||
readonly current: T | null;
|
||||
|
|
|
@ -35,9 +35,12 @@ const Hangul_Syllables = /[\uAC00-\uD7AF]/;
|
|||
// From https://github.com/mathiasbynens/unicode-12.1.0/tree/master/Binary_Property/Ideographic
|
||||
const isIdeographic = /[\u3006\u3007\u3021-\u3029\u3038-\u303A\u3400-\u4DB5\u4E00-\u9FEF\uF900-\uFA6D\uFA70-\uFAD9]|[\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD821[\uDC00-\uDFF7]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDD70-\uDEFB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/;
|
||||
|
||||
export function combineNames(given: string, family?: string): null | string {
|
||||
export function combineNames(
|
||||
given: string,
|
||||
family?: string
|
||||
): undefined | string {
|
||||
if (!given) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Users who haven't upgraded to dual-name, or went minimal, will just have a given name
|
||||
|
|
|
@ -14391,7 +14391,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.js",
|
||||
"line": " const localVideoRef = react_1.useRef(null);",
|
||||
"lineNumber": 38,
|
||||
"lineNumber": 39,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T21:35:52.858Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14400,7 +14400,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.js",
|
||||
"line": " const localVideoRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 12,
|
||||
"lineNumber": 14,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14409,7 +14409,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 57,
|
||||
"lineNumber": 61,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14418,7 +14418,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const videoContainerRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 22,
|
||||
"lineNumber": 16,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Element is measured. Its HTML is not used."
|
||||
|
@ -14427,25 +14427,16 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const localVideoRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 23,
|
||||
"lineNumber": 17,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.js",
|
||||
"line": " const remoteVideoRef = react_1.default.useRef(null);",
|
||||
"lineNumber": 24,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the remote video element for rendering."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const videoContainerRef = React.useRef(null);",
|
||||
"lineNumber": 89,
|
||||
"lineNumber": 48,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Element is measured. Its HTML is not used."
|
||||
|
@ -14454,20 +14445,11 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 90,
|
||||
"lineNumber": 49,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const remoteVideoRef = React.useRef(null);",
|
||||
"lineNumber": 91,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the remote video element for rendering."
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/CaptionEditor.js",
|
||||
|
|
Loading…
Add table
Reference in a new issue