about summary refs log tree commit diff
diff options
context:
space:
mode:
authorOndřej Hruška <ondra@ondrovo.com>2017-10-21 20:24:53 +0200
committerGitHub <noreply@github.com>2017-10-21 20:24:53 +0200
commitd589dd7cd0512b558412a38a935b1a9cdcbf0ce7 (patch)
treef70869f4b0162c3faac2ec7c16c8da8bc7b17d2e
parenta7be86e875cb0eb38ca2140306f84267d819905c (diff)
Compose buttons bar redesign + generalize dropdown (#194)
* Generalize compose dropdown for re-use

* wip stuffs

* new tootbox look and removed old doodle button files

* use the house icon for ...
-rw-r--r--app/javascript/glitch/components/compose/advanced_options/index.js94
-rw-r--r--app/javascript/glitch/components/compose/attach_options/index.js133
-rw-r--r--app/javascript/glitch/components/compose/dropdown/index.js77
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js11
-rw-r--r--app/javascript/mastodon/features/compose/components/doodle_button.js41
-rw-r--r--app/javascript/mastodon/features/compose/containers/doodle_button_container.js15
-rw-r--r--app/javascript/styles/mastodon/components.scss5
7 files changed, 228 insertions, 148 deletions
diff --git a/app/javascript/glitch/components/compose/advanced_options/index.js b/app/javascript/glitch/components/compose/advanced_options/index.js
index b745d1cdf..8251baf4d 100644
--- a/app/javascript/glitch/components/compose/advanced_options/index.js
+++ b/app/javascript/glitch/components/compose/advanced_options/index.js
@@ -47,11 +47,9 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { injectIntl, defineMessages } from 'react-intl';
 
-//  Mastodon imports  //
-import IconButton from '../../../../mastodon/components/icon_button';
-
 //  Our imports  //
 import ComposeAdvancedOptionsToggle from './toggle';
+import ComposeDropdown from '../dropdown/index';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
@@ -77,11 +75,6 @@ const messages = defineMessages({
     { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
 });
 
-const iconStyle = {
-  height     : null,
-  lineHeight : '27px',
-};
-
 /*
 
 Implementation:
@@ -100,67 +93,6 @@ export default class ComposeAdvancedOptions extends React.PureComponent {
     intl     : PropTypes.object.isRequired,
   };
 
-  state = {
-    open: false,
-  };
-
-/*
-
-###  `onToggleDropdown()`
-
-This function toggles the opening and closing of the advanced options
-dropdown.
-
-*/
-
-  onToggleDropdown = () => {
-    this.setState({ open: !this.state.open });
-  };
-
-/*
-
-###  `onGlobalClick(e)`
-
-This function closes the advanced options dropdown if you click
-anywhere else on the screen.
-
-*/
-
-  onGlobalClick = (e) => {
-    if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
-      this.setState({ open: false });
-    }
-  }
-
-/*
-
-###  `componentDidMount()`, `componentWillUnmount()`
-
-This function closes the advanced options dropdown if you click
-anywhere else on the screen.
-
-*/
-
-  componentDidMount () {
-    window.addEventListener('click', this.onGlobalClick);
-    window.addEventListener('touchstart', this.onGlobalClick);
-  }
-  componentWillUnmount () {
-    window.removeEventListener('click', this.onGlobalClick);
-    window.removeEventListener('touchstart', this.onGlobalClick);
-  }
-
-/*
-
-###  `setRef(c)`
-
-`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
-
-*/
-
-  setRef = (c) => {
-    this.node = c;
-  }
 
 /*
 
@@ -171,7 +103,6 @@ anywhere else on the screen.
 */
 
   render () {
-    const { open } = this.state;
     const { intl, values } = this.props;
 
 /*
@@ -218,23 +149,14 @@ toggle as its `key` so that React can keep track of it.
 Finally, we can render our component.
 
 */
-
     return (
-      <div ref={this.setRef} className={`advanced-options-dropdown ${open ?  'open' : ''} ${anyEnabled ? 'active' : ''} `}>
-        <div className='advanced-options-dropdown__value'>
-          <IconButton
-            className='advanced-options-dropdown__value'
-            title={intl.formatMessage(messages.advanced_options_icon_title)}
-            icon='ellipsis-h' active={open || anyEnabled}
-            size={18}
-            style={iconStyle}
-            onClick={this.onToggleDropdown}
-          />
-        </div>
-        <div className='advanced-options-dropdown__dropdown'>
-          {optionElems}
-        </div>
-      </div>
+      <ComposeDropdown
+        title={intl.formatMessage(messages.advanced_options_icon_title)}
+        icon='home'
+        highlight={anyEnabled}
+      >
+        {optionElems}
+      </ComposeDropdown>
     );
   }
 
diff --git a/app/javascript/glitch/components/compose/attach_options/index.js b/app/javascript/glitch/components/compose/attach_options/index.js
new file mode 100644
index 000000000..4340972f0
--- /dev/null
+++ b/app/javascript/glitch/components/compose/attach_options/index.js
@@ -0,0 +1,133 @@
+//  Package imports  //
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { injectIntl, defineMessages } from 'react-intl';
+
+//  Our imports  //
+import ComposeDropdown from '../dropdown/index';
+import { uploadCompose } from '../../../../mastodon/actions/compose';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { openModal } from '../../../../mastodon/actions/modal';
+
+//  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+const messages = defineMessages({
+  upload :
+    { id: 'compose.attach.upload', defaultMessage: 'Upload a file' },
+  doodle :
+    { id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
+  attach :
+    { id: 'compose.attach', defaultMessage: 'Attach...' },
+});
+
+const mapStateToProps = state => ({
+  // This horrible expression is copied from vanilla upload_button_container
+  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
+  resetFileKey: state.getIn(['compose', 'resetFileKey']),
+  acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSelectFile (files) {
+    dispatch(uploadCompose(files));
+  },
+  onOpenDoodle () {
+    dispatch(openModal('DOODLE', { noEsc: true }));
+  },
+});
+
+@injectIntl
+@connect(mapStateToProps, mapDispatchToProps)
+export default class ComposeAttachOptions extends ImmutablePureComponent {
+
+  static propTypes = {
+    intl     : PropTypes.object.isRequired,
+    resetFileKey: PropTypes.number,
+    acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
+    disabled: PropTypes.bool,
+    onSelectFile: PropTypes.func.isRequired,
+    onOpenDoodle: PropTypes.func.isRequired,
+  };
+
+  handleItemClick = bt => {
+    if (bt === 'upload') {
+      this.fileElement.click();
+    }
+
+    if (bt === 'doodle') {
+      this.props.onOpenDoodle();
+    }
+
+    this.dropdown.setState({ open: false });
+  };
+
+  handleFileChange = (e) => {
+    if (e.target.files.length > 0) {
+      this.props.onSelectFile(e.target.files);
+    }
+  }
+
+  setFileRef = (c) => {
+    this.fileElement = c;
+  }
+
+  setDropdownRef = (c) => {
+    this.dropdown = c;
+  }
+
+  render () {
+    const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
+
+    const options = [
+      { icon: 'cloud-upload', text: messages.upload, name: 'upload' },
+      { icon: 'paint-brush', text: messages.doodle, name: 'doodle' },
+    ];
+
+    const optionElems = options.map((item) => {
+      const hdl = () => this.handleItemClick(item.name);
+      return (
+        <div
+          role='button'
+          tabIndex='0'
+          key={item.name}
+          onClick={hdl}
+          className='privacy-dropdown__option'
+        >
+          <div className='privacy-dropdown__option__icon'>
+            <i className={`fa fa-fw fa-${item.icon}`} />
+          </div>
+
+          <div className='privacy-dropdown__option__content'>
+            <strong>{intl.formatMessage(item.text)}</strong>
+          </div>
+        </div>
+      );
+    });
+
+    return (
+      <div>
+        <ComposeDropdown
+          title={intl.formatMessage(messages.attach)}
+          icon='paperclip'
+          disabled={disabled}
+          ref={this.setDropdownRef}
+        >
+          {optionElems}
+        </ComposeDropdown>
+        <input
+          key={resetFileKey}
+          ref={this.setFileRef}
+          type='file'
+          multiple={false}
+          accept={acceptContentTypes.toArray().join(',')}
+          onChange={this.handleFileChange}
+          disabled={disabled}
+          style={{ display: 'none' }}
+        />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/glitch/components/compose/dropdown/index.js b/app/javascript/glitch/components/compose/dropdown/index.js
new file mode 100644
index 000000000..5f6467155
--- /dev/null
+++ b/app/javascript/glitch/components/compose/dropdown/index.js
@@ -0,0 +1,77 @@
+//  Package imports  //
+import React from 'react';
+import PropTypes from 'prop-types';
+
+//  Mastodon imports  //
+import IconButton from '../../../../mastodon/components/icon_button';
+
+const iconStyle = {
+  height     : null,
+  lineHeight : '27px',
+};
+
+export default class ComposeDropdown extends React.PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    icon: PropTypes.string,
+    highlight: PropTypes.bool,
+    disabled: PropTypes.bool,
+    children: PropTypes.arrayOf(PropTypes.node).isRequired,
+  };
+
+  state = {
+    open: false,
+  };
+
+  onGlobalClick = (e) => {
+    if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
+      this.setState({ open: false });
+    }
+  };
+
+  componentDidMount () {
+    window.addEventListener('click', this.onGlobalClick);
+    window.addEventListener('touchstart', this.onGlobalClick);
+  }
+  componentWillUnmount () {
+    window.removeEventListener('click', this.onGlobalClick);
+    window.removeEventListener('touchstart', this.onGlobalClick);
+  }
+
+  onToggleDropdown = () => {
+    if (this.props.disabled) return;
+    this.setState({ open: !this.state.open });
+  };
+
+  setRef = (c) => {
+    this.node = c;
+  };
+
+  render () {
+    const { open } = this.state;
+    let { highlight, title, icon, disabled } = this.props;
+
+    if (!icon) icon = 'ellipsis-h';
+
+    return (
+      <div ref={this.setRef} className={`advanced-options-dropdown ${open ?  'open' : ''} ${highlight ? 'active' : ''} `}>
+        <div className='advanced-options-dropdown__value'>
+          <IconButton
+            className={'inverted'}
+            title={title}
+            icon={icon} active={open || highlight}
+            size={18}
+            style={iconStyle}
+            disabled={disabled}
+            onClick={this.onToggleDropdown}
+          />
+        </div>
+        <div className='advanced-options-dropdown__dropdown'>
+          {this.props.children}
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 17f5bde4b..4b393bf8b 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -5,8 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
-import UploadButtonContainer from '../containers/upload_button_container';
-import DoodleButtonContainer from '../containers/doodle_button_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import Collapsable from '../../../components/collapsable';
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
@@ -20,6 +18,7 @@ import { isMobile } from '../../../is_mobile';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { length } from 'stringz';
 import { countableText } from '../util/counter';
+import ComposeAttachOptions from '../../../../glitch/components/compose/attach_options/index';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -241,12 +240,12 @@ export default class ComposeForm extends ImmutablePureComponent {
         </div>
 
         <div className='compose-form__buttons'>
-          <UploadButtonContainer />
-          <DoodleButtonContainer />
-          <PrivacyDropdownContainer />
-          <ComposeAdvancedOptionsContainer />
+          <ComposeAttachOptions />
           <SensitiveButtonContainer />
+          <div className='compose-form__buttons-separator' />
+          <PrivacyDropdownContainer />
           <SpoilerButtonContainer />
+          <ComposeAdvancedOptionsContainer />
         </div>
 
         <div className='compose-form__publish'>
diff --git a/app/javascript/mastodon/features/compose/components/doodle_button.js b/app/javascript/mastodon/features/compose/components/doodle_button.js
deleted file mode 100644
index 0af02458f..000000000
--- a/app/javascript/mastodon/features/compose/components/doodle_button.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-const messages = defineMessages({
-  doodle: { id: 'doodle_button.label', defaultMessage: 'Add a drawing' },
-});
-
-const iconStyle = {
-  height: null,
-  lineHeight: '27px',
-};
-
-@injectIntl
-export default class UploadButton extends ImmutablePureComponent {
-
-  static propTypes = {
-    disabled: PropTypes.bool,
-    onOpenCanvas: PropTypes.func.isRequired,
-    style: PropTypes.object,
-    intl: PropTypes.object.isRequired,
-  };
-
-  handleClick = () => {
-    this.props.onOpenCanvas();
-  }
-
-  render () {
-
-    const { intl, disabled } = this.props;
-
-    return (
-      <div className='compose-form__upload-button'>
-        <IconButton icon='pencil' title={intl.formatMessage(messages.doodle)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/compose/containers/doodle_button_container.js b/app/javascript/mastodon/features/compose/containers/doodle_button_container.js
deleted file mode 100644
index 5ada4514f..000000000
--- a/app/javascript/mastodon/features/compose/containers/doodle_button_container.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { connect } from 'react-redux';
-import DoodleButton from '../components/doodle_button';
-import { openModal } from '../../../actions/modal';
-
-const mapStateToProps = state => ({
-  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
-});
-
-const mapDispatchToProps = dispatch => ({
-  onOpenCanvas () {
-    dispatch(openModal('DOODLE', { noEsc: true }));
-  },
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(DoodleButton);
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 6fe179581..306a0457d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -322,6 +322,11 @@
   }
 }
 
+.compose-form__buttons-separator {
+  border-left: 1px solid #c3c3c3;
+  margin: 0 3px;
+}
+
 .compose-form__upload-button-icon {
   line-height: 27px;
 }