about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/composer
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer')
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js52
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js35
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js121
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/index.js29
4 files changed, 218 insertions, 19 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index ec0e405a4..9d2e0b3da 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -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}
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
index 5b4a7444c..80ac1b63a 100644
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/index.js
@@ -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,
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
new file mode 100644
index 000000000..c329b04cd
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
@@ -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>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/index.js b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
new file mode 100644
index 000000000..5232c3b31
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
@@ -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);