Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid mode pagination #654

Merged
merged 36 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
901a9a1
Install storybook and add basic files
Feb 8, 2022
9182963
Get storybook working
Feb 8, 2022
1fcc8ac
Get grid view working
Feb 25, 2022
e6750e3
Hook up menu button
Feb 25, 2022
c6c1b52
Fix some issues
Feb 26, 2022
20b64e5
Fix index.html caching issue
Mar 2, 2022
1d2709c
Update publication component and tests
Mar 2, 2022
b4bbd6b
Update Room tests
Mar 2, 2022
2e86433
Update constants
Mar 2, 2022
cc1a12f
Upgrade typescript
Mar 2, 2022
f319175
Update menu tests
Mar 2, 2022
fcc7188
Update TS things for new version
Mar 2, 2022
33c0895
Rename state variable
Mar 2, 2022
cee52ed
Add test for ParticipantAudioTracks
Mar 2, 2022
7222268
Remove unused passcode stuff from twilio mock
Mar 2, 2022
e28b800
Automatically disable conversations in storybook
Mar 2, 2022
608ab6f
Fix tests and linting issues
Mar 2, 2022
171b007
Add tests for GridView
Mar 2, 2022
f031142
Install mui pagination component
Mar 8, 2022
fd96c6b
Fix issue with storybook controls
Mar 8, 2022
a4540bf
Add pagination controls to state
Mar 8, 2022
cdf6395
Create usePagination hook
Mar 8, 2022
68ff5dc
Add pagination to GridView component
Mar 8, 2022
b3890f7
Add setting for MaxGridParticipants
Mar 8, 2022
8ac77ad
Remove unused constant and fix a few glitches
Mar 8, 2022
57dda78
Add tests for usePagination
Mar 8, 2022
fd68fd1
Update tests for GridView component
Mar 8, 2022
e44d4f3
Merge branch 'feature/grid-mode' into grid-mode-pagination
Mar 8, 2022
650d90d
Fix linting issue
Mar 8, 2022
6a33ab3
Only render Pagination component when there is more than one page
Mar 9, 2022
46dd5af
Display participant count in menu
Mar 10, 2022
da451f4
Fix tests
Mar 10, 2022
926531f
remove console log
Mar 10, 2022
2460c76
Fix linter errors
Mar 10, 2022
7b97094
Fix test language
Mar 14, 2022
dfdcf80
Fix issue with chat window rendering incorrectly
Mar 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
405 changes: 175 additions & 230 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@twilio-labs/plugin-rtc": "^0.8.2",
"@twilio/conversations": "^1.2.3",
"@twilio/video-processors": "^1.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import VideoInputList from './VideoInputList/VideoInputList';
import MaxGridParticipants from './MaxGridParticipants/MaxGridParticipants';

const useStyles = makeStyles((theme: Theme) => ({
container: {
Expand Down Expand Up @@ -70,6 +71,13 @@ export default function DeviceSelectionDialog({ open, onClose }: { open: boolean
<div className={classes.listSection}>
<AudioOutputList />
</div>
<Divider />
<div className={classes.listSection}>
<Typography variant="h6" className={classes.headline}>
Grid View
</Typography>
<MaxGridParticipants />
</div>
</DialogContent>
<Divider />
<DialogActions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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];

export default function MaxGridParticipants() {
const { maxGridParticipants, setMaxGridParticipants } = useAppState();

return (
<div>
<Typography variant="subtitle2" gutterBottom>
Max Grid Participants
</Typography>
<Grid container alignItems="center" justifyContent="space-between">
<div className="inputSelect">
<FormControl fullWidth>
<Select
onChange={e => setMaxGridParticipants(parseInt(e.target.value as string))}
value={maxGridParticipants}
variant="outlined"
>
{MAX_PARTICIPANT_OPTIONS.map(option => (
<MenuItem value={option} key={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</Grid>
</div>
);
}
76 changes: 73 additions & 3 deletions src/components/GridView/GridView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React from 'react';
import { GridView } from './GridView';
import { shallow } from 'enzyme';
import useGridLayout from '../../hooks/useGridLayout/useGridLayout';
import { usePagination } from '../../hooks/usePagination/usePagination';

const mockLocalParticipant = { identity: 'test-local-participant', sid: 0 };
const mockParticipants = [
{ identity: 'test-participant-1', sid: 1 },
{ identity: 'test-participant-2', sid: 2 },
Expand All @@ -12,13 +14,12 @@ const mockParticipants = [

jest.mock('../../constants', () => ({
GRID_MODE_ASPECT_RATIO: 9 / 16,
GRID_MODE_MAX_PARTICIPANTS: 2,
GRID_MODE_MARGIN: 3,
}));
jest.mock('../../hooks/useParticipants/useParticipants', () => () => mockParticipants);
jest.mock('../../hooks/useVideoContext/useVideoContext', () => () => ({
room: {
localParticipant: { identity: 'test-local-participant' },
localParticipant: mockLocalParticipant,
},
}));
jest.mock('../../hooks/useGridLayout/useGridLayout', () =>
Expand All @@ -28,10 +29,79 @@ jest.mock('../../hooks/useGridLayout/useGridLayout', () =>
}))
);

jest.mock('../../hooks/usePagination/usePagination', () => ({
usePagination: jest.fn(() => ({
currentPage: 2,
totalPages: 4,
setCurrentPage: jest.fn(),
paginatedParticipants: [mockLocalParticipant, ...mockParticipants],
})),
}));

const mockUsePagination = usePagination as jest.Mock<any>;

describe('the GridView component', () => {
it('should render correctly', () => {
const wrapper = shallow(<GridView />);
expect(wrapper).toMatchSnapshot();
expect(useGridLayout).toHaveBeenCalledWith(2);
expect(useGridLayout).toHaveBeenCalledWith(5);
});

it('should not render the previous page button when the user is viewing the first page', () => {
mockUsePagination.mockImplementationOnce(() => ({
currentPage: 1,
totalPages: 4,
setCurrentPage: jest.fn(),
paginatedParticipants: [mockLocalParticipant, ...mockParticipants],
}));

const wrapper = shallow(<GridView />);
expect(
wrapper
.find('.makeStyles-buttonContainerLeft-4')
.childAt(0)
.exists()
).toBe(false);
expect(
wrapper
.find('.makeStyles-buttonContainerRight-5')
.childAt(0)
.exists()
).toBe(true);
});

it('should not render the next page button when the user is viewing the last page', () => {
mockUsePagination.mockImplementationOnce(() => ({
currentPage: 4,
totalPages: 4,
setCurrentPage: jest.fn(),
paginatedParticipants: [mockLocalParticipant, ...mockParticipants],
}));

const wrapper = shallow(<GridView />);
expect(
wrapper
.find('.makeStyles-buttonContainerLeft-4')
.childAt(0)
.exists()
).toBe(true);
expect(
wrapper
.find('.makeStyles-buttonContainerRight-5')
.childAt(0)
.exists()
).toBe(false);
});

it('should not render the Pagination component when there is only one page', () => {
mockUsePagination.mockImplementationOnce(() => ({
currentPage: 1,
totalPages: 1,
setCurrentPage: jest.fn(),
paginatedParticipants: [mockLocalParticipant, ...mockParticipants],
}));

const wrapper = shallow(<GridView />);
expect(wrapper.find('.makeStyles-pagination-8').exists()).toBe(false);
});
});
131 changes: 104 additions & 27 deletions src/components/GridView/GridView.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,134 @@
import React from 'react';
import { GRID_MODE_ASPECT_RATIO, GRID_MODE_MARGIN, GRID_MODE_MAX_PARTICIPANTS } from '../../constants';
import { makeStyles, Theme } from '@material-ui/core';
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 { Pagination } from '@material-ui/lab';
import Participant from '../Participant/Participant';
import useGridLayout from '../../hooks/useGridLayout/useGridLayout';
import useParticipants from '../../hooks/useParticipants/useParticipants';
import useVideoContext from '../../hooks/useVideoContext/useVideoContext';
import { usePagination } from '../../hooks/usePagination/usePagination';

const useStyles = makeStyles((theme: Theme) => ({
const CONTAINER_GUTTER = '50px';

const useStyles = makeStyles({
container: {
position: 'relative',
gridArea: '1 / 1 / 2 / 3',
},
participantContainer: {
position: 'absolute',
display: 'flex',
width: 'calc(100% - 200px)',
top: CONTAINER_GUTTER,
right: CONTAINER_GUTTER,
bottom: CONTAINER_GUTTER,
left: CONTAINER_GUTTER,
margin: '0 auto',
alignContent: 'center',
flexWrap: 'wrap',
justifyContent: 'center',
gridArea: '1 / 1 / 2 / 3',
},
participant: {
'&:nth-child(n + 26)': {
display: 'none',
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',
},
},
}));
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',
},
});

export function GridView() {
const classes = useStyles();
const { room } = useVideoContext();
const participants = useParticipants();

const { participantVideoWidth, containerRef } = useGridLayout(
Math.min(participants.length + 1, GRID_MODE_MAX_PARTICIPANTS)
);
const { paginatedParticipants, setCurrentPage, currentPage, totalPages } = usePagination([
room!.localParticipant,
...participants,
]);

const { participantVideoWidth, containerRef } = useGridLayout(paginatedParticipants.length);

const participantWidth = `${participantVideoWidth}px`;
const participantHeight = `${Math.floor(participantVideoWidth * GRID_MODE_ASPECT_RATIO)}px`;

return (
<div className={classes.container} ref={containerRef}>
<div
className={classes.participant}
style={{ width: participantWidth, height: participantHeight, margin: GRID_MODE_MARGIN }}
>
<Participant participant={room!.localParticipant} isLocalParticipant={true} />
<div className={classes.container}>
<div className={clsx(classes.buttonContainer, classes.buttonContainerLeft)}>
{!(currentPage === 1) && (
<IconButton className={classes.paginationButton} onClick={() => setCurrentPage(page => page - 1)}>
<ArrowBack />
</IconButton>
)}
</div>
<div className={clsx(classes.buttonContainer, classes.buttonContainerRight)}>
{!(currentPage === totalPages) && (
<IconButton className={classes.paginationButton} onClick={() => setCurrentPage(page => page + 1)}>
<ArrowForward />
</IconButton>
)}
</div>
<div className={classes.paginationContainer}>
{totalPages > 1 && (
<Pagination
className={classes.pagination}
page={currentPage}
count={totalPages}
onChange={(_, value) => setCurrentPage(value)}
shape="rounded"
color="primary"
size="small"
hidePrevButton
hideNextButton
/>
)}
</div>
<div className={classes.participantContainer} ref={containerRef}>
{paginatedParticipants.map(participant => (
<div
key={participant.sid}
style={{ width: participantWidth, height: participantHeight, margin: GRID_MODE_MARGIN }}
>
<Participant participant={participant} isLocalParticipant={participant === room!.localParticipant} />
</div>
))}
</div>
{participants.map(participant => (
<div
key={participant.sid}
className={classes.participant}
style={{ width: participantWidth, height: participantHeight, margin: GRID_MODE_MARGIN }}
>
<Participant participant={participant} />
</div>
))}
</div>
);
}