1 import React
from 'react';
2 import PropTypes
from 'prop-types';
3 import ImmutablePropTypes
from 'react-immutable-proptypes';
4 import ImmutablePureComponent
from 'react-immutable-pure-component';
5 import { defineMessages
, injectIntl
, FormattedMessage
} from 'react-intl';
6 import classNames
from 'classnames';
7 import { vote
, fetchPoll
} from 'mastodon/actions/polls';
8 import Motion
from 'mastodon/features/ui/util/optional_motion';
9 import spring
from 'react-motion/lib/spring';
10 import escapeTextContentForBrowser
from 'escape-html';
11 import emojify
from 'mastodon/features/emoji/emoji';
12 import RelativeTimestamp
from './relative_timestamp';
13 import Icon
from 'mastodon/components/icon';
15 const messages
= defineMessages({
16 closed: { id: 'poll.closed', defaultMessage: 'Closed' },
17 voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
20 const makeEmojiMap
= record
=> record
.get('emojis').reduce((obj
, emoji
) => {
21 obj
[`:${emoji.get('shortcode')}:`] = emoji
.toJS();
25 export default @injectIntl
26 class Poll
extends ImmutablePureComponent
{
29 poll: ImmutablePropTypes
.map
,
30 intl: PropTypes
.object
.isRequired
,
31 dispatch: PropTypes
.func
,
32 disabled: PropTypes
.bool
,
40 static getDerivedStateFromProps (props
, state
) {
41 const { poll
, intl
} = props
;
42 const expires_at
= poll
.get('expires_at');
43 const expired
= poll
.get('expired') || expires_at
!== null && (new Date(expires_at
)).getTime() < intl
.now();
44 return (expired
=== state
.expired
) ? null : { expired
};
47 componentDidMount () {
51 componentDidUpdate () {
55 componentWillUnmount () {
56 clearTimeout(this._timer
);
60 const { poll
, intl
} = this.props
;
61 clearTimeout(this._timer
);
62 if (!this.state
.expired
) {
63 const delay
= (new Date(poll
.get('expires_at'))).getTime() - intl
.now();
64 this._timer
= setTimeout(() => {
65 this.setState({ expired: true });
70 handleOptionChange
= e
=> {
71 const { target: { value
} } = e
;
73 if (this.props
.poll
.get('multiple')) {
74 const tmp
= { ...this.state
.selected
};
80 this.setState({ selected: tmp
});
84 this.setState({ selected: tmp
});
89 if (this.props
.disabled
) {
93 this.props
.dispatch(vote(this.props
.poll
.get('id'), Object
.keys(this.state
.selected
)));
96 handleRefresh
= () => {
97 if (this.props
.disabled
) {
101 this.props
.dispatch(fetchPoll(this.props
.poll
.get('id')));
104 renderOption (option
, optionIndex
, showResults
) {
105 const { poll
, disabled
, intl
} = this.props
;
106 const pollVotesCount
= poll
.get('voters_count') || poll
.get('votes_count');
107 const percent
= pollVotesCount
=== 0 ? 0 : (option
.get('votes_count') / pollVotesCount
) * 100;
108 const leading
= poll
.get('options').filterNot(other
=> other
.get('title') === option
.get('title')).every(other
=> option
.get('votes_count') >= other
.get('votes_count'));
109 const active
= !!this.state
.selected
[`${optionIndex}`];
110 const voted
= option
.get('voted') || (poll
.get('own_votes') && poll
.get('own_votes').includes(optionIndex
));
112 let titleEmojified
= option
.get('title_emojified');
113 if (!titleEmojified
) {
114 const emojiMap
= makeEmojiMap(poll
);
115 titleEmojified
= emojify(escapeTextContentForBrowser(option
.get('title')), emojiMap
);
119 <li key
={option
.get('title')}>
121 <Motion defaultStyle
={{ width: 0 }} style
={{ width: spring(percent
, { stiffness: 180, damping: 12 }) }}>
123 <span className
={classNames('poll__chart', { leading
})} style
={{ width: `${width}%` }} />
128 <label className
={classNames('poll__text', { selectable: !showResults
})}>
131 type
={poll
.get('multiple') ? 'checkbox' : 'radio'}
134 onChange
={this.handleOptionChange
}
138 {!showResults
&& <span className
={classNames('poll__input', { checkbox: poll
.get('multiple'), active
})} />}
139 {showResults
&& <span className
='poll__number'>
140 {!!voted
&& <Icon id
='check' className
='poll__vote__mark' title
={intl
.formatMessage(messages
.voted
)} />}
141 {Math
.round(percent
)}%
144 <span dangerouslySetInnerHTML
={{ __html: titleEmojified
}} />
151 const { poll
, intl
} = this.props
;
152 const { expired
} = this.state
;
158 const timeRemaining
= expired
? intl
.formatMessage(messages
.closed
) : <RelativeTimestamp timestamp
={poll
.get('expires_at')} futureDate
/>;
159 const showResults
= poll
.get('voted') || expired
;
160 const disabled
= this.props
.disabled
|| Object
.entries(this.state
.selected
).every(item
=> !item
);
162 let votesCount
= null;
164 if (poll
.get('voters_count') !== null && poll
.get('voters_count') !== undefined) {
165 votesCount
= <FormattedMessage id
='poll.total_people' defaultMessage
='{count, plural, one {# person} other {# people}}' values
={{ count: poll
.get('voters_count') }} />;
167 votesCount
= <FormattedMessage id
='poll.total_votes' defaultMessage
='{count, plural, one {# vote} other {# votes}}' values
={{ count: poll
.get('votes_count') }} />;
171 <div className
='poll'>
173 {poll
.get('options').map((option
, i
) => this.renderOption(option
, i
, showResults
))}
176 <div className
='poll__footer'>
177 {!showResults
&& <button className
='button button-secondary' disabled
={disabled
} onClick
={this.handleVote
}><FormattedMessage id
='poll.vote' defaultMessage
='Vote' /></button
>}
178 {showResults
&& !this.props
.disabled
&& <span
><button className
='poll__link' onClick
={this.handleRefresh
}><FormattedMessage id
='poll.refresh' defaultMessage
='Refresh' /></button
> · </span
>}
180 {poll
.get('expires_at') && <span
> · {timeRemaining
}</span
>}