{
+ 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.set('forcePollOpen', false);
+ Session.set('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,
+ } = useSubscription(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;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/EmptySlideArea.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/EmptySlideArea.tsx
new file mode 100644
index 0000000000..358da3373f
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/EmptySlideArea.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Session } from 'meteor/session';
+import Styled from '../styles';
+
+const intlMessages = defineMessages({
+ noPresentationSelected: {
+ id: 'app.poll.noPresentationSelected',
+ description: 'no presentation label',
+ },
+ clickHereToSelect: {
+ id: 'app.poll.clickHereToSelect',
+ description: 'open uploader modal button label',
+ },
+});
+
+const EmptySlideArea: React.FC = () => {
+ const intl = useIntl();
+ return (
+
+
+ {intl.formatMessage(intlMessages.noPresentationSelected)}
+
+ Session.set('showUploadPresentationView', true)}
+ />
+
+ );
+};
+
+export default EmptySlideArea;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/LiveResult.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/LiveResult.tsx
new file mode 100644
index 0000000000..65d4f6aa9b
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/LiveResult.tsx
@@ -0,0 +1,255 @@
+import { useMutation, useSubscription } from '@apollo/client';
+import React, { useCallback } from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Session } from 'meteor/session';
+import {
+ Bar, BarChart, ResponsiveContainer, XAxis, YAxis,
+} from 'recharts';
+import Styled from '../styles';
+import {
+ ResponseInfo,
+ UserInfo,
+ getCurrentPollData,
+ getCurrentPollDataResponse,
+} from '../queries';
+import logger from '/imports/startup/client/logger';
+import Settings from '/imports/ui/services/settings';
+import { POLL_CANCEL, POLL_PUBLISH_RESULT } from '../mutation';
+import { layoutDispatch } from '../../../layout/context';
+import { ACTIONS, PANELS } from '../../../layout/enums';
+
+const intlMessages = defineMessages({
+ usersTitle: {
+ id: 'app.poll.liveResult.usersTitle',
+ description: 'heading label for poll users',
+ },
+ responsesTitle: {
+ id: 'app.poll.liveResult.responsesTitle',
+ description: 'heading label for poll responses',
+ },
+ publishLabel: {
+ id: 'app.poll.publishLabel',
+ description: 'label for the publish button',
+ },
+ cancelPollLabel: {
+ id: 'app.poll.cancelPollLabel',
+ description: 'label for cancel poll button',
+ },
+ backLabel: {
+ id: 'app.poll.backLabel',
+ description: 'label for the return to poll options button',
+ },
+ doneLabel: {
+ id: 'app.createBreakoutRoom.doneLabel',
+ description: 'label shown when all users have responded',
+ },
+ waitingLabel: {
+ id: 'app.poll.waitingLabel',
+ description: 'label shown while waiting for responses',
+ },
+ secretPollLabel: {
+ id: 'app.poll.liveResult.secretLabel',
+ description: 'label shown instead of users in poll responses if poll is secret',
+ },
+ activePollInstruction: {
+ id: 'app.poll.activePollInstruction',
+ description: 'instructions displayed when a poll is active',
+ },
+});
+
+interface LiveResultProps {
+ questionText: string;
+ responses: Array;
+ usersCount: number;
+ numberOfAnswerCount: number;
+ animations: boolean;
+ pollId: string;
+ users: Array;
+ isSecret: boolean;
+}
+
+const CHAT_CONFIG = window.meetingClientSettings.public.chat;
+const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_group_id;
+
+const LiveResult: React.FC = ({
+ questionText,
+ responses,
+ usersCount,
+ numberOfAnswerCount,
+ animations,
+ pollId,
+ users,
+ isSecret,
+}) => {
+ const intl = useIntl();
+ const [pollPublishResult] = useMutation(POLL_PUBLISH_RESULT);
+ const [stopPoll] = useMutation(POLL_CANCEL);
+
+ const layoutContextDispatch = layoutDispatch();
+ const publishPoll = useCallback((pId: string) => {
+ pollPublishResult({
+ variables: {
+ pollId: pId,
+ },
+ });
+ }, []);
+
+ return (
+
+
+ {intl.formatMessage(intlMessages.activePollInstruction)}
+
+
+ {questionText ? {questionText} : null}
+
+ {usersCount !== numberOfAnswerCount
+ ? (
+
+ {`${intl.formatMessage(intlMessages.waitingLabel, {
+ 0: numberOfAnswerCount,
+ 1: usersCount,
+ })} `}
+
+ )
+ : {intl.formatMessage(intlMessages.doneLabel)}}
+ {usersCount !== numberOfAnswerCount
+ ? : null}
+
+
+
+
+
+
+
+
+
+ {numberOfAnswerCount >= 0
+ ? (
+
+ {
+ Session.set('pollInitiated', false);
+ publishPoll(pollId);
+ stopPoll();
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
+ value: true,
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
+ value: PANELS.CHAT,
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_ID_CHAT_OPEN,
+ value: PUBLIC_CHAT_KEY,
+ });
+ }}
+ disabled={numberOfAnswerCount <= 0}
+ label={intl.formatMessage(intlMessages.publishLabel)}
+ data-test="publishPollingLabel"
+ color="primary"
+ />
+ {
+ Session.set('pollInitiated', false);
+ Session.set('resetPollPanel', true);
+ stopPoll();
+ }}
+ label={intl.formatMessage(intlMessages.cancelPollLabel)}
+ data-test="cancelPollLabel"
+ />
+
+ ) : (
+
{
+ stopPoll();
+ }}
+ label={intl.formatMessage(intlMessages.backLabel)}
+ color="primary"
+ data-test="restartPoll"
+ />
+ )}
+
+ {
+ !isSecret
+ ? (
+
+
+
+ {intl.formatMessage(intlMessages.usersTitle)}
+ {intl.formatMessage(intlMessages.responsesTitle)}
+
+ {
+ users.map((user) => (
+
+ {user.user.name}
+ {user.optionDescIds.join()}
+
+ ))
+ }
+
+
+ )
+ : (
+
+ {intl.formatMessage(intlMessages.secretPollLabel)}
+
+ )
+ }
+
+ );
+};
+
+const LiveResultContainer: React.FC = () => {
+ const {
+ data: currentPollData,
+ loading: currentPollLoading,
+ error: currentPollDataError,
+ } = useSubscription(getCurrentPollData);
+
+ if (currentPollLoading || !currentPollData) {
+ return null;
+ }
+
+ if (currentPollDataError) {
+ logger.error(currentPollDataError);
+ return (
+
+ {JSON.stringify(currentPollDataError)}
+
+ );
+ }
+
+ if (!currentPollData.poll.length) return null;
+ // @ts-ignore - JS code
+ const { animations } = Settings.application;
+ const currentPoll = currentPollData.poll[0];
+ const isSecret = currentPoll.secret;
+ const {
+ questionText,
+ responses,
+ pollId,
+ users,
+ } = currentPoll;
+
+ const numberOfAnswerCount = currentPoll.responses_aggregate.aggregate.sum.optionResponsesCount;
+ const numberOfUsersCount = currentPoll.users_aggregate.aggregate.count;
+
+ return (
+
+ );
+};
+
+export default LiveResultContainer;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollInputs.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollInputs.tsx
new file mode 100644
index 0000000000..04283ac632
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollInputs.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Meteor } from 'meteor/meteor';
+import { pollTypes } from '../service';
+import Styled from '../styles';
+
+const POLL_SETTINGS = Meteor.settings.public.poll;
+const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
+const MAX_INPUT_CHARS = POLL_SETTINGS.maxTypedAnswerLength;
+const MIN_OPTIONS_LENGTH = 2;
+
+const intlMessages = defineMessages({
+ customPlaceholder: {
+ id: 'app.poll.customPlaceholder',
+ description: 'custom poll input field placeholder text',
+ },
+ delete: {
+ id: 'app.poll.optionDelete.label',
+ description: '',
+ },
+ deleteRespDesc: {
+ id: 'app.poll.deleteRespDesc',
+ description: '',
+ },
+ emptyPollOpt: {
+ id: 'app.poll.emptyPollOpt',
+ description: 'screen reader for blank poll option',
+ },
+});
+
+interface PollInputsProps {
+ optList: Array<{ val: string }>;
+ handleInputChange: (e: React.ChangeEvent, i: number) => void;
+ handleRemoveOption: (i: number) => void;
+ type: string | null;
+ error: string | null;
+}
+
+const PollInputs: React.FC = ({
+ optList,
+ handleInputChange,
+ handleRemoveOption,
+ type,
+ error,
+}) => {
+ const intl = useIntl();
+ let hasVal = false;
+ return optList.slice(0, MAX_CUSTOM_FIELDS).map((o: { val: string }, i: number) => {
+ const pollOptionKey = `poll-option-${i}`;
+ if (o.val && o.val.length > 0) hasVal = true;
+ return (
+
+
+ handleInputChange(e, i)}
+ maxLength={MAX_INPUT_CHARS}
+ onPaste={(e) => { e.stopPropagation(); }}
+ onCut={(e) => { e.stopPropagation(); }}
+ onCopy={(e) => { e.stopPropagation(); }}
+ />
+ {optList.length > MIN_OPTIONS_LENGTH && (
+ {
+ handleRemoveOption(i);
+ }}
+ />
+ )}
+
+ {intl.formatMessage(
+ intlMessages.deleteRespDesc,
+ { 0: o.val || intl.formatMessage(intlMessages.emptyPollOpt) },
+ )}
+
+
+ {!hasVal && type !== pollTypes.Response && error ? (
+ {error}
+ ) : (
+
+ )}
+
+ );
+ });
+};
+
+export default PollInputs;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollQuestionArea.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollQuestionArea.tsx
new file mode 100644
index 0000000000..654afc3f87
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/PollQuestionArea.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Meteor } from 'meteor/meteor';
+import DraggableTextArea from '/imports/ui/components/poll/dragAndDrop/component';
+import { pollTypes } from '../service';
+import Styled from '../styles';
+
+const POLL_SETTINGS = Meteor.settings.public.poll;
+const MAX_INPUT_CHARS = POLL_SETTINGS.maxTypedAnswerLength;
+
+const QUESTION_MAX_INPUT_CHARS = 1200;
+
+const intlMessages = defineMessages({
+ questionAndOptionsPlaceholder: {
+ id: 'app.poll.questionAndoptions.label',
+ description: 'poll input questions and options label',
+ },
+ questionLabel: {
+ id: 'app.poll.question.label',
+ description: '',
+ },
+ optionalQuestionLabel: {
+ id: 'app.poll.optionalQuestion.label',
+ description: '',
+ },
+});
+
+interface PollQuestionAreaProps {
+ customInput: boolean;
+ optList: Array<{ val: string }>;
+ warning: string | null;
+ type: string | null;
+ error: string | null;
+ questionAndOptions: string | string[];
+ handleTextareaChange: (e: React.ChangeEvent) => void;
+ setIsPasting: (isPasting: boolean) => void;
+ handlePollLetterOptions: () => void;
+ textareaRef: React.RefObject;
+ question: string | string[];
+}
+
+const PollQuestionArea: React.FC = ({
+ customInput,
+ optList,
+ warning,
+ error,
+ type,
+ questionAndOptions,
+ handleTextareaChange,
+ setIsPasting,
+ handlePollLetterOptions,
+ textareaRef,
+ question,
+}) => {
+ const intl = useIntl();
+ const hasOptionError = (customInput && optList.length === 0 && error);
+ const hasWarning = (customInput && warning);
+ const hasQuestionError = (type === pollTypes.Response
+ && questionAndOptions.length === 0 && error);
+ const questionsAndOptionsPlaceholder = intlMessages.questionAndOptionsPlaceholder;
+ const questionPlaceholder = (type === pollTypes.Response)
+ ? intlMessages.questionLabel
+ : intlMessages.optionalQuestionLabel;
+ return (
+
+ handleTextareaChange(e)}
+ onPaste={(e) => { e.stopPropagation(); setIsPasting(true); }}
+ onCut={(e) => { e.stopPropagation(); }}
+ onCopy={(e) => { e.stopPropagation(); }}
+ onKeyPress={(event) => {
+ if (event.key === 'Enter' && customInput) {
+ handlePollLetterOptions();
+ }
+ }}
+ rows="5"
+ cols="35"
+ maxLength={QUESTION_MAX_INPUT_CHARS}
+ aria-label={intl.formatMessage(customInput ? questionsAndOptionsPlaceholder
+ : questionPlaceholder)}
+ placeholder={intl.formatMessage(customInput ? questionsAndOptionsPlaceholder
+ : questionPlaceholder)}
+ {...{ MAX_INPUT_CHARS }}
+ as={customInput ? DraggableTextArea : 'textarea'}
+ ref={textareaRef}
+ />
+ {hasQuestionError || hasOptionError ? (
+ {error}
+ ) : (
+
+ )}
+ {hasWarning ? (
+ {warning}
+ ) : (
+
+ )}
+
+ );
+};
+
+export default PollQuestionArea;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseArea.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseArea.tsx
new file mode 100644
index 0000000000..4be4ef9cd7
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseArea.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Meteor } from 'meteor/meteor';
+import Checkbox from '/imports/ui/components/common/checkbox/component';
+import Toggle from '/imports/ui/components/common/switch/component';
+import Styled from '../styles';
+import { pollTypes, isDefaultPoll } from '../service';
+import StartPollButton from './StartPollButton';
+import PollInputs from './PollInputs';
+
+const POLL_SETTINGS = Meteor.settings.public.poll;
+const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
+
+const intlMessages = defineMessages({
+ enableMultipleResponseLabel: {
+ id: 'app.poll.enableMultipleResponseLabel',
+ description: 'label for checkbox to enable multiple choice',
+ },
+ addOptionLabel: {
+ id: 'app.poll.addItem.label',
+ description: '',
+ },
+ secretPollLabel: {
+ id: 'app.poll.secretPoll.label',
+ description: '',
+ },
+ isSecretPollLabel: {
+ id: 'app.poll.secretPoll.isSecretLabel',
+ 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',
+ },
+});
+
+interface ResponseAreaProps {
+ type: string | null;
+ toggleIsMultipleResponse: () => void;
+ isMultipleResponse: boolean;
+ optList: Array<{ val: string }>;
+ handleAddOption: () => void;
+ secretPoll: boolean;
+ question: string | string[];
+ setError: (err: string) => void;
+ setIsPolling: (isPolling: boolean) => void;
+ hasCurrentPresentation: boolean;
+ handleToggle: () => void;
+ error: string | null;
+ handleInputChange: (e: React.ChangeEvent, i: number) => void;
+ handleRemoveOption: (i: number) => void;
+}
+
+const ResponseArea: React.FC = ({
+ type,
+ toggleIsMultipleResponse,
+ isMultipleResponse,
+ optList,
+ handleAddOption,
+ secretPoll,
+ question,
+ setError,
+ setIsPolling,
+ hasCurrentPresentation,
+ handleToggle,
+ error,
+ handleInputChange,
+ handleRemoveOption,
+}) => {
+ const intl = useIntl();
+ const defaultPoll = isDefaultPoll(type as string);
+ if (defaultPoll || type === pollTypes.Response) {
+ return (
+
+ {defaultPoll && (
+
+
+
+
+
+ {intl.formatMessage(intlMessages.enableMultipleResponseLabel)}
+
+
+ )}
+ {defaultPoll && (
+
+ )}
+ {defaultPoll && (
+ = MAX_CUSTOM_FIELDS}
+ onClick={() => handleAddOption()}
+ />
+ )}
+
+
+
+ {intl.formatMessage(intlMessages.secretPollLabel)}
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+
+ {secretPoll
+ ? intl.formatMessage(intlMessages.on)
+ : intl.formatMessage(intlMessages.off)}
+
+ handleToggle()}
+ ariaLabel={intl.formatMessage(intlMessages.secretPollLabel)}
+ showToggleLabel={false}
+ data-test="anonymousPollBtn"
+ />
+
+
+
+ {secretPoll && (
+
+ {intl.formatMessage(intlMessages.isSecretPollLabel)}
+
+ )}
+
+
+ );
+ }
+ return null;
+};
+
+export default ResponseArea;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseChoices.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseChoices.tsx
new file mode 100644
index 0000000000..4ed2d38d6c
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseChoices.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import Styled from '../styles';
+import { pollTypes } from '../service';
+import ResponseArea from './ResponseArea';
+
+const intlMessages = defineMessages({
+ typedResponseDesc: {
+ id: 'app.poll.typedResponse.desc',
+ description: '',
+ },
+ responseChoices: {
+ id: 'app.poll.responseChoices.label',
+ description: '',
+ },
+ pollingQuestion: {
+ id: 'app.polling.pollQuestionTitle',
+ description: 'polling question header',
+ },
+});
+
+interface ResponseChoicesProps {
+ type: string | null;
+ toggleIsMultipleResponse: () => void;
+ isMultipleResponse: boolean;
+ optList: Array<{ val: string }>;
+ handleAddOption: () => void;
+ secretPoll: boolean;
+ question: string | string[];
+ setError: (err: string) => void;
+ setIsPolling: (isPolling: boolean) => void;
+ hasCurrentPresentation: boolean;
+ handleToggle: () => void;
+ error: string | null;
+ handleInputChange: (e: React.ChangeEvent, i: number) => void;
+ handleRemoveOption: (i: number) => void;
+ customInput: boolean;
+ questionAndOptions: string[] | string;
+}
+
+const ResponseChoices: React.FC = ({
+ type,
+ toggleIsMultipleResponse,
+ isMultipleResponse,
+ optList,
+ handleAddOption,
+ secretPoll,
+ question,
+ setError,
+ setIsPolling,
+ hasCurrentPresentation,
+ handleToggle,
+ error,
+ handleInputChange,
+ handleRemoveOption,
+ customInput,
+ questionAndOptions,
+}) => {
+ const intl = useIntl();
+ if ((!customInput && type) || (questionAndOptions && customInput)) {
+ return (
+
+ {customInput && questionAndOptions && (
+
+
+ {intl.formatMessage(intlMessages.pollingQuestion)}
+
+
+ {question}
+
+
+ )}
+
+ {intl.formatMessage(intlMessages.responseChoices)}
+
+ {type === pollTypes.Response && (
+
+ {intl.formatMessage(intlMessages.typedResponseDesc)}
+
+ )}
+
+
+ );
+ }
+ return null;
+};
+
+export default ResponseChoices;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseTypes.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseTypes.tsx
new file mode 100644
index 0000000000..bb756f46da
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/ResponseTypes.tsx
@@ -0,0 +1,158 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { pollTypes } from '../service';
+import Styled from '../styles';
+
+const intlMessages = defineMessages({
+ responseTypesLabel: {
+ id: 'app.poll.responseTypes.label',
+ description: '',
+ },
+ tf: {
+ id: 'app.poll.tf',
+ description: 'label for true / false poll',
+ },
+ true: {
+ id: 'app.poll.answer.true',
+ description: '',
+ },
+ false: {
+ id: 'app.poll.answer.false',
+ description: '',
+ },
+ a4: {
+ id: 'app.poll.a4',
+ description: 'label for A / B / C / D poll',
+ },
+ 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: '',
+ },
+ userResponse: {
+ id: 'app.poll.userResponse.label',
+ description: '',
+ },
+});
+
+interface ResponseTypesProps {
+ customInput: boolean;
+ setType: (type: string | null) => void;
+ type: string | null;
+ setOptList: (optList: Array<{ val: string }>) => void;
+}
+
+const ResponseTypes: React.FC = ({
+ customInput,
+ setType,
+ type,
+ setOptList,
+}) => {
+ const intl = useIntl();
+ if (!customInput) {
+ return (
+
+
+ {intl.formatMessage(intlMessages.responseTypesLabel)}
+
+
+ {
+ setType(pollTypes.TrueFalse);
+ setOptList([
+ { val: intl.formatMessage(intlMessages.true) },
+ { val: intl.formatMessage(intlMessages.false) },
+ ]);
+ }}
+ />
+ {
+ if (!customInput) {
+ setType(pollTypes.Letter);
+ setOptList([
+ { val: intl.formatMessage(intlMessages.a) },
+ { val: intl.formatMessage(intlMessages.b) },
+ { val: intl.formatMessage(intlMessages.c) },
+ { val: intl.formatMessage(intlMessages.d) },
+ ]);
+ }
+ }}
+ />
+ {
+ setType(pollTypes.YesNoAbstention);
+ setOptList([
+ { val: intl.formatMessage(intlMessages.yes) },
+ { val: intl.formatMessage(intlMessages.no) },
+ { val: intl.formatMessage(intlMessages.abstention) },
+ ]);
+ }}
+ />
+ { setType(pollTypes.Response); }}
+ />
+
+
+ );
+ }
+ return null;
+};
+
+export default ResponseTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/StartPollButton.tsx b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/StartPollButton.tsx
new file mode 100644
index 0000000000..83dd7c170a
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/components/StartPollButton.tsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import { defineMessages, useIntl } from 'react-intl';
+import { Meteor } from 'meteor/meteor';
+import { useMutation } from '@apollo/client';
+import Styled from '../styles';
+import { pollTypes, checkPollType } from '../service';
+import { POLL_CREATE } from '../mutation';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
+
+const POLL_SETTINGS = Meteor.settings.public.poll;
+const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
+
+const intlMessages = defineMessages({
+ startPollLabel: {
+ id: 'app.poll.start.label',
+ description: '',
+ },
+ questionErr: {
+ id: 'app.poll.questionErr',
+ description: 'question text area error label',
+ },
+ optionErr: {
+ id: 'app.poll.optionErr',
+ description: 'poll input error label',
+ },
+ yes: {
+ id: 'app.poll.y',
+ description: '',
+ },
+ no: {
+ id: 'app.poll.n',
+ description: '',
+ },
+ abstention: {
+ id: 'app.poll.abstention',
+ description: '',
+ },
+ true: {
+ id: 'app.poll.answer.true',
+ description: '',
+ },
+ false: {
+ id: 'app.poll.answer.false',
+ description: '',
+ },
+});
+
+interface StartPollButtonProps {
+ optList: Array<{val: string}>;
+ question: string | string[];
+ type: string | null;
+ setError: (err: string) => void;
+ setIsPolling: (isPolling: boolean) => void;
+ secretPoll: boolean;
+ isMultipleResponse: boolean;
+ hasCurrentPresentation: boolean;
+}
+
+const StartPollButton: React.FC = ({
+ optList,
+ question,
+ type,
+ setError,
+ setIsPolling,
+ secretPoll,
+ isMultipleResponse,
+ hasCurrentPresentation,
+}) => {
+ const intl = useIntl();
+
+ const [createPoll] = useMutation(POLL_CREATE);
+
+ const startPoll = (
+ pollType: string | null,
+ secretPoll: boolean,
+ question: string | string[],
+ isMultipleResponse: boolean,
+ answers: (string | null)[] = [],
+ ) => {
+ const pollId = hasCurrentPresentation || PUBLIC_CHAT_KEY;
+
+ createPoll({
+ variables: {
+ pollType,
+ pollId: `${pollId}/${new Date().getTime()}`,
+ secretPoll,
+ question,
+ isMultipleResponse,
+ answers,
+ },
+ });
+ };
+
+ return (
+ {
+ const optionsList = optList.slice(0, MAX_CUSTOM_FIELDS);
+ let hasVal = false;
+ optionsList.forEach((o) => {
+ if (o.val.trim().length > 0) hasVal = true;
+ });
+
+ let err = null;
+ if (type === pollTypes.Response && question.length === 0) {
+ err = intl.formatMessage(intlMessages.questionErr);
+ }
+ if (!hasVal && type !== pollTypes.Response) {
+ err = intl.formatMessage(intlMessages.optionErr);
+ }
+
+ if (err) {
+ setError(err);
+ } else {
+ setIsPolling(true);
+ const verifiedPollType = checkPollType(
+ type,
+ optionsList,
+ intl.formatMessage(intlMessages.yes),
+ intl.formatMessage(intlMessages.no),
+ intl.formatMessage(intlMessages.abstention),
+ intl.formatMessage(intlMessages.true),
+ intl.formatMessage(intlMessages.false),
+ );
+ const verifiedOptions = optionsList.map((o) => {
+ if (o.val.trim().length > 0) return o.val;
+ return null;
+ });
+ if (verifiedPollType === pollTypes.Custom) {
+ startPoll(
+ verifiedPollType,
+ secretPoll,
+ question,
+ isMultipleResponse,
+ verifiedOptions?.filter(Boolean),
+ );
+ } else {
+ startPoll(verifiedPollType, secretPoll, question, isMultipleResponse);
+ }
+ }
+ }}
+ />
+ );
+};
+
+export default StartPollButton;
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/mutation.ts b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/mutation.ts
new file mode 100644
index 0000000000..aaa05a0541
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/mutation.ts
@@ -0,0 +1,61 @@
+import { gql } from '@apollo/client';
+
+export const POLL_PUBLISH_RESULT = gql`
+ mutation PollPublishResult($pollId: String!) {
+ pollPublishResult(
+ pollId: $pollId,
+ )
+ }
+`;
+
+export const POLL_SUBMIT_TYPED_VOTE = gql`
+ mutation PollSubmitTypedVote($pollId: String!, $answer: String!) {
+ pollSubmitUserTypedVote(
+ pollId: $pollId,
+ answer: $answer,
+ )
+ }
+`;
+
+export const POLL_SUBMIT_VOTE = gql`
+ mutation PollSubmitVote($pollId: String!, $answerIds: [Int]!) {
+ pollSubmitUserVote(
+ pollId: $pollId,
+ answerIds: $answerIds,
+ )
+ }
+`;
+
+export const POLL_CANCEL = gql`
+ mutation PollCancel {
+ pollCancel
+ }
+`;
+
+export const POLL_CREATE = gql`
+ mutation PollCreate(
+ $pollType: String!,
+ $pollId: String!,
+ $secretPoll: Boolean!,
+ $question: String!,
+ $isMultipleResponse: Boolean!,
+ $answers: [String]!
+ ) {
+ pollCreate(
+ pollType: $pollType,
+ pollId: $pollId,
+ secretPoll: $secretPoll,
+ question: $question,
+ isMultipleResponse: $isMultipleResponse,
+ answers: $answers,
+ )
+ }
+`;
+
+export default {
+ POLL_PUBLISH_RESULT,
+ POLL_SUBMIT_TYPED_VOTE,
+ POLL_SUBMIT_VOTE,
+ POLL_CANCEL,
+ POLL_CREATE,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/queries.ts b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/queries.ts
new file mode 100644
index 0000000000..a6d05a8a27
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/queries.ts
@@ -0,0 +1,103 @@
+import { gql } from '@apollo/client';
+
+export interface GetHasCurrentPresentationResponse {
+ pres_page_aggregate: {
+ aggregate: {
+ count: number;
+ };
+ };
+}
+
+export interface UserInfo {
+ user: {
+ name: string;
+ userId: string;
+ };
+ optionDescIds: Array;
+}
+
+export interface ResponseInfo {
+ optionResponsesCount: number;
+ optionDesc: string;
+ pollResponsesCount: number;
+}
+
+export interface PollInfo {
+ published: boolean;
+ pollId: string;
+ secret: boolean;
+ questionText: string;
+ ended: boolean;
+ multipleResponses: boolean;
+ users: Array;
+ responses: Array;
+ users_aggregate: {
+ aggregate: {
+ count: number;
+ };
+ };
+ responses_aggregate: {
+ aggregate: {
+ count: number;
+ sum: {
+ optionResponsesCount: number;
+ }
+ };
+ };
+}
+
+export interface getCurrentPollDataResponse {
+ poll: Array;
+}
+
+export const getHasCurrentPresentation = gql`
+ subscription getHasCurrentPresentation {
+ pres_page_aggregate(where: {isCurrentPage: {_eq: true}}) {
+ aggregate {
+ count
+ }
+ }
+ }
+`;
+
+export const getCurrentPollData = gql`
+subscription getCurrentPollData {
+ poll(order_by: {createdAt: desc}, limit: 1,) {
+ pollId
+ published
+ secret
+ questionText
+ ended
+ multipleResponses
+ users(where: {responded: {_eq: true}}) {
+ user {
+ name
+ userId
+ }
+ optionDescIds
+ }
+ responses {
+ optionResponsesCount
+ optionDesc
+ pollResponsesCount
+ }
+ users_aggregate {
+ aggregate {
+ count
+ }
+ }
+ responses_aggregate {
+ aggregate {
+ count
+ sum {
+ optionResponsesCount
+ }
+ }
+ }
+ }
+ }
+`;
+
+export default {
+ getHasCurrentPresentation,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/service.ts
new file mode 100644
index 0000000000..f17bd32f15
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/service.ts
@@ -0,0 +1,118 @@
+export const pollTypes = {
+ YesNo: 'YN',
+ YesNoAbstention: 'YNA',
+ TrueFalse: 'TF',
+ Letter: 'A-',
+ A2: 'A-2',
+ A3: 'A-3',
+ A4: 'A-4',
+ A5: 'A-5',
+ Custom: 'CUSTOM',
+ Response: 'R-',
+};
+
+export const validateInput = (input: string) => {
+ let i = input;
+ while (/^\s/.test(i)) i = i.substring(1);
+ return i;
+};
+
+export const getSplittedQuestionAndOptions = (questionAndOptions: string[] | string) => {
+ const inputList = Array.isArray(questionAndOptions)
+ ? questionAndOptions
+ : questionAndOptions.split('\n').filter((val: string) => val !== '');
+ const splittedQuestion = inputList.length > 0 ? inputList[0] : questionAndOptions;
+ const optList = inputList.slice(1);
+
+ const optionsList = optList.map((val) => {
+ const option = validateInput(val);
+ return { val: option };
+ });
+
+ return {
+ splittedQuestion,
+ optionsList,
+ };
+};
+
+export const removeEmptyLineSpaces = (input: string) => {
+ const filteredInput = input.split('\n').filter((val) => val.trim() !== '');
+ return filteredInput;
+};
+
+export const isDefaultPoll = (pollType: string) => pollType !== pollTypes.Response;
+
+const matchYesNoPoll = (yesValue: string, noValue: string, contentString: string) => {
+ const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`;
+ const ynOptionsRegex = new RegExp(ynPollString, 'gi');
+ const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || [];
+ return ynPoll;
+};
+
+const matchYesNoAbstentionPoll = (yesValue:string, noValue:string, abstentionValue:string, contentString:string) => {
+ /* eslint max-len: [off] */
+ const ynaPollString = `(${yesValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${abstentionValue})|(${yesValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${yesValue})|(${noValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${abstentionValue})|(${noValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${yesValue})`;
+ const ynaOptionsRegex = new RegExp(ynaPollString, 'gi');
+ const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || [];
+ return ynaPoll;
+};
+
+const matchTrueFalsePoll = (trueValue:string, falseValue:string, contentString:string) => {
+ const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`;
+ const tgOptionsRegex = new RegExp(tfPollString, 'gi');
+ const tfPoll = contentString.match(tgOptionsRegex) || [];
+ return tfPoll;
+};
+
+export const checkPollType = (
+ type: string | null,
+ optList: { val: string }[],
+ yesValue: string,
+ noValue: string,
+ abstentionValue: string,
+ trueValue: string,
+ falseValue: string,
+) => {
+ /* eslint no-underscore-dangle: "off" */
+ let _type = type;
+ let pollString = '';
+ let defaultMatch: RegExpMatchArray | [] | null = null;
+ let isDefault = null;
+
+ switch (_type) {
+ case pollTypes.Letter:
+ pollString = optList.map((x) => x.val.toUpperCase()).sort().join('');
+ defaultMatch = pollString.match(/^(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi);
+ isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
+ _type = isDefault && Array.isArray(defaultMatch) ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom;
+ break;
+ case pollTypes.TrueFalse:
+ pollString = optList.map((x) => x.val).join('/');
+ defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString);
+ isDefault = defaultMatch.length > 0 && pollString.length === (defaultMatch[0]?.length);
+ if (!isDefault) _type = pollTypes.Custom;
+ break;
+ case pollTypes.YesNoAbstention:
+ pollString = optList.map((x) => x.val).join('/');
+ defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString);
+ isDefault = Array.isArray(defaultMatch) && defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
+ if (!isDefault) {
+ // also try to match only yes/no
+ defaultMatch = matchYesNoPoll(yesValue, noValue, pollString);
+ isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
+ _type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom;
+ }
+ break;
+ default:
+ break;
+ }
+ return _type;
+};
+
+export default {
+ pollTypes,
+ validateInput,
+ getSplittedQuestionAndOptions,
+ removeEmptyLineSpaces,
+ isDefaultPoll,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/styles.ts
new file mode 100644
index 0000000000..3d9be73c2e
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/poll-graphql/styles.ts
@@ -0,0 +1,634 @@
+import styled, { css, keyframes } from 'styled-components';
+import Button from '/imports/ui/components/common/button/component';
+import {
+ jumboPaddingY,
+ smPaddingX,
+ smPaddingY,
+ lgPaddingX,
+ borderRadius,
+ borderSize,
+ pollInputHeight,
+ pollSmMargin,
+ pollMdMargin,
+ mdPaddingX,
+ pollStatsElementWidth,
+ pollResultWidth,
+ borderSizeLarge,
+} from '/imports/ui/stylesheets/styled-components/general';
+import {
+ colorText,
+ colorBlueLight,
+ colorGray,
+ colorGrayLight,
+ colorGrayLighter,
+ colorGrayLightest,
+ colorDanger,
+ colorWarning,
+ colorHeading,
+ colorPrimary,
+ colorGrayDark,
+ colorWhite,
+ pollBlue,
+ pollStatsBorderColor,
+} from '/imports/ui/stylesheets/styled-components/palette';
+import { fontSizeBase, fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography';
+
+const ToggleLabel = styled.span`
+ margin-right: ${smPaddingX};
+
+ [dir="rtl"] & {
+ margin: 0 0 0 ${smPaddingX};
+ }
+`;
+
+const PollOptionInput = styled.input`
+ margin-right: 1rem;
+
+ [dir="rtl"] & {
+ margin-right: 0;
+ margin-left: 1rem;
+ }
+
+ &:focus {
+ outline: none;
+ border-radius: ${borderSize};
+ box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
+ }
+
+ width: 100%;
+ color: ${colorText};
+ -webkit-appearance: none;
+ padding: calc(${smPaddingY} * 2) ${smPaddingX};
+ border-radius: ${borderRadius};
+ font-size: ${fontSizeBase};
+ border: 1px solid ${colorGrayLighter};
+ box-shadow: 0 0 0 1px ${colorGrayLighter};
+`;
+// @ts-ignore - Button is a JS Component
+const DeletePollOptionButton = styled(Button)`
+ font-size: ${fontSizeBase};
+ flex: none;
+ width: 40px;
+ position: relative;
+ & > i {
+ font-size: 150%;
+ }
+`;
+
+const ErrorSpacer = styled.div`
+ position: relative;
+ height: 1.25rem;
+`;
+
+const InputError = styled(ErrorSpacer)`
+ color: ${colorDanger};
+ font-size: ${fontSizeSmall};
+`;
+
+const Instructions = styled.div`
+ margin-bottom: ${lgPaddingX};
+ color: ${colorText};
+`;
+
+type PollQuestionAreaProps = {
+ hasError: boolean;
+};
+
+const PollQuestionArea = styled.textarea`
+ resize: none;
+
+ &:focus {
+ outline: none;
+ border-radius: ${borderSize};
+ box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
+ }
+
+ width: 100%;
+ color: ${colorText};
+ -webkit-appearance: none;
+ padding: calc(${smPaddingY} * 2) ${smPaddingX};
+ border-radius: ${borderRadius};
+ font-size: ${fontSizeBase};
+ border: 1px solid ${colorGrayLighter};
+ box-shadow: 0 0 0 1px ${colorGrayLighter};
+
+ ${({ hasError }) => hasError && `
+ border-color: ${colorDanger};
+ box-shadow: 0 0 0 1px ${colorDanger};
+ `}
+`;
+
+const SectionHeading = styled.h4`
+ margin-top: 0;
+ font-weight: 600;
+ color: ${colorHeading};
+`;
+
+const ResponseType = styled.div`
+ display: flex;
+ justify-content: space-between;
+ flex-flow: wrap;
+ overflow-wrap: break-word;
+ position: relative;
+ width: 100%;
+ margin-bottom: ${lgPaddingX};
+
+ & > button {
+ position: relative;
+ width: 100%;
+ }
+`;
+
+// @ts-ignore - Button is a JS Component
+const PollConfigButton = styled(Button)`
+ border: solid ${colorGrayLight} 1px;
+ min-height: ${pollInputHeight};
+ font-size: ${fontSizeBase};
+ white-space: pre-wrap;
+ width: 100%;
+ margin-bottom: 1rem;
+
+ & > span {
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ ${({ selected }) => selected && `
+ background-color: ${colorGrayLightest};
+ font-size: ${fontSizeBase};
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: ${colorGrayLightest} !important;
+ box-shadow: none !important;
+ }
+ `}
+
+ ${({ small }) => small && `
+ width: 49% !important;
+ `}
+
+ ${({ full }) => full && `
+ width: 100%;
+ `}
+`;
+
+const PollParagraph = styled.div`
+ color: ${colorText};
+ margin-bottom: 0.9rem;
+`;
+
+const PollCheckbox = styled.div`
+ display: inline-block;
+ margin-right: ${pollSmMargin};
+ margin-bottom: ${pollMdMargin};
+`;
+
+// @ts-ignore - Button is a JS Component
+const AddItemButton = styled(Button)`
+ top: 1px;
+ position: relative;
+ display: block;
+ width: 100%;
+ text-align: left;
+ color: ${colorPrimary};
+ padding-left: 0;
+ padding-right: 0;
+ font-size: ${fontSizeBase};
+ white-space: pre-wrap;
+
+ &:hover {
+ & > span {
+ opacity: 1;
+ }
+ }
+`;
+
+const Row = styled.div`
+ display: flex;
+ flex-flow: wrap;
+ flex-grow: 1;
+ justify-content: space-between;
+ margin-top: 0.7rem;
+ margin-bottom: 0.7rem;
+`;
+
+const Warning = styled.div`
+ color: ${colorWarning};
+ font-size: ${fontSizeSmall};
+`;
+
+const CustomInputRow = styled.div`
+ display: flex;
+ flex-flow: nowrap;
+ flex-grow: 1;
+ justify-content: space-between;
+`;
+
+const Col = styled.div`
+ display: flex;
+ position: relative;
+ flex-flow: column;
+ flex-grow: 1;
+
+ &:last-child {
+ padding-right: 0;
+ padding-left: 1rem;
+
+ [dir="rtl"] & {
+ padding-right: 0.1rem;
+ padding-left: 0;
+ }
+ }
+`;
+
+const Toggle = styled.label`
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+`;
+
+// @ts-ignore - Button is a JS Component
+const StartPollBtn = styled(Button)`
+ position: relative;
+ width: 100%;
+ min-height: ${pollInputHeight};
+ margin-top: 1rem;
+ font-size: ${fontSizeBase};
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+
+ &:hover {
+ & > span {
+ opacity: 1;
+ }
+ }
+`;
+
+const NoSlidePanelContainer = styled.div`
+ color: ${colorGrayDark};
+ text-align: center;
+`;
+
+// @ts-ignore - Button is a JS Component
+const PollButton = styled(Button)`
+ margin-top: ${smPaddingY};
+ margin-bottom: ${smPaddingY};
+ // background-color: ${colorWhite};
+ box-shadow: 0 0 0 1px ${colorPrimary};
+ color: ${colorWhite};
+ background-color: ${colorPrimary}
+
+ & > span {
+ color: ${colorGray};
+ }
+
+ & > span:hover {
+ color: ${colorWhite};
+ opacity: 1;
+ }
+
+ &:active {
+ background-color: ${colorWhite};
+ box-shadow: 0 0 0 1px ${pollBlue};
+
+ & > span {
+ color: ${pollBlue};
+ }
+ }
+
+ &:focus {
+ background-color: ${colorWhite};
+ box-shadow: 0 0 0 1px ${pollBlue};
+
+ & > span {
+ color: ${pollBlue};
+ }
+ }
+
+ &:nth-child(even) {
+ margin-right: inherit;
+ margin-left: ${smPaddingY};
+
+ [dir="rtl"] & {
+ margin-right: ${smPaddingY};
+ margin-left: inherit;
+ }
+ }
+
+ &:nth-child(odd) {
+ margin-right: 1rem;
+ margin-left: inherit;
+
+ [dir="rtl"] & {
+ margin-right: inherit;
+ margin-left: ${smPaddingY};
+ }
+ }
+
+ &:hover {
+ box-shadow: 0 0 0 1px ${colorWhite};
+ background-color: ${colorWhite};
+ color: ${pollBlue};
+
+ & > span {
+ color: ${pollBlue};
+ opacity: 1;
+ }
+ }
+`;
+
+const DragAndDropPollContainer = styled.div`
+ width: 200px !important;
+ height: 200px !important;
+`;
+
+const Question = styled.div`
+ margin-bottom: ${lgPaddingX};
+`;
+
+const OptionWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
+const ResponseArea = styled.div`
+ display: flex;
+ flex-flow: column wrap;
+`;
+
+const CustomInputHeading = styled(SectionHeading)`
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ padding-bottom: ${jumboPaddingY};
+`;
+
+const CustomInputHeadingCol = styled(Col)`
+ overflow: hidden;
+`;
+
+const CustomInputToggleCol = styled(Col)`
+ flex-shrink: 0;
+`;
+
+const AnonymousHeading = styled(CustomInputHeading)``;
+
+const AnonymousHeadingCol = styled(CustomInputHeadingCol)``;
+
+const AnonymousToggleCol = styled(CustomInputToggleCol)``;
+
+const AnonymousRow = styled(Row)`
+ flex-flow: nowrap;
+ width: 100%;
+`;
+
+const ResultLeft = styled.td`
+ padding: 0 .5rem 0 0;
+ border-bottom: 1px solid ${colorGrayLightest};
+
+ [dir="rtl"] & {
+ padding: 0 0 0 .5rem;
+ }
+ padding-bottom: .25rem;
+ word-break: break-all;
+`;
+
+const ResultRight = styled.td`
+ padding-bottom: .25rem;
+ word-break: break-all;
+`;
+
+const Main = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const Left = styled.div`
+ font-weight: bold;
+ max-width: ${pollResultWidth};
+ min-width: ${pollStatsElementWidth};
+ word-wrap: break-word;
+ flex: 6;
+
+ padding: ${smPaddingY};
+ margin-top: ${pollSmMargin};
+ margin-bottom: ${pollSmMargin};
+ color: ${colorText};
+
+ position: relative;
+`;
+
+const Center = styled.div`
+ position: relative;
+ flex: 3;
+ border-left: 1px solid ${colorGrayLighter};
+ border-right : none;
+ width: 100%;
+ height: 100%;
+
+ [dir="rtl"] & {
+ border-left: none;
+ border-right: 1px solid ${colorGrayLighter};
+ }
+
+ padding: ${smPaddingY};
+ margin-top: ${pollSmMargin};
+ margin-bottom: ${pollSmMargin};
+ color: ${colorText};
+`;
+
+const Right = styled.div`
+ text-align: right;
+ max-width: ${pollStatsElementWidth};
+ min-width: ${pollStatsElementWidth};
+ flex: 1;
+
+ [dir="rtl"] & {
+ text-align: left;
+ }
+
+ padding: ${smPaddingY};
+ margin-top: ${pollSmMargin};
+ margin-bottom: ${pollSmMargin};
+ color: ${colorText};
+
+ position: relative;
+`;
+
+const BarShade = styled.div`
+ background-color: ${colorGrayLighter};
+ height: 100%;
+ min-height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+`;
+
+const BarVal = styled.div`
+ position: inherit;
+`;
+
+const Stats = styled.div`
+ margin-bottom: ${smPaddingX};
+ display: flex;
+ flex-direction: column;
+ border: 1px solid ${pollStatsBorderColor};
+ border-radius: ${borderSizeLarge};
+ padding: ${mdPaddingX};
+
+ & > div {
+ display: flex;
+ flex-direction: row;
+
+ & > div:nth-child(even) {
+ position: relative;
+ height: 75%;
+ width: 50%;
+ text-align: center;
+ }
+ }
+`;
+
+const Title = styled.span`
+ font-weight: bold;
+ word-break: break-all;
+ white-space: pre-wrap;
+`;
+
+const Status = styled.div`
+ margin-bottom: .5rem;
+`;
+
+const ellipsis = keyframes`
+ to {
+ width: 1.25em;
+ margin-right: 0;
+ margin-left: 0;
+ }
+`;
+
+interface ConnectingAnimationProps {
+ animations: boolean;
+}
+
+const ConnectingAnimation = styled.span`
+ &:after {
+ overflow: hidden;
+ display: inline-block;
+ vertical-align: bottom;
+ content: "\\2026"; /* ascii code for the ellipsis character */
+ width: 0;
+ margin: 0 1.25em 0 0;
+
+ [dir="rtl"] & {
+ margin: 0 0 0 1.25em;
+ }
+
+ ${({ animations }) => animations && css`
+ animation: ${ellipsis} steps(4, end) 900ms infinite;
+ `}
+ }
+`;
+
+const ButtonsActions = styled.div`
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+`;
+
+// @ts-ignore - Button is a JS Component
+const PublishButton = styled(Button)`
+ width: 48%;
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+`;
+
+const CancelButton = styled(PublishButton)``;
+
+// @ts-ignore - Button is a JS Component
+const LiveResultButton = styled(Button)`
+ width: 100%;
+ margin-top: ${smPaddingY};
+ margin-bottom: ${smPaddingY};
+ font-size: ${fontSizeBase};
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+`;
+
+const Separator = styled.div`
+ display: flex;
+ flex: 1 1 100%;
+ height: 1px;
+ min-height: 1px;
+ background-color: ${colorGrayLightest};
+ padding: 0;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+`;
+
+const THeading = styled.th`
+ text-align: left;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
+`;
+
+export default {
+ ToggleLabel,
+ PollOptionInput,
+ DeletePollOptionButton,
+ ErrorSpacer,
+ InputError,
+ Instructions,
+ PollQuestionArea,
+ SectionHeading,
+ ResponseType,
+ PollConfigButton,
+ PollParagraph,
+ PollCheckbox,
+ AddItemButton,
+ Row,
+ Col,
+ Toggle,
+ StartPollBtn,
+ NoSlidePanelContainer,
+ PollButton,
+ DragAndDropPollContainer,
+ Warning,
+ CustomInputRow,
+ Question,
+ OptionWrapper,
+ ResponseArea,
+ CustomInputHeading,
+ CustomInputHeadingCol,
+ CustomInputToggleCol,
+ AnonymousHeading,
+ AnonymousHeadingCol,
+ AnonymousToggleCol,
+ AnonymousRow,
+ ResultLeft,
+ ResultRight,
+ Main,
+ Left,
+ Center,
+ Right,
+ BarShade,
+ BarVal,
+ Stats,
+ Title,
+ Status,
+ ConnectingAnimation,
+ ButtonsActions,
+ PublishButton,
+ CancelButton,
+ LiveResultButton,
+ Separator,
+ THeading,
+};
diff --git a/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts b/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts
index 8139151903..5078a5fb42 100644
--- a/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts
+++ b/bigbluebutton-html5/imports/ui/core/hooks/useMeeting.ts
@@ -1,16 +1,15 @@
-import createUseSubscription from './createUseSubscription';
+import useCreateUseSubscription from './createUseSubscription';
import MEETING_SUBSCRIPTION from '../graphql/queries/meetingSubscription';
import { Meeting } from '../../Types/meeting';
-const useMeetingSubscription = createUseSubscription(MEETING_SUBSCRIPTION);
+const useMeetingSubscription = useCreateUseSubscription(MEETING_SUBSCRIPTION, {}, true);
export const useMeeting = (fn: (c: Partial) => Partial) => {
const response = useMeetingSubscription(fn);
- const returnObject = {
+ return {
...response,
data: response.data?.[0],
};
- return returnObject;
};
export default useMeeting;
diff --git a/bigbluebutton-tests/playwright/core/elements.js b/bigbluebutton-tests/playwright/core/elements.js
index 5fcffc8f8c..892fd5339a 100644
--- a/bigbluebutton-tests/playwright/core/elements.js
+++ b/bigbluebutton-tests/playwright/core/elements.js
@@ -264,9 +264,7 @@ const pollAnswerOptionDesc = 'div[data-test="optionsAnswers"]';
exports.firstPollAnswerDescOption = `${pollAnswerOptionDesc} input:nth-child(1)`;
exports.secondPollAnswerDescOption = `${pollAnswerOptionDesc}>>nth=1`;
exports.submitAnswersMultiple = 'button[data-test="submitAnswersMultiple"]';
-exports.numberVotes = 'div[data-test="numberOfVotes"]';
-exports.answer1 = 'div[data-test="numberOfVotes"]>>nth=0';
-exports.answer2 = 'div[data-test="numberOfVotes"]>>nth=1';
+exports.userVoteLiveResult = 'td[data-test="userVoteLiveResult"]';
exports.errorNoValueInput = 'div[data-test="errorNoValueInput"]';
exports.smartSlides1 = 'smartSlidesPresentation.pdf';
exports.responsePollQuestion = 'div[data-test="pollQuestion"]';
@@ -278,7 +276,7 @@ exports.closePollingBtn = 'button[data-test="closePolling"]';
exports.yesNoOption = 'button[data-test="yesNoQuickPoll"]';
exports.yesNoAbstentionOption = 'li[role="menuitem"]>>nth=1';
exports.pollAnswerOptionE = 'button[data-test="pollAnswerOption"]>>nth=4';
-exports.answerE = 'div[data-test="numberOfVotes"]>>nth=4';
+
// Presentation
exports.currentSlideImg = '[id="whiteboard-element"] [class="tl-image"]';
exports.uploadPresentationFileName = 'uploadTest.png';
diff --git a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.js b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.js
index d64a0c1c86..cc55e942af 100644
--- a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.js
+++ b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.js
@@ -60,7 +60,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
- await this.modPage.hasText(e.numberVotes, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'True');
await this.modPage.waitAndClick(e.cancelPollBtn);
//ABCD
@@ -69,7 +69,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.pollLetterAlternatives);
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
- await this.modPage.hasText(e.numberVotes, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'A');
await this.modPage.waitAndClick(e.cancelPollBtn);
//Yes/No/Abstention
@@ -78,7 +78,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.pollYesNoAbstentionBtn);
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
- await this.modPage.hasText(e.numberVotes, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'Yes');
await this.modPage.waitAndClick(e.cancelPollBtn);
//User Response
@@ -89,7 +89,7 @@ class LearningDashboard extends MultiUsers {
await this.userPage.waitForSelector(e.pollingContainer);
await this.userPage.type(e.pollAnswerOptionInput, e.answerMessage);
await this.userPage.waitAndClick(e.pollSubmitAnswer);
- await this.modPage.hasText(e.numberVotes, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, e.answerMessage);
await this.modPage.waitAndClick(e.cancelPollBtn);
//Checks
diff --git a/bigbluebutton-tests/playwright/polling/poll.js b/bigbluebutton-tests/playwright/polling/poll.js
index 5cada50c33..faaa07aaa4 100644
--- a/bigbluebutton-tests/playwright/polling/poll.js
+++ b/bigbluebutton-tests/playwright/polling/poll.js
@@ -17,7 +17,9 @@ class Polling extends MultiUsers {
async createPoll() {
await util.startPoll(this.modPage);
await this.modPage.hasElement(e.pollMenuButton);
-
+ await this.modPage.hasElement(e.publishPollingLabel);
+ await this.modPage.hasElement(e.cancelPollBtn);
+ await this.userPage.hasElement(e.pollingContainer);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
}
@@ -58,16 +60,13 @@ class Polling extends MultiUsers {
await this.userPage.type(e.pollAnswerOptionInput, e.answerMessage);
await this.userPage.waitAndClick(e.pollSubmitAnswer);
- await this.modPage.hasText(e.receivedAnswer, e.answerMessage);
+ await this.modPage.hasText(e.userVoteLiveResult, e.answerMessage);
await this.modPage.waitAndClick(e.publishPollingLabel);
- await this.modPage.waitForSelector(e.restartPoll);
+ await this.modPage.wasRemoved(e.pollingContainer);
await this.modPage.hasElement(e.wbDrawnRectangle, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.hasElement(e.wbDrawnRectangle);
-
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
}
async stopPoll() {
@@ -110,20 +109,20 @@ class Polling extends MultiUsers {
await this.checkLastOptionText();
await this.modPage.waitAndClick(e.closePollingBtn);
-
- await this.modPage.waitAndClick(e.actions);
- await this.modPage.waitAndClick(e.managePresentations);
- await this.modPage.waitAndClick(e.removePresentation);
- await this.modPage.waitAndClick(e.confirmManagePresentation);
}
async notAbleStartNewPollWithoutPresentation() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.actions);
await this.modPage.waitAndClick(e.managePresentations);
- await this.modPage.waitAndClick(e.removePresentation);
- await this.modPage.waitAndClick(e.confirmManagePresentation);
+ const allRemovePresentationBtn = await this.modPage.getLocator(e.removePresentation).all();
+ // reversing the order of clicking is needed to avoid failure as the tooltip shows in front of the below button
+ const reversedRemovePresentationButtons = allRemovePresentationBtn.reverse();
+ for (const removeBtn of reversedRemovePresentationButtons) {
+ await removeBtn.click({ timeout: ELEMENT_WAIT_TIME });
+ }
+ await this.modPage.waitAndClick(e.confirmManagePresentation);
await this.modPage.waitAndClick(e.actions);
await this.modPage.waitAndClick(e.polling);
await this.modPage.hasElement(e.noPresentation);
@@ -147,7 +146,7 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.currentPollQuestion, /Test/);
- await this.modPage.hasText(e.answer1, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, '1');
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
@@ -175,8 +174,8 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.secondPollAnswerOptionBtn);
await this.userPage.waitAndClickElement(e.submitAnswersMultiple);
- await this.modPage.hasText(e.answer1, '1');
- await this.modPage.hasText(e.answer2, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, '2');
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
@@ -193,11 +192,10 @@ class Polling extends MultiUsers {
await this.userPage.type(e.pollAnswerOptionInput, 'test');
await this.userPage.waitAndClick(e.pollSubmitAnswer);
await this.userPage.wasRemoved(e.pollingContainer, ELEMENT_WAIT_LONGER_TIME);
- await this.modPage.hasText(e.receivedAnswer, 'test');
+ await this.modPage.hasText(e.userVoteLiveResult, 'test');
await this.modPage.waitAndClick(e.publishPollingLabel);
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
+ await this.modPage.wasRemoved(e.pollingContainer);
// Multiple Choices
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
@@ -206,23 +204,21 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.firstPollAnswerDescOption);
await this.userPage.waitAndClick(e.secondPollAnswerDescOption);
await this.userPage.waitAndClick(e.submitAnswersMultiple);
- await this.modPage.hasText(e.answer1, '1');
- await this.modPage.hasText(e.answer2, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'A) 2222');
+ await this.modPage.hasText(e.userVoteLiveResult, 'B) 3333');
await this.modPage.waitAndClick(e.publishPollingLabel);
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
+ await this.modPage.wasRemoved(e.pollingContainer);
// One option answer
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
await skipSlide(this.modPage);
await this.modPage.waitAndClick(e.quickPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionE);
- await this.modPage.hasText(e.answerE, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'E) 22222');
await this.modPage.waitAndClick(e.publishPollingLabel);
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
+ await this.modPage.wasRemoved(e.pollingContainer);
// Yes/No/Abstention
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
@@ -230,24 +226,21 @@ class Polling extends MultiUsers {
await this.modPage.waitAndClick(e.yesNoOption);
await this.modPage.waitAndClick(e.yesNoAbstentionOption)
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
- await this.modPage.hasText(e.answer1, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'Yes');
await this.modPage.waitAndClick(e.publishPollingLabel);
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
+ await this.modPage.wasRemoved(e.pollingContainer);
// True/False
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
await skipSlide(this.modPage);
await this.modPage.waitAndClick(e.quickPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
- await this.modPage.hasText(e.answer1, '1');
+ await this.modPage.hasText(e.userVoteLiveResult, 'True');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.hasElementDisabled(e.nextSlide);
-
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
+ await this.modPage.wasRemoved(e.pollingContainer);
}
async pollResultsOnChat() {
@@ -273,24 +266,29 @@ class Polling extends MultiUsers {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await util.startPoll(this.modPage);
+ const wbDrawnRectangleLocator = await this.modPage.getLocator(e.wbDrawnRectangle);
+ const initialWbDrawnRectangleCount = await wbDrawnRectangleLocator.count();
+
await this.modPage.hasElementDisabled(e.publishPollingLabel);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasElement(e.publishPollingLabel);
await this.modPage.waitAndClick(e.publishPollingLabel);
+ await expect(wbDrawnRectangleLocator).toHaveCount(initialWbDrawnRectangleCount + 1);
- const wbDrawnRectangleLocator = await this.modPage.getLocator(e.wbDrawnRectangle).last();
- await expect(wbDrawnRectangleLocator).toBeVisible({ timeout: ELEMENT_WAIT_TIME});
+ const lastWbDrawnRectangleLocator = await wbDrawnRectangleLocator.last();
+ await expect(lastWbDrawnRectangleLocator).toBeVisible({ timeout: ELEMENT_WAIT_TIME});
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const wbBox = await modWbLocator.boundingBox();
- await wbDrawnRectangleLocator.dblclick();
+ // poll results should be editable by the presenter
+ await lastWbDrawnRectangleLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
- await wbDrawnRectangleLocator.dblclick();
+ await lastWbDrawnRectangleLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.modPage.page.keyboard.type('test');
- await expect(wbDrawnRectangleLocator).toContainText('test');
+ await expect(lastWbDrawnRectangleLocator).toContainText('test');
// user turns to presenter to edit the poll results
await this.modPage.waitAndClick(e.userListItem);
@@ -300,10 +298,10 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.resetZoomButton);
const wbDrawnRectangleUserLocator = await this.userPage.getLocator(e.wbDrawnRectangle).last();
- await wbDrawnRectangleUserLocator.dblclick();
+ await wbDrawnRectangleUserLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.userPage.page.keyboard.type('testUser');
await expect(wbDrawnRectangleUserLocator).toContainText('testUser');
-
+
await this.modPage.waitAndClick(e.currentUser);
await this.modPage.waitAndClick(e.takePresenter);
await this.userPage.waitAndClick(e.hidePublicChat);
@@ -311,17 +309,12 @@ class Polling extends MultiUsers {
async pollResultsInDifferentPresentation() {
await waitAndClearDefaultPresentationNotification(this.modPage);
-
-
- await uploadSinglePresentation(this.modPage, e.questionSlideFileName);
await util.startPoll(this.modPage);
+ await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
+ await uploadSinglePresentation(this.modPage, e.questionSlideFileName);
await this.modPage.waitAndClick(e.publishPollingLabel);
-
// Check poll results
await this.modPage.hasElement(e.wbDrawnRectangle);
-
- await this.modPage.waitAndClick(e.closePollingBtn);
- await this.modPage.wasRemoved(e.closePollingBtn);
}
async startNewPoll() {
diff --git a/bigbluebutton-tests/playwright/polling/polling.spec.js b/bigbluebutton-tests/playwright/polling/polling.spec.js
index aac9c79afc..30873465a6 100644
--- a/bigbluebutton-tests/playwright/polling/polling.spec.js
+++ b/bigbluebutton-tests/playwright/polling/polling.spec.js
@@ -24,7 +24,7 @@ test.describe('Polling', async () => {
await polling.quickPoll();
});
- test('Create poll with user response @ci @flaky', async () => {
+ test('Create poll with user response @ci', async () => {
await polling.pollUserResponse();
});
@@ -61,7 +61,8 @@ test.describe('Polling', async () => {
await polling.pollResultsOnWhiteboard();
});
- test('Poll results in a different presentation', async () => {
+ test('Poll results in a different presentation', async ({}, testInfo) => {
+ test.fixme(!testInfo.config.fullyParallel, 'Currently only works in parallel mode. Poll results not being displayed in the presentation');
await polling.pollResultsInDifferentPresentation();
});
});
diff --git a/bigbluebutton-tests/playwright/presentation/util.js b/bigbluebutton-tests/playwright/presentation/util.js
index 9152a85ab4..9bc16a5cb9 100644
--- a/bigbluebutton-tests/playwright/presentation/util.js
+++ b/bigbluebutton-tests/playwright/presentation/util.js
@@ -24,7 +24,7 @@ async function getCurrentPresentationHeight(locator) {
async function uploadSinglePresentation(test, fileName, uploadTimeout = UPLOAD_PDF_WAIT_TIME) {
const firstSlideSrc = await test.page.evaluate(selector => document.querySelector(selector)
- .style
+ ?.style
.backgroundImage
.split('"')[1],
[e.currentSlideImg]);