242 lines
7.1 KiB
JavaScript
242 lines
7.1 KiB
JavaScript
import React from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import { connect } from 'react-redux'
|
|
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'
|
|
import { me } from '../initial_state'
|
|
import Motion from '../features/ui/util/reduced_motion'
|
|
import { vote } from '../actions/polls'
|
|
import { CX } from '../constants'
|
|
import emojify from './emoji/emoji'
|
|
import RelativeTimestamp from './relative_timestamp'
|
|
import Button from './button'
|
|
import DotTextSeperator from './dot_text_seperator'
|
|
import Text from './text'
|
|
|
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
|
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS()
|
|
return obj
|
|
}, {})
|
|
|
|
class Poll extends ImmutablePureComponent {
|
|
|
|
state = {
|
|
selected: {},
|
|
}
|
|
|
|
handleOptionChange = e => {
|
|
const { target: { value } } = e
|
|
|
|
if (this.props.poll.get('multiple')) {
|
|
const tmp = { ...this.state.selected }
|
|
if (tmp[value]) {
|
|
delete tmp[value]
|
|
} else {
|
|
tmp[value] = true
|
|
}
|
|
this.setState({ selected: tmp })
|
|
} else {
|
|
const tmp = {}
|
|
tmp[value] = true
|
|
this.setState({ selected: tmp })
|
|
}
|
|
}
|
|
|
|
handleVote = () => {
|
|
if (this.props.disabled) return
|
|
|
|
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)))
|
|
}
|
|
|
|
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'))
|
|
const optionHasNoVotes = option.get('votes_count') === 0
|
|
const active = !!selected[`${optionIndex}`]
|
|
const showResults = poll.get('voted') || poll.get('expired')
|
|
const multiple = poll.get('multiple')
|
|
const correctedWidthPercent = optionHasNoVotes ? 100 : percent
|
|
|
|
let titleEmojified = option.get('title_emojified')
|
|
if (!titleEmojified) {
|
|
const emojiMap = makeEmojiMap(poll)
|
|
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)
|
|
}
|
|
|
|
const chartClasses = CX({
|
|
d: 1,
|
|
posAbs: 1,
|
|
top0: 1,
|
|
left0: 1,
|
|
circle: 1,
|
|
h100PC: 1,
|
|
bgSecondary: !leading && !optionHasNoVotes,
|
|
bgBrandLight: leading,
|
|
})
|
|
|
|
// : todo :
|
|
const inputClasses = CX('poll__input', {
|
|
'poll__input--checkbox': multiple,
|
|
'poll__input--active': active,
|
|
})
|
|
|
|
const listItemClasses = CX({
|
|
d: 1,
|
|
flexRow: 1,
|
|
py10: showResults,
|
|
mb10: 1,
|
|
border1PX: !showResults || !!me,
|
|
borderColorSecondary: !showResults || !!me,
|
|
circle: !showResults || !!me,
|
|
overflowHidden: !!me,
|
|
cursorPointer: !showResults,
|
|
bgSubtle_onHover: !showResults,
|
|
bgSubtle: !showResults && active,
|
|
})
|
|
|
|
const textContainerClasses = CX({
|
|
d: 1,
|
|
w100PC: 1,
|
|
px15: 1,
|
|
py10: !showResults,
|
|
cursorPointer: !showResults,
|
|
aiCenter: !showResults,
|
|
})
|
|
|
|
// : todo : fix widths and truncate for large poll options
|
|
return (
|
|
<li className={listItemClasses} key={option.get('title')}>
|
|
{
|
|
showResults && (
|
|
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(correctedWidthPercent, { stiffness: 180, damping: 24 }) }}>
|
|
{({ width }) =>
|
|
<span className={chartClasses} style={{ width: `${width}%` }} />
|
|
}
|
|
</Motion>
|
|
)
|
|
}
|
|
|
|
<label className={textContainerClasses}>
|
|
<Text
|
|
size='medium'
|
|
color='primary'
|
|
weight={(leading && showResults) ? 'bold' : 'normal'}
|
|
className={[_s.displayFlex, _s.flexRow, _s.w100PC, _s.aiCenter].join(' ')}
|
|
>
|
|
{
|
|
!showResults &&
|
|
<input
|
|
name='vote-options'
|
|
type={multiple ? 'checkbox' : 'radio'}
|
|
value={optionIndex}
|
|
checked={active}
|
|
onChange={this.handleOptionChange}
|
|
disabled={disabled}
|
|
className={[_s.d, _s.mr10].join(' ')}
|
|
/>
|
|
}
|
|
|
|
{
|
|
/* : todo : */
|
|
!showResults && <span className={inputClasses} />
|
|
}
|
|
|
|
<span
|
|
className={_s.text}
|
|
dangerouslySetInnerHTML={{ __html: titleEmojified }}
|
|
/>
|
|
|
|
{
|
|
showResults &&
|
|
<span className={_s.mlAuto}>
|
|
{Math.round(percent)}%
|
|
</span>
|
|
}
|
|
</Text>
|
|
</label>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
const { poll, intl } = this.props
|
|
|
|
if (!poll) return null
|
|
|
|
const timeRemaining = poll.get('expired') ?
|
|
intl.formatMessage(messages.closed)
|
|
: <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(' ')}>
|
|
{
|
|
poll.get('options').map((option, i) => this.renderOption(option, i))
|
|
}
|
|
</ul>
|
|
|
|
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
|
|
{
|
|
!showResults && me &&
|
|
<Button
|
|
isNarrow
|
|
className={_s.mr10}
|
|
isDisabled={disabled}
|
|
onClick={this.handleVote}
|
|
>
|
|
<Text color='inherit' size='small' className={_s.px10}>
|
|
{intl.formatMessage(messages.vote)}
|
|
</Text>
|
|
</Button>
|
|
}
|
|
|
|
<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>
|
|
<DotTextSeperator />
|
|
<Text color='secondary' className={_s.ml5}>
|
|
{timeRemaining}
|
|
</Text>
|
|
</React.Fragment>
|
|
}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
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)) |