diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2418dfc85b..af6842abf9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/sticker-creator/src/colors.scss b/sticker-creator/src/colors.scss index a9da725b36..96733f9591 100644 --- a/sticker-creator/src/colors.scss +++ b/sticker-creator/src/colors.scss @@ -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; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 4f34caa529..15a5fa6001 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; } } diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 71cba62a48..2a63fe6be8 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -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 diff --git a/stylesheets/components/CallControls.scss b/stylesheets/components/CallControls.scss new file mode 100644 index 0000000000..db95c0a3f6 --- /dev/null +++ b/stylesheets/components/CallControls.scss @@ -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; +} diff --git a/stylesheets/components/CallSettingsButton.scss b/stylesheets/components/CallSettingsButton.scss new file mode 100644 index 0000000000..9129d5e765 --- /dev/null +++ b/stylesheets/components/CallSettingsButton.scss @@ -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' + ); +} diff --git a/stylesheets/components/CallingButton.scss b/stylesheets/components/CallingButton.scss index a31ab97f0f..04cc5d3c7c 100644 --- a/stylesheets/components/CallingButton.scss +++ b/stylesheets/components/CallingButton.scss @@ -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; } } diff --git a/stylesheets/components/CallingLobby.scss b/stylesheets/components/CallingLobby.scss index eaf23c5d95..fe4c67d156 100644 --- a/stylesheets/components/CallingLobby.scss +++ b/stylesheets/components/CallingLobby.scss @@ -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; } diff --git a/stylesheets/components/CallingLobbyJoinButton.scss b/stylesheets/components/CallingLobbyJoinButton.scss deleted file mode 100644 index 84666b6089..0000000000 --- a/stylesheets/components/CallingLobbyJoinButton.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -.module-CallingLobbyJoinButton { - margin-bottom: 32px; -} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index b86f31e3a6..077581a166 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/components/CallParticipantCount.tsx b/ts/components/CallParticipantCount.tsx new file mode 100644 index 0000000000..5e711f562e --- /dev/null +++ b/ts/components/CallParticipantCount.tsx @@ -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 ( + + {innerText} + + ); + } + + return ( + + ); +} diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index d96e81f6c6..e94ff9b392 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -319,6 +319,18 @@ export function GroupCallMany(): JSX.Element { ); } +export function GroupCallSpeakerView(): JSX.Element { + return ( + + ); +} + export function GroupCallReconnecting(): JSX.Element { return ( void; }; -type DirectCallHeaderMessagePropsType = { - i18n: LocalizerType; - callState: CallState; - joinedAt: number | null; -}; - export const isInSpeakerView = ( call: Pick | 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 = ( - - ); - 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 ( + + ); + } + // joinedAt is only available for direct calls + if (isConnected) { + return ; + } + 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 (
- +
{conversation.title}
+
{callStatus}
+
+
+ + + +
+
- - - + > + +
{localPreviewNode} diff --git a/ts/components/CallingButton.stories.tsx b/ts/components/CallingButton.stories.tsx index 34340a84cd..4d51cdf705 100644 --- a/ts/components/CallingButton.stories.tsx +++ b/ts/components/CallingButton.stories.tsx @@ -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'), diff --git a/ts/components/CallingButton.tsx b/ts/components/CallingButton.tsx index 253e4a65d7..d07ec980ac 100644 --- a/ts/components/CallingButton.tsx +++ b/ts/components/CallingButton.tsx @@ -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 ( - -
+ - -
-
+ +
); } diff --git a/ts/components/CallingHeader.stories.tsx b/ts/components/CallingHeader.stories.tsx index 45e0275fb2..251c5dbde3 100644 --- a/ts/components/CallingHeader.stories.tsx +++ b/ts/components/CallingHeader.stories.tsx @@ -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 ( - - ); + return ; } export function LongTitle(args: PropsType): JSX.Element { diff --git a/ts/components/CallingHeader.tsx b/ts/components/CallingHeader.tsx index 99081b09d0..14b05acc04 100644 --- a/ts/components/CallingHeader.tsx +++ b/ts/components/CallingHeader.tsx @@ -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({
{message}
) : null}
- {isGroupCall && participantCount ? ( + {togglePip && (
- ) : null} -
- -
+ )} {isGroupCall && participantCount > 2 && toggleSpeakerView && (
- )} - {togglePip && ( -
- -
)} +
+ + + +
{onCancel && (
- +
)} diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 5d911ef90c..9244dc32df 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -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; @@ -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 ( + + ); + } + 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 (
@@ -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')}
-
- - - +
+
+
{conversation.title}
+
{callStatus}
+
+
+ + + +
+
+ { + setIsCallConnecting(true); + onJoinCall(); + }} + variant={callingLobbyJoinButtonVariant} + /> +
- - { - setIsCallConnecting(true); - onJoinCall(); - }} - variant={callingLobbyJoinButtonVariant} - />
); @@ -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, + ]); +} diff --git a/ts/components/CallingLobbyJoinButton.tsx b/ts/components/CallingLobbyJoinButton.tsx index c016655891..1dad068178 100644 --- a/ts/components/CallingLobbyJoinButton.tsx +++ b/ts/components/CallingLobbyJoinButton.tsx @@ -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.CallIsFull]: i18n( - 'icu:calling__call-is-full' + 'icu:CallingLobbyJoinButton--call-full' + ), + [CallingLobbyJoinButtonVariant.Loading]: ( + + ), + [CallingLobbyJoinButtonVariant.Join]: i18n( + 'icu:CallingLobbyJoinButton--join' + ), + [CallingLobbyJoinButtonVariant.Start]: i18n( + 'icu:CallingLobbyJoinButton--start' ), - [CallingLobbyJoinButtonVariant.Loading]: , - [CallingLobbyJoinButtonVariant.Join]: i18n('icu:calling__join'), - [CallingLobbyJoinButtonVariant.Start]: i18n('icu:calling__start'), }; return ( <> {Boolean(width && height) && (
-
- {participant.hasRemoteAudio === false ? ( - - ) : null} +
{participant.hasRemoteVideo === false ? ( ) : null} {participant.presenting ? ( ) : null} + {participant.hasRemoteAudio === false ? ( + + ) : null}
) diff --git a/ts/components/CallingToastManager.tsx b/ts/components/CallingToastManager.tsx index 65ce586e6e..14606cf18a 100644 --- a/ts/components/CallingToastManager.tsx +++ b/ts/components/CallingToastManager.tsx @@ -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]); +} diff --git a/ts/components/GroupCallOverflowArea.tsx b/ts/components/GroupCallOverflowArea.tsx index 7655e8d1af..fcac255688 100644 --- a/ts/components/GroupCallOverflowArea.tsx +++ b/ts/components/GroupCallOverflowArea.tsx @@ -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;