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) => ( - - )} - - - - ); -};