gab-social/app/javascript/gabsocial/components/poll.js

243 lines
7.2 KiB
JavaScript
Raw Normal View History

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
2020-03-07 04:53:28 +00:00
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
import escapeTextContentForBrowser from 'escape-html'
import spring from 'react-motion/lib/spring'
2020-05-10 04:26:58 +01:00
import { me } from '../initial_state'
import Motion from '../features/ui/util/reduced_motion'
2020-03-08 16:46:00 +00:00
import { vote } from '../actions/polls'
import { CX } from '../constants'
import emojify from './emoji/emoji'
2020-03-08 16:46:00 +00:00
import RelativeTimestamp from './relative_timestamp'
import Button from './button'
import DotTextSeperator from './dot_text_seperator'
import Text from './text'
2020-03-07 04:53:28 +00:00
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
2020-03-07 04:53:28 +00:00
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS()
return obj
}, {})
class Poll extends ImmutablePureComponent {
state = {
selected: {},
2020-03-07 04:53:28 +00:00
}
handleOptionChange = e => {
2020-03-07 04:53:28 +00:00
const { target: { value } } = e
if (this.props.poll.get('multiple')) {
2020-03-07 04:53:28 +00:00
const tmp = { ...this.state.selected }
if (tmp[value]) {
2020-03-07 04:53:28 +00:00
delete tmp[value]
} else {
2020-03-07 04:53:28 +00:00
tmp[value] = true
}
2020-03-07 04:53:28 +00:00
this.setState({ selected: tmp })
} else {
2020-03-07 04:53:28 +00:00
const tmp = {}
tmp[value] = true
this.setState({ selected: tmp })
}
2020-03-07 04:53:28 +00:00
}
handleVote = () => {
2020-03-07 04:53:28 +00:00
if (this.props.disabled) return
2020-03-07 04:53:28 +00:00
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)))
}
2020-03-07 04:53:28 +00:00
renderOption(option, optionIndex) {
const { poll, disabled } = this.props
const { selected } = this.state
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'))
2020-05-02 07:25:55 +01:00
const optionHasNoVotes = option.get('votes_count') === 0
2020-03-07 04:53:28 +00:00
const active = !!selected[`${optionIndex}`]
const showResults = poll.get('voted') || poll.get('expired')
const multiple = poll.get('multiple')
2020-05-02 07:25:55 +01:00
const correctedWidthPercent = optionHasNoVotes ? 100 : percent
2020-03-07 04:53:28 +00:00
let titleEmojified = option.get('title_emojified')
if (!titleEmojified) {
2020-03-07 04:53:28 +00:00
const emojiMap = makeEmojiMap(poll)
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)
}
const chartClasses = CX({
d: 1,
2020-04-23 07:13:29 +01:00
posAbs: 1,
2020-03-07 04:53:28 +00:00
top0: 1,
left0: 1,
radiusSmall: 1,
h100PC: 1,
2020-05-02 07:25:55 +01:00
bgSecondary: !leading && !optionHasNoVotes,
bgTertiary: !leading && optionHasNoVotes,
2020-04-29 23:32:49 +01:00
bgBrandLight: leading,
2020-03-07 04:53:28 +00:00
})
2020-05-02 07:25:55 +01:00
// : todo :
const inputClasses = CX('poll__input', {
'poll__input--checkbox': multiple,
'poll__input--active': active,
2020-03-07 04:53:28 +00:00
})
const listItemClasses = CX({
d: 1,
2020-03-07 04:53:28 +00:00
flexRow: 1,
2020-03-11 23:56:18 +00:00
py10: showResults,
mb10: 1,
border1PX: !showResults || !!me,
borderColorSecondary: !showResults || !!me,
circle: !showResults || !!me,
overflowHidden: !!me,
2020-03-07 04:53:28 +00:00
cursorPointer: !showResults,
2020-04-29 23:32:49 +01:00
bgSubtle_onHover: !showResults,
bgSubtle: !showResults && active,
2020-03-07 04:53:28 +00:00
})
const textContainerClasses = CX({
d: 1,
w100PC: 1,
2020-03-11 23:56:18 +00:00
px15: 1,
py10: !showResults,
2020-03-07 04:53:28 +00:00
cursorPointer: !showResults,
aiCenter: !showResults,
2020-03-07 04:53:28 +00:00
})
// : todo : fix widths and truncate for large poll options
return (
2020-03-07 04:53:28 +00:00
<li className={listItemClasses} key={option.get('title')}>
{
showResults && (
2020-05-02 07:25:55 +01:00
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(correctedWidthPercent, { stiffness: 180, damping: 24 }) }}>
{({ width }) =>
<span className={chartClasses} style={{ width: `${width}%` }} />
}
</Motion>
)
}
2020-03-07 04:53:28 +00:00
<label className={textContainerClasses}>
<Text
size='medium'
color='primary'
2020-05-02 07:25:55 +01:00
weight={(leading && showResults) ? 'bold' : 'normal'}
className={[_s.displayFlex, _s.flexRow, _s.w100PC, _s.aiCenter].join(' ')}
2020-03-07 04:53:28 +00:00
>
{
!showResults &&
<input
name='vote-options'
type={multiple ? 'checkbox' : 'radio'}
value={optionIndex}
checked={active}
onChange={this.handleOptionChange}
disabled={disabled}
className={[_s.d, _s.mr10].join(' ')}
2020-03-07 04:53:28 +00:00
/>
}
{
2020-04-25 18:00:51 +01:00
/* : todo : */
2020-03-07 04:53:28 +00:00
!showResults && <span className={inputClasses} />
}
2020-03-08 16:46:00 +00:00
<span
className={_s.text}
dangerouslySetInnerHTML={{ __html: titleEmojified }}
/>
2020-03-07 04:53:28 +00:00
{
showResults &&
2020-04-24 04:17:27 +01:00
<span className={_s.mlAuto}>
2020-03-07 04:53:28 +00:00
{Math.round(percent)}%
</span>
}
</Text>
</label>
</li>
2020-03-07 04:53:28 +00:00
)
}
2020-03-07 04:53:28 +00:00
render() {
const { poll, intl } = this.props
2020-03-07 04:53:28 +00:00
if (!poll) return null
const timeRemaining = poll.get('expired') ?
intl.formatMessage(messages.closed)
2020-03-07 04:53:28 +00:00
: <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />
const showResults = poll.get('voted') || poll.get('expired')
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item)
return (
<div className={[_s.d, _s.px15, _s.py10].join(' ')}>
<ul className={[_s.d, _s.listStyleNone].join(' ')}>
2020-03-07 04:53:28 +00:00
{
poll.get('options').map((option, i) => this.renderOption(option, i))
}
</ul>
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
{
2020-05-10 04:26:58 +01:00
!showResults && me &&
2020-03-07 04:53:28 +00:00
<Button
2020-04-23 07:13:29 +01:00
isNarrow
2020-03-11 23:56:18 +00:00
className={_s.mr10}
2020-04-23 07:13:29 +01:00
isDisabled={disabled}
2020-03-07 04:53:28 +00:00
onClick={this.handleVote}
>
2020-03-11 23:56:18 +00:00
<Text color='inherit' size='small' className={_s.px10}>
2020-03-07 04:53:28 +00:00
{intl.formatMessage(messages.vote)}
</Text>
</Button>
}
2020-03-07 04:53:28 +00:00
<Text color='secondary'>
<FormattedMessage
id='poll.total_votes'
defaultMessage='{count, plural, one {# vote} other {# votes}}'
values={{
count: poll.get('votes_count'),
}}
/>
{
poll.get('expires_at') &&
<React.Fragment>
2020-03-07 04:53:28 +00:00
<DotTextSeperator />
2020-03-11 23:56:18 +00:00
<Text color='secondary' className={_s.ml5}>
2020-03-07 04:53:28 +00:00
{timeRemaining}
</Text>
</React.Fragment>
2020-03-07 04:53:28 +00:00
}
</Text>
</div>
</div>
2020-03-07 04:53:28 +00:00
)
}
}
const mapStateToProps = (state, { pollId }) => ({
poll: state.getIn(['polls', pollId]),
})
const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
vote: { id: 'poll.vote', defaultMessage: 'Vote' },
refresh: { id: 'poll.refresh', defaultMessage: 'Refresh' },
})
Poll.propTypes = {
poll: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func,
disabled: PropTypes.bool,
}
export default injectIntl(connect(mapStateToProps)(Poll))