2019-03-03 22:18:23 +01:00
import PropTypes from 'prop-types' ;
2023-05-23 17:15:17 +02:00
2019-03-03 22:18:23 +01:00
import { defineMessages , injectIntl , FormattedMessage } from 'react-intl' ;
2023-05-23 17:15:17 +02:00
2019-03-03 22:18:23 +01:00
import classNames from 'classnames' ;
2023-05-23 17:15:17 +02:00
import ImmutablePropTypes from 'react-immutable-proptypes' ;
import ImmutablePureComponent from 'react-immutable-pure-component' ;
2019-03-06 05:35:52 +01:00
import escapeTextContentForBrowser from 'escape-html' ;
2023-05-23 17:15:17 +02:00
import spring from 'react-motion/lib/spring' ;
2024-01-16 11:27:26 +01:00
import CheckIcon from '@/material-icons/400-24px/check.svg?react' ;
2023-05-23 17:15:17 +02:00
import { Icon } from 'mastodon/components/icon' ;
2019-03-06 05:35:52 +01:00
import emojify from 'mastodon/features/emoji/emoji' ;
2023-05-23 17:15:17 +02:00
import Motion from 'mastodon/features/ui/util/optional_motion' ;
2023-05-09 03:11:56 +02:00
import { RelativeTimestamp } from './relative_timestamp' ;
2019-03-03 22:18:23 +01:00
const messages = defineMessages ( {
2021-10-13 04:59:31 +02:00
closed : {
id : 'poll.closed' ,
defaultMessage : 'Closed' ,
} ,
voted : {
id : 'poll.voted' ,
defaultMessage : 'You voted for this answer' ,
} ,
votes : {
id : 'poll.votes' ,
defaultMessage : '{votes, plural, one {# vote} other {# votes}}' ,
} ,
2019-03-03 22:18:23 +01:00
} ) ;
2019-03-20 17:29:12 +01:00
const makeEmojiMap = record => record . get ( 'emojis' ) . reduce ( ( obj , emoji ) => {
obj [ ` : ${ emoji . get ( 'shortcode' ) } : ` ] = emoji . toJS ( ) ;
return obj ;
} , { } ) ;
2019-03-03 22:18:23 +01:00
class Poll extends ImmutablePureComponent {
2022-09-29 06:21:51 +02:00
static contextTypes = {
identity : PropTypes . object ,
} ;
2019-03-03 22:18:23 +01:00
static propTypes = {
2019-03-03 23:45:02 +01:00
poll : ImmutablePropTypes . map ,
2023-02-26 20:13:27 +01:00
lang : PropTypes . string ,
2019-03-03 22:18:23 +01:00
intl : PropTypes . object . isRequired ,
disabled : PropTypes . bool ,
2020-04-16 20:16:20 +02:00
refresh : PropTypes . func ,
2020-04-17 21:54:25 +02:00
onVote : PropTypes . func ,
2019-03-03 22:18:23 +01:00
} ;
state = {
selected : { } ,
2019-09-16 14:32:26 +02:00
expired : null ,
2019-03-03 22:18:23 +01:00
} ;
2019-09-16 14:32:26 +02:00
static getDerivedStateFromProps ( props , state ) {
2023-06-01 13:46:19 +02:00
const { poll } = props ;
2019-10-27 12:45:55 +01:00
const expires _at = poll . get ( 'expires_at' ) ;
2023-06-01 13:46:19 +02:00
const expired = poll . get ( 'expired' ) || expires _at !== null && ( new Date ( expires _at ) ) . getTime ( ) < Date . now ( ) ;
2019-09-16 14:32:26 +02:00
return ( expired === state . expired ) ? null : { expired } ;
}
componentDidMount ( ) {
this . _setupTimer ( ) ;
}
componentDidUpdate ( ) {
this . _setupTimer ( ) ;
}
componentWillUnmount ( ) {
clearTimeout ( this . _timer ) ;
}
_setupTimer ( ) {
2023-06-01 13:46:19 +02:00
const { poll } = this . props ;
2019-09-16 14:32:26 +02:00
clearTimeout ( this . _timer ) ;
if ( ! this . state . expired ) {
2023-06-01 13:46:19 +02:00
const delay = ( new Date ( poll . get ( 'expires_at' ) ) ) . getTime ( ) - Date . now ( ) ;
2019-09-16 14:32:26 +02:00
this . _timer = setTimeout ( ( ) => {
this . setState ( { expired : true } ) ;
} , delay ) ;
}
}
2019-12-03 19:53:16 +01:00
_toggleOption = value => {
2019-03-03 22:18:23 +01:00
if ( this . props . poll . get ( 'multiple' ) ) {
const tmp = { ... this . state . selected } ;
2019-03-04 01:54:14 +01:00
if ( tmp [ value ] ) {
delete tmp [ value ] ;
} else {
tmp [ value ] = true ;
}
2019-03-03 22:18:23 +01:00
this . setState ( { selected : tmp } ) ;
} else {
const tmp = { } ;
tmp [ value ] = true ;
this . setState ( { selected : tmp } ) ;
}
2023-01-30 01:45:35 +01:00
} ;
2019-12-03 19:53:16 +01:00
handleOptionChange = ( { target : { value } } ) => {
this . _toggleOption ( value ) ;
2019-03-03 22:18:23 +01:00
} ;
2019-12-03 19:53:16 +01:00
handleOptionKeyPress = ( e ) => {
if ( e . key === 'Enter' || e . key === ' ' ) {
this . _toggleOption ( e . target . getAttribute ( 'data-index' ) ) ;
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
2023-01-30 01:45:35 +01:00
} ;
2019-12-03 19:53:16 +01:00
2019-03-03 22:18:23 +01:00
handleVote = ( ) => {
if ( this . props . disabled ) {
return ;
}
2020-04-17 21:54:25 +02:00
this . props . onVote ( Object . keys ( this . state . selected ) ) ;
2019-03-03 22:18:23 +01:00
} ;
handleRefresh = ( ) => {
if ( this . props . disabled ) {
return ;
}
2020-04-16 20:16:20 +02:00
this . props . refresh ( ) ;
2019-03-03 22:18:23 +01:00
} ;
2023-07-05 10:32:04 +02:00
handleReveal = ( ) => {
this . setState ( { revealed : true } ) ;
2023-10-09 13:38:29 +02:00
} ;
2023-07-05 10:32:04 +02:00
2019-09-16 14:32:26 +02:00
renderOption ( option , optionIndex , showResults ) {
2023-02-26 20:13:27 +01:00
const { poll , lang , disabled , intl } = this . props ;
2019-09-29 22:58:01 +02: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 22:18:23 +01:00
2023-06-01 00:10:21 +02:00
const title = option . getIn ( [ 'translation' , 'title' ] ) || option . get ( 'title' ) ;
let titleHtml = option . getIn ( [ 'translation' , 'titleHtml' ] ) || option . get ( 'titleHtml' ) ;
if ( ! titleHtml ) {
2019-03-20 17:29:12 +01:00
const emojiMap = makeEmojiMap ( poll ) ;
2023-06-01 00:10:21 +02:00
titleHtml = emojify ( escapeTextContentForBrowser ( title ) , emojiMap ) ;
2019-03-20 17:29:12 +01:00
}
2019-03-03 22:18:23 +01:00
return (
< li key = { option . get ( 'title' ) } >
2020-04-02 17:10:55 +02:00
< label className = { classNames ( 'poll__option' , { selectable : ! showResults } ) } >
2019-03-03 22:18:23 +01:00
< input
name = 'vote-options'
type = { poll . get ( 'multiple' ) ? 'checkbox' : 'radio' }
value = { optionIndex }
checked = { active }
onChange = { this . handleOptionChange }
2019-03-04 01:54:14 +01:00
disabled = { disabled }
2019-03-03 22:18:23 +01:00
/ >
2019-12-03 19:53:16 +01:00
{ ! showResults && (
< span
className = { classNames ( 'poll__input' , { checkbox : poll . get ( 'multiple' ) , active } ) }
2023-04-04 16:33:44 +02:00
tabIndex = { 0 }
2019-12-03 19:53:16 +01:00
role = { poll . get ( 'multiple' ) ? 'checkbox' : 'radio' }
onKeyPress = { this . handleOptionKeyPress }
aria - checked = { active }
2023-06-01 00:10:21 +02:00
aria - label = { title }
2023-02-26 20:13:27 +01:00
lang = { lang }
2019-12-03 19:53:16 +01:00
data - index = { optionIndex }
/ >
) }
2021-10-13 04:59:31 +02:00
{ showResults && (
< span
className = 'poll__number'
title = { intl . formatMessage ( messages . votes , {
votes : option . get ( 'votes_count' ) ,
} ) }
>
{ Math . round ( percent ) } %
< / span >
) }
2019-03-03 22:18:23 +01:00
2020-04-02 17:10:55 +02:00
< span
2021-01-22 10:09:23 +01:00
className = 'poll__option__text translate'
2023-02-26 20:13:27 +01:00
lang = { lang }
2023-06-01 00:10:21 +02:00
dangerouslySetInnerHTML = { { _ _html : titleHtml } }
2020-04-02 17:10:55 +02:00
/ >
{ ! ! voted && < span className = 'poll__voted' >
2023-10-24 19:45:08 +02:00
< Icon id = 'check' icon = { CheckIcon } className = 'poll__voted__mark' title = { intl . formatMessage ( messages . voted ) } / >
2020-04-02 17:10:55 +02:00
< / span > }
2019-03-03 22:18:23 +01:00
< / label >
2020-04-02 17:10:55 +02:00
{ showResults && (
< Motion defaultStyle = { { width : 0 } } style = { { width : spring ( percent , { stiffness : 180 , damping : 12 } ) } } >
{ ( { width } ) =>
< span className = { classNames ( 'poll__chart' , { leading } ) } style = { { width : ` ${ width } % ` } } / >
}
< / Motion >
) }
2019-03-03 22:18:23 +01:00
< / li >
) ;
}
render ( ) {
const { poll , intl } = this . props ;
2023-07-05 10:32:04 +02:00
const { revealed , expired } = this . state ;
2019-03-03 23:45:02 +01:00
if ( ! poll ) {
return null ;
}
2019-09-16 14:32:26 +02:00
const timeRemaining = expired ? intl . formatMessage ( messages . closed ) : < RelativeTimestamp timestamp = { poll . get ( 'expires_at' ) } futureDate / > ;
2023-07-05 10:32:04 +02:00
const showResults = poll . get ( 'voted' ) || revealed || expired ;
2019-03-03 23:45:02 +01:00
const disabled = this . props . disabled || Object . entries ( this . state . selected ) . every ( item => ! item ) ;
2019-03-03 22:18:23 +01:00
2019-09-29 22:58:01 +02: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 22:18:23 +01:00
return (
< div className = 'poll' >
< ul >
2019-09-16 14:32:26 +02:00
{ poll . get ( 'options' ) . map ( ( option , i ) => this . renderOption ( option , i , showResults ) ) }
2019-03-03 22:18:23 +01:00
< / ul >
< div className = 'poll__footer' >
2022-09-29 06:21:51 +02:00
{ ! showResults && < button className = 'button button-secondary' disabled = { disabled || ! this . context . identity . signedIn } onClick = { this . handleVote } > < FormattedMessage id = 'poll.vote' defaultMessage = 'Vote' / > < / button > }
2023-07-05 10:32:04 +02:00
{ ! showResults && < > < button className = 'poll__link' onClick = { this . handleReveal } > < FormattedMessage id = 'poll.reveal' defaultMessage = 'See results' / > < / button > · < / > }
{ showResults && ! this . props . disabled && < > < button className = 'poll__link' onClick = { this . handleRefresh } > < FormattedMessage id = 'poll.refresh' defaultMessage = 'Refresh' / > < / button > · < / > }
2019-09-29 22:58:01 +02:00
{ votesCount }
2023-07-05 10:32:04 +02:00
{ poll . get ( 'expires_at' ) && < > · { timeRemaining } < / > }
2019-03-03 22:18:23 +01:00
< / div >
< / div >
) ;
}
}
2023-03-24 03:17:53 +01:00
export default injectIntl ( Poll ) ;