[Glitch] Port polls creation UI from upstream

This commit is contained in:
Thibaut Girka
2019-03-06 12:30:11 +01:00
committed by ThibG
parent 3e5a0bc825
commit 8fe86cebaa
9 changed files with 403 additions and 20 deletions

View File

@@ -31,6 +31,7 @@ import {
openModal,
} from 'flavours/glitch/actions/modal';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { addPoll, removePoll } from 'flavours/glitch/actions/compose';
// Components.
import ComposerOptions from './options';
@@ -39,6 +40,7 @@ import ComposerReply from './reply';
import ComposerSpoiler from './spoiler';
import ComposerTextarea from './textarea';
import ComposerUploadForm from './upload_form';
import ComposerPollForm from './poll_form';
import ComposerWarning from './warning';
import ComposerHashtagWarning from './hashtag_warning';
import ComposerDirectWarning from './direct_warning';
@@ -102,6 +104,7 @@ function mapStateToProps (state) {
suggestions: state.getIn(['compose', 'suggestions']),
text: state.getIn(['compose', 'text']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
poll: state.getIn(['compose', 'poll']),
spoilersAlwaysOn: spoilersAlwaysOn,
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
@@ -134,6 +137,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onChangeVisibility(value) {
dispatch(changeComposeVisibility(value));
},
onTogglePoll() {
dispatch((_, getState) => {
if (getState().getIn(['compose', 'poll'])) {
dispatch(removePoll());
} else {
dispatch(addPoll());
}
});
},
onClearSuggestions() {
dispatch(clearComposeSuggestions());
},
@@ -394,6 +406,7 @@ class Composer extends React.Component {
isUploading,
layout,
media,
poll,
onCancelReply,
onChangeAdvancedOption,
onChangeDescription,
@@ -401,6 +414,7 @@ class Composer extends React.Component {
onChangeSpoilerness,
onChangeText,
onChangeVisibility,
onTogglePoll,
onClearSuggestions,
onCloseModal,
onFetchSuggestions,
@@ -463,30 +477,38 @@ class Composer extends React.Component {
suggestions={suggestions}
value={text}
/>
{isUploading || media && media.size ? (
<ComposerUploadForm
intl={intl}
media={media}
onChangeDescription={onChangeDescription}
onOpenFocalPointModal={onOpenFocalPointModal}
onRemove={onUndoUpload}
progress={progress}
uploading={isUploading}
handleRef={handleRefUploadForm}
/>
) : null}
<div className='compose-form__modifiers'>
{isUploading || media && media.size ? (
<ComposerUploadForm
intl={intl}
media={media}
onChangeDescription={onChangeDescription}
onOpenFocalPointModal={onOpenFocalPointModal}
onRemove={onUndoUpload}
progress={progress}
uploading={isUploading}
handleRef={handleRefUploadForm}
/>
) : null}
{!!poll && (
<ComposerPollForm />
)}
</div>
<ComposerOptions
acceptContentTypes={acceptContentTypes}
advancedOptions={advancedOptions}
disabled={isSubmitting}
full={media ? media.size >= 4 || media.some(
item => item.get('type') === 'video'
) : false}
allowMedia={!poll && (media ? media.size < 4 && !media.some(
item => item.get('type') === 'video'
) : true)}
hasMedia={media && !!media.size}
allowPoll={!(media && !!media.size)}
hasPoll={!!poll}
intl={intl}
onChangeAdvancedOption={onChangeAdvancedOption}
onChangeSensitivity={onChangeSensitivity}
onChangeVisibility={onChangeVisibility}
onTogglePoll={onTogglePoll}
onDoodleOpen={onOpenDoodleModal}
onModalClose={onCloseModal}
onModalOpen={onOpenActionsModal}

View File

@@ -98,6 +98,14 @@ const messages = defineMessages({
defaultMessage: 'Upload a file',
id: 'compose.attach.upload',
},
add_poll: {
defaultMessage: 'Add a poll',
id: 'poll_button.add_poll',
},
remove_poll: {
defaultMessage: 'Remove poll',
id: 'poll_button.remove_poll',
},
});
// Handlers.
@@ -160,12 +168,15 @@ export default class ComposerOptions extends React.PureComponent {
acceptContentTypes,
advancedOptions,
disabled,
full,
allowMedia,
hasMedia,
allowPoll,
hasPoll,
intl,
onChangeAdvancedOption,
onChangeSensitivity,
onChangeVisibility,
onTogglePoll,
onModalClose,
onModalOpen,
onToggleSpoiler,
@@ -209,7 +220,7 @@ export default class ComposerOptions extends React.PureComponent {
<div className='composer--options'>
<input
accept={acceptContentTypes}
disabled={disabled || full}
disabled={disabled || !allowMedia}
key={resetFileKey}
onChange={handleChangeFiles}
ref={handleRefFileElement}
@@ -218,7 +229,7 @@ export default class ComposerOptions extends React.PureComponent {
{...hiddenComponent}
/>
<Dropdown
disabled={disabled || full}
disabled={disabled || !allowMedia}
icon='paperclip'
items={[
{
@@ -237,6 +248,19 @@ export default class ComposerOptions extends React.PureComponent {
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.attach)}
/>
<IconButton
active={hasPoll}
disabled={disabled || !allowPoll}
icon='tasks'
inverted
onClick={onTogglePoll}
size={18}
style={{
height: null,
lineHeight: null,
}}
title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
/>
<Motion
defaultStyle={{ scale: 0.87 }}
style={{
@@ -329,12 +353,15 @@ ComposerOptions.propTypes = {
acceptContentTypes: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
disabled: PropTypes.bool,
full: PropTypes.bool,
allowMedia: PropTypes.bool,
hasMedia: PropTypes.bool,
allowPoll: PropTypes.bool,
hasPoll: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChangeAdvancedOption: PropTypes.func,
onChangeSensitivity: PropTypes.func,
onChangeVisibility: PropTypes.func,
onTogglePoll: PropTypes.func,
onDoodleOpen: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,

View File

@@ -0,0 +1,121 @@
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 IconButton from 'flavours/glitch/components/icon_button';
import Icon from 'flavours/glitch/components/icon';
import classNames from 'classnames';
const messages = defineMessages({
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
});
@injectIntl
class Option extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
isPollMultiple: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleOptionTitleChange = e => {
this.props.onChange(this.props.index, e.target.value);
};
handleOptionRemove = () => {
this.props.onRemove(this.props.index);
};
render () {
const { isPollMultiple, title, index, intl } = this.props;
return (
<li>
<label className='poll__text editable'>
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
<input
type='text'
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
maxlength={25}
value={title}
onChange={this.handleOptionTitleChange}
/>
</label>
<div className='poll__cancel'>
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
</div>
</li>
);
}
}
export default
@injectIntl
class PollForm extends ImmutablePureComponent {
static propTypes = {
options: ImmutablePropTypes.list,
expiresIn: PropTypes.number,
isMultiple: PropTypes.bool,
onChangeOption: PropTypes.func.isRequired,
onAddOption: PropTypes.func.isRequired,
onRemoveOption: PropTypes.func.isRequired,
onChangeSettings: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleAddOption = () => {
this.props.onAddOption('');
};
handleSelectDuration = e => {
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
};
render () {
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
if (!options) {
return null;
}
return (
<div className='compose-form__poll-wrapper'>
<ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
</ul>
<div className='poll__footer'>
{options.size < 4 && (
<button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
)}
<select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
</select>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import PollForm from './components/poll_form';
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
const mapStateToProps = state => ({
options: state.getIn(['compose', 'poll', 'options']),
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
});
const mapDispatchToProps = dispatch => ({
onAddOption(title) {
dispatch(addPollOption(title));
},
onRemoveOption(index) {
dispatch(removePollOption(index));
},
onChangeOption(index, title) {
dispatch(changePollOption(index, title));
},
onChangeSettings(expiresIn, isMultiple) {
dispatch(changePollSettings(expiresIn, isMultiple));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);