The code powering m.abunchtell.com https://m.abunchtell.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

141 lines
4.9 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import ImmutablePureComponent from 'react-immutable-pure-component';
  5. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  6. import classNames from 'classnames';
  7. import { vote, fetchPoll } from 'mastodon/actions/polls';
  8. import Motion from 'mastodon/features/ui/util/optional_motion';
  9. import spring from 'react-motion/lib/spring';
  10. import escapeTextContentForBrowser from 'escape-html';
  11. import emojify from 'mastodon/features/emoji/emoji';
  12. import RelativeTimestamp from './relative_timestamp';
  13. const messages = defineMessages({
  14. closed: { id: 'poll.closed', defaultMessage: 'Closed' },
  15. });
  16. const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
  17. obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
  18. return obj;
  19. }, {});
  20. export default @injectIntl
  21. class Poll extends ImmutablePureComponent {
  22. static propTypes = {
  23. poll: ImmutablePropTypes.map,
  24. intl: PropTypes.object.isRequired,
  25. dispatch: PropTypes.func,
  26. disabled: PropTypes.bool,
  27. };
  28. state = {
  29. selected: {},
  30. };
  31. handleOptionChange = e => {
  32. const { target: { value } } = e;
  33. if (this.props.poll.get('multiple')) {
  34. const tmp = { ...this.state.selected };
  35. if (tmp[value]) {
  36. delete tmp[value];
  37. } else {
  38. tmp[value] = true;
  39. }
  40. this.setState({ selected: tmp });
  41. } else {
  42. const tmp = {};
  43. tmp[value] = true;
  44. this.setState({ selected: tmp });
  45. }
  46. };
  47. handleVote = () => {
  48. if (this.props.disabled) {
  49. return;
  50. }
  51. this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
  52. };
  53. handleRefresh = () => {
  54. if (this.props.disabled) {
  55. return;
  56. }
  57. this.props.dispatch(fetchPoll(this.props.poll.get('id')));
  58. };
  59. renderOption (option, optionIndex) {
  60. const { poll, disabled } = this.props;
  61. const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
  62. const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
  63. const active = !!this.state.selected[`${optionIndex}`];
  64. const showResults = poll.get('voted') || poll.get('expired');
  65. let titleEmojified = option.get('title_emojified');
  66. if (!titleEmojified) {
  67. const emojiMap = makeEmojiMap(poll);
  68. titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
  69. }
  70. return (
  71. <li key={option.get('title')}>
  72. {showResults && (
  73. <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
  74. {({ width }) =>
  75. <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
  76. }
  77. </Motion>
  78. )}
  79. <label className={classNames('poll__text', { selectable: !showResults })}>
  80. <input
  81. name='vote-options'
  82. type={poll.get('multiple') ? 'checkbox' : 'radio'}
  83. value={optionIndex}
  84. checked={active}
  85. onChange={this.handleOptionChange}
  86. disabled={disabled}
  87. />
  88. {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
  89. {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
  90. <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
  91. </label>
  92. </li>
  93. );
  94. }
  95. render () {
  96. const { poll, intl } = this.props;
  97. if (!poll) {
  98. return null;
  99. }
  100. const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
  101. const showResults = poll.get('voted') || poll.get('expired');
  102. const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
  103. return (
  104. <div className='poll'>
  105. <ul>
  106. {poll.get('options').map((option, i) => this.renderOption(option, i))}
  107. </ul>
  108. <div className='poll__footer'>
  109. {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
  110. {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
  111. <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
  112. {poll.get('expires_at') && <span> · {timeRemaining}</span>}
  113. </div>
  114. </div>
  115. );
  116. }
  117. }