gab-social/app/javascript/gabsocial/components/poll.js
mgabdev 6fbea0a59e Progress on little important things
removing .mov for now until we can figure out solution with videojs, added model to track username changes, got chat creation flow down, progress on bookmark collections, albums, filtering blocks/mutes from group, explore, collection timelines
2020-12-22 01:36:38 -05:00

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,
radiusSmall: 1,
h100PC: 1,
bgSecondary: !leading && !optionHasNoVotes,
bgTertiary: !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,
borderColorSecondary: !showResults,
circle: !showResults,
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))