The code powering m.abunchtell.com https://m.abunchtell.com
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

177 řádky
6.0 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. import Icon from 'mastodon/components/icon';
  14. const messages = defineMessages({
  15. closed: { id: 'poll.closed', defaultMessage: 'Closed' },
  16. voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
  17. });
  18. const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
  19. obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
  20. return obj;
  21. }, {});
  22. export default @injectIntl
  23. class Poll extends ImmutablePureComponent {
  24. static propTypes = {
  25. poll: ImmutablePropTypes.map,
  26. intl: PropTypes.object.isRequired,
  27. dispatch: PropTypes.func,
  28. disabled: PropTypes.bool,
  29. };
  30. state = {
  31. selected: {},
  32. expired: null,
  33. };
  34. static getDerivedStateFromProps (props, state) {
  35. const { poll, intl } = props;
  36. const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now();
  37. return (expired === state.expired) ? null : { expired };
  38. }
  39. componentDidMount () {
  40. this._setupTimer();
  41. }
  42. componentDidUpdate () {
  43. this._setupTimer();
  44. }
  45. componentWillUnmount () {
  46. clearTimeout(this._timer);
  47. }
  48. _setupTimer () {
  49. const { poll, intl } = this.props;
  50. clearTimeout(this._timer);
  51. if (!this.state.expired) {
  52. const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
  53. this._timer = setTimeout(() => {
  54. this.setState({ expired: true });
  55. }, delay);
  56. }
  57. }
  58. handleOptionChange = e => {
  59. const { target: { value } } = e;
  60. if (this.props.poll.get('multiple')) {
  61. const tmp = { ...this.state.selected };
  62. if (tmp[value]) {
  63. delete tmp[value];
  64. } else {
  65. tmp[value] = true;
  66. }
  67. this.setState({ selected: tmp });
  68. } else {
  69. const tmp = {};
  70. tmp[value] = true;
  71. this.setState({ selected: tmp });
  72. }
  73. };
  74. handleVote = () => {
  75. if (this.props.disabled) {
  76. return;
  77. }
  78. this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
  79. };
  80. handleRefresh = () => {
  81. if (this.props.disabled) {
  82. return;
  83. }
  84. this.props.dispatch(fetchPoll(this.props.poll.get('id')));
  85. };
  86. renderOption (option, optionIndex, showResults) {
  87. const { poll, disabled, intl } = this.props;
  88. const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
  89. const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
  90. const active = !!this.state.selected[`${optionIndex}`];
  91. const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
  92. let titleEmojified = option.get('title_emojified');
  93. if (!titleEmojified) {
  94. const emojiMap = makeEmojiMap(poll);
  95. titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
  96. }
  97. return (
  98. <li key={option.get('title')}>
  99. {showResults && (
  100. <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
  101. {({ width }) =>
  102. <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
  103. }
  104. </Motion>
  105. )}
  106. <label className={classNames('poll__text', { selectable: !showResults })}>
  107. <input
  108. name='vote-options'
  109. type={poll.get('multiple') ? 'checkbox' : 'radio'}
  110. value={optionIndex}
  111. checked={active}
  112. onChange={this.handleOptionChange}
  113. disabled={disabled}
  114. />
  115. {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
  116. {showResults && <span className='poll__number'>
  117. {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
  118. {Math.round(percent)}%
  119. </span>}
  120. <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
  121. </label>
  122. </li>
  123. );
  124. }
  125. render () {
  126. const { poll, intl } = this.props;
  127. const { expired } = this.state;
  128. if (!poll) {
  129. return null;
  130. }
  131. const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
  132. const showResults = poll.get('voted') || expired;
  133. const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
  134. return (
  135. <div className='poll'>
  136. <ul>
  137. {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
  138. </ul>
  139. <div className='poll__footer'>
  140. {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
  141. {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
  142. <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
  143. {poll.get('expires_at') && <span> · {timeRemaining}</span>}
  144. </div>
  145. </div>
  146. );
  147. }
  148. }