about summary refs log tree commit diff
path: root/app/javascript
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
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')
-rw-r--r--app/javascript/mastodon/emoji.js4
-rw-r--r--app/javascript/mastodon/emojione_light.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js324
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js2
-rw-r--r--app/javascript/mastodon/locales/ar.json4
-rw-r--r--app/javascript/mastodon/locales/bg.json4
-rw-r--r--app/javascript/mastodon/locales/ca.json4
-rw-r--r--app/javascript/mastodon/locales/de.json4
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json25
-rw-r--r--app/javascript/mastodon/locales/en.json4
-rw-r--r--app/javascript/mastodon/locales/eo.json4
-rw-r--r--app/javascript/mastodon/locales/es.json4
-rw-r--r--app/javascript/mastodon/locales/fa.json4
-rw-r--r--app/javascript/mastodon/locales/fi.json4
-rw-r--r--app/javascript/mastodon/locales/fr.json4
-rw-r--r--app/javascript/mastodon/locales/he.json4
-rw-r--r--app/javascript/mastodon/locales/hr.json4
-rw-r--r--app/javascript/mastodon/locales/hu.json4
-rw-r--r--app/javascript/mastodon/locales/id.json4
-rw-r--r--app/javascript/mastodon/locales/io.json4
-rw-r--r--app/javascript/mastodon/locales/it.json4
-rw-r--r--app/javascript/mastodon/locales/ja.json4
-rw-r--r--app/javascript/mastodon/locales/ko.json4
-rw-r--r--app/javascript/mastodon/locales/nl.json11
-rw-r--r--app/javascript/mastodon/locales/no.json4
-rw-r--r--app/javascript/mastodon/locales/oc.json4
-rw-r--r--app/javascript/mastodon/locales/pl.json4
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json8
-rw-r--r--app/javascript/mastodon/locales/pt.json4
-rw-r--r--app/javascript/mastodon/locales/ru.json4
-rw-r--r--app/javascript/mastodon/locales/th.json4
-rw-r--r--app/javascript/mastodon/locales/tr.json4
-rw-r--r--app/javascript/mastodon/locales/uk.json4
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json4
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json4
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json4
-rw-r--r--app/javascript/mastodon/reducers/compose.js2
-rw-r--r--app/javascript/styles/application.scss1
-rw-r--r--app/javascript/styles/components.scss218
-rw-r--r--app/javascript/styles/emoji_picker.scss197
41 files changed, 640 insertions, 272 deletions
diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index 7a0dab9d1..407b21b4b 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -3,6 +3,8 @@ import Trie from 'substring-trie';
 
 const trie = new Trie(Object.keys(unicodeMapping));
 
+const assetHost = process.env.CDN_HOST || '';
+
 const emojify = (str, customEmojis = {}) => {
   let rtn = '';
   for (;;) {
@@ -37,7 +39,7 @@ const emojify = (str, customEmojis = {}) => {
       str = str.slice(i + 1);
     } else {
       const [filename, shortCode] = unicodeMapping[match];
-      rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
+      rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="${assetHost}/emoji/${filename}.svg" />`;
       str = str.slice(i + match.length);
     }
   }
diff --git a/app/javascript/mastodon/emojione_light.js b/app/javascript/mastodon/emojione_light.js
index 0d07d012f..ecba35d03 100644
--- a/app/javascript/mastodon/emojione_light.js
+++ b/app/javascript/mastodon/emojione_light.js
@@ -9,5 +9,5 @@ const excluded = ['®', '©', '™'];
 module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap)
   .filter(c => !excluded.includes(c))
   .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
-  .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] }))
+  .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname.replace(/^0+/g, ''), shortCode.slice(1, shortCode.length - 1)] }))
   .reduce((x, y) => Object.assign(x, y), { });
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 413142d81..bb747b611 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -138,7 +138,7 @@ export default class ComposeForm extends ImmutablePureComponent {
 
   handleEmojiPick = (data) => {
     const position     = this.autosuggestTextarea.textarea.selectionStart;
-    const emojiChar    = data.unicode.split('-').map(code => String.fromCodePoint(parseInt(code, 16))).join('');
+    const emojiChar    = data.native;
     this._restoreCaret = position + emojiChar.length + 1;
     this.props.onPickEmoji(position, data);
   }
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>
     );
   }
 
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index c0b93a3a1..bd2fca337 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -1,5 +1,5 @@
 export function EmojiPicker () {
-  return import(/* webpackChunkName: "emojione_picker" */'emojione-picker');
+  return import(/* webpackChunkName: "emoji_picker" */'emoji-mart');
 }
 
 export function Compose () {
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 3a6fa2874..e27c19f4e 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "الأنشطة",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "الأعلام",
   "emoji_button.food": "الطعام والشراب",
   "emoji_button.label": "أدرج إيموجي",
   "emoji_button.nature": "الطبيعة",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "أشياء",
   "emoji_button.people": "الناس",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "ابحث...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "رموز",
   "emoji_button.travel": "أماكن و أسفار",
   "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 9afe2d038..85ede7dfd 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 7d45b4d6b..901241322 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activitat",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Menjar i Beure",
   "emoji_button.label": "Inserir emoji",
   "emoji_button.nature": "Natura",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objectes",
   "emoji_button.people": "Gent",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Cercar...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Símbols",
   "emoji_button.travel": "Viatges i Llocs",
   "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 712c635c8..c5fbc3a4c 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, in dem du den folgenden Code einfügst.",
   "embed.preview": "So wird es aussehen:",
   "emoji_button.activity": "Aktivitäten",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flaggen",
   "emoji_button.food": "Essen und Trinken",
   "emoji_button.label": "Emoji einfügen",
   "emoji_button.nature": "Natur",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Dinge",
   "emoji_button.people": "Leute",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Suche…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reise und Orte",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 42df796a7..b9a951a7f 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -517,6 +517,22 @@
         "id": "emoji_button.search"
       },
       {
+        "defaultMessage": "No emojos!! (╯°□°)╯︵ ┻━┻",
+        "id": "emoji_button.not_found"
+      },
+      {
+        "defaultMessage": "Custom",
+        "id": "emoji_button.custom"
+      },
+      {
+        "defaultMessage": "Frequently used",
+        "id": "emoji_button.recent"
+      },
+      {
+        "defaultMessage": "Search results",
+        "id": "emoji_button.search_results"
+      },
+      {
         "defaultMessage": "People",
         "id": "emoji_button.people"
       },
@@ -1334,15 +1350,6 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Close",
-        "id": "lightbox.close"
-      }
-    ],
-    "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
-  },
-  {
-    "descriptors": [
-      {
         "defaultMessage": "Play",
         "id": "video.play"
       },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 436079aeb..7ce57c4fe 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 945fcd8e0..8dfb05bc3 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 5182b5094..0489da64d 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
   "embed.preview": "Así es como se verá:",
   "emoji_button.activity": "Actividad",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Marcas",
   "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Naturaleza",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Gente",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Buscar…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
   "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 23f4a41d6..64a9172cf 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -67,13 +67,17 @@
   "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
   "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
   "emoji_button.activity": "فعالیت",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "پرچم‌ها",
   "emoji_button.food": "غذا و نوشیدنی",
   "emoji_button.label": "افزودن شکلک",
   "emoji_button.nature": "طبیعت",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "اشیا",
   "emoji_button.people": "مردم",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "جستجو...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "نمادها",
   "emoji_button.travel": "سفر و مکان",
   "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index fc409a932..ea8433107 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 5a436891b..a4ecc6c17 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
   "embed.preview": "Il apparaîtra comme cela : ",
   "emoji_button.activity": "Activités",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
   "emoji_button.label": "Insérer un emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objets",
   "emoji_button.people": "Personnages",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Recherche…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux et voyages",
   "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 06b401d39..dbba9d3bc 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "פעילות",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "דגלים",
   "emoji_button.food": "אוכל ושתיה",
   "emoji_button.label": "הוספת אמוג'י",
   "emoji_button.nature": "טבע",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "חפצים",
   "emoji_button.people": "אנשים",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "חיפוש...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "סמלים",
   "emoji_button.travel": "טיולים ואתרים",
   "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index cb28ce9c1..4c877d95b 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivnost",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Zastave",
   "emoji_button.food": "Hrana & Piće",
   "emoji_button.label": "Umetni smajlije",
   "emoji_button.nature": "Priroda",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekti",
   "emoji_button.people": "Ljudi",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Traži...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Simboli",
   "emoji_button.travel": "Putovanja & Mjesta",
   "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index a13e4fee2..699fc265d 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 349423cce..cbacc68ff 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivitas",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Bendera",
   "emoji_button.food": "Makanan & Minuman",
   "emoji_button.label": "Tambahkan emoji",
   "emoji_button.nature": "Alam",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Benda-benda",
   "emoji_button.people": "Orang",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Cari...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Simbol",
   "emoji_button.travel": "Tempat Wisata",
   "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 5f19509e2..9b2cc27d3 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index cedbb947c..2106e59c7 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Inserisci emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index e78ac4c26..013cd6ef1 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -67,13 +67,17 @@
   "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
   "embed.preview": "表示例:",
   "emoji_button.activity": "活動",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "国旗",
   "emoji_button.food": "食べ物",
   "emoji_button.label": "絵文字を追加",
   "emoji_button.nature": "自然",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物",
   "emoji_button.people": "人々",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "検索...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "記号",
   "emoji_button.travel": "旅行と場所",
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 46ed772cf..57d74ea6f 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "활동",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "국기",
   "emoji_button.food": "음식",
   "emoji_button.label": "emoji를 추가",
   "emoji_button.nature": "자연",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "물건",
   "emoji_button.people": "사람들",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "검색...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "기호",
   "emoji_button.travel": "여행과 장소",
   "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index b696bccfd..c9f837b20 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -33,9 +33,8 @@
   "column.home": "Start",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
-  "column.pins": "Pinned toot",
-  "column.public": "Globale tijdlijn",
   "column.pins": "Vastgezette toots",
+  "column.public": "Globale tijdlijn",
   "column_back_button.label": "terug",
   "column_header.hide_settings": "Instellingen verbergen",
   "column_header.moveLeft_settings": "Kolom naar links verplaatsen",
@@ -68,13 +67,17 @@
   "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
   "embed.preview": "Zo komt het eruit te zien:",
   "emoji_button.activity": "Activiteiten",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Vlaggen",
   "emoji_button.food": "Eten en drinken",
   "emoji_button.label": "Emoji toevoegen",
   "emoji_button.nature": "Natuur",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Voorwerpen",
   "emoji_button.people": "Mensen",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Zoeken...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbolen",
   "emoji_button.travel": "Reizen en plekken",
   "empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
@@ -87,7 +90,6 @@
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.appsshort": "Apps",
-  "getting_started.donate": "Doneren",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Beginnen",
   "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
@@ -112,10 +114,9 @@
   "navigation_bar.info": "Uitgebreide informatie",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "Vastgezette toots",
   "navigation_bar.preferences": "Instellingen",
   "navigation_bar.public_timeline": "Globale tijdlijn",
-  "navigation_bar.pins": "Vastgezette toots",
   "notification.favourite": "{name} markeerde jouw toot als favoriet",
   "notification.follow": "{name} volgt jou nu",
   "notification.mention": "{name} vermeldde jou",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 742017c66..a34ee51f4 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivitet",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flagg",
   "emoji_button.food": "Mat og drikke",
   "emoji_button.label": "Sett inn emoji",
   "emoji_button.nature": "Natur",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekter",
   "emoji_button.people": "Mennesker",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Søk...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Reise & steder",
   "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 512e4120d..81babfb2b 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
   "embed.preview": "Semblarà aquò : ",
   "emoji_button.activity": "Activitats",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Drapèus",
   "emoji_button.food": "Beure e manjar",
   "emoji_button.label": "Inserir un emoji",
   "emoji_button.nature": "Natura",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objèctes",
   "emoji_button.people": "Gents",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Cercar…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Simbòls",
   "emoji_button.travel": "Viatges & lòcs",
   "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 1d2443690..a35668b6b 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Osadź ten status na swojej stronie wklejając poniższy kod.",
   "embed.preview": "Tak będzie to wyglądać:",
   "emoji_button.activity": "Aktywność",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flagi",
   "emoji_button.food": "Żywność i napoje",
   "emoji_button.label": "Wstaw emoji",
   "emoji_button.nature": "Natura",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekty",
   "emoji_button.people": "Ludzie",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Szukaj…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Podróże i miejsca",
   "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index a5def0ad0..7ef7a9bc5 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -35,7 +35,6 @@
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
   "column.public": "Global",
-  "column.pins": "Postagens fixadas",
   "column_back_button.label": "Voltar",
   "column_header.hide_settings": "Esconder configurações",
   "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@@ -68,13 +67,17 @@
   "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
   "embed.preview": "Aqui está uma previsão de como ficará:",
   "emoji_button.activity": "Atividades",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Bandeiras",
   "emoji_button.food": "Comidas & Bebidas",
   "emoji_button.label": "Inserir Emoji",
   "emoji_button.nature": "Natureza",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Pessoas",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Buscar...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
@@ -114,9 +117,6 @@
   "navigation_bar.pins": "Postagens fixadas",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
-  "navigation_bar.preferences": "Preferências",
-  "navigation_bar.public_timeline": "Global",
-  "navigation_bar.pins": "Postagens fixadas",
   "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
   "notification.follow": "{name} te seguiu",
   "notification.mention": "{name} te mencionou",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index cff528f83..7f9681fff 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Inserir Emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index fcc147c87..f6094d630 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Занятия",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Флаги",
   "emoji_button.food": "Еда и напитки",
   "emoji_button.label": "Вставить эмодзи",
   "emoji_button.nature": "Природа",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Предметы",
   "emoji_button.people": "Люди",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Найти...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Символы",
   "emoji_button.travel": "Путешествия",
   "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index f2752f5e0..c1b6b74e4 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Food & Drink",
   "emoji_button.label": "Insert emoji",
   "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objects",
   "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 2676b851c..b9b249bc0 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Aktivite",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Bayraklar",
   "emoji_button.food": "Yiyecek ve İçecek",
   "emoji_button.label": "Emoji ekle",
   "emoji_button.nature": "Doğa",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Nesneler",
   "emoji_button.people": "İnsanlar",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Emoji ara...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Semboller",
   "emoji_button.travel": "Seyahat ve Yerler",
   "empty_column.community": "Yerel zaman tüneliniz boş. Daha fazla eğlence için herkese açık bir gönderi paylaşın.",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 6b5ab64ef..9d3e7ec7c 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -67,13 +67,17 @@
   "embed.instructions": "Embed this status on your website by copying the code below.",
   "embed.preview": "Here is what it will look like:",
   "emoji_button.activity": "Заняття",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "Прапори",
   "emoji_button.food": "Їжа та напої",
   "emoji_button.label": "Вставити емодзі",
   "emoji_button.nature": "Природа",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Предмети",
   "emoji_button.people": "Люди",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "Знайти...",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "Символи",
   "emoji_button.travel": "Подорожі",
   "empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 6037e7581..70589adfb 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -67,13 +67,17 @@
   "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
   "embed.preview": "到时大概长这样:",
   "emoji_button.activity": "活动",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "旗帜",
   "emoji_button.food": "食物和饮料",
   "emoji_button.label": "加入表情符号",
   "emoji_button.nature": "自然",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物体",
   "emoji_button.people": "人物",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "搜索…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "符号",
   "emoji_button.travel": "旅途和地点",
   "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 66d32fb7e..e702a385e 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -67,13 +67,17 @@
   "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
   "embed.preview": "看上去會是這樣:",
   "emoji_button.activity": "活動",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "飲飲食食",
   "emoji_button.label": "加入表情符號",
   "emoji_button.nature": "自然",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物品",
   "emoji_button.people": "人物",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "搜尋…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊景物",
   "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index b3cc6add7..964ce15c0 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -67,13 +67,17 @@
   "embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。",
   "embed.preview": "看上去會變成這樣:",
   "emoji_button.activity": "活動",
+  "emoji_button.custom": "Custom",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "食物與飲料",
   "emoji_button.label": "插入表情符號",
   "emoji_button.nature": "自然",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物件",
   "emoji_button.people": "人",
+  "emoji_button.recent": "Frequently used",
   "emoji_button.search": "搜尋…",
+  "emoji_button.search_results": "Search results",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊與地點",
   "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 34f5dab7f..526dbd0c5 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -120,7 +120,7 @@ const insertSuggestion = (state, position, token, completion) => {
 };
 
 const insertEmoji = (state, position, emojiData) => {
-  const emoji = emojiData.unicode.split('-').map(code => String.fromCodePoint(parseInt(code, 16))).join('');
+  const emoji = emojiData.native;
 
   return state.withMutations(map => {
     map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index f418ba699..0eb6ac6d8 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -15,6 +15,7 @@
 @import 'accounts';
 @import 'stream_entries';
 @import 'components';
+@import 'emoji_picker';
 @import 'about';
 @import 'tables';
 @import 'admin';
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index e0a310b6c..abf5cfd5b 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -390,17 +390,11 @@
 .compose-form__autosuggest-wrapper {
   position: relative;
 
-  .emoji-picker__dropdown {
+  .emoji-picker-dropdown {
     position: absolute;
     right: 5px;
     top: 5px;
 
-    &.dropdown--active::after {
-      border-color: transparent transparent $base-border-color;
-      bottom: -1px;
-      right: 8px;
-    }
-
     ::-webkit-scrollbar-track:hover,
     ::-webkit-scrollbar-track:active {
       background-color: rgba($base-overlay-background, 0.3);
@@ -2583,197 +2577,55 @@ button.icon-button.active i.fa-retweet {
   animation-direction: alternate;
 }
 
-.emoji-dialog {
-  width: 245px;
-  height: 270px;
+.emoji-picker-dropdown__menu {
   background: $simple-background-color;
-  box-sizing: border-box;
+  position: absolute;
+  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
   border-radius: 4px;
-  overflow: hidden;
-  position: relative;
-  box-shadow: 0 0 8px rgba($base-shadow-color, 0.2);
-
-  .emojione {
-    margin: 0;
-    width: 100%;
-    height: auto;
-  }
-
-  .emoji-dialog-header {
-    padding: 0 10px;
-
-    ul {
-      padding: 0;
-      margin: 0;
-      list-style: none;
-    }
-
-    li {
-      display: inline-block;
-      box-sizing: border-box;
-      padding: 10px 5px;
-      cursor: pointer;
-      border-bottom: 2px solid transparent;
-
-      .emoji {
-        width: 18px;
-        height: 18px;
-      }
-
-      img,
-      svg {
-        width: 18px;
-        height: 18px;
-        filter: grayscale(100%);
-      }
-
-      &:hover {
-        img,
-        svg {
-          filter: grayscale(0);
-        }
-      }
-
-      &.active {
-        border-bottom-color: $ui-highlight-color;
-
-        img,
-        svg {
-          filter: grayscale(0);
-        }
-      }
-    }
-  }
-
-  .emoji-row {
-    box-sizing: border-box;
-    overflow-y: hidden;
-    padding-left: 10px;
-
-    .emoji {
-      display: inline-block;
-      padding: 2.5px;
-      border-radius: 4px;
-    }
-  }
-
-  .emoji-category-header {
-    box-sizing: border-box;
-    overflow-y: hidden;
-    padding: 10px 8px 10px 16px;
-    display: table;
-
-    > * {
-      display: table-cell;
-      vertical-align: middle;
-    }
-  }
+  margin-top: 5px;
 
-  .emoji-category-title {
-    font-size: 12px;
-    text-transform: uppercase;
-    font-weight: 500;
-    color: darken($ui-secondary-color, 18%);
-    cursor: default;
+  .emoji-mart-scroll {
+    transition: opacity 200ms ease;
   }
 
-  .emoji-category-heading-decoration {
-    text-align: right;
+  &.selecting .emoji-mart-scroll {
+    opacity: 0.5;
   }
+}
 
-  .modifiers {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-    vertical-align: middle;
-    white-space: nowrap;
-    margin-top: 4px;
-
-    li {
-      display: inline-block;
-      padding: 0 2px;
-
-      &:last-of-type {
-        padding-right: 0;
-      }
-    }
-
-    .modifier {
-      display: inline-block;
-      border-radius: 10px;
-      width: 15px;
-      height: 15px;
-      position: relative;
-      cursor: pointer;
-
-      &.active::after {
-        content: "";
-        display: block;
-        position: absolute;
-        width: 7px;
-        height: 7px;
-        border-radius: 10px;
-        border: 2px solid $base-border-color;
-        top: 2px;
-        left: 2px;
-      }
-    }
-  }
+.emoji-picker-dropdown__modifiers {
+  position: absolute;
+  top: 60px;
+  right: 11px;
+  cursor: pointer;
+}
 
-  .emoji-search-wrapper {
-    padding: 10px;
-    border-bottom: 1px solid lighten($ui-secondary-color, 4%);
-  }
+.emoji-picker-dropdown__modifiers__menu {
+  position: absolute;
+  z-index: 4;
+  top: -4px;
+  left: -8px;
+  background: $simple-background-color;
+  border-radius: 4px;
+  box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
+  overflow: hidden;
 
-  .emoji-search {
-    font-size: 14px;
-    font-weight: 400;
-    padding: 7px 9px;
-    font-family: inherit;
+  button {
     display: block;
-    width: 100%;
-    background: rgba($ui-secondary-color, 0.3);
-    color: darken($ui-secondary-color, 18%);
-    border: 1px solid $ui-secondary-color;
-    border-radius: 4px;
-  }
-
-  .emoji-categories-wrapper {
-    position: absolute;
-    top: 42px;
-    bottom: 0;
-    left: 0;
-    right: 0;
-  }
-
-  .emoji-search-wrapper + .emoji-categories-wrapper {
-    top: 93px;
-  }
-
-  .emoji-row .emoji {
-    img,
-    svg {
-      transition: transform 60ms ease-in-out;
-    }
-
-    &:hover {
-      background: lighten($ui-secondary-color, 3%);
+    cursor: pointer;
+    border: 0;
+    padding: 4px 8px;
+    background: transparent;
 
-      img,
-      svg {
-        transform: translateZ(0) scale(1.2);
-      }
+    &:hover,
+    &:focus,
+    &:active {
+      background: rgba($ui-secondary-color, 0.4);
     }
   }
 
-  .emoji {
-    width: 22px;
+  .emoji-mart-emoji {
     height: 22px;
-    cursor: pointer;
-
-    &:focus {
-      outline: 0;
-    }
   }
 }
 
diff --git a/app/javascript/styles/emoji_picker.scss b/app/javascript/styles/emoji_picker.scss
new file mode 100644
index 000000000..dbd9dbd97
--- /dev/null
+++ b/app/javascript/styles/emoji_picker.scss
@@ -0,0 +1,197 @@
+.emoji-mart {
+  &,
+  * {
+    box-sizing: border-box;
+    line-height: 1.15;
+  }
+
+  font-size: 13px;
+  display: inline-block;
+  color: $ui-base-color;
+
+  .emoji-mart-emoji {
+    padding: 6px;
+  }
+}
+
+.emoji-mart-bar {
+  border: 0 solid darken($ui-secondary-color, 8%);
+
+  &:first-child {
+    border-bottom-width: 1px;
+    border-top-left-radius: 5px;
+    border-top-right-radius: 5px;
+    background: $ui-secondary-color;
+  }
+
+  &:last-child {
+    border-top-width: 1px;
+    border-bottom-left-radius: 5px;
+    border-bottom-right-radius: 5px;
+    display: none;
+  }
+}
+
+.emoji-mart-anchors {
+  display: flex;
+  justify-content: space-between;
+  padding: 0 6px;
+  color: $ui-primary-color;
+  line-height: 0;
+}
+
+.emoji-mart-anchor {
+  position: relative;
+  flex: 1;
+  text-align: center;
+  padding: 12px 4px;
+  overflow: hidden;
+  transition: color .1s ease-out;
+  cursor: pointer;
+
+  &:hover {
+    color: darken($ui-primary-color, 4%);
+  }
+}
+
+.emoji-mart-anchor-selected {
+  color: darken($ui-highlight-color, 3%);
+
+  &:hover {
+    color: darken($ui-highlight-color, 3%);
+  }
+
+  .emoji-mart-anchor-bar {
+    bottom: 0;
+  }
+}
+
+.emoji-mart-anchor-bar {
+  position: absolute;
+  bottom: -3px;
+  left: 0;
+  width: 100%;
+  height: 3px;
+  background-color: darken($ui-highlight-color, 3%);
+}
+
+.emoji-mart-anchors {
+  i {
+    display: inline-block;
+    width: 100%;
+    max-width: 22px;
+  }
+
+  svg {
+    fill: currentColor;
+    max-height: 18px;
+  }
+}
+
+.emoji-mart-scroll {
+  overflow-y: scroll;
+  height: 270px;
+  padding: 0 6px 6px;
+  background: $simple-background-color;
+}
+
+.emoji-mart-search {
+  padding: 10px;
+  padding-right: 45px;
+  background: $simple-background-color;
+
+  input {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 7px 9px;
+    font-family: inherit;
+    display: block;
+    width: 100%;
+    background: rgba($ui-secondary-color, 0.3);
+    color: $ui-primary-color;
+    border: 1px solid $ui-secondary-color;
+    border-radius: 4px;
+
+    &::-moz-focus-inner {
+      border: 0;
+    }
+
+    &::-moz-focus-inner,
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
+  }
+}
+
+.emoji-mart-category .emoji-mart-emoji {
+  cursor: pointer;
+
+  span {
+    z-index: 1;
+    position: relative;
+    text-align: center;
+  }
+
+  &:hover::before {
+    z-index: 0;
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba($ui-secondary-color, 0.7);
+    border-radius: 100%;
+  }
+}
+
+.emoji-mart-category-label {
+  z-index: 2;
+  position: relative;
+  position: -webkit-sticky;
+  position: sticky;
+  top: 0;
+
+  span {
+    display: block;
+    width: 100%;
+    font-weight: 500;
+    padding: 5px 6px;
+    background: $simple-background-color;
+  }
+}
+
+.emoji-mart-emoji {
+  position: relative;
+  display: inline-block;
+  font-size: 0;
+
+  span {
+    width: 22px;
+    height: 22px;
+  }
+}
+
+.emoji-mart-no-results {
+  font-size: 14px;
+  text-align: center;
+  padding-top: 70px;
+  color: $ui-primary-color;
+
+  .emoji-mart-category-label {
+    display: none;
+  }
+
+  .emoji-mart-no-results-label {
+    margin-top: .2em;
+  }
+
+  .emoji-mart-emoji:hover::before {
+    content: none;
+  }
+}
+
+.emoji-mart-preview {
+  display: none;
+}