diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.tsx similarity index 56% rename from app/soapbox/components/poll.js rename to app/soapbox/components/poll.tsx index 22e06beb0..b11925fff 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.tsx @@ -1,45 +1,49 @@ import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import spring from 'react-motion/lib/spring'; +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 SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types'; 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}}' }, }); -export default @injectIntl -class Poll extends ImmutablePureComponent { +interface IPoll { + poll?: PollEntity, + intl: IntlShape, + dispatch?: Function, + disabled?: boolean, + me?: string | null | false | undefined, + status?: string, +} - static propTypes = { - poll: ImmutablePropTypes.map, - intl: PropTypes.object.isRequired, - dispatch: PropTypes.func, - disabled: PropTypes.bool, - me: SoapboxPropTypes.me, - status: PropTypes.string, - }; +interface IPollState { + selected: Record, +} + +class Poll extends ImmutablePureComponent { state = { - selected: {}, + selected: {} as Record, }; - _toggleOption = value => { - if (this.props.me) { - if (this.props.poll.get('multiple')) { + _toggleOption = (value: string) => { + const { me, poll } = this.props; + + if (me) { + if (poll?.multiple) { const tmp = { ...this.state.selected }; if (tmp[value]) { delete tmp[value]; @@ -48,7 +52,7 @@ class Poll extends ImmutablePureComponent { } this.setState({ selected: tmp }); } else { - const tmp = {}; + const tmp: Record = {}; tmp[value] = true; this.setState({ selected: tmp }); } @@ -57,28 +61,32 @@ class Poll extends ImmutablePureComponent { } } - handleOptionChange = ({ target: { value } }) => { - this._toggleOption(value); + handleOptionChange = (e: React.ChangeEvent): void => { + this._toggleOption(e.currentTarget.value); }; - handleOptionKeyPress = (e) => { + handleOptionKeyPress = (e: React.KeyboardEvent): void => { if (e.key === 'Enter' || e.key === ' ') { - this._toggleOption(e.target.getAttribute('data-index')); + const dataIndex = e.currentTarget.getAttribute('data-index'); + + if (dataIndex) { + this._toggleOption(dataIndex); + } + e.stopPropagation(); e.preventDefault(); } } handleVote = () => { - if (this.props.disabled) { - return; - } - - this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected))); + 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, @@ -86,21 +94,20 @@ class Poll extends ImmutablePureComponent { } handleRefresh = () => { - if (this.props.disabled) { - return; - } - - this.props.dispatch(fetchPoll(this.props.poll.get('id'))); + const { disabled, dispatch, poll } = this.props; + if (disabled || !poll || !dispatch) return; + dispatch(fetchPoll(poll.id)); }; - renderOption(option, optionIndex, showResults) { + renderOption(option: PollOption, optionIndex: number, showResults: boolean): JSX.Element | null { const { poll, disabled, intl } = this.props; + if (!poll) return null; - const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 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 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.get('title_emojified'); + const titleEmojified = option.title_emojified; return (
  • @@ -115,7 +122,7 @@ class Poll extends ImmutablePureComponent { @@ -151,15 +160,14 @@ class Poll extends ImmutablePureComponent { return null; } - const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || poll.get('expired'); + 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); - const voted = poll.voted; return ( -
    +
      - {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))} + {poll.options.map((option, i) => this.renderOption(option, i, showResults))}
    @@ -170,8 +178,8 @@ class Poll extends ImmutablePureComponent { · )} - - {poll.get('expires_at') && · {timeRemaining}} + + {poll.expires_at && · {timeRemaining}}
    @@ -179,3 +187,5 @@ class Poll extends ImmutablePureComponent { } } + +export default injectIntl(Poll); diff --git a/app/soapbox/containers/poll_container.js b/app/soapbox/containers/poll_container.js deleted file mode 100644 index 50d21517a..000000000 --- a/app/soapbox/containers/poll_container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux'; - -import Poll from 'soapbox/components/poll'; - -const mapStateToProps = (state, { pollId }) => ({ - poll: state.getIn(['polls', pollId]), - me: state.get('me'), -}); - - -export default connect(mapStateToProps)(Poll); diff --git a/app/soapbox/containers/poll_container.ts b/app/soapbox/containers/poll_container.ts new file mode 100644 index 000000000..8d83977af --- /dev/null +++ b/app/soapbox/containers/poll_container.ts @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; + +import Poll from 'soapbox/components/poll'; + +import type { RootState } from 'soapbox/store'; + +interface IPollContainer { + pollId: string, +} + +const mapStateToProps = (state: RootState, { pollId }: IPollContainer) => ({ + poll: state.polls.get(pollId), + me: state.me, +}); + + +export default connect(mapStateToProps)(Poll); diff --git a/package.json b/package.json index 4b8b7f358..a63867743 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@types/lodash": "^4.14.180", "@types/qrcode.react": "^1.0.2", "@types/react-helmet": "^6.1.5", + "@types/react-motion": "^0.0.32", "@types/react-router-dom": "^5.3.3", "@types/react-toggle": "^4.0.3", "@types/semver": "^7.3.9", diff --git a/yarn.lock b/yarn.lock index 15eb690b9..275ca81c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2109,6 +2109,13 @@ dependencies: "@types/react" "*" +"@types/react-motion@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.32.tgz#c7355cca054664f1aeadd7388f6890e9355e1783" + integrity sha512-xePjDdhy6/6AX3CUQCeQ2GSF0RwF+lXSpUSrm8tmdUXRf5Ps/dULwouTJ8YHhDvX7WlwYRKZjHXatadz/x3HXA== + dependencies: + "@types/react" "*" + "@types/react-redux@^7.1.16": version "7.1.18" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.18.tgz#2bf8fd56ebaae679a90ebffe48ff73717c438e04"