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 }) { { - setIsGridModeActive(isGrid => !isGrid); + setIsGridViewActive(isGrid => !isGrid); setMenuOpen(false); }} > - {isGridModeActive ? ( + {isGridViewActive ? ( ) : ( )} - {isGridModeActive ? 'Collaboration Mode' : 'Grid Mode'} + {isGridViewActive ? 'Presentation View' : 'Grid View'} setAboutOpen(true)}> diff --git a/src/components/MobileGridView/MobileGridView.tsx b/src/components/MobileGridView/MobileGridView.tsx index 1e67d52a6..144a72ebe 100644 --- a/src/components/MobileGridView/MobileGridView.tsx +++ b/src/components/MobileGridView/MobileGridView.tsx @@ -1,5 +1,5 @@ import { CSSProperties } from 'react'; -import { makeStyles } from '@material-ui/core'; +import { makeStyles, createStyles, Theme } from '@material-ui/core'; import Participant from '../Participant/Participant'; import useDominantSpeaker from '../../hooks/useDominantSpeaker/useDominantSpeaker'; import useGridParticipants from '../../hooks/useGridParticipants/useGridParticipants'; @@ -11,33 +11,36 @@ import 'swiper/modules/pagination/pagination.min.css'; import { Pagination } from 'swiper'; import { Participant as IParticipant } from 'twilio-video'; -const useStyles = makeStyles({ - participantContainer: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - height: '100%', - '& .swiper': { +const useStyles = makeStyles((theme: Theme) => + createStyles({ + participantContainer: { + background: theme.gridViewBackgroundColor, + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, height: '100%', - '--swiper-pagination-bullet-inactive-color': 'white', + '& .swiper': { + height: '100%', + '--swiper-pagination-bullet-inactive-color': 'white', + }, + '& .swiper-wrapper': { + height: '100%', + }, + '& .swiper-slide': { + height: '90%', // To leave room for the pagination indicators + paddingBottom: '1em', + }, }, - '& .swiper-wrapper': { - height: '100%', - }, - '& .swiper-slide': { - height: '90%', // To leave room for the pagination indicators - paddingBottom: '1em', + swiperSlide: { + display: 'flex', + flexWrap: 'wrap', + alignSelf: 'center', + alignContent: 'flex-start', }, - }, - swiperSlide: { - display: 'flex', - flexWrap: 'wrap', - alignSelf: 'center', - alignContent: 'flex-start', - }, -}); + }) +); export function MobileGridView() { const classes = useStyles(); diff --git a/src/components/ParticipantAudioTracks/ParticipantAudioTracks.tsx b/src/components/ParticipantAudioTracks/ParticipantAudioTracks.tsx index bfd811ba2..ff5f7c6d4 100644 --- a/src/components/ParticipantAudioTracks/ParticipantAudioTracks.tsx +++ b/src/components/ParticipantAudioTracks/ParticipantAudioTracks.tsx @@ -15,7 +15,7 @@ function Participant({ participant }: { participant: RemoteParticipant }) { /* This ParticipantAudioTracks component will render the audio track for all participants in the room. It is in a separate component so that the audio tracks will always be rendered, and that they will never be - unnecessarily unmounted/mounted as the user switches between Grid View and Collaboration View. + unnecessarily unmounted/mounted as the user switches between Grid View and Presentation View. */ export function ParticipantAudioTracks() { const participants = useParticipants(); diff --git a/src/components/ParticipantInfo/ParticipantInfo.test.tsx b/src/components/ParticipantInfo/ParticipantInfo.test.tsx index 1fb2b5b01..16dc201fe 100644 --- a/src/components/ParticipantInfo/ParticipantInfo.test.tsx +++ b/src/components/ParticipantInfo/ParticipantInfo.test.tsx @@ -20,7 +20,7 @@ const mockUsePublications = usePublications as jest.Mock; const mockUseIsTrackSwitchedOff = useIsTrackSwitchedOff as jest.Mock; const mockUseParticipantIsReconnecting = useParticipantIsReconnecting as jest.Mock; -mockUseAppState.mockImplementation(() => ({ isGridModeActive: false })); +mockUseAppState.mockImplementation(() => ({ isGridViewActive: false })); describe('the ParticipantInfo component', () => { it('should render the AvatarIcon component when no video tracks are published', () => { diff --git a/src/components/ParticipantInfo/ParticipantInfo.tsx b/src/components/ParticipantInfo/ParticipantInfo.tsx index 1f31b9e85..03196eb48 100644 --- a/src/components/ParticipantInfo/ParticipantInfo.tsx +++ b/src/components/ParticipantInfo/ParticipantInfo.tsx @@ -14,7 +14,7 @@ import useIsTrackSwitchedOff from '../../hooks/useIsTrackSwitchedOff/useIsTrackS import usePublications from '../../hooks/usePublications/usePublications'; import useTrack from '../../hooks/useTrack/useTrack'; import useParticipantIsReconnecting from '../../hooks/useParticipantIsReconnecting/useParticipantIsReconnecting'; -import { GRID_MODE_MARGIN } from '../../constants'; +import { GRID_VIEW_MARGIN } from '../../constants'; import { useAppState } from '../../state'; const borderWidth = 2; @@ -104,7 +104,7 @@ const useStyles = makeStyles((theme: Theme) => identity: { background: 'rgba(0, 0, 0, 0.5)', color: 'white', - padding: '0.18em 0.3em', + padding: '0.18em 0.3em 0.18em 0', margin: 0, display: 'flex', alignItems: 'center', @@ -116,7 +116,7 @@ const useStyles = makeStyles((theme: Theme) => bottom: 0, left: 0, }, - typeography: { + typography: { color: 'white', [theme.breakpoints.down('sm')]: { fontSize: '0.75rem', @@ -128,11 +128,9 @@ const useStyles = makeStyles((theme: Theme) => cursorPointer: { cursor: 'pointer', }, - dominantSpeaker: { - border: `solid ${borderWidth}px #7BEAA5`, - margin: `${GRID_MODE_MARGIN} - borderWidth`, - }, - mobileGridMode: { + gridView: { + border: `${theme.participantBorderWidth}px solid ${theme.gridViewBackgroundColor}`, + borderRadius: '8px', [theme.breakpoints.down('sm')]: { position: 'relative', width: '100%', @@ -145,6 +143,10 @@ const useStyles = makeStyles((theme: Theme) => }, }, }, + dominantSpeaker: { + border: `solid ${borderWidth}px #7BEAA5`, + margin: `${GRID_VIEW_MARGIN} - borderWidth`, + }, }) ); @@ -181,7 +183,7 @@ export default function ParticipantInfo({ const audioTrack = useTrack(audioPublication) as LocalAudioTrack | RemoteAudioTrack | undefined; const isParticipantReconnecting = useParticipantIsReconnecting(participant); - const { isGridModeActive } = useAppState(); + const { isGridViewActive } = useAppState(); const classes = useStyles(); @@ -191,7 +193,7 @@ export default function ParticipantInfo({ [classes.hideParticipant]: hideParticipant, [classes.cursorPointer]: Boolean(onClick), [classes.dominantSpeaker]: isDominantSpeaker, - [classes.mobileGridMode]: isGridModeActive, + [classes.gridView]: isGridViewActive, })} onClick={onClick} data-cy-participant={participant.identity} @@ -206,7 +208,7 @@ export default function ParticipantInfo({ )} - + {participant.identity} {isLocalParticipant && ' (You)'} @@ -222,7 +224,7 @@ export default function ParticipantInfo({ )} {isParticipantReconnecting && (
- + Reconnecting...
diff --git a/src/components/ParticipantList/ParticipantList.tsx b/src/components/ParticipantList/ParticipantList.tsx index 8aa157f70..05254c317 100644 --- a/src/components/ParticipantList/ParticipantList.tsx +++ b/src/components/ParticipantList/ParticipantList.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import Participant from '../Participant/Participant'; import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import useMainParticipant from '../../hooks/useMainParticipant/useMainParticipant'; -import useCollaborationParticipants from '../../hooks/useCollaborationParticipants/useCollaborationParticipants'; +import usePresentationParticipants from '../../hooks/usePresentationParticipants/usePresentationParticipants'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; import useSelectedParticipant from '../VideoProvider/useSelectedParticipant/useSelectedParticipant'; import useScreenShareParticipant from '../../hooks/useScreenShareParticipant/useScreenShareParticipant'; @@ -45,7 +45,7 @@ export default function ParticipantList() { const classes = useStyles(); const { room } = useVideoContext(); const localParticipant = room!.localParticipant; - const participants = useCollaborationParticipants(); + const participants = usePresentationParticipants(); const [selectedParticipant, setSelectedParticipant] = useSelectedParticipant(); const screenShareParticipant = useScreenShareParticipant(); const mainParticipant = useMainParticipant(); diff --git a/src/components/Room/Room.test.tsx b/src/components/Room/Room.test.tsx index 7bee8dcb7..35f101966 100644 --- a/src/components/Room/Room.test.tsx +++ b/src/components/Room/Room.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { renderHook } from '@testing-library/react-hooks'; -import Room, { useSetCollaborationViewOnScreenShare } from './Room'; +import Room, { useSetPresentationViewOnScreenShare } from './Room'; import useChatContext from '../../hooks/useChatContext/useChatContext'; import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; import { useAppState } from '../../state'; @@ -28,7 +28,7 @@ const mockToggleChatWindow = jest.fn(); const mockOpenBackgroundSelection = jest.fn(); mockUseChatContext.mockImplementation(() => ({ setIsChatWindowOpen: mockToggleChatWindow })); mockUseVideoContext.mockImplementation(() => ({ setIsBackgroundSelectionOpen: mockOpenBackgroundSelection })); -mockUseAppState.mockImplementation(() => ({ isGridModeActive: false })); +mockUseAppState.mockImplementation(() => ({ isGridViewActive: false })); describe('the Room component', () => { it('should render correctly when the chat window and background selection windows are closed', () => { @@ -48,7 +48,7 @@ describe('the Room component', () => { expect(wrapper.prop('className')).toContain('rightDrawerOpen'); }); - it('should render correctly when grid mode is inactive', () => { + it('should render correctly when grid view is inactive', () => { mockUseVideoContext.mockImplementationOnce(() => ({ isBackgroundSelectionOpen: true })); const wrapper = shallow(); expect(wrapper.find('MainParticipant').exists()).toBe(true); @@ -56,9 +56,9 @@ describe('the Room component', () => { expect(wrapper.find('GridView').exists()).toBe(false); }); - it('should render correctly when grid mode is active', () => { + it('should render correctly when grid view is active', () => { mockUseVideoContext.mockImplementationOnce(() => ({ isBackgroundSelectionOpen: true })); - mockUseAppState.mockImplementationOnce(() => ({ isGridModeActive: true })); + mockUseAppState.mockImplementationOnce(() => ({ isGridViewActive: true })); const wrapper = shallow(); expect(wrapper.find('MainParticipant').exists()).toBe(false); expect(wrapper.find('ParticipantList').exists()).toBe(false); @@ -66,45 +66,119 @@ describe('the Room component', () => { }); }); -describe('the useSetCollaborationViewOnScreenShare hook', () => { - const mockSetIsGridModeActive = jest.fn(); +describe('the useSetPresentationViewOnScreenShare hook', () => { + const mockSetIsGridViewActive = jest.fn(); beforeEach(jest.clearAllMocks); - it('should not deactivate grid mode when there is no screen share participant', () => { + it('should not deactivate grid view when there is no screen share participant', () => { renderHook(() => - useSetCollaborationViewOnScreenShare(undefined, { localParticipant: {} } as any, mockSetIsGridModeActive) + useSetPresentationViewOnScreenShare(undefined, { localParticipant: {} } as any, mockSetIsGridViewActive, true) ); - expect(mockSetIsGridModeActive).not.toBeCalled(); + expect(mockSetIsGridViewActive).not.toBeCalled(); }); - it('should deactivate grid mode when a remote participant shares their screen', () => { + it('should deactivate grid view when a remote participant shares their screen', () => { const { rerender } = renderHook( ({ screenShareParticipant }) => - useSetCollaborationViewOnScreenShare( + useSetPresentationViewOnScreenShare( screenShareParticipant, { localParticipant: {} } as any, - mockSetIsGridModeActive + mockSetIsGridViewActive, + true ), { initialProps: { screenShareParticipant: undefined } } ); - expect(mockSetIsGridModeActive).not.toBeCalled(); + expect(mockSetIsGridViewActive).not.toBeCalled(); rerender({ screenShareParticipant: {} } as any); - expect(mockSetIsGridModeActive).toBeCalledWith(false); + expect(mockSetIsGridViewActive).toBeCalledWith(false); }); - it('should not deactivate grid mode when the local participant shares their screen', () => { + it('should reactivate grid view when screenshare ends if grid view was active before another participant started screensharing', () => { + const { rerender } = renderHook( + ({ screenShareParticipant }) => + useSetPresentationViewOnScreenShare( + screenShareParticipant, + { localParticipant: {} } as any, + mockSetIsGridViewActive, + true + ), + { initialProps: { screenShareParticipant: undefined } } + ); + expect(mockSetIsGridViewActive).not.toBeCalled(); + // screenshare starts + rerender({ screenShareParticipant: {} } as any); + expect(mockSetIsGridViewActive).toBeCalledWith(false); + // screenshare ends + rerender({ screenShareParticipant: undefined } as any); + expect(mockSetIsGridViewActive).toBeCalledWith(true); + }); + + it('should not activate grid view when screenshare ends if presentation view was active before another participant started screensharing', () => { + const { rerender } = renderHook( + ({ screenShareParticipant }) => + useSetPresentationViewOnScreenShare( + screenShareParticipant, + { localParticipant: {} } as any, + mockSetIsGridViewActive, + false + ), + { initialProps: { screenShareParticipant: undefined } } + ); + expect(mockSetIsGridViewActive).not.toBeCalled(); + // screenshare starts + rerender({ screenShareParticipant: {} } as any); + expect(mockSetIsGridViewActive).toBeCalledWith(false); + // screenshare ends + rerender({ screenShareParticipant: undefined } as any); + // mockSetIsGridViewActive should only be called once with "false" since we're not reactivating grid mode + expect(mockSetIsGridViewActive).toBeCalledTimes(1); + }); + + it('should not activate presentation view when screenshare ends if it was active before screensharing, but the user switched to grid view during the screenshare', () => { + const mockScreenShareParticipant = {}; + const mockRoom = { localParticipant: {} } as any; + + const { rerender } = renderHook( + ({ screenShareParticipant, isGridViewActive }) => + useSetPresentationViewOnScreenShare( + screenShareParticipant, + mockRoom, + mockSetIsGridViewActive, + isGridViewActive + ), + { initialProps: { screenShareParticipant: undefined, isGridViewActive: false } } + ); + + expect(mockSetIsGridViewActive).not.toBeCalled(); + + // start screenshare + rerender({ screenShareParticipant: mockScreenShareParticipant, isGridViewActive: false } as any); + expect(mockSetIsGridViewActive).toBeCalledWith(false); + + // enable grid mode + rerender({ screenShareParticipant: mockScreenShareParticipant, isGridViewActive: true } as any); + + // stop screenshare + rerender({ screenShareParticipant: undefined, isGridViewActive: true } as any); + + // mockSetIsGridViewActive should only be called once with "false" since we're not reactivating grid mode + expect(mockSetIsGridViewActive).toBeCalledTimes(1); + }); + + it('should not deactivate grid view when the local participant shares their screen', () => { const mockLocalParticipant = {}; const { rerender } = renderHook( ({ screenShareParticipant }) => - useSetCollaborationViewOnScreenShare( + useSetPresentationViewOnScreenShare( screenShareParticipant, { localParticipant: mockLocalParticipant } as any, - mockSetIsGridModeActive + mockSetIsGridViewActive, + true ), { initialProps: { screenShareParticipant: undefined } } ); - expect(mockSetIsGridModeActive).not.toBeCalled(); + expect(mockSetIsGridViewActive).not.toBeCalled(); rerender({ screenShareParticipant: mockLocalParticipant } as any); - expect(mockSetIsGridModeActive).not.toBeCalled(); + expect(mockSetIsGridViewActive).not.toBeCalled(); }); }); diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index 73186067a..941eb4bd5 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import BackgroundSelectionDialog from '../BackgroundSelectionDialog/BackgroundSelectionDialog'; import ChatWindow from '../ChatWindow/ChatWindow'; import clsx from 'clsx'; @@ -34,14 +34,38 @@ const useStyles = makeStyles((theme: Theme) => { }; }); -export function useSetCollaborationViewOnScreenShare( +/** + * This hook turns on presentation view when screensharing is active, regardless of if the + * user was already using presentation view or grid view. Once screensharing has ended, the user's + * view will return to whatever they were using prior to screenshare starting. + */ + +export function useSetPresentationViewOnScreenShare( screenShareParticipant: Participant | undefined, room: IRoom | null, - setIsGridModeActive: React.Dispatch> + setIsGridModeActive: React.Dispatch>, + isGridModeActive: boolean ) { + const isGridViewActiveRef = useRef(isGridModeActive); + + // Save the user's view setting whenever they change to presentation view or grid view: + useEffect(() => { + isGridViewActiveRef.current = isGridModeActive; + }, [isGridModeActive]); + useEffect(() => { if (screenShareParticipant && screenShareParticipant !== room!.localParticipant) { + // When screensharing starts, save the user's previous view setting (presentation or grid): + const prevIsGridViewActive = isGridViewActiveRef.current; + // Turn off grid view so that the user can see the screen that is being shared: setIsGridModeActive(false); + return () => { + // If the user was using grid view prior to screensharing, turn grid view back on + // once screensharing stops: + if (prevIsGridViewActive) { + setIsGridModeActive(prevIsGridViewActive); + } + }; } }, [screenShareParticipant, setIsGridModeActive, room]); } @@ -50,14 +74,14 @@ export default function Room() { const classes = useStyles(); const { isChatWindowOpen } = useChatContext(); const { isBackgroundSelectionOpen, room } = useVideoContext(); - const { isGridModeActive, setIsGridModeActive } = useAppState(); + const { isGridViewActive, setIsGridViewActive } = useAppState(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const screenShareParticipant = useScreenShareParticipant(); - // Here we switch to collaboration view when a participant starts sharing their screen, but - // the user is still free to switch back to grid mode. - useSetCollaborationViewOnScreenShare(screenShareParticipant, room, setIsGridModeActive); + // Here we switch to presentation view when a participant starts sharing their screen, but + // the user is still free to switch back to grid view. + useSetPresentationViewOnScreenShare(screenShareParticipant, room, setIsGridViewActive, isGridViewActive); return (
- {isGridModeActive ? ( + {isGridViewActive ? ( isMobile ? ( ) : ( diff --git a/src/constants.ts b/src/constants.ts index f97634548..88ebfb107 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,5 +12,5 @@ export const SELECTED_VIDEO_INPUT_KEY = 'TwilioVideoApp-selectedVideoInput'; // This is used to store the current background settings in localStorage export const SELECTED_BACKGROUND_SETTINGS_KEY = 'TwilioVideoApp-selectedBackgroundSettings'; -export const GRID_MODE_ASPECT_RATIO = 9 / 16; // 16:9 -export const GRID_MODE_MARGIN = 3; +export const GRID_VIEW_ASPECT_RATIO = 9 / 16; // 16:9 +export const GRID_VIEW_MARGIN = 3; diff --git a/src/hooks/useGridLayout/useGridLayout.ts b/src/hooks/useGridLayout/useGridLayout.ts index 338931ad6..375a0cd0f 100644 --- a/src/hooks/useGridLayout/useGridLayout.ts +++ b/src/hooks/useGridLayout/useGridLayout.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { GRID_MODE_ASPECT_RATIO, GRID_MODE_MARGIN } from '../../constants'; +import { GRID_VIEW_ASPECT_RATIO, GRID_VIEW_MARGIN } from '../../constants'; /** * This function determines how many columns and rows are to be used @@ -13,7 +13,7 @@ export const layoutIsTooSmall = ( containerHeight: number ) => { const videoWidth = newVideoSize; - const videoHeight = newVideoSize * GRID_MODE_ASPECT_RATIO; + const videoHeight = newVideoSize * GRID_VIEW_ASPECT_RATIO; const columns = Math.floor(containerWidth / videoWidth); const rows = Math.ceil(participantCount / columns); @@ -34,8 +34,8 @@ export default function useGridLayout(participantCount: number) { const updateLayout = useCallback(() => { if (!containerRef.current) return; - const containerWidth = containerRef.current.offsetWidth - GRID_MODE_MARGIN * 2; - const containerHeight = containerRef.current.offsetHeight - GRID_MODE_MARGIN * 2; + const containerWidth = containerRef.current.offsetWidth - GRID_VIEW_MARGIN * 2; + const containerHeight = containerRef.current.offsetHeight - GRID_VIEW_MARGIN * 2; // Here we use binary search to guess the new size of each video in the grid // so that they all fit nicely for any screen size up to a width of 16384px. @@ -55,7 +55,7 @@ export default function useGridLayout(participantCount: number) { let newParticipantVideoWidth = Math.ceil(minVideoWidth); - setParticipantVideoWidth(newParticipantVideoWidth - GRID_MODE_MARGIN * 2); + setParticipantVideoWidth(newParticipantVideoWidth - GRID_VIEW_MARGIN * 2); }, [participantCount]); useEffect(() => { diff --git a/src/hooks/useParticipants/useParticipants.test.ts b/src/hooks/useParticipants/useParticipants.test.ts index af8f6bc91..aa6000e73 100644 --- a/src/hooks/useParticipants/useParticipants.test.ts +++ b/src/hooks/useParticipants/useParticipants.test.ts @@ -1,6 +1,6 @@ import { act, renderHook } from '@testing-library/react-hooks'; import EventEmitter from 'events'; -import useCollaborationParticipants from './useParticipants'; +import useParticipants from './useParticipants'; import useVideoContext from '../useVideoContext/useVideoContext'; jest.mock('../useVideoContext/useVideoContext'); @@ -23,12 +23,12 @@ describe('the useParticipants hook', () => { }); it('should return an array of mockParticipant.tracks by default', () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(useParticipants); expect(result.current).toEqual(['participant1', 'participant2']); }); it('should return respond to "participantConnected" events', async () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(useParticipants); act(() => { mockRoom.emit('participantConnected', 'newParticipant'); }); @@ -36,7 +36,7 @@ describe('the useParticipants hook', () => { }); it('should return respond to "participantDisconnected" events', async () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(useParticipants); act(() => { mockRoom.emit('participantDisconnected', 'participant1'); }); @@ -44,7 +44,7 @@ describe('the useParticipants hook', () => { }); it('should clean up listeners on unmount', () => { - const { unmount } = renderHook(useCollaborationParticipants); + const { unmount } = renderHook(useParticipants); unmount(); expect(mockRoom.listenerCount('participantConnected')).toBe(0); expect(mockRoom.listenerCount('participantDisconnected')).toBe(0); diff --git a/src/hooks/useParticipants/useParticipants.ts b/src/hooks/useParticipants/useParticipants.ts index d98ad5ede..4240d9071 100644 --- a/src/hooks/useParticipants/useParticipants.ts +++ b/src/hooks/useParticipants/useParticipants.ts @@ -4,7 +4,7 @@ import useVideoContext from '../useVideoContext/useVideoContext'; /** * This hook returns an array of the video room's participants. Unlike the hooks - * "useCollaborationParticipants" and "useGridParticipants", this hook does not reorder + * "usePresentationParticipants" and "useGridParticipants", this hook does not reorder * the list of participants whenever the dominantSpeaker changes. This will prevent unnecessary * re-renders because components that use this hook will only update when a participant connects * to or disconnects from the room. diff --git a/src/hooks/useCollaborationParticipants/useCollaborationParticipants.test.tsx b/src/hooks/usePresentationParticipants/usePresentationParticipants.test.tsx similarity index 83% rename from src/hooks/useCollaborationParticipants/useCollaborationParticipants.test.tsx rename to src/hooks/usePresentationParticipants/usePresentationParticipants.test.tsx index 45e1ac9dc..2a6b69a13 100644 --- a/src/hooks/useCollaborationParticipants/useCollaborationParticipants.test.tsx +++ b/src/hooks/usePresentationParticipants/usePresentationParticipants.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import EventEmitter from 'events'; import useDominantSpeaker from '../useDominantSpeaker/useDominantSpeaker'; -import useCollaborationParticipants from './useCollaborationParticipants'; +import usePresentationParticipants from './usePresentationParticipants'; import useVideoContext from '../useVideoContext/useVideoContext'; jest.mock('../useVideoContext/useVideoContext'); @@ -10,7 +10,7 @@ jest.mock('../useDominantSpeaker/useDominantSpeaker'); const mockUseDominantSpeaker = useDominantSpeaker as jest.Mock; const mockedVideoContext = useVideoContext as jest.Mock; -describe('the useCollaborationParticipants hook', () => { +describe('the usePresentationParticipants hook', () => { let mockRoom: any; beforeEach(() => { @@ -25,12 +25,12 @@ describe('the useCollaborationParticipants hook', () => { }); it('should return an array of mockParticipant.tracks by default', () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(usePresentationParticipants); expect(result.current).toEqual(['participant1', 'participant2']); }); it('should return respond to "participantConnected" events', async () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(usePresentationParticipants); act(() => { mockRoom.emit('participantConnected', 'newParticipant'); }); @@ -38,7 +38,7 @@ describe('the useCollaborationParticipants hook', () => { }); it('should return respond to "participantDisconnected" events', async () => { - const { result } = renderHook(useCollaborationParticipants); + const { result } = renderHook(usePresentationParticipants); act(() => { mockRoom.emit('participantDisconnected', 'participant1'); }); @@ -51,7 +51,7 @@ describe('the useCollaborationParticipants hook', () => { [1, 'participant2'], [2, 'participant3'], ]); - const { result, rerender } = renderHook(useCollaborationParticipants); + const { result, rerender } = renderHook(usePresentationParticipants); expect(result.current).toEqual(['participant1', 'participant2', 'participant3']); mockUseDominantSpeaker.mockImplementation(() => 'participant2'); rerender(); @@ -65,7 +65,7 @@ describe('the useCollaborationParticipants hook', () => { }); it('should clean up listeners on unmount', () => { - const { unmount } = renderHook(useCollaborationParticipants); + const { unmount } = renderHook(usePresentationParticipants); unmount(); expect(mockRoom.listenerCount('participantConnected')).toBe(0); expect(mockRoom.listenerCount('participantDisconnected')).toBe(0); diff --git a/src/hooks/useCollaborationParticipants/useCollaborationParticipants.tsx b/src/hooks/usePresentationParticipants/usePresentationParticipants.tsx similarity index 96% rename from src/hooks/useCollaborationParticipants/useCollaborationParticipants.tsx rename to src/hooks/usePresentationParticipants/usePresentationParticipants.tsx index 459794588..19ce4ae4f 100644 --- a/src/hooks/useCollaborationParticipants/useCollaborationParticipants.tsx +++ b/src/hooks/usePresentationParticipants/usePresentationParticipants.tsx @@ -3,7 +3,7 @@ import { RemoteParticipant } from 'twilio-video'; import useDominantSpeaker from '../useDominantSpeaker/useDominantSpeaker'; import useVideoContext from '../useVideoContext/useVideoContext'; -export default function useCollaborationParticipants() { +export default function usePresentationParticipants() { const { room } = useVideoContext(); const dominantSpeaker = useDominantSpeaker(); const [participants, setParticipants] = useState(Array.from(room?.participants.values() ?? [])); diff --git a/src/state/index.tsx b/src/state/index.tsx index 0e4efd21d..286cfce11 100644 --- a/src/state/index.tsx +++ b/src/state/index.tsx @@ -23,8 +23,8 @@ export interface StateContextType { dispatchSetting: React.Dispatch; roomType?: RoomType; updateRecordingRules(room_sid: string, rules: RecordingRules): Promise; - isGridModeActive: boolean; - setIsGridModeActive: React.Dispatch>; + isGridViewActive: boolean; + setIsGridViewActive: React.Dispatch>; maxGridParticipants: number; setMaxGridParticipants: React.Dispatch>; } @@ -43,11 +43,11 @@ export const StateContext = createContext(null!); export default function AppStateProvider(props: React.PropsWithChildren<{}>) { const [error, setError] = useState(null); const [isFetching, setIsFetching] = useState(false); - const [isGridModeActive, setIsGridModeActive] = useLocalStorageState('grid-mode-active-key', false); + const [isGridViewActive, setIsGridViewActive] = useLocalStorageState('grid-view-active-key', true); const [activeSinkId, setActiveSinkId] = useActiveSinkId(); const [settings, dispatchSetting] = useReducer(settingsReducer, initialSettings); const [roomType, setRoomType] = useState(); - const [maxGridParticipants, setMaxGridParticipants] = useLocalStorageState('max-grid-participants-key', 25); + const [maxGridParticipants, setMaxGridParticipants] = useLocalStorageState('max-grid-participants-key', 9); let contextValue = { error, @@ -58,8 +58,8 @@ export default function AppStateProvider(props: React.PropsWithChildren<{}>) { settings, dispatchSetting, roomType, - isGridModeActive, - setIsGridModeActive, + isGridViewActive, + setIsGridViewActive, maxGridParticipants, setMaxGridParticipants, } as StateContextType; diff --git a/src/theme.ts b/src/theme.ts index ce00306cf..9a7e4cb01 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -11,6 +11,7 @@ declare module '@material-ui/core/styles/createTheme' { sidebarMobilePadding: number; participantBorderWidth: number; rightDrawerWidth: number; + gridViewBackgroundColor: string; } // allow configuration using `createMuiTheme` @@ -24,6 +25,7 @@ declare module '@material-ui/core/styles/createTheme' { sidebarMobilePadding: number; participantBorderWidth: number; rightDrawerWidth?: number; + gridViewBackgroundColor: string; } } @@ -124,4 +126,5 @@ export default createTheme({ participantBorderWidth: 2, mobileTopBarHeight: 52, rightDrawerWidth: 320, + gridViewBackgroundColor: '#121C2D', });