about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/features/emoji/emoji_mart_search_light.js93
-rw-r--r--app/javascript/mastodon/features/emoji/emoji_picker.js7
-rw-r--r--app/javascript/mastodon/features/emoji/emoji_utils.js190
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js2
4 files changed, 210 insertions, 82 deletions
diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js
index 5da8de1cf..5755bf1c4 100644
--- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js
+++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js
@@ -1,55 +1,61 @@
 // This code is largely borrowed from:
-// https://github.com/missive/emoji-mart/blob/bbd4fbe/src/utils/emoji-index.js
+// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
 
 import data from './emoji_mart_data_light';
 import { getData, getSanitizedData, intersect } from './emoji_utils';
 
+let originalPool = {};
 let index = {};
 let emojisList = {};
 let emoticonsList = {};
-let previousInclude = [];
-let previousExclude = [];
 
 for (let emoji in data.emojis) {
-  let emojiData = data.emojis[emoji],
-    { short_names, emoticons } = emojiData,
-    id = short_names[0];
+  let emojiData = data.emojis[emoji];
+  let { short_names, emoticons } = emojiData;
+  let id = short_names[0];
+
+  if (emoticons) {
+    emoticons.forEach(emoticon => {
+      if (emoticonsList[emoticon]) {
+        return;
+      }
 
-  for (let emoticon of (emoticons || [])) {
-    if (!emoticonsList[emoticon]) {
       emoticonsList[emoticon] = id;
-    }
+    });
   }
 
   emojisList[id] = getSanitizedData(id);
+  originalPool[id] = emojiData;
+}
+
+function addCustomToPool(custom, pool) {
+  custom.forEach((emoji) => {
+    let emojiId = emoji.id || emoji.short_names[0];
+
+    if (emojiId && !pool[emojiId]) {
+      pool[emojiId] = getData(emoji);
+      emojisList[emojiId] = getSanitizedData(emoji);
+    }
+  });
 }
 
 function search(value, { emojisToShowFilter, maxResults, include, exclude, custom = [] } = {}) {
+  addCustomToPool(custom, originalPool);
+
   maxResults = maxResults || 75;
   include = include || [];
   exclude = exclude || [];
 
-  if (custom.length) {
-    for (const emoji of custom) {
-      data.emojis[emoji.id] = getData(emoji);
-      emojisList[emoji.id] = getSanitizedData(emoji);
-    }
-
-    data.categories.push({
-      name: 'Custom',
-      emojis: custom.map(emoji => emoji.id),
-    });
-  }
-
-  let results = null;
-  let pool = data.emojis;
+  let results = null,
+    pool = originalPool;
 
   if (value.length) {
     if (value === '-' || value === '-1') {
       return [emojisList['-1']];
     }
 
-    let values = value.toLowerCase().split(/[\s|,|\-|_]+/);
+    let values = value.toLowerCase().split(/[\s|,|\-|_]+/),
+      allResults = [];
 
     if (values.length > 2) {
       values = [values[0], values[1]];
@@ -58,33 +64,32 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
     if (include.length || exclude.length) {
       pool = {};
 
-      if (previousInclude !== include.sort().join(',') || previousExclude !== exclude.sort().join(',')) {
-        previousInclude = include.sort().join(',');
-        previousExclude = exclude.sort().join(',');
-        index = {};
-      }
-
-      for (let category of data.categories) {
+      data.categories.forEach(category => {
         let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
         let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
         if (!isIncluded || isExcluded) {
-          continue;
+          return;
         }
 
-        for (let emojiId of category.emojis) {
-          pool[emojiId] = data.emojis[emojiId];
+        category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
+      });
+
+      if (custom.length) {
+        let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
+        let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
+        if (customIsIncluded && !customIsExcluded) {
+          addCustomToPool(custom, pool);
         }
       }
-    } else if (previousInclude.length || previousExclude.length) {
-      index = {};
     }
 
-    let allResults = values.map((value) => {
-      let aPool = pool;
-      let aIndex = index;
-      let length = 0;
+    allResults = values.map((value) => {
+      let aPool = pool,
+        aIndex = index,
+        length = 0;
 
-      for (let char of value.split('')) {
+      for (let charIndex = 0; charIndex < value.length; charIndex++) {
+        const char = value[charIndex];
         length++;
 
         aIndex[char] = aIndex[char] || {};
@@ -104,9 +109,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
 
             if (subIndex !== -1) {
               let score = subIndex + 1;
-              if (sub === id) {
-                score = 0;
-              }
+              if (sub === id) score = 0;
 
               aIndex.results.push(emojisList[id]);
               aIndex.pool[id] = emoji;
@@ -130,7 +133,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
     }).filter(a => a);
 
     if (allResults.length > 1) {
-      results = intersect(...allResults);
+      results = intersect.apply(null, allResults);
     } else if (allResults.length) {
       results = allResults[0];
     } else {
diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.js b/app/javascript/mastodon/features/emoji/emoji_picker.js
new file mode 100644
index 000000000..7e145381e
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/emoji_picker.js
@@ -0,0 +1,7 @@
+import Picker from 'emoji-mart/dist-es/components/picker';
+import Emoji from 'emoji-mart/dist-es/components/emoji';
+
+export {
+  Picker,
+  Emoji,
+};
diff --git a/app/javascript/mastodon/features/emoji/emoji_utils.js b/app/javascript/mastodon/features/emoji/emoji_utils.js
index 2742185d9..dbf725c1f 100644
--- a/app/javascript/mastodon/features/emoji/emoji_utils.js
+++ b/app/javascript/mastodon/features/emoji/emoji_utils.js
@@ -1,11 +1,9 @@
 // This code is largely borrowed from:
-// https://github.com/missive/emoji-mart/blob/bbd4fbe/src/utils/index.js
+// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
 
 import data from './emoji_mart_data_light';
 
-const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/;
-
-function buildSearch(thisData) {
+const buildSearch = (data) => {
   const search = [];
 
   let addToSearch = (strings, split) => {
@@ -24,19 +22,68 @@ function buildSearch(thisData) {
     });
   };
 
-  addToSearch(thisData.short_names, true);
-  addToSearch(thisData.name, true);
-  addToSearch(thisData.keywords, false);
-  addToSearch(thisData.emoticons, false);
+  addToSearch(data.short_names, true);
+  addToSearch(data.name, true);
+  addToSearch(data.keywords, false);
+  addToSearch(data.emoticons, false);
 
-  return search;
-}
+  return search.join(',');
+};
+
+const _String = String;
+
+const stringFromCodePoint = _String.fromCodePoint || function () {
+  let MAX_SIZE = 0x4000;
+  let codeUnits = [];
+  let highSurrogate;
+  let lowSurrogate;
+  let index = -1;
+  let length = arguments.length;
+  if (!length) {
+    return '';
+  }
+  let result = '';
+  while (++index < length) {
+    let codePoint = Number(arguments[index]);
+    if (
+      !isFinite(codePoint) ||       // `NaN`, `+Infinity`, or `-Infinity`
+      codePoint < 0 ||              // not a valid Unicode code point
+      codePoint > 0x10FFFF ||       // not a valid Unicode code point
+      Math.floor(codePoint) !== codePoint // not an integer
+    ) {
+      throw RangeError('Invalid code point: ' + codePoint);
+    }
+    if (codePoint <= 0xFFFF) { // BMP code point
+      codeUnits.push(codePoint);
+    } else { // Astral code point; split in surrogate halves
+      // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
+      codePoint -= 0x10000;
+      highSurrogate = (codePoint >> 10) + 0xD800;
+      lowSurrogate = (codePoint % 0x400) + 0xDC00;
+      codeUnits.push(highSurrogate, lowSurrogate);
+    }
+    if (index + 1 === length || codeUnits.length > MAX_SIZE) {
+      result += String.fromCharCode.apply(null, codeUnits);
+      codeUnits.length = 0;
+    }
+  }
+  return result;
+};
+
+
+const _JSON = JSON;
+
+const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/;
+const SKINS = [
+  '1F3FA', '1F3FB', '1F3FC',
+  '1F3FD', '1F3FE', '1F3FF',
+];
 
 function unifiedToNative(unified) {
   let unicodes = unified.split('-'),
     codePoints = unicodes.map((u) => `0x${u}`);
 
-  return String.fromCodePoint(...codePoints);
+  return stringFromCodePoint.apply(null, codePoints);
 }
 
 function sanitize(emoji) {
@@ -70,11 +117,11 @@ function sanitize(emoji) {
   };
 }
 
-function getSanitizedData(emoji) {
-  return sanitize(getData(emoji));
+function getSanitizedData() {
+  return sanitize(getData(...arguments));
 }
 
-function getData(emoji) {
+function getData(emoji, skin, set) {
   let emojiData = {};
 
   if (typeof emoji === 'string') {
@@ -83,6 +130,9 @@ function getData(emoji) {
     if (matches) {
       emoji = matches[1];
 
+      if (matches[2]) {
+        skin = parseInt(matches[2]);
+      }
     }
 
     if (data.short_names.hasOwnProperty(emoji)) {
@@ -92,17 +142,6 @@ function getData(emoji) {
     if (data.emojis.hasOwnProperty(emoji)) {
       emojiData = data.emojis[emoji];
     }
-  } else if (emoji.custom) {
-    emojiData = emoji;
-
-    emojiData.search = buildSearch({
-      short_names: emoji.short_names,
-      name: emoji.name,
-      keywords: emoji.keywords,
-      emoticons: emoji.emoticons,
-    });
-
-    emojiData.search = emojiData.search.join(',');
   } else if (emoji.id) {
     if (data.short_names.hasOwnProperty(emoji.id)) {
       emoji.id = data.short_names[emoji.id];
@@ -110,31 +149,110 @@ function getData(emoji) {
 
     if (data.emojis.hasOwnProperty(emoji.id)) {
       emojiData = data.emojis[emoji.id];
+      skin = skin || emoji.skin;
+    }
+  }
+
+  if (!Object.keys(emojiData).length) {
+    emojiData = emoji;
+    emojiData.custom = true;
+
+    if (!emojiData.search) {
+      emojiData.search = buildSearch(emoji);
     }
   }
 
   emojiData.emoticons = emojiData.emoticons || [];
   emojiData.variations = emojiData.variations || [];
 
+  if (emojiData.skin_variations && skin > 1 && set) {
+    emojiData = JSON.parse(_JSON.stringify(emojiData));
+
+    let skinKey = SKINS[skin - 1],
+      variationData = emojiData.skin_variations[skinKey];
+
+    if (!variationData.variations && emojiData.variations) {
+      delete emojiData.variations;
+    }
+
+    if (variationData[`has_img_${set}`]) {
+      emojiData.skin_tone = skin;
+
+      for (let k in variationData) {
+        let v = variationData[k];
+        emojiData[k] = v;
+      }
+    }
+  }
+
   if (emojiData.variations && emojiData.variations.length) {
-    emojiData = JSON.parse(JSON.stringify(emojiData));
+    emojiData = JSON.parse(_JSON.stringify(emojiData));
     emojiData.unified = emojiData.variations.shift();
   }
 
   return emojiData;
 }
 
+function uniq(arr) {
+  return arr.reduce((acc, item) => {
+    if (acc.indexOf(item) === -1) {
+      acc.push(item);
+    }
+    return acc;
+  }, []);
+}
+
 function intersect(a, b) {
-  let set;
-  let list;
-  if (a.length < b.length) {
-    set = new Set(a);
-    list = b;
-  } else {
-    set = new Set(b);
-    list = a;
+  const uniqA = uniq(a);
+  const uniqB = uniq(b);
+
+  return uniqA.filter(item => uniqB.indexOf(item) >= 0);
+}
+
+function deepMerge(a, b) {
+  let o = {};
+
+  for (let key in a) {
+    let originalValue = a[key],
+      value = originalValue;
+
+    if (b.hasOwnProperty(key)) {
+      value = b[key];
+    }
+
+    if (typeof value === 'object') {
+      value = deepMerge(originalValue, value);
+    }
+
+    o[key] = value;
   }
-  return Array.from(new Set(list.filter(x => set.has(x))));
+
+  return o;
+}
+
+// https://github.com/sonicdoe/measure-scrollbar
+function measureScrollbar() {
+  const div = document.createElement('div');
+
+  div.style.width = '100px';
+  div.style.height = '100px';
+  div.style.overflow = 'scroll';
+  div.style.position = 'absolute';
+  div.style.top = '-9999px';
+
+  document.body.appendChild(div);
+  const scrollbarWidth = div.offsetWidth - div.clientWidth;
+  document.body.removeChild(div);
+
+  return scrollbarWidth;
 }
 
-export { getData, getSanitizedData, intersect };
+export {
+  getData,
+  getSanitizedData,
+  uniq,
+  intersect,
+  deepMerge,
+  unifiedToNative,
+  measureScrollbar,
+};
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 6978da2f9..8f7b91d21 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: "emoji_picker" */'emoji-mart');
+  return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji_picker');
 }
 
 export function Compose () {