New call UI and controls
This commit is contained in:
parent
33c5c683c7
commit
8bb355f971
22 changed files with 741 additions and 360 deletions
|
@ -1646,7 +1646,7 @@
|
|||
},
|
||||
"icu:calling__start": {
|
||||
"messageformat": "Start Call",
|
||||
"description": "Button label in the call lobby for starting a call"
|
||||
"description": "(deleted 2023/10/13) Button label in the call lobby for starting a call"
|
||||
},
|
||||
"icu:calling__join": {
|
||||
"messageformat": "Join Call",
|
||||
|
@ -1668,9 +1668,21 @@
|
|||
"messageformat": "Call is full",
|
||||
"description": "Text in the call lobby when you can't join because the call is full"
|
||||
},
|
||||
"icu:CallingLobbyJoinButton--join": {
|
||||
"messageformat": "Join",
|
||||
"description": "Button label in the call lobby for joining a call"
|
||||
},
|
||||
"icu:CallingLobbyJoinButton--start": {
|
||||
"messageformat": "Start",
|
||||
"description": "Button label in the call lobby for starting a call"
|
||||
},
|
||||
"icu:CallingLobbyJoinButton--call-full": {
|
||||
"messageformat": "Call full",
|
||||
"description": "Button in the call lobby when you can't join because the call is full"
|
||||
},
|
||||
"icu:calling__button--video__label": {
|
||||
"messageformat": "Camera",
|
||||
"description": "Label under the video button"
|
||||
"description": "(deleted 2023/10/13) Label under the video button"
|
||||
},
|
||||
"icu:calling__button--video-disabled": {
|
||||
"messageformat": "Camera disabled",
|
||||
|
@ -1686,7 +1698,7 @@
|
|||
},
|
||||
"icu:calling__button--audio__label": {
|
||||
"messageformat": "Mute",
|
||||
"description": "Label under the audio button"
|
||||
"description": "(deleted 2023/10/13) Label under the audio button"
|
||||
},
|
||||
"icu:calling__button--audio-disabled": {
|
||||
"messageformat": "Microphone disabled",
|
||||
|
@ -1702,7 +1714,7 @@
|
|||
},
|
||||
"icu:calling__button--presenting__label": {
|
||||
"messageformat": "Share",
|
||||
"description": "Label under the share screen button"
|
||||
"description": "(deleted 2023/10/13) Label under the share screen button"
|
||||
},
|
||||
"icu:calling__button--presenting-disabled": {
|
||||
"messageformat": "Presenting disabled",
|
||||
|
@ -1718,7 +1730,7 @@
|
|||
},
|
||||
"icu:calling__button--ring__label": {
|
||||
"messageformat": "Ring",
|
||||
"description": "Label under the ring button"
|
||||
"description": "(deleted 2023/10/13) Label under the ring button"
|
||||
},
|
||||
"icu:calling__button--ring__disabled-because-group-is-too-large": {
|
||||
"messageformat": "Group is too large to ring the participants.",
|
||||
|
@ -1732,6 +1744,14 @@
|
|||
"messageformat": "Enable ringing",
|
||||
"description": "Button tooltip label for turning ringing on"
|
||||
},
|
||||
"icu:CallingButton__ring-off": {
|
||||
"messageformat": "Turn off ringing",
|
||||
"description": "Button tooltip label for turning ringing off"
|
||||
},
|
||||
"icu:CallingButton--ring-on": {
|
||||
"messageformat": "Turn on ringing",
|
||||
"description": "Button tooltip label for turning ringing on"
|
||||
},
|
||||
"icu:calling__your-video-is-off": {
|
||||
"messageformat": "Your camera is off",
|
||||
"description": "Label in the calling lobby indicating that your camera is off"
|
||||
|
@ -3509,7 +3529,39 @@
|
|||
},
|
||||
"icu:callDuration": {
|
||||
"messageformat": "Signal {duration}",
|
||||
"description": "Shown in the call screen to indicate how long the call has been connected"
|
||||
"description": "(deleted 2023/10/13) Shown in the call screen to indicate how long the call has been connected"
|
||||
},
|
||||
"icu:CallControls__InfoDisplay--participants": {
|
||||
"messageformat": "{count, plural, one {# person} other {# people}}",
|
||||
"description": "Shown in the call screen and lobby for group calls to specify the number of members in the call or in the group. Count is at always at least 1."
|
||||
},
|
||||
"icu:CallControls__InfoDisplay--audio-call": {
|
||||
"messageformat": "Audio call",
|
||||
"description": "Shown in the call lobby for a direct 1:1 call when the caller's video is disabled, to specify that an audio call will be placed when clicking the Start button."
|
||||
},
|
||||
"icu:CallControls__JoinLeaveButton--hangup-1-1": {
|
||||
"messageformat": "End",
|
||||
"description": "Title for the hangup button for a direct 1:1 call with only 2 participants."
|
||||
},
|
||||
"icu:CallControls__JoinLeaveButton--hangup-group": {
|
||||
"messageformat": "Leave",
|
||||
"description": "Title for the hangup button for a group call."
|
||||
},
|
||||
"icu:CallControls__MutedToast--muted": {
|
||||
"messageformat": "Mic off",
|
||||
"description": "Shown in a call when the user mutes their audio input using the Mute toggle button."
|
||||
},
|
||||
"icu:CallControls__MutedToast--unmuted": {
|
||||
"messageformat": "Mic on",
|
||||
"description": "Shown in a call when the user is muted and then unmutes their audio input using the Mute toggle button."
|
||||
},
|
||||
"icu:CallControls__RingingToast--ringing-on": {
|
||||
"messageformat": "Ringing on",
|
||||
"description": "Shown in a group call lobby when call ringing is disabled, then the user enables ringing using the Ringing toggle button."
|
||||
},
|
||||
"icu:CallControls__RingingToast--ringing-off": {
|
||||
"messageformat": "Ringing off",
|
||||
"description": "Shown in a group call lobby when call ringing is enabled, then the user disables ringing using the Ringing toggle button."
|
||||
},
|
||||
"icu:callingDeviceSelection__settings": {
|
||||
"messageformat": "Settings",
|
||||
|
|
|
@ -18,6 +18,7 @@ $color-gray-60: #5e5e5e;
|
|||
$color-gray-62: #545454;
|
||||
$color-gray-65: #4a4a4a;
|
||||
$color-gray-75: #3b3b3b;
|
||||
$color-gray-78: #343434;
|
||||
$color-gray-80: #2e2e2e;
|
||||
$color-gray-90: #1b1b1b;
|
||||
$color-gray-95: #121212;
|
||||
|
|
|
@ -3568,21 +3568,6 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 32px;
|
||||
padding-top: 32px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&--inline {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
&__background {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -3648,8 +3633,8 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
.module-ongoing-call {
|
||||
$local-preview-width: 136px;
|
||||
$local-preview-height: 102px;
|
||||
$local-preview-width: 107px;
|
||||
$local-preview-height: 80px;
|
||||
|
||||
&__remote-video-enabled {
|
||||
background-color: $color-gray-95;
|
||||
|
@ -3817,7 +3802,7 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
// stylelint-disable-next-line declaration-property-value-disallowed-list
|
||||
transform: translate(0, 0);
|
||||
transition: transform 200ms linear, width 200ms linear, height 200ms linear;
|
||||
|
@ -3829,7 +3814,7 @@ button.module-image__border-overlay:focus {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0 solid transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
transition-property: border-width, border-color;
|
||||
// Turn on the transition when the user stops speaking to fade out.
|
||||
transition-duration: 300ms;
|
||||
|
@ -3918,14 +3903,13 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
&__local-preview-fullsize {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
inset-inline-start: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: $z-index-negative;
|
||||
|
||||
video {
|
||||
|
@ -3959,14 +3943,15 @@ button.module-image__border-overlay:focus {
|
|||
&__local-preview-offset {
|
||||
flex: 1 0;
|
||||
max-width: $local-preview-width;
|
||||
margin-inline-start: 16px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__local-preview {
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
height: $local-preview-height;
|
||||
margin-block: 2px 16px;
|
||||
margin-block-end: 16px;
|
||||
margin-inline: 0 16px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
@ -4020,7 +4005,11 @@ button.module-image__border-overlay:focus {
|
|||
width: 100%;
|
||||
|
||||
&__button {
|
||||
margin-inline-end: 25px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
&__button:last-child {
|
||||
margin-inline-end: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4118,24 +4107,32 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
.module-calling-participants-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 320px;
|
||||
height: 440px;
|
||||
background-color: $color-gray-80;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
color: $color-white;
|
||||
margin-inline-end: 12px;
|
||||
margin-top: 54px;
|
||||
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
||||
margin-inline-end: 340px;
|
||||
margin-block-end: 85px;
|
||||
margin-block-start: 20px;
|
||||
outline: 1px solid $color-gray-62;
|
||||
overflow: hidden;
|
||||
padding: 14px;
|
||||
width: 280px;
|
||||
padding-bottom: 0;
|
||||
padding-block: 5px 0;
|
||||
padding-inline: 5px;
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: var(--window-width);
|
||||
height: var(--window-height);
|
||||
justify-content: flex-end;
|
||||
inset-inline-start: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: $z-index-calling;
|
||||
}
|
||||
|
||||
|
@ -4149,36 +4146,34 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
&__list {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
margin-inline: -14px;
|
||||
margin-top: 22px;
|
||||
overflow: scroll;
|
||||
padding-bottom: 24px;
|
||||
padding-inline: 14px;
|
||||
padding-top: 0;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding-block: 0;
|
||||
padding-inline: 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: $color-gray-45;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: $color-gray-80;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__contact {
|
||||
@include font-body-1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-block: 2px;
|
||||
padding-block: 5px;
|
||||
padding-inline: 10px;
|
||||
list-style-type: none;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 6px;
|
||||
&:hover {
|
||||
background-color: $color-gray-62;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-and-name {
|
||||
|
@ -4189,6 +4184,7 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
&__name {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
margin-inline-start: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -4199,6 +4195,9 @@ button.module-image__border-overlay:focus {
|
|||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-block-end: 2px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 10px 5px;
|
||||
}
|
||||
|
||||
&__close {
|
||||
|
@ -4206,17 +4205,22 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
@include color-svg('../images/icons/v3/x/x.svg', $color-gray-15);
|
||||
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
z-index: $z-index-above-base;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
outline: 2px solid $color-ultramarine;
|
||||
background: $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
display: flex;
|
||||
flex-basis: 64px;
|
||||
}
|
||||
|
||||
&__muted {
|
||||
&--video {
|
||||
@include color-svg(
|
||||
|
@ -4224,9 +4228,9 @@ button.module-image__border-overlay:focus {
|
|||
$color-white
|
||||
);
|
||||
display: inline-block;
|
||||
margin-inline-start: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-inline-start: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&--audio {
|
||||
|
@ -4235,9 +4239,9 @@ button.module-image__border-overlay:focus {
|
|||
$color-white
|
||||
);
|
||||
display: inline-block;
|
||||
margin-inline-start: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-inline-start: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4247,9 +4251,9 @@ button.module-image__border-overlay:focus {
|
|||
$color-white
|
||||
);
|
||||
display: inline-block;
|
||||
margin-inline-start: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-inline-start: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ $color-gray-60: #5e5e5e;
|
|||
$color-gray-62: #545454;
|
||||
$color-gray-65: #4a4a4a;
|
||||
$color-gray-75: #3b3b3b;
|
||||
$color-gray-78: #343434;
|
||||
$color-gray-80: #2e2e2e;
|
||||
$color-gray-90: #1b1b1b;
|
||||
$color-gray-95: #121212;
|
||||
|
@ -253,7 +254,7 @@ $header-height: 52px;
|
|||
|
||||
$ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
||||
$calling-background-color: $color-gray-95;
|
||||
$calling-background-color: $color-gray-90;
|
||||
|
||||
// General
|
||||
|
||||
|
|
108
stylesheets/components/CallControls.scss
Normal file
108
stylesheets/components/CallControls.scss
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CallControls {
|
||||
position: static;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 480px;
|
||||
min-width: 480px;
|
||||
height: 80px;
|
||||
background-color: $color-gray-78;
|
||||
border-radius: 18px;
|
||||
margin-block-end: 16px;
|
||||
padding-block: 22px;
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.CallControls__InfoDisplay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.CallControls__CallTitle {
|
||||
display: flex;
|
||||
max-height: 40px;
|
||||
color: $color-gray-15;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.CallControls__Status {
|
||||
display: flex;
|
||||
min-height: 18px;
|
||||
max-height: 36px;
|
||||
color: $color-gray-20;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus-within {
|
||||
outline: 2px solid $color-ultramarine;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CallControls__Status--ParticipantCount {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
align-items: center;
|
||||
&::after {
|
||||
content: '';
|
||||
display: flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-inline-start: 1px;
|
||||
@include color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right.svg',
|
||||
$color-gray-20
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.CallControls__ButtonContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.CallControls__JoinLeaveButtonContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.CallControls__JoinLeaveButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
padding-block: 7px;
|
||||
padding-inline: 16px;
|
||||
border-radius: 40px;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px $color-gray-80, 0 0 0 3px $color-ultramarine !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CallControls__JoinLeaveButton--hangup {
|
||||
background-color: $color-accent-red;
|
||||
}
|
||||
|
||||
.CallControls__JoinLeaveButton .module-spinner__container {
|
||||
margin-block: -5px;
|
||||
}
|
53
stylesheets/components/CallSettingsButton.scss
Normal file
53
stylesheets/components/CallSettingsButton.scss
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@mixin CallSettingsButton-icon($path) {
|
||||
@include color-svg($path, $color-gray-15);
|
||||
}
|
||||
|
||||
.CallSettingsButton__Button {
|
||||
align-items: center;
|
||||
background-color: rgba($color-gray-80, 0.7);
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
width: 32px;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
outline-offset: 1px;
|
||||
outline: 2px solid $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon--Cancel {
|
||||
@include CallSettingsButton-icon('../images/icons/v3/x/x.svg');
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon--GridView {
|
||||
@include CallSettingsButton-icon('../images/icons/v3/grid/grid.svg');
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon--Pip {
|
||||
@include CallSettingsButton-icon('../images/icons/v3/pip/pip.svg');
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon--Settings {
|
||||
@include CallSettingsButton-icon('../images/icons/v3/settings/settings.svg');
|
||||
}
|
||||
|
||||
.CallSettingsButton__Icon--SpeakerView {
|
||||
@include CallSettingsButton-icon(
|
||||
'../images/icons/v3/speaker_view/speaker_view.svg'
|
||||
);
|
||||
}
|
|
@ -11,22 +11,28 @@
|
|||
|
||||
&__icon {
|
||||
align-items: center;
|
||||
border-radius: 52px;
|
||||
border-radius: 40px;
|
||||
border: none;
|
||||
display: flex;
|
||||
height: 52px;
|
||||
height: 36px;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
width: 52px;
|
||||
margin-inline: 6px;
|
||||
width: 36px;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
outline-offset: 1px;
|
||||
outline: 2px solid $color-ultramarine;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin calling-button-icon($icon, $background-color, $icon-color) {
|
||||
background-color: $background-color;
|
||||
|
||||
div {
|
||||
@include color-svg($icon, $icon-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,55 +117,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__participants {
|
||||
@include icon('../images/icons/v3/group/group.svg');
|
||||
display: inline-block;
|
||||
|
||||
&--container {
|
||||
@include button-reset;
|
||||
border: none;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&--shown {
|
||||
background-color: $color-gray-75;
|
||||
border-radius: 16px;
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
&--count {
|
||||
@include font-body-2;
|
||||
margin-inline-start: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&__settings {
|
||||
@include icon('../images/icons/v3/tune/tune.svg');
|
||||
}
|
||||
|
||||
&__grid-view {
|
||||
@include icon('../images/icons/v3/grid/grid.svg');
|
||||
}
|
||||
|
||||
&__speaker-view {
|
||||
@include icon('../images/icons/v3/speaker_view/speaker_view.svg');
|
||||
}
|
||||
|
||||
&__pip {
|
||||
@include icon('../images/icons/v3/pip/pip.svg');
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
@include icon('../images/icons/v3/x/x.svg');
|
||||
}
|
||||
|
||||
&__container {
|
||||
&__button-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin-inline: 6px;
|
||||
max-width: 64px;
|
||||
margin-inline: 10px;
|
||||
|
||||
transition: margin-inline-start 0.3s ease-out, opacity 0.3s ease-out;
|
||||
@media (prefers-reduced-motion) {
|
||||
|
@ -179,12 +141,40 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include font-subtitle;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
color: $color-white;
|
||||
@include calling-text-shadow;
|
||||
user-select: none;
|
||||
&__tooltip {
|
||||
background-color: $color-gray-80;
|
||||
color: $color-gray-15;
|
||||
font-size: 13px;
|
||||
outline: 1px solid $color-gray-62;
|
||||
padding-block: 5px;
|
||||
padding-inline: 12px;
|
||||
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
||||
}
|
||||
|
||||
&__tooltip .module-tooltip-arrow::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-style: solid;
|
||||
border-width: 7px;
|
||||
}
|
||||
|
||||
&__tooltip[data-placement='bottom'] .module-tooltip-arrow::before {
|
||||
border-color: transparent transparent $color-gray-62 transparent;
|
||||
margin-block-start: -14px;
|
||||
margin-inline-start: -7px;
|
||||
}
|
||||
|
||||
&__tooltip[data-placement='bottom'] .module-tooltip-arrow::after {
|
||||
border-bottom-color: $color-gray-80 !important;
|
||||
}
|
||||
|
||||
&__tooltip[data-placement='top'] .module-tooltip-arrow::before {
|
||||
border-color: $color-gray-62 transparent transparent transparent;
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: -7px;
|
||||
}
|
||||
|
||||
&__tooltip[data-placement='top'] .module-tooltip-arrow::after {
|
||||
border-top-color: $color-gray-80 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
&--camera-is-on {
|
||||
@include lonely-local-video-preview;
|
||||
top: 15px;
|
||||
height: 100%;
|
||||
max-height: calc(100% - 127px);
|
||||
width: auto;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-CallingLobbyJoinButton {
|
||||
margin-bottom: 32px;
|
||||
}
|
|
@ -38,8 +38,9 @@
|
|||
@import './components/CallsTab.scss';
|
||||
@import './components/CallingAudioIndicator.scss';
|
||||
@import './components/CallingButton.scss';
|
||||
@import './components/CallControls.scss';
|
||||
@import './components/CallSettingsButton.scss';
|
||||
@import './components/CallingLobby.scss';
|
||||
@import './components/CallingLobbyJoinButton.scss';
|
||||
@import './components/CallingPreCallInfo.scss';
|
||||
@import './components/CallingScreenSharingController.scss';
|
||||
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||
|
|
51
ts/components/CallParticipantCount.tsx
Normal file
51
ts/components/CallParticipantCount.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
groupMemberCount?: number;
|
||||
participantCount: number;
|
||||
toggleParticipants: () => void;
|
||||
};
|
||||
|
||||
export function CallParticipantCount({
|
||||
i18n,
|
||||
groupMemberCount,
|
||||
participantCount,
|
||||
toggleParticipants,
|
||||
}: PropsType): JSX.Element {
|
||||
const count = participantCount || groupMemberCount || 1;
|
||||
const innerText = i18n('icu:CallControls__InfoDisplay--participants', {
|
||||
count: String(count),
|
||||
});
|
||||
|
||||
// Call not started, can't click to show participants
|
||||
if (!participantCount) {
|
||||
return (
|
||||
<span
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(count),
|
||||
})}
|
||||
className="CallControls__Status--InactiveCallParticipantCount"
|
||||
>
|
||||
{innerText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(count),
|
||||
})}
|
||||
className="CallControls__Status--ParticipantCount"
|
||||
onClick={toggleParticipants}
|
||||
type="button"
|
||||
>
|
||||
{innerText}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -319,6 +319,18 @@ export function GroupCallMany(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function GroupCallSpeakerView(): JSX.Element {
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Group,
|
||||
viewMode: CallViewMode.Speaker,
|
||||
remoteParticipants: allRemoteParticipants.slice(0, 3),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallReconnecting(): JSX.Element {
|
||||
return (
|
||||
<CallScreen
|
||||
|
|
|
@ -17,6 +17,8 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { TooltipPlacement } from './Tooltip';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import type {
|
||||
ActiveCallType,
|
||||
|
@ -33,11 +35,13 @@ import {
|
|||
import { AvatarColors } from '../types/Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
useMutedToast,
|
||||
useReconnectingToast,
|
||||
useScreenSharingStoppedToast,
|
||||
} from './CallingToastManager';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
||||
import { CallParticipantCount } from './CallParticipantCount';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
@ -52,7 +56,7 @@ import {
|
|||
useKeyboardShortcuts,
|
||||
} from '../hooks/useKeyboardShortcuts';
|
||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -82,12 +86,6 @@ export type PropsType = {
|
|||
toggleSpeakerView: () => void;
|
||||
};
|
||||
|
||||
type DirectCallHeaderMessagePropsType = {
|
||||
i18n: LocalizerType;
|
||||
callState: CallState;
|
||||
joinedAt: number | null;
|
||||
};
|
||||
|
||||
export const isInSpeakerView = (
|
||||
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
|
||||
): boolean => {
|
||||
|
@ -97,11 +95,11 @@ export const isInSpeakerView = (
|
|||
);
|
||||
};
|
||||
|
||||
function DirectCallHeaderMessage({
|
||||
callState,
|
||||
i18n,
|
||||
function CallDuration({
|
||||
joinedAt,
|
||||
}: DirectCallHeaderMessagePropsType): JSX.Element | null {
|
||||
}: {
|
||||
joinedAt: number | null;
|
||||
}): JSX.Element | null {
|
||||
const [acceptedDuration, setAcceptedDuration] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
|
@ -117,14 +115,8 @@ function DirectCallHeaderMessage({
|
|||
return clearInterval.bind(null, interval);
|
||||
}, [joinedAt]);
|
||||
|
||||
if (callState === CallState.Accepted && acceptedDuration) {
|
||||
return (
|
||||
<>
|
||||
{i18n('icu:callDuration', {
|
||||
duration: renderDuration(acceptedDuration),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
if (acceptedDuration) {
|
||||
return <>{renderDuration(acceptedDuration)}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -161,7 +153,6 @@ export function CallScreen({
|
|||
presentingSource,
|
||||
remoteParticipants,
|
||||
showNeedsScreenRecordingPermissionsWarning,
|
||||
showParticipantsList,
|
||||
} = activeCall;
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(
|
||||
|
@ -260,6 +251,7 @@ export function CallScreen({
|
|||
};
|
||||
}, [toggleAudio, toggleVideo]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useReconnectingToast({ activeCall, i18n });
|
||||
useScreenSharingStoppedToast({ activeCall, i18n });
|
||||
|
||||
|
@ -272,10 +264,10 @@ export function CallScreen({
|
|||
);
|
||||
|
||||
const isSendingVideo = hasLocalVideo || presentingSource;
|
||||
const isReconnecting: boolean = callingIsReconnecting(activeCall);
|
||||
|
||||
let isRinging: boolean;
|
||||
let hasCallStarted: boolean;
|
||||
let headerMessage: ReactNode | undefined;
|
||||
let headerTitle: string | undefined;
|
||||
let isConnected: boolean;
|
||||
let participantCount: number;
|
||||
|
@ -287,14 +279,6 @@ export function CallScreen({
|
|||
activeCall.callState === CallState.Prering ||
|
||||
activeCall.callState === CallState.Ringing;
|
||||
hasCallStarted = !isRinging;
|
||||
headerMessage = (
|
||||
<DirectCallHeaderMessage
|
||||
i18n={i18n}
|
||||
callState={activeCall.callState || CallState.Prering}
|
||||
joinedAt={activeCall.joinedAt}
|
||||
/>
|
||||
);
|
||||
headerTitle = isRinging ? undefined : conversation.title;
|
||||
isConnected = activeCall.callState === CallState.Accepted;
|
||||
participantCount = isConnected ? 2 : 0;
|
||||
remoteParticipantsElement = hasCallStarted ? (
|
||||
|
@ -302,7 +286,7 @@ export function CallScreen({
|
|||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
isReconnecting={isReconnecting(activeCall)}
|
||||
isReconnecting={isReconnecting}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
) : (
|
||||
|
@ -338,7 +322,7 @@ export function CallScreen({
|
|||
remoteParticipants={activeCall.remoteParticipants}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
remoteAudioLevels={activeCall.remoteAudioLevels}
|
||||
isCallReconnecting={isReconnecting(activeCall)}
|
||||
isCallReconnecting={isReconnecting}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -454,6 +438,46 @@ export function CallScreen({
|
|||
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
||||
}
|
||||
|
||||
const callStatus: ReactNode | string = React.useMemo(() => {
|
||||
if (isRinging) {
|
||||
return i18n('icu:outgoingCallRinging');
|
||||
}
|
||||
if (isReconnecting) {
|
||||
return i18n('icu:callReconnecting');
|
||||
}
|
||||
if (isGroupCall) {
|
||||
return (
|
||||
<CallParticipantCount
|
||||
i18n={i18n}
|
||||
participantCount={participantCount}
|
||||
toggleParticipants={toggleParticipants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// joinedAt is only available for direct calls
|
||||
if (isConnected) {
|
||||
return <CallDuration joinedAt={activeCall.joinedAt} />;
|
||||
}
|
||||
if (hasLocalVideo) {
|
||||
return i18n('icu:ContactListItem__menu__video-call');
|
||||
}
|
||||
if (hasLocalAudio) {
|
||||
return i18n('icu:CallControls__InfoDisplay--audio-call');
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
i18n,
|
||||
isRinging,
|
||||
isConnected,
|
||||
activeCall.joinedAt,
|
||||
isReconnecting,
|
||||
isGroupCall,
|
||||
participantCount,
|
||||
hasLocalVideo,
|
||||
hasLocalAudio,
|
||||
toggleParticipants,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -489,11 +513,8 @@ export function CallScreen({
|
|||
i18n={i18n}
|
||||
isInSpeakerView={isInSpeakerView(activeCall)}
|
||||
isGroupCall={isGroupCall}
|
||||
message={headerMessage}
|
||||
participantCount={participantCount}
|
||||
showParticipantsList={showParticipantsList}
|
||||
title={headerTitle}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
toggleSpeakerView={toggleSpeakerView}
|
||||
|
@ -516,38 +537,56 @@ export function CallScreen({
|
|||
<div className="module-ongoing-call__footer__local-preview-offset" />
|
||||
<div
|
||||
className={classNames(
|
||||
'CallControls',
|
||||
'module-ongoing-call__footer__actions',
|
||||
controlsFadeClass
|
||||
)}
|
||||
>
|
||||
<CallingButton
|
||||
buttonType={presentingButtonType}
|
||||
i18n={i18n}
|
||||
<div className="CallControls__InfoDisplay">
|
||||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<CallingButton
|
||||
buttonType={presentingButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={togglePresenting}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="CallControls__JoinLeaveButtonContainer"
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={togglePresenting}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleVideo}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleAudio}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={CallingButtonType.HANG_UP}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={hangUp}
|
||||
/>
|
||||
>
|
||||
<Button
|
||||
className="CallControls__JoinLeaveButton CallControls__JoinLeaveButton--hangup"
|
||||
onClick={hangUp}
|
||||
variant={ButtonVariant.Destructive}
|
||||
>
|
||||
{isGroupCall
|
||||
? i18n('icu:CallControls__JoinLeaveButton--hangup-group')
|
||||
: i18n('icu:CallControls__JoinLeaveButton--hangup-1-1')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-ongoing-call__footer__local-preview">
|
||||
{localPreviewNode}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
},
|
||||
},
|
||||
args: {
|
||||
buttonType: CallingButtonType.HANG_UP,
|
||||
buttonType: CallingButtonType.RING_ON,
|
||||
i18n,
|
||||
onClick: action('on-click'),
|
||||
onMouseEnter: action('on-mouse-enter'),
|
||||
|
|
|
@ -13,7 +13,6 @@ export enum CallingButtonType {
|
|||
AUDIO_DISABLED = 'AUDIO_DISABLED',
|
||||
AUDIO_OFF = 'AUDIO_OFF',
|
||||
AUDIO_ON = 'AUDIO_ON',
|
||||
HANG_UP = 'HANG_UP',
|
||||
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
|
||||
PRESENTING_OFF = 'PRESENTING_OFF',
|
||||
PRESENTING_ON = 'PRESENTING_ON',
|
||||
|
@ -48,88 +47,69 @@ export function CallingButton({
|
|||
|
||||
let classNameSuffix = '';
|
||||
let tooltipContent = '';
|
||||
let label = '';
|
||||
let disabled = false;
|
||||
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
||||
classNameSuffix = 'audio--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--audio-disabled');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
disabled = true;
|
||||
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
||||
classNameSuffix = 'audio--off';
|
||||
tooltipContent = i18n('icu:calling__button--audio-on');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
} else if (buttonType === CallingButtonType.AUDIO_ON) {
|
||||
classNameSuffix = 'audio--on';
|
||||
tooltipContent = i18n('icu:calling__button--audio-off');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
||||
classNameSuffix = 'video--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--video-disabled');
|
||||
disabled = true;
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
||||
classNameSuffix = 'video--off';
|
||||
tooltipContent = i18n('icu:calling__button--video-on');
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_ON) {
|
||||
classNameSuffix = 'video--on';
|
||||
tooltipContent = i18n('icu:calling__button--video-off');
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.HANG_UP) {
|
||||
classNameSuffix = 'hangup';
|
||||
tooltipContent = i18n('icu:calling__hangup');
|
||||
label = i18n('icu:calling__hangup');
|
||||
} else if (buttonType === CallingButtonType.RING_DISABLED) {
|
||||
classNameSuffix = 'ring--disabled';
|
||||
disabled = true;
|
||||
tooltipContent = i18n(
|
||||
'icu:calling__button--ring__disabled-because-group-is-too-large'
|
||||
);
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
} else if (buttonType === CallingButtonType.RING_OFF) {
|
||||
classNameSuffix = 'ring--off';
|
||||
tooltipContent = i18n('icu:calling__button--ring__on');
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
tooltipContent = i18n('icu:CallingButton--ring-on');
|
||||
} else if (buttonType === CallingButtonType.RING_ON) {
|
||||
classNameSuffix = 'ring--on';
|
||||
tooltipContent = i18n('icu:calling__button--ring__off');
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
tooltipContent = i18n('icu:CallingButton__ring-off');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
||||
classNameSuffix = 'presenting--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-disabled');
|
||||
disabled = true;
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
|
||||
classNameSuffix = 'presenting--on';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-off');
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
|
||||
classNameSuffix = 'presenting--off';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-on');
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
}
|
||||
|
||||
const className = classNames(
|
||||
'CallingButton__icon',
|
||||
`CallingButton__icon--${classNameSuffix}`
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltipContent}
|
||||
direction={tooltipDirection}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'CallingButton__container',
|
||||
!isVisible && 'CallingButton__container--hidden'
|
||||
<div className="CallingButton">
|
||||
<Tooltip
|
||||
className="CallingButton__tooltip"
|
||||
wrapperClassName={classNames(
|
||||
'CallingButton__button-container',
|
||||
!isVisible && 'CallingButton__button-container--hidden'
|
||||
)}
|
||||
content={tooltipContent}
|
||||
direction={tooltipDirection}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={tooltipContent}
|
||||
className={className}
|
||||
className={classNames(
|
||||
'CallingButton__icon',
|
||||
`CallingButton__icon--${classNameSuffix}`
|
||||
)}
|
||||
disabled={disabled}
|
||||
id={uniqueButtonId}
|
||||
onClick={onClick}
|
||||
|
@ -139,10 +119,7 @@ export function CallingButton({
|
|||
>
|
||||
<div />
|
||||
</button>
|
||||
<label className="CallingButton__label" htmlFor={uniqueButtonId}>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,9 +24,7 @@ export default {
|
|||
isGroupCall: false,
|
||||
message: '',
|
||||
participantCount: 0,
|
||||
showParticipantsList: false,
|
||||
title: 'With Someone',
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
},
|
||||
|
@ -52,14 +50,7 @@ export function WithParticipants(args: PropsType): JSX.Element {
|
|||
}
|
||||
|
||||
export function WithParticipantsShown(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingHeader
|
||||
{...args}
|
||||
isGroupCall
|
||||
participantCount={10}
|
||||
showParticipantsList
|
||||
/>
|
||||
);
|
||||
return <CallingHeader {...args} isGroupCall participantCount={10} />;
|
||||
}
|
||||
|
||||
export function LongTitle(args: PropsType): JSX.Element {
|
||||
|
|
|
@ -15,9 +15,7 @@ export type PropsType = {
|
|||
message?: ReactNode;
|
||||
onCancel?: () => void;
|
||||
participantCount: number;
|
||||
showParticipantsList: boolean;
|
||||
title?: string;
|
||||
toggleParticipants?: () => void;
|
||||
togglePip?: () => void;
|
||||
toggleSettings: () => void;
|
||||
toggleSpeakerView?: () => void;
|
||||
|
@ -30,9 +28,7 @@ export function CallingHeader({
|
|||
message,
|
||||
onCancel,
|
||||
participantCount,
|
||||
showParticipantsList,
|
||||
title,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
toggleSpeakerView,
|
||||
|
@ -46,48 +42,24 @@ export function CallingHeader({
|
|||
<div className="module-ongoing-call__header-message">{message}</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall && participantCount ? (
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:calling__participants', {
|
||||
people: String(participantCount),
|
||||
})}
|
||||
content={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(participantCount),
|
||||
})}
|
||||
className={classNames(
|
||||
'CallingButton__participants--container',
|
||||
{
|
||||
'CallingButton__participants--shown': showParticipantsList,
|
||||
}
|
||||
)}
|
||||
onClick={toggleParticipants}
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
>
|
||||
<i className="CallingButton__participants" />
|
||||
<span className="CallingButton__participants--count">
|
||||
{participantCount}
|
||||
</span>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Pip" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:callingDeviceSelection__settings')}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallingButton__settings"
|
||||
onClick={toggleSettings}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{isGroupCall && participantCount > 2 && toggleSpeakerView && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
|
@ -96,6 +68,7 @@ export function CallingHeader({
|
|||
? i18n('icu:calling__switch-view--to-grid')
|
||||
: i18n('icu:calling__switch-view--to-speaker')
|
||||
}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
|
@ -104,38 +77,53 @@ export function CallingHeader({
|
|||
? i18n('icu:calling__switch-view--to-grid')
|
||||
: i18n('icu:calling__switch-view--to-speaker')
|
||||
}
|
||||
className={
|
||||
isInSpeakerView
|
||||
? 'CallingButton__grid-view'
|
||||
: 'CallingButton__speaker-view'
|
||||
}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={toggleSpeakerView}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('icu:calling__pip--on')} theme={Theme.Dark}>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__pip"
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'CallSettingsButton__Icon',
|
||||
isInSpeakerView
|
||||
? 'CallSettingsButton__Icon--GridView'
|
||||
: 'CallSettingsButton__Icon--SpeakerView'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={toggleSettings}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Settings" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{onCancel && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('icu:cancel')} theme={Theme.Dark}>
|
||||
<Tooltip
|
||||
content={i18n('icu:cancel')}
|
||||
theme={Theme.Dark}
|
||||
className="CallingButton__tooltip"
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:cancel')}
|
||||
className="CallingButton__cancel"
|
||||
className="CallSettingsButton__Button CallSettingsButton__Button--Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
/>
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Cancel" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { TooltipPlacement } from './Tooltip';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { CallParticipantCount } from './CallParticipantCount';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||
import {
|
||||
|
@ -23,6 +24,7 @@ import { useIsOnline } from '../hooks/useIsOnline';
|
|||
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { useCallingToasts } from './CallingToast';
|
||||
import { useMutedToast } from './CallingToastManager';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -84,7 +86,6 @@ export function CallingLobby({
|
|||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
setOutgoingRing,
|
||||
showParticipantsList,
|
||||
toggleParticipants,
|
||||
toggleSettings,
|
||||
outgoingRing,
|
||||
|
@ -143,8 +144,6 @@ export function CallingLobby({
|
|||
|
||||
const [isCallConnecting, setIsCallConnecting] = React.useState(false);
|
||||
|
||||
useWasInitiallyMutedToast(hasLocalAudio, i18n);
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
|
@ -201,6 +200,38 @@ export function CallingLobby({
|
|||
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Start;
|
||||
}
|
||||
|
||||
const callStatus = React.useMemo(() => {
|
||||
if (isGroupCall) {
|
||||
return (
|
||||
<CallParticipantCount
|
||||
i18n={i18n}
|
||||
groupMemberCount={groupMembers?.length ?? 0}
|
||||
participantCount={peekedParticipants.length}
|
||||
toggleParticipants={toggleParticipants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (hasLocalVideo) {
|
||||
return i18n('icu:ContactListItem__menu__video-call');
|
||||
}
|
||||
if (hasLocalAudio) {
|
||||
return i18n('icu:CallControls__InfoDisplay--audio-call');
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
isGroupCall,
|
||||
peekedParticipants.length,
|
||||
i18n,
|
||||
hasLocalVideo,
|
||||
hasLocalAudio,
|
||||
groupMembers?.length,
|
||||
toggleParticipants,
|
||||
]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useWasInitiallyMutedToast(hasLocalAudio, i18n);
|
||||
useOutgoingRingToast(isRingButtonVisible, outgoingRing, i18n);
|
||||
|
||||
return (
|
||||
<FocusTrap>
|
||||
<div className="module-calling__container">
|
||||
|
@ -222,8 +253,6 @@ export function CallingLobby({
|
|||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
participantCount={peekedParticipants.length}
|
||||
showParticipantsList={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
onCancel={onCallCanceled}
|
||||
/>
|
||||
|
@ -249,37 +278,44 @@ export function CallingLobby({
|
|||
{i18n('icu:calling__your-video-is-off')}
|
||||
</div>
|
||||
|
||||
<div className="module-calling__buttons module-calling__buttons--inline">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={ringButtonType}
|
||||
i18n={i18n}
|
||||
isVisible={isRingButtonVisible}
|
||||
onClick={toggleOutgoingRing}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<div className="CallControls">
|
||||
<div className="CallControls__InfoDisplay">
|
||||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={ringButtonType}
|
||||
i18n={i18n}
|
||||
isVisible={isRingButtonVisible}
|
||||
onClick={toggleOutgoingRing}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div className="CallControls__JoinLeaveButtonContainer">
|
||||
<CallingLobbyJoinButton
|
||||
disabled={!canJoin}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
variant={callingLobbyJoinButtonVariant}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CallingLobbyJoinButton
|
||||
disabled={!canJoin}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
variant={callingLobbyJoinButtonVariant}
|
||||
/>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
|
@ -313,3 +349,51 @@ function useWasInitiallyMutedToast(
|
|||
}
|
||||
}, [hideToast, wasInitiallyMuted, hasLocalAudio]);
|
||||
}
|
||||
|
||||
function useOutgoingRingToast(
|
||||
isRingButtonVisible: boolean,
|
||||
outgoingRing: boolean,
|
||||
i18n: LocalizerType
|
||||
): void {
|
||||
const [previousOutgoingRing, setPreviousOutgoingRing] = React.useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const RINGING_TOAST_KEY = 'ringing';
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRingButtonVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviousOutgoingRing(outgoingRing);
|
||||
}, [isRingButtonVisible, outgoingRing]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRingButtonVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
previousOutgoingRing !== undefined &&
|
||||
outgoingRing !== previousOutgoingRing
|
||||
) {
|
||||
hideToast(RINGING_TOAST_KEY);
|
||||
showToast({
|
||||
key: RINGING_TOAST_KEY,
|
||||
content: outgoingRing
|
||||
? i18n('icu:CallControls__RingingToast--ringing-on')
|
||||
: i18n('icu:CallControls__RingingToast--ringing-off'),
|
||||
autoClose: true,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isRingButtonVisible,
|
||||
outgoingRing,
|
||||
previousOutgoingRing,
|
||||
hideToast,
|
||||
showToast,
|
||||
i18n,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -9,9 +9,6 @@ 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',
|
||||
|
@ -47,18 +44,24 @@ export function CallingLobbyJoinButton({
|
|||
|
||||
const childrenByVariant: Record<CallingLobbyJoinButtonVariant, ReactChild> = {
|
||||
[CallingLobbyJoinButtonVariant.CallIsFull]: i18n(
|
||||
'icu:calling__call-is-full'
|
||||
'icu:CallingLobbyJoinButton--call-full'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Loading]: (
|
||||
<Spinner size="18px" svgSize="small" />
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Join]: i18n(
|
||||
'icu:CallingLobbyJoinButton--join'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Start]: i18n(
|
||||
'icu:CallingLobbyJoinButton--start'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Loading]: <Spinner svgSize="small" />,
|
||||
[CallingLobbyJoinButtonVariant.Join]: i18n('icu:calling__join'),
|
||||
[CallingLobbyJoinButtonVariant.Start]: i18n('icu:calling__start'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(width && height) && (
|
||||
<Button
|
||||
className="module-CallingLobbyJoinButton"
|
||||
className="CallingLobbyJoinButton CallControls__JoinLeaveButton"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={{ width, height }}
|
||||
|
@ -79,7 +82,7 @@ export function CallingLobbyJoinButton({
|
|||
{Object.values(CallingLobbyJoinButtonVariant).map(candidateVariant => (
|
||||
<Button
|
||||
key={candidateVariant}
|
||||
className="module-CallingLobbyJoinButton"
|
||||
className="CallingLobbyJoinButton CallControls__JoinLeaveButton"
|
||||
variant={ButtonVariant.Calling}
|
||||
onClick={noop}
|
||||
ref={(button: HTMLButtonElement | null) => {
|
||||
|
@ -95,14 +98,10 @@ export function CallingLobbyJoinButton({
|
|||
// 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)
|
||||
)
|
||||
Math.ceil(Math.max(previousWidth, variantWidth))
|
||||
);
|
||||
setHeight((previousHeight = 0) =>
|
||||
Math.ceil(
|
||||
Math.max(previousHeight, variantHeight + PADDING_VERTICAL)
|
||||
)
|
||||
Math.ceil(Math.max(previousHeight, variantHeight))
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -146,16 +146,16 @@ export const CallingParticipantsList = React.memo(
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{participant.hasRemoteAudio === false ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
<div className="module-calling-participants-list__status">
|
||||
{participant.hasRemoteVideo === false ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
{participant.presenting ? (
|
||||
<span className="module-calling-participants-list__presenting" />
|
||||
) : null}
|
||||
{participant.hasRemoteAudio === false ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
|
|
@ -98,3 +98,35 @@ export function useScreenSharingStoppedToast({
|
|||
}
|
||||
}, [activeCall, previousPresenter, showToast, i18n]);
|
||||
}
|
||||
|
||||
export function useMutedToast(
|
||||
hasLocalAudio: boolean,
|
||||
i18n: LocalizerType
|
||||
): void {
|
||||
const [previousHasLocalAudio, setPreviousHasLocalAudio] = useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const MUTED_TOAST_KEY = 'muted';
|
||||
|
||||
useEffect(() => {
|
||||
setPreviousHasLocalAudio(hasLocalAudio);
|
||||
}, [hasLocalAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previousHasLocalAudio !== undefined &&
|
||||
hasLocalAudio !== previousHasLocalAudio
|
||||
) {
|
||||
hideToast(MUTED_TOAST_KEY);
|
||||
showToast({
|
||||
key: MUTED_TOAST_KEY,
|
||||
content: hasLocalAudio
|
||||
? i18n('icu:CallControls__MutedToast--unmuted')
|
||||
: i18n('icu:CallControls__MutedToast--muted'),
|
||||
autoClose: true,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD = 20;
|
|||
const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75;
|
||||
|
||||
// This should be an integer, as sub-pixel widths can cause performance issues.
|
||||
export const OVERFLOW_PARTICIPANT_WIDTH = 140;
|
||||
export const OVERFLOW_PARTICIPANT_WIDTH = 107;
|
||||
|
||||
export type PropsType = {
|
||||
getFrameBuffer: () => Buffer;
|
||||
|
|
Loading…
Reference in a new issue