123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- import React from 'react';
- import PropTypes from 'prop-types';
- import ImmutablePropTypes from 'react-immutable-proptypes';
- import ImmutablePureComponent from 'react-immutable-pure-component';
- import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
- import classNames from 'classnames';
- import Motion from 'mastodon/features/ui/util/optional_motion';
- import spring from 'react-motion/lib/spring';
- import escapeTextContentForBrowser from 'escape-html';
- import emojify from 'mastodon/features/emoji/emoji';
- import RelativeTimestamp from './relative_timestamp';
- import Icon from 'mastodon/components/icon';
- const messages = defineMessages({
- closed: {
- id: 'poll.closed',
- defaultMessage: 'Closed',
- },
- voted: {
- id: 'poll.voted',
- defaultMessage: 'You voted for this answer',
- },
- votes: {
- id: 'poll.votes',
- defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
- },
- });
- const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
- obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
- return obj;
- }, {});
- export default @injectIntl
- class Poll extends ImmutablePureComponent {
- static contextTypes = {
- identity: PropTypes.object,
- };
- static propTypes = {
- poll: ImmutablePropTypes.map,
- intl: PropTypes.object.isRequired,
- disabled: PropTypes.bool,
- refresh: PropTypes.func,
- onVote: PropTypes.func,
- };
- state = {
- selected: {},
- expired: null,
- };
- static getDerivedStateFromProps (props, state) {
- const { poll, intl } = props;
- const expires_at = poll.get('expires_at');
- const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
- return (expired === state.expired) ? null : { expired };
- }
- componentDidMount () {
- this._setupTimer();
- }
- componentDidUpdate () {
- this._setupTimer();
- }
- componentWillUnmount () {
- clearTimeout(this._timer);
- }
- _setupTimer () {
- const { poll, intl } = this.props;
- clearTimeout(this._timer);
- if (!this.state.expired) {
- const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
- this._timer = setTimeout(() => {
- this.setState({ expired: true });
- }, delay);
- }
- }
- _toggleOption = value => {
- if (this.props.poll.get('multiple')) {
- const tmp = { ...this.state.selected };
- if (tmp[value]) {
- delete tmp[value];
- } else {
- tmp[value] = true;
- }
- this.setState({ selected: tmp });
- } else {
- const tmp = {};
- tmp[value] = true;
- this.setState({ selected: tmp });
- }
- }
- handleOptionChange = ({ target: { value } }) => {
- this._toggleOption(value);
- };
- handleOptionKeyPress = (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- this._toggleOption(e.target.getAttribute('data-index'));
- e.stopPropagation();
- e.preventDefault();
- }
- }
- handleVote = () => {
- if (this.props.disabled) {
- return;
- }
- this.props.onVote(Object.keys(this.state.selected));
- };
- handleRefresh = () => {
- if (this.props.disabled) {
- return;
- }
- this.props.refresh();
- };
- renderOption (option, optionIndex, showResults) {
- const { poll, disabled, intl } = this.props;
- const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
- const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
- const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
- const active = !!this.state.selected[`${optionIndex}`];
- const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
- let titleEmojified = option.get('title_emojified');
- if (!titleEmojified) {
- const emojiMap = makeEmojiMap(poll);
- titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
- }
- return (
- <li key={option.get('title')}>
- <label className={classNames('poll__option', { selectable: !showResults })}>
- <input
- name='vote-options'
- type={poll.get('multiple') ? 'checkbox' : 'radio'}
- value={optionIndex}
- checked={active}
- onChange={this.handleOptionChange}
- disabled={disabled}
- />
- {!showResults && (
- <span
- className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
- tabIndex='0'
- role={poll.get('multiple') ? 'checkbox' : 'radio'}
- onKeyPress={this.handleOptionKeyPress}
- aria-checked={active}
- aria-label={option.get('title')}
- data-index={optionIndex}
- />
- )}
- {showResults && (
- <span
- className='poll__number'
- title={intl.formatMessage(messages.votes, {
- votes: option.get('votes_count'),
- })}
- >
- {Math.round(percent)}%
- </span>
- )}
- <span
- className='poll__option__text translate'
- dangerouslySetInnerHTML={{ __html: titleEmojified }}
- />
- {!!voted && <span className='poll__voted'>
- <Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
- </span>}
- </label>
- {showResults && (
- <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
- {({ width }) =>
- <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
- }
- </Motion>
- )}
- </li>
- );
- }
- render () {
- const { poll, intl } = this.props;
- const { expired } = this.state;
- if (!poll) {
- return null;
- }
- const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
- const showResults = poll.get('voted') || expired;
- const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
- let votesCount = null;
- if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
- votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
- } else {
- votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
- }
- return (
- <div className='poll'>
- <ul>
- {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
- </ul>
- <div className='poll__footer'>
- {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
- {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
- {votesCount}
- {poll.get('expires_at') && <span> · {timeRemaining}</span>}
- </div>
- </div>
- );
- }
- }
|