about summary refs log tree commit diff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx10
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx28
-rw-r--r--app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx52
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx5
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx14
5 files changed, 105 insertions, 4 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index b7f225170..165e811e3 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -28,6 +28,8 @@ export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
 
+export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
+
 export function changeCompose(text) {
   return {
     type: COMPOSE_CHANGE,
@@ -260,3 +262,11 @@ export function changeComposeListability(checked) {
     checked
   };
 };
+
+export function insertEmojiCompose(position, emoji) {
+  return {
+    type: COMPOSE_EMOJI_INSERT,
+    position,
+    emoji
+  };
+};
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index bcc4fe1e7..047c974f2 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -15,6 +15,7 @@ import UnlistedToggleContainer from '../containers/unlisted_toggle_container';
 import SpoilerToggleContainer from '../containers/spoiler_toggle_container';
 import PrivateToggleContainer from '../containers/private_toggle_container';
 import SensitiveToggleContainer from '../containers/sensitive_toggle_container';
+import EmojiPickerDropdown from './emoji_picker_dropdown';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -48,6 +49,7 @@ const ComposeForm = React.createClass({
     onSuggestionSelected: React.PropTypes.func.isRequired,
     onChangeSpoilerText: React.PropTypes.func.isRequired,
     onPaste: React.PropTypes.func.isRequired,
+    onPickEmoji: React.PropTypes.func.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -76,6 +78,7 @@ const ComposeForm = React.createClass({
   },
 
   onSuggestionSelected (tokenStart, token, value) {
+    this._restoreCaret = null;
     this.props.onSuggestionSelected(tokenStart, token, value);
   },
 
@@ -88,8 +91,18 @@ const ComposeForm = React.createClass({
       // If replying to zero or one users, places the cursor at the end of the textbox.
       // If replying to more than one user, selects any usernames past the first;
       // this provides a convenient shortcut to drop everyone else from the conversation.
-      const selectionEnd   = this.props.text.length;
-      const selectionStart = (this.props.preselectDate !== prevProps.preselectDate) ? (this.props.text.search(/\s/) + 1) : selectionEnd;
+      let selectionEnd, selectionStart;
+
+      if (this.props.preselectDate !== prevProps.preselectDate) {
+        selectionEnd   = this.props.text.length;
+        selectionStart = this.props.text.search(/\s/) + 1;
+      } else if (typeof this._restoreCaret === 'number') {
+        selectionStart = this._restoreCaret;
+        selectionEnd   = this._restoreCaret;
+      } else {
+        selectionEnd   = this.props.text.length;
+        selectionStart = selectionEnd;
+      }
 
       this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
       this.autosuggestTextarea.textarea.focus();
@@ -100,6 +113,12 @@ const ComposeForm = React.createClass({
     this.autosuggestTextarea = c;
   },
 
+  handleEmojiPick (data) {
+    const position     = this.autosuggestTextarea.textarea.selectionStart;
+    this._restoreCaret = position + data.shortname.length + 1;
+    this.props.onPickEmoji(position, data);
+  },
+
   render () {
     const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
     const disabled = this.props.is_submitting || this.props.is_uploading;
@@ -156,7 +175,10 @@ const ComposeForm = React.createClass({
         <div style={{ marginTop: '10px', overflow: 'hidden' }}>
           <div style={{ float: 'right' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
           <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
-          <UploadButtonContainer style={{ paddingTop: '4px' }} />
+          <div style={{ display: 'flex', paddingTop: '4px' }}>
+            <UploadButtonContainer />
+            <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
+          </div>
         </div>
 
         <SpoilerToggleContainer />
diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
new file mode 100644
index 000000000..6419ff08a
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
@@ -0,0 +1,52 @@
+import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
+import EmojiPicker from 'emojione-picker';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  emoji: { id: 'emoji_button.label', defaultMessage: 'Emoji' }
+});
+
+const settings = {
+  imageType: 'png',
+  sprites: false,
+  imagePathPNG: '/emoji/'
+};
+
+const EmojiPickerDropdown = React.createClass({
+
+  propTypes: {
+    intl: React.PropTypes.object.isRequired,
+    onPickEmoji: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  setRef (c) {
+    this.dropdown = c;
+  },
+
+  handleChange (data) {
+    this.dropdown.hide();
+    this.props.onPickEmoji(data);
+  },
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Dropdown ref={this.setRef} style={{ marginLeft: '5px' }}>
+        <DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, marginTop: '-1px', display: 'block', marginLeft: '2px' }}>
+          <i className={`fa fa-smile-o`} style={{ verticalAlign: 'middle' }} />
+        </DropdownTrigger>
+
+        <DropdownContent>
+          <EmojiPicker emojione={settings} onChange={this.handleChange} />
+        </DropdownContent>
+      </Dropdown>
+    );
+  }
+
+});
+
+export default injectIntl(EmojiPickerDropdown);
diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
index a34201747..a67adbdd6 100644
--- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
@@ -9,6 +9,7 @@ import {
   fetchComposeSuggestions,
   selectComposeSuggestion,
   changeComposeSpoilerText,
+  insertEmojiCompose
 } from '../../../actions/compose';
 
 const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
@@ -70,6 +71,10 @@ const mapDispatchToProps = (dispatch) => ({
     dispatch(uploadCompose(files));
   },
 
+  onPickEmoji (position, data) {
+    dispatch(insertEmojiCompose(position, data));
+  },
+
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index dead5fd77..b0001351f 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -20,7 +20,8 @@ import {
   COMPOSE_SPOILERNESS_CHANGE,
   COMPOSE_SPOILER_TEXT_CHANGE,
   COMPOSE_VISIBILITY_CHANGE,
-  COMPOSE_LISTABILITY_CHANGE
+  COMPOSE_LISTABILITY_CHANGE,
+  COMPOSE_EMOJI_INSERT
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { STORE_HYDRATE } from '../actions/store';
@@ -105,6 +106,15 @@ const insertSuggestion = (state, position, token, completion) => {
   });
 };
 
+const insertEmoji = (state, position, emojiData) => {
+  const emoji = emojiData.shortname;
+
+  return state.withMutations(map => {
+    map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
+    map.set('focusDate', new Date());
+  });
+};
+
 export default function compose(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
@@ -177,6 +187,8 @@ export default function compose(state = initialState, action) {
     } else {
       return state;
     }
+  case COMPOSE_EMOJI_INSERT:
+    return insertEmoji(state, action.position, action.emoji);
   default:
     return state;
   }