diff --git a/cypress/integration/twilio-video.spec.js b/cypress/integration/twilio-video.spec.js
index 608bce14e..0abeccd39 100644
--- a/cypress/integration/twilio-video.spec.js
+++ b/cypress/integration/twilio-video.spec.js
@@ -11,7 +11,14 @@ const getRoomName = () =>
context('A video app user', () => {
describe('before entering a room', () => {
it('should see their audio level indicator moving in the media device panel', () => {
- cy.visit('/');
+ // These tests were written before Grid View was implemented. This app now activates
+ // Grid View by default, so here we activate Presentation View before visiting the app so
+ // that the tests can pass.
+ cy.visit('/', {
+ onBeforeLoad: window => {
+ window.localStorage.setItem('grid-view-active-key', false);
+ },
+ });
cy.get('#input-user-name').type('testuser');
cy.get('#input-room-name').type(getRoomName());
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 89cb6f21c..f14a7a771 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -19,6 +19,14 @@ module.exports = (on, config) => {
args,
});
const page = (participants[name] = await browser.newPage()); // keep track of this participant for future use
+
+ // These tests were written before Grid View was implemented. This app now activates
+ // Grid View by default, so here we activate Presentation View before visiting the app so
+ // that the tests can pass.
+ await page.evaluateOnNewDocument(() => {
+ localStorage.clear();
+ localStorage.setItem('grid-view-active-key', false);
+ });
await page.goto(config.baseUrl);
await page.type('#input-user-name', name);
await page.type('#input-room-name', roomName);
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 7e1de916f..c5415ec20 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -1,7 +1,14 @@
import detectSound from './detectSound';
Cypress.Commands.add('joinRoom', (username, roomname) => {
- cy.visit('/');
+ // These tests were written before Grid View was implemented. This app now activates
+ // Grid View by default, so here we activate Presentation View before visiting the app so
+ // that the tests can pass.
+ cy.visit('/', {
+ onBeforeLoad: window => {
+ window.localStorage.setItem('grid-view-active-key', false);
+ },
+ });
cy.get('#input-user-name').type(username);
cy.get('#input-room-name').type(roomname);
cy.get('[type="submit"]').click();
diff --git a/src/components/DeviceSelectionDialog/MaxGridParticipants/MaxGridParticipants.tsx b/src/components/DeviceSelectionDialog/MaxGridParticipants/MaxGridParticipants.tsx
index 77a23746e..c9e453f1c 100644
--- a/src/components/DeviceSelectionDialog/MaxGridParticipants/MaxGridParticipants.tsx
+++ b/src/components/DeviceSelectionDialog/MaxGridParticipants/MaxGridParticipants.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { FormControl, MenuItem, Typography, Select, Grid } from '@material-ui/core';
import { useAppState } from '../../../state';
-const MAX_PARTICIPANT_OPTIONS = [9, 16, 25, 36, 49];
+const MAX_PARTICIPANT_OPTIONS = [4, 9, 16, 25];
export default function MaxGridParticipants() {
const { maxGridParticipants, setMaxGridParticipants } = useAppState();
diff --git a/src/components/GridView/GridView.test.tsx b/src/components/GridView/GridView.test.tsx
index b3b1eca83..d5c199193 100644
--- a/src/components/GridView/GridView.test.tsx
+++ b/src/components/GridView/GridView.test.tsx
@@ -3,7 +3,7 @@ import { GridView } from './GridView';
import { shallow } from 'enzyme';
import useGridLayout from '../../hooks/useGridLayout/useGridLayout';
import { useAppState } from '../../state';
-import { usePagination } from '../../hooks/usePagination/usePagination';
+import { usePagination } from './usePagination/usePagination';
const mockLocalParticipant = { identity: 'test-local-participant', sid: 0 };
const mockParticipants = [
@@ -14,10 +14,10 @@ const mockParticipants = [
];
jest.mock('../../constants', () => ({
- GRID_MODE_ASPECT_RATIO: 9 / 16,
- GRID_MODE_MARGIN: 3,
+ GRID_VIEW_ASPECT_RATIO: 9 / 16,
+ GRID_VIEW_MARGIN: 3,
}));
-jest.mock('../../hooks/useCollaborationParticipants/useCollaborationParticipants', () => () => mockParticipants);
+jest.mock('../../hooks/usePresentationParticipants/usePresentationParticipants', () => () => mockParticipants);
jest.mock('../../hooks/useVideoContext/useVideoContext', () => () => ({
room: {
localParticipant: mockLocalParticipant,
@@ -30,7 +30,7 @@ jest.mock('../../hooks/useGridLayout/useGridLayout', () =>
}))
);
-jest.mock('../../hooks/usePagination/usePagination', () => ({
+jest.mock('./usePagination/usePagination', () => ({
usePagination: jest.fn(() => ({
currentPage: 2,
totalPages: 4,
@@ -50,7 +50,7 @@ describe('the GridView component', () => {
it('should render correctly', () => {
const wrapper = shallow();
expect(wrapper).toMatchSnapshot();
- expect(useGridLayout).toHaveBeenCalledWith(5);
+ expect(useGridLayout).toHaveBeenCalledWith(9);
});
it('should not render the previous page button when the user is viewing the first page', () => {
diff --git a/src/components/GridView/GridView.tsx b/src/components/GridView/GridView.tsx
index 293fb53c0..6e88b57e8 100644
--- a/src/components/GridView/GridView.tsx
+++ b/src/components/GridView/GridView.tsx
@@ -2,80 +2,85 @@ import React from 'react';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowForward from '@material-ui/icons/ArrowForward';
import clsx from 'clsx';
-import { GRID_MODE_ASPECT_RATIO, GRID_MODE_MARGIN } from '../../constants';
-import { IconButton, makeStyles } from '@material-ui/core';
+import { GRID_VIEW_ASPECT_RATIO, GRID_VIEW_MARGIN } from '../../constants';
+import { IconButton, makeStyles, createStyles, Theme } from '@material-ui/core';
import { Pagination } from '@material-ui/lab';
import Participant from '../Participant/Participant';
import useGridLayout from '../../hooks/useGridLayout/useGridLayout';
import useVideoContext from '../../hooks/useVideoContext/useVideoContext';
-import { usePagination } from '../../hooks/usePagination/usePagination';
+import { usePagination } from './usePagination/usePagination';
import useDominantSpeaker from '../../hooks/useDominantSpeaker/useDominantSpeaker';
import useGridParticipants from '../../hooks/useGridParticipants/useGridParticipants';
+import { useAppState } from '../../state';
const CONTAINER_GUTTER = '50px';
-const useStyles = makeStyles({
- container: {
- position: 'relative',
- gridArea: '1 / 1 / 2 / 3',
- },
- participantContainer: {
- position: 'absolute',
- display: 'flex',
- top: CONTAINER_GUTTER,
- right: CONTAINER_GUTTER,
- bottom: CONTAINER_GUTTER,
- left: CONTAINER_GUTTER,
- margin: '0 auto',
- alignContent: 'center',
- flexWrap: 'wrap',
- justifyContent: 'center',
- },
- buttonContainer: {
- position: 'absolute',
- top: 0,
- bottom: 0,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- },
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ container: {
+ background: theme.gridViewBackgroundColor,
+ position: 'relative',
+ gridArea: '1 / 1 / 2 / 3',
+ },
+ participantContainer: {
+ position: 'absolute',
+ display: 'flex',
+ top: CONTAINER_GUTTER,
+ right: CONTAINER_GUTTER,
+ bottom: CONTAINER_GUTTER,
+ left: CONTAINER_GUTTER,
+ margin: '0 auto',
+ alignContent: 'center',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ buttonContainer: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
- buttonContainerLeft: {
- right: `calc(100% - ${CONTAINER_GUTTER})`,
- left: 0,
- },
- buttonContainerRight: {
- right: 0,
- left: `calc(100% - ${CONTAINER_GUTTER})`,
- },
- pagination: {
- '& .MuiPaginationItem-root': {
- color: 'white',
+ buttonContainerLeft: {
+ right: `calc(100% - ${CONTAINER_GUTTER})`,
+ left: 0,
+ },
+ buttonContainerRight: {
+ right: 0,
+ left: `calc(100% - ${CONTAINER_GUTTER})`,
+ },
+ pagination: {
+ '& .MuiPaginationItem-root': {
+ color: 'white',
+ },
+ },
+ paginationButton: {
+ color: 'black',
+ background: 'rgba(255, 255, 255, 0.8)',
+ width: '40px',
+ height: '40px',
+ '&:hover': {
+ background: 'rgba(255, 255, 255)',
+ },
},
- },
- paginationButton: {
- color: 'black',
- background: 'rgba(255, 255, 255, 0.8)',
- width: '40px',
- height: '40px',
- '&:hover': {
- background: 'rgba(255, 255, 255)',
+ paginationContainer: {
+ position: 'absolute',
+ top: `calc(100% - ${CONTAINER_GUTTER})`,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
},
- },
- paginationContainer: {
- position: 'absolute',
- top: `calc(100% - ${CONTAINER_GUTTER})`,
- right: 0,
- bottom: 0,
- left: 0,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- },
-});
+ })
+);
export function GridView() {
const classes = useStyles();
+ const { maxGridParticipants } = useAppState();
const { room } = useVideoContext();
const gridParticipants = useGridParticipants();
const dominantSpeaker = useDominantSpeaker(true);
@@ -85,10 +90,11 @@ export function GridView() {
...gridParticipants,
]);
- const { participantVideoWidth, containerRef } = useGridLayout(paginatedParticipants.length);
+ const gridLayoutParticipantCount = currentPage === 1 ? paginatedParticipants.length : maxGridParticipants;
+ const { participantVideoWidth, containerRef } = useGridLayout(gridLayoutParticipantCount);
const participantWidth = `${participantVideoWidth}px`;
- const participantHeight = `${Math.floor(participantVideoWidth * GRID_MODE_ASPECT_RATIO)}px`;
+ const participantHeight = `${Math.floor(participantVideoWidth * GRID_VIEW_ASPECT_RATIO)}px`;
return (
@@ -125,7 +131,7 @@ export function GridView() {
{paginatedParticipants.map(participant => (
;
@@ -32,7 +32,7 @@ describe('the usePagination hook', () => {
act(() => {
result.current.setCurrentPage(4);
});
- expect(result.current.paginatedParticipants).toEqual([8, 9, 10]);
+ expect(result.current.paginatedParticipants).toEqual([10]);
expect(result.current.currentPage).toBe(4);
});
@@ -45,12 +45,12 @@ describe('the usePagination hook', () => {
result.current.setCurrentPage(4);
});
- expect(result.current.paginatedParticipants).toEqual([8, 9, 10]);
+ expect(result.current.paginatedParticipants).toEqual([10]);
expect(result.current.totalPages).toBe(4);
rerender({ participants: [1, 2, 3, 4, 5] });
- expect(result.current.paginatedParticipants).toEqual([3, 4, 5]);
+ expect(result.current.paginatedParticipants).toEqual([4, 5]);
expect(result.current.totalPages).toBe(2);
rerender({ participants: [1, 2, 3] });
@@ -78,7 +78,7 @@ describe('the usePagination hook', () => {
result.current.setCurrentPage(4);
});
- expect(result.current.paginatedParticipants).toEqual([8, 9, 10]);
+ expect(result.current.paginatedParticipants).toEqual([10]);
expect(result.current.totalPages).toBe(4);
mockUseAppState.mockImplementation(() => ({ maxGridParticipants: 2 }));
@@ -92,16 +92,16 @@ describe('the usePagination hook', () => {
const { result, rerender } = renderHook(() => usePagination([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as any[]));
act(() => {
- result.current.setCurrentPage(4);
+ result.current.setCurrentPage(3);
});
- expect(result.current.paginatedParticipants).toEqual([8, 9, 10]);
+ expect(result.current.paginatedParticipants).toEqual([7, 8, 9]);
expect(result.current.totalPages).toBe(4);
mockUseAppState.mockImplementation(() => ({ maxGridParticipants: 4 }));
rerender();
- expect(result.current.paginatedParticipants).toEqual([7, 8, 9, 10]);
+ expect(result.current.paginatedParticipants).toEqual([9, 10]);
expect(result.current.totalPages).toBe(3);
});
});
diff --git a/src/hooks/usePagination/usePagination.ts b/src/components/GridView/usePagination/usePagination.ts
similarity index 62%
rename from src/hooks/usePagination/usePagination.ts
rename to src/components/GridView/usePagination/usePagination.ts
index dc99cfe94..d5d606cd9 100644
--- a/src/hooks/usePagination/usePagination.ts
+++ b/src/components/GridView/usePagination/usePagination.ts
@@ -1,13 +1,12 @@
import { useState, useEffect } from 'react';
import { Participant } from 'twilio-video';
-import { useAppState } from '../../state';
+import { useAppState } from '../../../state';
export function usePagination(participants: Participant[]) {
const [currentPage, setCurrentPage] = useState(1); // Pages are 1 indexed
const { maxGridParticipants } = useAppState();
const totalPages = Math.ceil(participants.length / maxGridParticipants);
- const isLastPage = currentPage === totalPages;
const isBeyondLastPage = currentPage > totalPages;
useEffect(() => {
@@ -16,16 +15,10 @@ export function usePagination(participants: Participant[]) {
}
}, [isBeyondLastPage, totalPages]);
- let paginatedParticipants;
-
- if (isLastPage) {
- paginatedParticipants = participants.slice(-maxGridParticipants);
- } else {
- paginatedParticipants = participants.slice(
- (currentPage - 1) * maxGridParticipants,
- currentPage * maxGridParticipants
- );
- }
+ let paginatedParticipants = participants.slice(
+ (currentPage - 1) * maxGridParticipants,
+ currentPage * maxGridParticipants
+ );
return { paginatedParticipants, setCurrentPage, currentPage, totalPages };
}
diff --git a/src/components/MenuBar/Menu/Menu.test.tsx b/src/components/MenuBar/Menu/Menu.test.tsx
index 32599f600..ac43c2d86 100644
--- a/src/components/MenuBar/Menu/Menu.test.tsx
+++ b/src/components/MenuBar/Menu/Menu.test.tsx
@@ -38,7 +38,7 @@ mockUseLocalVideoToggle.mockImplementation(() => [true, () => {}]);
describe('the Menu component', () => {
let mockUpdateRecordingRules: jest.Mock;
- let mockSetIsGridModeActive = jest.fn();
+ let mockSetIsGridViewActive = jest.fn();
beforeEach(() => jest.clearAllMocks());
@@ -48,7 +48,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: 'group',
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
mockUseFlipCameraToggle.mockImplementation(() => ({
flipCameraDisabled: false,
@@ -90,7 +90,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: 'group',
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
const { getByText } = render();
fireEvent.click(getByText('More'));
@@ -102,7 +102,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: 'group-small',
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
const { getByText } = render();
fireEvent.click(getByText('More'));
@@ -114,7 +114,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: 'go',
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
const { getByText, queryByText } = render();
fireEvent.click(getByText('More'));
@@ -126,7 +126,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: 'peer-to-peer',
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
const { getByText, queryByText } = render();
fireEvent.click(getByText('More'));
@@ -138,7 +138,7 @@ describe('the Menu component', () => {
isFetching: false,
updateRecordingRules: mockUpdateRecordingRules,
roomType: undefined,
- setIsGridModeActive: mockSetIsGridModeActive,
+ setIsGridViewActive: mockSetIsGridViewActive,
}));
const { getByText } = render();
fireEvent.click(getByText('More'));
@@ -210,30 +210,30 @@ describe('the Menu component', () => {
expect(wrapper.find(DeviceSelectionDialog).prop('open')).toBe(true);
});
- it('should show the Grid Mode button when grid mode is inactive', () => {
+ it('should show the Grid View button when grid view is inactive', () => {
mockUseAppState.mockImplementation(() => ({
- setIsGridModeActive: mockSetIsGridModeActive,
- isGridModeActive: false,
+ setIsGridViewActive: mockSetIsGridViewActive,
+ isGridViewActive: false,
}));
const { getByText } = render();
fireEvent.click(getByText('More'));
- fireEvent.click(getByText('Grid Mode'));
+ fireEvent.click(getByText('Grid View'));
- expect(mockSetIsGridModeActive.mock.calls[0][0](false)).toBe(true);
+ expect(mockSetIsGridViewActive.mock.calls[0][0](false)).toBe(true);
});
- it('should show the Collaboration Mode button when grid mode is active', () => {
+ it('should show the Presentation View button when grid view is active', () => {
mockUseAppState.mockImplementation(() => ({
- setIsGridModeActive: mockSetIsGridModeActive,
- isGridModeActive: true,
+ setIsGridViewActive: mockSetIsGridViewActive,
+ isGridViewActive: true,
}));
const { getByText } = render();
fireEvent.click(getByText('More'));
- fireEvent.click(getByText('Collaboration Mode'));
+ fireEvent.click(getByText('Presentation View'));
- expect(mockSetIsGridModeActive.mock.calls[0][0](true)).toBe(false);
+ expect(mockSetIsGridViewActive.mock.calls[0][0](true)).toBe(false);
});
it('should render the correct icon', () => {
diff --git a/src/components/MenuBar/Menu/Menu.tsx b/src/components/MenuBar/Menu/Menu.tsx
index 37c3e61d8..0bb1d2c4a 100644
--- a/src/components/MenuBar/Menu/Menu.tsx
+++ b/src/components/MenuBar/Menu/Menu.tsx
@@ -36,7 +36,7 @@ export default function Menu(props: { buttonClassName?: string }) {
const [menuOpen, setMenuOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
- const { isFetching, updateRecordingRules, roomType, setIsGridModeActive, isGridModeActive } = useAppState();
+ const { isFetching, updateRecordingRules, roomType, setIsGridViewActive, isGridViewActive } = useAppState();
const { setIsChatWindowOpen } = useChatContext();
const isRecording = useIsRecording();
const { room, setIsBackgroundSelectionOpen } = useVideoContext();
@@ -137,18 +137,18 @@ export default function Menu(props: { buttonClassName?: string }) {