import React, { useMemo } from 'react'; import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component'; import { defineMessages, useIntl } from 'react-intl'; import { range } from 'ramda'; import { uniqueId } from '/imports/utils/string-utils'; import { useIsImportPresentationWithAnnotationsFromBreakoutRoomsEnabled, useIsImportSharedNotesFromBreakoutRoomsEnabled } from '/imports/ui/services/features'; import useMeeting from '/imports/ui/core/hooks/useMeeting'; import { useLazyQuery, useQuery, useMutation } from '@apollo/client'; import Styled from './styles'; import { getBreakouts, getBreakoutsResponse, getUser, getUserResponse, } from './queries'; import { PRESENTATIONS_SUBSCRIPTION } from '/imports/ui/components/whiteboard/queries'; import logger from '/imports/startup/client/logger'; import BreakoutRoomUserAssignment from './breakout-room-user-assignment/component'; import deviceInfo from '/imports/utils/deviceInfo'; import BreakoutRoomUserAssignmentMobile from './breakout-room-user-assignment-mobile/component'; import RoomManagmentState from './room-managment-state/component'; import { Rooms, RoomToWithSettings, BreakoutUser, moveUserRegistery, Presentation, RoomPresentations, } from './room-managment-state/types'; import { BREAKOUT_ROOM_CREATE, BREAKOUT_ROOM_MOVE_USER } from '../mutations'; import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription'; const MIN_BREAKOUT_ROOMS = 2; const MIN_BREAKOUT_TIME = 5; const DEFAULT_BREAKOUT_TIME = 15; const CURRENT_SLIDE_PREFIX = 'current-'; interface CreateBreakoutRoomContainerProps { isOpen: boolean setIsOpen: (isOpen: boolean) => void priority: string, isUpdate?: boolean, } interface CreateBreakoutRoomProps extends CreateBreakoutRoomContainerProps { isBreakoutRecordable: boolean, users: Array, runningRooms: getBreakoutsResponse['breakoutRoom'], presentations: Array, currentPresentation: string, } const intlMessages = defineMessages({ breakoutRoomTitle: { id: 'app.createBreakoutRoom.title', description: 'modal title', }, breakoutRoomDesc: { id: 'app.createBreakoutRoom.modalDesc', description: 'modal description', }, breakoutRoomUpdateDesc: { id: 'app.updateBreakoutRoom.modalDesc', description: 'update modal description', }, cancelLabel: { id: 'app.updateBreakoutRoom.cancelLabel', description: 'used in the button that close update modal', }, updateTitle: { id: 'app.updateBreakoutRoom.title', description: 'update breakout title', }, updateConfirm: { id: 'app.updateBreakoutRoom.confirm', description: 'Update to breakout confirm button label', }, resetUserRoom: { id: 'app.update.resetRoom', description: 'Reset user room button label', }, confirmButton: { id: 'app.createBreakoutRoom.confirm', description: 'confirm button label', }, dismissLabel: { id: 'app.presentationUploder.dismissLabel', description: 'used in the button that close modal', }, numberOfRooms: { id: 'app.createBreakoutRoom.numberOfRooms', description: 'number of rooms label', }, duration: { id: 'app.createBreakoutRoom.durationInMinutes', description: 'duration time label', }, resetAssignments: { id: 'app.createBreakoutRoom.resetAssignments', description: 'reset assignments label', }, resetAssignmentsDesc: { id: 'app.createBreakoutRoom.resetAssignmentsDesc', description: 'reset assignments label description', }, randomlyAssign: { id: 'app.createBreakoutRoom.randomlyAssign', description: 'randomly assign label', }, randomlyAssignDesc: { id: 'app.createBreakoutRoom.randomlyAssignDesc', description: 'randomly assign label description', }, breakoutRoom: { id: 'app.createBreakoutRoom.room', description: 'breakout room', }, freeJoinLabel: { id: 'app.createBreakoutRoom.freeJoin', description: 'free join label', }, captureNotesLabel: { id: 'app.createBreakoutRoom.captureNotes', description: 'capture shared notes label', }, captureSlidesLabel: { id: 'app.createBreakoutRoom.captureSlides', description: 'capture slides label', }, captureNotesType: { id: 'app.notes.label', description: 'indicates notes have been captured', }, captureSlidesType: { id: 'app.shortcut-help.whiteboard', description: 'indicates the whiteboard has been captured', }, roomLabel: { id: 'app.createBreakoutRoom.room', description: 'Room label', }, leastOneWarnBreakout: { id: 'app.createBreakoutRoom.leastOneWarnBreakout', description: 'warn message label', }, notAssigned: { id: 'app.createBreakoutRoom.notAssigned', description: 'Not assigned label', }, breakoutRoomLabel: { id: 'app.createBreakoutRoom.breakoutRoomLabel', description: 'breakout room label', }, addParticipantLabel: { id: 'app.createBreakoutRoom.addParticipantLabel', description: 'add Participant label', }, nextLabel: { id: 'app.createBreakoutRoom.nextLabel', description: 'Next label', }, backLabel: { id: 'app.audio.backLabel', description: 'Back label', }, minusRoomTime: { id: 'app.createBreakoutRoom.minusRoomTime', description: 'aria label for btn to decrease room time', }, addRoomTime: { id: 'app.createBreakoutRoom.addRoomTime', description: 'aria label for btn to increase room time', }, record: { id: 'app.createBreakoutRoom.record', description: 'label for checkbox to allow record', }, roomTime: { id: 'app.createBreakoutRoom.roomTime', description: 'used to provide current room time for aria label', }, numberOfRoomsIsValid: { id: 'app.createBreakoutRoom.numberOfRoomsError', description: 'Label an error message', }, roomNameEmptyIsValid: { id: 'app.createBreakoutRoom.emptyRoomNameError', description: 'Label an error message', }, roomNameDuplicatedIsValid: { id: 'app.createBreakoutRoom.duplicatedRoomNameError', description: 'Label an error message', }, you: { id: 'app.userList.you', description: 'Text for identifying your user', }, minimumDurationWarnBreakout: { id: 'app.createBreakoutRoom.minimumDurationWarnBreakout', description: 'minimum duration warning message label', }, roomNameInputDesc: { id: 'app.createBreakoutRoom.roomNameInputDesc', description: 'aria description for room name change', }, movedUserLabel: { id: 'app.createBreakoutRoom.movedUserLabel', description: 'screen reader alert when users are moved to rooms', }, manageRooms: { id: 'app.createBreakoutRoom.manageRoomsLabel', description: 'Label for manage rooms', }, sendInvitationToMods: { id: 'app.createBreakoutRoom.sendInvitationToMods', description: 'label for checkbox send invitation to moderators', }, }); const CreateBreakoutRoom: React.FC = ({ isOpen, setIsOpen, priority, isUpdate = false, isBreakoutRecordable, users, runningRooms, presentations, currentPresentation, }) => { const { isMobile } = deviceInfo; const intl = useIntl(); const [numberOfRoomsIsValid, setNumberOfRoomsIsValid] = React.useState(true); const [durationIsValid, setDurationIsValid] = React.useState(true); const [freeJoin, setFreeJoin] = React.useState(false); const [record, setRecord] = React.useState(false); const [captureSlides, setCaptureSlides] = React.useState(false); const [leastOneUserIsValid, setLeastOneUserIsValid] = React.useState(false); const [captureNotes, setCaptureNotes] = React.useState(false); const [inviteMods, setInviteMods] = React.useState(false); const [numberOfRooms, setNumberOfRooms] = React.useState(MIN_BREAKOUT_ROOMS); const [durationTime, setDurationTime] = React.useState(DEFAULT_BREAKOUT_TIME); const isImportPresentationWithAnnotationsEnabled = useIsImportPresentationWithAnnotationsFromBreakoutRoomsEnabled(); const isImportSharedNotesEnabled = useIsImportSharedNotesFromBreakoutRoomsEnabled(); const [roomPresentations, setRoomPresentations] = React.useState([]); const [createBreakoutRoom] = useMutation(BREAKOUT_ROOM_CREATE); const [moveUser] = useMutation(BREAKOUT_ROOM_MOVE_USER); const roomsRef = React.useRef({}); const moveRegisterRef = React.useRef({}); const setRoomsRef = (rooms: Rooms) => { roomsRef.current = rooms; }; const setMoveRegisterRef = (moveRegister: moveUserRegistery) => { moveRegisterRef.current = moveRegister; }; const checkboxCallbackFactory = (call: (value: boolean) => void) => (e: React.ChangeEvent) => { const { checked } = e.target; call(checked); }; const getRoomPresentation = (position: number) => { if (roomPresentations[position]) return roomPresentations[position]; return `${CURRENT_SLIDE_PREFIX}${currentPresentation}`; }; const createRoom = () => { const rooms = roomsRef.current; const roomsArray: RoomToWithSettings[] = []; /* eslint no-restricted-syntax: "off" */ for (let i = 0; i < numberOfRooms; i += 1) { const roomNumber = i + 1; if (rooms[roomNumber]) { const r = rooms[roomNumber]; roomsArray.push({ name: r.name, sequence: r.id, captureNotesFilename: `${r.name.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureNotesType)}`, captureSlidesFilename: `${r.name.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureSlidesType)}`, isDefaultName: r.name === intl.formatMessage(intlMessages.breakoutRoom, { 0: r.id, }), users: r.users.map((u) => u.userId), freeJoin, shortName: r.name, allPages: !getRoomPresentation(r.id).startsWith(CURRENT_SLIDE_PREFIX), presId: getRoomPresentation(r.id).replace(CURRENT_SLIDE_PREFIX, ''), }); } else { const defaultName = intl.formatMessage(intlMessages.breakoutRoom, { 0: roomNumber, }); roomsArray.push({ name: defaultName, sequence: roomNumber, captureNotesFilename: `${defaultName.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureNotesType)}`, captureSlidesFilename: `${defaultName.replace(/\s/g, '_')}_${intl.formatMessage(intlMessages.captureSlidesType)}`, isDefaultName: true, freeJoin, shortName: defaultName, users: [], allPages: !getRoomPresentation(roomNumber).startsWith(CURRENT_SLIDE_PREFIX), presId: getRoomPresentation(roomNumber).replace(CURRENT_SLIDE_PREFIX, ''), }); } } createBreakoutRoom( { variables: { record, captureNotes, captureSlides, durationInMinutes: durationTime, sendInviteToModerators: inviteMods, rooms: roomsArray, }, }, ); setIsOpen(false); }; const userUpdate = () => { const userIds = Object.keys(moveRegisterRef.current); userIds.forEach((userId) => { const { fromRoomId, toRoomId } = moveRegisterRef.current[userId]; if (fromRoomId !== toRoomId) { moveUser({ variables: { userId, fromBreakoutRoomId: fromRoomId || '', toBreakoutRoomId: toRoomId, }, }); } }); setIsOpen(false); }; const title = useMemo(() => ( {isUpdate ? intl.formatMessage(intlMessages.breakoutRoomUpdateDesc) : intl.formatMessage(intlMessages.breakoutRoomDesc)} ), [isUpdate]); const checkboxesInfo = useMemo(() => { return [ { allowed: true, htmlFor: 'freeJoinCheckbox', key: 'free-join-breakouts', id: 'freeJoinCheckbox', onChange: checkboxCallbackFactory((e: boolean) => { setFreeJoin(e); setLeastOneUserIsValid(true); }), label: intl.formatMessage(intlMessages.freeJoinLabel), }, { allowed: isBreakoutRecordable, htmlFor: 'recordBreakoutCheckbox', key: 'record-breakouts', id: 'recordBreakoutCheckbox', onChange: checkboxCallbackFactory(setRecord), label: intl.formatMessage(intlMessages.record), }, { allowed: isImportPresentationWithAnnotationsEnabled, htmlFor: 'captureSlidesBreakoutCheckbox', key: 'capture-slides-breakouts', id: 'captureSlidesBreakoutCheckbox', onChange: checkboxCallbackFactory(setCaptureSlides), label: intl.formatMessage(intlMessages.captureSlidesLabel), }, { allowed: isImportSharedNotesEnabled, htmlFor: 'captureNotesBreakoutCheckbox', key: 'capture-notes-breakouts', id: 'captureNotesBreakoutCheckbox', onChange: checkboxCallbackFactory(setCaptureNotes), label: intl.formatMessage(intlMessages.captureNotesLabel), }, { allowed: true, htmlFor: 'sendInvitationToAssignedModeratorsCheckbox', key: 'send-invitation-to-assigned-moderators-breakouts', id: 'sendInvitationToAssignedModeratorsCheckbox', onChange: checkboxCallbackFactory(setInviteMods), label: intl.formatMessage(intlMessages.sendInvitationToMods), }, ]; }, [isBreakoutRecordable, isImportPresentationWithAnnotationsEnabled, isImportSharedNotesEnabled]); const form = useMemo(() => { if (isUpdate) return null; const BREAKOUT_LIM = window.meetingClientSettings.public.app.breakouts.breakoutRoomLimit; const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS; return (
{intl.formatMessage(intlMessages.numberOfRooms)} ) => { const { value } = e.target; setNumberOfRooms(Number.parseInt(value, 10)); setNumberOfRoomsIsValid(true); }} aria-label={intl.formatMessage(intlMessages.numberOfRooms)} > { range(MIN_BREAKOUT_ROOMS, MAX_BREAKOUT_ROOMS + 1).map((item) => ()) }
{intl.formatMessage(intlMessages.duration)} ) => { const { value } = e.target; const v = Number.parseInt(value, 10); setDurationTime(v); setDurationIsValid(v >= MIN_BREAKOUT_TIME); }} onBlur={(e: React.FocusEvent) => { const { value } = e.target; const v = Number.parseInt(value, 10); setDurationTime((v && !(v <= 0)) ? v : MIN_BREAKOUT_TIME); setDurationIsValid(true); }} aria-label={intl.formatMessage(intlMessages.duration)} data-test="durationTime" /> { intl.formatMessage( intlMessages.minimumDurationWarnBreakout, { 0: MIN_BREAKOUT_TIME }, ) } {checkboxesInfo .filter((item) => item.allowed) .map((item) => ( {item.label} ))}
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)} {intl.formatMessage(intlMessages.randomlyAssignDesc)}
); }, [ durationTime, durationIsValid, numberOfRooms, numberOfRoomsIsValid, isImportPresentationWithAnnotationsEnabled, isImportSharedNotesEnabled, ]); return ( setIsOpen(false), disabled: false, }} isOpen={isOpen} priority={priority} > {title} {form} ); }; const CreateBreakoutRoomContainer: React.FC = ({ isOpen, setIsOpen, priority, isUpdate = false, }) => { const [fetchedBreakouts, setFetchedBreakouts] = React.useState(false); // isBreakoutRecordable - get from meeting breakout policies breakoutPolicies/record const { data: currentMeeting, } = useMeeting((m) => ({ breakoutPolicies: m.breakoutPolicies, })); const { data: usersData, loading: usersLoading, error: usersError, } = useQuery(getUser, { fetchPolicy: 'network-only', }); const [ loadBreakouts, { data: breakoutsData, loading: breakoutsLoading, error: breakoutsError, }, ] = useLazyQuery(getBreakouts, { fetchPolicy: 'network-only', }); const { data: presentationData } = useDeduplicatedSubscription(PRESENTATIONS_SUBSCRIPTION); const presentations = presentationData?.pres_presentation || []; const currentPresentation = presentations.find((p: Presentation) => p.current)?.presentationId || ''; if (usersLoading || breakoutsLoading || !currentMeeting) { return null; } if (!fetchedBreakouts) { loadBreakouts(); setFetchedBreakouts(true); } if (breakoutsLoading) return null; if (usersError || breakoutsError) { logger.info('Error loading users', usersError); logger.info('Error loading breakouts', breakoutsError); return (
{JSON.stringify(usersError) || JSON.stringify(breakoutsError)}
); } return ( ); }; export default CreateBreakoutRoomContainer;