diff --git a/src/frontend/components/ViewersList/InstructorViewersList/index.spec.tsx b/src/frontend/components/ViewersList/InstructorViewersList/index.spec.tsx
deleted file mode 100644
index b349fe7249..0000000000
--- a/src/frontend/components/ViewersList/InstructorViewersList/index.spec.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import { act, render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-
-import { useParticipantsStore } from 'data/stores/useParticipantsStore';
-import {
- participantMockFactory,
- videoMockFactory,
-} from 'utils/tests/factories';
-import { wrapInIntlProvider } from 'utils/tests/intl';
-import { converse } from 'utils/window';
-import { InstructorViewersList } from '.';
-
-jest.mock('utils/window', () => ({
- converse: {
- acceptParticipantToJoin: jest.fn(),
- askParticipantToJoin: jest.fn(),
- kickParticipant: jest.fn(),
- rejectParticipantToJoin: jest.fn(),
- },
-}));
-
-const mockedParticipantOnDemands1 = participantMockFactory();
-const mockedParticipantOnDemands2 = participantMockFactory();
-
-const mockedParticipantOnStage1 = participantMockFactory();
-const mockedParticipantOnStage2 = participantMockFactory();
-
-const mockedParticipantOnStage1Full = {
- ...mockedParticipantOnStage1,
- isInstructor: false,
- isOnStage: true,
-};
-
-const mockedParticipantOnStage2Full = {
- ...mockedParticipantOnStage2,
- isInstructor: false,
- isOnStage: true,
-};
-
-const participant1 = {
- id: 'example.jid.student1@prosody.org',
- isInstructor: true,
- isOnStage: false,
- name: 'Student 1',
-};
-
-const participant2 = {
- id: 'example.jid.student2@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 2',
-};
-
-const participant3 = {
- id: 'example.jid.student3@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 3',
-};
-
-describe('', () => {
- beforeEach(() => jest.resetAllMocks());
-
- it('displays severals participants, not on stage and not asking, and then remove some of them', () => {
- const video = videoMockFactory();
-
- render(wrapInIntlProvider());
-
- expect(screen.queryByText('Demands')).toEqual(null);
- expect(screen.queryByText('On stage')).toEqual(null);
- expect(screen.queryByText('Other participants')).toEqual(null);
-
- act(() => useParticipantsStore.getState().addParticipant(participant1));
- act(() => useParticipantsStore.getState().addParticipant(participant2));
- act(() => useParticipantsStore.getState().addParticipant(participant3));
-
- screen.getByText('Student 1');
- screen.getByText('Student 2');
- screen.getByText('Student 3');
-
- act(() => useParticipantsStore.getState().removeParticipant('Student 2'));
-
- expect(screen.queryByText('Student 2')).not.toBeInTheDocument();
- });
-
- it('displays severals participants, some on stage, and some asking, and some not on stage and not asking', () => {
- const video = videoMockFactory({
- participants_asking_to_join: [
- mockedParticipantOnDemands1,
- mockedParticipantOnDemands2,
- ],
- participants_in_discussion: [
- mockedParticipantOnStage1,
- mockedParticipantOnStage2,
- ],
- });
-
- useParticipantsStore.setState({
- participants: [
- participant1,
- participant2,
- mockedParticipantOnStage1Full,
- mockedParticipantOnStage2Full,
- ],
- });
-
- render(wrapInIntlProvider());
-
- screen.getByText('Demands');
- screen.getByText('On stage');
- screen.getByText('Other participants');
-
- screen.getByText('Student 1');
- screen.getByText('Student 2');
- screen.getByText(mockedParticipantOnDemands1.name);
- screen.getByText(mockedParticipantOnDemands2.name);
- screen.getByText(mockedParticipantOnStage1Full.name);
- screen.getByText(mockedParticipantOnStage2Full.name);
- });
-
- it('displays a demanding participant and accepts it', () => {
- const video = videoMockFactory({
- participants_asking_to_join: [mockedParticipantOnDemands1],
- });
-
- render(wrapInIntlProvider());
-
- screen.getByText('Demands');
- expect(screen.queryByText('On stage')).toEqual(null);
- expect(screen.queryByText('Other participants')).toEqual(null);
- screen.getByText(mockedParticipantOnDemands1.name);
-
- const acceptButton = screen.getByRole('button', { name: 'Accept' });
- act(() => userEvent.click(acceptButton));
- expect(converse.acceptParticipantToJoin).toHaveBeenCalledTimes(1);
- });
-
- it('displays a demanding participant and rejects it', () => {
- const video = videoMockFactory({
- participants_asking_to_join: [mockedParticipantOnDemands1],
- });
-
- render(wrapInIntlProvider());
-
- screen.getByText('Demands');
- expect(screen.queryByText('On stage')).toEqual(null);
- expect(screen.queryByText('Other participants')).toEqual(null);
- screen.getByText(mockedParticipantOnDemands1.name);
-
- const rejectButton = screen.getAllByRole('button')[0];
- act(() => userEvent.click(rejectButton));
- expect(converse.rejectParticipantToJoin).toHaveBeenCalledTimes(1);
- });
-
- it('displays an on-stage participant and kicks it', () => {
- const video = videoMockFactory({
- participants_in_discussion: [mockedParticipantOnStage1],
- });
-
- useParticipantsStore.setState({
- participants: [mockedParticipantOnStage1Full],
- });
-
- render(wrapInIntlProvider());
-
- expect(screen.queryByText('Demands')).toEqual(null);
- screen.getByText('On stage');
- expect(screen.queryByText('Other participants')).toEqual(null);
- screen.getByText(mockedParticipantOnStage1Full.name);
-
- const terminateButton = screen.getByRole('button', { name: 'Terminate' });
- act(() => userEvent.click(terminateButton));
- expect(converse.kickParticipant).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/src/frontend/components/ViewersList/InstructorViewersList/index.tsx b/src/frontend/components/ViewersList/InstructorViewersList/index.tsx
deleted file mode 100644
index 48ea609e7d..0000000000
--- a/src/frontend/components/ViewersList/InstructorViewersList/index.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import React from 'react';
-import { Box, Button, List } from 'grommet';
-import { AddCircle } from 'grommet-icons';
-import { defineMessages, useIntl } from 'react-intl';
-
-import {
- useParticipantsStore,
- ParticipantType,
-} from 'data/stores/useParticipantsStore';
-import { Video } from 'types/tracks';
-import { ViewersListHeader } from 'components/ViewersList/components/ViewersListHeader';
-import { ViewersListItem } from 'components/ViewersList/components/ViewersListItem';
-import { converse } from 'utils/window';
-import { ViewersListItemContainer } from 'components/ViewersList/components/ViewersListItemContainer';
-import { ViewersListTextButton } from 'components/ViewersList/components/ViewersListTextButton';
-
-const messages = defineMessages({
- demands: {
- defaultMessage: 'Demands',
- description:
- 'Participants asking for going on stage are displayed under this label.',
- id: 'components.ViewersList.demands',
- },
- onStage: {
- defaultMessage: 'On stage',
- description: 'On-stage participants are displayed under this label.',
- id: 'components.ViewersList.onStage',
- },
- otherViewers: {
- defaultMessage: 'Other participants',
- description: 'Connected participants are displayed under this label.',
- id: 'components.ViewersList.otherViewers',
- },
- acceptButton: {
- defaultMessage: 'Accept',
- description:
- 'The text displayed in the button in charge of accepting on-stage request, in the viewers list.',
- id: 'components.ViewersList.acceptButton',
- },
- endOnStageButton: {
- defaultMessage: 'Terminate',
- description:
- 'The text displayed in the button in charge of making students on stage exiting the stage, in the viewers list.',
- id: 'components.ViewersList.endOnStageButton',
- },
-});
-
-interface InstructorViewersListProps {
- video: Video;
-}
-
-export const InstructorViewersList = ({
- video,
-}: InstructorViewersListProps) => {
- const participants = useParticipantsStore((state) => state.participants);
- const participantsOnStage = participants.filter(
- (participant) =>
- video.participants_in_discussion.some(
- (p) => p.name === participant.name,
- ) || participant.isInstructor,
- );
-
- const participantsNotOnStageAndNotAsking = participants.filter(
- (participant) =>
- !participantsOnStage.includes(participant) &&
- !video.participants_asking_to_join.some(
- (p) => p.name === participant.name,
- ),
- );
- const intl = useIntl();
-
- return (
-
- {video.participants_asking_to_join.length !== 0 && (
-
-
-
- participantA.name.localeCompare(participantB.name),
- )}
- pad="none"
- >
- {(item: ParticipantType, index: number) => (
-
-
-
- }
- onClick={() => converse.rejectParticipantToJoin(item)}
- plain
- style={{ padding: '0px', transform: 'rotate(45deg)' }}
- />
-
- converse.acceptParticipantToJoin(item, video)
- }
- text={intl.formatMessage(messages.acceptButton)}
- />
-
-
- )}
-
-
- )}
-
- {participantsOnStage.length !== 0 && (
-
-
-
- {(item: ParticipantType, index: number) => (
-
-
- {!item.isInstructor && (
- converse.kickParticipant(item)}
- text={intl.formatMessage(messages.endOnStageButton)}
- />
- )}
-
- )}
-
-
- )}
- {participantsNotOnStageAndNotAsking.length !== 0 && (
-
-
-
- {(item: ParticipantType, index: number) => (
-
-
-
- )}
-
-
- )}
-
- );
-};
diff --git a/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-1-snap.png b/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-1-snap.png
deleted file mode 100644
index 93ba990ed7..0000000000
Binary files a/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-1-snap.png and /dev/null differ
diff --git a/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-2-snap.png b/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-2-snap.png
deleted file mode 100644
index 2dce7ad3f1..0000000000
Binary files a/src/frontend/components/ViewersList/StudentViewersList/__image_snapshots__/index-spec-tsx-student-viewers-list-renders-student-viewers-list-component-with-data-and-compares-it-with-previous-render-2-snap.png and /dev/null differ
diff --git a/src/frontend/components/ViewersList/StudentViewersList/index.spec.tsx b/src/frontend/components/ViewersList/StudentViewersList/index.spec.tsx
deleted file mode 100644
index ad850acebc..0000000000
--- a/src/frontend/components/ViewersList/StudentViewersList/index.spec.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { act, render, screen } from '@testing-library/react';
-import { Grommet } from 'grommet';
-import React from 'react';
-
-import { useParticipantsStore } from 'data/stores/useParticipantsStore';
-import {
- participantMockFactory,
- videoMockFactory,
-} from 'utils/tests/factories';
-import { imageSnapshot } from 'utils/tests/imageSnapshot';
-import { wrapInIntlProvider } from 'utils/tests/intl';
-import { GlobalStyles } from 'utils/theme/baseStyles';
-import { theme } from 'utils/theme/theme';
-
-import { StudentViewersList } from '.';
-
-describe('', () => {
- it('adds and removes several users from the list.', () => {
- const video = videoMockFactory();
- const { rerender } = render(
- wrapInIntlProvider(),
- );
- expect(screen.queryByText('On stage')).toEqual(null);
- expect(screen.queryByText('Other participants')).toEqual(null);
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.instructor@prosody.org',
- isInstructor: true,
- isOnStage: true,
- name: 'Instructor',
- }),
- );
- screen.getByText('Instructor');
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student1@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 1',
- }),
- );
- screen.getByText('Student 1');
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student2@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 2',
- }),
- );
- screen.getByText('Student 2');
-
- act(() => useParticipantsStore.getState().removeParticipant('Student 2'));
- expect(screen.queryByText('Student 2')).not.toBeInTheDocument();
-
- const student2 = participantMockFactory({ name: 'Student 2' });
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student2@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 2',
- }),
- );
- rerender(
- wrapInIntlProvider(
- ,
- ),
- );
- screen.getByText('Student 2');
- });
-
- it('renders StudentViewersList component with data, and compares it with previous render.', async () => {
- const video = videoMockFactory();
- render(
- wrapInIntlProvider(
-
-
-
- ,
- ),
- );
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.instructor@prosody.org',
- isInstructor: true,
- isOnStage: true,
- name: 'Instructor',
- }),
- );
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student1@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 1',
- }),
- );
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student2@prosody.org',
- isInstructor: false,
- isOnStage: false,
- name: 'Student 2',
- }),
- );
-
- act(() =>
- useParticipantsStore.getState().addParticipant({
- id: 'example.jid.student3@prosody.org',
- isInstructor: false,
- isOnStage: true,
- name: 'Student 3',
- }),
- );
-
- await imageSnapshot();
-
- act(() => useParticipantsStore.getState().removeParticipant('Student 2'));
-
- await imageSnapshot();
- });
-});
diff --git a/src/frontend/components/ViewersList/StudentViewersList/index.tsx b/src/frontend/components/ViewersList/StudentViewersList/index.tsx
deleted file mode 100644
index f28030839d..0000000000
--- a/src/frontend/components/ViewersList/StudentViewersList/index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from 'react';
-import { Box, List } from 'grommet';
-import { defineMessages, useIntl } from 'react-intl';
-
-import { ViewersListHeader } from 'components/ViewersList/components/ViewersListHeader';
-import { ViewersListItem } from 'components/ViewersList/components/ViewersListItem';
-import { ViewersListItemContainer } from 'components/ViewersList/components/ViewersListItemContainer';
-import {
- useParticipantsStore,
- ParticipantType,
-} from 'data/stores/useParticipantsStore';
-import { Video } from 'types/tracks';
-
-const messages = defineMessages({
- onStage: {
- defaultMessage: 'On stage',
- description: 'On-stage participants are displayed under this label.',
- id: 'components.ViewersList.onStage',
- },
- otherViewers: {
- defaultMessage: 'Other participants',
- description: 'Connected participants are displayed under this label.',
- id: 'components.ViewersList.otherViewers',
- },
-});
-
-interface StudentViewersListProps {
- video: Video;
-}
-
-export const StudentViewersList = ({ video }: StudentViewersListProps) => {
- const participants = useParticipantsStore((state) => state.participants);
- const participantsOnStage = participants.filter(
- (participant) =>
- video.participants_in_discussion.some(
- (p) => p.name === participant.name,
- ) || participant.isInstructor,
- );
-
- const participantsNotOnStage = participants.filter(
- (participant) => !participantsOnStage.includes(participant),
- );
- const intl = useIntl();
-
- return (
-
- {participantsOnStage.length !== 0 && (
-
-
-
- {(item: ParticipantType, index: number) => (
-
-
-
- )}
-
-
- )}
- {participantsNotOnStage.length !== 0 && (
-
-
-
- {(item: ParticipantType, index: number) => (
-
-
-
- )}
-
-
- )}
-
- );
-};
diff --git a/src/frontend/components/ViewersList/components/ViewersListHeader/index.tsx b/src/frontend/components/ViewersList/components/ViewersListHeader/index.tsx
index e807d85149..005f237c9d 100644
--- a/src/frontend/components/ViewersList/components/ViewersListHeader/index.tsx
+++ b/src/frontend/components/ViewersList/components/ViewersListHeader/index.tsx
@@ -24,12 +24,12 @@ export const ViewersListHeader = ({
size: 'xsmall',
}}
pad={{
- horizontal: '12px',
+ horizontal: 'small',
vertical: '2px',
}}
round="14px"
>
-
+
{text}
diff --git a/src/frontend/components/ViewersList/components/ViewersListItemContainer/index.tsx b/src/frontend/components/ViewersList/components/ViewersListItemContainer/index.tsx
index 6c90253b2f..1dd2aa5bd2 100644
--- a/src/frontend/components/ViewersList/components/ViewersListItemContainer/index.tsx
+++ b/src/frontend/components/ViewersList/components/ViewersListItemContainer/index.tsx
@@ -18,8 +18,8 @@ export const ViewersListItemContainer = ({
align="center"
justify="between"
direction="row"
- gap="10px"
- pad={{ horizontal: '20px', vertical: '4px' }}
+ gap="small"
+ pad={{ horizontal: 'medium', vertical: 'xsmall' }}
>
{children}
diff --git a/src/frontend/components/ViewersList/index.spec.tsx b/src/frontend/components/ViewersList/index.spec.tsx
index e280e8caa9..f3f799fb12 100644
--- a/src/frontend/components/ViewersList/index.spec.tsx
+++ b/src/frontend/components/ViewersList/index.spec.tsx
@@ -1,5 +1,6 @@
+import userEvent from '@testing-library/user-event';
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { act, render, screen } from '@testing-library/react';
import {
participantMockFactory,
@@ -7,57 +8,239 @@ import {
} from 'utils/tests/factories';
import { wrapInIntlProvider } from 'utils/tests/intl';
import { useParticipantsStore } from 'data/stores/useParticipantsStore/index';
+import { converse } from 'utils/window';
import { ViewersList } from '.';
-const mockedAskingParticipant = participantMockFactory();
-const mockedAskingParticipantFull = {
- ...mockedAskingParticipant,
+jest.mock('utils/window', () => ({
+ converse: {
+ acceptParticipantToJoin: jest.fn(),
+ askParticipantToJoin: jest.fn(),
+ kickParticipant: jest.fn(),
+ rejectParticipantToJoin: jest.fn(),
+ },
+}));
+
+const mockedParticipantOnDemands1 = participantMockFactory();
+const mockedParticipantOnDemands2 = participantMockFactory();
+
+const mockedParticipantOnStage1 = participantMockFactory();
+const mockedParticipantOnStage2 = participantMockFactory();
+
+const mockedParticipantOnStage1Full = {
+ ...mockedParticipantOnStage1,
isInstructor: false,
+ isOnStage: true,
+};
+
+const mockedParticipantOnStage2Full = {
+ ...mockedParticipantOnStage2,
+ isInstructor: false,
+ isOnStage: true,
+};
+
+const participant1 = {
+ id: 'example.jid.student1@prosody.org',
+ isInstructor: true,
isOnStage: false,
+ name: 'Student 1',
};
-const notOnStageAndNotAskingParticipantFull = {
- id: 'id',
- name: 'name',
+const participant2 = {
+ id: 'example.jid.student2@prosody.org',
isInstructor: false,
isOnStage: false,
+ name: 'Student 2',
};
-const mockVideo = videoMockFactory({
- participants_asking_to_join: [mockedAskingParticipant],
-});
+const participant3 = {
+ id: 'example.jid.student3@prosody.org',
+ isInstructor: false,
+ isOnStage: false,
+ name: 'Student 3',
+};
+
+describe(' when user is an instructor', () => {
+ beforeEach(() => jest.resetAllMocks());
+
+ it('displays severals participants, not on stage and not asking, and then remove some of them', () => {
+ const video = videoMockFactory();
+
+ render(
+ wrapInIntlProvider(),
+ );
+
+ expect(screen.queryByText('Demands')).toEqual(null);
+ expect(screen.queryByText('On stage')).toEqual(null);
+ expect(screen.queryByText('Other participants')).toEqual(null);
+
+ act(() => useParticipantsStore.getState().addParticipant(participant1));
+ act(() => useParticipantsStore.getState().addParticipant(participant2));
+ act(() => useParticipantsStore.getState().addParticipant(participant3));
+
+ screen.getByText('Student 1');
+ screen.getByText('Student 2');
+ screen.getByText('Student 3');
+
+ act(() => useParticipantsStore.getState().removeParticipant('Student 2'));
+
+ expect(screen.queryByText('Student 2')).not.toBeInTheDocument();
+ });
+
+ it('displays severals participants, some on stage, and some asking, and some not on stage and not asking', () => {
+ const video = videoMockFactory({
+ participants_asking_to_join: [
+ mockedParticipantOnDemands1,
+ mockedParticipantOnDemands2,
+ ],
+ participants_in_discussion: [
+ mockedParticipantOnStage1,
+ mockedParticipantOnStage2,
+ ],
+ });
-describe('', () => {
- beforeEach(() => {
useParticipantsStore.setState({
- participants: [notOnStageAndNotAskingParticipantFull],
+ participants: [
+ participant1,
+ participant2,
+ mockedParticipantOnStage1Full,
+ mockedParticipantOnStage2Full,
+ ],
});
- jest.resetAllMocks();
- });
- it('renders the correct viewers list if the user is not an instructor', () => {
render(
- wrapInIntlProvider(
- ,
- ),
+ wrapInIntlProvider(),
);
+ screen.getByText('Demands');
+ screen.getByText('On stage');
screen.getByText('Other participants');
- screen.getByText(notOnStageAndNotAskingParticipantFull.name);
- expect(screen.queryByText('Demands')).toEqual(null);
- expect(screen.queryByText(mockedAskingParticipantFull.name)).toEqual(null);
+ screen.getByText('Student 1');
+ screen.getByText('Student 2');
+ screen.getByText(mockedParticipantOnDemands1.name);
+ screen.getByText(mockedParticipantOnDemands2.name);
+ screen.getByText(mockedParticipantOnStage1Full.name);
+ screen.getByText(mockedParticipantOnStage2Full.name);
});
- it('renders the correct viewers list if the user is an instructor', () => {
+ it('displays a demanding participant and accepts it', () => {
+ const video = videoMockFactory({
+ participants_asking_to_join: [mockedParticipantOnDemands1],
+ });
+
render(
- wrapInIntlProvider(),
+ wrapInIntlProvider(),
);
- screen.getByText('Other participants');
- screen.getByText(notOnStageAndNotAskingParticipantFull.name);
+ screen.getByText('Demands');
+ expect(screen.queryByText('On stage')).toEqual(null);
+ expect(screen.queryByText('Other participants')).toEqual(null);
+ screen.getByText(mockedParticipantOnDemands1.name);
+
+ const acceptButton = screen.getByRole('button', { name: 'Accept' });
+ act(() => userEvent.click(acceptButton));
+ expect(converse.acceptParticipantToJoin).toHaveBeenCalledTimes(1);
+ });
+
+ it('displays a demanding participant and rejects it', () => {
+ const video = videoMockFactory({
+ participants_asking_to_join: [mockedParticipantOnDemands1],
+ });
+
+ render(
+ wrapInIntlProvider(),
+ );
screen.getByText('Demands');
- screen.getByText(mockedAskingParticipantFull.name);
+ expect(screen.queryByText('On stage')).toEqual(null);
+ expect(screen.queryByText('Other participants')).toEqual(null);
+ screen.getByText(mockedParticipantOnDemands1.name);
+
+ const rejectButton = screen.getAllByRole('button')[0];
+ act(() => userEvent.click(rejectButton));
+ expect(converse.rejectParticipantToJoin).toHaveBeenCalledTimes(1);
+ });
+
+ it('displays an on-stage participant and kicks it', () => {
+ const video = videoMockFactory({
+ participants_in_discussion: [mockedParticipantOnStage1],
+ });
+
+ useParticipantsStore.setState({
+ participants: [mockedParticipantOnStage1Full],
+ });
+
+ render(
+ wrapInIntlProvider(),
+ );
+
+ expect(screen.queryByText('Demands')).toEqual(null);
+ screen.getByText('On stage');
+ expect(screen.queryByText('Other participants')).toEqual(null);
+ screen.getByText(mockedParticipantOnStage1Full.name);
+
+ const terminateButton = screen.getByRole('button', { name: 'Terminate' });
+ act(() => userEvent.click(terminateButton));
+ expect(converse.kickParticipant).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe(' when user is a student', () => {
+ it('adds and removes several users from the list.', () => {
+ const video = videoMockFactory();
+ const { rerender } = render(
+ wrapInIntlProvider(),
+ );
+ expect(screen.queryByText('Demands')).toEqual(null);
+ expect(screen.queryByText('On stage')).toEqual(null);
+ expect(screen.queryByText('Other participants')).toEqual(null);
+
+ act(() =>
+ useParticipantsStore.getState().addParticipant({
+ id: 'example.jid.instructor@prosody.org',
+ isInstructor: true,
+ isOnStage: true,
+ name: 'Instructor',
+ }),
+ );
+ screen.getByText('Instructor');
+
+ act(() =>
+ useParticipantsStore.getState().addParticipant({
+ id: 'example.jid.student1@prosody.org',
+ isInstructor: false,
+ isOnStage: false,
+ name: 'Student 1',
+ }),
+ );
+ screen.getByText('Student 1');
+
+ act(() =>
+ useParticipantsStore.getState().addParticipant({
+ id: 'example.jid.student2@prosody.org',
+ isInstructor: false,
+ isOnStage: false,
+ name: 'Student 2',
+ }),
+ );
+ screen.getByText('Student 2');
+
+ expect(screen.queryByText('Demands')).toEqual(null);
+
+ act(() => useParticipantsStore.getState().removeParticipant('Student 2'));
+ expect(screen.queryByText('Student 2')).not.toBeInTheDocument();
+
+ act(() =>
+ useParticipantsStore.getState().addParticipant({
+ id: 'example.jid.student2@prosody.org',
+ isInstructor: false,
+ isOnStage: false,
+ name: 'Student 2',
+ }),
+ );
+ rerender(
+ wrapInIntlProvider(),
+ );
+ screen.getByText('Student 2');
});
});
diff --git a/src/frontend/components/ViewersList/index.tsx b/src/frontend/components/ViewersList/index.tsx
index 5b087a1656..91459440c8 100644
--- a/src/frontend/components/ViewersList/index.tsx
+++ b/src/frontend/components/ViewersList/index.tsx
@@ -1,18 +1,185 @@
-import React from 'react';
+import React, { useMemo } from 'react';
+import { Box, Button, List } from 'grommet';
+import { AddCircle } from 'grommet-icons';
+import { defineMessages, useIntl } from 'react-intl';
-import { InstructorViewersList } from 'components/ViewersList/InstructorViewersList';
-import { StudentViewersList } from 'components/ViewersList/StudentViewersList';
+import {
+ useParticipantsStore,
+ ParticipantType,
+} from 'data/stores/useParticipantsStore';
import { Video } from 'types/tracks';
+import { ViewersListHeader } from 'components/ViewersList/components/ViewersListHeader';
+import { ViewersListItem } from 'components/ViewersList/components/ViewersListItem';
+import { converse } from 'utils/window';
+import { ViewersListItemContainer } from 'components/ViewersList/components/ViewersListItemContainer';
+import { ViewersListTextButton } from 'components/ViewersList/components/ViewersListTextButton';
-interface ViewersList {
+const messages = defineMessages({
+ demands: {
+ defaultMessage: 'Demands',
+ description:
+ 'Participants asking for going on stage are displayed under this label.',
+ id: 'components.ViewersList.demands',
+ },
+ onStage: {
+ defaultMessage: 'On stage',
+ description: 'On-stage participants are displayed under this label.',
+ id: 'components.ViewersList.onStage',
+ },
+ otherViewers: {
+ defaultMessage: 'Other participants',
+ description: 'Connected participants are displayed under this label.',
+ id: 'components.ViewersList.otherViewers',
+ },
+ acceptButton: {
+ defaultMessage: 'Accept',
+ description:
+ 'The text displayed in the button in charge of accepting on-stage request, in the viewers list.',
+ id: 'components.ViewersList.acceptButton',
+ },
+ endOnStageButton: {
+ defaultMessage: 'Terminate',
+ description:
+ 'The text displayed in the button in charge of making students on stage exiting the stage, in the viewers list.',
+ id: 'components.ViewersList.endOnStageButton',
+ },
+});
+
+interface ViewersListProps {
isInstructor: boolean;
video: Video;
}
-export const ViewersList = ({ isInstructor, video }: ViewersList) => {
- return isInstructor ? (
-
- ) : (
-
+export const ViewersList = ({ isInstructor, video }: ViewersListProps) => {
+ const participants = useParticipantsStore((state) => state.participants);
+ const participantsOnStage = useMemo(
+ () =>
+ participants.filter(
+ (participant) =>
+ participant.isInstructor ||
+ video.participants_in_discussion.some(
+ (p) => p.name === participant.name,
+ ),
+ ),
+ [participants, video.participants_in_discussion],
+ );
+
+ const participantsNotOnStageAndNotAsking = useMemo(
+ () =>
+ participants.filter(
+ (participant) =>
+ !participantsOnStage.includes(participant) &&
+ (isInstructor
+ ? !video.participants_asking_to_join.some(
+ (p) => p.name === participant.name,
+ )
+ : true),
+ ),
+ [
+ isInstructor,
+ participants,
+ participantsOnStage,
+ video.participants_asking_to_join,
+ ],
+ );
+ const intl = useIntl();
+
+ return (
+
+ {isInstructor && video.participants_asking_to_join.length !== 0 && (
+
+
+
+ participantA.name.localeCompare(participantB.name),
+ )}
+ pad="none"
+ >
+ {(item: ParticipantType, index: number) => (
+
+
+
+ }
+ onClick={() => converse.rejectParticipantToJoin(item)}
+ plain
+ style={{ padding: '0px', transform: 'rotate(45deg)' }}
+ />
+
+ converse.acceptParticipantToJoin(item, video)
+ }
+ text={intl.formatMessage(messages.acceptButton)}
+ />
+
+
+ )}
+
+
+ )}
+
+ {participantsOnStage.length !== 0 && (
+
+
+
+ {(item: ParticipantType, index: number) => (
+
+
+ {isInstructor && !item.isInstructor && (
+ converse.kickParticipant(item)}
+ text={intl.formatMessage(messages.endOnStageButton)}
+ />
+ )}
+
+ )}
+
+
+ )}
+ {participantsNotOnStageAndNotAsking.length !== 0 && (
+
+
+
+ {(item: ParticipantType, index: number) => (
+
+
+
+ )}
+
+
+ )}
+
);
};