import classNames from 'classnames'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage, IntlShape } from 'react-intl'; import { spring } from 'react-motion'; import { openModal } from 'soapbox/actions/modals'; import { vote, fetchPoll } from 'soapbox/actions/polls'; import Icon from 'soapbox/components/icon'; import { Text } from 'soapbox/components/ui'; import Motion from 'soapbox/features/ui/util/optional_motion'; import RelativeTimestamp from './relative_timestamp'; import type { Poll as PollEntity, PollOption } from 'soapbox/types/entities'; 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}}' }, }); interface IPoll { poll?: PollEntity, intl: IntlShape, dispatch?: Function, disabled?: boolean, me?: string | null | false | undefined, status?: string, } interface IPollState { selected: Record, } class Poll extends ImmutablePureComponent { state = { selected: {} as Record, }; _toggleOption = (value: string) => { const { me, poll } = this.props; if (me) { if (poll?.multiple) { const tmp = { ...this.state.selected }; if (tmp[value]) { delete tmp[value]; } else { tmp[value] = true; } this.setState({ selected: tmp }); } else { const tmp: Record = {}; tmp[value] = true; this.setState({ selected: tmp }); } } else { this.openUnauthorizedModal(); } } handleOptionChange = (e: React.ChangeEvent): void => { this._toggleOption(e.currentTarget.value); }; handleOptionKeyPress = (e: React.KeyboardEvent): void => { if (e.key === 'Enter' || e.key === ' ') { const dataIndex = e.currentTarget.getAttribute('data-index'); if (dataIndex) { this._toggleOption(dataIndex); } e.stopPropagation(); e.preventDefault(); } } handleVote = () => { const { disabled, dispatch, poll } = this.props; if (disabled || !dispatch || !poll) return; dispatch(vote(poll.id, Object.keys(this.state.selected))); }; openUnauthorizedModal = () => { const { dispatch, status } = this.props; if (!dispatch) return; dispatch(openModal('UNAUTHORIZED', { action: 'POLL_VOTE', ap_id: status, })); } handleRefresh = () => { const { disabled, dispatch, poll } = this.props; if (disabled || !poll || !dispatch) return; dispatch(fetchPoll(poll.id)); }; renderOption(option: PollOption, optionIndex: number, showResults: boolean): JSX.Element | null { const { poll, disabled, intl } = this.props; if (!poll) return null; const percent = poll.votes_count === 0 ? 0 : (option.votes_count / poll.votes_count) * 100; const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count); const active = !!this.state.selected[`${optionIndex}`]; const voted = poll.own_votes?.includes(optionIndex); const titleEmojified = option.title_emojified; return (
  • {showResults && ( {({ width }) => } )}
  • ); } render() { const { poll, intl } = this.props; if (!poll) { return null; } const timeRemaining = poll.expired ? intl.formatMessage(messages.closed) : ; const showResults = poll.voted || poll.expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); return (
      {poll.options.map((option, i) => this.renderOption(option, i, showResults))}
    {!showResults && } {showResults && !this.props.disabled && ( · )} {poll.expires_at && · {timeRemaining}}
    ); } } export default injectIntl(Poll);