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