diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 3f6360d521..e022811319 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1150,6 +1150,34 @@
"message": "Calling",
"description": "Header for calling options on the settings screen"
},
+ "calling__start": {
+ "message": "Start Call",
+ "description": "Button label in the call lobby for starting a call"
+ },
+ "calling__join": {
+ "message": "Join Call",
+ "description": "Button label in the call lobby for joining a call"
+ },
+ "calling__button--video-off": {
+ "message": "Turn off camera",
+ "description": "Button tooltip label for turning off the camera"
+ },
+ "calling__button--video-on": {
+ "message": "Turn on camera",
+ "description": "Button tooltip label for turning on the camera"
+ },
+ "calling__button--audio-off": {
+ "message": "Turn off microphone",
+ "description": "Button tooltip label for turning off the microphone"
+ },
+ "calling__button--audio-on": {
+ "message": "Turn on microphone",
+ "description": "Button tooltip label for turning on the microphone"
+ },
+ "calling__your-video-is-off": {
+ "message": "Your video is off",
+ "description": "Label in the calling lobby indicating that your camera is off"
+ },
"alwaysRelayCallsDescription": {
"message": "Always relay calls",
"description": "Description of the always relay calls setting"
@@ -2889,6 +2917,10 @@
"message": "Settings",
"description": "Title for device selection settings"
},
+ "calling__participants": {
+ "message": "Participants",
+ "description": "Title for participants list toggle"
+ },
"calling__pip": {
"message": "Picture-in-picture",
"description": "Title for picture-in-picture toggle"
diff --git a/images/icons/v2/group-solid-24.svg b/images/icons/v2/group-solid-24.svg
new file mode 100644
index 0000000000..6e9d3d0593
--- /dev/null
+++ b/images/icons/v2/group-solid-24.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 4510dc6054..22e0b8a686 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -5815,6 +5815,65 @@ button.module-image__border-overlay:focus {
}
// Module: Calling
+.module-calling {
+ &__container {
+ align-items: center;
+ background-color: $color-gray-95;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ position: relative;
+ width: 100%;
+ }
+
+ &__header {
+ color: #ffffff;
+ font-style: normal;
+ padding-bottom: 24px;
+ padding-top: 24px;
+ text-align: center;
+ text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
+ top: 0;
+ width: 100%;
+
+ &--header-name {
+ font-weight: 600;
+ font-size: 15px;
+ line-height: 21px;
+ letter-spacing: -0.009em;
+ }
+ }
+
+ &__buttons {
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ padding-bottom: 32px;
+ padding-top: 32px;
+ position: absolute;
+ text-align: center;
+ width: 100%;
+ }
+
+ &__background {
+ background-repeat: no-repeat;
+ background-size: cover;
+ border-radius: 4px 4px 0 0;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+
+ &--blur {
+ backdrop-filter: blur(7px);
+ backface-visibility: hidden;
+ background-color: $color-black-alpha-40;
+ border-radius: 4px 4px 0 0;
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ }
+ }
+}
.module-incoming-call {
align-items: center;
@@ -5862,114 +5921,140 @@ button.module-image__border-overlay:focus {
margin-right: 16px;
}
-.module-incoming-call__button--accept-video-as-audio {
- background-color: $color-gray-45;
+.module-incoming-call__button {
+ &--accept-video-as-audio {
+ background-color: $color-gray-45;
- @include keyboard-mode {
- &:focus {
- box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
+ }
+ }
+
+ @include mouse-mode {
+ &:hover {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+
+ div {
+ @include color-svg(
+ '../images/icons/v2/video-off-solid-24.svg',
+ $color-white
+ );
+ height: 24px;
+ width: 24px;
}
}
- @include mouse-mode {
- &:hover {
- box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ &--accept-video {
+ background-color: $color-accent-green;
+
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
+ }
+ }
+
+ @include mouse-mode {
+ &:hover {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+
+ div {
+ @include color-svg('../images/icons/v2/video-solid-24.svg', $color-white);
+ height: 24px;
+ width: 24px;
}
}
- div {
- @include color-svg(
- '../images/icons/v2/video-off-solid-24.svg',
- $color-white
- );
- height: 24px;
- width: 24px;
+ &--accept-audio {
+ background-color: $color-accent-green;
+
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
+ }
+ }
+
+ @include mouse-mode {
+ &:hover {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+
+ div {
+ @include color-svg(
+ '../images/icons/v2/phone-right-solid-24.svg',
+ $color-white
+ );
+ height: 24px;
+ width: 24px;
+ }
+ }
+
+ &--decline {
+ background-color: $color-accent-red;
+
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
+ }
+ }
+
+ @include mouse-mode {
+ &:hover {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+
+ div {
+ @include color-svg('../images/icons/v2/phone-down-24.svg', $color-white);
+ height: 24px;
+ width: 24px;
+ }
}
}
-.module-incoming-call__button--accept-video {
- background-color: $color-accent-green;
-
- @include keyboard-mode {
- &:focus {
- box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
- }
- }
-
- @include mouse-mode {
- &:hover {
- box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
- }
- }
-
- div {
- @include color-svg('../images/icons/v2/video-solid-24.svg', $color-white);
- height: 24px;
- width: 24px;
- }
-}
-
-.module-incoming-call__button--accept-audio {
- background-color: $color-accent-green;
-
- @include keyboard-mode {
- &:focus {
- box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
- }
- }
-
- @include mouse-mode {
- &:hover {
- box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
- }
- }
-
- div {
- @include color-svg(
- '../images/icons/v2/phone-right-solid-24.svg',
- $color-white
- );
- height: 24px;
- width: 24px;
- }
-}
-
-.module-incoming-call__button--decline {
- background-color: $color-accent-red;
-
- @include keyboard-mode {
- &:focus {
- box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
- }
- }
-
- @include mouse-mode {
- &:hover {
- box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
- }
- }
-
- div {
- @include color-svg('../images/icons/v2/phone-down-24.svg', $color-white);
- height: 24px;
- width: 24px;
- }
-}
-
-.module-incoming-call__icon,
-.module-ongoing-call__icon {
+.module-incoming-call__button,
+.module-calling-button__icon {
align-items: center;
border-radius: 40px;
border: none;
display: flex;
height: 40px;
justify-content: center;
- margin-left: 24px;
+ margin-left: 12px;
+ margin-right: 12px;
outline: none;
width: 40px;
}
-.module-ongoing-call__icon {
+.module-calling-button {
+ &__participants {
+ @include color-svg('../images/icons/v2/group-solid-24.svg', $color-white);
+ height: 22px;
+ width: 22px;
+ }
+
+ &__settings {
+ @include color-svg(
+ '../images/icons/v2/settings-solid-16.svg',
+ $color-white
+ );
+ height: 22px;
+ width: 22px;
+ }
+
+ &__pip {
+ @include color-svg('../images/icons/v2/collapse-24.svg', $color-white);
+ height: 22px;
+ width: 22px;
+ }
+}
+
+.module-calling-button__icon {
border-radius: 56px;
height: 56px;
width: 56px;
@@ -6036,79 +6121,6 @@ button.module-image__border-overlay:focus {
}
}
-.module-ongoing-call,
-.module-call-need-permission-screen {
- background-color: $color-gray-95;
- height: 100vh;
- width: 100%;
- position: relative;
-}
-
-.module-ongoing-call__remote-video-enabled {
- background-color: $color-gray-95;
- height: 100vh;
- width: 100%;
-}
-
-.module-ongoing-call__remote-video-disabled {
- background-color: $color-gray-95;
- height: 100vh;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.module-ongoing-call__local-video {
- transform: rotateY(180deg);
- background-color: transparent;
- bottom: 160px;
- height: 152px;
- position: absolute;
- right: 32px;
- width: 210px;
-}
-
-.module-ongoing-call__header {
- background: linear-gradient($color-black-alpha-40, transparent);
- padding-bottom: 24px;
- padding-top: 24px;
- position: absolute;
- text-align: center;
- top: 0;
- width: 100%;
-
- font-style: normal;
- color: #ffffff;
- text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
-}
-
-.module-ongoing-call__header-name {
- font-weight: 600;
- font-size: 15px;
- line-height: 21px;
- letter-spacing: -0.009em;
-}
-
-.module-ongoing-call__header-message {
- font-weight: normal;
- font-size: 13px;
- line-height: 18px;
- letter-spacing: -0.0025em;
-}
-
-.module-ongoing-call__actions {
- background: linear-gradient(transparent, $color-black-alpha-40);
- bottom: 0;
- display: flex;
- justify-content: center;
- padding-bottom: 32px;
- padding-top: 32px;
- position: absolute;
- text-align: center;
- width: 100%;
-}
-
@keyframes module-ongoing-call__controls--fade-in {
from {
opacity: 0;
@@ -6127,48 +6139,129 @@ button.module-image__border-overlay:focus {
}
}
-.module-ongoing-call__controls--fadeIn {
- animation: {
- name: module-ongoing-call__controls--fade-in;
- duration: 400ms;
- timing-function: $ease-out-expo;
- fill-mode: forwards;
+.module-ongoing-call {
+ &__remote-video-enabled {
+ background-color: $color-gray-95;
+ height: 100vh;
+ width: 100%;
+ }
+
+ &__remote-video-disabled {
+ background-color: $color-gray-95;
+ height: 100vh;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__local-video {
+ background-color: transparent;
+ bottom: 160px;
+ height: 152px;
+ position: absolute;
+ right: 32px;
+ transform: rotateY(180deg);
+ width: 210px;
+ }
+
+ &__header {
+ position: absolute;
+ }
+
+ &__header-message {
+ font-weight: normal;
+ font-size: 13px;
+ line-height: 18px;
+ letter-spacing: -0.0025em;
+ }
+
+ &__actions {
+ background: linear-gradient(transparent, $color-black-alpha-40);
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ padding-bottom: 32px;
+ padding-top: 32px;
+ position: absolute;
+ text-align: center;
+ width: 100%;
+ }
+
+ &__controls--fadeIn {
+ animation: {
+ name: module-ongoing-call__controls--fade-in;
+ duration: 400ms;
+ timing-function: $ease-out-expo;
+ fill-mode: forwards;
+ }
+ }
+
+ &__controls--fadeOut {
+ animation: {
+ name: module-ongoing-call__controls--fade-out;
+ duration: 1200ms;
+ timing-function: $ease-out-expo;
+ fill-mode: forwards;
+ }
}
}
-.module-ongoing-call__controls--fadeOut {
- animation: {
- name: module-ongoing-call__controls--fade-out;
- duration: 1200ms;
- timing-function: $ease-out-expo;
- fill-mode: forwards;
- }
-}
-
-.module-ongoing-call__settings {
+.module-calling-tools {
+ display: flex;
+ justify-content: flex-end;
position: absolute;
- top: 25px;
- right: 65px;
+ top: 24px;
+ width: 100%;
- &--button {
- @include color-svg(
- '../images/icons/v2/settings-solid-16.svg',
- $color-white
- );
- height: 22px;
- width: 22px;
+ &__button {
+ margin-right: 25px;
}
}
-.module-ongoing-call__pip {
- position: absolute;
- top: 25px;
- right: 25px;
+.module-calling-lobby {
+ &__actions {
+ flex: 0 0 100px;
+ }
- &--button {
- @include color-svg('../images/icons/v2/collapse-24.svg', $color-white);
- height: 22px;
- width: 22px;
+ &__button {
+ margin-left: 8px;
+ margin-right: 8px;
+ width: 160px;
+ }
+
+ &__video {
+ @include font-body-2;
+ align-items: center;
+ background-color: $color-gray-80;
+ border-radius: 8px;
+ color: $color-white;
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ justify-content: center;
+ margin-bottom: 24px;
+ margin-top: 24px;
+ max-width: 640px;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+ }
+
+ &__video-off {
+ &--icon {
+ @include color-svg(
+ '../images/icons/v2/video-off-solid-24.svg',
+ $color-white
+ );
+ height: 24px;
+ margin-bottom: 8px;
+ width: 24px;
+ }
+
+ &--text {
+ z-index: 1;
+ }
}
}
@@ -6203,25 +6296,6 @@ button.module-image__border-overlay:focus {
width: 32px;
}
- &--background {
- background-repeat: no-repeat;
- background-size: cover;
- border-radius: 4px 4px 0 0;
- height: 100%;
- position: absolute;
- width: 100%;
- }
-
- &--blur {
- backdrop-filter: blur(7px);
- backface-visibility: hidden;
- background-color: $color-black-alpha-40;
- border-radius: 4px 4px 0 0;
- height: 100%;
- position: absolute;
- width: 100%;
- }
-
&--avatar img {
-webkit-user-drag: none;
-webkit-user-select: none;
@@ -6270,11 +6344,15 @@ button.module-image__border-overlay:focus {
}
.module-call-need-permission-screen {
+ align-items: center;
+ background-color: $color-gray-95;
+ color: $color-gray-05;
display: flex;
flex-direction: column;
+ height: 100vh;
justify-content: center;
- align-items: center;
- color: $color-gray-05;
+ position: relative;
+ width: 100%;
&__text {
margin: 2em 1em;
@@ -9342,6 +9420,52 @@ button.module-image__border-overlay:focus {
justify-content: flex-end;
}
+.module-button {
+ &__gray {
+ @include font-body-1-bold;
+ background-color: $color-gray-45;
+ border-radius: 4px;
+ border: none;
+ color: $color-white;
+ outline: none;
+ padding: 7px 14px;
+
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+ }
+
+ &__green {
+ @include font-body-1-bold;
+ background-color: $color-accent-green;
+ border-radius: 4px;
+ border: none;
+ color: $color-white;
+ outline: none;
+ padding: 7px 14px;
+
+ @include keyboard-mode {
+ &:focus {
+ box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
+ }
+ }
+ }
+}
+
+.module-background-color {
+ &__default {
+ background-color: $color-black-alpha-40;
+ }
+
+ @each $color, $value in $conversation-colors {
+ &__#{$color} {
+ background-color: $value;
+ }
+ }
+}
+
/* Third-party module: react-tooltip-lite */
.react-tooltip-lite {
diff --git a/ts/components/CallBackgroundBlur.tsx b/ts/components/CallBackgroundBlur.tsx
new file mode 100644
index 0000000000..5b983a9aab
--- /dev/null
+++ b/ts/components/CallBackgroundBlur.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import classNames from 'classnames';
+import { ColorType } from '../types/Colors';
+
+export type PropsType = {
+ avatarPath?: string;
+ children: React.ReactNode;
+ color?: ColorType;
+};
+
+export const CallBackgroundBlur = ({
+ avatarPath,
+ children,
+ color,
+}: PropsType): JSX.Element => {
+ const backgroundProps = avatarPath
+ ? {
+ style: {
+ backgroundImage: `url("${avatarPath}")`,
+ },
+ }
+ : {
+ className: classNames(
+ 'module-calling__background',
+ `module-background-color__${color || 'default'}`
+ ),
+ };
+
+ return (
+ <>
+
+
+ {children}
+ >
+ );
+};
diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx
index ccac225d52..258677b05a 100644
--- a/ts/components/CallManager.stories.tsx
+++ b/ts/components/CallManager.stories.tsx
@@ -15,6 +15,7 @@ const callDetails = {
isIncoming: true,
isVideoCall: true,
+ id: '3051234567',
avatarPath: undefined,
color: 'ultramarine' as ColorType,
title: 'Rick Sanchez',
@@ -27,6 +28,7 @@ const defaultProps = {
acceptCall: action('accept-call'),
callDetails,
callState: CallState.Accepted,
+ cancelCall: action('cancel-call'),
closeNeedPermissionScreen: action('close-need-permission-screen'),
declineCall: action('decline-call'),
hangUp: action('hang-up'),
@@ -41,6 +43,8 @@ const defaultProps = {
setLocalVideo: action('set-local-video'),
setRendererCanvas: action('set-renderer-canvas'),
settingsDialogOpen: false,
+ startCall: action('start-call'),
+ toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'),
toggleSettings: action('toggle-settings'),
};
diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx
index d0ec022f16..6168175242 100644
--- a/ts/components/CallManager.tsx
+++ b/ts/components/CallManager.tsx
@@ -1,22 +1,26 @@
import React from 'react';
import { CallingPip } from './CallingPip';
import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
+import { CallingLobby } from './CallingLobby';
import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen';
import {
IncomingCallBar,
PropsType as IncomingCallBarPropsType,
} from './IncomingCallBar';
import { CallState, CallEndedReason } from '../types/Calling';
-import { CallDetailsType } from '../state/ducks/calling';
+import { CallDetailsType, OutgoingCallType } from '../state/ducks/calling';
type CallManagerPropsType = {
callDetails?: CallDetailsType;
- callState?: CallState;
callEndedReason?: CallEndedReason;
+ callState?: CallState;
+ cancelCall: () => void;
pip: boolean;
closeNeedPermissionScreen: () => void;
renderDeviceSelection: () => JSX.Element;
settingsDialogOpen: boolean;
+ startCall: (payload: OutgoingCallType) => void;
+ toggleParticipants: () => void;
};
type PropsType = IncomingCallBarPropsType &
@@ -28,6 +32,7 @@ export const CallManager = ({
callDetails,
callState,
callEndedReason,
+ cancelCall,
closeNeedPermissionScreen,
declineCall,
hangUp,
@@ -42,10 +47,12 @@ export const CallManager = ({
setLocalVideo,
setRendererCanvas,
settingsDialogOpen,
+ startCall,
+ toggleParticipants,
togglePip,
toggleSettings,
}: PropsType): JSX.Element | null => {
- if (!callDetails || !callState) {
+ if (!callDetails) {
return null;
}
const incoming = callDetails.isIncoming;
@@ -68,6 +75,31 @@ export const CallManager = ({
return null;
}
+ if (!callState) {
+ return (
+ <>
+ {
+ startCall({ callDetails });
+ }}
+ setLocalPreview={setLocalPreview}
+ setLocalAudio={setLocalAudio}
+ setLocalVideo={setLocalVideo}
+ toggleParticipants={toggleParticipants}
+ toggleSettings={toggleSettings}
+ />
+ {settingsDialogOpen && renderDeviceSelection()}
+ >
+ );
+ }
+
if (outgoing || ongoing) {
if (pip) {
return (
diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx
index 6672ba9d9a..e87b11c3a4 100644
--- a/ts/components/CallScreen.stories.tsx
+++ b/ts/components/CallScreen.stories.tsx
@@ -5,7 +5,7 @@ import { action } from '@storybook/addon-actions';
import { CallState } from '../types/Calling';
import { ColorType } from '../types/Colors';
-import { CallScreen } from './CallScreen';
+import { CallScreen, PropsType } from './CallScreen';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
@@ -16,6 +16,7 @@ const callDetails = {
isIncoming: true,
isVideoCall: true,
+ id: '3051234567',
avatarPath: undefined,
color: 'ultramarine' as ColorType,
title: 'Rick Sanchez',
@@ -24,13 +25,20 @@ const callDetails = {
profileName: 'Rick Sanchez',
};
-const defaultProps = {
+const createProps = (overrideProps: Partial = {}): PropsType => ({
callDetails,
- callState: CallState.Accepted,
+ callState: select(
+ 'callState',
+ CallState,
+ overrideProps.callState || CallState.Accepted
+ ),
hangUp: action('hang-up'),
- hasLocalAudio: true,
- hasLocalVideo: true,
- hasRemoteVideo: true,
+ hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
+ hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
+ hasRemoteVideo: boolean(
+ 'hasRemoteVideo',
+ overrideProps.hasRemoteVideo || false
+ ),
i18n,
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
@@ -38,82 +46,38 @@ const defaultProps = {
setRendererCanvas: action('set-renderer-canvas'),
togglePip: action('toggle-pip'),
toggleSettings: action('toggle-settings'),
-};
+});
-const permutations = [
- {
- title: 'Call Screen',
- props: {},
- },
- {
- title: 'Call Screen (Pre-ring)',
- props: {
- callState: CallState.Prering,
- },
- },
- {
- title: 'Call Screen (Ringing)',
- props: {
- callState: CallState.Ringing,
- },
- },
- {
- title: 'Call Screen (Reconnecting)',
- props: {
- callState: CallState.Reconnecting,
- },
- },
- {
- title: 'Call Screen (Ended)',
- props: {
- callState: CallState.Ended,
- },
- },
- {
- title: 'Calling (no local audio)',
- props: {
- ...defaultProps,
- hasLocalAudio: false,
- },
- },
- {
- title: 'Calling (no local video)',
- props: {
- ...defaultProps,
- hasLocalVideo: false,
- },
- },
- {
- title: 'Calling (no remote video)',
- props: {
- ...defaultProps,
- hasRemoteVideo: false,
- },
- },
-];
+const story = storiesOf('Components/CallScreen', module);
-storiesOf('Components/CallScreen', module)
- .add('Knobs Playground', () => {
- const callState = select('callState', CallState, CallState.Accepted);
- const hasLocalAudio = boolean('hasLocalAudio', true);
- const hasLocalVideo = boolean('hasLocalVideo', true);
- const hasRemoteVideo = boolean('hasRemoteVideo', true);
+story.add('Default', () => {
+ return ;
+});
- return (
-
- );
- })
- .add('Iterations', () => {
- return permutations.map(({ props, title }) => (
- <>
- {title}
-
- >
- ));
- });
+story.add('Pre-Ring', () => {
+ return ;
+});
+
+story.add('Ringing', () => {
+ return ;
+});
+
+story.add('Reconnecting', () => {
+ return ;
+});
+
+story.add('Ended', () => {
+ return ;
+});
+
+story.add('hasLocalAudio', () => {
+ return ;
+});
+
+story.add('hasLocalVideo', () => {
+ return ;
+});
+
+story.add('hasRemoteVideo', () => {
+ return ;
+});
diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx
index 113e060c85..d623f41286 100644
--- a/ts/components/CallScreen.tsx
+++ b/ts/components/CallScreen.tsx
@@ -9,30 +9,10 @@ import {
SetRendererCanvasType,
} from '../state/ducks/calling';
import { Avatar } from './Avatar';
+import { CallingButton, CallingButtonType } from './CallingButton';
import { CallState } from '../types/Calling';
import { LocalizerType } from '../types/Util';
-type CallingButtonProps = {
- classNameSuffix: string;
- onClick: () => void;
-};
-
-const CallingButton = ({
- classNameSuffix,
- onClick,
-}: CallingButtonProps): JSX.Element => {
- const className = classNames(
- 'module-ongoing-call__icon',
- `module-ongoing-call__icon${classNameSuffix}`
- );
-
- return (
-
- );
-};
-
export type PropsType = {
callDetails?: CallDetailsType;
callState?: CallState;
@@ -137,10 +117,10 @@ export class CallScreen extends React.Component {
let eventHandled = false;
- if (event.key === 'V') {
+ if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
this.toggleVideo();
eventHandled = true;
- } else if (event.key === 'M') {
+ } else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
this.toggleAudio();
eventHandled = true;
}
@@ -225,42 +205,41 @@ export class CallScreen extends React.Component {
!showControls && !isAudioOnly && callState === CallState.Accepted,
});
- const toggleAudioSuffix = hasLocalAudio
- ? '--audio--enabled'
- : '--audio--disabled';
- const toggleVideoSuffix = hasLocalVideo
- ? '--video--enabled'
- : '--video--disabled';
+ const videoButtonType = hasLocalVideo
+ ? CallingButtonType.VIDEO_ON
+ : CallingButtonType.VIDEO_OFF;
+ const audioButtonType = hasLocalAudio
+ ? CallingButtonType.AUDIO_ON
+ : CallingButtonType.AUDIO_OFF;
return (
-
+
{callDetails.title}
{this.renderMessage(callState)}
-
+
-
-
@@ -276,18 +255,24 @@ export class CallScreen extends React.Component
{
)}
>
{
hangUp({ callId: callDetails.callId });
}}
+ tooltipDistance={24}
/>
diff --git a/ts/components/CallingButton.stories.tsx b/ts/components/CallingButton.stories.tsx
new file mode 100644
index 0000000000..e82044d150
--- /dev/null
+++ b/ts/components/CallingButton.stories.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { number, select } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+
+import {
+ CallingButton,
+ CallingButtonType,
+ PropsType,
+ TooltipDirection,
+} from './CallingButton';
+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 => ({
+ buttonType: select(
+ 'buttonType',
+ CallingButtonType,
+ overrideProps.buttonType || CallingButtonType.HANG_UP
+ ),
+ i18n,
+ onClick: action('on-click'),
+ tooltipDirection: select(
+ 'tooltipDirection',
+ TooltipDirection,
+ overrideProps.tooltipDirection || TooltipDirection.DOWN
+ ),
+ tooltipDistance: number(
+ 'tooltipDistance',
+ overrideProps.tooltipDistance || 16
+ ),
+});
+
+const story = storiesOf('Components/CallingButton', module);
+
+story.add('Default', () => {
+ const props = createProps();
+ return ;
+});
+
+story.add('Audio On', () => {
+ const props = createProps({
+ buttonType: CallingButtonType.AUDIO_ON,
+ });
+ return ;
+});
+
+story.add('Audio Off', () => {
+ const props = createProps({
+ buttonType: CallingButtonType.AUDIO_OFF,
+ });
+ return ;
+});
+
+story.add('Video On', () => {
+ const props = createProps({
+ buttonType: CallingButtonType.VIDEO_ON,
+ });
+ return ;
+});
+
+story.add('Video Off', () => {
+ const props = createProps({
+ buttonType: CallingButtonType.VIDEO_OFF,
+ });
+ return ;
+});
+
+story.add('Tooltip right', () => {
+ const props = createProps({
+ tooltipDirection: TooltipDirection.RIGHT,
+ });
+ return ;
+});
diff --git a/ts/components/CallingButton.tsx b/ts/components/CallingButton.tsx
new file mode 100644
index 0000000000..ddeda65619
--- /dev/null
+++ b/ts/components/CallingButton.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import classNames from 'classnames';
+import Tooltip from 'react-tooltip-lite';
+import { LocalizerType } from '../types/Util';
+
+export enum TooltipDirection {
+ UP = 'up',
+ RIGHT = 'right',
+ DOWN = 'down',
+ LEFT = 'left',
+}
+
+export enum CallingButtonType {
+ AUDIO_OFF = 'AUDIO_OFF',
+ AUDIO_ON = 'AUDIO_ON',
+ HANG_UP = 'HANG_UP',
+ VIDEO_OFF = 'VIDEO_OFF',
+ VIDEO_ON = 'VIDEO_ON',
+}
+
+export type PropsType = {
+ buttonType: CallingButtonType;
+ i18n: LocalizerType;
+ onClick: () => void;
+ tooltipDirection?: TooltipDirection;
+ tooltipDistance?: number;
+};
+
+export const CallingButton = ({
+ buttonType,
+ i18n,
+ onClick,
+ tooltipDirection = TooltipDirection.DOWN,
+ tooltipDistance = 16,
+}: PropsType): JSX.Element => {
+ let classNameSuffix = '';
+ let tooltipContent = '';
+ if (buttonType === CallingButtonType.AUDIO_OFF) {
+ classNameSuffix = 'audio--disabled';
+ tooltipContent = i18n('calling__button--audio-on');
+ } else if (buttonType === CallingButtonType.AUDIO_ON) {
+ classNameSuffix = 'audio--enabled';
+ tooltipContent = i18n('calling__button--audio-off');
+ } else if (buttonType === CallingButtonType.VIDEO_OFF) {
+ classNameSuffix = 'video--disabled';
+ tooltipContent = i18n('calling__button--video-on');
+ } else if (buttonType === CallingButtonType.VIDEO_ON) {
+ classNameSuffix = 'video--enabled';
+ tooltipContent = i18n('calling__button--video-off');
+ } else if (buttonType === CallingButtonType.HANG_UP) {
+ classNameSuffix = 'hangup';
+ tooltipContent = i18n('calling__hangup');
+ }
+
+ const className = classNames(
+ 'module-calling-button__icon',
+ `module-calling-button__icon--${classNameSuffix}`
+ );
+
+ return (
+
+ );
+};
diff --git a/ts/components/CallingLobby.stories.tsx b/ts/components/CallingLobby.stories.tsx
new file mode 100644
index 0000000000..0a6faff6da
--- /dev/null
+++ b/ts/components/CallingLobby.stories.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { boolean } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+
+import { ColorType } from '../types/Colors';
+import { CallingLobby, PropsType } from './CallingLobby';
+import { setup as setupI18n } from '../../js/modules/i18n';
+import enMessages from '../../_locales/en/messages.json';
+
+const i18n = setupI18n('en', enMessages);
+
+const callDetails = {
+ callId: 0,
+ isIncoming: true,
+ isVideoCall: true,
+
+ id: '3051234567',
+ avatarPath: undefined,
+ color: 'ultramarine' as ColorType,
+ title: 'Rick Sanchez',
+ name: 'Rick Sanchez',
+ phoneNumber: '3051234567',
+ profileName: 'Rick Sanchez',
+};
+
+const createProps = (overrideProps: Partial = {}): PropsType => ({
+ callDetails,
+ hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
+ hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
+ i18n,
+ isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
+ onCallCanceled: action('on-call-canceled'),
+ onJoinCall: action('on-join-call'),
+ setLocalAudio: action('set-local-audio'),
+ setLocalPreview: action('set-local-preview'),
+ setLocalVideo: action('set-local-video'),
+ toggleParticipants: action('toggle-participants'),
+ toggleSettings: action('toggle-settings'),
+});
+
+const story = storiesOf('Components/CallingLobby', module);
+
+story.add('Default', () => {
+ const props = createProps();
+ return ;
+});
+
+story.add('Local Video', () => {
+ const props = createProps({
+ hasLocalVideo: true,
+ });
+ return ;
+});
+
+story.add('Group Call', () => {
+ const props = createProps({ isGroupCall: true });
+ return ;
+});
diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx
new file mode 100644
index 0000000000..58e6b1afe8
--- /dev/null
+++ b/ts/components/CallingLobby.tsx
@@ -0,0 +1,181 @@
+import React from 'react';
+import {
+ CallDetailsType,
+ SetLocalAudioType,
+ SetLocalPreviewType,
+ SetLocalVideoType,
+} from '../state/ducks/calling';
+import { CallState } from '../types/Calling';
+import {
+ CallingButton,
+ CallingButtonType,
+ TooltipDirection,
+} from './CallingButton';
+import { CallBackgroundBlur } from './CallBackgroundBlur';
+import { LocalizerType } from '../types/Util';
+
+export type PropsType = {
+ callDetails: CallDetailsType;
+ callState?: CallState;
+ hasLocalAudio: boolean;
+ hasLocalVideo: boolean;
+ i18n: LocalizerType;
+ isGroupCall: boolean;
+ onCallCanceled: () => void;
+ onJoinCall: () => void;
+ setLocalAudio: (_: SetLocalAudioType) => void;
+ setLocalVideo: (_: SetLocalVideoType) => void;
+ setLocalPreview: (_: SetLocalPreviewType) => void;
+ toggleParticipants: () => void;
+ toggleSettings: () => void;
+};
+
+export const CallingLobby = ({
+ callDetails,
+ hasLocalAudio,
+ hasLocalVideo,
+ i18n,
+ isGroupCall = false,
+ onCallCanceled,
+ onJoinCall,
+ setLocalAudio,
+ setLocalPreview,
+ setLocalVideo,
+ toggleParticipants,
+ toggleSettings,
+}: PropsType): JSX.Element => {
+ const localVideoRef = React.useRef(null);
+
+ const toggleAudio = React.useCallback((): void => {
+ if (!callDetails) {
+ return;
+ }
+
+ setLocalAudio({ enabled: !hasLocalAudio });
+ }, [callDetails, hasLocalAudio, setLocalAudio]);
+
+ const toggleVideo = React.useCallback((): void => {
+ if (!callDetails) {
+ return;
+ }
+
+ setLocalVideo({ enabled: !hasLocalVideo });
+ }, [callDetails, hasLocalVideo, setLocalVideo]);
+
+ React.useEffect(() => {
+ setLocalPreview({ element: localVideoRef });
+
+ return () => {
+ setLocalPreview({ element: undefined });
+ };
+ }, [setLocalPreview]);
+
+ React.useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent): void {
+ let eventHandled = false;
+
+ if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
+ toggleVideo();
+ eventHandled = true;
+ } else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
+ toggleAudio();
+ eventHandled = true;
+ }
+
+ if (eventHandled) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [toggleVideo, toggleAudio]);
+
+ const videoButtonType = hasLocalVideo
+ ? CallingButtonType.VIDEO_ON
+ : CallingButtonType.VIDEO_OFF;
+ const audioButtonType = hasLocalAudio
+ ? CallingButtonType.AUDIO_ON
+ : CallingButtonType.AUDIO_OFF;
+
+ return (
+
+
+
+ {callDetails.title}
+
+
+ {isGroupCall ? (
+
+ ) : null}
+
+
+
+
+ {hasLocalVideo ? (
+
+ ) : (
+
+
+
+ {i18n('calling__your-video-is-off')}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/ts/components/CallingPip.stories.tsx b/ts/components/CallingPip.stories.tsx
index 908a49397b..21ce15acbd 100644
--- a/ts/components/CallingPip.stories.tsx
+++ b/ts/components/CallingPip.stories.tsx
@@ -15,6 +15,7 @@ const callDetails = {
isIncoming: true,
isVideoCall: true,
+ id: '3051234567',
avatarPath: undefined,
color: 'ultramarine' as ColorType,
title: 'Rick Sanchez',
@@ -24,7 +25,7 @@ const callDetails = {
};
const createProps = (overrideProps: Partial = {}): PropsType => ({
- callDetails,
+ callDetails: overrideProps.callDetails || callDetails,
hangUp: action('hang-up'),
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
hasRemoteVideo: boolean(
@@ -43,3 +44,23 @@ story.add('Default', () => {
const props = createProps();
return ;
});
+
+story.add('Contact (with avatar)', () => {
+ const props = createProps({
+ callDetails: {
+ ...callDetails,
+ avatarPath: 'https://www.fillmurray.com/64/64',
+ },
+ });
+ return ;
+});
+
+story.add('Contact (no color)', () => {
+ const props = createProps({
+ callDetails: {
+ ...callDetails,
+ color: undefined,
+ },
+ });
+ return ;
+});
diff --git a/ts/components/CallingPip.tsx b/ts/components/CallingPip.tsx
index edb2791004..c61e3e9f29 100644
--- a/ts/components/CallingPip.tsx
+++ b/ts/components/CallingPip.tsx
@@ -6,6 +6,7 @@ import {
SetRendererCanvasType,
} from '../state/ducks/calling';
import { Avatar } from './Avatar';
+import { CallBackgroundBlur } from './CallBackgroundBlur';
import { LocalizerType } from '../types/Util';
function renderAvatar(
@@ -21,35 +22,24 @@ function renderAvatar(
title,
} = callDetails;
- const backgroundStyle = avatarPath
- ? {
- backgroundImage: `url("${avatarPath}")`,
- }
- : {
- backgroundColor: color,
- };
-
return (
);
}
diff --git a/ts/components/IncomingCallBar.stories.tsx b/ts/components/IncomingCallBar.stories.tsx
index 8a0e47c5ca..a53cb8ca31 100644
--- a/ts/components/IncomingCallBar.stories.tsx
+++ b/ts/components/IncomingCallBar.stories.tsx
@@ -17,6 +17,7 @@ const defaultProps = {
isIncoming: true,
isVideoCall: true,
+ id: '3051234567',
avatarPath: undefined,
contactColor: 'ultramarine' as ColorType,
name: 'Rick Sanchez',
diff --git a/ts/components/IncomingCallBar.tsx b/ts/components/IncomingCallBar.tsx
index 14aac615d4..444758e560 100644
--- a/ts/components/IncomingCallBar.tsx
+++ b/ts/components/IncomingCallBar.tsx
@@ -31,7 +31,7 @@ const CallButton = ({
}: CallButtonProps): JSX.Element => {
return (