diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68c5531a7a..9080080e7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,9 @@ right (and close / open it)
- rename LiveRegistration model in LiveSession
- Disable jitsi prejoin page
- Admin can access all LiveSession belonging to a video
+- Move on-stage request functional (in Instructor view) from under
+the video to the ViewersList in the right panel
+
### Fixed
diff --git a/src/frontend/components/LiveVideoPanel/index.spec.tsx b/src/frontend/components/LiveVideoPanel/index.spec.tsx
index c29e3cf147..69ee31148c 100644
--- a/src/frontend/components/LiveVideoPanel/index.spec.tsx
+++ b/src/frontend/components/LiveVideoPanel/index.spec.tsx
@@ -1,29 +1,48 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
+import { getDecodedJwt } from 'data/appData';
import {
LivePanelItem,
useLivePanelState,
} from 'data/stores/useLivePanelState';
-import { videoMockFactory } from 'utils/tests/factories';
+import { useParticipantsStore } from 'data/stores/useParticipantsStore';
+import {
+ participantMockFactory,
+ videoMockFactory,
+} from 'utils/tests/factories';
import { renderImageSnapshot } from 'utils/tests/imageSnapshot';
import { wrapInIntlProvider } from 'utils/tests/intl';
-
+import { DecodedJwt } from 'types/jwt';
import { LiveVideoPanel } from '.';
+const mockAskingParticipant = participantMockFactory();
+const mockParticipant = participantMockFactory();
+
+const mockVideo = videoMockFactory({
+ participants_asking_to_join: [mockAskingParticipant],
+ participants_in_discussion: [mockParticipant],
+});
+
jest.mock('data/appData', () => ({
- getDecodedJwt: () => ({
- permissions: {
- can_access_dashboard: false,
- can_update: false,
- },
- }),
+ getDecodedJwt: jest.fn(),
}));
-
-const video = videoMockFactory();
+const mockGetDecodedJwt = getDecodedJwt as jest.MockedFunction<
+ typeof getDecodedJwt
+>;
describe('', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
it('closes the panel if no item is selected', () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_access_dashboard: false,
+ can_update: false,
+ },
+ } as DecodedJwt);
const mockSetPanelVisibility = jest.fn();
useLivePanelState.setState({
currentItem: undefined,
@@ -36,7 +55,7 @@ describe('', () => {
});
const { container } = render(
- wrapInIntlProvider(),
+ wrapInIntlProvider(),
);
expect(mockSetPanelVisibility).toBeCalled();
@@ -47,6 +66,12 @@ describe('', () => {
});
it('renders the content with selection', () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_access_dashboard: false,
+ can_update: false,
+ },
+ } as DecodedJwt);
useLivePanelState.setState({
currentItem: LivePanelItem.APPLICATION,
availableItems: [
@@ -56,7 +81,7 @@ describe('', () => {
],
});
- render(wrapInIntlProvider());
+ render(wrapInIntlProvider());
screen.getByRole('tablist');
screen.getByRole('tab', { name: 'application' });
@@ -66,13 +91,96 @@ describe('', () => {
screen.getByText('application content');
});
+ it('renders the correct viewers list if the user is not an instructor', () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_update: false,
+ },
+ } as DecodedJwt);
+
+ useParticipantsStore.setState({
+ participants: [
+ {
+ ...mockParticipant,
+ isInstructor: false,
+ isOnStage: false,
+ },
+ ],
+ });
+ useLivePanelState.setState({
+ currentItem: LivePanelItem.VIEWERS_LIST,
+ availableItems: [
+ LivePanelItem.APPLICATION,
+ LivePanelItem.CHAT,
+ LivePanelItem.VIEWERS_LIST,
+ ],
+ });
+
+ render(wrapInIntlProvider());
+
+ screen.getByRole('tablist');
+ screen.getByRole('tab', { name: 'application' });
+ screen.getByRole('tab', { name: 'chat' });
+ screen.getByRole('tab', { name: 'viewers' });
+
+ screen.getByText('On stage');
+ screen.getByText(mockParticipant.name);
+ expect(screen.queryByText('Demands')).toEqual(null);
+ expect(screen.queryByText(mockAskingParticipant.name)).toEqual(null);
+ });
+
+ it('renders the correct viewers list if the user is an instructor', () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_update: true,
+ },
+ } as DecodedJwt);
+
+ useParticipantsStore.setState({
+ participants: [
+ {
+ ...mockParticipant,
+ isInstructor: false,
+ isOnStage: false,
+ },
+ ],
+ });
+
+ useLivePanelState.setState({
+ currentItem: LivePanelItem.VIEWERS_LIST,
+ availableItems: [
+ LivePanelItem.APPLICATION,
+ LivePanelItem.CHAT,
+ LivePanelItem.VIEWERS_LIST,
+ ],
+ });
+
+ render(wrapInIntlProvider());
+
+ screen.getByRole('tablist');
+ screen.getByRole('tab', { name: 'application' });
+ screen.getByRole('tab', { name: 'chat' });
+ screen.getByRole('tab', { name: 'viewers' });
+
+ screen.getByText('On stage');
+ screen.getByText(mockParticipant.name);
+ screen.getByText('Demands');
+ screen.getByText(mockAskingParticipant.name);
+ });
+
it('does not render tabs with only one item available', () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_access_dashboard: false,
+ can_update: false,
+ },
+ } as DecodedJwt);
useLivePanelState.setState({
currentItem: LivePanelItem.APPLICATION,
availableItems: [LivePanelItem.APPLICATION],
});
- render(wrapInIntlProvider());
+ render(wrapInIntlProvider());
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
expect(
@@ -87,6 +195,12 @@ describe('', () => {
});
it('renders with appropriate style on large screen', async () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_access_dashboard: false,
+ can_update: false,
+ },
+ } as DecodedJwt);
useLivePanelState.setState({
currentItem: LivePanelItem.APPLICATION,
availableItems: [
@@ -96,10 +210,16 @@ describe('', () => {
],
});
- await renderImageSnapshot();
+ await renderImageSnapshot();
});
it('renders with appropriate style on small screen', async () => {
+ mockGetDecodedJwt.mockReturnValue({
+ permissions: {
+ can_access_dashboard: false,
+ can_update: false,
+ },
+ } as DecodedJwt);
useLivePanelState.setState({
currentItem: LivePanelItem.APPLICATION,
availableItems: [
@@ -109,6 +229,6 @@ describe('', () => {
],
});
- await renderImageSnapshot(, 300, 300);
+ await renderImageSnapshot(, 300, 300);
});
});
diff --git a/src/frontend/components/LiveVideoPanel/index.tsx b/src/frontend/components/LiveVideoPanel/index.tsx
index c9421aa1bf..23cf904259 100644
--- a/src/frontend/components/LiveVideoPanel/index.tsx
+++ b/src/frontend/components/LiveVideoPanel/index.tsx
@@ -3,7 +3,8 @@ import { Tabs, Box, Grommet, ResponsiveContext, ThemeType } from 'grommet';
import styled from 'styled-components';
import { Chat } from 'components/Chat';
-import { StudentViewersList } from 'components/StudentViewersList';
+import { ViewersList } from 'components/ViewersList';
+import { getDecodedJwt } from 'data/appData';
import {
LivePanelItem,
useLivePanelState,
@@ -11,7 +12,6 @@ import {
import { Video } from 'types/tracks';
import { ShouldNotHappen } from 'utils/errors/exception';
import { theme } from 'utils/theme/theme';
-
import { LiveVideoTabPanel } from './LiveVideoTabPanel';
const StyledGrommet = styled(Grommet)`
@@ -57,6 +57,7 @@ export const LiveVideoPanel = ({ video }: LiveVideoPanelProps) => {
setPanelVisibility: state.setPanelVisibility,
}),
);
+ const canUpdate = getDecodedJwt().permissions.can_update;
// close panel if there is nothing to display
useEffect(() => {
@@ -97,7 +98,7 @@ export const LiveVideoPanel = ({ video }: LiveVideoPanelProps) => {
content =
application content
;
break;
case LivePanelItem.VIEWERS_LIST:
- content = ;
+ content = ;
break;
default:
throw new ShouldNotHappen(currentItem);
diff --git a/src/frontend/components/StudentLiveWrapper/index.spec.tsx b/src/frontend/components/StudentLiveWrapper/index.spec.tsx
index a59c762aa9..afad6c6596 100644
--- a/src/frontend/components/StudentLiveWrapper/index.spec.tsx
+++ b/src/frontend/components/StudentLiveWrapper/index.spec.tsx
@@ -280,7 +280,8 @@ describe(' as a viewer', () => {
const viewersTabButton = screen.getByRole('tab', { name: 'viewers' });
userEvent.click(viewersTabButton);
- screen.getByText('Other participants');
+ expect(screen.queryByText('On stage')).toBeNull();
+ expect(screen.queryByText('Other participants')).toBeNull();
expect(useLivePanelState.getState().availableItems).toEqual([
LivePanelItem.CHAT,
@@ -586,7 +587,8 @@ describe(' as a streamer', () => {
const viewersTabButton = screen.getByRole('tab', { name: 'viewers' });
userEvent.click(viewersTabButton);
- screen.getByText('Other participants');
+ expect(screen.queryByText('On stage')).toBeNull();
+ expect(screen.queryByText('Other participants')).toBeNull();
expect(useLivePanelState.getState().availableItems).toEqual([
LivePanelItem.CHAT,
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListHeader/__image_snapshots__/index-spec-tsx-student-viewers-list-header-renders-student-viewers-list-header-component-and-compares-it-with-previous-render-1-snap.png b/src/frontend/components/StudentViewersList/StudentViewersListHeader/__image_snapshots__/index-spec-tsx-student-viewers-list-header-renders-student-viewers-list-header-component-and-compares-it-with-previous-render-1-snap.png
deleted file mode 100644
index ead25364b8..0000000000
Binary files a/src/frontend/components/StudentViewersList/StudentViewersListHeader/__image_snapshots__/index-spec-tsx-student-viewers-list-header-renders-student-viewers-list-header-component-and-compares-it-with-previous-render-1-snap.png and /dev/null differ
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.spec.tsx b/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.spec.tsx
deleted file mode 100644
index 08787821a4..0000000000
--- a/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.spec.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import { screen } from '@testing-library/react';
-
-import { renderImageSnapshot } from 'utils/tests/imageSnapshot';
-import { StudentViewersListHeader } from '.';
-
-describe('', () => {
- it('renders StudentViewersListHeader component and compares it with previous render.', async () => {
- await renderImageSnapshot(
- ,
- );
- screen.getByText('An example text');
- });
-});
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.tsx b/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.tsx
deleted file mode 100644
index 48f1bd3f9e..0000000000
--- a/src/frontend/components/StudentViewersList/StudentViewersListHeader/index.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import { Box, Text } from 'grommet';
-import styled from 'styled-components';
-
-const StyledTextHeader = styled(Text)`
- font-family: 'Roboto-Regular';
- letter-spacing: -0.2px;
-`;
-
-interface StudentViewersListHeaderProps {
- text: string;
-}
-
-export const StudentViewersListHeader = ({
- text,
-}: StudentViewersListHeaderProps) => {
- return (
-
-
-
- {text}
-
-
-
- );
-};
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-a-student-and-compares-it-with-previous-render-1-snap.png b/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-a-student-and-compares-it-with-previous-render-1-snap.png
deleted file mode 100644
index a410003448..0000000000
Binary files a/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-a-student-and-compares-it-with-previous-render-1-snap.png and /dev/null differ
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-an-instructor-and-compares-it-with-previous-render-1-snap.png b/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-an-instructor-and-compares-it-with-previous-render-1-snap.png
deleted file mode 100644
index 47fdf16720..0000000000
Binary files a/src/frontend/components/StudentViewersList/StudentViewersListItem/__image_snapshots__/index-spec-tsx-student-viewers-list-item-renders-student-viewers-list-item-component-for-an-instructor-and-compares-it-with-previous-render-1-snap.png and /dev/null differ
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListItem/index.spec.tsx b/src/frontend/components/StudentViewersList/StudentViewersListItem/index.spec.tsx
deleted file mode 100644
index d3dcc26f0f..0000000000
--- a/src/frontend/components/StudentViewersList/StudentViewersListItem/index.spec.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { screen } from '@testing-library/react';
-
-import { renderImageSnapshot } from 'utils/tests/imageSnapshot';
-import { StudentViewersListItem } from '.';
-
-describe('', () => {
- it('renders StudentViewersListItem component, for a student, and compares it with previous render.', async () => {
- await renderImageSnapshot(
- ,
- );
- screen.getByText('An example name');
- });
-
- it('renders StudentViewersListItem component, for an instructor, and compares it with previous render.', async () => {
- await renderImageSnapshot(
- ,
- );
- screen.getByText('An example name');
- });
-});
diff --git a/src/frontend/components/StudentViewersList/StudentViewersListItem/index.tsx b/src/frontend/components/StudentViewersList/StudentViewersListItem/index.tsx
deleted file mode 100644
index d316a52584..0000000000
--- a/src/frontend/components/StudentViewersList/StudentViewersListItem/index.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Box, Text } from 'grommet';
-import React from 'react';
-import styled from 'styled-components';
-
-import { ChatAvatar } from 'components/Chat/SharedChatComponents/ChatAvatar';
-
-const StyledText = styled(Text)`
- font-family: 'Roboto-Medium';
- letter-spacing: 0.07px;
-`;
-
-interface StudentViewersListItemProps {
- isInstructor: boolean;
- name: string;
-}
-
-export const StudentViewersListItem = ({
- isInstructor,
- name,
-}: StudentViewersListItemProps) => {
- return (
-
-
-
- {name}
-
-
- );
-};
diff --git a/src/frontend/components/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/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/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/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/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/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/StudentViewersList/index.spec.tsx b/src/frontend/components/StudentViewersList/index.spec.tsx
deleted file mode 100644
index 6364c95b09..0000000000
--- a/src/frontend/components/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);
- screen.getByText('Other participants');
-
- 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/StudentViewersList/index.tsx b/src/frontend/components/StudentViewersList/index.tsx
deleted file mode 100644
index 283bbd2b96..0000000000
--- a/src/frontend/components/StudentViewersList/index.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import { Box, List } from 'grommet';
-import { defineMessages, useIntl } from 'react-intl';
-
-import {
- useParticipantsStore,
- ParticipantType,
-} from 'data/stores/useParticipantsStore/index';
-import { Video } from 'types/tracks';
-import { StudentViewersListHeader } from './StudentViewersListHeader';
-import { StudentViewersListItem } from './StudentViewersListItem';
-
-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) => (
-
- )}
-
-
- )}
-
-
-
- {(item: ParticipantType, index: number) => (
-
- )}
-
-
-
- );
-};