Update call lobby UI to match new designs

This commit is contained in:
Evan Hahn 2021-08-17 16:45:18 -05:00 committed by GitHub
parent 50c4fa06cc
commit 763c35e546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 857 additions and 435 deletions

View File

@ -1300,7 +1300,11 @@
},
"calling__call-is-full": {
"message": "Call is full",
"description": "Button label in the call lobby when you can't join because the call is full"
"description": "Text in the call lobby when you can't join because the call is full"
},
"calling__button--video__label": {
"message": "Camera",
"description": "Label under the video button"
},
"calling__button--video-disabled": {
"message": "Camera disabled",
@ -1314,6 +1318,10 @@
"message": "Turn on camera",
"description": "Button tooltip label for turning on the camera"
},
"calling__button--audio__label": {
"message": "Mute",
"description": "Label under the audio button"
},
"calling__button--audio-disabled": {
"message": "Microphone disabled",
"description": "Button tooltip label when the microphone is disabled"
@ -1326,6 +1334,10 @@
"message": "Unmute mic",
"description": "Button tooltip label for turning on the microphone"
},
"calling__button--presenting__label": {
"message": "Share",
"description": "Label under the share screen button"
},
"calling__button--presenting-disabled": {
"message": "Presenting disabled",
"description": "Button tooltip label for when screen sharing is disabled"
@ -1342,11 +1354,11 @@
"message": "Your camera is off",
"description": "Label in the calling lobby indicating that your camera is off"
},
"calling__lobby-summary--zero": {
"calling__pre-call-info--empty-group": {
"message": "No one else is here",
"description": "Shown in the calling lobby to describe who is in the call"
},
"calling__lobby-summary--single": {
"calling__pre-call-info--1-person-in-call": {
"message": "$first$ is in this call",
"description": "Shown in the calling lobby to describe who is in the call",
"placeholders": {
@ -1356,11 +1368,11 @@
}
}
},
"calling__lobby-summary--self": {
"calling__pre-call-info--another-device-in-call": {
"message": "One of your other devices is in this call",
"description": "Shown in the calling lobby to describe when it is just you"
},
"calling__lobby-summary--double": {
"calling__pre-call-info--2-people-in-call": {
"message": "$first$ and $second$ are in this call",
"description": "Shown in the calling lobby to describe who is in the call",
"placeholders": {
@ -1374,7 +1386,7 @@
}
}
},
"calling__lobby-summary--triple": {
"calling__pre-call-info--3-people-in-call": {
"message": "$first$, $second$, and $third$ are in this call",
"description": "Shown in the calling lobby to describe who is in the call",
"placeholders": {
@ -1392,7 +1404,7 @@
}
}
},
"calling__lobby-summary--many": {
"calling__pre-call-info--many-people-in-call": {
"message": "$first$, $second$, and $others$ others are in this call",
"description": "Shown in the calling lobby to describe who is in the call",
"placeholders": {
@ -1410,6 +1422,76 @@
}
}
},
"calling__pre-call-info--will-ring-1": {
"message": "Signal will ring $person$",
"description": "Shown in the calling lobby to describe who will be rung",
"placeholders": {
"person": {
"content": "$1",
"example": "Sam"
}
}
},
"calling__pre-call-info--will-notify-1": {
"message": "$person$ will be notified",
"description": "Shown in the calling lobby to describe who will be notified",
"placeholders": {
"person": {
"content": "$1",
"example": "Sam"
}
}
},
"calling__pre-call-info--will-notify-2": {
"message": "$first$ and $second$ will be notified",
"description": "Shown in the calling lobby to describe who will be notified",
"placeholders": {
"first": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
}
}
},
"calling__pre-call-info--will-notify-3": {
"message": "$first$, $second$, and $third$ will be notified",
"description": "Shown in the calling lobby to describe who will be notified",
"placeholders": {
"first": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
},
"third": {
"content": "$3",
"example": "April"
}
}
},
"calling__pre-call-info--will-notify-many": {
"message": "$first$, $second$, and $others$ others will be notified",
"description": "Shown in the calling lobby to describe who will be notified",
"placeholders": {
"person": {
"content": "$1",
"example": "Sam"
},
"second": {
"content": "$2",
"example": "Cayce"
},
"others": {
"content": "$3",
"example": "5"
}
}
},
"calling__in-this-call--zero": {
"message": "No one else is here",
"description": "Shown in the participants list to describe how many people are in the call"

View File

@ -222,6 +222,10 @@
text-align: inherit;
}
@mixin calling-text-shadow {
text-shadow: 0 0 4px $color-black-alpha-40;
}
// --- Buttons
// Individual traits

View File

@ -5201,7 +5201,7 @@ button.module-image__border-overlay:focus {
padding-bottom: 24px;
padding-top: calc(24px + var(--title-bar-drag-area-height));
text-align: center;
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
@include calling-text-shadow;
width: 100%;
&--header-name {
@ -5224,6 +5224,10 @@ button.module-image__border-overlay:focus {
position: absolute;
text-align: center;
width: 100%;
&--inline {
position: static;
}
}
&__background {
@ -5237,7 +5241,6 @@ button.module-image__border-overlay:focus {
width: 100%;
&--blur {
position: absolute;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
@ -5248,31 +5251,24 @@ button.module-image__border-overlay:focus {
}
}
&__video-off {
&--icon {
&__camera-is-off {
@include calling-text-shadow;
@include font-body-1;
color: $color-white;
display: flex;
z-index: 1;
&::before {
content: '';
display: block;
@include color-svg(
'../images/icons/v2/video-off-solid-24.svg',
$color-white
);
height: 24px;
margin-bottom: 8px;
margin-right: 10px;
width: 24px;
}
&--text {
color: $color-white;
z-index: 1;
}
&--container {
display: flex;
flex-direction: row;
margin-top: 12px;
.module-calling__video-off--text {
margin-left: 10px;
}
}
}
}
@ -5352,20 +5348,31 @@ button.module-image__border-overlay:focus {
height: $size;
width: $size;
}
&__cancel {
@include color-svg('../images/icons/v2/x-24.svg', $color-white);
height: $size;
width: $size;
}
}
.module-calling-button__container {
display: inline-flex;
flex-direction: column;
}
.module-calling-button__icon {
border-radius: 56px;
height: 56px;
width: 56px;
border-radius: 52px;
height: 52px;
width: 52px;
@mixin calling-button-icon($icon, $background-color, $icon-color) {
background-color: $background-color;
div {
@include color-svg($icon, $icon-color);
height: 28px;
width: 28px;
height: 24px;
width: 24px;
}
}
@ -5433,6 +5440,16 @@ button.module-image__border-overlay:focus {
}
}
.module-calling-button__label {
@include font-subtitle;
margin-top: 8px;
text-align: center;
text-transform: lowercase;
color: $color-white;
@include calling-text-shadow;
user-select: none;
}
@keyframes module-ongoing-call__controls--fade-in {
from {
opacity: 0;
@ -5804,84 +5821,6 @@ button.module-image__border-overlay:focus {
}
}
.module-calling-lobby {
&__actions {
align-items: flex-start;
display: flex;
flex-direction: row;
flex: 0 0 100px;
}
&__button {
margin-left: 8px;
margin-right: 8px;
width: 160px;
&[disabled] {
opacity: 0.5;
}
}
// The dimensions of this element are set by JavaScript.
&__local-preview {
$transition: 200ms ease-out;
@include font-body-2;
border-radius: 8px;
color: $color-white;
display: flex;
flex-direction: column;
max-height: 100%;
max-width: 100%;
overflow: hidden;
position: relative;
transition: width $transition, height $transition;
&-container {
align-items: center;
display: flex;
flex-direction: column;
flex: 1 1 auto;
justify-content: center;
margin: 24px;
overflow: hidden;
width: 90%;
}
&__video-on {
background-color: $color-gray-80;
display: block;
flex-grow: 1;
object-fit: contain;
transform: rotateY(180deg);
width: 100%;
height: 100%;
}
&__video-off {
&__icon {
@include color-svg(
'../images/icons/v2/video-off-solid-24.svg',
$color-white
);
height: 24px;
margin-bottom: 8px;
width: 24px;
}
&__text {
z-index: 1;
}
}
}
&__info {
color: $color-white;
margin-bottom: 36px;
margin-top: 12px;
}
}
.module-calling-pip {
backface-visibility: hidden;
background-color: $color-gray-95;
@ -9547,40 +9486,6 @@ button.module-image__border-overlay:focus {
outline: none;
padding: 7px 12px;
}
&__gray {
@include font-body-1-bold;
background-color: $color-gray-45;
border-radius: 4px;
border: none;
color: $color-white;
line-height: 24px;
outline: none;
padding: 7px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
}
&__green {
@include font-body-1-bold;
background-color: $color-accent-green;
border-radius: 4px;
border: none;
color: $color-white;
line-height: 24px;
outline: none;
padding: 7px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
}
}
// Module: Group Contact Details

View File

@ -134,4 +134,26 @@
@include hover-and-active-states($background-color, $color-white);
}
}
&--calling {
$color: $color-white;
$background-color: $color-accent-green;
@include rounded-corners;
color: $color;
background: $background-color;
&:disabled {
color: fade-out($color, 0.4);
background: fade-out($background-color, 0.6);
}
@include light-theme {
@include hover-and-active-states($background-color, $color-black);
}
@include dark-theme {
@include hover-and-active-states($background-color, $color-white);
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-CallingLobby {
&__local-preview {
height: 100%;
object-fit: cover;
opacity: 0.6;
position: absolute;
transform: rotateY(180deg);
width: 100%;
z-index: -1;
}
&__camera-is-off {
@include calling-text-shadow;
@include font-subtitle;
align-items: center;
color: $color-white;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
text-align: center;
transition: opacity 100ms ease-out;
user-select: none;
&--visible {
opacity: 1;
}
&--invisible {
opacity: 0;
}
&::before {
content: '';
display: block;
@include color-svg(
'../images/icons/v2/video-off-solid-24.svg',
$color-white
);
height: 24px;
margin-bottom: 8px;
width: 24px;
}
}
}

View File

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

View File

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-CallingPreCallInfo {
text-align: center;
user-select: none;
&__title,
&__subtitle {
-webkit-box-orient: vertical;
color: $color-white;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
@include calling-text-shadow;
}
&__title {
-webkit-line-clamp: 1;
@include font-title-2;
margin-top: 16px;
}
&__subtitle {
-webkit-line-clamp: 2;
@include font-body-1;
margin-top: 8px;
}
}

View File

@ -37,6 +37,9 @@
@import './components/AvatarTextEditor.scss';
@import './components/BetterAvatarBubble.scss';
@import './components/Button.scss';
@import './components/CallingLobby.scss';
@import './components/CallingLobbyJoinButton.scss';
@import './components/CallingPreCallInfo.scss';
@import './components/CallingScreenSharingController.scss';
@import './components/CallingSelectPresentingSourcesModal.scss';
@import './components/ChatColorPicker.scss';

View File

@ -19,6 +19,7 @@ story.add('Kitchen sink', () => (
ButtonVariant.SecondaryAffirmative,
ButtonVariant.SecondaryDestructive,
ButtonVariant.Destructive,
ButtonVariant.Calling,
].map(variant => (
<React.Fragment key={variant}>
<p>
@ -50,3 +51,9 @@ story.add('aria-label', () => (
onClick={action('onClick')}
/>
));
story.add('Custom styles', () => (
<Button onClick={action('onClick')} style={{ transform: 'rotate(5deg)' }}>
Hello world
</Button>
));

View File

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { MouseEventHandler, ReactNode } from 'react';
import React, { CSSProperties, MouseEventHandler, ReactNode } from 'react';
import classNames from 'classnames';
import { assert } from '../util/assert';
@ -17,12 +17,15 @@ export enum ButtonVariant {
SecondaryAffirmative,
SecondaryDestructive,
Destructive,
Calling,
}
type PropsType = {
className?: string;
disabled?: boolean;
size?: ButtonSize;
style?: CSSProperties;
tabIndex?: number;
variant?: ButtonVariant;
} & (
| {
@ -64,6 +67,7 @@ const VARIANT_CLASS_NAMES = new Map<ButtonVariant, string>([
'module-Button--secondary module-Button--secondary--destructive',
],
[ButtonVariant.Destructive, 'module-Button--destructive'],
[ButtonVariant.Calling, 'module-Button--calling'],
]);
export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
@ -73,6 +77,8 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
className,
disabled = false,
size = ButtonSize.Medium,
style,
tabIndex,
variant = ButtonVariant.Primary,
} = props;
const ariaLabel = props['aria-label'];
@ -105,6 +111,8 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
disabled={disabled}
onClick={onClick}
ref={ref}
style={style}
tabIndex={tabIndex}
// The `type` should either be "button" or "submit", which is effectively static.
// eslint-disable-next-line react/button-has-type
type={type}

View File

@ -7,20 +7,26 @@ import { AvatarColorType } from '../types/Colors';
export type PropsType = {
avatarPath?: string;
children: React.ReactNode;
children?: React.ReactNode;
className?: string;
color?: AvatarColorType;
};
export const CallBackgroundBlur = ({
avatarPath,
children,
className,
color,
}: PropsType): JSX.Element => {
return (
<div
className={classNames('module-calling__background', {
[`module-background-color__${color || 'default'}`]: !avatarPath,
})}
className={classNames(
'module-calling__background',
{
[`module-background-color__${color || 'default'}`]: !avatarPath,
},
className
)}
>
{avatarPath && (
<div

View File

@ -137,6 +137,7 @@ story.add('Ongoing Group Call', () => (
deviceCount: 0,
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
groupMembers: [],
peekedParticipants: [],
remoteParticipants: [],
},
@ -189,6 +190,7 @@ story.add('Group call - Safety Number Changed', () => (
deviceCount: 0,
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
groupMembers: [],
peekedParticipants: [],
remoteParticipants: [],
},

View File

@ -163,6 +163,9 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
let isCallFull: boolean;
let showCallLobby: boolean;
let groupMembers:
| undefined
| Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
switch (activeCall.callMode) {
case CallMode.Direct: {
@ -182,11 +185,13 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
}
showCallLobby = !callState;
isCallFull = false;
groupMembers = undefined;
break;
}
case CallMode.Group: {
showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
({ groupMembers } = activeCall);
break;
}
default:
@ -199,6 +204,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
<CallingLobby
availableCameras={availableCameras}
conversation={conversation}
groupMembers={groupMembers}
hasLocalAudio={hasLocalAudio}
hasLocalVideo={hasLocalVideo}
i18n={i18n}

View File

@ -94,6 +94,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
deviceCount: (overrideProps.remoteParticipants || []).length,
groupMembers: overrideProps.remoteParticipants || [],
// Because remote participants are a superset, we can use them in place of peeked
// participants.
peekedParticipants:

View File

@ -313,7 +313,6 @@ export const CallScreen: React.FC<PropsType> = ({
className={classNames('module-ongoing-call__header', controlsFadeClass)}
>
<CallingHeader
canPip
i18n={i18n}
isInSpeakerView={isInSpeakerView}
isGroupCall={isGroupCall}
@ -357,11 +356,8 @@ export const CallScreen: React.FC<PropsType> = ({
sharedGroupNames={[]}
size={80}
/>
<div className="module-calling__video-off--container">
<div className="module-calling__video-off--icon" />
<span className="module-calling__video-off--text">
{i18n('calling__your-video-is-off')}
</span>
<div className="module-calling__camera-is-off">
{i18n('calling__your-video-is-off')}
</div>
</CallBackgroundBlur>
</div>

View File

@ -1,8 +1,9 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { v4 as uuid } from 'uuid';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { Theme } from '../util/theme';
import { LocalizerType } from '../types/Util';
@ -33,42 +34,55 @@ export const CallingButton = ({
onClick,
tooltipDirection,
}: PropsType): JSX.Element => {
const uniqueButtonId = useMemo(() => uuid(), []);
let classNameSuffix = '';
let tooltipContent = '';
let label = '';
let disabled = false;
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
classNameSuffix = 'audio--disabled';
tooltipContent = i18n('calling__button--audio-disabled');
label = i18n('calling__button--audio__label');
disabled = true;
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
classNameSuffix = 'audio--off';
tooltipContent = i18n('calling__button--audio-on');
label = i18n('calling__button--audio__label');
} else if (buttonType === CallingButtonType.AUDIO_ON) {
classNameSuffix = 'audio--on';
tooltipContent = i18n('calling__button--audio-off');
label = i18n('calling__button--audio__label');
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
classNameSuffix = 'video--disabled';
tooltipContent = i18n('calling__button--video-disabled');
disabled = true;
label = i18n('calling__button--video__label');
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
classNameSuffix = 'video--off';
tooltipContent = i18n('calling__button--video-on');
label = i18n('calling__button--video__label');
} else if (buttonType === CallingButtonType.VIDEO_ON) {
classNameSuffix = 'video--on';
tooltipContent = i18n('calling__button--video-off');
label = i18n('calling__button--video__label');
} else if (buttonType === CallingButtonType.HANG_UP) {
classNameSuffix = 'hangup';
tooltipContent = i18n('calling__hangup');
label = i18n('calling__hangup');
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
classNameSuffix = 'presenting--disabled';
tooltipContent = i18n('calling__button--presenting-disabled');
disabled = true;
label = i18n('calling__button--presenting__label');
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
classNameSuffix = 'presenting--on';
tooltipContent = i18n('calling__button--presenting-off');
label = i18n('calling__button--presenting__label');
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
classNameSuffix = 'presenting--off';
tooltipContent = i18n('calling__button--presenting-on');
label = i18n('calling__button--presenting__label');
}
const className = classNames(
@ -82,15 +96,24 @@ export const CallingButton = ({
direction={tooltipDirection}
theme={Theme.Dark}
>
<button
aria-label={tooltipContent}
className={className}
disabled={disabled}
onClick={onClick}
type="button"
>
<div />
</button>
<div className="module-calling-button__container">
<button
aria-label={tooltipContent}
className={className}
disabled={disabled}
id={uniqueButtonId}
onClick={onClick}
type="button"
>
<div />
</button>
<label
className="module-calling-button__label"
htmlFor={uniqueButtonId}
>
{label}
</label>
</div>
</Tooltip>
);
};

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -13,7 +13,6 @@ import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
canPip: boolean('canPip', Boolean(overrideProps.canPip)),
i18n,
isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)),
message: overrideProps.message,
@ -35,14 +34,18 @@ const story = storiesOf('Components/CallingHeader', module);
story.add('Default', () => <CallingHeader {...createProps()} />);
story.add('Has Pip', () => (
<CallingHeader {...createProps({ canPip: true })} />
story.add('Lobby style', () => (
<CallingHeader
{...createProps()}
title={undefined}
togglePip={undefined}
onCancel={action('onClose')}
/>
));
story.add('With Participants', () => (
<CallingHeader
{...createProps({
canPip: true,
isGroupCall: true,
participantCount: 10,
})}
@ -52,7 +55,6 @@ story.add('With Participants', () => (
story.add('With Participants (shown)', () => (
<CallingHeader
{...createProps({
canPip: true,
isGroupCall: true,
participantCount: 10,
showParticipantsList: true,

View File

@ -8,11 +8,11 @@ import { Tooltip } from './Tooltip';
import { Theme } from '../util/theme';
export type PropsType = {
canPip?: boolean;
i18n: LocalizerType;
isInSpeakerView?: boolean;
isGroupCall?: boolean;
message?: string;
onCancel?: () => void;
participantCount: number;
showParticipantsList: boolean;
title?: string;
@ -23,11 +23,11 @@ export type PropsType = {
};
export const CallingHeader = ({
canPip = false,
i18n,
isInSpeakerView,
isGroupCall = false,
message,
onCancel,
participantCount,
showParticipantsList,
title,
@ -44,7 +44,7 @@ export const CallingHeader = ({
<div className="module-ongoing-call__header-message">{message}</div>
) : null}
<div className="module-calling-tools">
{isGroupCall ? (
{participantCount ? (
<div className="module-calling-tools__button">
<Tooltip
content={i18n('calling__participants', [String(participantCount)])}
@ -111,7 +111,7 @@ export const CallingHeader = ({
</Tooltip>
</div>
)}
{canPip && (
{togglePip && (
<div className="module-calling-tools__button">
<Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}>
<button
@ -123,6 +123,18 @@ export const CallingHeader = ({
</Tooltip>
</div>
)}
{onCancel && (
<div className="module-calling-tools__button">
<Tooltip content={i18n('cancel')} theme={Theme.Dark}>
<button
aria-label={i18n('cancel')}
className="module-calling-button__cancel"
onClick={onCancel}
type="button"
/>
</Tooltip>
</div>
)}
</div>
</div>
);

View File

@ -1,7 +1,8 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { times } from 'lodash';
import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
@ -26,33 +27,53 @@ const camera = {
},
};
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
availableCameras: overrideProps.availableCameras || [camera],
conversation: {
title: 'Rick Sanchez',
},
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
i18n,
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
me: overrideProps.me || {
color: AvatarColors[0],
uuid: generateUuid(),
},
onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'),
peekedParticipants: overrideProps.peekedParticipants || [],
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
showParticipantsList: boolean(
'showParticipantsList',
Boolean(overrideProps.showParticipantsList)
),
toggleParticipants: action('toggle-participants'),
toggleSettings: action('toggle-settings'),
});
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
const isGroupCall = boolean(
'isGroupCall',
overrideProps.isGroupCall || false
);
const conversation = isGroupCall
? getDefaultConversation({
title: 'Tahoe Trip',
type: 'group',
})
: getDefaultConversation();
return {
availableCameras: overrideProps.availableCameras || [camera],
conversation,
groupMembers: isGroupCall
? times(3, () => getDefaultConversation())
: undefined,
hasLocalAudio: boolean(
'hasLocalAudio',
overrideProps.hasLocalAudio || false
),
hasLocalVideo: boolean(
'hasLocalVideo',
overrideProps.hasLocalVideo || false
),
i18n,
isGroupCall,
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
me: overrideProps.me || {
color: AvatarColors[0],
uuid: generateUuid(),
},
onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'),
peekedParticipants: overrideProps.peekedParticipants || [],
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'),
showParticipantsList: boolean(
'showParticipantsList',
Boolean(overrideProps.showParticipantsList)
),
toggleParticipants: action('toggle-participants'),
toggleSettings: action('toggle-settings'),
};
};
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
getDefaultConversation({
@ -123,26 +144,6 @@ story.add('Group Call - 1 peeked participant (self)', () => {
return <CallingLobby {...props} />;
});
story.add('Group Call - 2 peeked participants', () => {
const props = createProps({
isGroupCall: true,
peekedParticipants: ['Sam', 'Cayce'].map(title =>
fakePeekedParticipant({ title })
),
});
return <CallingLobby {...props} />;
});
story.add('Group Call - 3 peeked participants', () => {
const props = createProps({
isGroupCall: true,
peekedParticipants: ['Sam', 'Cayce', 'April'].map(title =>
fakePeekedParticipant({ title })
),
});
return <CallingLobby {...props} />;
});
story.add('Group Call - 4 peeked participants', () => {
const props = createProps({
isGroupCall: true,

View File

@ -1,9 +1,8 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactNode } from 'react';
import Measure from 'react-measure';
import { debounce } from 'lodash';
import React from 'react';
import classNames from 'classnames';
import {
SetLocalAudioType,
SetLocalPreviewType,
@ -13,25 +12,32 @@ import { CallingButton, CallingButtonType } from './CallingButton';
import { TooltipPlacement } from './Tooltip';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { CallingHeader } from './CallingHeader';
import { Spinner } from './Spinner';
import { CallingPreCallInfo } from './CallingPreCallInfo';
import {
CallingLobbyJoinButton,
CallingLobbyJoinButtonVariant,
} from './CallingLobbyJoinButton';
import { AvatarColorType } from '../types/Colors';
import { LocalizerType } from '../types/Util';
import { ConversationType } from '../state/ducks/conversations';
import {
REQUESTED_VIDEO_WIDTH,
REQUESTED_VIDEO_HEIGHT,
} from '../calling/constants';
// We request dimensions but may not get them depending on the user's webcam. This is our
// fallback while we don't know.
const VIDEO_ASPECT_RATIO_FALLBACK =
REQUESTED_VIDEO_WIDTH / REQUESTED_VIDEO_HEIGHT;
export type PropsType = {
availableCameras: Array<MediaDeviceInfo>;
conversation: {
title: string;
};
conversation: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
>;
groupMembers?: Array<Pick<ConversationType, 'title'>>;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
i18n: LocalizerType;
@ -56,6 +62,7 @@ export type PropsType = {
export const CallingLobby = ({
availableCameras,
conversation,
groupMembers,
hasLocalAudio,
hasLocalVideo,
i18n,
@ -72,19 +79,10 @@ export const CallingLobby = ({
toggleParticipants,
toggleSettings,
}: PropsType): JSX.Element => {
const [
localPreviewContainerWidth,
setLocalPreviewContainerWidth,
] = React.useState<null | number>(null);
const [
localPreviewContainerHeight,
setLocalPreviewContainerHeight,
] = React.useState<null | number>(null);
const [localVideoAspectRatio, setLocalVideoAspectRatio] = React.useState(
VIDEO_ASPECT_RATIO_FALLBACK
);
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
const toggleAudio = React.useCallback((): void => {
setLocalAudio({ enabled: !hasLocalAudio });
}, [hasLocalAudio, setLocalAudio]);
@ -93,24 +91,6 @@ export const CallingLobby = ({
setLocalVideo({ enabled: !hasLocalVideo });
}, [hasLocalVideo, setLocalVideo]);
const hasEverMeasured =
localPreviewContainerWidth !== null && localPreviewContainerHeight !== null;
const setLocalPreviewContainerDimensions = React.useMemo(() => {
const set = (bounds: Readonly<{ width: number; height: number }>) => {
setLocalPreviewContainerWidth(bounds.width);
setLocalPreviewContainerHeight(bounds.height);
};
if (hasEverMeasured) {
return debounce(set, 100, { maxWait: 3000 });
}
return set;
}, [
hasEverMeasured,
setLocalPreviewContainerWidth,
setLocalPreviewContainerHeight,
]);
React.useEffect(() => {
setLocalPreview({ element: localVideoRef });
@ -119,21 +99,6 @@ export const CallingLobby = ({
};
}, [setLocalPreview]);
// This isn't perfect because it doesn't react to changes in the webcam's aspect ratio.
// For example, if you changed from Webcam A to Webcam B and Webcam B had a different
// aspect ratio, we wouldn't update.
//
// Unfortunately, RingRTC (1) doesn't update these dimensions with the "real" camera
// dimensions (2) doesn't give us any hooks or callbacks. For now, this works okay.
// We have `object-fit: contain` in the CSS in case we're wrong; not ideal, but
// usable.
React.useEffect(() => {
const videoEl = localVideoRef.current;
if (hasLocalVideo && videoEl && videoEl.width && videoEl.height) {
setLocalVideoAspectRatio(videoEl.width / videoEl.height);
}
}, [hasLocalVideo, setLocalVideoAspectRatio]);
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent): void {
let eventHandled = false;
@ -171,179 +136,89 @@ export const CallingLobby = ({
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
// It should be rare to see yourself in this list, but it's possible if (1) you rejoin
// quickly, causing the server to return stale state (2) you have joined on another
// device.
const participantNames = peekedParticipants.map(participant =>
participant.uuid === me.uuid
? i18n('you')
: participant.firstName || participant.title
);
const hasYou = peekedParticipants.some(
participant => participant.uuid === me.uuid
);
const canJoin = !isCallFull && !isCallConnecting;
let joinButtonChildren: ReactNode;
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
if (isCallFull) {
joinButtonChildren = i18n('calling__call-is-full');
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.CallIsFull;
} else if (isCallConnecting) {
joinButtonChildren = <Spinner svgSize="small" />;
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Loading;
} else if (peekedParticipants.length) {
joinButtonChildren = i18n('calling__join');
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Join;
} else {
joinButtonChildren = i18n('calling__start');
}
let localPreviewStyles: React.CSSProperties;
// It'd be nice to use `hasEverMeasured` here, too, but TypeScript isn't smart enough
// to understand the logic here.
if (
localPreviewContainerWidth !== null &&
localPreviewContainerHeight !== null
) {
const containerAspectRatio =
localPreviewContainerWidth / localPreviewContainerHeight;
localPreviewStyles =
containerAspectRatio < localVideoAspectRatio
? {
width: '100%',
height: Math.floor(
localPreviewContainerWidth / localVideoAspectRatio
),
}
: {
width: Math.floor(
localPreviewContainerHeight * localVideoAspectRatio
),
height: '100%',
};
} else {
localPreviewStyles = { display: 'none' };
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Start;
}
return (
<div className="module-calling__container">
{shouldShowLocalVideo ? (
<video
className="module-CallingLobby__local-preview"
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur
className="module-CallingLobby__local-preview"
avatarPath={me.avatarPath}
color={me.color}
/>
)}
<CallingHeader
title={conversation.title}
i18n={i18n}
isGroupCall={isGroupCall}
participantCount={peekedParticipants.length}
showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings}
onCancel={onCallCanceled}
/>
<Measure
bounds
onResize={({ bounds }) => {
if (!bounds) {
window.log.error('We should be measuring bounds');
return;
}
setLocalPreviewContainerDimensions(bounds);
}}
>
{({ measureRef }) => (
<div
ref={measureRef}
className="module-calling-lobby__local-preview-container"
>
<div
className="module-calling-lobby__local-preview"
style={localPreviewStyles}
>
{hasLocalVideo && availableCameras.length > 0 ? (
<video
className="module-calling-lobby__local-preview__video-on"
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur avatarPath={me.avatarPath} color={me.color}>
<div className="module-calling-lobby__local-preview__video-off__icon" />
<span className="module-calling-lobby__local-preview__video-off__text">
{i18n('calling__your-video-is-off')}
</span>
</CallBackgroundBlur>
)}
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
isCallFull={isCallFull}
me={me}
peekedParticipants={peekedParticipants}
/>
<div className="module-calling__buttons">
<CallingButton
buttonType={videoButtonType}
i18n={i18n}
onClick={toggleVideo}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={audioButtonType}
i18n={i18n}
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
/>
</div>
</div>
</div>
<div
className={classNames(
'module-CallingLobby__camera-is-off',
`module-CallingLobby__camera-is-off--${
shouldShowLocalVideo ? 'invisible' : 'visible'
}`
)}
</Measure>
{isGroupCall ? (
<div className="module-calling-lobby__info">
{participantNames.length === 0 &&
i18n('calling__lobby-summary--zero')}
{participantNames.length === 1 &&
hasYou &&
i18n('calling__lobby-summary--self')}
{participantNames.length === 1 &&
!hasYou &&
i18n('calling__lobby-summary--single', participantNames)}
{participantNames.length === 2 &&
i18n('calling__lobby-summary--double', {
first: participantNames[0],
second: participantNames[1],
})}
{participantNames.length === 3 &&
i18n('calling__lobby-summary--triple', {
first: participantNames[0],
second: participantNames[1],
third: participantNames[2],
})}
{participantNames.length > 3 &&
i18n('calling__lobby-summary--many', {
first: participantNames[0],
second: participantNames[1],
others: String(participantNames.length - 2),
})}
</div>
) : null}
<div className="module-calling-lobby__actions">
<button
className="module-button__gray module-calling-lobby__button"
onClick={onCallCanceled}
tabIndex={0}
type="button"
>
{i18n('cancel')}
</button>
<button
className="module-button__green module-calling-lobby__button"
disabled={!canJoin}
onClick={
canJoin
? () => {
setIsCallConnecting(true);
onJoinCall();
}
: undefined
}
tabIndex={0}
type="button"
>
{joinButtonChildren}
</button>
>
{i18n('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}
/>
</div>
<CallingLobbyJoinButton
disabled={!canJoin}
i18n={i18n}
onClick={() => {
setIsCallConnecting(true);
onJoinCall();
}}
variant={callingLobbyJoinButtonVariant}
/>
</div>
);
};

View File

@ -0,0 +1,107 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { FunctionComponent, ReactChild, useState } from 'react';
import { noop } from 'lodash';
import type { LocalizerType } from '../types/Util';
import { Button, ButtonVariant } from './Button';
import { Spinner } from './Spinner';
const PADDING_HORIZONTAL = 48;
const PADDING_VERTICAL = 12;
export enum CallingLobbyJoinButtonVariant {
CallIsFull = 'CallIsFull',
Join = 'Join',
Loading = 'Loading',
Start = 'Start',
}
/**
* This component is a little weird. Why not just render a button with some children?
*
* The contents of this component can change but we don't want its size to change, so we
* render all the variants invisibly, compute the maximum size, and then render the
* "final" button with those dimensions.
*
* For example, we might initially render "Join call" and then render a spinner when you
* click the button. The button shouldn't resize in that situation.
*/
export const CallingLobbyJoinButton: FunctionComponent<{
disabled?: boolean;
i18n: LocalizerType;
onClick: () => void;
variant: CallingLobbyJoinButtonVariant;
}> = ({ disabled, i18n, onClick, variant }) => {
const [width, setWidth] = useState<undefined | number>();
const [height, setHeight] = useState<undefined | number>();
const childrenByVariant: Record<CallingLobbyJoinButtonVariant, ReactChild> = {
[CallingLobbyJoinButtonVariant.CallIsFull]: i18n('calling__call-is-full'),
[CallingLobbyJoinButtonVariant.Loading]: <Spinner svgSize="small" />,
[CallingLobbyJoinButtonVariant.Join]: i18n('calling__join'),
[CallingLobbyJoinButtonVariant.Start]: i18n('calling__start'),
};
return (
<>
{Boolean(width && height) && (
<Button
className="module-CallingLobbyJoinButton"
disabled={disabled}
onClick={onClick}
style={{ width, height }}
tabIndex={0}
variant={ButtonVariant.Calling}
>
{childrenByVariant[variant]}
</Button>
)}
<div
style={{
visibility: 'hidden',
position: 'fixed',
left: -9999,
top: -9999,
}}
>
{Object.values(CallingLobbyJoinButtonVariant).map(candidateVariant => (
<Button
key={candidateVariant}
className="module-CallingLobbyJoinButton"
variant={ButtonVariant.Calling}
onClick={noop}
ref={(button: HTMLButtonElement | null) => {
if (!button) {
return;
}
const {
width: variantWidth,
height: variantHeight,
} = button.getBoundingClientRect();
// We could set the padding in CSS, but we don't do that in case some other
// styling causes a re-render of the button but not of the component. This
// is easiest to reproduce in Storybook, where the font hasn't loaded yet;
// we compute the size, then the font makes the text a bit larger, and
// there's a layout issue.
setWidth((previousWidth = 0) =>
Math.ceil(
Math.max(previousWidth, variantWidth + PADDING_HORIZONTAL)
)
);
setHeight((previousHeight = 0) =>
Math.ceil(
Math.max(previousHeight, variantHeight + PADDING_VERTICAL)
)
);
}}
>
{childrenByVariant[candidateVariant]}
</Button>
))}
</div>
</>
);
};

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { times } from 'lodash';
import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
@ -109,6 +110,7 @@ story.add('Group Call', () => {
callMode: CallMode.Group as CallMode.Group,
connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
groupMembers: times(3, () => getDefaultConversation()),
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
deviceCount: 0,

View File

@ -0,0 +1,100 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { times, range } from 'lodash';
import { storiesOf } from '@storybook/react';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { CallingPreCallInfo } from './CallingPreCallInfo';
const i18n = setupI18n('en', enMessages);
const getDefaultGroupConversation = () =>
getDefaultConversation({
name: 'Tahoe Trip',
phoneNumber: undefined,
profileName: undefined,
title: 'Tahoe Trip',
type: 'group',
});
const otherMembers = times(6, () => getDefaultConversation());
const story = storiesOf('Components/CallingPreCallInfo', module);
story.add('Direct conversation', () => (
<CallingPreCallInfo
conversation={getDefaultConversation()}
i18n={i18n}
me={getDefaultConversation()}
/>
));
story.add('Group conversation, empty group', () => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={[]}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={[]}
/>
));
times(5, numberOfOtherPeople => {
story.add(
`Group conversation, group has ${numberOfOtherPeople} other member${
numberOfOtherPeople === 1 ? '' : 's'
}`,
() => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers.slice(0, numberOfOtherPeople)}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={[]}
/>
)
);
});
range(1, 5).forEach(numberOfOtherPeople => {
story.add(
`Group conversation, ${numberOfOtherPeople} peeked participant${
numberOfOtherPeople === 1 ? '' : 's'
}`,
() => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers}
i18n={i18n}
me={getDefaultConversation()}
peekedParticipants={otherMembers.slice(0, numberOfOtherPeople)}
/>
)
);
});
story.add('Group conversation, you on an other device', () => {
const me = getDefaultConversation();
return (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers}
i18n={i18n}
me={me}
peekedParticipants={[me]}
/>
);
});
story.add('Group conversation, call is full', () => (
<CallingPreCallInfo
conversation={getDefaultGroupConversation()}
groupMembers={otherMembers}
i18n={i18n}
isCallFull
me={getDefaultConversation()}
peekedParticipants={otherMembers}
/>
));

View File

@ -0,0 +1,159 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { FunctionComponent } from 'react';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import { Avatar, AvatarSize } from './Avatar';
import { Emojify } from './conversation/Emojify';
import { missingCaseError } from '../util/missingCaseError';
type PropsType = {
conversation: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
>;
i18n: LocalizerType;
me: Pick<ConversationType, 'uuid'>;
// The following should only be set for group conversations.
groupMembers?: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
isCallFull?: boolean;
peekedParticipants?: Array<
Pick<ConversationType, 'firstName' | 'title' | 'uuid'>
>;
};
export const CallingPreCallInfo: FunctionComponent<PropsType> = ({
conversation,
groupMembers = [],
i18n,
isCallFull = false,
me,
peekedParticipants = [],
}) => {
let subtitle: string;
if (isCallFull) {
subtitle = i18n('calling__call-is-full');
} else if (peekedParticipants.length) {
// It should be rare to see yourself in this list, but it's possible if (1) you rejoin
// quickly, causing the server to return stale state (2) you have joined on another
// device.
let hasYou = false;
const participantNames = peekedParticipants.map(participant => {
if (participant.uuid === me.uuid) {
hasYou = true;
return i18n('you');
}
return getParticipantName(participant);
});
switch (participantNames.length) {
case 1:
subtitle = hasYou
? i18n('calling__pre-call-info--another-device-in-call')
: i18n('calling__pre-call-info--1-person-in-call', participantNames);
break;
case 2:
subtitle = i18n('calling__pre-call-info--2-people-in-call', {
first: participantNames[0],
second: participantNames[1],
});
break;
case 3:
subtitle = i18n('calling__pre-call-info--3-people-in-call', {
first: participantNames[0],
second: participantNames[1],
third: participantNames[2],
});
break;
default:
subtitle = i18n('calling__pre-call-info--many-people-in-call', {
first: participantNames[0],
second: participantNames[1],
others: String(participantNames.length - 2),
});
break;
}
} else if (conversation.type === 'direct') {
subtitle = i18n('calling__pre-call-info--will-ring-1', [
getParticipantName(conversation),
]);
} else if (conversation.type === 'group') {
const memberNames = groupMembers.map(getParticipantName);
switch (memberNames.length) {
case 0:
subtitle = i18n('calling__pre-call-info--empty-group');
break;
case 1:
subtitle = i18n('calling__pre-call-info--will-notify-1', [
memberNames[0],
]);
break;
case 2:
subtitle = i18n('calling__pre-call-info--will-notify-2', {
first: memberNames[0],
second: memberNames[1],
});
break;
case 3:
subtitle = i18n('calling__pre-call-info--will-notify-3', {
first: memberNames[0],
second: memberNames[1],
third: memberNames[2],
});
break;
default:
subtitle = i18n('calling__pre-call-info--will-notify-many', {
first: memberNames[0],
second: memberNames[1],
others: String(memberNames.length - 2),
});
break;
}
} else {
throw missingCaseError(conversation.type);
}
return (
<div className="module-CallingPreCallInfo">
<Avatar
avatarPath={conversation.avatarPath}
color={conversation.color}
acceptedMessageRequest={conversation.acceptedMessageRequest}
conversationType={conversation.type}
isMe={conversation.isMe}
name={conversation.name}
noteToSelf={false}
phoneNumber={conversation.phoneNumber}
profileName={conversation.profileName}
sharedGroupNames={conversation.sharedGroupNames}
size={AvatarSize.ONE_HUNDRED_TWELVE}
title={conversation.title}
unblurredAvatarPath={conversation.unblurredAvatarPath}
i18n={i18n}
/>
<div className="module-CallingPreCallInfo__title">
<Emojify text={conversation.title} />
</div>
<div className="module-CallingPreCallInfo__subtitle">{subtitle}</div>
</div>
);
};
function getParticipantName(
participant: Readonly<Pick<ConversationType, 'firstName' | 'title'>>
): string {
return participant.firstName || participant.title;
}

View File

@ -115,9 +115,23 @@ const mapStateToActiveCallProp = (
};
case CallMode.Group: {
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
const groupMembers: Array<ConversationType> = [];
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<ConversationType> = [];
const { memberships = [] } = conversation;
for (let i = 0; i < memberships.length; i += 1) {
const { conversationId } = memberships[i];
const member = conversationSelectorByUuid(conversationId);
if (!member) {
window.log.error('Group member has no corresponding conversation');
continue;
}
groupMembers.push(member);
}
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
const remoteParticipant = call.remoteParticipants[i];
@ -183,6 +197,7 @@ const mapStateToActiveCallProp = (
connectionState: call.connectionState,
conversationsWithSafetyNumberChanges,
deviceCount: call.peekInfo.deviceCount,
groupMembers,
joinState: call.joinState,
maxDevices: call.peekInfo.maxDevices,
peekedParticipants,

View File

@ -60,6 +60,7 @@ type ActiveGroupCallType = ActiveCallBaseType & {
joinState: GroupCallJoinState;
maxDevices: number;
deviceCount: number;
groupMembers: Array<Pick<ConversationType, 'firstName' | 'title' | 'uuid'>>;
peekedParticipants: Array<ConversationType>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
};