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 @@ +group-solid-24 \ 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 ? ( +
+
+
+ {hasLocalVideo ? ( +
+ ); +}; 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 (