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": {
|
"calling__call-is-full": {
|
||||||
"message": "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": {
|
"calling__button--video-disabled": {
|
||||||
"message": "Camera disabled",
|
"message": "Camera disabled",
|
||||||
|
@ -1314,6 +1318,10 @@
|
||||||
"message": "Turn on camera",
|
"message": "Turn on camera",
|
||||||
"description": "Button tooltip label for turning on the 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": {
|
"calling__button--audio-disabled": {
|
||||||
"message": "Microphone disabled",
|
"message": "Microphone disabled",
|
||||||
"description": "Button tooltip label when the microphone is disabled"
|
"description": "Button tooltip label when the microphone is disabled"
|
||||||
|
@ -1326,6 +1334,10 @@
|
||||||
"message": "Unmute mic",
|
"message": "Unmute mic",
|
||||||
"description": "Button tooltip label for turning on the microphone"
|
"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": {
|
"calling__button--presenting-disabled": {
|
||||||
"message": "Presenting disabled",
|
"message": "Presenting disabled",
|
||||||
"description": "Button tooltip label for when screen sharing is disabled"
|
"description": "Button tooltip label for when screen sharing is disabled"
|
||||||
|
@ -1342,11 +1354,11 @@
|
||||||
"message": "Your camera is off",
|
"message": "Your camera is off",
|
||||||
"description": "Label in the calling lobby indicating that your camera is off"
|
"description": "Label in the calling lobby indicating that your camera is off"
|
||||||
},
|
},
|
||||||
"calling__lobby-summary--zero": {
|
"calling__pre-call-info--empty-group": {
|
||||||
"message": "No one else is here",
|
"message": "No one else is here",
|
||||||
"description": "Shown in the calling lobby to describe who is in the call"
|
"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",
|
"message": "$first$ is in this call",
|
||||||
"description": "Shown in the calling lobby to describe who is in the call",
|
"description": "Shown in the calling lobby to describe who is in the call",
|
||||||
"placeholders": {
|
"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",
|
"message": "One of your other devices is in this call",
|
||||||
"description": "Shown in the calling lobby to describe when it is just you"
|
"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",
|
"message": "$first$ and $second$ are in this call",
|
||||||
"description": "Shown in the calling lobby to describe who is in the call",
|
"description": "Shown in the calling lobby to describe who is in the call",
|
||||||
"placeholders": {
|
"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",
|
"message": "$first$, $second$, and $third$ are in this call",
|
||||||
"description": "Shown in the calling lobby to describe who is in the call",
|
"description": "Shown in the calling lobby to describe who is in the call",
|
||||||
"placeholders": {
|
"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",
|
"message": "$first$, $second$, and $others$ others are in this call",
|
||||||
"description": "Shown in the calling lobby to describe who is in the call",
|
"description": "Shown in the calling lobby to describe who is in the call",
|
||||||
"placeholders": {
|
"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": {
|
"calling__in-this-call--zero": {
|
||||||
"message": "No one else is here",
|
"message": "No one else is here",
|
||||||
"description": "Shown in the participants list to describe how many people are in the call"
|
"description": "Shown in the participants list to describe how many people are in the call"
|
||||||
|
|
|
@ -222,6 +222,10 @@
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin calling-text-shadow {
|
||||||
|
text-shadow: 0 0 4px $color-black-alpha-40;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Buttons
|
// --- Buttons
|
||||||
|
|
||||||
// Individual traits
|
// Individual traits
|
||||||
|
|
|
@ -5201,7 +5201,7 @@ button.module-image__border-overlay:focus {
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
padding-top: calc(24px + var(--title-bar-drag-area-height));
|
padding-top: calc(24px + var(--title-bar-drag-area-height));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
|
@include calling-text-shadow;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&--header-name {
|
&--header-name {
|
||||||
|
@ -5224,6 +5224,10 @@ button.module-image__border-overlay:focus {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&--inline {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__background {
|
&__background {
|
||||||
|
@ -5237,7 +5241,6 @@ button.module-image__border-overlay:focus {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&--blur {
|
&--blur {
|
||||||
position: absolute;
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
@ -5248,31 +5251,24 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__video-off {
|
&__camera-is-off {
|
||||||
&--icon {
|
@include calling-text-shadow;
|
||||||
|
@include font-body-1;
|
||||||
|
color: $color-white;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
'../images/icons/v2/video-off-solid-24.svg',
|
'../images/icons/v2/video-off-solid-24.svg',
|
||||||
$color-white
|
$color-white
|
||||||
);
|
);
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-bottom: 8px;
|
margin-right: 10px;
|
||||||
width: 24px;
|
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;
|
height: $size;
|
||||||
width: $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 {
|
.module-calling-button__icon {
|
||||||
border-radius: 56px;
|
border-radius: 52px;
|
||||||
height: 56px;
|
height: 52px;
|
||||||
width: 56px;
|
width: 52px;
|
||||||
|
|
||||||
@mixin calling-button-icon($icon, $background-color, $icon-color) {
|
@mixin calling-button-icon($icon, $background-color, $icon-color) {
|
||||||
background-color: $background-color;
|
background-color: $background-color;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@include color-svg($icon, $icon-color);
|
@include color-svg($icon, $icon-color);
|
||||||
height: 28px;
|
height: 24px;
|
||||||
width: 28px;
|
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 {
|
@keyframes module-ongoing-call__controls--fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
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 {
|
.module-calling-pip {
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
background-color: $color-gray-95;
|
background-color: $color-gray-95;
|
||||||
|
@ -9547,40 +9486,6 @@ button.module-image__border-overlay:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 7px 12px;
|
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
|
// Module: Group Contact Details
|
||||||
|
|
|
@ -134,4 +134,26 @@
|
||||||
@include hover-and-active-states($background-color, $color-white);
|
@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/AvatarTextEditor.scss';
|
||||||
@import './components/BetterAvatarBubble.scss';
|
@import './components/BetterAvatarBubble.scss';
|
||||||
@import './components/Button.scss';
|
@import './components/Button.scss';
|
||||||
|
@import './components/CallingLobby.scss';
|
||||||
|
@import './components/CallingLobbyJoinButton.scss';
|
||||||
|
@import './components/CallingPreCallInfo.scss';
|
||||||
@import './components/CallingScreenSharingController.scss';
|
@import './components/CallingScreenSharingController.scss';
|
||||||
@import './components/CallingSelectPresentingSourcesModal.scss';
|
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||||
@import './components/ChatColorPicker.scss';
|
@import './components/ChatColorPicker.scss';
|
||||||
|
|
|
@ -19,6 +19,7 @@ story.add('Kitchen sink', () => (
|
||||||
ButtonVariant.SecondaryAffirmative,
|
ButtonVariant.SecondaryAffirmative,
|
||||||
ButtonVariant.SecondaryDestructive,
|
ButtonVariant.SecondaryDestructive,
|
||||||
ButtonVariant.Destructive,
|
ButtonVariant.Destructive,
|
||||||
|
ButtonVariant.Calling,
|
||||||
].map(variant => (
|
].map(variant => (
|
||||||
<React.Fragment key={variant}>
|
<React.Fragment key={variant}>
|
||||||
<p>
|
<p>
|
||||||
|
@ -50,3 +51,9 @@ story.add('aria-label', () => (
|
||||||
onClick={action('onClick')}
|
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
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 classNames from 'classnames';
|
||||||
|
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
|
@ -17,12 +17,15 @@ export enum ButtonVariant {
|
||||||
SecondaryAffirmative,
|
SecondaryAffirmative,
|
||||||
SecondaryDestructive,
|
SecondaryDestructive,
|
||||||
Destructive,
|
Destructive,
|
||||||
|
Calling,
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
|
style?: CSSProperties;
|
||||||
|
tabIndex?: number;
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
|
@ -64,6 +67,7 @@ const VARIANT_CLASS_NAMES = new Map<ButtonVariant, string>([
|
||||||
'module-Button--secondary module-Button--secondary--destructive',
|
'module-Button--secondary module-Button--secondary--destructive',
|
||||||
],
|
],
|
||||||
[ButtonVariant.Destructive, 'module-Button--destructive'],
|
[ButtonVariant.Destructive, 'module-Button--destructive'],
|
||||||
|
[ButtonVariant.Calling, 'module-Button--calling'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
||||||
|
@ -73,6 +77,8 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
||||||
className,
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Medium,
|
||||||
|
style,
|
||||||
|
tabIndex,
|
||||||
variant = ButtonVariant.Primary,
|
variant = ButtonVariant.Primary,
|
||||||
} = props;
|
} = props;
|
||||||
const ariaLabel = props['aria-label'];
|
const ariaLabel = props['aria-label'];
|
||||||
|
@ -105,6 +111,8 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
tabIndex={tabIndex}
|
||||||
// The `type` should either be "button" or "submit", which is effectively static.
|
// The `type` should either be "button" or "submit", which is effectively static.
|
||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
type={type}
|
type={type}
|
||||||
|
|
|
@ -7,20 +7,26 @@ import { AvatarColorType } from '../types/Colors';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
children: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
color?: AvatarColorType;
|
color?: AvatarColorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CallBackgroundBlur = ({
|
export const CallBackgroundBlur = ({
|
||||||
avatarPath,
|
avatarPath,
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
color,
|
color,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('module-calling__background', {
|
className={classNames(
|
||||||
|
'module-calling__background',
|
||||||
|
{
|
||||||
[`module-background-color__${color || 'default'}`]: !avatarPath,
|
[`module-background-color__${color || 'default'}`]: !avatarPath,
|
||||||
})}
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{avatarPath && (
|
{avatarPath && (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -137,6 +137,7 @@ story.add('Ongoing Group Call', () => (
|
||||||
deviceCount: 0,
|
deviceCount: 0,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
|
groupMembers: [],
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
remoteParticipants: [],
|
remoteParticipants: [],
|
||||||
},
|
},
|
||||||
|
@ -189,6 +190,7 @@ story.add('Group call - Safety Number Changed', () => (
|
||||||
deviceCount: 0,
|
deviceCount: 0,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
|
groupMembers: [],
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
remoteParticipants: [],
|
remoteParticipants: [],
|
||||||
},
|
},
|
||||||
|
|
|
@ -163,6 +163,9 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
|
|
||||||
let isCallFull: boolean;
|
let isCallFull: boolean;
|
||||||
let showCallLobby: boolean;
|
let showCallLobby: boolean;
|
||||||
|
let groupMembers:
|
||||||
|
| undefined
|
||||||
|
| Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
||||||
|
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct: {
|
case CallMode.Direct: {
|
||||||
|
@ -182,11 +185,13 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
}
|
}
|
||||||
showCallLobby = !callState;
|
showCallLobby = !callState;
|
||||||
isCallFull = false;
|
isCallFull = false;
|
||||||
|
groupMembers = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CallMode.Group: {
|
case CallMode.Group: {
|
||||||
showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
|
showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
|
||||||
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
||||||
|
({ groupMembers } = activeCall);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -199,6 +204,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
||||||
<CallingLobby
|
<CallingLobby
|
||||||
availableCameras={availableCameras}
|
availableCameras={availableCameras}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
groupMembers={groupMembers}
|
||||||
hasLocalAudio={hasLocalAudio}
|
hasLocalAudio={hasLocalAudio}
|
||||||
hasLocalVideo={hasLocalVideo}
|
hasLocalVideo={hasLocalVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -94,6 +94,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
deviceCount: (overrideProps.remoteParticipants || []).length,
|
deviceCount: (overrideProps.remoteParticipants || []).length,
|
||||||
|
groupMembers: overrideProps.remoteParticipants || [],
|
||||||
// Because remote participants are a superset, we can use them in place of peeked
|
// Because remote participants are a superset, we can use them in place of peeked
|
||||||
// participants.
|
// participants.
|
||||||
peekedParticipants:
|
peekedParticipants:
|
||||||
|
|
|
@ -313,7 +313,6 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
||||||
>
|
>
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
canPip
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInSpeakerView={isInSpeakerView}
|
isInSpeakerView={isInSpeakerView}
|
||||||
isGroupCall={isGroupCall}
|
isGroupCall={isGroupCall}
|
||||||
|
@ -357,11 +356,8 @@ export const CallScreen: React.FC<PropsType> = ({
|
||||||
sharedGroupNames={[]}
|
sharedGroupNames={[]}
|
||||||
size={80}
|
size={80}
|
||||||
/>
|
/>
|
||||||
<div className="module-calling__video-off--container">
|
<div className="module-calling__camera-is-off">
|
||||||
<div className="module-calling__video-off--icon" />
|
|
||||||
<span className="module-calling__video-off--text">
|
|
||||||
{i18n('calling__your-video-is-off')}
|
{i18n('calling__your-video-is-off')}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</CallBackgroundBlur>
|
</CallBackgroundBlur>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
@ -33,42 +34,55 @@ export const CallingButton = ({
|
||||||
onClick,
|
onClick,
|
||||||
tooltipDirection,
|
tooltipDirection,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
const uniqueButtonId = useMemo(() => uuid(), []);
|
||||||
|
|
||||||
let classNameSuffix = '';
|
let classNameSuffix = '';
|
||||||
let tooltipContent = '';
|
let tooltipContent = '';
|
||||||
|
let label = '';
|
||||||
let disabled = false;
|
let disabled = false;
|
||||||
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
||||||
classNameSuffix = 'audio--disabled';
|
classNameSuffix = 'audio--disabled';
|
||||||
tooltipContent = i18n('calling__button--audio-disabled');
|
tooltipContent = i18n('calling__button--audio-disabled');
|
||||||
|
label = i18n('calling__button--audio__label');
|
||||||
disabled = true;
|
disabled = true;
|
||||||
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
||||||
classNameSuffix = 'audio--off';
|
classNameSuffix = 'audio--off';
|
||||||
tooltipContent = i18n('calling__button--audio-on');
|
tooltipContent = i18n('calling__button--audio-on');
|
||||||
|
label = i18n('calling__button--audio__label');
|
||||||
} else if (buttonType === CallingButtonType.AUDIO_ON) {
|
} else if (buttonType === CallingButtonType.AUDIO_ON) {
|
||||||
classNameSuffix = 'audio--on';
|
classNameSuffix = 'audio--on';
|
||||||
tooltipContent = i18n('calling__button--audio-off');
|
tooltipContent = i18n('calling__button--audio-off');
|
||||||
|
label = i18n('calling__button--audio__label');
|
||||||
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
||||||
classNameSuffix = 'video--disabled';
|
classNameSuffix = 'video--disabled';
|
||||||
tooltipContent = i18n('calling__button--video-disabled');
|
tooltipContent = i18n('calling__button--video-disabled');
|
||||||
disabled = true;
|
disabled = true;
|
||||||
|
label = i18n('calling__button--video__label');
|
||||||
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
||||||
classNameSuffix = 'video--off';
|
classNameSuffix = 'video--off';
|
||||||
tooltipContent = i18n('calling__button--video-on');
|
tooltipContent = i18n('calling__button--video-on');
|
||||||
|
label = i18n('calling__button--video__label');
|
||||||
} else if (buttonType === CallingButtonType.VIDEO_ON) {
|
} else if (buttonType === CallingButtonType.VIDEO_ON) {
|
||||||
classNameSuffix = 'video--on';
|
classNameSuffix = 'video--on';
|
||||||
tooltipContent = i18n('calling__button--video-off');
|
tooltipContent = i18n('calling__button--video-off');
|
||||||
|
label = i18n('calling__button--video__label');
|
||||||
} else if (buttonType === CallingButtonType.HANG_UP) {
|
} else if (buttonType === CallingButtonType.HANG_UP) {
|
||||||
classNameSuffix = 'hangup';
|
classNameSuffix = 'hangup';
|
||||||
tooltipContent = i18n('calling__hangup');
|
tooltipContent = i18n('calling__hangup');
|
||||||
|
label = i18n('calling__hangup');
|
||||||
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
||||||
classNameSuffix = 'presenting--disabled';
|
classNameSuffix = 'presenting--disabled';
|
||||||
tooltipContent = i18n('calling__button--presenting-disabled');
|
tooltipContent = i18n('calling__button--presenting-disabled');
|
||||||
disabled = true;
|
disabled = true;
|
||||||
|
label = i18n('calling__button--presenting__label');
|
||||||
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
|
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
|
||||||
classNameSuffix = 'presenting--on';
|
classNameSuffix = 'presenting--on';
|
||||||
tooltipContent = i18n('calling__button--presenting-off');
|
tooltipContent = i18n('calling__button--presenting-off');
|
||||||
|
label = i18n('calling__button--presenting__label');
|
||||||
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
|
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
|
||||||
classNameSuffix = 'presenting--off';
|
classNameSuffix = 'presenting--off';
|
||||||
tooltipContent = i18n('calling__button--presenting-on');
|
tooltipContent = i18n('calling__button--presenting-on');
|
||||||
|
label = i18n('calling__button--presenting__label');
|
||||||
}
|
}
|
||||||
|
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
|
@ -82,15 +96,24 @@ export const CallingButton = ({
|
||||||
direction={tooltipDirection}
|
direction={tooltipDirection}
|
||||||
theme={Theme.Dark}
|
theme={Theme.Dark}
|
||||||
>
|
>
|
||||||
|
<div className="module-calling-button__container">
|
||||||
<button
|
<button
|
||||||
aria-label={tooltipContent}
|
aria-label={tooltipContent}
|
||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
id={uniqueButtonId}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</button>
|
</button>
|
||||||
|
<label
|
||||||
|
className="module-calling-button__label"
|
||||||
|
htmlFor={uniqueButtonId}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -13,7 +13,6 @@ import enMessages from '../../_locales/en/messages.json';
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
canPip: boolean('canPip', Boolean(overrideProps.canPip)),
|
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)),
|
isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)),
|
||||||
message: overrideProps.message,
|
message: overrideProps.message,
|
||||||
|
@ -35,14 +34,18 @@ const story = storiesOf('Components/CallingHeader', module);
|
||||||
|
|
||||||
story.add('Default', () => <CallingHeader {...createProps()} />);
|
story.add('Default', () => <CallingHeader {...createProps()} />);
|
||||||
|
|
||||||
story.add('Has Pip', () => (
|
story.add('Lobby style', () => (
|
||||||
<CallingHeader {...createProps({ canPip: true })} />
|
<CallingHeader
|
||||||
|
{...createProps()}
|
||||||
|
title={undefined}
|
||||||
|
togglePip={undefined}
|
||||||
|
onCancel={action('onClose')}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
story.add('With Participants', () => (
|
story.add('With Participants', () => (
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
{...createProps({
|
{...createProps({
|
||||||
canPip: true,
|
|
||||||
isGroupCall: true,
|
isGroupCall: true,
|
||||||
participantCount: 10,
|
participantCount: 10,
|
||||||
})}
|
})}
|
||||||
|
@ -52,7 +55,6 @@ story.add('With Participants', () => (
|
||||||
story.add('With Participants (shown)', () => (
|
story.add('With Participants (shown)', () => (
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
{...createProps({
|
{...createProps({
|
||||||
canPip: true,
|
|
||||||
isGroupCall: true,
|
isGroupCall: true,
|
||||||
participantCount: 10,
|
participantCount: 10,
|
||||||
showParticipantsList: true,
|
showParticipantsList: true,
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { Tooltip } from './Tooltip';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
canPip?: boolean;
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isInSpeakerView?: boolean;
|
isInSpeakerView?: boolean;
|
||||||
isGroupCall?: boolean;
|
isGroupCall?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
onCancel?: () => void;
|
||||||
participantCount: number;
|
participantCount: number;
|
||||||
showParticipantsList: boolean;
|
showParticipantsList: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -23,11 +23,11 @@ export type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CallingHeader = ({
|
export const CallingHeader = ({
|
||||||
canPip = false,
|
|
||||||
i18n,
|
i18n,
|
||||||
isInSpeakerView,
|
isInSpeakerView,
|
||||||
isGroupCall = false,
|
isGroupCall = false,
|
||||||
message,
|
message,
|
||||||
|
onCancel,
|
||||||
participantCount,
|
participantCount,
|
||||||
showParticipantsList,
|
showParticipantsList,
|
||||||
title,
|
title,
|
||||||
|
@ -44,7 +44,7 @@ export const CallingHeader = ({
|
||||||
<div className="module-ongoing-call__header-message">{message}</div>
|
<div className="module-ongoing-call__header-message">{message}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="module-calling-tools">
|
<div className="module-calling-tools">
|
||||||
{isGroupCall ? (
|
{participantCount ? (
|
||||||
<div className="module-calling-tools__button">
|
<div className="module-calling-tools__button">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={i18n('calling__participants', [String(participantCount)])}
|
content={i18n('calling__participants', [String(participantCount)])}
|
||||||
|
@ -111,7 +111,7 @@ export const CallingHeader = ({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canPip && (
|
{togglePip && (
|
||||||
<div className="module-calling-tools__button">
|
<div className="module-calling-tools__button">
|
||||||
<Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}>
|
<Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}>
|
||||||
<button
|
<button
|
||||||
|
@ -123,6 +123,18 @@ export const CallingHeader = ({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { times } from 'lodash';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { boolean } from '@storybook/addon-knobs';
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
@ -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],
|
availableCameras: overrideProps.availableCameras || [camera],
|
||||||
conversation: {
|
conversation,
|
||||||
title: 'Rick Sanchez',
|
groupMembers: isGroupCall
|
||||||
},
|
? times(3, () => getDefaultConversation())
|
||||||
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
|
: undefined,
|
||||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
hasLocalAudio: boolean(
|
||||||
|
'hasLocalAudio',
|
||||||
|
overrideProps.hasLocalAudio || false
|
||||||
|
),
|
||||||
|
hasLocalVideo: boolean(
|
||||||
|
'hasLocalVideo',
|
||||||
|
overrideProps.hasLocalVideo || false
|
||||||
|
),
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
|
isGroupCall,
|
||||||
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
|
||||||
me: overrideProps.me || {
|
me: overrideProps.me || {
|
||||||
color: AvatarColors[0],
|
color: AvatarColors[0],
|
||||||
|
@ -52,7 +72,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
),
|
),
|
||||||
toggleParticipants: action('toggle-participants'),
|
toggleParticipants: action('toggle-participants'),
|
||||||
toggleSettings: action('toggle-settings'),
|
toggleSettings: action('toggle-settings'),
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
|
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
|
||||||
getDefaultConversation({
|
getDefaultConversation({
|
||||||
|
@ -123,26 +144,6 @@ story.add('Group Call - 1 peeked participant (self)', () => {
|
||||||
return <CallingLobby {...props} />;
|
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', () => {
|
story.add('Group Call - 4 peeked participants', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
isGroupCall: true,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { ReactNode } from 'react';
|
import React from 'react';
|
||||||
import Measure from 'react-measure';
|
import classNames from 'classnames';
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import {
|
import {
|
||||||
SetLocalAudioType,
|
SetLocalAudioType,
|
||||||
SetLocalPreviewType,
|
SetLocalPreviewType,
|
||||||
|
@ -13,25 +12,32 @@ import { CallingButton, CallingButtonType } from './CallingButton';
|
||||||
import { TooltipPlacement } from './Tooltip';
|
import { TooltipPlacement } from './Tooltip';
|
||||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||||
import { CallingHeader } from './CallingHeader';
|
import { CallingHeader } from './CallingHeader';
|
||||||
import { Spinner } from './Spinner';
|
import { CallingPreCallInfo } from './CallingPreCallInfo';
|
||||||
|
import {
|
||||||
|
CallingLobbyJoinButton,
|
||||||
|
CallingLobbyJoinButtonVariant,
|
||||||
|
} from './CallingLobbyJoinButton';
|
||||||
import { AvatarColorType } from '../types/Colors';
|
import { AvatarColorType } from '../types/Colors';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
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 = {
|
export type PropsType = {
|
||||||
availableCameras: Array<MediaDeviceInfo>;
|
availableCameras: Array<MediaDeviceInfo>;
|
||||||
conversation: {
|
conversation: Pick<
|
||||||
title: string;
|
ConversationType,
|
||||||
};
|
| 'acceptedMessageRequest'
|
||||||
|
| 'avatarPath'
|
||||||
|
| 'color'
|
||||||
|
| 'isMe'
|
||||||
|
| 'name'
|
||||||
|
| 'phoneNumber'
|
||||||
|
| 'profileName'
|
||||||
|
| 'sharedGroupNames'
|
||||||
|
| 'title'
|
||||||
|
| 'type'
|
||||||
|
| 'unblurredAvatarPath'
|
||||||
|
>;
|
||||||
|
groupMembers?: Array<Pick<ConversationType, 'title'>>;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -56,6 +62,7 @@ export type PropsType = {
|
||||||
export const CallingLobby = ({
|
export const CallingLobby = ({
|
||||||
availableCameras,
|
availableCameras,
|
||||||
conversation,
|
conversation,
|
||||||
|
groupMembers,
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -72,19 +79,10 @@ export const CallingLobby = ({
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
}: PropsType): JSX.Element => {
|
}: 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 localVideoRef = React.useRef<null | HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
|
||||||
|
|
||||||
const toggleAudio = React.useCallback((): void => {
|
const toggleAudio = React.useCallback((): void => {
|
||||||
setLocalAudio({ enabled: !hasLocalAudio });
|
setLocalAudio({ enabled: !hasLocalAudio });
|
||||||
}, [hasLocalAudio, setLocalAudio]);
|
}, [hasLocalAudio, setLocalAudio]);
|
||||||
|
@ -93,24 +91,6 @@ export const CallingLobby = ({
|
||||||
setLocalVideo({ enabled: !hasLocalVideo });
|
setLocalVideo({ enabled: !hasLocalVideo });
|
||||||
}, [hasLocalVideo, setLocalVideo]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
setLocalPreview({ element: localVideoRef });
|
setLocalPreview({ element: localVideoRef });
|
||||||
|
|
||||||
|
@ -119,21 +99,6 @@ export const CallingLobby = ({
|
||||||
};
|
};
|
||||||
}, [setLocalPreview]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
function handleKeyDown(event: KeyboardEvent): void {
|
function handleKeyDown(event: KeyboardEvent): void {
|
||||||
let eventHandled = false;
|
let eventHandled = false;
|
||||||
|
@ -171,105 +136,66 @@ export const CallingLobby = ({
|
||||||
? CallingButtonType.AUDIO_ON
|
? CallingButtonType.AUDIO_ON
|
||||||
: CallingButtonType.AUDIO_OFF;
|
: 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;
|
const canJoin = !isCallFull && !isCallConnecting;
|
||||||
|
|
||||||
let joinButtonChildren: ReactNode;
|
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
|
||||||
if (isCallFull) {
|
if (isCallFull) {
|
||||||
joinButtonChildren = i18n('calling__call-is-full');
|
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.CallIsFull;
|
||||||
} else if (isCallConnecting) {
|
} else if (isCallConnecting) {
|
||||||
joinButtonChildren = <Spinner svgSize="small" />;
|
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Loading;
|
||||||
} else if (peekedParticipants.length) {
|
} else if (peekedParticipants.length) {
|
||||||
joinButtonChildren = i18n('calling__join');
|
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Join;
|
||||||
} else {
|
} else {
|
||||||
joinButtonChildren = i18n('calling__start');
|
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.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' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-calling__container">
|
<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
|
<CallingHeader
|
||||||
title={conversation.title}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupCall={isGroupCall}
|
isGroupCall={isGroupCall}
|
||||||
participantCount={peekedParticipants.length}
|
participantCount={peekedParticipants.length}
|
||||||
showParticipantsList={showParticipantsList}
|
showParticipantsList={showParticipantsList}
|
||||||
toggleParticipants={toggleParticipants}
|
toggleParticipants={toggleParticipants}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
|
onCancel={onCallCanceled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Measure
|
<CallingPreCallInfo
|
||||||
bounds
|
conversation={conversation}
|
||||||
onResize={({ bounds }) => {
|
groupMembers={groupMembers}
|
||||||
if (!bounds) {
|
i18n={i18n}
|
||||||
window.log.error('We should be measuring bounds');
|
isCallFull={isCallFull}
|
||||||
return;
|
me={me}
|
||||||
}
|
peekedParticipants={peekedParticipants}
|
||||||
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
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<CallBackgroundBlur avatarPath={me.avatarPath} color={me.color}>
|
<div
|
||||||
<div className="module-calling-lobby__local-preview__video-off__icon" />
|
className={classNames(
|
||||||
<span className="module-calling-lobby__local-preview__video-off__text">
|
'module-CallingLobby__camera-is-off',
|
||||||
{i18n('calling__your-video-is-off')}
|
`module-CallingLobby__camera-is-off--${
|
||||||
</span>
|
shouldShowLocalVideo ? 'invisible' : 'visible'
|
||||||
</CallBackgroundBlur>
|
}`
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
{i18n('calling__your-video-is-off')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="module-calling__buttons">
|
<div className="module-calling__buttons module-calling__buttons--inline">
|
||||||
<CallingButton
|
<CallingButton
|
||||||
buttonType={videoButtonType}
|
buttonType={videoButtonType}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -283,67 +209,16 @@ export const CallingLobby = ({
|
||||||
tooltipDirection={TooltipPlacement.Top}
|
tooltipDirection={TooltipPlacement.Top}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Measure>
|
|
||||||
|
|
||||||
{isGroupCall ? (
|
<CallingLobbyJoinButton
|
||||||
<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"
|
|
||||||
disabled={!canJoin}
|
disabled={!canJoin}
|
||||||
onClick={
|
i18n={i18n}
|
||||||
canJoin
|
onClick={() => {
|
||||||
? () => {
|
|
||||||
setIsCallConnecting(true);
|
setIsCallConnecting(true);
|
||||||
onJoinCall();
|
onJoinCall();
|
||||||
}
|
}}
|
||||||
: undefined
|
variant={callingLobbyJoinButtonVariant}
|
||||||
}
|
/>
|
||||||
tabIndex={0}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{joinButtonChildren}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { times } from 'lodash';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { boolean } from '@storybook/addon-knobs';
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
@ -109,6 +110,7 @@ story.add('Group Call', () => {
|
||||||
callMode: CallMode.Group as CallMode.Group,
|
callMode: CallMode.Group as CallMode.Group,
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
conversationsWithSafetyNumberChanges: [],
|
conversationsWithSafetyNumberChanges: [],
|
||||||
|
groupMembers: times(3, () => getDefaultConversation()),
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
deviceCount: 0,
|
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: {
|
case CallMode.Group: {
|
||||||
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
||||||
|
const groupMembers: Array<ConversationType> = [];
|
||||||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||||
const peekedParticipants: Array<ConversationType> = [];
|
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) {
|
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
|
||||||
const remoteParticipant = call.remoteParticipants[i];
|
const remoteParticipant = call.remoteParticipants[i];
|
||||||
|
|
||||||
|
@ -183,6 +197,7 @@ const mapStateToActiveCallProp = (
|
||||||
connectionState: call.connectionState,
|
connectionState: call.connectionState,
|
||||||
conversationsWithSafetyNumberChanges,
|
conversationsWithSafetyNumberChanges,
|
||||||
deviceCount: call.peekInfo.deviceCount,
|
deviceCount: call.peekInfo.deviceCount,
|
||||||
|
groupMembers,
|
||||||
joinState: call.joinState,
|
joinState: call.joinState,
|
||||||
maxDevices: call.peekInfo.maxDevices,
|
maxDevices: call.peekInfo.maxDevices,
|
||||||
peekedParticipants,
|
peekedParticipants,
|
||||||
|
|
|
@ -60,6 +60,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
|
||||||
joinState: GroupCallJoinState;
|
joinState: GroupCallJoinState;
|
||||||
maxDevices: number;
|
maxDevices: number;
|
||||||
deviceCount: number;
|
deviceCount: number;
|
||||||
|
groupMembers: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
|
||||||
peekedParticipants: Array<ConversationType>;
|
peekedParticipants: Array<ConversationType>;
|
||||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue