Restore call view mode after presentation end

This commit is contained in:
Fedor Indutny 2022-05-25 11:03:27 -07:00 committed by GitHub
parent 9e1528fa24
commit 80c90540f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 289 additions and 51 deletions

View file

@ -12,6 +12,7 @@ import {
CallEndedReason, CallEndedReason,
CallMode, CallMode,
CallState, CallState,
CallViewMode,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
} from '../types/Calling'; } from '../types/Calling';
@ -51,7 +52,11 @@ const getCommonActiveCallData = () => ({
hasLocalAudio: boolean('hasLocalAudio', true), hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false), hasLocalVideo: boolean('hasLocalVideo', false),
localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0), localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0),
isInSpeakerView: boolean('isInSpeakerView', false), viewMode: select(
'viewMode',
[CallViewMode.Grid, CallViewMode.Presentation, CallViewMode.Speaker],
CallViewMode.Grid
),
outgoingRing: boolean('outgoingRing', true), outgoingRing: boolean('outgoingRing', true),
pip: boolean('pip', false), pip: boolean('pip', false),
settingsDialogOpen: boolean('settingsDialogOpen', false), settingsDialogOpen: boolean('settingsDialogOpen', false),
@ -101,6 +106,8 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
setOutgoingRing: action('set-outgoing-ring'), setOutgoingRing: action('set-outgoing-ring'),
startCall: action('start-call'), startCall: action('start-call'),
stopRingtone: action('stop-ringtone'), stopRingtone: action('stop-ringtone'),
switchToPresentationView: action('switch-to-presentation-view'),
switchFromPresentationView: action('switch-from-presentation-view'),
theme: ThemeType.light, theme: ThemeType.light,
toggleParticipants: action('toggle-participants'), toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),

View file

@ -91,6 +91,8 @@ export type PropsType = {
setPresenting: (_?: PresentedSource) => void; setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void; setRendererCanvas: (_: SetRendererCanvasType) => void;
stopRingtone: () => unknown; stopRingtone: () => unknown;
switchToPresentationView: () => void;
switchFromPresentationView: () => void;
hangUpActiveCall: () => void; hangUpActiveCall: () => void;
theme: ThemeType; theme: ThemeType;
togglePip: () => void; togglePip: () => void;
@ -127,6 +129,8 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
setRendererCanvas, setRendererCanvas,
setOutgoingRing, setOutgoingRing,
startCall, startCall,
switchToPresentationView,
switchFromPresentationView,
theme, theme,
toggleParticipants, toggleParticipants,
togglePip, togglePip,
@ -270,8 +274,9 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation} setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
setLocalPreview={setLocalPreview} setLocalPreview={setLocalPreview}
setRendererCanvas={setRendererCanvas} setRendererCanvas={setRendererCanvas}
switchToPresentationView={switchToPresentationView}
switchFromPresentationView={switchFromPresentationView}
togglePip={togglePip} togglePip={togglePip}
toggleSpeakerView={toggleSpeakerView}
/> />
); );
} }
@ -313,6 +318,8 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
setLocalVideo={setLocalVideo} setLocalVideo={setLocalVideo}
setPresenting={setPresenting} setPresenting={setPresenting}
stickyControls={showParticipantsList} stickyControls={showParticipantsList}
switchToPresentationView={switchToPresentationView}
switchFromPresentationView={switchFromPresentationView}
toggleScreenRecordingPermissionsDialog={ toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog toggleScreenRecordingPermissionsDialog
} }

View file

@ -10,6 +10,7 @@ import { action } from '@storybook/addon-actions';
import type { GroupCallRemoteParticipantType } from '../types/Calling'; import type { GroupCallRemoteParticipantType } from '../types/Calling';
import { import {
CallMode, CallMode,
CallViewMode,
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
@ -45,7 +46,7 @@ type OverridePropsBase = {
hasLocalAudio?: boolean; hasLocalAudio?: boolean;
hasLocalVideo?: boolean; hasLocalVideo?: boolean;
localAudioLevel?: number; localAudioLevel?: number;
isInSpeakerView?: boolean; viewMode?: CallViewMode;
}; };
type DirectCallOverrideProps = OverridePropsBase & { type DirectCallOverrideProps = OverridePropsBase & {
@ -126,9 +127,10 @@ const createActiveCallProp = (
[0, 0.5, 1], [0, 0.5, 1],
overrideProps.localAudioLevel || 0 overrideProps.localAudioLevel || 0
), ),
isInSpeakerView: boolean( viewMode: select(
'isInSpeakerView', 'viewMode',
overrideProps.isInSpeakerView || false [CallViewMode.Grid, CallViewMode.Speaker, CallViewMode.Presentation],
overrideProps.viewMode || CallViewMode.Grid
), ),
outgoingRing: true, outgoingRing: true,
pip: false, pip: false,
@ -172,6 +174,8 @@ const createProps = (
setPresenting: action('toggle-presenting'), setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'), setRendererCanvas: action('set-renderer-canvas'),
stickyControls: boolean('stickyControls', false), stickyControls: boolean('stickyControls', false),
switchToPresentationView: action('switch-to-presentation-view'),
switchFromPresentationView: action('switch-from-presentation-view'),
toggleParticipants: action('toggle-participants'), toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),
toggleScreenRecordingPermissionsDialog: action( toggleScreenRecordingPermissionsDialog: action(

View file

@ -12,6 +12,7 @@ import type {
SetLocalVideoType, SetLocalVideoType,
SetRendererCanvasType, SetRendererCanvasType,
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import { isInSpeakerView } from '../state/selectors/calling';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import { CallingHeader } from './CallingHeader'; import { CallingHeader } from './CallingHeader';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo'; import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
@ -61,6 +62,8 @@ export type PropsType = {
setPresenting: (_?: PresentedSource) => void; setPresenting: (_?: PresentedSource) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void; setRendererCanvas: (_: SetRendererCanvasType) => void;
stickyControls: boolean; stickyControls: boolean;
switchToPresentationView: () => void;
switchFromPresentationView: () => void;
toggleParticipants: () => void; toggleParticipants: () => void;
togglePip: () => void; togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown; toggleScreenRecordingPermissionsDialog: () => unknown;
@ -120,6 +123,8 @@ export const CallScreen: React.FC<PropsType> = ({
setPresenting, setPresenting,
setRendererCanvas, setRendererCanvas,
stickyControls, stickyControls,
switchToPresentationView,
switchFromPresentationView,
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleScreenRecordingPermissionsDialog, toggleScreenRecordingPermissionsDialog,
@ -131,18 +136,17 @@ export const CallScreen: React.FC<PropsType> = ({
hasLocalAudio, hasLocalAudio,
hasLocalVideo, hasLocalVideo,
localAudioLevel, localAudioLevel,
isInSpeakerView,
presentingSource, presentingSource,
remoteParticipants, remoteParticipants,
showNeedsScreenRecordingPermissionsWarning, showNeedsScreenRecordingPermissionsWarning,
showParticipantsList, showParticipantsList,
} = activeCall; } = activeCall;
useActivateSpeakerViewOnPresenting( useActivateSpeakerViewOnPresenting({
remoteParticipants, remoteParticipants,
isInSpeakerView, switchToPresentationView,
toggleSpeakerView switchFromPresentationView,
); });
const activeCallShortcuts = useActiveCallShortcuts(hangUpActiveCall); const activeCallShortcuts = useActiveCallShortcuts(hangUpActiveCall);
useKeyboardShortcuts(activeCallShortcuts); useKeyboardShortcuts(activeCallShortcuts);
@ -293,7 +297,7 @@ export const CallScreen: React.FC<PropsType> = ({
<GroupCallRemoteParticipants <GroupCallRemoteParticipants
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n} i18n={i18n}
isInSpeakerView={isInSpeakerView} isInSpeakerView={isInSpeakerView(activeCall)}
remoteParticipants={activeCall.remoteParticipants} remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
remoteAudioLevels={activeCall.remoteAudioLevels} remoteAudioLevels={activeCall.remoteAudioLevels}
@ -448,7 +452,7 @@ export const CallScreen: React.FC<PropsType> = ({
> >
<CallingHeader <CallingHeader
i18n={i18n} i18n={i18n}
isInSpeakerView={isInSpeakerView} isInSpeakerView={isInSpeakerView(activeCall)}
isGroupCall={isGroupCall} isGroupCall={isGroupCall}
message={headerMessage} message={headerMessage}
participantCount={participantCount} participantCount={participantCount}

View file

@ -14,6 +14,7 @@ import { CallingPip } from './CallingPip';
import type { ActiveCallType } from '../types/Calling'; import type { ActiveCallType } from '../types/Calling';
import { import {
CallMode, CallMode,
CallViewMode,
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
@ -40,7 +41,11 @@ const getCommonActiveCallData = () => ({
hasLocalAudio: boolean('hasLocalAudio', true), hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false), hasLocalVideo: boolean('hasLocalVideo', false),
localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0), localAudioLevel: select('localAudioLevel', [0, 0.5, 1], 0),
isInSpeakerView: boolean('isInSpeakerView', false), viewMode: select(
'viewMode',
[CallViewMode.Grid, CallViewMode.Speaker, CallViewMode.Presentation],
CallViewMode.Grid
),
joinedAt: Date.now(), joinedAt: Date.now(),
outgoingRing: true, outgoingRing: true,
pip: true, pip: true,
@ -67,8 +72,9 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
setGroupCallVideoRequest: action('set-group-call-video-request'), setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalPreview: action('set-local-preview'), setLocalPreview: action('set-local-preview'),
setRendererCanvas: action('set-renderer-canvas'), setRendererCanvas: action('set-renderer-canvas'),
switchFromPresentationView: action('switch-to-presentation-view'),
switchToPresentationView: action('switch-to-presentation-view'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),
toggleSpeakerView: action('toggleSpeakerView'),
}); });
const story = storiesOf('Components/CallingPip', module); const story = storiesOf('Components/CallingPip', module);

View file

@ -57,8 +57,9 @@ export type PropsType = {
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void; setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;
setLocalPreview: (_: SetLocalPreviewType) => void; setLocalPreview: (_: SetLocalPreviewType) => void;
setRendererCanvas: (_: SetRendererCanvasType) => void; setRendererCanvas: (_: SetRendererCanvasType) => void;
switchToPresentationView: () => void;
switchFromPresentationView: () => void;
togglePip: () => void; togglePip: () => void;
toggleSpeakerView: () => void;
}; };
const PIP_HEIGHT = 156; const PIP_HEIGHT = 156;
@ -75,8 +76,9 @@ export const CallingPip = ({
setGroupCallVideoRequest, setGroupCallVideoRequest,
setLocalPreview, setLocalPreview,
setRendererCanvas, setRendererCanvas,
switchToPresentationView,
switchFromPresentationView,
togglePip, togglePip,
toggleSpeakerView,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element | null => {
const videoContainerRef = React.useRef<null | HTMLDivElement>(null); const videoContainerRef = React.useRef<null | HTMLDivElement>(null);
const localVideoRef = React.useRef(null); const localVideoRef = React.useRef(null);
@ -88,11 +90,11 @@ export const CallingPip = ({
offsetY: PIP_TOP_MARGIN, offsetY: PIP_TOP_MARGIN,
}); });
useActivateSpeakerViewOnPresenting( useActivateSpeakerViewOnPresenting({
activeCall.remoteParticipants, remoteParticipants: activeCall.remoteParticipants,
activeCall.isInSpeakerView, switchToPresentationView,
toggleSpeakerView switchFromPresentationView,
); });
React.useEffect(() => { React.useEffect(() => {
setLocalPreview({ element: localVideoRef }); setLocalPreview({ element: localVideoRef });

View file

@ -11,19 +11,30 @@ type RemoteParticipant = {
uuid?: string; uuid?: string;
}; };
export function useActivateSpeakerViewOnPresenting( export function useActivateSpeakerViewOnPresenting({
remoteParticipants: ReadonlyArray<RemoteParticipant>, remoteParticipants,
isInSpeakerView: boolean, switchToPresentationView,
toggleSpeakerView: () => void switchFromPresentationView,
): void { }: {
remoteParticipants: ReadonlyArray<RemoteParticipant>;
switchToPresentationView: () => void;
switchFromPresentationView: () => void;
}): void {
const presenterUuid = remoteParticipants.find( const presenterUuid = remoteParticipants.find(
participant => participant.presenting participant => participant.presenting
)?.uuid; )?.uuid;
const prevPresenterUuid = usePrevious(presenterUuid, presenterUuid); const prevPresenterUuid = usePrevious(presenterUuid, presenterUuid);
useEffect(() => { useEffect(() => {
if (prevPresenterUuid !== presenterUuid && !isInSpeakerView) { if (prevPresenterUuid !== presenterUuid && presenterUuid) {
toggleSpeakerView(); switchToPresentationView();
} else if (prevPresenterUuid && !presenterUuid) {
switchFromPresentationView();
} }
}, [isInSpeakerView, presenterUuid, prevPresenterUuid, toggleSpeakerView]); }, [
presenterUuid,
prevPresenterUuid,
switchToPresentationView,
switchFromPresentationView,
]);
} }

View file

@ -27,6 +27,7 @@ import type {
import { import {
CallingDeviceType, CallingDeviceType,
CallMode, CallMode,
CallViewMode,
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
@ -104,7 +105,7 @@ export type ActiveCallStateType = {
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
localAudioLevel: number; localAudioLevel: number;
isInSpeakerView: boolean; viewMode: CallViewMode;
joinedAt?: number; joinedAt?: number;
outgoingRing: boolean; outgoingRing: boolean;
pip: boolean; pip: boolean;
@ -427,6 +428,8 @@ 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'; const TOGGLE_SPEAKER_VIEW = 'calling/TOGGLE_SPEAKER_VIEW';
const SWITCH_TO_PRESENTATION_VIEW = 'calling/SWITCH_TO_PRESENTATION_VIEW';
const SWITCH_FROM_PRESENTATION_VIEW = 'calling/SWITCH_FROM_PRESENTATION_VIEW';
type AcceptCallPendingActionType = { type AcceptCallPendingActionType = {
type: 'calling/ACCEPT_CALL_PENDING'; type: 'calling/ACCEPT_CALL_PENDING';
@ -597,6 +600,14 @@ type ToggleSpeakerViewActionType = {
type: 'calling/TOGGLE_SPEAKER_VIEW'; type: 'calling/TOGGLE_SPEAKER_VIEW';
}; };
type SwitchToPresentationViewActionType = {
type: 'calling/SWITCH_TO_PRESENTATION_VIEW';
};
type SwitchFromPresentationViewActionType = {
type: 'calling/SWITCH_FROM_PRESENTATION_VIEW';
};
export type CallingActionType = export type CallingActionType =
| AcceptCallPendingActionType | AcceptCallPendingActionType
| CancelCallActionType | CancelCallActionType
@ -632,7 +643,9 @@ export type CallingActionType =
| TogglePipActionType | TogglePipActionType
| SetPresentingFulfilledActionType | SetPresentingFulfilledActionType
| ToggleSettingsActionType | ToggleSettingsActionType
| ToggleSpeakerViewActionType; | ToggleSpeakerViewActionType
| SwitchToPresentationViewActionType
| SwitchFromPresentationViewActionType;
// Action Creators // Action Creators
@ -1314,6 +1327,18 @@ function toggleSpeakerView(): ToggleSpeakerViewActionType {
}; };
} }
function switchToPresentationView(): SwitchToPresentationViewActionType {
return {
type: SWITCH_TO_PRESENTATION_VIEW,
};
}
function switchFromPresentationView(): SwitchFromPresentationViewActionType {
return {
type: SWITCH_FROM_PRESENTATION_VIEW,
};
}
export const actions = { export const actions = {
acceptCall, acceptCall,
callStateChange, callStateChange,
@ -1349,6 +1374,8 @@ export const actions = {
setOutgoingRing, setOutgoingRing,
startCall, startCall,
startCallingLobby, startCallingLobby,
switchToPresentationView,
switchFromPresentationView,
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleScreenRecordingPermissionsDialog, toggleScreenRecordingPermissionsDialog,
@ -1457,7 +1484,7 @@ export function reducer(
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1485,7 +1512,7 @@ export function reducer(
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1508,7 +1535,7 @@ export function reducer(
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: action.payload.asVideoCall, hasLocalVideo: action.payload.asVideoCall,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1662,7 +1689,7 @@ export function reducer(
hasLocalAudio: action.payload.hasLocalAudio, hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo, hasLocalVideo: action.payload.hasLocalVideo,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
pip: false, pip: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
settingsDialogOpen: false, settingsDialogOpen: false,
@ -2129,11 +2156,61 @@ export function reducer(
return state; return state;
} }
let newViewMode: CallViewMode;
if (activeCallState.viewMode === CallViewMode.Grid) {
newViewMode = CallViewMode.Speaker;
} else {
// This will switch presentation/speaker to grid
newViewMode = CallViewMode.Grid;
}
return { return {
...state, ...state,
activeCallState: { activeCallState: {
...activeCallState, ...activeCallState,
isInSpeakerView: !activeCallState.isInSpeakerView, viewMode: newViewMode,
},
};
}
if (action.type === SWITCH_TO_PRESENTATION_VIEW) {
const { activeCallState } = state;
if (!activeCallState) {
log.warn('Cannot switch to speaker view when there is no active call');
return state;
}
// "Presentation" mode reverts to "Grid" when the call is over so don't
// switch it if it is in "Speaker" mode.
if (activeCallState.viewMode === CallViewMode.Speaker) {
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
viewMode: CallViewMode.Presentation,
},
};
}
if (action.type === SWITCH_FROM_PRESENTATION_VIEW) {
const { activeCallState } = state;
if (!activeCallState) {
log.warn('Cannot switch to speaker view when there is no active call');
return state;
}
if (activeCallState.viewMode !== CallViewMode.Presentation) {
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
viewMode: CallViewMode.Grid,
}, },
}; };
} }

View file

@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { import type {
ActiveCallStateType,
CallingStateType, CallingStateType,
CallsByConversationType, CallsByConversationType,
DirectCallStateType, DirectCallStateType,
@ -13,6 +14,7 @@ import type {
import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling'; import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling';
import { getUserUuid } from './user'; import { getUserUuid } from './user';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
import { CallViewMode } from '../../types/Calling';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
export type CallStateType = DirectCallStateType | GroupCallStateType; export type CallStateType = DirectCallStateType | GroupCallStateType;
@ -71,3 +73,12 @@ export const getIncomingCall = createSelector(
return getIncomingCallHelper(callsByConversation, ourUuid); return getIncomingCallHelper(callsByConversation, ourUuid);
} }
); );
export const isInSpeakerView = (
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
): boolean => {
return Boolean(
call?.viewMode === CallViewMode.Presentation ||
call?.viewMode === CallViewMode.Speaker
);
};

View file

@ -131,7 +131,7 @@ const mapStateToActiveCallProp = (
hasLocalAudio: activeCallState.hasLocalAudio, hasLocalAudio: activeCallState.hasLocalAudio,
hasLocalVideo: activeCallState.hasLocalVideo, hasLocalVideo: activeCallState.hasLocalVideo,
localAudioLevel: activeCallState.localAudioLevel, localAudioLevel: activeCallState.localAudioLevel,
isInSpeakerView: activeCallState.isInSpeakerView, viewMode: activeCallState.viewMode,
joinedAt: activeCallState.joinedAt, joinedAt: activeCallState.joinedAt,
outgoingRing: activeCallState.outgoingRing, outgoingRing: activeCallState.outgoingRing,
pip: activeCallState.pip, pip: activeCallState.pip,

View file

@ -23,6 +23,7 @@ import { calling as callingService } from '../../../services/calling';
import { import {
CallMode, CallMode,
CallState, CallState,
CallViewMode,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
} from '../../../types/Calling'; } from '../../../types/Calling';
@ -52,7 +53,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: true, outgoingRing: true,
@ -131,7 +132,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: false, outgoingRing: false,
@ -140,6 +141,22 @@ describe('calling duck', () => {
}, },
}; };
const stateWithActivePresentationViewGroupCall = {
...stateWithGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
viewMode: CallViewMode.Presentation,
},
};
const stateWithActiveSpeakerViewGroupCall = {
...stateWithGroupCall,
activeCallState: {
...stateWithActiveGroupCall.activeCallState,
viewMode: CallViewMode.Speaker,
},
};
const ourUuid = UUID.generate().toString(); const ourUuid = UUID.generate().toString();
const getEmptyRootState = () => { const getEmptyRootState = () => {
@ -437,7 +454,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: false, outgoingRing: false,
@ -530,7 +547,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: false, outgoingRing: false,
@ -1122,7 +1139,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: false, outgoingRing: false,
@ -1651,7 +1668,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -1937,7 +1954,7 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
pip: false, pip: false,
@ -2013,7 +2030,7 @@ describe('calling duck', () => {
describe('toggleSpeakerView', () => { describe('toggleSpeakerView', () => {
const { toggleSpeakerView } = actions; const { toggleSpeakerView } = actions;
it('toggles speaker view', () => { it('toggles speaker view from grid view', () => {
const afterOneToggle = reducer( const afterOneToggle = reducer(
stateWithActiveGroupCall, stateWithActiveGroupCall,
toggleSpeakerView() toggleSpeakerView()
@ -2021,9 +2038,92 @@ describe('calling duck', () => {
const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView()); const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView());
const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView()); const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView());
assert.isTrue(afterOneToggle.activeCallState?.isInSpeakerView); assert.strictEqual(
assert.isFalse(afterTwoToggles.activeCallState?.isInSpeakerView); afterOneToggle.activeCallState?.viewMode,
assert.isTrue(afterThreeToggles.activeCallState?.isInSpeakerView); CallViewMode.Speaker
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Grid
);
assert.strictEqual(
afterThreeToggles.activeCallState?.viewMode,
CallViewMode.Speaker
);
});
it('toggles speaker view from presentation view', () => {
const afterOneToggle = reducer(
stateWithActivePresentationViewGroupCall,
toggleSpeakerView()
);
const afterTwoToggles = reducer(afterOneToggle, toggleSpeakerView());
const afterThreeToggles = reducer(afterTwoToggles, toggleSpeakerView());
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Grid
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Speaker
);
assert.strictEqual(
afterThreeToggles.activeCallState?.viewMode,
CallViewMode.Grid
);
});
});
describe('switchToPresentationView', () => {
const { switchToPresentationView, switchFromPresentationView } = actions;
it('toggles presentation view from grid view', () => {
const afterOneToggle = reducer(
stateWithActiveGroupCall,
switchToPresentationView()
);
const afterTwoToggles = reducer(
afterOneToggle,
switchToPresentationView()
);
const finalState = reducer(
afterOneToggle,
switchFromPresentationView()
);
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Presentation
);
assert.strictEqual(
afterTwoToggles.activeCallState?.viewMode,
CallViewMode.Presentation
);
assert.strictEqual(
finalState.activeCallState?.viewMode,
CallViewMode.Grid
);
});
it('does not toggle presentation view from speaker view', () => {
const afterOneToggle = reducer(
stateWithActiveSpeakerViewGroupCall,
switchToPresentationView()
);
const finalState = reducer(
afterOneToggle,
switchFromPresentationView()
);
assert.strictEqual(
afterOneToggle.activeCallState?.viewMode,
CallViewMode.Speaker
);
assert.strictEqual(
finalState.activeCallState?.viewMode,
CallViewMode.Speaker
);
}); });
}); });
}); });

View file

@ -7,6 +7,7 @@ import { noopAction } from '../../../state/ducks/noop';
import { import {
CallMode, CallMode,
CallState, CallState,
CallViewMode,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
} from '../../../types/Calling'; } from '../../../types/Calling';
@ -52,7 +53,7 @@ describe('state/selectors/calling', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
localAudioLevel: 0, localAudioLevel: 0,
isInSpeakerView: false, viewMode: CallViewMode.Grid,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedUuids: [], safetyNumberChangedUuids: [],
outgoingRing: true, outgoingRing: true,

View file

@ -11,6 +11,14 @@ export enum CallMode {
Group = 'Group', Group = 'Group',
} }
// Speaker and Presentation has the same UI, but Presentation mode will switch
// to Grid mode when the presentation is over.
export enum CallViewMode {
Grid = 'Grid',
Speaker = 'Speaker',
Presentation = 'Presentation',
}
export type PresentableSource = { export type PresentableSource = {
appIcon?: string; appIcon?: string;
id: string; id: string;
@ -29,7 +37,7 @@ type ActiveCallBaseType = {
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
localAudioLevel: number; localAudioLevel: number;
isInSpeakerView: boolean; viewMode: CallViewMode;
isSharingScreen?: boolean; isSharingScreen?: boolean;
joinedAt?: number; joinedAt?: number;
outgoingRing: boolean; outgoingRing: boolean;