2019-03-03 21:18:23 +00:00
import React from 'react' ;
import PropTypes from 'prop-types' ;
import ImmutablePropTypes from 'react-immutable-proptypes' ;
import ImmutablePureComponent from 'react-immutable-pure-component' ;
import { defineMessages , injectIntl , FormattedMessage } from 'react-intl' ;
import classNames from 'classnames' ;
2020-04-16 19:16:20 +01:00
import { vote } from 'mastodon/actions/polls' ;
2019-03-03 21:18:23 +00:00
import Motion from 'mastodon/features/ui/util/optional_motion' ;
import spring from 'react-motion/lib/spring' ;
2019-03-06 04:35:52 +00:00
import escapeTextContentForBrowser from 'escape-html' ;
import emojify from 'mastodon/features/emoji/emoji' ;
2019-04-03 17:16:55 +01:00
import RelativeTimestamp from './relative_timestamp' ;
2019-09-22 13:15:18 +01:00
import Icon from 'mastodon/components/icon' ;
2019-03-03 21:18:23 +00:00
const messages = defineMessages ( {
2019-03-04 21:52:41 +00:00
closed : { id : 'poll.closed' , defaultMessage : 'Closed' } ,
2019-09-28 18:41:36 +01:00
voted : { id : 'poll.voted' , defaultMessage : 'You voted for this answer' , description : 'Tooltip of the "voted" checkmark in polls' } ,
2019-03-03 21:18:23 +00:00
} ) ;
2019-03-20 16:29:12 +00:00
const makeEmojiMap = record => record . get ( 'emojis' ) . reduce ( ( obj , emoji ) => {
obj [ ` : ${ emoji . get ( 'shortcode' ) } : ` ] = emoji . toJS ( ) ;
return obj ;
} , { } ) ;
2019-03-03 21:18:23 +00:00
export default @ injectIntl
class Poll extends ImmutablePureComponent {
static propTypes = {
2019-03-03 22:45:02 +00:00
poll : ImmutablePropTypes . map ,
2019-03-03 21:18:23 +00:00
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func ,
disabled : PropTypes . bool ,
2020-04-16 19:16:20 +01:00
refresh : PropTypes . func ,
2019-03-03 21:18:23 +00:00
} ;
state = {
selected : { } ,
2019-09-16 13:32:26 +01:00
expired : null ,
2019-03-03 21:18:23 +00:00
} ;
2019-09-16 13:32:26 +01:00
static getDerivedStateFromProps ( props , state ) {
const { poll , intl } = props ;
2019-10-27 11:45:55 +00:00
const expires _at = poll . get ( 'expires_at' ) ;
const expired = poll . get ( 'expired' ) || expires _at !== null && ( new Date ( expires _at ) ) . getTime ( ) < intl . now ( ) ;
2019-09-16 13:32:26 +01:00
return ( expired === state . expired ) ? null : { expired } ;
}
componentDidMount ( ) {
this . _setupTimer ( ) ;
}
componentDidUpdate ( ) {
this . _setupTimer ( ) ;
}
componentWillUnmount ( ) {
clearTimeout ( this . _timer ) ;
}
_setupTimer ( ) {
const { poll , intl } = this . props ;
clearTimeout ( this . _timer ) ;
if ( ! this . state . expired ) {
const delay = ( new Date ( poll . get ( 'expires_at' ) ) ) . getTime ( ) - intl . now ( ) ;
this . _timer = setTimeout ( ( ) => {
this . setState ( { expired : true } ) ;
} , delay ) ;
}
}
2019-12-03 18:53:16 +00:00
_toggleOption = value => {
2019-03-03 21:18:23 +00:00
if ( this . props . poll . get ( 'multiple' ) ) {
const tmp = { ... this . state . selected } ;
2019-03-04 00:54:14 +00:00
if ( tmp [ value ] ) {
delete tmp [ value ] ;
} else {
tmp [ value ] = true ;
}
2019-03-03 21:18:23 +00:00
this . setState ( { selected : tmp } ) ;
} else {
const tmp = { } ;
tmp [ value ] = true ;
this . setState ( { selected : tmp } ) ;
}
2019-12-03 18:53:16 +00:00
}
handleOptionChange = ( { target : { value } } ) => {
this . _toggleOption ( value ) ;
2019-03-03 21:18:23 +00:00
} ;
2019-12-03 18:53:16 +00:00
handleOptionKeyPress = ( e ) => {
if ( e . key === 'Enter' || e . key === ' ' ) {
this . _toggleOption ( e . target . getAttribute ( 'data-index' ) ) ;
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
}
2019-03-03 21:18:23 +00:00
handleVote = ( ) => {
if ( this . props . disabled ) {
return ;
}
this . props . dispatch ( vote ( this . props . poll . get ( 'id' ) , Object . keys ( this . state . selected ) ) ) ;
} ;
handleRefresh = ( ) => {
if ( this . props . disabled ) {
return ;
}
2020-04-16 19:16:20 +01:00
this . props . refresh ( ) ;
2019-03-03 21:18:23 +00:00
} ;
2019-09-16 13:32:26 +01:00
renderOption ( option , optionIndex , showResults ) {
2019-09-28 18:41:36 +01:00
const { poll , disabled , intl } = this . props ;
2019-09-29 21:58:01 +01:00
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 ) ) ;
2019-03-03 21:18:23 +00:00
2019-03-20 16:29:12 +00:00
let titleEmojified = option . get ( 'title_emojified' ) ;
if ( ! titleEmojified ) {
const emojiMap = makeEmojiMap ( poll ) ;
titleEmojified = emojify ( escapeTextContentForBrowser ( option . get ( 'title' ) ) , emojiMap ) ;
}
2019-03-03 21:18:23 +00:00
return (
< li key = { option . get ( 'title' ) } >
2020-04-02 16:10:55 +01:00
< label className = { classNames ( 'poll__option' , { selectable : ! showResults } ) } >
2019-03-03 21:18:23 +00:00
< input
name = 'vote-options'
type = { poll . get ( 'multiple' ) ? 'checkbox' : 'radio' }
value = { optionIndex }
checked = { active }
onChange = { this . handleOptionChange }
2019-03-04 00:54:14 +00:00
disabled = { disabled }
2019-03-03 21:18:23 +00:00
/ >
2019-12-03 18:53:16 +00:00
{ ! showResults && (
< span
className = { classNames ( 'poll__input' , { checkbox : poll . get ( 'multiple' ) , active } ) }
tabIndex = '0'
role = { poll . get ( 'multiple' ) ? 'checkbox' : 'radio' }
onKeyPress = { this . handleOptionKeyPress }
aria - checked = { active }
aria - label = { option . get ( 'title' ) }
data - index = { optionIndex }
/ >
) }
2019-09-22 13:15:18 +01:00
{ showResults && < span className = 'poll__number' >
{ Math . round ( percent ) } %
< / s p a n > }
2019-03-03 21:18:23 +00:00
2020-04-02 16:10:55 +01:00
< span
className = 'poll__option__text'
dangerouslySetInnerHTML = { { _ _html : titleEmojified } }
/ >
{ ! ! voted && < span className = 'poll__voted' >
< Icon id = 'check' className = 'poll__voted__mark' title = { intl . formatMessage ( messages . voted ) } / >
< / s p a n > }
2019-03-03 21:18:23 +00:00
< / l a b e l >
2020-04-02 16:10:55 +01:00
{ showResults && (
< Motion defaultStyle = { { width : 0 } } style = { { width : spring ( percent , { stiffness : 180 , damping : 12 } ) } } >
{ ( { width } ) =>
< span className = { classNames ( 'poll__chart' , { leading } ) } style = { { width : ` ${ width } % ` } } / >
}
< / M o t i o n >
) }
2019-03-03 21:18:23 +00:00
< / l i >
) ;
}
render ( ) {
const { poll , intl } = this . props ;
2019-09-16 13:32:26 +01:00
const { expired } = this . state ;
2019-03-03 22:45:02 +00:00
if ( ! poll ) {
return null ;
}
2019-09-16 13:32:26 +01:00
const timeRemaining = expired ? intl . formatMessage ( messages . closed ) : < RelativeTimestamp timestamp = { poll . get ( 'expires_at' ) } futureDate / > ;
const showResults = poll . get ( 'voted' ) || expired ;
2019-03-03 22:45:02 +00:00
const disabled = this . props . disabled || Object . entries ( this . state . selected ) . every ( item => ! item ) ;
2019-03-03 21:18:23 +00:00
2019-09-29 21:58:01 +01:00
let votesCount = null ;
if ( poll . get ( 'voters_count' ) !== null && poll . get ( 'voters_count' ) !== undefined ) {
votesCount = < FormattedMessage id = 'poll.total_people' defaultMessage = '{count, plural, one {# person} other {# people}}' values = { { count : poll . get ( 'voters_count' ) } } / > ;
} else {
votesCount = < FormattedMessage id = 'poll.total_votes' defaultMessage = '{count, plural, one {# vote} other {# votes}}' values = { { count : poll . get ( 'votes_count' ) } } / > ;
}
2019-03-03 21:18:23 +00:00
return (
< div className = 'poll' >
< ul >
2019-09-16 13:32:26 +01:00
{ poll . get ( 'options' ) . map ( ( option , i ) => this . renderOption ( option , i , showResults ) ) }
2019-03-03 21:18:23 +00:00
< / u l >
< div className = 'poll__footer' >
{ ! showResults && < button className = 'button button-secondary' disabled = { disabled } onClick = { this . handleVote } > < FormattedMessage id = 'poll.vote' defaultMessage = 'Vote' / > < / b u t t o n > }
{ showResults && ! this . props . disabled && < span > < button className = 'poll__link' onClick = { this . handleRefresh } > < FormattedMessage id = 'poll.refresh' defaultMessage = 'Refresh' / > < / b u t t o n > · < / s p a n > }
2019-09-29 21:58:01 +01:00
{ votesCount }
2019-03-05 02:51:18 +00:00
{ poll . get ( 'expires_at' ) && < span > · { timeRemaining } < / s p a n > }
2019-03-03 21:18:23 +00:00
< / d i v >
< / d i v >
) ;
}
}