import React, { useEffect, useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Meteor } from 'meteor/meteor'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import Header from '/imports/ui/components/common/control-header/component'; import { useMutation } from '@apollo/client'; import { Input } from '../layout/layoutTypes'; import { layoutDispatch, layoutSelectInput } from '../layout/context'; import { addAlert } from '../screenreader-alert/service'; import { PANELS, ACTIONS } from '../layout/enums'; import useMeeting from '/imports/ui/core/hooks/useMeeting'; import { POLL_CANCEL } from './mutations'; import { GetHasCurrentPresentationResponse, getHasCurrentPresentation } from './queries'; import EmptySlideArea from './components/EmptySlideArea'; import { getSplittedQuestionAndOptions, pollTypes, validateInput } from './service'; import Toggle from '/imports/ui/components/common/switch/component'; import Styled from './styles'; import ResponseChoices from './components/ResponseChoices'; import ResponseTypes from './components/ResponseTypes'; import PollQuestionArea from './components/PollQuestionArea'; import LiveResultContainer from './components/LiveResult'; import Session from '/imports/ui/services/storage/in-memory'; import useDeduplicatedSubscription from '../../core/hooks/useDeduplicatedSubscription'; const POLL_SETTINGS = Meteor.settings.public.poll; const ALLOW_CUSTOM_INPUT = POLL_SETTINGS.allowCustomResponseInput; const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom; const intlMessages = defineMessages({ pollPaneTitle: { id: 'app.poll.pollPaneTitle', description: 'heading label for the poll menu', }, closeLabel: { id: 'app.poll.closeLabel', description: 'label for poll pane close button', }, hidePollDesc: { id: 'app.poll.hidePollDesc', description: 'aria label description for hide poll button', }, quickPollInstruction: { id: 'app.poll.quickPollInstruction', description: 'instructions for using pre configured polls', }, activePollInstruction: { id: 'app.poll.activePollInstruction', description: 'instructions displayed when a poll is active', }, dragDropPollInstruction: { id: 'app.poll.dragDropPollInstruction', description: 'instructions for upload poll options via drag and drop', }, ariaInputCount: { id: 'app.poll.ariaInputCount', description: 'aria label for custom poll input field', }, customPlaceholder: { id: 'app.poll.customPlaceholder', description: 'custom poll input field placeholder text', }, noPresentationSelected: { id: 'app.poll.noPresentationSelected', description: 'no presentation label', }, clickHereToSelect: { id: 'app.poll.clickHereToSelect', description: 'open uploader modal button label', }, questionErr: { id: 'app.poll.questionErr', description: 'question text area error label', }, questionAndOptionsPlaceholder: { id: 'app.poll.questionAndoptions.label', description: 'poll input questions and options label', }, customInputToggleLabel: { id: 'app.poll.customInput.label', description: 'poll custom input toogle button label', }, customInputInstructionsLabel: { id: 'app.poll.customInputInstructions.label', description: 'poll custom input instructions label', }, maxOptionsWarning: { id: 'app.poll.maxOptionsWarning.label', description: 'poll max options error', }, optionErr: { id: 'app.poll.optionErr', description: 'poll input error label', }, tf: { id: 'app.poll.tf', description: 'label for true / false poll', }, a4: { id: 'app.poll.a4', description: 'label for A / B / C / D poll', }, delete: { id: 'app.poll.optionDelete.label', description: '', }, questionLabel: { id: 'app.poll.question.label', description: '', }, optionalQuestionLabel: { id: 'app.poll.optionalQuestion.label', description: '', }, userResponse: { id: 'app.poll.userResponse.label', description: '', }, responseChoices: { id: 'app.poll.responseChoices.label', description: '', }, typedResponseDesc: { id: 'app.poll.typedResponse.desc', description: '', }, responseTypesLabel: { id: 'app.poll.responseTypes.label', description: '', }, addOptionLabel: { id: 'app.poll.addItem.label', description: '', }, startPollLabel: { id: 'app.poll.start.label', description: '', }, secretPollLabel: { id: 'app.poll.secretPoll.label', description: '', }, isSecretPollLabel: { id: 'app.poll.secretPoll.isSecretLabel', description: '', }, true: { id: 'app.poll.answer.true', description: '', }, false: { id: 'app.poll.answer.false', description: '', }, a: { id: 'app.poll.answer.a', description: '', }, b: { id: 'app.poll.answer.b', description: '', }, c: { id: 'app.poll.answer.c', description: '', }, d: { id: 'app.poll.answer.d', description: '', }, e: { id: 'app.poll.answer.e', description: '', }, yna: { id: 'app.poll.yna', description: '', }, yes: { id: 'app.poll.y', description: '', }, no: { id: 'app.poll.n', description: '', }, abstention: { id: 'app.poll.abstention', description: '', }, enableMultipleResponseLabel: { id: 'app.poll.enableMultipleResponseLabel', description: 'label for checkbox to enable multiple choice', }, startPollDesc: { id: 'app.poll.startPollDesc', description: '', }, showRespDesc: { id: 'app.poll.showRespDesc', description: '', }, addRespDesc: { id: 'app.poll.addRespDesc', description: '', }, deleteRespDesc: { id: 'app.poll.deleteRespDesc', description: '', }, on: { id: 'app.switch.onLabel', description: 'label for toggle switch on state', }, off: { id: 'app.switch.offLabel', description: 'label for toggle switch off state', }, removePollOpt: { id: 'app.poll.removePollOpt', description: 'screen reader alert for removed poll option', }, emptyPollOpt: { id: 'app.poll.emptyPollOpt', description: 'screen reader for blank poll option', }, pollingQuestion: { id: 'app.polling.pollQuestionTitle', description: 'polling question header', }, }); interface PollCreationPanelProps { layoutContextDispatch: (action: { type: string; value: string | boolean; }) => void; hasPoll: boolean; hasCurrentPresentation: boolean; } const PollCreationPanel: React.FC = ({ layoutContextDispatch, hasPoll, hasCurrentPresentation, }) => { const [stopPoll] = useMutation(POLL_CANCEL); const intl = useIntl(); const textareaRef = useRef(null); const [customInput, setCustomInput] = React.useState(false); const [question, setQuestion] = useState(''); const [questionAndOptions, setQuestionAndOptions] = useState(''); const [optList, setOptList] = useState>([]); const [error, setError] = useState(null); const [isMultipleResponse, setIsMultipleResponse] = useState(false); const [secretPoll, setSecretPoll] = useState(false); const [warning, setWarning] = useState(''); const [isPasting, setIsPasting] = useState(false); const [type, setType] = useState(''); const handleInputChange = ( e: React.ChangeEvent, index: number, ) => { const list = [...optList]; const validatedVal = validateInput(e.target.value).replace(/\s{2,}/g, ' '); const charsRemovedCount = e.target.value.length - validatedVal.length; const clearError = validatedVal.length > 0 && type !== pollTypes.Response; const input = e.target; const caretStart = e.target.selectionStart ?? 0; const caretEnd = e.target.selectionEnd ?? 0; let questionAndOptionsList: string[] = []; list[index] = { val: validatedVal }; if (questionAndOptions.length > 0) { const QnO = questionAndOptions as string; questionAndOptionsList = QnO.split('\n'); questionAndOptionsList[index + 1] = validatedVal; } setOptList(list); setQuestionAndOptions(questionAndOptionsList.length > 0 ? questionAndOptionsList.join('\n') : ''); setError(clearError ? null : error); input.focus(); input.selectionStart = caretStart - charsRemovedCount; input.selectionEnd = caretEnd - charsRemovedCount; }; const setQuestionAndOptionsFn = (input: string[] | string) => { const { splittedQuestion, optionsList } = getSplittedQuestionAndOptions(input); const optionsListLength = optionsList.length; let maxOptionsWarning = warning; const clearWarning = maxOptionsWarning && optionsListLength <= MAX_CUSTOM_FIELDS; const clearError = input.length > 0 && type === pollTypes.Response; if (optionsListLength > MAX_CUSTOM_FIELDS && optList[MAX_CUSTOM_FIELDS] === undefined) { setWarning(intl.formatMessage(intlMessages.maxOptionsWarning)); if (isPasting) { maxOptionsWarning = intl.formatMessage(intlMessages.maxOptionsWarning); setIsPasting(false); } } setQuestionAndOptions(input); setOptList(optionsList); setQuestion(splittedQuestion); setError(clearError ? null : error); setWarning(clearWarning ? null : maxOptionsWarning); }; const handleTextareaChange = (e: React.ChangeEvent) => { const validatedInput = validateInput(e.target.value); const clearError = validatedInput.length > 0 && type === pollTypes.Response; if (!customInput) { setQuestion(validatedInput); setError(clearError ? null : error); } else { setQuestionAndOptionsFn(validatedInput); } }; const handleRemoveOption = (index: number) => { const list = [...optList]; const removed = list[index]; let questionAndOptionsList: string[] = []; let clearWarning = false; list.splice(index, 1); // If customInput then removing text from input field. if (customInput) { const QnO = questionAndOptions as string; questionAndOptionsList = QnO.split('\n'); delete questionAndOptionsList[index + 1]; questionAndOptionsList = questionAndOptionsList.filter((val: string) => val !== undefined); clearWarning = !!warning && list.length <= MAX_CUSTOM_FIELDS; } setOptList(list); setQuestionAndOptions(questionAndOptionsList.length > 0 ? questionAndOptionsList.join('\n') : ''); setWarning(clearWarning ? null : warning); addAlert(`${intl.formatMessage(intlMessages.removePollOpt, { 0: removed.val || intl.formatMessage(intlMessages.emptyPollOpt) })}`); }; const handleAddOption = () => { setOptList([...optList, { val: '' }]); }; const handleToggle = () => { const toggledValue = !secretPoll; Session.setItem('secretPoll', toggledValue); setSecretPoll(toggledValue); }; const handlePollLetterOptions = () => { if (optList.length === 0) { setType(pollTypes.Letter); setOptList([ { val: '' }, { val: '' }, { val: '' }, { val: '' }, ]); } }; const toggleIsMultipleResponse = () => { setIsMultipleResponse((prev) => !prev); return !isMultipleResponse; }; useEffect(() => { return () => { Session.setItem('secretPoll', false); }; }, []); useEffect(() => { if (textareaRef.current) { textareaRef.current?.focus(); } }, [textareaRef]); const pollOptions = () => { if (!hasCurrentPresentation) return ; if (hasPoll) return ; return ( <> { ALLOW_CUSTOM_INPUT && ( {customInput ? intl.formatMessage(intlMessages.on) : intl.formatMessage(intlMessages.off)} { setCustomInput(!customInput); setType(pollTypes.Custom); }} ariaLabel={intl.formatMessage(intlMessages.customInputToggleLabel)} showToggleLabel={false} data-test="autoOptioningPollBtn" /> ) } {customInput && ( {intl.formatMessage(intlMessages.customInputInstructionsLabel)} )} { setType(null); setOptList([]); setQuestion(''); setQuestionAndOptions(''); }} hasCurrentPresentation={hasCurrentPresentation} handleToggle={handleToggle} error={error} handleInputChange={handleInputChange} handleRemoveOption={handleRemoveOption} customInput={customInput} questionAndOptions={questionAndOptions} /> ); }; return (
{ layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, value: false, }); layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, value: PANELS.NONE, }); }, }} rightButtonProps={{ 'aria-label': `${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`, 'data-test': 'closePolling', icon: 'close', label: intl.formatMessage(intlMessages.closeLabel), onClick: () => { if (hasPoll) stopPoll(); layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, value: false, }); layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, value: PANELS.NONE, }); Session.setItem('forcePollOpen', false); Session.setItem('pollInitiated', false); }, }} customRightButton={null} /> {pollOptions()} {intl.formatMessage(intlMessages.showRespDesc)} {intl.formatMessage(intlMessages.addRespDesc)} {intl.formatMessage(intlMessages.startPollDesc)}
); }; const PollCreationPanelContainer: React.FC = () => { const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent); const layoutContextDispatch = layoutDispatch(); const { sidebarContentPanel } = sidebarContent; const { data: currentUser, loading: currentUserLoading, } = useCurrentUser((u) => { return { presenter: u?.presenter, }; }); const { data: currentMeeting, loading: currentMeetingLoading, } = useMeeting((m) => { return { componentsFlags: m?.componentsFlags, }; }); const { data: getHasCurrentPresentationData, loading: getHasCurrentPresentationLoading, } = useDeduplicatedSubscription(getHasCurrentPresentation); if (currentUserLoading || !currentUser) return null; if (currentMeetingLoading || !currentMeeting) return null; if (getHasCurrentPresentationLoading || !getHasCurrentPresentationData) return null; if (!currentUser.presenter && sidebarContentPanel === PANELS.POLL) { layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN, value: false, }); layoutContextDispatch({ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL, value: PANELS.NONE, }); } return ( 0} /> ); }; export default PollCreationPanelContainer;