Group calling: add speaker view

This commit is contained in:
Evan Hahn 2021-01-08 16:57:54 -06:00 committed by Scott Nonnenberg
parent fbfcdbf84e
commit b281420a40
16 changed files with 174 additions and 19 deletions

View File

@ -3147,6 +3147,14 @@
"message": "Fullscreen call", "message": "Fullscreen call",
"description": "Title for picture-in-picture toggle" "description": "Title for picture-in-picture toggle"
}, },
"calling__switch-view--to-grid": {
"message": "Switch to grid view",
"description": "Title for grid/speaker view toggle when on a call"
},
"calling__switch-view--to-speaker": {
"message": "Switch to speaker view",
"description": "Title for grid/speaker view toggle when on a call"
},
"calling__hangup": { "calling__hangup": {
"message": "Leave call", "message": "Leave call",
"description": "Title for hang up button" "description": "Title for hang up button"

View File

@ -0,0 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m21.5 8.5h-4a1 1 0 0 1 -1-1v-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1zm-6.5-1v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm-7.5 0v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm7.5 5.5v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm-7.5 0v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm7.5 5.5v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm-7.5 0v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm15-5.5v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1zm0 5.5v-2a1 1 0 0 0 -1-1h-4a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1z"/></svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@ -0,0 +1 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m15 19.5h-12.5a1 1 0 0 1 -1-1v-13a1 1 0 0 1 1-1h12.5a1 1 0 0 1 1 1v13a1 1 0 0 1 -1 1zm7.5-12v-2a1 1 0 0 0 -1-1h-3a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1zm0 5.5v-2a1 1 0 0 0 -1-1h-3a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1zm0 5.5v-2a1 1 0 0 0 -1-1h-3a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1z"/></svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@ -6090,11 +6090,13 @@ button.module-image__border-overlay:focus {
} }
.module-calling-button { .module-calling-button {
$size: 22px;
&__participants { &__participants {
@include color-svg('../images/icons/v2/group-solid-24.svg', $color-white); @include color-svg('../images/icons/v2/group-solid-24.svg', $color-white);
display: inline-block; display: inline-block;
height: 22px; height: $size;
width: 22px; width: $size;
&--container { &--container {
@include button-reset; @include button-reset;
@ -6123,14 +6125,32 @@ button.module-image__border-overlay:focus {
'../images/icons/v2/settings-solid-16.svg', '../images/icons/v2/settings-solid-16.svg',
$color-white $color-white
); );
height: 22px; height: $size;
width: 22px; width: $size;
}
&__grid-view {
@include color-svg(
'../images/icons/v2/grid-view-solid-24.svg',
$color-white
);
height: $size;
width: $size;
}
&__speaker-view {
@include color-svg(
'../images/icons/v2/speaker-view-solid-24.svg',
$color-white
);
height: $size;
width: $size;
} }
&__pip { &__pip {
@include color-svg('../images/icons/v2/pip-minimize-24.svg', $color-white); @include color-svg('../images/icons/v2/pip-minimize-24.svg', $color-white);
height: 22px; height: $size;
width: 22px; width: $size;
} }
} }

View File

@ -42,6 +42,7 @@ const getCommonActiveCallData = () => ({
joinedAt: Date.now(), joinedAt: Date.now(),
hasLocalAudio: boolean('hasLocalAudio', true), hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false), hasLocalVideo: boolean('hasLocalVideo', false),
isInSpeakerView: boolean('isInSpeakerView', false),
pip: boolean('pip', false), pip: boolean('pip', false),
settingsDialogOpen: boolean('settingsDialogOpen', false), settingsDialogOpen: boolean('settingsDialogOpen', false),
showParticipantsList: boolean('showParticipantsList', false), showParticipantsList: boolean('showParticipantsList', false),
@ -87,6 +88,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
toggleParticipants: action('toggle-participants'), toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),
toggleSettings: action('toggle-settings'), toggleSettings: action('toggle-settings'),
toggleSpeakerView: action('toggle-speaker-view'),
}); });
const story = storiesOf('Components/CallManager', module); const story = storiesOf('Components/CallManager', module);

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@ -73,6 +73,7 @@ export interface PropsType {
hangUp: (_: HangUpType) => void; hangUp: (_: HangUpType) => void;
togglePip: () => void; togglePip: () => void;
toggleSettings: () => void; toggleSettings: () => void;
toggleSpeakerView: () => void;
} }
interface ActiveCallManagerPropsType extends PropsType { interface ActiveCallManagerPropsType extends PropsType {
@ -100,6 +101,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleSettings, toggleSettings,
toggleSpeakerView,
}) => { }) => {
const { const {
conversation, conversation,
@ -265,6 +267,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
toggleParticipants={toggleParticipants} toggleParticipants={toggleParticipants}
togglePip={togglePip} togglePip={togglePip}
toggleSettings={toggleSettings} toggleSettings={toggleSettings}
toggleSpeakerView={toggleSpeakerView}
/> />
{settingsDialogOpen && renderDeviceSelection()} {settingsDialogOpen && renderDeviceSelection()}
{showParticipantsList && activeCall.callMode === CallMode.Group ? ( {showParticipantsList && activeCall.callMode === CallMode.Group ? (

View File

@ -44,6 +44,7 @@ const conversation = {
interface OverridePropsBase { interface OverridePropsBase {
hasLocalAudio?: boolean; hasLocalAudio?: boolean;
hasLocalVideo?: boolean; hasLocalVideo?: boolean;
isInSpeakerView?: boolean;
} }
interface DirectCallOverrideProps extends OverridePropsBase { interface DirectCallOverrideProps extends OverridePropsBase {
@ -113,6 +114,10 @@ const createActiveCallProp = (
'hasLocalVideo', 'hasLocalVideo',
overrideProps.hasLocalVideo || false overrideProps.hasLocalVideo || false
), ),
isInSpeakerView: boolean(
'isInSpeakerView',
overrideProps.isInSpeakerView || false
),
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
showParticipantsList: false, showParticipantsList: false,
@ -152,6 +157,7 @@ const createProps = (
toggleParticipants: action('toggle-participants'), toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),
toggleSettings: action('toggle-settings'), toggleSettings: action('toggle-settings'),
toggleSpeakerView: action('toggle-speaker-view'),
}); });
const story = storiesOf('Components/CallScreen', module); const story = storiesOf('Components/CallScreen', module);

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useRef, useEffect, useCallback } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
@ -53,6 +53,7 @@ export type PropsType = {
toggleParticipants: () => void; toggleParticipants: () => void;
togglePip: () => void; togglePip: () => void;
toggleSettings: () => void; toggleSettings: () => void;
toggleSpeakerView: () => void;
}; };
export const CallScreen: React.FC<PropsType> = ({ export const CallScreen: React.FC<PropsType> = ({
@ -71,6 +72,7 @@ export const CallScreen: React.FC<PropsType> = ({
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleSettings, toggleSettings,
toggleSpeakerView,
}) => { }) => {
const { const {
conversation, conversation,
@ -190,6 +192,7 @@ export const CallScreen: React.FC<PropsType> = ({
<GroupCallRemoteParticipants <GroupCallRemoteParticipants
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n} i18n={i18n}
isInSpeakerView={activeCall.isInSpeakerView}
remoteParticipants={activeCall.remoteParticipants} remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
/> />
@ -244,6 +247,7 @@ export const CallScreen: React.FC<PropsType> = ({
<CallingHeader <CallingHeader
canPip canPip
i18n={i18n} i18n={i18n}
isInSpeakerView={activeCall.isInSpeakerView}
isGroupCall={activeCall.callMode === CallMode.Group} isGroupCall={activeCall.callMode === CallMode.Group}
message={headerMessage} message={headerMessage}
participantCount={participantCount} participantCount={participantCount}
@ -252,6 +256,7 @@ export const CallScreen: React.FC<PropsType> = ({
toggleParticipants={toggleParticipants} toggleParticipants={toggleParticipants}
togglePip={togglePip} togglePip={togglePip}
toggleSettings={toggleSettings} toggleSettings={toggleSettings}
toggleSpeakerView={toggleSpeakerView}
/> />
</div> </div>
{remoteParticipantsElement} {remoteParticipantsElement}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
@ -10,6 +10,7 @@ import { Theme } from '../util/theme';
export type PropsType = { export type PropsType = {
canPip?: boolean; canPip?: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isInSpeakerView?: boolean;
isGroupCall?: boolean; isGroupCall?: boolean;
message?: string; message?: string;
participantCount: number; participantCount: number;
@ -18,11 +19,13 @@ export type PropsType = {
toggleParticipants?: () => void; toggleParticipants?: () => void;
togglePip?: () => void; togglePip?: () => void;
toggleSettings: () => void; toggleSettings: () => void;
toggleSpeakerView?: () => void;
}; };
export const CallingHeader = ({ export const CallingHeader = ({
canPip = false, canPip = false,
i18n, i18n,
isInSpeakerView,
isGroupCall = false, isGroupCall = false,
message, message,
participantCount, participantCount,
@ -31,6 +34,7 @@ export const CallingHeader = ({
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleSettings, toggleSettings,
toggleSpeakerView,
}: PropsType): JSX.Element => ( }: PropsType): JSX.Element => (
<div className="module-calling__header"> <div className="module-calling__header">
{title ? ( {title ? (
@ -80,6 +84,33 @@ export const CallingHeader = ({
/> />
</Tooltip> </Tooltip>
</div> </div>
{isGroupCall && participantCount > 2 && toggleSpeakerView && (
<div className="module-calling-tools__button">
<Tooltip
content={i18n(
isInSpeakerView
? 'calling__switch-view--to-grid'
: 'calling__switch-view--to-speaker'
)}
theme={Theme.Dark}
>
<button
aria-label={i18n(
isInSpeakerView
? 'calling__switch-view--to-grid'
: 'calling__switch-view--to-speaker'
)}
className={
isInSpeakerView
? 'module-calling-button__grid-view'
: 'module-calling-button__speaker-view'
}
onClick={toggleSpeakerView}
type="button"
/>
</Tooltip>
</div>
)}
{canPip && ( {canPip && (
<div className="module-calling-tools__button"> <div className="module-calling-tools__button">
<Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}> <Tooltip content={i18n('calling__pip--on')} theme={Theme.Dark}>

View File

@ -39,6 +39,7 @@ const getCommonActiveCallData = () => ({
conversation, conversation,
hasLocalAudio: boolean('hasLocalAudio', true), hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false), hasLocalVideo: boolean('hasLocalVideo', false),
isInSpeakerView: boolean('isInSpeakerView', false),
joinedAt: Date.now(), joinedAt: Date.now(),
pip: true, pip: true,
settingsDialogOpen: false, settingsDialogOpen: false,

View File

@ -38,6 +38,7 @@ interface GridArrangement {
interface PropsType { interface PropsType {
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
i18n: LocalizerType; i18n: LocalizerType;
isInSpeakerView: boolean;
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>; remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void; setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
} }
@ -68,6 +69,7 @@ interface PropsType {
export const GroupCallRemoteParticipants: React.FC<PropsType> = ({ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
i18n, i18n,
isInSpeakerView,
remoteParticipants, remoteParticipants,
setGroupCallVideoRequest, setGroupCallVideoRequest,
}) => { }) => {
@ -122,6 +124,14 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
[remoteParticipants] [remoteParticipants]
); );
const gridParticipants: Array<GroupCallRemoteParticipantType> = useMemo(() => { const gridParticipants: Array<GroupCallRemoteParticipantType> = useMemo(() => {
if (!sortedParticipants.length) {
return [];
}
const candidateParticipants = isInSpeakerView
? [sortedParticipants[0]]
: sortedParticipants;
// Imagine that we laid out all of the rows end-to-end. That's the maximum total // Imagine that we laid out all of the rows end-to-end. That's the maximum total
// width. So if there were 5 rows and the container was 100px wide, then we can't // width. So if there were 5 rows and the container was 100px wide, then we can't
// possibly fit more than 500px of participants. // possibly fit more than 500px of participants.
@ -130,11 +140,16 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
// We do the same thing for participants, "laying them out end-to-end" until they // We do the same thing for participants, "laying them out end-to-end" until they
// exceed the maximum total width. // exceed the maximum total width.
let totalWidth = 0; let totalWidth = 0;
return takeWhile(sortedParticipants, remoteParticipant => { return takeWhile(candidateParticipants, remoteParticipant => {
totalWidth += remoteParticipant.videoAspectRatio * MIN_RENDERED_HEIGHT; totalWidth += remoteParticipant.videoAspectRatio * MIN_RENDERED_HEIGHT;
return totalWidth < maxTotalWidth; return totalWidth < maxTotalWidth;
}).sort(stableParticipantComparator); }).sort(stableParticipantComparator);
}, [maxRowCount, containerDimensions.width, sortedParticipants]); }, [
containerDimensions.width,
isInSpeakerView,
maxRowCount,
sortedParticipants,
]);
const overflowedParticipants: Array<GroupCallRemoteParticipantType> = useMemo( const overflowedParticipants: Array<GroupCallRemoteParticipantType> = useMemo(
() => () =>
sortedParticipants sortedParticipants

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
@ -68,12 +68,13 @@ export interface GroupCallStateType {
export interface ActiveCallStateType { export interface ActiveCallStateType {
conversationId: string; conversationId: string;
joinedAt?: number;
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
isInSpeakerView: boolean;
joinedAt?: number;
pip: boolean; pip: boolean;
settingsDialogOpen: boolean;
safetyNumberChangedUuids: Array<string>; safetyNumberChangedUuids: Array<string>;
settingsDialogOpen: boolean;
showParticipantsList: boolean; showParticipantsList: boolean;
} }
@ -243,6 +244,7 @@ const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS'; const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
const TOGGLE_PIP = 'calling/TOGGLE_PIP'; const TOGGLE_PIP = 'calling/TOGGLE_PIP';
const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS'; const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS';
const TOGGLE_SPEAKER_VIEW = 'calling/TOGGLE_SPEAKER_VIEW';
type AcceptCallPendingActionType = { type AcceptCallPendingActionType = {
type: 'calling/ACCEPT_CALL_PENDING'; type: 'calling/ACCEPT_CALL_PENDING';
@ -365,6 +367,10 @@ type ToggleSettingsActionType = {
type: 'calling/TOGGLE_SETTINGS'; type: 'calling/TOGGLE_SETTINGS';
}; };
type ToggleSpeakerViewActionType = {
type: 'calling/TOGGLE_SPEAKER_VIEW';
};
export type CallingActionType = export type CallingActionType =
| AcceptCallPendingActionType | AcceptCallPendingActionType
| CancelCallActionType | CancelCallActionType
@ -389,7 +395,8 @@ export type CallingActionType =
| StartDirectCallActionType | StartDirectCallActionType
| ToggleParticipantsActionType | ToggleParticipantsActionType
| TogglePipActionType | TogglePipActionType
| ToggleSettingsActionType; | ToggleSettingsActionType
| ToggleSpeakerViewActionType;
// Action Creators // Action Creators
@ -856,6 +863,12 @@ function toggleSettings(): ToggleSettingsActionType {
}; };
} }
function toggleSpeakerView(): ToggleSpeakerViewActionType {
return {
type: TOGGLE_SPEAKER_VIEW,
};
}
export const actions = { export const actions = {
acceptCall, acceptCall,
cancelCall, cancelCall,
@ -884,6 +897,7 @@ export const actions = {
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleSettings, toggleSettings,
toggleSpeakerView,
}; };
export type ActionsType = typeof actions; export type ActionsType = typeof actions;
@ -974,6 +988,7 @@ export function reducer(
conversationId: action.payload.conversationId, conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
isInSpeakerView: false,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -999,6 +1014,7 @@ export function reducer(
conversationId: action.payload.conversationId, conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
isInSpeakerView: false,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1019,6 +1035,7 @@ export function reducer(
conversationId: action.payload.conversationId, conversationId: action.payload.conversationId,
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: action.payload.asVideoCall, hasLocalVideo: action.payload.asVideoCall,
isInSpeakerView: false,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1084,6 +1101,7 @@ export function reducer(
conversationId: action.payload.conversationId, conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
isInSpeakerView: false,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1409,6 +1427,24 @@ export function reducer(
}; };
} }
if (action.type === TOGGLE_SPEAKER_VIEW) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn(
'Cannot toggle speaker view when there is no active call'
);
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
isInSpeakerView: !activeCallState.isInSpeakerView,
},
};
}
if (action.type === MARK_CALL_UNTRUSTED) { if (action.type === MARK_CALL_UNTRUSTED) {
const { activeCallState } = state; const { activeCallState } = state;
if (!activeCallState) { if (!activeCallState) {

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
@ -75,6 +75,7 @@ const mapStateToActiveCallProp = (
conversation, conversation,
hasLocalAudio: activeCallState.hasLocalAudio, hasLocalAudio: activeCallState.hasLocalAudio,
hasLocalVideo: activeCallState.hasLocalVideo, hasLocalVideo: activeCallState.hasLocalVideo,
isInSpeakerView: activeCallState.isInSpeakerView,
joinedAt: activeCallState.joinedAt, joinedAt: activeCallState.joinedAt,
pip: activeCallState.pip, pip: activeCallState.pip,
settingsDialogOpen: activeCallState.settingsDialogOpen, settingsDialogOpen: activeCallState.settingsDialogOpen,

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
@ -43,6 +43,7 @@ describe('calling duck', () => {
conversationId: 'fake-direct-call-conversation-id', conversationId: 'fake-direct-call-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -98,6 +99,7 @@ describe('calling duck', () => {
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -202,6 +204,7 @@ describe('calling duck', () => {
conversationId: 'fake-direct-call-conversation-id', conversationId: 'fake-direct-call-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -578,6 +581,7 @@ describe('calling duck', () => {
conversationId: 'fake-group-call-conversation-id', conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -815,6 +819,7 @@ describe('calling duck', () => {
conversationId: 'fake-conversation-id', conversationId: 'fake-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -1050,6 +1055,7 @@ describe('calling duck', () => {
conversationId: 'fake-conversation-id', conversationId: 'fake-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -1120,6 +1126,23 @@ describe('calling duck', () => {
assert.isTrue(afterThreeToggles.activeCallState?.pip); assert.isTrue(afterThreeToggles.activeCallState?.pip);
}); });
}); });
describe('toggleSpeakerView', () => {
const { toggleSpeakerView } = actions;
it('toggles speaker view', () => {
const afterOneToggle = reducer(
stateWithActiveGroupCall,
toggleSpeakerView()
);
const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView());
const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView());
assert.isTrue(afterOneToggle.activeCallState?.isInSpeakerView);
assert.isFalse(afterTwoToggles.activeCallState?.isInSpeakerView);
assert.isTrue(afterThreeToggles.activeCallState?.isInSpeakerView);
});
});
}); });
describe('helpers', () => { describe('helpers', () => {

View File

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
@ -40,6 +40,7 @@ describe('state/selectors/calling', () => {
conversationId: 'fake-direct-call-conversation-id', conversationId: 'fake-direct-call-conversation-id',
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
isInSpeakerView: false,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
@ -14,6 +14,7 @@ interface ActiveCallBaseType {
conversation: ConversationType; conversation: ConversationType;
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
isInSpeakerView: boolean;
joinedAt?: number; joinedAt?: number;
pip: boolean; pip: boolean;
settingsDialogOpen: boolean; settingsDialogOpen: boolean;