New call UI and controls

This commit is contained in:
ayumi-signal 2023-10-25 06:40:22 -07:00 committed by GitHub
parent 33c5c683c7
commit 8bb355f971
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 741 additions and 360 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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

View 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;
}

View 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'
);
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -1,6 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-CallingLobbyJoinButton {
margin-bottom: 32px;
}

View file

@ -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';

View 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>
);
}

View file

@ -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

View file

@ -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,16 +537,23 @@ export function CallScreen({
<div className="module-ongoing-call__footer__local-preview-offset" />
<div
className={classNames(
'CallControls',
'module-ongoing-call__footer__actions',
controlsFadeClass
)}
>
<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}
@ -533,6 +561,7 @@ export function CallScreen({
onMouseEnter={onControlsMouseEnter}
onMouseLeave={onControlsMouseLeave}
onClick={toggleVideo}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={audioButtonType}
@ -540,14 +569,24 @@ export function CallScreen({
onMouseEnter={onControlsMouseEnter}
onMouseLeave={onControlsMouseLeave}
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={CallingButtonType.HANG_UP}
i18n={i18n}
</div>
<div
className="CallControls__JoinLeaveButtonContainer"
onMouseEnter={onControlsMouseEnter}
onMouseLeave={onControlsMouseLeave}
>
<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}

View file

@ -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'),

View file

@ -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 (
<div className="CallingButton">
<Tooltip
className="CallingButton__tooltip"
wrapperClassName={classNames(
'CallingButton__button-container',
!isVisible && 'CallingButton__button-container--hidden'
)}
content={tooltipContent}
direction={tooltipDirection}
theme={Theme.Dark}
>
<div
className={classNames(
'CallingButton__container',
!isVisible && 'CallingButton__container--hidden'
)}
>
<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>
</div>
);
}

View file

@ -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 {

View file

@ -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"
>
<span
className={classNames(
'CallSettingsButton__Icon',
isInSpeakerView
? 'CallSettingsButton__Icon--GridView'
: 'CallSettingsButton__Icon--SpeakerView'
)}
/>
</button>
</Tooltip>
</div>
)}
{togglePip && (
<div className="module-calling-tools__button">
<Tooltip content={i18n('icu:calling__pip--on')} theme={Theme.Dark}>
<Tooltip
content={i18n('icu:callingDeviceSelection__settings')}
className="CallingButton__tooltip"
theme={Theme.Dark}
>
<button
aria-label={i18n('icu:calling__pip--on')}
className="CallingButton__pip"
onClick={togglePip}
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>
)}

View file

@ -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,7 +278,12 @@ export function CallingLobby({
{i18n('icu:calling__your-video-is-off')}
</div>
<div className="module-calling__buttons module-calling__buttons--inline">
<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}
@ -270,7 +304,7 @@ export function CallingLobby({
tooltipDirection={TooltipPlacement.Top}
/>
</div>
<div className="CallControls__JoinLeaveButtonContainer">
<CallingLobbyJoinButton
disabled={!canJoin}
i18n={i18n}
@ -281,6 +315,8 @@ export function CallingLobby({
variant={callingLobbyJoinButtonVariant}
/>
</div>
</div>
</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,
]);
}

View file

@ -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))
);
}}
>

View file

@ -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>
)

View file

@ -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]);
}

View file

@ -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;