6fbea0a59e
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
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,
|
|
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)) |