From 2f5fff0222304d9f42b42ead29eba3035ef947d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 22 Oct 2021 14:52:45 +0200 Subject: [PATCH 1/6] Show user what options they have voted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/importer/normalizer.js | 3 ++- app/soapbox/components/poll.js | 25 ++++++++++++++-------- app/styles/polls.scss | 12 +++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js index 6d566fd92..922e57986 100644 --- a/app/soapbox/actions/importer/normalizer.js +++ b/app/soapbox/actions/importer/normalizer.js @@ -74,8 +74,9 @@ export function normalizePoll(poll) { const emojiMap = makeEmojiMap(normalPoll); - normalPoll.options = poll.options.map(option => ({ + normalPoll.options = poll.options.map((option, index) => ({ ...option, + voted: poll.own_votes && poll.own_votes.includes(index), title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index ae66ba18f..be909a760 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'soapbox/features/emoji/emoji'; import RelativeTimestamp from './relative_timestamp'; +import Icon from 'soapbox/components/icon'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, + voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, }); const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { @@ -68,12 +70,13 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(fetchPoll(this.props.poll.get('id'))); }; - renderOption(option, optionIndex) { - const { poll, disabled } = this.props; - 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 active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + renderOption(option, optionIndex, showResults) { + const { poll, disabled, intl } = this.props; + + 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 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) { @@ -102,7 +105,10 @@ class Poll extends ImmutablePureComponent { /> {!showResults && } - {showResults && {Math.round(percent)}%} + {showResults && + {!!voted && } + {Math.round(percent)}% + } @@ -120,11 +126,12 @@ class Poll extends ImmutablePureComponent { const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : ; const showResults = poll.get('voted') || poll.get('expired'); const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + const voted = poll.get('own_votes').size > 0; return ( -
+
    - {poll.get('options').map((option, i) => this.renderOption(option, i))} + {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
diff --git a/app/styles/polls.scss b/app/styles/polls.scss index d3b094798..b1696611a 100644 --- a/app/styles/polls.scss +++ b/app/styles/polls.scss @@ -106,6 +106,18 @@ text-align: right; } + &.voted &__number { + width: 52px; + padding-left: 8px; + flex: 0 0 52px; + } + + &__vote__mark { + float: left; + color: var(--highlight-text-color); + line-height: 18px; + } + &__footer { padding-top: 6px; padding-bottom: 5px; From 040dfe652a1cbe6d1b4a57367c3a16b9627fb4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 22 Oct 2021 15:04:21 +0200 Subject: [PATCH 2/6] Change vote results to display tied leading options as leading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/poll.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index be909a760..166fad837 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -74,7 +74,7 @@ class Poll extends ImmutablePureComponent { const { poll, disabled, intl } = this.props; 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 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)); From 00aacc5e33e2d353839cf75bc31f576de0d4d095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 22 Oct 2021 15:06:34 +0200 Subject: [PATCH 3/6] Add voters count support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/poll.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index 166fad837..f7f78db6b 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -73,10 +73,11 @@ class Poll extends ImmutablePureComponent { renderOption(option, optionIndex, showResults) { const { poll, disabled, intl } = this.props; - 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 active = !!this.state.selected[`${optionIndex}`]; - const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); + 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) { @@ -128,6 +129,14 @@ class Poll extends ImmutablePureComponent { const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); const voted = poll.get('own_votes').size > 0; + let votesCount = null; + + if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { + votesCount = ; + } else { + votesCount = ; + } + return (
    @@ -137,7 +146,7 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } {showResults && !this.props.disabled && · } - + {votesCount} {poll.get('expires_at') && · {timeRemaining}}
From e6f30abd91d9f7d9ce3137bdffde4a2d4dd30894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 22 Oct 2021 15:13:17 +0200 Subject: [PATCH 4/6] Fix poll options not being selectable via keyboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/poll.js | 28 ++++++++++++++++--- .../features/compose/components/poll_form.js | 13 +++++++-- app/styles/polls.scss | 17 +++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index f7f78db6b..ee22a9d1e 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -36,9 +36,7 @@ class Poll extends ImmutablePureComponent { selected: {}, }; - handleOptionChange = e => { - const { target: { value } } = e; - + _toggleOption = value => { if (this.props.poll.get('multiple')) { const tmp = { ...this.state.selected }; if (tmp[value]) { @@ -52,8 +50,20 @@ class Poll extends ImmutablePureComponent { 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; @@ -105,7 +115,17 @@ class Poll extends ImmutablePureComponent { disabled={disabled} /> - {!showResults && } + {!showResults && ( + + )} {showResults && {!!voted && } {Math.round(percent)}% diff --git a/app/soapbox/features/compose/components/poll_form.js b/app/soapbox/features/compose/components/poll_form.js index 8c26798a5..95d2cad7e 100644 --- a/app/soapbox/features/compose/components/poll_form.js +++ b/app/soapbox/features/compose/components/poll_form.js @@ -16,10 +16,11 @@ const messages = defineMessages({ add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, + switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' }, + switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, - hint: { id: 'compose_form.poll.type.hint', defaultMessage: 'Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.' }, }); @injectIntl @@ -63,6 +64,12 @@ class Option extends React.PureComponent { this.props.onClearSuggestions(); } + handleCheckboxKeypress = e => { + if (e.key === 'Enter' || e.key === ' ') { + this.handleToggleMultiple(e); + } + } + onSuggestionsFetchRequested = (token) => { this.props.onFetchSuggestions(token); } @@ -80,9 +87,11 @@ class Option extends React.PureComponent { Date: Fri, 22 Oct 2021 15:23:07 +0200 Subject: [PATCH 5/6] Add single option votes tooltip in polls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/poll.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index ee22a9d1e..d0498af2a 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -14,7 +14,8 @@ import Icon from 'soapbox/components/icon'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, - voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, + 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) => { @@ -126,7 +127,7 @@ class Poll extends ImmutablePureComponent { data-index={optionIndex} /> )} - {showResults && + {showResults && {!!voted && } {Math.round(percent)}% } From 813cd5830598032cad75d9d8c23992b60bf6e3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 22 Oct 2021 15:27:49 +0200 Subject: [PATCH 6/6] Do not use voters_count for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/poll.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js index d0498af2a..866182ca4 100644 --- a/app/soapbox/components/poll.js +++ b/app/soapbox/components/poll.js @@ -84,11 +84,10 @@ class Poll extends ImmutablePureComponent { 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)); + 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 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) { @@ -150,14 +149,6 @@ class Poll extends ImmutablePureComponent { const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); const voted = poll.get('own_votes').size > 0; - let votesCount = null; - - if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { - votesCount = ; - } else { - votesCount = ; - } - return (
    @@ -167,7 +158,7 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } {showResults && !this.props.disabled && · } - {votesCount} + {poll.get('expires_at') && · {timeRemaining}}