2018-10-24 22:17:13 +08:00
|
|
|
import React, { Component } from 'react';
|
2017-09-26 07:45:44 +08:00
|
|
|
import PropTypes from 'prop-types';
|
2016-06-30 06:00:06 +08:00
|
|
|
import Button from '/imports/ui/components/button/component';
|
2017-09-26 07:45:44 +08:00
|
|
|
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
2018-04-14 02:52:18 +08:00
|
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
2019-03-28 23:58:10 +08:00
|
|
|
import cx from 'classnames';
|
2021-02-08 17:57:39 +08:00
|
|
|
import { Meteor } from 'meteor/meteor';
|
2018-01-08 14:17:18 +08:00
|
|
|
import { styles } from './styles.scss';
|
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
|
|
|
import AudioService from '/imports/ui/components/audio/service';
|
2021-02-08 19:12:08 +08:00
|
|
|
import Checkbox from '/imports/ui/components/checkbox/component';
|
2016-05-06 02:50:18 +08:00
|
|
|
|
2021-05-01 22:59:20 +08:00
|
|
|
const MAX_INPUT_CHARS = Meteor.settings.public.poll.maxTypedAnswerLength;
|
2020-09-22 00:00:05 +08:00
|
|
|
|
2017-04-25 10:08:18 +08:00
|
|
|
const intlMessages = defineMessages({
|
|
|
|
pollingTitleLabel: {
|
|
|
|
id: 'app.polling.pollingTitle',
|
2018-04-14 03:05:15 +08:00
|
|
|
},
|
|
|
|
pollAnswerLabel: {
|
|
|
|
id: 'app.polling.pollAnswerLabel',
|
|
|
|
},
|
|
|
|
pollAnswerDesc: {
|
|
|
|
id: 'app.polling.pollAnswerDesc',
|
2017-04-25 10:08:18 +08:00
|
|
|
},
|
2021-05-28 02:16:11 +08:00
|
|
|
pollQuestionTitle: {
|
2021-02-19 23:34:20 +08:00
|
|
|
id: 'app.polling.pollQuestionTitle',
|
2020-09-21 20:07:36 +08:00
|
|
|
},
|
2021-05-26 19:34:48 +08:00
|
|
|
responseIsSecret: {
|
|
|
|
id: 'app.polling.responseSecret',
|
|
|
|
},
|
|
|
|
responseNotSecret: {
|
|
|
|
id: 'app.polling.responseNotSecret',
|
2021-04-29 23:10:18 +08:00
|
|
|
},
|
2020-09-21 20:07:36 +08:00
|
|
|
submitLabel: {
|
|
|
|
id: 'app.polling.submitLabel',
|
|
|
|
},
|
|
|
|
submitAriaLabel: {
|
|
|
|
id: 'app.polling.submitAriaLabel',
|
|
|
|
},
|
|
|
|
responsePlaceholder: {
|
|
|
|
id: 'app.polling.responsePlaceholder',
|
2021-02-05 01:55:18 +08:00
|
|
|
},
|
2017-04-25 10:08:18 +08:00
|
|
|
});
|
|
|
|
|
2020-09-22 00:00:05 +08:00
|
|
|
const validateInput = (i) => {
|
|
|
|
let _input = i;
|
|
|
|
if (/^\s/.test(_input)) _input = '';
|
|
|
|
return _input;
|
|
|
|
};
|
|
|
|
|
2018-10-24 22:17:13 +08:00
|
|
|
class Polling extends Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2018-10-17 06:06:50 +08:00
|
|
|
|
2021-02-05 01:55:18 +08:00
|
|
|
this.state = {
|
2020-09-21 20:07:36 +08:00
|
|
|
typedAns: '',
|
2021-02-08 17:57:39 +08:00
|
|
|
checkedAnswers: [],
|
|
|
|
};
|
2021-02-05 01:55:18 +08:00
|
|
|
|
2018-10-24 22:17:13 +08:00
|
|
|
this.play = this.play.bind(this);
|
2020-09-22 00:00:05 +08:00
|
|
|
this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this);
|
2021-02-05 01:55:18 +08:00
|
|
|
this.renderButtonAnswers = this.renderButtonAnswers.bind(this);
|
|
|
|
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
|
|
|
|
this.handleSubmit = this.handleSubmit.bind(this);
|
|
|
|
this.renderCheckboxAnswers = this.renderCheckboxAnswers.bind(this);
|
2021-03-12 01:45:48 +08:00
|
|
|
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
|
2018-10-24 22:17:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.play();
|
|
|
|
}
|
|
|
|
|
|
|
|
play() {
|
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
|
|
|
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
2020-12-01 00:09:35 +08:00
|
|
|
+ Meteor.settings.public.app.basename
|
|
|
|
+ Meteor.settings.public.app.instanceId}`
|
Correctly set audio input/output devices
When refusing ("thumbs down" button) echo test, user is able to select a different input device. This should work fine for chrome, firefox and safari (once user grants permission when asked by html5client).
For output devices, we depend on setSinkId function, which is enabled by default on current chrome release (2020) but not in Firefox (user needs to enable "setSinkId in about:config page). This implementation is listed as (?) in MDN.
In other words, output device selection should work out of the box for chrome, only.
When selecting an outputDevice, all alert sounds (hangup, screenshare , polling, etc) also goes to the same output device.
This solves #10592
2020-10-07 07:37:55 +08:00
|
|
|
+ '/resources/sounds/Poll.mp3');
|
2018-10-24 22:17:13 +08:00
|
|
|
}
|
|
|
|
|
2020-09-22 00:00:05 +08:00
|
|
|
handleUpdateResponseInput(e) {
|
|
|
|
this.responseInput.value = validateInput(e.target.value);
|
|
|
|
this.setState({ typedAns: this.responseInput.value });
|
|
|
|
}
|
|
|
|
|
2021-03-08 01:26:10 +08:00
|
|
|
handleSubmit(pollId) {
|
|
|
|
const { handleVote } = this.props;
|
|
|
|
const { checkedAnswers } = this.state;
|
|
|
|
handleVote(pollId, checkedAnswers);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCheckboxChange(pollId, answerId) {
|
|
|
|
const { checkedAnswers } = this.state;
|
|
|
|
if (checkedAnswers.includes(answerId)) {
|
|
|
|
checkedAnswers.splice(checkedAnswers.indexOf(answerId), 1);
|
|
|
|
} else {
|
|
|
|
checkedAnswers.push(answerId);
|
|
|
|
}
|
|
|
|
checkedAnswers.sort();
|
|
|
|
this.setState({ checkedAnswers });
|
|
|
|
}
|
|
|
|
|
2021-03-12 01:45:48 +08:00
|
|
|
handleMessageKeyDown(e) {
|
|
|
|
const {
|
|
|
|
poll,
|
|
|
|
handleTypedVote,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const {
|
|
|
|
typedAns,
|
|
|
|
} = this.state;
|
|
|
|
|
2021-03-17 01:10:10 +08:00
|
|
|
if (e.keyCode === 13 && typedAns.length > 0) {
|
2021-03-12 01:45:48 +08:00
|
|
|
handleTypedVote(poll.pollId, typedAns);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 19:12:08 +08:00
|
|
|
renderButtonAnswers(pollAnswerStyles) {
|
2021-02-05 01:55:18 +08:00
|
|
|
const {
|
|
|
|
isMeteorConnected,
|
|
|
|
intl,
|
|
|
|
poll,
|
|
|
|
handleVote,
|
2020-09-21 20:07:36 +08:00
|
|
|
handleTypedVote,
|
2021-02-05 01:55:18 +08:00
|
|
|
pollAnswerIds,
|
2021-06-12 00:55:53 +08:00
|
|
|
pollTypes,
|
|
|
|
isDefaultPoll,
|
2021-02-05 01:55:18 +08:00
|
|
|
} = this.props;
|
|
|
|
|
2021-05-02 04:50:24 +08:00
|
|
|
const {
|
|
|
|
typedAns,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
if (!poll) return null;
|
2021-02-08 19:12:08 +08:00
|
|
|
|
2021-09-22 00:04:15 +08:00
|
|
|
const { answers, question, pollType } = poll;
|
2021-06-12 00:55:53 +08:00
|
|
|
const defaultPoll = isDefaultPoll(pollType);
|
2021-03-07 22:12:49 +08:00
|
|
|
|
|
|
|
return (
|
2021-09-21 20:19:39 +08:00
|
|
|
<div>
|
2021-04-30 00:54:39 +08:00
|
|
|
{
|
2021-05-27 01:52:55 +08:00
|
|
|
poll.pollType !== pollTypes.Response && (
|
2021-04-30 00:54:39 +08:00
|
|
|
<span>
|
|
|
|
{
|
2021-04-29 23:10:18 +08:00
|
|
|
question.length === 0 && (
|
2021-04-30 00:54:39 +08:00
|
|
|
<div className={styles.pollingTitle}>
|
2021-05-26 19:34:48 +08:00
|
|
|
{intl.formatMessage(intlMessages.pollingTitleLabel)}
|
2021-04-30 00:54:39 +08:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
<div className={cx(pollAnswerStyles)}>
|
2021-09-21 20:19:39 +08:00
|
|
|
{answers.map((pollAnswer) => {
|
2021-04-30 00:54:39 +08:00
|
|
|
const formattedMessageIndex = pollAnswer.key.toLowerCase();
|
|
|
|
let label = pollAnswer.key;
|
2021-06-12 00:55:53 +08:00
|
|
|
if (defaultPoll && pollAnswerIds[formattedMessageIndex]) {
|
2021-04-30 00:54:39 +08:00
|
|
|
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2021-05-02 04:50:24 +08:00
|
|
|
<div
|
2021-04-30 00:54:39 +08:00
|
|
|
key={pollAnswer.id}
|
|
|
|
className={styles.pollButtonWrapper}
|
2021-05-02 04:50:24 +08:00
|
|
|
>
|
2021-04-30 00:54:39 +08:00
|
|
|
<Button
|
|
|
|
disabled={!isMeteorConnected}
|
|
|
|
className={styles.pollingButton}
|
|
|
|
color="primary"
|
|
|
|
size="md"
|
|
|
|
label={label}
|
|
|
|
key={pollAnswer.key}
|
2021-09-21 20:19:39 +08:00
|
|
|
onClick={() => handleVote(poll.pollId, [pollAnswer.id])}
|
2021-04-30 00:54:39 +08:00
|
|
|
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
|
|
|
|
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
|
|
|
|
data-test="pollAnswerOption"
|
|
|
|
/>
|
|
|
|
<div
|
|
|
|
className={styles.hidden}
|
|
|
|
id={`pollAnswerLabel${pollAnswer.key}`}
|
|
|
|
>
|
|
|
|
{intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })}
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className={styles.hidden}
|
|
|
|
id={`pollAnswerDesc${pollAnswer.key}`}
|
|
|
|
>
|
|
|
|
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
|
|
|
|
</div>
|
2021-05-02 04:50:24 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
2021-05-01 22:59:20 +08:00
|
|
|
</div>
|
2021-05-02 04:50:24 +08:00
|
|
|
</span>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
{
|
2021-09-17 20:12:43 +08:00
|
|
|
poll.pollType === pollTypes.Response
|
2021-05-02 04:50:24 +08:00
|
|
|
&& (
|
|
|
|
<div className={styles.typedResponseWrapper}>
|
|
|
|
<input
|
|
|
|
data-test="pollAnswerOption"
|
|
|
|
onChange={(e) => {
|
|
|
|
this.handleUpdateResponseInput(e);
|
|
|
|
}}
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
this.handleMessageKeyDown(e);
|
|
|
|
}}
|
|
|
|
type="text"
|
|
|
|
className={styles.typedResponseInput}
|
|
|
|
placeholder={intl.formatMessage(intlMessages.responsePlaceholder)}
|
|
|
|
maxLength={MAX_INPUT_CHARS}
|
|
|
|
ref={(r) => { this.responseInput = r; }}
|
|
|
|
/>
|
|
|
|
<Button
|
|
|
|
data-test="submitAnswer"
|
|
|
|
className={styles.submitVoteBtn}
|
|
|
|
disabled={typedAns.length === 0}
|
|
|
|
color="primary"
|
|
|
|
size="sm"
|
|
|
|
label={intl.formatMessage(intlMessages.submitLabel)}
|
|
|
|
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
|
|
|
onClick={() => {
|
|
|
|
handleTypedVote(poll.pollId, typedAns);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
2021-09-17 20:12:43 +08:00
|
|
|
<div className={styles.pollingSecret}>
|
|
|
|
{intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)}
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-03-07 22:12:49 +08:00
|
|
|
);
|
2021-02-05 01:55:18 +08:00
|
|
|
}
|
|
|
|
|
2021-09-22 00:04:15 +08:00
|
|
|
renderCheckboxAnswers() {
|
2021-02-05 01:55:18 +08:00
|
|
|
const {
|
|
|
|
isMeteorConnected,
|
|
|
|
intl,
|
|
|
|
poll,
|
|
|
|
pollAnswerIds,
|
|
|
|
} = this.props;
|
2021-02-08 21:23:43 +08:00
|
|
|
const { checkedAnswers } = this.state;
|
2021-03-07 22:12:49 +08:00
|
|
|
const { question } = poll;
|
2021-02-05 01:55:18 +08:00
|
|
|
return (
|
|
|
|
<div>
|
2021-03-07 22:12:49 +08:00
|
|
|
{question.length === 0
|
|
|
|
&& (
|
|
|
|
<div className={styles.pollingTitle}>
|
|
|
|
{intl.formatMessage(intlMessages.pollingTitleLabel)}
|
|
|
|
</div>
|
2021-05-02 04:50:24 +08:00
|
|
|
)}
|
2021-03-08 01:26:10 +08:00
|
|
|
<table className={styles.multipleResponseAnswersTable}>
|
2021-03-07 22:12:49 +08:00
|
|
|
{poll.answers.map((pollAnswer) => {
|
|
|
|
const formattedMessageIndex = pollAnswer.key.toLowerCase();
|
|
|
|
let label = pollAnswer.key;
|
|
|
|
if (pollAnswerIds[formattedMessageIndex]) {
|
|
|
|
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
|
|
|
|
}
|
2021-02-08 19:12:08 +08:00
|
|
|
|
2021-03-07 22:12:49 +08:00
|
|
|
return (
|
2021-03-08 01:26:10 +08:00
|
|
|
<tr
|
2021-03-07 22:12:49 +08:00
|
|
|
key={pollAnswer.id}
|
|
|
|
className={styles.checkboxContainer}
|
|
|
|
>
|
2021-03-08 01:26:10 +08:00
|
|
|
<td>
|
|
|
|
<Checkbox
|
|
|
|
disabled={!isMeteorConnected}
|
|
|
|
id={`answerInput${pollAnswer.key}`}
|
|
|
|
onChange={() => this.handleCheckboxChange(poll.pollId, pollAnswer.id)}
|
|
|
|
checked={checkedAnswers.includes(pollAnswer.id)}
|
|
|
|
className={styles.checkbox}
|
|
|
|
ariaLabelledBy={`pollAnswerLabel${pollAnswer.key}`}
|
|
|
|
ariaDescribedBy={`pollAnswerDesc${pollAnswer.key}`}
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
<td className={styles.multipleResponseAnswersTableAnswerText}>
|
|
|
|
<label id={`pollAnswerLabel${pollAnswer.key}`}>
|
|
|
|
{label}
|
|
|
|
</label>
|
|
|
|
<div
|
|
|
|
className={styles.hidden}
|
|
|
|
id={`pollAnswerDesc${pollAnswer.key}`}
|
|
|
|
>
|
|
|
|
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
</tr>
|
2021-03-07 22:12:49 +08:00
|
|
|
);
|
|
|
|
})}
|
2021-03-08 01:26:10 +08:00
|
|
|
</table>
|
2021-02-05 01:55:18 +08:00
|
|
|
<div>
|
|
|
|
<Button
|
2021-03-07 22:12:49 +08:00
|
|
|
className={styles.submitVoteBtn}
|
2021-02-08 21:23:58 +08:00
|
|
|
disabled={!isMeteorConnected || checkedAnswers.length === 0}
|
2021-02-08 17:57:39 +08:00
|
|
|
color="primary"
|
2021-03-07 22:12:49 +08:00
|
|
|
size="sm"
|
|
|
|
label={intl.formatMessage(intlMessages.submitLabel)}
|
|
|
|
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
|
2021-02-08 17:57:39 +08:00
|
|
|
onClick={() => this.handleSubmit(poll.pollId)}
|
2021-02-05 01:55:18 +08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-02-08 17:57:39 +08:00
|
|
|
);
|
2021-02-05 01:55:18 +08:00
|
|
|
}
|
|
|
|
|
2018-10-24 22:17:13 +08:00
|
|
|
render() {
|
2019-05-23 02:00:44 +08:00
|
|
|
const {
|
2019-06-28 00:46:14 +08:00
|
|
|
intl,
|
|
|
|
poll,
|
2019-05-23 02:00:44 +08:00
|
|
|
} = this.props;
|
2020-09-29 03:28:35 +08:00
|
|
|
|
|
|
|
if (!poll) return null;
|
|
|
|
|
2020-09-21 20:07:36 +08:00
|
|
|
const { stackOptions, answers, question } = poll;
|
2019-03-28 23:58:10 +08:00
|
|
|
const pollAnswerStyles = {
|
|
|
|
[styles.pollingAnswers]: true,
|
|
|
|
[styles.removeColumns]: answers.length === 1,
|
|
|
|
[styles.stacked]: stackOptions,
|
|
|
|
};
|
2018-10-24 22:17:13 +08:00
|
|
|
|
|
|
|
return (
|
2018-10-26 00:07:24 +08:00
|
|
|
<div className={styles.overlay}>
|
2019-03-28 23:58:10 +08:00
|
|
|
<div
|
2021-03-23 16:38:45 +08:00
|
|
|
data-test="pollingContainer"
|
2019-03-28 23:58:10 +08:00
|
|
|
className={cx({
|
|
|
|
[styles.pollingContainer]: true,
|
|
|
|
[styles.autoWidth]: stackOptions,
|
|
|
|
})}
|
|
|
|
role="alert"
|
|
|
|
>
|
2021-05-02 04:50:24 +08:00
|
|
|
{
|
|
|
|
question.length > 0 && (
|
|
|
|
<span className={styles.qHeader}>
|
|
|
|
<div className={styles.qTitle}>
|
2021-09-21 20:19:39 +08:00
|
|
|
{intl.formatMessage(intlMessages.pollQuestionTitle)}
|
2021-05-02 04:50:24 +08:00
|
|
|
</div>
|
|
|
|
<div data-test="pollQuestion" className={styles.qText}>{question}</div>
|
|
|
|
</span>
|
|
|
|
)
|
2020-09-21 20:07:36 +08:00
|
|
|
}
|
2021-02-08 19:12:08 +08:00
|
|
|
{poll.isMultipleResponse ? this.renderCheckboxAnswers(pollAnswerStyles) : this.renderButtonAnswers(pollAnswerStyles)}
|
2018-10-24 22:17:13 +08:00
|
|
|
</div>
|
2021-05-01 22:59:20 +08:00
|
|
|
</div>
|
|
|
|
);
|
2018-10-24 22:17:13 +08:00
|
|
|
}
|
|
|
|
}
|
2017-04-25 10:08:18 +08:00
|
|
|
|
2018-04-14 02:52:18 +08:00
|
|
|
export default injectIntl(injectWbResizeEvent(Polling));
|
2017-09-26 07:45:44 +08:00
|
|
|
|
2018-04-14 02:52:18 +08:00
|
|
|
Polling.propTypes = {
|
2017-09-26 07:45:44 +08:00
|
|
|
intl: PropTypes.shape({
|
|
|
|
formatMessage: PropTypes.func.isRequired,
|
|
|
|
}).isRequired,
|
|
|
|
handleVote: PropTypes.func.isRequired,
|
2021-03-12 01:45:48 +08:00
|
|
|
handleTypedVote: PropTypes.func.isRequired,
|
2017-09-26 07:45:44 +08:00
|
|
|
poll: PropTypes.shape({
|
|
|
|
pollId: PropTypes.string.isRequired,
|
2018-03-13 00:29:22 +08:00
|
|
|
answers: PropTypes.arrayOf(PropTypes.shape({
|
|
|
|
id: PropTypes.number.isRequired,
|
|
|
|
key: PropTypes.string.isRequired,
|
|
|
|
}).isRequired).isRequired,
|
2017-09-26 07:45:44 +08:00
|
|
|
}).isRequired,
|
|
|
|
};
|