Update call lobby UI to match new designs
This commit is contained in:
parent
50c4fa06cc
commit
763c35e546
26 changed files with 857 additions and 435 deletions
|
@ -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"
|
||||
|
|
|
@ -222,6 +222,10 @@
|
|||
text-align: inherit;
|
||||
}
|
||||
|
||||
@mixin calling-text-shadow {
|
||||
text-shadow: 0 0 4px $color-black-alpha-40;
|
||||
}
|
||||
|
||||
// --- Buttons
|
||||
|
||||
// Individual traits
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
48
stylesheets/components/CallingLobby.scss
Normal file
48
stylesheets/components/CallingLobby.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
6
stylesheets/components/CallingLobbyJoinButton.scss
Normal file
6
stylesheets/components/CallingLobbyJoinButton.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-CallingLobbyJoinButton {
|
||||
margin-bottom: 32px;
|
||||
}
|
29
stylesheets/components/CallingPreCallInfo.scss
Normal file
29
stylesheets/components/CallingPreCallInfo.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -19,6 +19,7 @@ story.add('Kitchen sink', () => (
|
|||
ButtonVariant.SecondaryAffirmative,
|
||||
ButtonVariant.SecondaryDestructive,
|
||||
ButtonVariant.Destructive,
|
||||
ButtonVariant.Calling,
|
||||
].map(variant => (
|
||||
<React.Fragment key={variant}>
|
||||
<p>
|
||||
|
@ -50,3 +51,9 @@ story.add('aria-label', () => (
|
|||
onClick={action('onClick')}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Custom styles', () => (
|
||||
<Button onClick={action('onClick')} style={{ transform: 'rotate(5deg)' }}>
|
||||
Hello world
|
||||
</Button>
|
||||
));
|
||||
|
|
|
@ -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<ButtonVariant, string>([
|
|||
'module-Button--secondary module-Button--secondary--destructive',
|
||||
],
|
||||
[ButtonVariant.Destructive, 'module-Button--destructive'],
|
||||
[ButtonVariant.Calling, 'module-Button--calling'],
|
||||
]);
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
||||
|
@ -73,6 +77,8 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
|||
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<HTMLButtonElement, PropsType>(
|
|||
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}
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
className={classNames('module-calling__background', {
|
||||
className={classNames(
|
||||
'module-calling__background',
|
||||
{
|
||||
[`module-background-color__${color || 'default'}`]: !avatarPath,
|
||||
})}
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{avatarPath && (
|
||||
<div
|
||||
|
|
|
@ -137,6 +137,7 @@ story.add('Ongoing Group Call', () => (
|
|||
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: [],
|
||||
},
|
||||
|
|
|
@ -163,6 +163,9 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
|
||||
let isCallFull: boolean;
|
||||
let showCallLobby: boolean;
|
||||
let groupMembers:
|
||||
| undefined
|
||||
| Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
||||
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
|
@ -182,11 +185,13 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
}
|
||||
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<ActiveCallManagerPropsType> = ({
|
|||
<CallingLobby
|
||||
availableCameras={availableCameras}
|
||||
conversation={conversation}
|
||||
groupMembers={groupMembers}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
|
|
|
@ -94,6 +94,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
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:
|
||||
|
|
|
@ -313,7 +313,6 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
||||
>
|
||||
<CallingHeader
|
||||
canPip
|
||||
i18n={i18n}
|
||||
isInSpeakerView={isInSpeakerView}
|
||||
isGroupCall={isGroupCall}
|
||||
|
@ -357,11 +356,8 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
sharedGroupNames={[]}
|
||||
size={80}
|
||||
/>
|
||||
<div className="module-calling__video-off--container">
|
||||
<div className="module-calling__video-off--icon" />
|
||||
<span className="module-calling__video-off--text">
|
||||
<div className="module-calling__camera-is-off">
|
||||
{i18n('calling__your-video-is-off')}
|
||||
</span>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<div className="module-calling-button__container">
|
||||
<button
|
||||
aria-label={tooltipContent}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
id={uniqueButtonId}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<div />
|
||||
</button>
|
||||
<label
|
||||
className="module-calling-button__label"
|
||||
htmlFor={uniqueButtonId}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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> = {}): 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', () => <CallingHeader {...createProps()} />);
|
||||
|
||||
story.add('Has Pip', () => (
|
||||
<CallingHeader {...createProps({ canPip: true })} />
|
||||
story.add('Lobby style', () => (
|
||||
<CallingHeader
|
||||
{...createProps()}
|
||||
title={undefined}
|
||||
togglePip={undefined}
|
||||
onCancel={action('onClose')}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('With Participants', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
canPip: true,
|
||||
isGroupCall: true,
|
||||
participantCount: 10,
|
||||
})}
|
||||
|
@ -52,7 +55,6 @@ story.add('With Participants', () => (
|
|||
story.add('With Participants (shown)', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
canPip: true,
|
||||
isGroupCall: true,
|
||||
participantCount: 10,
|
||||
showParticipantsList: true,
|
||||
|
|
|
@ -8,11 +8,11 @@ import { Tooltip } from './Tooltip';
|
|||
import { Theme } from '../util/theme';
|
||||
|
||||
export type PropsType = {
|
||||
canPip?: boolean;
|
||||
i18n: LocalizerType;
|
||||
isInSpeakerView?: boolean;
|
||||
isGroupCall?: boolean;
|
||||
message?: string;
|
||||
onCancel?: () => 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 = ({
|
|||
<div className="module-ongoing-call__header-message">{message}</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
{participantCount ? (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('calling__participants', [String(participantCount)])}
|
||||
|
@ -111,7 +111,7 @@ export const CallingHeader = ({
|
|||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{canPip && (
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}>
|
||||
<button
|
||||
|
@ -123,6 +123,18 @@ export const CallingHeader = ({
|
|||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{onCancel && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('cancel')} theme={Theme.Dark}>
|
||||
<button
|
||||
aria-label={i18n('cancel')}
|
||||
className="module-calling-button__cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,15 +27,34 @@ const camera = {
|
|||
},
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||
const isGroupCall = boolean(
|
||||
'isGroupCall',
|
||||
overrideProps.isGroupCall || false
|
||||
);
|
||||
const conversation = isGroupCall
|
||||
? getDefaultConversation({
|
||||
title: 'Tahoe Trip',
|
||||
type: 'group',
|
||||
})
|
||||
: getDefaultConversation();
|
||||
|
||||
return {
|
||||
availableCameras: overrideProps.availableCameras || [camera],
|
||||
conversation: {
|
||||
title: 'Rick Sanchez',
|
||||
},
|
||||
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
conversation,
|
||||
groupMembers: isGroupCall
|
||||
? times(3, () => getDefaultConversation())
|
||||
: undefined,
|
||||
hasLocalAudio: boolean(
|
||||
'hasLocalAudio',
|
||||
overrideProps.hasLocalAudio || false
|
||||
),
|
||||
hasLocalVideo: boolean(
|
||||
'hasLocalVideo',
|
||||
overrideProps.hasLocalVideo || false
|
||||
),
|
||||
i18n,
|
||||
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
|
||||
isGroupCall,
|
||||
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
||||
me: overrideProps.me || {
|
||||
color: AvatarColors[0],
|
||||
|
@ -52,7 +72,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
|
||||
getDefaultConversation({
|
||||
|
@ -123,26 +144,6 @@ story.add('Group Call - 1 peeked participant (self)', () => {
|
|||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 2 peeked participants', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
peekedParticipants: ['Sam', 'Cayce'].map(title =>
|
||||
fakePeekedParticipant({ title })
|
||||
),
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 3 peeked participants', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
peekedParticipants: ['Sam', 'Cayce', 'April'].map(title =>
|
||||
fakePeekedParticipant({ title })
|
||||
),
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 4 peeked participants', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
|
|
|
@ -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<MediaDeviceInfo>;
|
||||
conversation: {
|
||||
title: string;
|
||||
};
|
||||
conversation: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
groupMembers?: Array<Pick<ConversationType, 'title'>>;
|
||||
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 | number>(null);
|
||||
const [
|
||||
localPreviewContainerHeight,
|
||||
setLocalPreviewContainerHeight,
|
||||
] = React.useState<null | number>(null);
|
||||
const [localVideoAspectRatio, setLocalVideoAspectRatio] = React.useState(
|
||||
VIDEO_ASPECT_RATIO_FALLBACK
|
||||
);
|
||||
const localVideoRef = React.useRef<null | HTMLVideoElement>(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,105 +136,66 @@ 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 = <Spinner svgSize="small" />;
|
||||
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 (
|
||||
<div className="module-calling__container">
|
||||
{shouldShowLocalVideo ? (
|
||||
<video
|
||||
className="module-CallingLobby__local-preview"
|
||||
ref={localVideoRef}
|
||||
autoPlay
|
||||
/>
|
||||
) : (
|
||||
<CallBackgroundBlur
|
||||
className="module-CallingLobby__local-preview"
|
||||
avatarPath={me.avatarPath}
|
||||
color={me.color}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CallingHeader
|
||||
title={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
participantCount={peekedParticipants.length}
|
||||
showParticipantsList={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
onCancel={onCallCanceled}
|
||||
/>
|
||||
|
||||
<Measure
|
||||
bounds
|
||||
onResize={({ bounds }) => {
|
||||
if (!bounds) {
|
||||
window.log.error('We should be measuring bounds');
|
||||
return;
|
||||
}
|
||||
setLocalPreviewContainerDimensions(bounds);
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="module-calling-lobby__local-preview-container"
|
||||
>
|
||||
<div
|
||||
className="module-calling-lobby__local-preview"
|
||||
style={localPreviewStyles}
|
||||
>
|
||||
{hasLocalVideo && availableCameras.length > 0 ? (
|
||||
<video
|
||||
className="module-calling-lobby__local-preview__video-on"
|
||||
ref={localVideoRef}
|
||||
autoPlay
|
||||
<CallingPreCallInfo
|
||||
conversation={conversation}
|
||||
groupMembers={groupMembers}
|
||||
i18n={i18n}
|
||||
isCallFull={isCallFull}
|
||||
me={me}
|
||||
peekedParticipants={peekedParticipants}
|
||||
/>
|
||||
) : (
|
||||
<CallBackgroundBlur avatarPath={me.avatarPath} color={me.color}>
|
||||
<div className="module-calling-lobby__local-preview__video-off__icon" />
|
||||
<span className="module-calling-lobby__local-preview__video-off__text">
|
||||
{i18n('calling__your-video-is-off')}
|
||||
</span>
|
||||
</CallBackgroundBlur>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'module-CallingLobby__camera-is-off',
|
||||
`module-CallingLobby__camera-is-off--${
|
||||
shouldShowLocalVideo ? 'invisible' : 'visible'
|
||||
}`
|
||||
)}
|
||||
>
|
||||
{i18n('calling__your-video-is-off')}
|
||||
</div>
|
||||
|
||||
<div className="module-calling__buttons">
|
||||
<div className="module-calling__buttons module-calling__buttons--inline">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
|
@ -283,67 +209,16 @@ export const CallingLobby = ({
|
|||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
|
||||
{isGroupCall ? (
|
||||
<div className="module-calling-lobby__info">
|
||||
{participantNames.length === 0 &&
|
||||
i18n('calling__lobby-summary--zero')}
|
||||
{participantNames.length === 1 &&
|
||||
hasYou &&
|
||||
i18n('calling__lobby-summary--self')}
|
||||
{participantNames.length === 1 &&
|
||||
!hasYou &&
|
||||
i18n('calling__lobby-summary--single', participantNames)}
|
||||
{participantNames.length === 2 &&
|
||||
i18n('calling__lobby-summary--double', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
})}
|
||||
{participantNames.length === 3 &&
|
||||
i18n('calling__lobby-summary--triple', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
third: participantNames[2],
|
||||
})}
|
||||
{participantNames.length > 3 &&
|
||||
i18n('calling__lobby-summary--many', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
others: String(participantNames.length - 2),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="module-calling-lobby__actions">
|
||||
<button
|
||||
className="module-button__gray module-calling-lobby__button"
|
||||
onClick={onCallCanceled}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
<CallingLobbyJoinButton
|
||||
disabled={!canJoin}
|
||||
onClick={
|
||||
canJoin
|
||||
? () => {
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{joinButtonChildren}
|
||||
</button>
|
||||
</div>
|
||||
}}
|
||||
variant={callingLobbyJoinButtonVariant}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
107
ts/components/CallingLobbyJoinButton.tsx
Normal file
107
ts/components/CallingLobbyJoinButton.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent, ReactChild, useState } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
const PADDING_HORIZONTAL = 48;
|
||||
const PADDING_VERTICAL = 12;
|
||||
|
||||
export enum CallingLobbyJoinButtonVariant {
|
||||
CallIsFull = 'CallIsFull',
|
||||
Join = 'Join',
|
||||
Loading = 'Loading',
|
||||
Start = 'Start',
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a little weird. Why not just render a button with some children?
|
||||
*
|
||||
* The contents of this component can change but we don't want its size to change, so we
|
||||
* render all the variants invisibly, compute the maximum size, and then render the
|
||||
* "final" button with those dimensions.
|
||||
*
|
||||
* For example, we might initially render "Join call" and then render a spinner when you
|
||||
* click the button. The button shouldn't resize in that situation.
|
||||
*/
|
||||
export const CallingLobbyJoinButton: FunctionComponent<{
|
||||
disabled?: boolean;
|
||||
i18n: LocalizerType;
|
||||
onClick: () => void;
|
||||
variant: CallingLobbyJoinButtonVariant;
|
||||
}> = ({ disabled, i18n, onClick, variant }) => {
|
||||
const [width, setWidth] = useState<undefined | number>();
|
||||
const [height, setHeight] = useState<undefined | number>();
|
||||
|
||||
const childrenByVariant: Record<CallingLobbyJoinButtonVariant, ReactChild> = {
|
||||
[CallingLobbyJoinButtonVariant.CallIsFull]: i18n('calling__call-is-full'),
|
||||
[CallingLobbyJoinButtonVariant.Loading]: <Spinner svgSize="small" />,
|
||||
[CallingLobbyJoinButtonVariant.Join]: i18n('calling__join'),
|
||||
[CallingLobbyJoinButtonVariant.Start]: i18n('calling__start'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(width && height) && (
|
||||
<Button
|
||||
className="module-CallingLobbyJoinButton"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={{ width, height }}
|
||||
tabIndex={0}
|
||||
variant={ButtonVariant.Calling}
|
||||
>
|
||||
{childrenByVariant[variant]}
|
||||
</Button>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'fixed',
|
||||
left: -9999,
|
||||
top: -9999,
|
||||
}}
|
||||
>
|
||||
{Object.values(CallingLobbyJoinButtonVariant).map(candidateVariant => (
|
||||
<Button
|
||||
key={candidateVariant}
|
||||
className="module-CallingLobbyJoinButton"
|
||||
variant={ButtonVariant.Calling}
|
||||
onClick={noop}
|
||||
ref={(button: HTMLButtonElement | null) => {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
width: variantWidth,
|
||||
height: variantHeight,
|
||||
} = button.getBoundingClientRect();
|
||||
|
||||
// We could set the padding in CSS, but we don't do that in case some other
|
||||
// styling causes a re-render of the button but not of the component. This
|
||||
// is easiest to reproduce in Storybook, where the font hasn't loaded yet;
|
||||
// we compute the size, then the font makes the text a bit larger, and
|
||||
// there's a layout issue.
|
||||
setWidth((previousWidth = 0) =>
|
||||
Math.ceil(
|
||||
Math.max(previousWidth, variantWidth + PADDING_HORIZONTAL)
|
||||
)
|
||||
);
|
||||
setHeight((previousHeight = 0) =>
|
||||
Math.ceil(
|
||||
Math.max(previousHeight, variantHeight + PADDING_VERTICAL)
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{childrenByVariant[candidateVariant]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -2,6 +2,7 @@
|
|||
// 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';
|
||||
|
@ -109,6 +110,7 @@ story.add('Group Call', () => {
|
|||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
groupMembers: times(3, () => getDefaultConversation()),
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
|
|
100
ts/components/CallingPreCallInfo.stories.tsx
Normal file
100
ts/components/CallingPreCallInfo.stories.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { times, range } from 'lodash';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
||||
import { CallingPreCallInfo } from './CallingPreCallInfo';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
const getDefaultGroupConversation = () =>
|
||||
getDefaultConversation({
|
||||
name: 'Tahoe Trip',
|
||||
phoneNumber: undefined,
|
||||
profileName: undefined,
|
||||
title: 'Tahoe Trip',
|
||||
type: 'group',
|
||||
});
|
||||
const otherMembers = times(6, () => getDefaultConversation());
|
||||
|
||||
const story = storiesOf('Components/CallingPreCallInfo', module);
|
||||
|
||||
story.add('Direct conversation', () => (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultConversation()}
|
||||
i18n={i18n}
|
||||
me={getDefaultConversation()}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group conversation, empty group', () => (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultGroupConversation()}
|
||||
groupMembers={[]}
|
||||
i18n={i18n}
|
||||
me={getDefaultConversation()}
|
||||
peekedParticipants={[]}
|
||||
/>
|
||||
));
|
||||
|
||||
times(5, numberOfOtherPeople => {
|
||||
story.add(
|
||||
`Group conversation, group has ${numberOfOtherPeople} other member${
|
||||
numberOfOtherPeople === 1 ? '' : 's'
|
||||
}`,
|
||||
() => (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultGroupConversation()}
|
||||
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
|
||||
i18n={i18n}
|
||||
me={getDefaultConversation()}
|
||||
peekedParticipants={[]}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
range(1, 5).forEach(numberOfOtherPeople => {
|
||||
story.add(
|
||||
`Group conversation, ${numberOfOtherPeople} peeked participant${
|
||||
numberOfOtherPeople === 1 ? '' : 's'
|
||||
}`,
|
||||
() => (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultGroupConversation()}
|
||||
groupMembers={otherMembers}
|
||||
i18n={i18n}
|
||||
me={getDefaultConversation()}
|
||||
peekedParticipants={otherMembers.slice(0, numberOfOtherPeople)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Group conversation, you on an other device', () => {
|
||||
const me = getDefaultConversation();
|
||||
return (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultGroupConversation()}
|
||||
groupMembers={otherMembers}
|
||||
i18n={i18n}
|
||||
me={me}
|
||||
peekedParticipants={[me]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Group conversation, call is full', () => (
|
||||
<CallingPreCallInfo
|
||||
conversation={getDefaultGroupConversation()}
|
||||
groupMembers={otherMembers}
|
||||
i18n={i18n}
|
||||
isCallFull
|
||||
me={getDefaultConversation()}
|
||||
peekedParticipants={otherMembers}
|
||||
/>
|
||||
));
|
159
ts/components/CallingPreCallInfo.tsx
Normal file
159
ts/components/CallingPreCallInfo.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
type PropsType = {
|
||||
conversation: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
i18n: LocalizerType;
|
||||
me: Pick<ConversationType, 'uuid'>;
|
||||
|
||||
// The following should only be set for group conversations.
|
||||
groupMembers?: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
||||
isCallFull?: boolean;
|
||||
peekedParticipants?: Array<
|
||||
Pick<ConversationType, 'firstName' | 'title' | 'uuid'>
|
||||
>;
|
||||
};
|
||||
|
||||
export const CallingPreCallInfo: FunctionComponent<PropsType> = ({
|
||||
conversation,
|
||||
groupMembers = [],
|
||||
i18n,
|
||||
isCallFull = false,
|
||||
me,
|
||||
peekedParticipants = [],
|
||||
}) => {
|
||||
let subtitle: string;
|
||||
if (isCallFull) {
|
||||
subtitle = i18n('calling__call-is-full');
|
||||
} else if (peekedParticipants.length) {
|
||||
// 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.
|
||||
let hasYou = false;
|
||||
const participantNames = peekedParticipants.map(participant => {
|
||||
if (participant.uuid === me.uuid) {
|
||||
hasYou = true;
|
||||
return i18n('you');
|
||||
}
|
||||
return getParticipantName(participant);
|
||||
});
|
||||
|
||||
switch (participantNames.length) {
|
||||
case 1:
|
||||
subtitle = hasYou
|
||||
? i18n('calling__pre-call-info--another-device-in-call')
|
||||
: i18n('calling__pre-call-info--1-person-in-call', participantNames);
|
||||
break;
|
||||
case 2:
|
||||
subtitle = i18n('calling__pre-call-info--2-people-in-call', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
subtitle = i18n('calling__pre-call-info--3-people-in-call', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
third: participantNames[2],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
subtitle = i18n('calling__pre-call-info--many-people-in-call', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
others: String(participantNames.length - 2),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else if (conversation.type === 'direct') {
|
||||
subtitle = i18n('calling__pre-call-info--will-ring-1', [
|
||||
getParticipantName(conversation),
|
||||
]);
|
||||
} else if (conversation.type === 'group') {
|
||||
const memberNames = groupMembers.map(getParticipantName);
|
||||
|
||||
switch (memberNames.length) {
|
||||
case 0:
|
||||
subtitle = i18n('calling__pre-call-info--empty-group');
|
||||
break;
|
||||
case 1:
|
||||
subtitle = i18n('calling__pre-call-info--will-notify-1', [
|
||||
memberNames[0],
|
||||
]);
|
||||
break;
|
||||
case 2:
|
||||
subtitle = i18n('calling__pre-call-info--will-notify-2', {
|
||||
first: memberNames[0],
|
||||
second: memberNames[1],
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
subtitle = i18n('calling__pre-call-info--will-notify-3', {
|
||||
first: memberNames[0],
|
||||
second: memberNames[1],
|
||||
third: memberNames[2],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
subtitle = i18n('calling__pre-call-info--will-notify-many', {
|
||||
first: memberNames[0],
|
||||
second: memberNames[1],
|
||||
others: String(memberNames.length - 2),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
throw missingCaseError(conversation.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-CallingPreCallInfo">
|
||||
<Avatar
|
||||
avatarPath={conversation.avatarPath}
|
||||
color={conversation.color}
|
||||
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||
conversationType={conversation.type}
|
||||
isMe={conversation.isMe}
|
||||
name={conversation.name}
|
||||
noteToSelf={false}
|
||||
phoneNumber={conversation.phoneNumber}
|
||||
profileName={conversation.profileName}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
size={AvatarSize.ONE_HUNDRED_TWELVE}
|
||||
title={conversation.title}
|
||||
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<div className="module-CallingPreCallInfo__title">
|
||||
<Emojify text={conversation.title} />
|
||||
</div>
|
||||
<div className="module-CallingPreCallInfo__subtitle">{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getParticipantName(
|
||||
participant: Readonly<Pick<ConversationType, 'firstName' | 'title'>>
|
||||
): string {
|
||||
return participant.firstName || participant.title;
|
||||
}
|
|
@ -115,9 +115,23 @@ const mapStateToActiveCallProp = (
|
|||
};
|
||||
case CallMode.Group: {
|
||||
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
||||
const groupMembers: Array<ConversationType> = [];
|
||||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||
const peekedParticipants: Array<ConversationType> = [];
|
||||
|
||||
const { memberships = [] } = conversation;
|
||||
for (let i = 0; i < memberships.length; i += 1) {
|
||||
const { conversationId } = memberships[i];
|
||||
const member = conversationSelectorByUuid(conversationId);
|
||||
|
||||
if (!member) {
|
||||
window.log.error('Group member has no corresponding conversation');
|
||||
continue;
|
||||
}
|
||||
|
||||
groupMembers.push(member);
|
||||
}
|
||||
|
||||
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
|
||||
const remoteParticipant = call.remoteParticipants[i];
|
||||
|
||||
|
@ -183,6 +197,7 @@ const mapStateToActiveCallProp = (
|
|||
connectionState: call.connectionState,
|
||||
conversationsWithSafetyNumberChanges,
|
||||
deviceCount: call.peekInfo.deviceCount,
|
||||
groupMembers,
|
||||
joinState: call.joinState,
|
||||
maxDevices: call.peekInfo.maxDevices,
|
||||
peekedParticipants,
|
||||
|
|
|
@ -60,6 +60,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
|
|||
joinState: GroupCallJoinState;
|
||||
maxDevices: number;
|
||||
deviceCount: number;
|
||||
groupMembers: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
||||
peekedParticipants: Array<ConversationType>;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue