diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5c925f5bbef4..b932a9448830 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1300,7 +1300,11 @@ }, "calling__call-is-full": { "message": "Call is full", - "description": "Button label in the call lobby when you can't join because the call is full" + "description": "Text in the call lobby when you can't join because the call is full" + }, + "calling__button--video__label": { + "message": "Camera", + "description": "Label under the video button" }, "calling__button--video-disabled": { "message": "Camera disabled", @@ -1314,6 +1318,10 @@ "message": "Turn on camera", "description": "Button tooltip label for turning on the camera" }, + "calling__button--audio__label": { + "message": "Mute", + "description": "Label under the audio button" + }, "calling__button--audio-disabled": { "message": "Microphone disabled", "description": "Button tooltip label when the microphone is disabled" @@ -1326,6 +1334,10 @@ "message": "Unmute mic", "description": "Button tooltip label for turning on the microphone" }, + "calling__button--presenting__label": { + "message": "Share", + "description": "Label under the share screen button" + }, "calling__button--presenting-disabled": { "message": "Presenting disabled", "description": "Button tooltip label for when screen sharing is disabled" @@ -1342,11 +1354,11 @@ "message": "Your camera is off", "description": "Label in the calling lobby indicating that your camera is off" }, - "calling__lobby-summary--zero": { + "calling__pre-call-info--empty-group": { "message": "No one else is here", "description": "Shown in the calling lobby to describe who is in the call" }, - "calling__lobby-summary--single": { + "calling__pre-call-info--1-person-in-call": { "message": "$first$ is in this call", "description": "Shown in the calling lobby to describe who is in the call", "placeholders": { @@ -1356,11 +1368,11 @@ } } }, - "calling__lobby-summary--self": { + "calling__pre-call-info--another-device-in-call": { "message": "One of your other devices is in this call", "description": "Shown in the calling lobby to describe when it is just you" }, - "calling__lobby-summary--double": { + "calling__pre-call-info--2-people-in-call": { "message": "$first$ and $second$ are in this call", "description": "Shown in the calling lobby to describe who is in the call", "placeholders": { @@ -1374,7 +1386,7 @@ } } }, - "calling__lobby-summary--triple": { + "calling__pre-call-info--3-people-in-call": { "message": "$first$, $second$, and $third$ are in this call", "description": "Shown in the calling lobby to describe who is in the call", "placeholders": { @@ -1392,7 +1404,7 @@ } } }, - "calling__lobby-summary--many": { + "calling__pre-call-info--many-people-in-call": { "message": "$first$, $second$, and $others$ others are in this call", "description": "Shown in the calling lobby to describe who is in the call", "placeholders": { @@ -1410,6 +1422,76 @@ } } }, + "calling__pre-call-info--will-ring-1": { + "message": "Signal will ring $person$", + "description": "Shown in the calling lobby to describe who will be rung", + "placeholders": { + "person": { + "content": "$1", + "example": "Sam" + } + } + }, + "calling__pre-call-info--will-notify-1": { + "message": "$person$ will be notified", + "description": "Shown in the calling lobby to describe who will be notified", + "placeholders": { + "person": { + "content": "$1", + "example": "Sam" + } + } + }, + "calling__pre-call-info--will-notify-2": { + "message": "$first$ and $second$ will be notified", + "description": "Shown in the calling lobby to describe who will be notified", + "placeholders": { + "first": { + "content": "$1", + "example": "Sam" + }, + "second": { + "content": "$2", + "example": "Cayce" + } + } + }, + "calling__pre-call-info--will-notify-3": { + "message": "$first$, $second$, and $third$ will be notified", + "description": "Shown in the calling lobby to describe who will be notified", + "placeholders": { + "first": { + "content": "$1", + "example": "Sam" + }, + "second": { + "content": "$2", + "example": "Cayce" + }, + "third": { + "content": "$3", + "example": "April" + } + } + }, + "calling__pre-call-info--will-notify-many": { + "message": "$first$, $second$, and $others$ others will be notified", + "description": "Shown in the calling lobby to describe who will be notified", + "placeholders": { + "person": { + "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" diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss index 61127aa0d3d3..e4411e53dd95 100644 --- a/stylesheets/_mixins.scss +++ b/stylesheets/_mixins.scss @@ -222,6 +222,10 @@ text-align: inherit; } +@mixin calling-text-shadow { + text-shadow: 0 0 4px $color-black-alpha-40; +} + // --- Buttons // Individual traits diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 774161d96130..b54e72487588 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -5201,7 +5201,7 @@ button.module-image__border-overlay:focus { padding-bottom: 24px; padding-top: calc(24px + var(--title-bar-drag-area-height)); text-align: center; - text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25); + @include calling-text-shadow; width: 100%; &--header-name { @@ -5224,6 +5224,10 @@ button.module-image__border-overlay:focus { position: absolute; text-align: center; width: 100%; + + &--inline { + position: static; + } } &__background { @@ -5237,7 +5241,6 @@ button.module-image__border-overlay:focus { width: 100%; &--blur { - position: absolute; background-repeat: no-repeat; background-size: cover; background-position: center; @@ -5248,31 +5251,24 @@ button.module-image__border-overlay:focus { } } - &__video-off { - &--icon { + &__camera-is-off { + @include calling-text-shadow; + @include font-body-1; + color: $color-white; + display: flex; + z-index: 1; + + &::before { + content: ''; + display: block; @include color-svg( '../images/icons/v2/video-off-solid-24.svg', $color-white ); height: 24px; - margin-bottom: 8px; + margin-right: 10px; width: 24px; } - - &--text { - color: $color-white; - z-index: 1; - } - - &--container { - display: flex; - flex-direction: row; - margin-top: 12px; - - .module-calling__video-off--text { - margin-left: 10px; - } - } } } @@ -5352,20 +5348,31 @@ button.module-image__border-overlay:focus { height: $size; width: $size; } + + &__cancel { + @include color-svg('../images/icons/v2/x-24.svg', $color-white); + height: $size; + width: $size; + } +} + +.module-calling-button__container { + display: inline-flex; + flex-direction: column; } .module-calling-button__icon { - border-radius: 56px; - height: 56px; - width: 56px; + border-radius: 52px; + height: 52px; + width: 52px; @mixin calling-button-icon($icon, $background-color, $icon-color) { background-color: $background-color; div { @include color-svg($icon, $icon-color); - height: 28px; - width: 28px; + height: 24px; + width: 24px; } } @@ -5433,6 +5440,16 @@ button.module-image__border-overlay:focus { } } +.module-calling-button__label { + @include font-subtitle; + margin-top: 8px; + text-align: center; + text-transform: lowercase; + color: $color-white; + @include calling-text-shadow; + user-select: none; +} + @keyframes module-ongoing-call__controls--fade-in { from { opacity: 0; @@ -5804,84 +5821,6 @@ button.module-image__border-overlay:focus { } } -.module-calling-lobby { - &__actions { - align-items: flex-start; - display: flex; - flex-direction: row; - flex: 0 0 100px; - } - - &__button { - margin-left: 8px; - margin-right: 8px; - width: 160px; - - &[disabled] { - opacity: 0.5; - } - } - - // The dimensions of this element are set by JavaScript. - &__local-preview { - $transition: 200ms ease-out; - - @include font-body-2; - border-radius: 8px; - color: $color-white; - display: flex; - flex-direction: column; - max-height: 100%; - max-width: 100%; - overflow: hidden; - position: relative; - transition: width $transition, height $transition; - - &-container { - align-items: center; - display: flex; - flex-direction: column; - flex: 1 1 auto; - justify-content: center; - margin: 24px; - overflow: hidden; - width: 90%; - } - - &__video-on { - background-color: $color-gray-80; - display: block; - flex-grow: 1; - object-fit: contain; - transform: rotateY(180deg); - width: 100%; - height: 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; - } - } - } - - &__info { - color: $color-white; - margin-bottom: 36px; - margin-top: 12px; - } -} - .module-calling-pip { backface-visibility: hidden; background-color: $color-gray-95; @@ -9547,40 +9486,6 @@ button.module-image__border-overlay:focus { outline: none; padding: 7px 12px; } - - &__gray { - @include font-body-1-bold; - background-color: $color-gray-45; - border-radius: 4px; - border: none; - color: $color-white; - line-height: 24px; - outline: none; - padding: 7px 14px; - - @include keyboard-mode { - &:focus { - box-shadow: 0px 0px 0px 2px $color-ultramarine; - } - } - } - - &__green { - @include font-body-1-bold; - background-color: $color-accent-green; - border-radius: 4px; - border: none; - color: $color-white; - line-height: 24px; - outline: none; - padding: 7px 14px; - - @include keyboard-mode { - &:focus { - box-shadow: 0px 0px 0px 2px $color-ultramarine; - } - } - } } // Module: Group Contact Details diff --git a/stylesheets/components/Button.scss b/stylesheets/components/Button.scss index 6f3da927a6c3..f18b2d13890a 100644 --- a/stylesheets/components/Button.scss +++ b/stylesheets/components/Button.scss @@ -134,4 +134,26 @@ @include hover-and-active-states($background-color, $color-white); } } + + &--calling { + $color: $color-white; + $background-color: $color-accent-green; + + @include rounded-corners; + color: $color; + background: $background-color; + + &:disabled { + color: fade-out($color, 0.4); + background: fade-out($background-color, 0.6); + } + + @include light-theme { + @include hover-and-active-states($background-color, $color-black); + } + + @include dark-theme { + @include hover-and-active-states($background-color, $color-white); + } + } } diff --git a/stylesheets/components/CallingLobby.scss b/stylesheets/components/CallingLobby.scss new file mode 100644 index 000000000000..8e2b6b8757c0 --- /dev/null +++ b/stylesheets/components/CallingLobby.scss @@ -0,0 +1,48 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-CallingLobby { + &__local-preview { + height: 100%; + object-fit: cover; + opacity: 0.6; + position: absolute; + transform: rotateY(180deg); + width: 100%; + z-index: -1; + } + + &__camera-is-off { + @include calling-text-shadow; + @include font-subtitle; + align-items: center; + color: $color-white; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; + text-align: center; + transition: opacity 100ms ease-out; + user-select: none; + + &--visible { + opacity: 1; + } + + &--invisible { + opacity: 0; + } + + &::before { + content: ''; + display: block; + @include color-svg( + '../images/icons/v2/video-off-solid-24.svg', + $color-white + ); + height: 24px; + margin-bottom: 8px; + width: 24px; + } + } +} diff --git a/stylesheets/components/CallingLobbyJoinButton.scss b/stylesheets/components/CallingLobbyJoinButton.scss new file mode 100644 index 000000000000..84666b6089ed --- /dev/null +++ b/stylesheets/components/CallingLobbyJoinButton.scss @@ -0,0 +1,6 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-CallingLobbyJoinButton { + margin-bottom: 32px; +} diff --git a/stylesheets/components/CallingPreCallInfo.scss b/stylesheets/components/CallingPreCallInfo.scss new file mode 100644 index 000000000000..36049720aa86 --- /dev/null +++ b/stylesheets/components/CallingPreCallInfo.scss @@ -0,0 +1,29 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-CallingPreCallInfo { + text-align: center; + user-select: none; + + &__title, + &__subtitle { + -webkit-box-orient: vertical; + color: $color-white; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + @include calling-text-shadow; + } + + &__title { + -webkit-line-clamp: 1; + @include font-title-2; + margin-top: 16px; + } + + &__subtitle { + -webkit-line-clamp: 2; + @include font-body-1; + margin-top: 8px; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index fc0d34c434e8..b0bea0f931d0 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -37,6 +37,9 @@ @import './components/AvatarTextEditor.scss'; @import './components/BetterAvatarBubble.scss'; @import './components/Button.scss'; +@import './components/CallingLobby.scss'; +@import './components/CallingLobbyJoinButton.scss'; +@import './components/CallingPreCallInfo.scss'; @import './components/CallingScreenSharingController.scss'; @import './components/CallingSelectPresentingSourcesModal.scss'; @import './components/ChatColorPicker.scss'; diff --git a/ts/components/Button.stories.tsx b/ts/components/Button.stories.tsx index 93766e6be1c4..776968c226bc 100644 --- a/ts/components/Button.stories.tsx +++ b/ts/components/Button.stories.tsx @@ -19,6 +19,7 @@ story.add('Kitchen sink', () => ( ButtonVariant.SecondaryAffirmative, ButtonVariant.SecondaryDestructive, ButtonVariant.Destructive, + ButtonVariant.Calling, ].map(variant => (

@@ -50,3 +51,9 @@ story.add('aria-label', () => ( onClick={action('onClick')} /> )); + +story.add('Custom styles', () => ( + +)); diff --git a/ts/components/Button.tsx b/ts/components/Button.tsx index c030ca0cad54..dceaec2ca68e 100644 --- a/ts/components/Button.tsx +++ b/ts/components/Button.tsx @@ -1,7 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { MouseEventHandler, ReactNode } from 'react'; +import React, { CSSProperties, MouseEventHandler, ReactNode } from 'react'; import classNames from 'classnames'; import { assert } from '../util/assert'; @@ -17,12 +17,15 @@ export enum ButtonVariant { SecondaryAffirmative, SecondaryDestructive, Destructive, + Calling, } type PropsType = { className?: string; disabled?: boolean; size?: ButtonSize; + style?: CSSProperties; + tabIndex?: number; variant?: ButtonVariant; } & ( | { @@ -64,6 +67,7 @@ const VARIANT_CLASS_NAMES = new Map([ 'module-Button--secondary module-Button--secondary--destructive', ], [ButtonVariant.Destructive, 'module-Button--destructive'], + [ButtonVariant.Calling, 'module-Button--calling'], ]); export const Button = React.forwardRef( @@ -73,6 +77,8 @@ export const Button = React.forwardRef( className, disabled = false, size = ButtonSize.Medium, + style, + tabIndex, variant = ButtonVariant.Primary, } = props; const ariaLabel = props['aria-label']; @@ -105,6 +111,8 @@ export const Button = React.forwardRef( disabled={disabled} onClick={onClick} ref={ref} + style={style} + tabIndex={tabIndex} // The `type` should either be "button" or "submit", which is effectively static. // eslint-disable-next-line react/button-has-type type={type} diff --git a/ts/components/CallBackgroundBlur.tsx b/ts/components/CallBackgroundBlur.tsx index d002dfdad128..11333fe3aacf 100644 --- a/ts/components/CallBackgroundBlur.tsx +++ b/ts/components/CallBackgroundBlur.tsx @@ -7,20 +7,26 @@ import { AvatarColorType } from '../types/Colors'; export type PropsType = { avatarPath?: string; - children: React.ReactNode; + children?: React.ReactNode; + className?: string; color?: AvatarColorType; }; export const CallBackgroundBlur = ({ avatarPath, children, + className, color, }: PropsType): JSX.Element => { return (

{avatarPath && (
( deviceCount: 0, joinState: GroupCallJoinState.Joined, maxDevices: 5, + groupMembers: [], peekedParticipants: [], remoteParticipants: [], }, @@ -189,6 +190,7 @@ story.add('Group call - Safety Number Changed', () => ( deviceCount: 0, joinState: GroupCallJoinState.Joined, maxDevices: 5, + groupMembers: [], peekedParticipants: [], remoteParticipants: [], }, diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index 014dc66037cc..5322e09e9263 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -163,6 +163,9 @@ const ActiveCallManager: React.FC = ({ let isCallFull: boolean; let showCallLobby: boolean; + let groupMembers: + | undefined + | Array>; switch (activeCall.callMode) { case CallMode.Direct: { @@ -182,11 +185,13 @@ const ActiveCallManager: React.FC = ({ } showCallLobby = !callState; isCallFull = false; + groupMembers = undefined; break; } case CallMode.Group: { showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined; isCallFull = activeCall.deviceCount >= activeCall.maxDevices; + ({ groupMembers } = activeCall); break; } default: @@ -199,6 +204,7 @@ const ActiveCallManager: React.FC = ({ ({ joinState: GroupCallJoinState.Joined, maxDevices: 5, deviceCount: (overrideProps.remoteParticipants || []).length, + groupMembers: overrideProps.remoteParticipants || [], // Because remote participants are a superset, we can use them in place of peeked // participants. peekedParticipants: diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 3ecc6dcc1aa7..deff1d2f83f9 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -313,7 +313,6 @@ export const CallScreen: React.FC = ({ className={classNames('module-ongoing-call__header', controlsFadeClass)} > = ({ sharedGroupNames={[]} size={80} /> -
-
- - {i18n('calling__your-video-is-off')} - +
+ {i18n('calling__your-video-is-off')}
diff --git a/ts/components/CallingButton.tsx b/ts/components/CallingButton.tsx index 9ca6be297484..80ea1c80a842 100644 --- a/ts/components/CallingButton.tsx +++ b/ts/components/CallingButton.tsx @@ -1,8 +1,9 @@ // Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { useMemo } from 'react'; import classNames from 'classnames'; +import { v4 as uuid } from 'uuid'; import { Tooltip, TooltipPlacement } from './Tooltip'; import { Theme } from '../util/theme'; import { LocalizerType } from '../types/Util'; @@ -33,42 +34,55 @@ export const CallingButton = ({ onClick, tooltipDirection, }: PropsType): JSX.Element => { + const uniqueButtonId = useMemo(() => uuid(), []); + let classNameSuffix = ''; let tooltipContent = ''; + let label = ''; let disabled = false; if (buttonType === CallingButtonType.AUDIO_DISABLED) { classNameSuffix = 'audio--disabled'; tooltipContent = i18n('calling__button--audio-disabled'); + label = i18n('calling__button--audio__label'); disabled = true; } else if (buttonType === CallingButtonType.AUDIO_OFF) { classNameSuffix = 'audio--off'; tooltipContent = i18n('calling__button--audio-on'); + label = i18n('calling__button--audio__label'); } else if (buttonType === CallingButtonType.AUDIO_ON) { classNameSuffix = 'audio--on'; tooltipContent = i18n('calling__button--audio-off'); + label = i18n('calling__button--audio__label'); } else if (buttonType === CallingButtonType.VIDEO_DISABLED) { classNameSuffix = 'video--disabled'; tooltipContent = i18n('calling__button--video-disabled'); disabled = true; + label = i18n('calling__button--video__label'); } else if (buttonType === CallingButtonType.VIDEO_OFF) { classNameSuffix = 'video--off'; tooltipContent = i18n('calling__button--video-on'); + label = i18n('calling__button--video__label'); } else if (buttonType === CallingButtonType.VIDEO_ON) { classNameSuffix = 'video--on'; tooltipContent = i18n('calling__button--video-off'); + label = i18n('calling__button--video__label'); } else if (buttonType === CallingButtonType.HANG_UP) { classNameSuffix = 'hangup'; tooltipContent = i18n('calling__hangup'); + label = i18n('calling__hangup'); } else if (buttonType === CallingButtonType.PRESENTING_DISABLED) { classNameSuffix = 'presenting--disabled'; tooltipContent = i18n('calling__button--presenting-disabled'); disabled = true; + label = i18n('calling__button--presenting__label'); } else if (buttonType === CallingButtonType.PRESENTING_ON) { classNameSuffix = 'presenting--on'; tooltipContent = i18n('calling__button--presenting-off'); + label = i18n('calling__button--presenting__label'); } else if (buttonType === CallingButtonType.PRESENTING_OFF) { classNameSuffix = 'presenting--off'; tooltipContent = i18n('calling__button--presenting-on'); + label = i18n('calling__button--presenting__label'); } const className = classNames( @@ -82,15 +96,24 @@ export const CallingButton = ({ direction={tooltipDirection} theme={Theme.Dark} > - +
+ + +
); }; diff --git a/ts/components/CallingHeader.stories.tsx b/ts/components/CallingHeader.stories.tsx index ccd6cebe800e..332c60513ff3 100644 --- a/ts/components/CallingHeader.stories.tsx +++ b/ts/components/CallingHeader.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -13,7 +13,6 @@ import enMessages from '../../_locales/en/messages.json'; const i18n = setupI18n('en', enMessages); const createProps = (overrideProps: Partial = {}): PropsType => ({ - canPip: boolean('canPip', Boolean(overrideProps.canPip)), i18n, isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)), message: overrideProps.message, @@ -35,14 +34,18 @@ const story = storiesOf('Components/CallingHeader', module); story.add('Default', () => ); -story.add('Has Pip', () => ( - +story.add('Lobby style', () => ( + )); story.add('With Participants', () => ( ( story.add('With Participants (shown)', () => ( void; participantCount: number; showParticipantsList: boolean; title?: string; @@ -23,11 +23,11 @@ export type PropsType = { }; export const CallingHeader = ({ - canPip = false, i18n, isInSpeakerView, isGroupCall = false, message, + onCancel, participantCount, showParticipantsList, title, @@ -44,7 +44,7 @@ export const CallingHeader = ({
{message}
) : null}
- {isGroupCall ? ( + {participantCount ? (
)} - {canPip && ( + {togglePip && (
)} + {onCancel && ( +
+ +
+ )}
); diff --git a/ts/components/CallingLobby.stories.tsx b/ts/components/CallingLobby.stories.tsx index 8342eca4166e..7b37038c9891 100644 --- a/ts/components/CallingLobby.stories.tsx +++ b/ts/components/CallingLobby.stories.tsx @@ -1,7 +1,8 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; +import { times } from 'lodash'; import { storiesOf } from '@storybook/react'; import { boolean } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; @@ -26,33 +27,53 @@ const camera = { }, }; -const createProps = (overrideProps: Partial = {}): PropsType => ({ - availableCameras: overrideProps.availableCameras || [camera], - conversation: { - title: 'Rick Sanchez', - }, - hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false), - hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false), - i18n, - isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false), - isCallFull: boolean('isCallFull', overrideProps.isCallFull || false), - me: overrideProps.me || { - color: AvatarColors[0], - uuid: generateUuid(), - }, - onCallCanceled: action('on-call-canceled'), - onJoinCall: action('on-join-call'), - peekedParticipants: overrideProps.peekedParticipants || [], - setLocalAudio: action('set-local-audio'), - setLocalPreview: action('set-local-preview'), - setLocalVideo: action('set-local-video'), - showParticipantsList: boolean( - 'showParticipantsList', - Boolean(overrideProps.showParticipantsList) - ), - toggleParticipants: action('toggle-participants'), - toggleSettings: action('toggle-settings'), -}); +const createProps = (overrideProps: Partial = {}): PropsType => { + const isGroupCall = boolean( + 'isGroupCall', + overrideProps.isGroupCall || false + ); + const conversation = isGroupCall + ? getDefaultConversation({ + title: 'Tahoe Trip', + type: 'group', + }) + : getDefaultConversation(); + + return { + availableCameras: overrideProps.availableCameras || [camera], + conversation, + groupMembers: isGroupCall + ? times(3, () => getDefaultConversation()) + : undefined, + hasLocalAudio: boolean( + 'hasLocalAudio', + overrideProps.hasLocalAudio || false + ), + hasLocalVideo: boolean( + 'hasLocalVideo', + overrideProps.hasLocalVideo || false + ), + i18n, + isGroupCall, + isCallFull: boolean('isCallFull', overrideProps.isCallFull || false), + me: overrideProps.me || { + color: AvatarColors[0], + uuid: generateUuid(), + }, + onCallCanceled: action('on-call-canceled'), + onJoinCall: action('on-join-call'), + peekedParticipants: overrideProps.peekedParticipants || [], + setLocalAudio: action('set-local-audio'), + setLocalPreview: action('set-local-preview'), + setLocalVideo: action('set-local-video'), + showParticipantsList: boolean( + 'showParticipantsList', + Boolean(overrideProps.showParticipantsList) + ), + toggleParticipants: action('toggle-participants'), + toggleSettings: action('toggle-settings'), + }; +}; const fakePeekedParticipant = (conversationProps: Partial) => getDefaultConversation({ @@ -123,26 +144,6 @@ story.add('Group Call - 1 peeked participant (self)', () => { return ; }); -story.add('Group Call - 2 peeked participants', () => { - const props = createProps({ - isGroupCall: true, - peekedParticipants: ['Sam', 'Cayce'].map(title => - fakePeekedParticipant({ title }) - ), - }); - return ; -}); - -story.add('Group Call - 3 peeked participants', () => { - const props = createProps({ - isGroupCall: true, - peekedParticipants: ['Sam', 'Cayce', 'April'].map(title => - fakePeekedParticipant({ title }) - ), - }); - return ; -}); - story.add('Group Call - 4 peeked participants', () => { const props = createProps({ isGroupCall: true, diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 6dc214be455e..b7f864456336 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -1,9 +1,8 @@ -// Copyright 2020 Signal Messenger, LLC +// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { ReactNode } from 'react'; -import Measure from 'react-measure'; -import { debounce } from 'lodash'; +import React from 'react'; +import classNames from 'classnames'; import { SetLocalAudioType, SetLocalPreviewType, @@ -13,25 +12,32 @@ import { CallingButton, CallingButtonType } from './CallingButton'; import { TooltipPlacement } from './Tooltip'; import { CallBackgroundBlur } from './CallBackgroundBlur'; import { CallingHeader } from './CallingHeader'; -import { Spinner } from './Spinner'; +import { CallingPreCallInfo } from './CallingPreCallInfo'; +import { + CallingLobbyJoinButton, + CallingLobbyJoinButtonVariant, +} from './CallingLobbyJoinButton'; import { AvatarColorType } from '../types/Colors'; import { LocalizerType } from '../types/Util'; import { ConversationType } from '../state/ducks/conversations'; -import { - REQUESTED_VIDEO_WIDTH, - REQUESTED_VIDEO_HEIGHT, -} from '../calling/constants'; - -// We request dimensions but may not get them depending on the user's webcam. This is our -// fallback while we don't know. -const VIDEO_ASPECT_RATIO_FALLBACK = - REQUESTED_VIDEO_WIDTH / REQUESTED_VIDEO_HEIGHT; export type PropsType = { availableCameras: Array; - conversation: { - title: string; - }; + conversation: Pick< + ConversationType, + | 'acceptedMessageRequest' + | 'avatarPath' + | 'color' + | 'isMe' + | 'name' + | 'phoneNumber' + | 'profileName' + | 'sharedGroupNames' + | 'title' + | 'type' + | 'unblurredAvatarPath' + >; + groupMembers?: Array>; hasLocalAudio: boolean; hasLocalVideo: boolean; i18n: LocalizerType; @@ -56,6 +62,7 @@ export type PropsType = { export const CallingLobby = ({ availableCameras, conversation, + groupMembers, hasLocalAudio, hasLocalVideo, i18n, @@ -72,19 +79,10 @@ export const CallingLobby = ({ toggleParticipants, toggleSettings, }: PropsType): JSX.Element => { - const [ - localPreviewContainerWidth, - setLocalPreviewContainerWidth, - ] = React.useState(null); - const [ - localPreviewContainerHeight, - setLocalPreviewContainerHeight, - ] = React.useState(null); - const [localVideoAspectRatio, setLocalVideoAspectRatio] = React.useState( - VIDEO_ASPECT_RATIO_FALLBACK - ); const localVideoRef = React.useRef(null); + const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0; + const toggleAudio = React.useCallback((): void => { setLocalAudio({ enabled: !hasLocalAudio }); }, [hasLocalAudio, setLocalAudio]); @@ -93,24 +91,6 @@ export const CallingLobby = ({ setLocalVideo({ enabled: !hasLocalVideo }); }, [hasLocalVideo, setLocalVideo]); - const hasEverMeasured = - localPreviewContainerWidth !== null && localPreviewContainerHeight !== null; - const setLocalPreviewContainerDimensions = React.useMemo(() => { - const set = (bounds: Readonly<{ width: number; height: number }>) => { - setLocalPreviewContainerWidth(bounds.width); - setLocalPreviewContainerHeight(bounds.height); - }; - - if (hasEverMeasured) { - return debounce(set, 100, { maxWait: 3000 }); - } - return set; - }, [ - hasEverMeasured, - setLocalPreviewContainerWidth, - setLocalPreviewContainerHeight, - ]); - React.useEffect(() => { setLocalPreview({ element: localVideoRef }); @@ -119,21 +99,6 @@ export const CallingLobby = ({ }; }, [setLocalPreview]); - // This isn't perfect because it doesn't react to changes in the webcam's aspect ratio. - // For example, if you changed from Webcam A to Webcam B and Webcam B had a different - // aspect ratio, we wouldn't update. - // - // Unfortunately, RingRTC (1) doesn't update these dimensions with the "real" camera - // dimensions (2) doesn't give us any hooks or callbacks. For now, this works okay. - // We have `object-fit: contain` in the CSS in case we're wrong; not ideal, but - // usable. - React.useEffect(() => { - const videoEl = localVideoRef.current; - if (hasLocalVideo && videoEl && videoEl.width && videoEl.height) { - setLocalVideoAspectRatio(videoEl.width / videoEl.height); - } - }, [hasLocalVideo, setLocalVideoAspectRatio]); - React.useEffect(() => { function handleKeyDown(event: KeyboardEvent): void { let eventHandled = false; @@ -171,179 +136,89 @@ export const CallingLobby = ({ ? CallingButtonType.AUDIO_ON : CallingButtonType.AUDIO_OFF; - // It should be rare to see yourself in this list, but it's possible if (1) you rejoin - // quickly, causing the server to return stale state (2) you have joined on another - // device. - const participantNames = peekedParticipants.map(participant => - participant.uuid === me.uuid - ? i18n('you') - : participant.firstName || participant.title - ); - const hasYou = peekedParticipants.some( - participant => participant.uuid === me.uuid - ); - const canJoin = !isCallFull && !isCallConnecting; - let joinButtonChildren: ReactNode; + let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant; if (isCallFull) { - joinButtonChildren = i18n('calling__call-is-full'); + callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.CallIsFull; } else if (isCallConnecting) { - joinButtonChildren = ; + callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Loading; } else if (peekedParticipants.length) { - joinButtonChildren = i18n('calling__join'); + callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Join; } else { - joinButtonChildren = i18n('calling__start'); - } - - let localPreviewStyles: React.CSSProperties; - // It'd be nice to use `hasEverMeasured` here, too, but TypeScript isn't smart enough - // to understand the logic here. - if ( - localPreviewContainerWidth !== null && - localPreviewContainerHeight !== null - ) { - const containerAspectRatio = - localPreviewContainerWidth / localPreviewContainerHeight; - localPreviewStyles = - containerAspectRatio < localVideoAspectRatio - ? { - width: '100%', - height: Math.floor( - localPreviewContainerWidth / localVideoAspectRatio - ), - } - : { - width: Math.floor( - localPreviewContainerHeight * localVideoAspectRatio - ), - height: '100%', - }; - } else { - localPreviewStyles = { display: 'none' }; + callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Start; } return (
+ {shouldShowLocalVideo ? ( +