about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-09-23 01:41:00 +0200
committerGitHub <noreply@github.com>2017-09-23 01:41:00 +0200
commit846cd4e8381c891816cf814582304b534db4ee5f (patch)
treefb7c718bd3071bf5128f6c270901623aab54a0f0 /app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
parent0de82dd316839ed329504bfbf9bd0f2d3d96e654 (diff)
Switch from EmojiOne to Twemoji, different emoji picker (#5046)
* Switch from EmojiOne to Twemoji, different emoji picker

* Make emoji-mart use a local spritesheet

* Fix emojify test

* yarn manage:translations
Diffstat (limited to 'app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js')
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js324
1 files changed, 258 insertions, 66 deletions
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index 9d05b7a34..43e175be5 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -1,12 +1,17 @@
 import React from 'react';
-import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
+import { Overlay } from 'react-overlays';
+import classNames from 'classnames';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
   emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
+  emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
+  custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
+  recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
+  search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
   people: { id: 'emoji_button.people', defaultMessage: 'People' },
   nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
   food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
@@ -17,13 +22,234 @@ const messages = defineMessages({
   flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
 });
 
-const settings = {
-  imageType: 'png',
-  sprites: false,
-  imagePathPNG: '/emoji/',
-};
+const assetHost = process.env.CDN_HOST || '';
 
-let EmojiPicker; // load asynchronously
+let EmojiPicker, Emoji; // load asynchronously
+
+const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`;
+
+class ModifierPickerMenu extends React.PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    onSelect: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+
+  handleClick = (e) => {
+    const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1;
+    this.props.onSelect(modifier);
+  }
+
+  componentWillReceiveProps (nextProps) {
+    if (nextProps.active) {
+      this.attachListeners();
+    } else {
+      this.removeListeners();
+    }
+  }
+
+  componentWillUnmount () {
+    this.removeListeners();
+  }
+
+  handleDocumentClick = e => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+    }
+  }
+
+  attachListeners () {
+    document.addEventListener('click', this.handleDocumentClick, false);
+    document.addEventListener('touchend', this.handleDocumentClick, false);
+  }
+
+  removeListeners () {
+    document.removeEventListener('click', this.handleDocumentClick, false);
+    document.removeEventListener('touchend', this.handleDocumentClick, false);
+  }
+
+  setRef = c => {
+    this.node = c;
+  }
+
+  render () {
+    const { active } = this.props;
+
+    return (
+      <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
+        <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
+      </div>
+    );
+  }
+
+}
+
+class ModifierPicker extends React.PureComponent {
+
+  static propTypes = {
+    active: PropTypes.bool,
+    modifier: PropTypes.number,
+    onChange: PropTypes.func,
+    onClose: PropTypes.func,
+    onOpen: PropTypes.func,
+  };
+
+  handleClick = () => {
+    if (this.props.active) {
+      this.props.onClose();
+    } else {
+      this.props.onOpen();
+    }
+  }
+
+  handleSelect = modifier => {
+    this.props.onChange(modifier);
+    this.props.onClose();
+  }
+
+  render () {
+    const { active, modifier } = this.props;
+
+    return (
+      <div className='emoji-picker-dropdown__modifiers'>
+        <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
+        <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
+      </div>
+    );
+  }
+
+}
+
+@injectIntl
+class EmojiPickerMenu extends React.PureComponent {
+
+  static propTypes = {
+    loading: PropTypes.bool,
+    onClose: PropTypes.func.isRequired,
+    onPick: PropTypes.func.isRequired,
+    style: PropTypes.object,
+    placement: PropTypes.string,
+    arrowOffsetLeft: PropTypes.string,
+    arrowOffsetTop: PropTypes.string,
+    intl: PropTypes.object.isRequired,
+  };
+
+  static defaultProps = {
+    style: {},
+    loading: true,
+    placement: 'bottom',
+  };
+
+  state = {
+    modifierOpen: false,
+    modifier: 1,
+  };
+
+  handleDocumentClick = e => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+    }
+  }
+
+  componentDidMount () {
+    document.addEventListener('click', this.handleDocumentClick, false);
+    document.addEventListener('touchend', this.handleDocumentClick, false);
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, false);
+    document.removeEventListener('touchend', this.handleDocumentClick, false);
+  }
+
+  setRef = c => {
+    this.node = c;
+  }
+
+  getI18n = () => {
+    const { intl } = this.props;
+
+    return {
+      search: intl.formatMessage(messages.emoji_search),
+      notfound: intl.formatMessage(messages.emoji_not_found),
+      categories: {
+        search: intl.formatMessage(messages.search_results),
+        recent: intl.formatMessage(messages.recent),
+        people: intl.formatMessage(messages.people),
+        nature: intl.formatMessage(messages.nature),
+        foods: intl.formatMessage(messages.food),
+        activity: intl.formatMessage(messages.activity),
+        places: intl.formatMessage(messages.travel),
+        objects: intl.formatMessage(messages.objects),
+        symbols: intl.formatMessage(messages.symbols),
+        flags: intl.formatMessage(messages.flags),
+        custom: intl.formatMessage(messages.custom),
+      },
+    };
+  }
+
+  handleClick = emoji => {
+    this.props.onClose();
+    this.props.onPick(emoji);
+  }
+
+  handleModifierOpen = () => {
+    this.setState({ modifierOpen: true });
+  }
+
+  handleModifierClose = () => {
+    this.setState({ modifierOpen: false });
+  }
+
+  handleModifierChange = modifier => {
+    if (modifier !== this.state.modifier) {
+      this.setState({ modifier });
+    }
+  }
+
+  render () {
+    const { loading, style, intl } = this.props;
+
+    if (loading) {
+      return <div style={{ width: 299 }} />;
+    }
+
+    const title = intl.formatMessage(messages.emoji);
+    const { modifierOpen, modifier } = this.state;
+
+    return (
+      <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
+        <EmojiPicker
+          perLine={8}
+          emojiSize={22}
+          sheetSize={32}
+          color=''
+          emoji=''
+          set='twitter'
+          title={title}
+          i18n={this.getI18n()}
+          onClick={this.handleClick}
+          skin={modifier}
+          backgroundImageFn={backgroundImageFn}
+        />
+
+        <ModifierPicker
+          active={modifierOpen}
+          modifier={modifier}
+          onOpen={this.handleModifierOpen}
+          onClose={this.handleModifierClose}
+          onChange={this.handleModifierChange}
+        />
+      </div>
+    );
+  }
+
+}
 
 @injectIntl
 export default class EmojiPickerDropdown extends React.PureComponent {
@@ -42,20 +268,17 @@ export default class EmojiPickerDropdown extends React.PureComponent {
     this.dropdown = c;
   }
 
-  handleChange = (data) => {
-    this.dropdown.hide();
-    this.props.onPickEmoji(data);
-  }
-
   onShowDropdown = () => {
     this.setState({ active: true });
+
     if (!EmojiPicker) {
       this.setState({ loading: true });
-      EmojiPickerAsync().then(TheEmojiPicker => {
-        EmojiPicker = TheEmojiPicker.default;
+
+      EmojiPickerAsync().then(EmojiMart => {
+        EmojiPicker = EmojiMart.Picker;
+        Emoji = EmojiMart.Emoji;
         this.setState({ loading: false });
       }).catch(() => {
-        // TODO: show the user an error?
         this.setState({ loading: false });
       });
     }
@@ -75,70 +298,39 @@ export default class EmojiPickerDropdown extends React.PureComponent {
     }
   }
 
-  onEmojiPickerKeyDown = (e) => {
+  handleKeyDown = e => {
     if (e.key === 'Escape') {
       this.onHideDropdown();
     }
   }
 
-  render () {
-    const { intl } = this.props;
+  setTargetRef = c => {
+    this.target = c;
+  }
 
-    const categories = {
-      people: {
-        title: intl.formatMessage(messages.people),
-        emoji: 'smile',
-      },
-      nature: {
-        title: intl.formatMessage(messages.nature),
-        emoji: 'hamster',
-      },
-      food: {
-        title: intl.formatMessage(messages.food),
-        emoji: 'pizza',
-      },
-      activity: {
-        title: intl.formatMessage(messages.activity),
-        emoji: 'soccer',
-      },
-      travel: {
-        title: intl.formatMessage(messages.travel),
-        emoji: 'earth_americas',
-      },
-      objects: {
-        title: intl.formatMessage(messages.objects),
-        emoji: 'bulb',
-      },
-      symbols: {
-        title: intl.formatMessage(messages.symbols),
-        emoji: 'clock9',
-      },
-      flags: {
-        title: intl.formatMessage(messages.flags),
-        emoji: 'flag_gb',
-      },
-    };
+  findTarget = () => {
+    return this.target;
+  }
 
-    const { active, loading } = this.state;
+  render () {
+    const { intl, onPickEmoji } = this.props;
     const title = intl.formatMessage(messages.emoji);
+    const { active, loading } = this.state;
 
     return (
-      <Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
-        <DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} >
+      <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
+        <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
           <img
-            className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
+            className={classNames('emojione', { 'pulse-loading': active && loading })}
             alt='🙂'
-            src='/emoji/1f602.svg'
+            src={`${assetHost}/emoji/1f602.svg`}
           />
-        </DropdownTrigger>
-
-        <DropdownContent className='dropdown__left'>
-          {
-            this.state.active && !this.state.loading &&
-            (<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />)
-          }
-        </DropdownContent>
-      </Dropdown>
+        </div>
+
+        <Overlay show={active} placement='bottom' target={this.findTarget}>
+          <EmojiPickerMenu loading={loading} onClose={this.onHideDropdown} onPick={onPickEmoji} />
+        </Overlay>
+      </div>
     );
   }