Просмотр исходного кода

Merge pull request #773 from threema-ch/new-compose-area

Integrate new compose-area
Danilo Bargen 6 лет назад
Родитель
Сommit
c553129e03

+ 1 - 2
.gitignore

@@ -6,8 +6,7 @@
 
 # Generated files
 js/
-dist/*.js
-dist/*.js.map
+dist/generated/
 dist/*.tar.gz
 public/css/*.css
 public/css/*.map

+ 7 - 0
CHANGELOG.md

@@ -4,6 +4,13 @@ This changelog lists the most important changes for each released version. For
 the full log, please refer to the git commit history.
 
 
+### Unreleased
+
+**NOTE:** If you are self-hosting threema-web, then you need to make sure to
+set the proper MIME type for `.wasm` (WebAssembly) files. See
+`docs/self_hosting.md` for more details.
+
+
 ### [v2.1.7][v2.1.7] (2019-02-07)
 
 Changes:

+ 2 - 2
README.md

@@ -82,8 +82,8 @@ Web on a server, please follow the instructions at
 
 To run unit tests:
 
-    npm run build:unittests
-    firefox tests/testsuite.html
+    npm run build:unittests && npm run testserver
+    firefox http://localhost:7777/tests/testsuite.html
 
 To run UI tests:
 

+ 9 - 3
dist/build-package.js

@@ -4,12 +4,18 @@ var process = require('process');
 
 var args = process.argv.slice(2);
 
+function abortOnError(code) {
+    if (code !== 0) {
+        process.exit(code);
+    }
+}
+
 if (os.type() === 'Linux') {
-   spawn('bash', ['dist/package.sh'].concat(args), {stdio: 'inherit'});
+   spawn('bash', ['dist/package.sh'].concat(args), {stdio: 'inherit'}).on('exit', abortOnError);
 } else if (os.type() === 'Darwin') {
-   spawn('bash', ['dist/package.sh'].concat(args), {stdio: 'inherit'});
+   spawn('bash', ['dist/package.sh'].concat(args), {stdio: 'inherit'}).on('exit', abortOnError);
 } else if (os.type() === 'Windows_NT') {
-   spawn('powershell', ['dist/package.sh'].concat(args), {stdio: 'inherit'});
+   spawn('powershell', ['dist/package.sh'].concat(args), {stdio: 'inherit'}).on('exit', abortOnError);
 } else {
    throw new Error("Unsupported OS found: " + os.type());
 }

+ 7 - 5
dist/package.sh

@@ -36,16 +36,18 @@ if [ -e "release" ]; then
 fi
 
 VERSION=$(grep "\"version\"" package.json  | sed 's/[[:blank:]]*\"version\": \"\([^\"]*\).*/\1/')$SUFFIX
-echo "+ Building version $VERSION"
+echo "+ Packaging version $VERSION"
 
 DIR="release/threema-web-$VERSION"
 
 echo "+ Create release directory..."
-mkdir -p $DIR/{dist,partials,directives,components,node_modules,partials/messenger.receiver,troubleshoot}
+mkdir -p $DIR/{partials,directives,components,node_modules,partials/messenger.receiver,troubleshoot}
 
 echo "+ Copy code..."
 cp -R index.html $DIR/
-cp -R dist/app.bundle.js $DIR/dist/
+cp -R dist/generated/*.bundle.js $DIR/
+cp -R dist/generated/*.bundle.js.map $DIR/
+cp -R dist/generated/*.wasm $DIR/
 cp -R public/* $DIR/
 cp -R troubleshoot/* $DIR/troubleshoot/
 cp -R src/partials/*.html $DIR/partials/
@@ -98,8 +100,8 @@ for target in "${targets[@]}"; do
 done
 
 echo "+ Update version number..."
-sed -i.bak -e "s/\[\[VERSION\]\]/${VERSION}/g" $DIR/index.html $DIR/troubleshoot/index.html $DIR/dist/app.bundle.js $DIR/manifest.webmanifest $DIR/browserconfig.xml $DIR/version.txt
-rm $DIR/index.html.bak $DIR/troubleshoot/index.html.bak $DIR/dist/app.bundle.js.bak $DIR/manifest.webmanifest.bak $DIR/browserconfig.xml.bak $DIR/version.txt.bak
+sed -i.bak -e "s/\[\[VERSION\]\]/${VERSION}/g" $DIR/index.html $DIR/troubleshoot/index.html $DIR/*.bundle.js $DIR/manifest.webmanifest $DIR/browserconfig.xml $DIR/version.txt
+rm $DIR/*.bak $DIR/troubleshoot/index.html.bak
 
 echo "+ Update permissions..."
 find $DIR/ -type f -exec chmod 644 {} \;

+ 9 - 3
docker/entrypoint.sh

@@ -5,10 +5,16 @@ set -euo pipefail
 echo "Patching config file..."
 cd /usr/share/nginx/html/
 if [ ! -z "$SALTYRTC_HOST" ]; then
-    sed -i "s/SALTYRTC_HOST: null,/SALTYRTC_HOST: '${SALTYRTC_HOST}',/g" dist/app.js
+    sed -i -E "s/SALTYRTC_HOST:\s*null,/SALTYRTC_HOST:'${SALTYRTC_HOST}',/g" *.bundle.js
+fi
+sed -i -E "s/SALTYRTC_PORT:\s*[^,]*,/SALTYRTC_PORT:${SALTYRTC_PORT},/g" *.bundle.js
+sed -i -E "s/SALTYRTC_SERVER_KEY:\s*\"[^\"]*\",/SALTYRTC_SERVER_KEY:\"${SALTYRTC_SERVER_KEY}\",/g" *.bundle.js
+
+# Add nginx mime type for wasm
+# See https://trac.nginx.org/nginx/ticket/1606
+if ! grep -q application/wasm "/etc/nginx/mime.types"; then
+    sed -i '2aapplication/wasm wasm;' /etc/nginx/mime.types
 fi
-sed -i "s/SALTYRTC_PORT: [^,]*,/SALTYRTC_PORT: ${SALTYRTC_PORT},/g" dist/app.js
-sed -i "s/SALTYRTC_SERVER_KEY: '[^']*',/SALTYRTC_SERVER_KEY: '${SALTYRTC_SERVER_KEY}',/g" dist/app.js
 
 echo "Starting Threema Web..."
 exec nginx -g 'daemon off;'

+ 2 - 2
docs/docker.md

@@ -5,11 +5,11 @@ A Docker image with Threema Web is published on Docker Hub at
 
 Alternatively you can build the image yourself:
 
-    $ docker build . -t threema/threema-web:v2.1
+    $ docker build . -t threema/threema-web:master
 
 To run the Docker image:
 
-    $ docker run --rm -p 8080:80 threema/threema-web
+    $ docker run --rm -p 8080:80 threema/threema-web:master
 
 Now you can open `http://localhost:8080/` in your browser to use Threema Web.
 

+ 12 - 2
docs/self_hosting.md

@@ -19,8 +19,18 @@ issue](https://github.com/threema-ch/threema-web/issues) on Github.
 
 Threema Web is a web application written in TypeScript with AngularJS 1. All
 that is required to host it is a web server that can deliver static content via
-https. We recommend using Nginx. Additionally, to build the release version
-yourself, a recent version of npm is required.
+https. We recommend using Nginx. Additionally, to build the
+release version yourself, a recent version of npm is required.
+
+*Note:* The web server needs to set the proper MIME type for `*.wasm`
+(WebAssembly) files. When using Nginx, add the following line to the types in
+`/etc/nginx/mime.types`:
+
+    application/wasm    wasm;
+
+When using Python,
+[someting like this](https://gist.github.com/dbrgn/6bf88d32b1b44b1b1d4140f92b8a7a0a)
+should work.
 
 ### Building from source
 

+ 2 - 2
index.html

@@ -19,7 +19,7 @@
     along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
 
 -->
-<html ng-app="3ema" ng-strict-di ng-controller="StatusController as ctrl">
+<html ng-strict-di ng-controller="StatusController as ctrl">
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -138,7 +138,7 @@
     <script src="node_modules/croppie/croppie.min.js?v=[[VERSION]]"></script>
 
     <!-- App -->
-    <script src="dist/app.bundle.js?v=[[VERSION]]"></script>
+    <script src="app.bundle.js?v=[[VERSION]]"></script>
 
 </body>
 </html>

+ 26 - 5
karma.conf.js

@@ -2,16 +2,33 @@ module.exports = function(config) {
 
     var configuration = {
         frameworks: ['jasmine'],
+        mime: {
+            'application/wasm': ['wasm'],
+        },
         files: [
+            // Angular core
             'node_modules/angular/angular.js',
-            'node_modules/angular-mocks/angular-mocks.js',
-            'node_modules/angular-translate/dist/angular-translate.min.js',
             'node_modules/angular-aria/angular-aria.min.js',
             'node_modules/angular-animate/angular-animate.min.js',
+            'node_modules/angular-sanitize/angular-sanitize.min.js',
+            'node_modules/angular-route/angular-route.min.js',
             'node_modules/angular-material/angular-material.min.js',
+            'node_modules/angular-translate/dist/angular-translate.min.js',
+
+            // Angular mocking
+            'node_modules/angular-mocks/angular-mocks.js',
+
+            // SaltyRTC
             'node_modules/@saltyrtc/chunked-dc/dist/chunked-dc.es5.js',
-            'dist/app.bundle.js',
-            'dist/unittests.bundle.js',
+
+            // App bundles
+            'dist/generated/app_noinit.bundle.js',
+            'dist/generated/unittest_karma.bundle.js',
+            {pattern: 'dist/generated/[0-9].*.bundle.js', included: false, serve: true},
+            {pattern: 'dist/generated/*.module.wasm', included: false, serve: true, type: 'wasm'},
+
+            // Tests
+            'tests/init.js',
             'tests/filters.js',
             'tests/service/message.js',
             'tests/service/mime.js',
@@ -23,8 +40,12 @@ module.exports = function(config) {
             'tests/service/keystore.js',
             'tests/service/notification.js',
             'tests/service/receiver.js',
-            'tests/ts/helpers.ts',
         ],
+        proxies: {
+            // Also serve all generated files on the root.
+            // This is required for the .wasm modules.
+            '/dist/generated/': '/base/dist/generated/',
+        },
         customLaunchers: {
             Chromium_ci_gitlab: {
                 base: 'Chromium',

Разница между файлами не показана из-за своего большого размера
+ 520 - 1121
package-lock.json


+ 8 - 5
package.json

@@ -10,18 +10,18 @@
     "build:tests": "echo -e 'NOTE: Use either \"npm build:unittests\" or \"npm build:uitests\"\n' && exit 1",
     "build:unittests": "webpack --config webpack.tests.js",
     "build:uitests": "npm run build:css && webpack --config webpack.tests.js",
-    "dist": "npm run build && echo \"\" && node dist/build-package.js",
+    "dist": "npm run clean && npm run build && echo \"\" && node dist/build-package.js",
     "serve:live": "echo 'NOTE: serve:live command has been renamed to devserver'",
     "devserver": "npm run build:css && concurrently --kill-others --names \"css,server\" -p name \"npm run build:css:watch\" \"npx webpack-dev-server --config webpack.dev.js\"",
     "testserver": "npx webpack-dev-server --config webpack.tests.js",
     "test": "echo -e 'NOTE: Use either \"npm run test:unit\" or \"npm run test:ui\"\n' && exit 1",
     "test:unit": "npm run build:unittests && karma start --single-run --log-level=debug --colors",
-    "test:ui": "npm run build:uitests && bash tests/ui/run.sh",
+    "test:ui": "bash tests/ui/run.sh",
     "lint": "npm run lint:ts && npm run lint:sass",
     "lint:ts": "tslint -c tslint.json --project tsconfig.json --exclude \"**/src/config.ts\"",
     "lint:sass": "sass-lint -c .sass-lint.yml -v -q",
     "lint:sass:fix": "sass-lint-auto-fix -c .sass-lint.yml",
-    "clean": "rm -rf js/ build/ dist/app*"
+    "clean": "rm -rf dist/generated"
   },
   "keywords": [
     "threema",
@@ -35,12 +35,14 @@
   "homepage": "https://threema.ch/",
   "dependencies": {
     "@babel/core": "^7.4.3",
+    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
     "@babel/plugin-transform-runtime": "^7.4.3",
     "@babel/preset-env": "^7.4.3",
     "@babel/runtime": "^7.4.3",
     "@saltyrtc/client": "^0.13.2",
     "@saltyrtc/task-relayed-data": "^0.3.1",
     "@saltyrtc/task-webrtc": "^0.13.0",
+    "@threema/compose-area": "^0.3.1",
     "@types/angular": "^1.6.53",
     "@types/angular-material": "^1.1.62",
     "@types/angular-sanitize": "^1.3.7",
@@ -64,6 +66,7 @@
     "babel-loader": "^8.0.5",
     "core-js": "^3.0.1",
     "croppie": "^2.6.3",
+    "emoji-regex": "^8.0.0",
     "file-saver": "2.0.0",
     "messageformat": "^2.0.5",
     "msgpack-lite": "~0.1.26",
@@ -83,14 +86,14 @@
   "devDependencies": {
     "@types/chai": "^4.1.7",
     "@types/jasmine": "^3.3.5",
-    "@types/selenium-webdriver": "^3.0.14",
+    "@types/selenium-webdriver": "^4.0.0",
     "angular-mocks": "^1.7.5",
     "chai": "^4.2.0",
     "concurrently": "~4.1.0",
     "geckodriver": "^1.14.1",
     "jasmine": "^3.3.1",
     "jasmine-core": "^3.3.0",
-    "karma": "^3.1.4",
+    "karma": "^4.1.0",
     "karma-chrome-launcher": "^2.2.0",
     "karma-firefox-launcher": "^1.1.0",
     "karma-jasmine": "^2.0.1",

+ 28 - 0
src/bootstrap.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// tslint:disable:no-console
+
+// A dependency graph that contains any wasm must all be imported asynchronously.
+import('./app')
+    .then(() => {
+        console.info('Bundle loaded, bootstrapping application.');
+        angular.bootstrap(document, ['3ema']);
+    })
+    .catch((e) => console.error('Could not bootstrap application', e));

+ 185 - 375
src/directives/compose_area.ts

@@ -15,15 +15,15 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
-import * as twemoji from 'twemoji';
+import {ComposeArea} from '@threema/compose-area';
 
-import {extractText, hasValue, isActionTrigger, logAdapter, replaceWhitespace} from '../helpers';
-import {emojify, shortnameToUnicode} from '../helpers/emoji';
+import {isActionTrigger} from '../helpers';
+import {emojifyNew, shortnameToUnicode} from '../helpers/emoji';
 import {BrowserService} from '../services/browser';
 import {ReceiverService} from '../services/receiver';
 import {StringService} from '../services/string';
 import {TimeoutService} from '../services/timeout';
-import {isElementNode, isTextNode} from '../typeguards';
+import {isEmojiInfo} from '../typeguards';
 
 /**
  * The compose area where messages are written.
@@ -39,6 +39,7 @@ export default [
     '$filter',
     '$log',
     '$rootScope',
+    'CONFIG',
     function(browserService: BrowserService,
              stringService: StringService,
              timeoutService: TimeoutService,
@@ -48,10 +49,14 @@ export default [
              $mdDialog: ng.material.IDialogService,
              $filter: ng.IFilterService,
              $log: ng.ILogService,
-             $rootScope: ng.IRootScopeService) {
+             $rootScope: ng.IRootScopeService,
+             CONFIG: threema.Config) {
         return {
             restrict: 'EA',
             scope: {
+                // Callback to get a reference to the initialized ComposeArea instance.
+                onInit: '=',
+
                 // Callback to submit text or file data
                 submit: '=',
 
@@ -70,7 +75,7 @@ export default [
 
                 receiver: '<receiver',
             },
-            link: function(scope: any, element) {
+            link: function(scope: any, wrapper: JQLite) {
                 // Logging
                 const logTag = '[Directives.ComposeArea]';
 
@@ -79,13 +84,19 @@ export default [
                 const TRIGGER_ACTIVE_CSS_CLASS = 'is-active';
 
                 // Elements
-                const composeArea: any = element;
-                const composeDiv: any = angular.element(element[0].querySelector('div.compose'));
-                const emojiTrigger: any = angular.element(element[0].querySelector('i.emoji-trigger'));
-                const emojiKeyboard: any = angular.element(element[0].querySelector('.emoji-keyboard'));
-                const sendTrigger: any = angular.element(element[0].querySelector('i.send-trigger'));
-                const fileTrigger: any = angular.element(element[0].querySelector('i.file-trigger'));
-                const fileInput: any = angular.element(element[0].querySelector('input.file-input'));
+                const select = (selector) => angular.element(wrapper[0].querySelector(selector));
+                const composeDiv = select('div.compose') as JQuery<HTMLElement>;
+                const emojiTrigger = select('i.emoji-trigger') as JQuery<HTMLElement>;
+                const emojiKeyboard = select('.emoji-keyboard') as JQuery<HTMLElement>;
+                const sendTrigger = select('i.send-trigger') as JQuery<HTMLElement>;
+                const fileTrigger = select('i.file-trigger') as JQuery<HTMLElement>;
+                const fileInput = select('input.file-input') as JQuery<HTMLInputElement>;
+
+                // Initialize compose area lib
+                const composeArea = ComposeArea.bind_to(composeDiv[0], CONFIG.DEBUG ? 'debug' : 'warn');
+                if (scope.onInit) {
+                    scope.onInit(composeArea);
+                }
 
                 // Set initial text
                 if (scope.initialData.initialText) {
@@ -95,16 +106,6 @@ export default [
                     composeDiv[0].innerText = scope.initialData.draft;
                 }
 
-                // The current caret position, used when inserting objects
-                let caretPosition: {
-                    // The position in the source HTML
-                    from?: number,
-                    to?: number,
-                    // The position in the visible character list
-                    fromChar?: number,
-                    toChar?: number,
-                } = null;
-
                 let chatBlocked = false;
 
                 // Function to update blocking state
@@ -115,7 +116,7 @@ export default [
                         sendTrigger.removeClass(TRIGGER_ENABLED_CSS_CLASS);
                         emojiTrigger.removeClass(TRIGGER_ENABLED_CSS_CLASS);
                         fileTrigger.removeClass(TRIGGER_ENABLED_CSS_CLASS);
-                        composeDiv.attr('contenteditable', false);
+                        composeDiv.attr('contenteditable', 'false');
                         if (emojiKeyboard.hasClass('active')) {
                             hideEmojiPicker();
                         }
@@ -127,7 +128,7 @@ export default [
                         }
                         emojiTrigger.addClass(TRIGGER_ENABLED_CSS_CLASS);
                         fileTrigger.addClass(TRIGGER_ENABLED_CSS_CLASS);
-                        composeDiv.attr('contenteditable', true);
+                        composeDiv.attr('contenteditable', 'true');
                     }
                 }
 
@@ -144,38 +145,6 @@ export default [
                     },
                 );
 
-                /**
-                 * Stop propagation of click events and hold htmlElement of the emojipicker
-                 */
-                const EmojiPickerContainer = (function() {
-                    let instance;
-
-                    function click(e) {
-                        e.stopPropagation();
-                    }
-
-                    return {
-                        get: function() {
-                            if (instance === undefined) {
-                                instance = {
-                                    htmlElement: composeArea[0].querySelector('div.twemoji-picker'),
-                                };
-                                // append stop propagation
-                                angular.element(instance.htmlElement).on('click', click);
-
-                            }
-                            return instance;
-                        },
-                        destroy: function() {
-                            if (instance !== undefined) {
-                                // remove stop propagation
-                                angular.element(instance.htmlElement).off('click', click);
-                                instance = undefined;
-                            }
-                        },
-                    };
-                })();
-
                 // Typing events
                 let stopTypingTimer: ng.IPromise<void> = null;
 
@@ -208,24 +177,14 @@ export default [
 
                 // Determine whether field is empty
                 function composeAreaIsEmpty() {
-                    const text = extractText(composeDiv[0], logAdapter($log.warn, logTag));
-                    return text.length === 0;
+                    return composeArea.get_text().length === 0;
                 }
 
                 // Submit the text from the compose area.
                 //
                 // Emoji images are converted to their alt text in this process.
                 function submitText(): Promise<any> {
-                    const rawText = extractText(composeDiv[0], logAdapter($log.warn, logTag));
-
-                    // Due to #731, and the hack introduced in #706, the
-                    // extracted text may contain non-breaking spaces (U+00A0).
-                    // Replace them with actual whitespace to avoid strange
-                    // behavior when submitting the text.
-                    //
-                    // TODO: Remove this once we have a compose area rewrite and can
-                    // fix the actual bug.
-                    const text = rawText.replace(/\u00A0/g, ' ');
+                    const text = composeArea.get_text().replace(/\r/g, '');
 
                     return new Promise((resolve, reject) => {
                         const submitTexts = (strings: string[]) => {
@@ -241,9 +200,8 @@ export default [
                                 .catch(reject);
                         };
 
-                        const fullText = text.trim().replace(/\r/g, '');
-                        if (fullText.length > scope.maxTextLength) {
-                            const pieces: string[] = stringService.byteChunk(fullText, scope.maxTextLength, 50);
+                        if (text.length > scope.maxTextLength) {
+                            const pieces: string[] = stringService.byteChunk(text, scope.maxTextLength, 50);
                             const confirm = $mdDialog.confirm()
                                 .title($translate.instant('messenger.MESSAGE_TOO_LONG_SPLIT_SUBJECT'))
                                 .textContent($translate.instant('messenger.MESSAGE_TOO_LONG_SPLIT_BODY', {
@@ -259,7 +217,7 @@ export default [
                                 reject();
                             });
                         } else {
-                            submitTexts([fullText]);
+                            submitTexts([text]);
                         }
                     });
                 }
@@ -268,8 +226,8 @@ export default [
                     if (!composeAreaIsEmpty()) {
                         submitText().then(() => {
                             // Clear compose div
-                            composeDiv[0].innerText = '';
-                            composeDiv[0].focus();
+                            composeArea.clear();
+                            composeArea.focus();
 
                             // Send stopTyping event
                             stopTyping();
@@ -289,6 +247,17 @@ export default [
                 }
 
                 // Handle typing events
+
+                let isComposing = false;
+
+                function onCompositionStart(ev: CompositionEvent): void {
+                    isComposing = true;
+                }
+
+                function onCompositionEnd(ev: CompositionEvent): void {
+                    isComposing = false;
+                }
+
                 function onKeyDown(ev: KeyboardEvent): void {
                     // If enter is pressed, prevent default event from being dispatched
                     if (!ev.shiftKey && ev.key === 'Enter') {
@@ -301,6 +270,22 @@ export default [
                         return;
                     }
 
+                    // If the enter key is part of a composition (e.g. when
+                    // entering text with an IME), don't submit the text.
+                    // See https://github.com/threema-ch/threema-web/issues/777
+                    if ((ev as any).isComposing || isComposing) {
+                        return;
+                    }
+
+                    // If a : is pressed, possibly insert emoji
+                    if (ev.key === ':') {
+                        const modified = onEmojiShortcodeKeyPressed(ev, ':', false);
+                        if (modified) {
+                            ev.preventDefault();
+                            return;
+                        }
+                    }
+
                     // At link time, the element is not yet evaluated.
                     // Therefore add following code to end of event loop.
                     $timeout(() => {
@@ -316,39 +301,23 @@ export default [
                 }
 
                 function onKeyUp(ev: KeyboardEvent): void {
-                    // At link time, the element is not yet evaluated.
-                    // Therefore add following code to end of event loop.
-                    $timeout(() => {
-                        // If the compose area contains only a single <br>, make it fully empty.
-                        // See also: https://stackoverflow.com/q/14638887/284318
-                        const text = extractText(composeDiv[0], logAdapter($log.warn, logTag), false);
-                        if (text === '\n') {
-                            composeDiv[0].innerText = '';
-                        } else if ((ev.keyCode === 190 || ev.key === ':') && caretPosition !== null) {
-                            // A ':' is pressed, try to parse
-                            const currentWord = stringService.getWord(text, caretPosition.fromChar, [':']);
-                            if (currentWord.realLength > 2 && currentWord.word.substr(0, 1) === ':') {
-                                const trimmed = currentWord.word.substr(1, currentWord.word.length - 2);
-                                const unicodeEmoji = shortnameToUnicode(trimmed);
-                                if (unicodeEmoji !== null) {
-                                    return insertEmoji(unicodeEmoji,
-                                        caretPosition.from - currentWord.realLength,
-                                        caretPosition.to);
-                                }
-                            }
-                        }
+                    // If the compose area contains only a single <br>, make it fully empty.
+                    // See also: https://stackoverflow.com/q/14638887/284318
+                    const text = composeArea.get_text(true);
+                    if (text === '\n') {
+                        composeDiv[0].innerText = '';
+                    }
 
-                        // Update typing information (use text instead method)
-                        if (text.trim().length === 0 || caretPosition === null) {
-                            stopTyping();
-                            scope.onTyping('');
-                        } else {
-                            startTyping();
-                            scope.onTyping(text.trim(), stringService.getWord(text, caretPosition.from));
-                        }
+                    // Update typing information
+                    if (text.trim().length === 0) {
+                        stopTyping();
+                        scope.onTyping('');
+                    } else {
+                        startTyping();
+                        scope.onTyping(text.trim());
+                    }
 
-                        updateView();
-                    }, 0);
+                    updateView();
                 }
 
                 // Function to fetch file contents
@@ -479,23 +448,17 @@ export default [
                     // Handle pasting of text
                     } else if (textIdx !== null) {
                         const text = ev.clipboardData.getData('text/plain');
-
-                        // Look up some filter functions
-                        // tslint:disable-next-line:max-line-length
-                        const escapeHtml = $filter('escapeHtml') as (a: string) => string;
-                        const mentionify = $filter('mentionify') as (a: string) => string;
-                        const nlToBr = $filter('nlToBr') as (a: string, b?: boolean) => string;
-
-                        // Escape HTML markup
-                        const escaped = escapeHtml(text);
-
-                        // Apply filters (emojify, convert newline, etc)
-                        const formatted = emojify(mentionify(replaceWhitespace(nlToBr(escaped, true))));
-
-                        // Insert resulting HTML
-                        document.execCommand('insertHTML', false, formatted);
-
-                        updateView();
+                        if (text) {
+                            const tokens = emojifyNew(text);
+                            for (const token of tokens) {
+                                if (isEmojiInfo(token)) {
+                                    insertEmoji(token);
+                                } else {
+                                    composeArea.insert_text(token);
+                                }
+                            }
+                            updateView();
+                        }
                     }
                 }
 
@@ -507,46 +470,48 @@ export default [
 
                 // Show emoji picker element
                 function showEmojiPicker() {
-                    const emojiPicker: HTMLElement = EmojiPickerContainer.get().htmlElement;
+                    // If the emoji picker is triggered too early, it's possible that the picker element
+                    // has not yet been fully loaded (e.g. during UI tests). Therefore enqueue the event
+                    // handler at the end of the event loop.
+                    $timeout(() => {
+                        const emojiPicker = wrapper[0].querySelector('div.twemoji-picker');
 
-                    // Show
-                    emojiKeyboard.addClass('active');
-                    emojiTrigger.addClass(TRIGGER_ACTIVE_CSS_CLASS);
+                        // Show
+                        emojiKeyboard.addClass('active');
+                        emojiTrigger.addClass(TRIGGER_ACTIVE_CSS_CLASS);
 
-                    // Find some selectors
-                    const allEmoji: any = angular.element(emojiPicker.querySelectorAll('.content .em'));
-                    const allEmojiTabs: any = angular.element(emojiPicker.querySelectorAll('.tab label img'));
+                        // Find some selectors
+                        const allEmoji = angular.element(emojiPicker.querySelectorAll('.content .em'));
+                        const allEmojiTabs = angular.element(emojiPicker.querySelectorAll('.tab label img'));
 
-                    // Add event handlers
-                    allEmoji.on('click', onEmojiChosen);
-                    allEmojiTabs.on('keydown', onEmojiTabSelected);
+                        // Add event handlers
+                        allEmoji.on('click', onEmojiChosen as any);
+                        allEmojiTabs.on('keydown', onEmojiTabSelected as any);
 
-                    // set focus to fix chat scroll bug
-                    $timeout(() => {
-                        composeDiv[0].focus();
+                        // Focus compose area again
+                        composeArea.focus();
                     });
                 }
 
                 // Hide emoji picker element
                 function hideEmojiPicker() {
-                    const emojiPicker: HTMLElement = EmojiPickerContainer.get().htmlElement;
+                    const emojiPicker = wrapper[0].querySelector('div.twemoji-picker');
 
                     // Hide
                     emojiKeyboard.removeClass('active');
                     emojiTrigger.removeClass(TRIGGER_ACTIVE_CSS_CLASS);
 
                     // Find some selectors
-                    const allEmoji: any = angular.element(emojiPicker.querySelectorAll('.content .em'));
-                    const allEmojiTabs: any = angular.element(emojiPicker.querySelectorAll('.tab label img'));
+                    const allEmoji = angular.element(emojiPicker.querySelectorAll('.content .em'));
+                    const allEmojiTabs = angular.element(emojiPicker.querySelectorAll('.tab label img'));
 
                     // Remove event handlers
-                    allEmoji.off('click', onEmojiChosen);
-                    allEmojiTabs.off('keydown', onEmojiTabSelected);
-                    EmojiPickerContainer.destroy();
+                    allEmoji.off('click', onEmojiChosen as any);
+                    allEmojiTabs.off('keydown', onEmojiTabSelected as any);
                 }
 
                 // Emoji trigger is clicked
-                function onEmojiTrigger(ev: UIEvent): void {
+                function onEmojiTrigger(ev: KeyboardEvent): void {
                     ev.stopPropagation();
                     if (chatBlocked) {
                         hideEmojiPicker();
@@ -563,124 +528,74 @@ export default [
                 // Emoji is chosen
                 function onEmojiChosen(ev: MouseEvent): void {
                     ev.stopPropagation();
-                    insertEmoji(this.textContent);
+                    insertSingleEmojiString((ev.target as Element).textContent);
                 }
 
                 // Emoji tab is selected
                 function onEmojiTabSelected(ev: KeyboardEvent): void {
                     if (ev.key === ' ' || ev.key === 'Enter') {
                         // Warning: Hacky
-                        this.parentElement.previousElementSibling.checked = true;
+                        (ev.target as any).parentElement.previousElementSibling.checked = true;
                     }
                 }
 
-                function insertEmoji(emoji, posFrom?: number, posTo?: number): void {
-                    const emojiElement = emojify(emoji);
-                    insertHTMLElement(emoji, emojiElement, posFrom, posTo);
+                // Insert a single emoji, passed in as string
+                function insertSingleEmojiString(emojiString: string): void {
+                    const tokens = emojifyNew(emojiString);
+                    if (tokens.length !== 1) {
+                        throw new Error(`Emoji parsing failed: Expected 1 element, found ${tokens.length}`);
+                    }
+                    const emoji = tokens[0];
+                    if (!isEmojiInfo(emoji)) {
+                        throw new Error(`Emoji parsing failed: Returned text, not emoji info`);
+                    }
+                    insertEmoji(emoji);
                 }
 
-                function insertMention(mentionString, posFrom?: number, posTo?: number): void {
-                    const mentionElement = ($filter('mentionify') as any)(mentionString) as string;
-                    insertHTMLElement(mentionString, mentionElement, posFrom, posTo);
+                // Insert a single emoji
+                function insertEmoji(emoji: threema.EmojiInfo): void {
+                    const img: HTMLElement = composeArea.insert_image(emoji.imgPath, emoji.emojiString, 'em');
+                    img.setAttribute('data-c', emoji.codepoint);
+                    img.draggable = false;
+                    img.ondragstart = () => false;
                 }
 
-                function insertHTMLElement(
-                    elementText: string, // The element as the original text representation, not yet converted to HTML
-                    elementHtml: string, // The element converted to HTML
-                    posFrom?: number,
-                    posTo?: number,
-                ): void {
-                    // In Chrome in right-to-left mode, our content editable
-                    // area may contain a DIV element.
-                    const childNodes = composeDiv[0].childNodes;
-                    const nestedDiv = childNodes.length === 1
-                        && childNodes[0].tagName !== undefined
-                        && childNodes[0].tagName.toLowerCase() === 'div';
-                    let contentElement;
-                    if (nestedDiv === true) {
-                        contentElement = composeDiv[0].childNodes[0];
-                    } else {
-                        contentElement = composeDiv[0];
+                // The emoji shortcode trigger (:) was inserted. Return a boolean
+                // indicating whether the compose area contents were modified.
+                //
+                // The `alreadyProcessed` indicates whether the key has already
+                // been processed in the DOM (onKeyUp) or not (onKeyDown).
+                function onEmojiShortcodeKeyPressed(ev, trigger: string, alreadyProcessed: boolean): boolean {
+                    const word = composeArea.get_word_at_caret();
+                    if (word === undefined) {
+                        return false;
                     }
-
-                    let currentHtml = '';
-                    for (let i = 0; i < contentElement.childNodes.length; i++) {
-                        const node: Node = contentElement.childNodes[i];
-
-                        if (isTextNode(node)) {
-                            currentHtml += node.textContent;
-                        } else if (isElementNode(node)) {
-                            const tag = node.tagName.toLowerCase();
-                            if (tag === 'img' || tag === 'span') {
-                                currentHtml += getOuterHtml(node);
-                            } else if (tag === 'br') {
-                                // Firefox inserts a <br> after editing content editable fields.
-                                // Remove the last <br> to fix this.
-                                if (i < contentElement.childNodes.length - 1) {
-                                    currentHtml += getOuterHtml(node);
-                                }
-                            } else if (tag === 'div') {
-                                // Safari inserts a <div><br></div> after editing content editable fields.
-                                // Remove the last instance to fix this.
-                                if (node.childNodes.length === 1
-                                    && isElementNode(node.lastChild)
-                                    && node.lastChild.tagName.toLowerCase() === 'br') {
-                                    // Ignore
-                                } else {
-                                    currentHtml += getOuterHtml(node);
-                                }
+                    let before = word.before();
+                    const after = word.after();
+                    if (!alreadyProcessed) {
+                        before += trigger;
+                    }
+                    if (after.length === 0 && before.length > 2) {
+                        if (before.startsWith(trigger) && before.endsWith(trigger)) {
+                            const trimmed = before.substr(1, before.length - 2);
+                            const unicodeEmoji = shortnameToUnicode(trimmed);
+                            if (unicodeEmoji !== null) {
+                                composeArea.select_word_at_caret();
+                                composeArea.store_selection_range();
+                                insertSingleEmojiString(unicodeEmoji);
+                                return true;
                             }
                         }
                     }
-
-                    // Because the browser may transform HTML code when
-                    // inserting it into the DOM, we temporarily write it to a
-                    // DOM element to ensure that the current representation
-                    // corresponds to the representation when inserted into the
-                    // DOM. (See #671 for details.)
-                    const tmpDiv = document.createElement('div');
-                    tmpDiv.innerHTML = elementHtml;
-                    const cleanedElementHtml = tmpDiv.innerHTML;
-
-                    // Insert element into currentHtml and determine new caret position
-                    let newPos = posFrom;
-                    if (caretPosition !== null) {
-                        // If the caret position is set, then the user has moved around
-                        // in the contenteditable field and might not be ad the end
-                        // of the line.
-                        posFrom = posFrom === undefined ? caretPosition.from : posFrom;
-                        posTo = posTo === undefined ? caretPosition.to : posTo;
-
-                        currentHtml = currentHtml.substr(0, posFrom)
-                            + cleanedElementHtml
-                            + currentHtml.substr(posTo);
-
-                        // Change caret position
-                        caretPosition.from += cleanedElementHtml.length;
-                        caretPosition.fromChar += elementText.length;
-                        newPos = posFrom + cleanedElementHtml.length;
-                    } else {
-                        // If the caret position is not set, then the user must be at the
-                        // end of the line. Insert element there.
-                        newPos = currentHtml.length;
-                        currentHtml += cleanedElementHtml;
-                        caretPosition = {
-                            from: currentHtml.length,
-                        };
-                    }
-                    caretPosition.to = caretPosition.from;
-                    caretPosition.toChar = caretPosition.fromChar;
-
-                    contentElement.innerHTML = currentHtml;
-                    setCaretPosition(newPos);
-
-                    // Update the draft text
-                    const text = extractText(composeDiv[0], logAdapter($log.warn, logTag));
-                    scope.onTyping(text);
-
-                    updateView();
+                    return false;
                 }
 
+                // TODO
+                // function insertMention(mentionString, posFrom?: number, posTo?: number): void {
+                //     const mentionElement = ($filter('mentionify') as any)(mentionString) as string;
+                //     insertHTMLElement(mentionString, mentionElement, posFrom, posTo);
+                // }
+
                 // File trigger is clicked
                 function onFileTrigger(ev: UIEvent): void {
                     ev.preventDefault();
@@ -688,7 +603,7 @@ export default [
                     if (chatBlocked) {
                         return;
                     }
-                    const input = element[0].querySelector('.file-input') as HTMLInputElement;
+                    const input = wrapper[0].querySelector('.file-input') as HTMLInputElement;
                     input.click();
                 }
 
@@ -716,137 +631,37 @@ export default [
                     }
                 }
 
-                // return the outer html of a node element
-                function getOuterHtml(node: Node): string {
-                    const pseudoElement = document.createElement('pseudo');
-                    pseudoElement.appendChild(node.cloneNode(true));
-                    return pseudoElement.innerHTML;
-                }
-
-                // return the html code position of the container element
-                function getPositions(offset: number, container: Node): { html: number, text: number } {
-                    let pos = null;
-                    let textPos = null;
-
-                    if (composeDiv[0].contains(container)) {
-                        let selectedElement;
-                        if (container === composeDiv[0]) {
-                            if (offset === 0) {
-                                return {
-                                    html: 0, text: 0,
-                                };
-                            }
-                            selectedElement = composeDiv[0].childNodes[offset - 1];
-                            pos = 0;
-                            textPos = 0;
-                        } else {
-                            selectedElement = container.previousSibling;
-                            pos = offset;
-                            textPos = offset;
-                        }
-
-                        while (selectedElement !== null) {
-                            if (selectedElement.nodeType === Node.TEXT_NODE) {
-                                pos += selectedElement.textContent.length;
-                                textPos += selectedElement.textContent.length;
-                            } else {
-                                pos += getOuterHtml(selectedElement).length;
-                                textPos += 1;
-                            }
-                            selectedElement = selectedElement.previousSibling;
-                        }
-                    }
-                    return {
-                        html: pos,
-                        text: textPos,
-                    };
-                }
-
-                // Update the current caret position or selection
-                function updateCaretPosition() {
-                    caretPosition = null;
-                    if (window.getSelection && composeDiv[0].innerHTML.length > 0) {
-                        const selection = window.getSelection();
-                        if (selection.rangeCount) {
-                            const range = selection.getRangeAt(0);
-                            const from = getPositions(range.startOffset, range.startContainer);
-                            if (from !== null && from.html >= 0) {
-                                const to = getPositions(range.endOffset, range.endContainer);
-                                caretPosition = {
-                                    from: from.html,
-                                    to: to.html,
-                                    fromChar: from.text,
-                                    toChar: to.text,
-                                };
-                            }
-                        }
-                    }
-                }
-
-                // Set the correct cart position in the content editable div.
-                // Pos is the position in the html content (not in the visible plain text).
-                function setCaretPosition(pos: number) {
-                    const rangeAt = (node: Node, offset?: number) => {
-                        const range = document.createRange();
-                        range.collapse(false);
-                        if (offset !== undefined) {
-                            range.setStart(node, offset);
-                        } else {
-                            range.setStartAfter(node);
-                        }
-                        const sel = window.getSelection();
-                        sel.removeAllRanges();
-                        sel.addRange(range);
-                    };
-
-                    for (let i = 0; i < composeDiv[0].childNodes.length; i++) {
-                        const node = composeDiv[0].childNodes[i];
-                        let size;
-                        let offset;
-                        switch (node.nodeType) {
-                            case Node.TEXT_NODE:
-                                size = node.textContent.length;
-                                offset = pos;
-                                break;
-                            case Node.ELEMENT_NODE:
-                                size = getOuterHtml(node).length;
-                                break;
-                            default:
-                                $log.warn(logTag, 'Unhandled node:', node);
-                        }
-
-                        if (pos < size) {
-                            // use this node
-                            rangeAt(node, offset);
-                        } else if (i === composeDiv[0].childNodes.length - 1) {
-                            rangeAt(node);
-                        }
-                        pos -= size;
-                    }
-                }
-
                 // Handle typing events
-                composeDiv.on('keydown', onKeyDown);
-                composeDiv.on('keyup', onKeyUp);
-                composeDiv.on('keyup mouseup', updateCaretPosition);
-                composeDiv.on('selectionchange', updateCaretPosition);
+                composeDiv.on('compositionstart', onCompositionStart as any);
+                composeDiv.on('compositionend', onCompositionEnd as any);
+                composeDiv.on('keydown', onKeyDown as any);
+                composeDiv.on('keyup', onKeyUp as any);
+
+                // Handle selection change
+                document.addEventListener('selectionchange', () => {
+                    composeArea.store_selection_range();
+                });
 
                 // Handle paste event
-                composeDiv.on('paste', onPaste);
+                composeDiv.on('paste', onPaste as any);
 
                 // Handle click on emoji trigger
-                emojiTrigger.on('click', onEmojiTrigger);
-                emojiTrigger.on('keypress', (ev: KeyboardEvent) => {
+                emojiTrigger.on('click', onEmojiTrigger as any);
+                emojiTrigger[0].addEventListener('keypress', (ev: KeyboardEvent) => {
                     if (isActionTrigger(ev)) {
-                        onEmojiTrigger(ev);
+                        $rootScope.$apply(() => {
+                            onEmojiTrigger(ev);
+                        });
                     }
                 });
 
                 // Handle click on file trigger
-                fileTrigger.on('click', onFileTrigger);
-                fileTrigger.on('keypress', (ev: KeyboardEvent) => {
+                fileTrigger.on('click', onFileTrigger as any);
+                fileTrigger[0].addEventListener('keypress', (ev: any) => {
                     if (isActionTrigger(ev)) {
-                        onFileTrigger(ev);
+                        $rootScope.$apply(() => {
+                            onFileTrigger(ev);
+                        });
                     }
                 });
 
@@ -854,10 +669,12 @@ export default [
                 fileInput.on('change', onFileSelected);
 
                 // Handle click on send trigger
-                sendTrigger.on('click', onSendTrigger);
-                sendTrigger.on('keypress', (ev: KeyboardEvent) => {
+                sendTrigger.on('click', onSendTrigger as any);
+                sendTrigger[0].addEventListener('keypress', (ev: any) => {
                     if (isActionTrigger(ev)) {
-                        onSendTrigger(ev);
+                        $rootScope.$apply(() => {
+                            onSendTrigger(ev);
+                        });
                     }
                 });
 
@@ -866,15 +683,7 @@ export default [
                 // Listen to broadcasts
                 const unsubscribeListeners = [];
                 unsubscribeListeners.push($rootScope.$on('onQuoted', (event: ng.IAngularEvent, args: any) => {
-                    composeDiv[0].focus();
-                }));
-
-                unsubscribeListeners.push($rootScope.$on('onMentionSelected', (event: ng.IAngularEvent, args: any) => {
-                    if (args.query && args.mention) {
-                        // Insert resulting HTML
-                        insertMention(args.mention, caretPosition ? caretPosition.to - args.query.length : null,
-                            caretPosition ? caretPosition.to : null);
-                    }
+                    composeArea.focus();
                 }));
 
                 // When switching chat, send stopTyping message
@@ -895,6 +704,7 @@ export default [
                     <div>
                         <div
                             class="compose"
+                            id="composeDiv"
                             contenteditable
                             autofocus
                             translate

+ 0 - 55
src/helpers.ts

@@ -396,61 +396,6 @@ export function copyDeep<T extends object>(obj: T): T {
     return JSON.parse(JSON.stringify(obj));
 }
 
-/**
- * Process a DOM node recursively and extract text from compose area.
- */
-export function extractText(targetNode: HTMLElement, logWarning: (msg: string) => void, trim = true) {
-    let text = '';
-    const visitChildNodes = (parentNode: HTMLElement) => {
-        // When pressing shift-enter and typing more text:
-        //
-        // - Firefox and chrome insert a <br> between two text nodes
-        // - Safari creates two <div>s without any line break in between
-        //   (except for the first line, which stays plain text)
-        //
-        // Thus, for Safari, we need to detect <div>s and insert a newline.
-
-        let lastNodeType;
-        // tslint:disable-next-line: prefer-for-of (see #98)
-        for (let i = 0; i < parentNode.childNodes.length; i++) {
-            const node = parentNode.childNodes[i] as HTMLElement;
-            switch (node.nodeType) {
-                case Node.TEXT_NODE:
-                    lastNodeType = 'text';
-                    // Append text, but strip leading and trailing newlines
-                    text += node.nodeValue.replace(/(^[\r\n]*|[\r\n]*$)/g, '');
-                    break;
-                case Node.ELEMENT_NODE:
-                    const tag = node.tagName.toLowerCase();
-                    const _lastNodeType = lastNodeType;
-                    lastNodeType = tag;
-                    if (tag === 'div') {
-                        text += '\n';
-                        visitChildNodes(node);
-                        break;
-                    } else if (tag === 'img') {
-                        if (_lastNodeType === 'div') {
-                            // An image following a div should go on a new line
-                            text += '\n';
-                        }
-                        text += (node as HTMLImageElement).alt;
-                        break;
-                    } else if (tag === 'br') {
-                        text += '\n';
-                        break;
-                    } else if (tag === 'span' && node.hasAttribute('text')) {
-                        text += node.getAttributeNode('text').value;
-                        break;
-                    }
-                default:
-                    logWarning(`Unhandled node: ${node}`);
-            }
-        }
-    };
-    visitChildNodes(targetNode);
-    return trim ? text.trim() : text;
-}
-
 /**
  * Replace spaces with `&nbsp;` and tabs with `&nbsp;&nbsp;`.
  */

Разница между файлами не показана из-за своего большого размера
+ 3522 - 3519
src/helpers/emoji.ts


+ 198 - 198
src/partials/emoji-picker.html

@@ -13,7 +13,7 @@
             <span class="em em-people-1f605" data-c="1f605" data-s=":sweat_smile:" title="grinning face with sweat">&#x1f605;</span>
             <span class="em em-people-1f602" data-c="1f602" data-s=":joy:" title="face with tears of joy">&#x1f602;</span>
             <span class="em em-people-1f923" data-c="1f923" data-s=":rofl:" title="rolling on the floor laughing">&#x1f923;</span>
-            <span class="em em-people-263a" data-c="263a" data-s=":relaxed:" title="smiling face">&#x263a;</span>
+            <span class="em em-people-263a" data-c="263a" data-s=":relaxed:" title="smiling face">&#x263a;&#xfe0f;</span>
             <span class="em em-people-1f60a" data-c="1f60a" data-s=":blush:" title="smiling face with smiling eyes">&#x1f60a;</span>
             <span class="em em-people-1f607" data-c="1f607" data-s=":innocent:" title="smiling face with halo">&#x1f607;</span>
             <span class="em em-people-1f642" data-c="1f642" data-s=":slight_smile:" title="slightly smiling face">&#x1f642;</span>
@@ -44,7 +44,7 @@
             <span class="em em-people-1f61f" data-c="1f61f" data-s=":worried:" title="worried face">&#x1f61f;</span>
             <span class="em em-people-1f615" data-c="1f615" data-s=":confused:" title="confused face">&#x1f615;</span>
             <span class="em em-people-1f641" data-c="1f641" data-s=":slight_frown:" title="slightly frowning face">&#x1f641;</span>
-            <span class="em em-people-2639" data-c="2639" data-s=":frowning2:" title="frowning face">&#x2639;</span>
+            <span class="em em-people-2639" data-c="2639" data-s=":frowning2:" title="frowning face">&#x2639;&#xfe0f;</span>
             <span class="em em-people-1f623" data-c="1f623" data-s=":persevere:" title="persevering face">&#x1f623;</span>
             <span class="em em-people-1f616" data-c="1f616" data-s=":confounded:" title="confounded face">&#x1f616;</span>
             <span class="em em-people-1f62b" data-c="1f62b" data-s=":tired_face:" title="tired face">&#x1f62b;</span>
@@ -102,7 +102,7 @@
             <span class="em em-people-1f4a9" data-c="1f4a9" data-s=":poop:" title="pile of poo">&#x1f4a9;</span>
             <span class="em em-people-1f47b" data-c="1f47b" data-s=":ghost:" title="ghost">&#x1f47b;</span>
             <span class="em em-people-1f480" data-c="1f480" data-s=":skull:" title="skull">&#x1f480;</span>
-            <span class="em em-people-2620" data-c="2620" data-s=":skull_crossbones:" title="skull and crossbones">&#x2620;</span>
+            <span class="em em-people-2620" data-c="2620" data-s=":skull_crossbones:" title="skull and crossbones">&#x2620;&#xfe0f;</span>
             <span class="em em-people-1f47d" data-c="1f47d" data-s=":alien:" title="alien">&#x1f47d;</span>
             <span class="em em-people-1f47e" data-c="1f47e" data-s=":space_invader:" title="alien monster">&#x1f47e;</span>
             <span class="em em-people-1f916" data-c="1f916" data-s=":robot:" title="robot face">&#x1f916;</span>
@@ -183,7 +183,7 @@
             <span class="em em-people-1f91e-1f3fd" data-c="1f91e-1f3fd" data-s=":fingers_crossed_tone3:" title="crossed fingers: medium skin tone">&#x1f91e;&#x1f3fd;</span>
             <span class="em em-people-1f91e-1f3fe" data-c="1f91e-1f3fe" data-s=":fingers_crossed_tone4:" title="crossed fingers: medium-dark skin tone">&#x1f91e;&#x1f3fe;</span>
             <span class="em em-people-1f91e-1f3ff" data-c="1f91e-1f3ff" data-s=":fingers_crossed_tone5:" title="crossed fingers: dark skin tone">&#x1f91e;&#x1f3ff;</span>
-            <span class="em em-people-270c" data-c="270c" data-s=":v:" title="victory hand">&#x270c;</span>
+            <span class="em em-people-270c" data-c="270c" data-s=":v:" title="victory hand">&#x270c;&#xfe0f;</span>
             <span class="em em-people-270c-1f3fb" data-c="270c-1f3fb" data-s=":v_tone1:" title="victory hand: light skin tone">&#x270c;&#x1f3fb;</span>
             <span class="em em-people-270c-1f3fc" data-c="270c-1f3fc" data-s=":v_tone2:" title="victory hand: medium-light skin tone">&#x270c;&#x1f3fc;</span>
             <span class="em em-people-270c-1f3fd" data-c="270c-1f3fd" data-s=":v_tone3:" title="victory hand: medium skin tone">&#x270c;&#x1f3fd;</span>
@@ -231,7 +231,7 @@
             <span class="em em-people-1f447-1f3fd" data-c="1f447-1f3fd" data-s=":point_down_tone3:" title="backhand index pointing down: medium skin tone">&#x1f447;&#x1f3fd;</span>
             <span class="em em-people-1f447-1f3fe" data-c="1f447-1f3fe" data-s=":point_down_tone4:" title="backhand index pointing down: medium-dark skin tone">&#x1f447;&#x1f3fe;</span>
             <span class="em em-people-1f447-1f3ff" data-c="1f447-1f3ff" data-s=":point_down_tone5:" title="backhand index pointing down: dark skin tone">&#x1f447;&#x1f3ff;</span>
-            <span class="em em-people-261d" data-c="261d" data-s=":point_up:" title="index pointing up">&#x261d;</span>
+            <span class="em em-people-261d" data-c="261d" data-s=":point_up:" title="index pointing up">&#x261d;&#xfe0f;</span>
             <span class="em em-people-261d-1f3fb" data-c="261d-1f3fb" data-s=":point_up_tone1:" title="index pointing up: light skin tone">&#x261d;&#x1f3fb;</span>
             <span class="em em-people-261d-1f3fc" data-c="261d-1f3fc" data-s=":point_up_tone2:" title="index pointing up: medium-light skin tone">&#x261d;&#x1f3fc;</span>
             <span class="em em-people-261d-1f3fd" data-c="261d-1f3fd" data-s=":point_up_tone3:" title="index pointing up: medium skin tone">&#x261d;&#x1f3fd;</span>
@@ -249,7 +249,7 @@
             <span class="em em-people-1f91a-1f3fd" data-c="1f91a-1f3fd" data-s=":raised_back_of_hand_tone3:" title="raised back of hand: medium skin tone">&#x1f91a;&#x1f3fd;</span>
             <span class="em em-people-1f91a-1f3fe" data-c="1f91a-1f3fe" data-s=":raised_back_of_hand_tone4:" title="raised back of hand: medium-dark skin tone">&#x1f91a;&#x1f3fe;</span>
             <span class="em em-people-1f91a-1f3ff" data-c="1f91a-1f3ff" data-s=":raised_back_of_hand_tone5:" title="raised back of hand: dark skin tone">&#x1f91a;&#x1f3ff;</span>
-            <span class="em em-people-1f590" data-c="1f590" data-s=":hand_splayed:" title="hand with fingers splayed">&#x1f590;</span>
+            <span class="em em-people-1f590" data-c="1f590" data-s=":hand_splayed:" title="hand with fingers splayed">&#x1f590;&#xfe0f;</span>
             <span class="em em-people-1f590-1f3fb" data-c="1f590-1f3fb" data-s=":hand_splayed_tone1:" title="hand with fingers splayed: light skin tone">&#x1f590;&#x1f3fb;</span>
             <span class="em em-people-1f590-1f3fc" data-c="1f590-1f3fc" data-s=":hand_splayed_tone2:" title="hand with fingers splayed: medium-light skin tone">&#x1f590;&#x1f3fc;</span>
             <span class="em em-people-1f590-1f3fd" data-c="1f590-1f3fd" data-s=":hand_splayed_tone3:" title="hand with fingers splayed: medium skin tone">&#x1f590;&#x1f3fd;</span>
@@ -297,7 +297,7 @@
             <span class="em em-people-1f595-1f3fd" data-c="1f595-1f3fd" data-s=":middle_finger_tone3:" title="middle finger: medium skin tone">&#x1f595;&#x1f3fd;</span>
             <span class="em em-people-1f595-1f3fe" data-c="1f595-1f3fe" data-s=":middle_finger_tone4:" title="middle finger: medium-dark skin tone">&#x1f595;&#x1f3fe;</span>
             <span class="em em-people-1f595-1f3ff" data-c="1f595-1f3ff" data-s=":middle_finger_tone5:" title="middle finger: dark skin tone">&#x1f595;&#x1f3ff;</span>
-            <span class="em em-people-270d" data-c="270d" data-s=":writing_hand:" title="writing hand">&#x270d;</span>
+            <span class="em em-people-270d" data-c="270d" data-s=":writing_hand:" title="writing hand">&#x270d;&#xfe0f;</span>
             <span class="em em-people-270d-1f3fb" data-c="270d-1f3fb" data-s=":writing_hand_tone1:" title="writing hand: light skin tone">&#x270d;&#x1f3fb;</span>
             <span class="em em-people-270d-1f3fc" data-c="270d-1f3fc" data-s=":writing_hand_tone2:" title="writing hand: medium-light skin tone">&#x270d;&#x1f3fc;</span>
             <span class="em em-people-270d-1f3fd" data-c="270d-1f3fd" data-s=":writing_hand_tone3:" title="writing hand: medium skin tone">&#x270d;&#x1f3fd;</span>
@@ -327,12 +327,12 @@
             <span class="em em-people-1f443-1f3fe" data-c="1f443-1f3fe" data-s=":nose_tone4:" title="nose: medium-dark skin tone">&#x1f443;&#x1f3fe;</span>
             <span class="em em-people-1f443-1f3ff" data-c="1f443-1f3ff" data-s=":nose_tone5:" title="nose: dark skin tone">&#x1f443;&#x1f3ff;</span>
             <span class="em em-people-1f463" data-c="1f463" data-s=":footprints:" title="footprints">&#x1f463;</span>
-            <span class="em em-people-1f441" data-c="1f441" data-s=":eye:" title="eye">&#x1f441;</span>
+            <span class="em em-people-1f441" data-c="1f441" data-s=":eye:" title="eye">&#x1f441;&#xfe0f;</span>
             <span class="em em-people-1f440" data-c="1f440" data-s=":eyes:" title="eyes">&#x1f440;</span>
             <span class="em em-people-1f9e0" data-c="1f9e0" data-s=":brain:" title="brain">&#x1f9e0;</span>
             <span class="em em-people-1f9b4" data-c="1f9b4" data-s=":bone:" title="bone">&#x1f9b4;</span>
             <span class="em em-people-1f9b7" data-c="1f9b7" data-s=":tooth:" title="tooth">&#x1f9b7;</span>
-            <span class="em em-people-1f5e3" data-c="1f5e3" data-s=":speaking_head:" title="speaking head">&#x1f5e3;</span>
+            <span class="em em-people-1f5e3" data-c="1f5e3" data-s=":speaking_head:" title="speaking head">&#x1f5e3;&#xfe0f;</span>
             <span class="em em-people-1f464" data-c="1f464" data-s=":bust_in_silhouette:" title="bust in silhouette">&#x1f464;</span>
             <span class="em em-people-1f465" data-c="1f465" data-s=":busts_in_silhouette:" title="busts in silhouette">&#x1f465;</span>
             <span class="em em-people-1f476" data-c="1f476" data-s=":baby:" title="baby">&#x1f476;</span>
@@ -521,7 +521,7 @@
             <span class="em em-people-1f482-1f3fd-200d-2640-fe0f" data-c="1f482-1f3fd-200d-2640-fe0f" data-s=":woman_guard_tone3:" title="woman guard: medium skin tone">&#x1f482;&#x1f3fd;&#x200d;&#x2640;&#xfe0f;</span>
             <span class="em em-people-1f482-1f3fe-200d-2640-fe0f" data-c="1f482-1f3fe-200d-2640-fe0f" data-s=":woman_guard_tone4:" title="woman guard: medium-dark skin tone">&#x1f482;&#x1f3fe;&#x200d;&#x2640;&#xfe0f;</span>
             <span class="em em-people-1f482-1f3ff-200d-2640-fe0f" data-c="1f482-1f3ff-200d-2640-fe0f" data-s=":woman_guard_tone5:" title="woman guard: dark skin tone">&#x1f482;&#x1f3ff;&#x200d;&#x2640;&#xfe0f;</span>
-            <span class="em em-people-1f575" data-c="1f575" data-s=":detective:" title="detective">&#x1f575;</span>
+            <span class="em em-people-1f575" data-c="1f575" data-s=":detective:" title="detective">&#x1f575;&#xfe0f;</span>
             <span class="em em-people-1f575-1f3fb" data-c="1f575-1f3fb" data-s=":detective_tone1:" title="detective: light skin tone">&#x1f575;&#x1f3fb;</span>
             <span class="em em-people-1f575-1f3fc" data-c="1f575-1f3fc" data-s=":detective_tone2:" title="detective: medium-light skin tone">&#x1f575;&#x1f3fc;</span>
             <span class="em em-people-1f575-1f3fd" data-c="1f575-1f3fd" data-s=":detective_tone3:" title="detective: medium skin tone">&#x1f575;&#x1f3fd;</span>
@@ -1037,7 +1037,7 @@
             <span class="em em-people-1f57a-1f3fe" data-c="1f57a-1f3fe" data-s=":man_dancing_tone4:" title="man dancing: medium-dark skin tone">&#x1f57a;&#x1f3fe;</span>
             <span class="em em-people-1f46f" data-c="1f46f" data-s=":people_with_bunny_ears_partying:" title="people with bunny ears">&#x1f46f;</span>
             <span class="em em-people-1f46f-200d-2642-fe0f" data-c="1f46f-200d-2642-fe0f" data-s=":men_with_bunny_ears_partying:" title="men with bunny ears">&#x1f46f;&#x200d;&#x2642;&#xfe0f;</span>
-            <span class="em em-people-1f574" data-c="1f574" data-s=":levitate:" title="man in suit levitating">&#x1f574;</span>
+            <span class="em em-people-1f574" data-c="1f574" data-s=":levitate:" title="man in suit levitating">&#x1f574;&#xfe0f;</span>
             <span class="em em-people-1f574-1f3fb" data-c="1f574-1f3fb" data-s=":levitate_tone1:" title="man in suit levitating: light skin tone">&#x1f574;&#x1f3fb;</span>
             <span class="em em-people-1f574-1f3fc" data-c="1f574-1f3fc" data-s=":levitate_tone2:" title="man in suit levitating: medium-light skin tone">&#x1f574;&#x1f3fc;</span>
             <span class="em em-people-1f574-1f3fd" data-c="1f574-1f3fd" data-s=":levitate_tone3:" title="man in suit levitating: medium skin tone">&#x1f574;&#x1f3fd;</span>
@@ -1124,7 +1124,7 @@
             <span class="em em-people-1f9e2" data-c="1f9e2" data-s=":billed_cap:" title="billed cap">&#x1f9e2;</span>
             <span class="em em-people-1f452" data-c="1f452" data-s=":womans_hat:" title="woman’s hat">&#x1f452;</span>
             <span class="em em-people-1f393" data-c="1f393" data-s=":mortar_board:" title="graduation cap">&#x1f393;</span>
-            <span class="em em-people-26d1" data-c="26d1" data-s=":helmet_with_cross:" title="rescue worker’s helmet">&#x26d1;</span>
+            <span class="em em-people-26d1" data-c="26d1" data-s=":helmet_with_cross:" title="rescue worker’s helmet">&#x26d1;&#xfe0f;</span>
             <span class="em em-people-1f451" data-c="1f451" data-s=":crown:" title="crown">&#x1f451;</span>
             <span class="em em-people-1f45d" data-c="1f45d" data-s=":pouch:" title="clutch bag">&#x1f45d;</span>
             <span class="em em-people-1f45b" data-c="1f45b" data-s=":purse:" title="purse">&#x1f45b;</span>
@@ -1132,7 +1132,7 @@
             <span class="em em-people-1f4bc" data-c="1f4bc" data-s=":briefcase:" title="briefcase">&#x1f4bc;</span>
             <span class="em em-people-1f392" data-c="1f392" data-s=":school_satchel:" title="school backpack">&#x1f392;</span>
             <span class="em em-people-1f453" data-c="1f453" data-s=":eyeglasses:" title="glasses">&#x1f453;</span>
-            <span class="em em-people-1f576" data-c="1f576" data-s=":dark_sunglasses:" title="sunglasses">&#x1f576;</span>
+            <span class="em em-people-1f576" data-c="1f576" data-s=":dark_sunglasses:" title="sunglasses">&#x1f576;&#xfe0f;</span>
             <span class="em em-people-1f97d" data-c="1f97d" data-s=":goggles:" title="goggles">&#x1f97d;</span>
             <span class="em em-people-1f302" data-c="1f302" data-s=":closed_umbrella:" title="closed umbrella">&#x1f302;</span>
         </div>
@@ -1191,8 +1191,8 @@
             <span class="em em-nature-1f41e" data-c="1f41e" data-s=":beetle:" title="lady beetle">&#x1f41e;</span>
             <span class="em em-nature-1f41c" data-c="1f41c" data-s=":ant:" title="ant">&#x1f41c;</span>
             <span class="em em-nature-1f997" data-c="1f997" data-s=":cricket:" title="cricket">&#x1f997;</span>
-            <span class="em em-nature-1f577" data-c="1f577" data-s=":spider:" title="spider">&#x1f577;</span>
-            <span class="em em-nature-1f578" data-c="1f578" data-s=":spider_web:" title="spider web">&#x1f578;</span>
+            <span class="em em-nature-1f577" data-c="1f577" data-s=":spider:" title="spider">&#x1f577;&#xfe0f;</span>
+            <span class="em em-nature-1f578" data-c="1f578" data-s=":spider_web:" title="spider web">&#x1f578;&#xfe0f;</span>
             <span class="em em-nature-1f982" data-c="1f982" data-s=":scorpion:" title="scorpion">&#x1f982;</span>
             <span class="em em-nature-1f99f" data-c="1f99f" data-s=":mosquito:" title="mosquito">&#x1f99f;</span>
             <span class="em em-nature-1f9a0" data-c="1f9a0" data-s=":microbe:" title="microbe">&#x1f9a0;</span>
@@ -1239,11 +1239,11 @@
             <span class="em em-nature-1f408" data-c="1f408" data-s=":cat2:" title="cat">&#x1f408;</span>
             <span class="em em-nature-1f413" data-c="1f413" data-s=":rooster:" title="rooster">&#x1f413;</span>
             <span class="em em-nature-1f983" data-c="1f983" data-s=":turkey:" title="turkey">&#x1f983;</span>
-            <span class="em em-nature-1f54a" data-c="1f54a" data-s=":dove:" title="dove">&#x1f54a;</span>
+            <span class="em em-nature-1f54a" data-c="1f54a" data-s=":dove:" title="dove">&#x1f54a;&#xfe0f;</span>
             <span class="em em-nature-1f407" data-c="1f407" data-s=":rabbit2:" title="rabbit">&#x1f407;</span>
             <span class="em em-nature-1f401" data-c="1f401" data-s=":mouse2:" title="mouse">&#x1f401;</span>
             <span class="em em-nature-1f400" data-c="1f400" data-s=":rat:" title="rat">&#x1f400;</span>
-            <span class="em em-nature-1f43f" data-c="1f43f" data-s=":chipmunk:" title="chipmunk">&#x1f43f;</span>
+            <span class="em em-nature-1f43f" data-c="1f43f" data-s=":chipmunk:" title="chipmunk">&#x1f43f;&#xfe0f;</span>
             <span class="em em-nature-1f994" data-c="1f994" data-s=":hedgehog:" title="hedgehog">&#x1f994;</span>
             <span class="em em-nature-1f43e" data-c="1f43e" data-s=":feet:" title="paw prints">&#x1f43e;</span>
             <span class="em em-nature-1f409" data-c="1f409" data-s=":dragon:" title="dragon">&#x1f409;</span>
@@ -1255,7 +1255,7 @@
             <span class="em em-nature-1f334" data-c="1f334" data-s=":palm_tree:" title="palm tree">&#x1f334;</span>
             <span class="em em-nature-1f331" data-c="1f331" data-s=":seedling:" title="seedling">&#x1f331;</span>
             <span class="em em-nature-1f33f" data-c="1f33f" data-s=":herb:" title="herb">&#x1f33f;</span>
-            <span class="em em-nature-2618" data-c="2618" data-s=":shamrock:" title="shamrock">&#x2618;</span>
+            <span class="em em-nature-2618" data-c="2618" data-s=":shamrock:" title="shamrock">&#x2618;&#xfe0f;</span>
             <span class="em em-nature-1f340" data-c="1f340" data-s=":four_leaf_clover:" title="four leaf clover">&#x1f340;</span>
             <span class="em em-nature-1f38d" data-c="1f38d" data-s=":bamboo:" title="pine decoration">&#x1f38d;</span>
             <span class="em em-nature-1f38b" data-c="1f38b" data-s=":tanabata_tree:" title="tanabata tree">&#x1f38b;</span>
@@ -1294,32 +1294,32 @@
             <span class="em em-nature-1f31f" data-c="1f31f" data-s=":star2:" title="glowing star">&#x1f31f;</span>
             <span class="em em-nature-2728" data-c="2728" data-s=":sparkles:" title="sparkles">&#x2728;</span>
             <span class="em em-nature-26a1" data-c="26a1" data-s=":zap:" title="high voltage">&#x26a1;</span>
-            <span class="em em-nature-2604" data-c="2604" data-s=":comet:" title="comet">&#x2604;</span>
+            <span class="em em-nature-2604" data-c="2604" data-s=":comet:" title="comet">&#x2604;&#xfe0f;</span>
             <span class="em em-nature-1f4a5" data-c="1f4a5" data-s=":boom:" title="collision">&#x1f4a5;</span>
             <span class="em em-nature-1f525" data-c="1f525" data-s=":fire:" title="fire">&#x1f525;</span>
-            <span class="em em-nature-1f32a" data-c="1f32a" data-s=":cloud_tornado:" title="tornado">&#x1f32a;</span>
+            <span class="em em-nature-1f32a" data-c="1f32a" data-s=":cloud_tornado:" title="tornado">&#x1f32a;&#xfe0f;</span>
             <span class="em em-nature-1f308" data-c="1f308" data-s=":rainbow:" title="rainbow">&#x1f308;</span>
-            <span class="em em-nature-2600" data-c="2600" data-s=":sunny:" title="sun">&#x2600;</span>
-            <span class="em em-nature-1f324" data-c="1f324" data-s=":white_sun_small_cloud:" title="sun behind small cloud">&#x1f324;</span>
+            <span class="em em-nature-2600" data-c="2600" data-s=":sunny:" title="sun">&#x2600;&#xfe0f;</span>
+            <span class="em em-nature-1f324" data-c="1f324" data-s=":white_sun_small_cloud:" title="sun behind small cloud">&#x1f324;&#xfe0f;</span>
             <span class="em em-nature-26c5" data-c="26c5" data-s=":partly_sunny:" title="sun behind cloud">&#x26c5;</span>
-            <span class="em em-nature-1f325" data-c="1f325" data-s=":white_sun_cloud:" title="sun behind large cloud">&#x1f325;</span>
-            <span class="em em-nature-2601" data-c="2601" data-s=":cloud:" title="cloud">&#x2601;</span>
-            <span class="em em-nature-1f326" data-c="1f326" data-s=":white_sun_rain_cloud:" title="sun behind rain cloud">&#x1f326;</span>
-            <span class="em em-nature-1f327" data-c="1f327" data-s=":cloud_rain:" title="cloud with rain">&#x1f327;</span>
-            <span class="em em-nature-26c8" data-c="26c8" data-s=":thunder_cloud_rain:" title="cloud with lightning and rain">&#x26c8;</span>
-            <span class="em em-nature-1f329" data-c="1f329" data-s=":cloud_lightning:" title="cloud with lightning">&#x1f329;</span>
-            <span class="em em-nature-1f328" data-c="1f328" data-s=":cloud_snow:" title="cloud with snow">&#x1f328;</span>
-            <span class="em em-nature-2744" data-c="2744" data-s=":snowflake:" title="snowflake">&#x2744;</span>
-            <span class="em em-nature-2603" data-c="2603" data-s=":snowman2:" title="snowman">&#x2603;</span>
+            <span class="em em-nature-1f325" data-c="1f325" data-s=":white_sun_cloud:" title="sun behind large cloud">&#x1f325;&#xfe0f;</span>
+            <span class="em em-nature-2601" data-c="2601" data-s=":cloud:" title="cloud">&#x2601;&#xfe0f;</span>
+            <span class="em em-nature-1f326" data-c="1f326" data-s=":white_sun_rain_cloud:" title="sun behind rain cloud">&#x1f326;&#xfe0f;</span>
+            <span class="em em-nature-1f327" data-c="1f327" data-s=":cloud_rain:" title="cloud with rain">&#x1f327;&#xfe0f;</span>
+            <span class="em em-nature-26c8" data-c="26c8" data-s=":thunder_cloud_rain:" title="cloud with lightning and rain">&#x26c8;&#xfe0f;</span>
+            <span class="em em-nature-1f329" data-c="1f329" data-s=":cloud_lightning:" title="cloud with lightning">&#x1f329;&#xfe0f;</span>
+            <span class="em em-nature-1f328" data-c="1f328" data-s=":cloud_snow:" title="cloud with snow">&#x1f328;&#xfe0f;</span>
+            <span class="em em-nature-2744" data-c="2744" data-s=":snowflake:" title="snowflake">&#x2744;&#xfe0f;</span>
+            <span class="em em-nature-2603" data-c="2603" data-s=":snowman2:" title="snowman">&#x2603;&#xfe0f;</span>
             <span class="em em-nature-26c4" data-c="26c4" data-s=":snowman:" title="snowman without snow">&#x26c4;</span>
-            <span class="em em-nature-1f32c" data-c="1f32c" data-s=":wind_blowing_face:" title="wind face">&#x1f32c;</span>
+            <span class="em em-nature-1f32c" data-c="1f32c" data-s=":wind_blowing_face:" title="wind face">&#x1f32c;&#xfe0f;</span>
             <span class="em em-nature-1f4a8" data-c="1f4a8" data-s=":dash:" title="dashing away">&#x1f4a8;</span>
             <span class="em em-nature-1f4a7" data-c="1f4a7" data-s=":droplet:" title="droplet">&#x1f4a7;</span>
             <span class="em em-nature-1f4a6" data-c="1f4a6" data-s=":sweat_drops:" title="sweat droplets">&#x1f4a6;</span>
             <span class="em em-nature-2614" data-c="2614" data-s=":umbrella:" title="umbrella with rain drops">&#x2614;</span>
-            <span class="em em-nature-2602" data-c="2602" data-s=":umbrella2:" title="umbrella">&#x2602;</span>
+            <span class="em em-nature-2602" data-c="2602" data-s=":umbrella2:" title="umbrella">&#x2602;&#xfe0f;</span>
             <span class="em em-nature-1f30a" data-c="1f30a" data-s=":ocean:" title="water wave">&#x1f30a;</span>
-            <span class="em em-nature-1f32b" data-c="1f32b" data-s=":fog:" title="fog">&#x1f32b;</span>
+            <span class="em em-nature-1f32b" data-c="1f32b" data-s=":fog:" title="fog">&#x1f32b;&#xfe0f;</span>
         </div>
    </div>
     <div class="tab">
@@ -1350,7 +1350,7 @@
             <span class="em em-food-1f966" data-c="1f966" data-s=":broccoli:" title="broccoli">&#x1f966;</span>
             <span class="em em-food-1f96c" data-c="1f96c" data-s=":leafy_green:" title="leafy green">&#x1f96c;</span>
             <span class="em em-food-1f952" data-c="1f952" data-s=":cucumber:" title="cucumber">&#x1f952;</span>
-            <span class="em em-food-1f336" data-c="1f336" data-s=":hot_pepper:" title="hot pepper">&#x1f336;</span>
+            <span class="em em-food-1f336" data-c="1f336" data-s=":hot_pepper:" title="hot pepper">&#x1f336;&#xfe0f;</span>
             <span class="em em-food-1f33d" data-c="1f33d" data-s=":corn:" title="ear of corn">&#x1f33d;</span>
             <span class="em em-food-1f955" data-c="1f955" data-s=":carrot:" title="carrot">&#x1f955;</span>
             <span class="em em-food-1f954" data-c="1f954" data-s=":potato:" title="potato">&#x1f954;</span>
@@ -1429,7 +1429,7 @@
             <span class="em em-food-1f37e" data-c="1f37e" data-s=":champagne:" title="bottle with popping cork">&#x1f37e;</span>
             <span class="em em-food-1f944" data-c="1f944" data-s=":spoon:" title="spoon">&#x1f944;</span>
             <span class="em em-food-1f374" data-c="1f374" data-s=":fork_and_knife:" title="fork and knife">&#x1f374;</span>
-            <span class="em em-food-1f37d" data-c="1f37d" data-s=":fork_knife_plate:" title="fork and knife with plate">&#x1f37d;</span>
+            <span class="em em-food-1f37d" data-c="1f37d" data-s=":fork_knife_plate:" title="fork and knife with plate">&#x1f37d;&#xfe0f;</span>
             <span class="em em-food-1f963" data-c="1f963" data-s=":bowl_with_spoon:" title="bowl with spoon">&#x1f963;</span>
             <span class="em em-food-1f961" data-c="1f961" data-s=":takeout_box:" title="takeout box">&#x1f961;</span>
             <span class="em em-food-1f962" data-c="1f962" data-s=":chopsticks:" title="chopsticks">&#x1f962;</span>
@@ -1465,18 +1465,18 @@
             <span class="em em-activity-1f94b" data-c="1f94b" data-s=":martial_arts_uniform:" title="martial arts uniform">&#x1f94b;</span>
             <span class="em em-activity-1f3bd" data-c="1f3bd" data-s=":running_shirt_with_sash:" title="running shirt">&#x1f3bd;</span>
             <span class="em em-activity-1f6f9" data-c="1f6f9" data-s=":skateboard:" title="skateboard">&#x1f6f9;</span>
-            <span class="em em-activity-26f8" data-c="26f8" data-s=":ice_skate:" title="ice skate">&#x26f8;</span>
+            <span class="em em-activity-26f8" data-c="26f8" data-s=":ice_skate:" title="ice skate">&#x26f8;&#xfe0f;</span>
             <span class="em em-activity-1f94c" data-c="1f94c" data-s=":curling_stone:" title="curling stone">&#x1f94c;</span>
             <span class="em em-activity-1f6f7" data-c="1f6f7" data-s=":sled:" title="sled">&#x1f6f7;</span>
             <span class="em em-activity-1f3bf" data-c="1f3bf" data-s=":ski:" title="skis">&#x1f3bf;</span>
-            <span class="em em-activity-26f7" data-c="26f7" data-s=":skier:" title="skier">&#x26f7;</span>
+            <span class="em em-activity-26f7" data-c="26f7" data-s=":skier:" title="skier">&#x26f7;&#xfe0f;</span>
             <span class="em em-activity-1f3c2" data-c="1f3c2" data-s=":snowboarder:" title="snowboarder">&#x1f3c2;</span>
             <span class="em em-activity-1f3c2-1f3fb" data-c="1f3c2-1f3fb" data-s=":snowboarder_tone1:" title="snowboarder: light skin tone">&#x1f3c2;&#x1f3fb;</span>
             <span class="em em-activity-1f3c2-1f3fc" data-c="1f3c2-1f3fc" data-s=":snowboarder_tone2:" title="snowboarder: medium-light skin tone">&#x1f3c2;&#x1f3fc;</span>
             <span class="em em-activity-1f3c2-1f3fd" data-c="1f3c2-1f3fd" data-s=":snowboarder_tone3:" title="snowboarder: medium skin tone">&#x1f3c2;&#x1f3fd;</span>
             <span class="em em-activity-1f3c2-1f3fe" data-c="1f3c2-1f3fe" data-s=":snowboarder_tone4:" title="snowboarder: medium-dark skin tone">&#x1f3c2;&#x1f3fe;</span>
             <span class="em em-activity-1f3c2-1f3ff" data-c="1f3c2-1f3ff" data-s=":snowboarder_tone5:" title="snowboarder: dark skin tone">&#x1f3c2;&#x1f3ff;</span>
-            <span class="em em-activity-1f3cb" data-c="1f3cb" data-s=":person_lifting_weights:" title="person lifting weights">&#x1f3cb;</span>
+            <span class="em em-activity-1f3cb" data-c="1f3cb" data-s=":person_lifting_weights:" title="person lifting weights">&#x1f3cb;&#xfe0f;</span>
             <span class="em em-activity-1f3cb-1f3fb" data-c="1f3cb-1f3fb" data-s=":person_lifting_weights_tone1:" title="person lifting weights: light skin tone">&#x1f3cb;&#x1f3fb;</span>
             <span class="em em-activity-1f3cb-1f3fc" data-c="1f3cb-1f3fc" data-s=":person_lifting_weights_tone2:" title="person lifting weights: medium-light skin tone">&#x1f3cb;&#x1f3fc;</span>
             <span class="em em-activity-1f3cb-1f3fd" data-c="1f3cb-1f3fd" data-s=":person_lifting_weights_tone3:" title="person lifting weights: medium skin tone">&#x1f3cb;&#x1f3fd;</span>
@@ -1502,7 +1502,7 @@
             <span class="em em-activity-1f938-1f3fd-200d-2642-fe0f" data-c="1f938-1f3fd-200d-2642-fe0f" data-s=":man_cartwheeling_tone3:" title="man cartwheeling: medium skin tone">&#x1f938;&#x1f3fd;&#x200d;&#x2642;&#xfe0f;</span>
             <span class="em em-activity-1f938-1f3fe-200d-2642-fe0f" data-c="1f938-1f3fe-200d-2642-fe0f" data-s=":man_cartwheeling_tone4:" title="man cartwheeling: medium-dark skin tone">&#x1f938;&#x1f3fe;&#x200d;&#x2642;&#xfe0f;</span>
             <span class="em em-activity-1f938-1f3ff-200d-2642-fe0f" data-c="1f938-1f3ff-200d-2642-fe0f" data-s=":man_cartwheeling_tone5:" title="man cartwheeling: dark skin tone">&#x1f938;&#x1f3ff;&#x200d;&#x2642;&#xfe0f;</span>
-            <span class="em em-activity-26f9" data-c="26f9" data-s=":person_bouncing_ball:" title="person bouncing ball">&#x26f9;</span>
+            <span class="em em-activity-26f9" data-c="26f9" data-s=":person_bouncing_ball:" title="person bouncing ball">&#x26f9;&#xfe0f;</span>
             <span class="em em-activity-26f9-1f3fb" data-c="26f9-1f3fb" data-s=":person_bouncing_ball_tone1:" title="person bouncing ball: light skin tone">&#x26f9;&#x1f3fb;</span>
             <span class="em em-activity-26f9-1f3fc" data-c="26f9-1f3fc" data-s=":person_bouncing_ball_tone2:" title="person bouncing ball: medium-light skin tone">&#x26f9;&#x1f3fc;</span>
             <span class="em em-activity-26f9-1f3fd" data-c="26f9-1f3fd" data-s=":person_bouncing_ball_tone3:" title="person bouncing ball: medium skin tone">&#x26f9;&#x1f3fd;</span>
@@ -1527,7 +1527,7 @@
             <span class="em em-activity-1f93e-1f3fd-200d-2642-fe0f" data-c="1f93e-1f3fd-200d-2642-fe0f" data-s=":man_playing_handball_tone3:" title="man playing handball: medium skin tone">&#x1f93e;&#x1f3fd;&#x200d;&#x2642;&#xfe0f;</span>
             <span class="em em-activity-1f93e-1f3fe-200d-2642-fe0f" data-c="1f93e-1f3fe-200d-2642-fe0f" data-s=":man_playing_handball_tone4:" title="man playing handball: medium-dark skin tone">&#x1f93e;&#x1f3fe;&#x200d;&#x2642;&#xfe0f;</span>
             <span class="em em-activity-1f93e-1f3ff-200d-2642-fe0f" data-c="1f93e-1f3ff-200d-2642-fe0f" data-s=":man_playing_handball_tone5:" title="man playing handball: dark skin tone">&#x1f93e;&#x1f3ff;&#x200d;&#x2642;&#xfe0f;</span>
-            <span class="em em-activity-1f3cc" data-c="1f3cc" data-s=":person_golfing:" title="person golfing">&#x1f3cc;</span>
+            <span class="em em-activity-1f3cc" data-c="1f3cc" data-s=":person_golfing:" title="person golfing">&#x1f3cc;&#xfe0f;</span>
             <span class="em em-activity-1f3cc-1f3fb" data-c="1f3cc-1f3fb" data-s=":person_golfing_tone1:" title="person golfing: light skin tone">&#x1f3cc;&#x1f3fb;</span>
             <span class="em em-activity-1f3cc-1f3fc" data-c="1f3cc-1f3fc" data-s=":person_golfing_tone2:" title="person golfing: medium-light skin tone">&#x1f3cc;&#x1f3fc;</span>
             <span class="em em-activity-1f3cc-1f3fd" data-c="1f3cc-1f3fd" data-s=":person_golfing_tone3:" title="person golfing: medium skin tone">&#x1f3cc;&#x1f3fd;</span>
@@ -1646,11 +1646,11 @@
             <span class="em em-activity-1f948" data-c="1f948" data-s=":second_place:" title="2nd place medal">&#x1f948;</span>
             <span class="em em-activity-1f949" data-c="1f949" data-s=":third_place:" title="3rd place medal">&#x1f949;</span>
             <span class="em em-activity-1f3c5" data-c="1f3c5" data-s=":medal:" title="sports medal">&#x1f3c5;</span>
-            <span class="em em-activity-1f396" data-c="1f396" data-s=":military_medal:" title="military medal">&#x1f396;</span>
-            <span class="em em-activity-1f3f5" data-c="1f3f5" data-s=":rosette:" title="rosette">&#x1f3f5;</span>
-            <span class="em em-activity-1f397" data-c="1f397" data-s=":reminder_ribbon:" title="reminder ribbon">&#x1f397;</span>
+            <span class="em em-activity-1f396" data-c="1f396" data-s=":military_medal:" title="military medal">&#x1f396;&#xfe0f;</span>
+            <span class="em em-activity-1f3f5" data-c="1f3f5" data-s=":rosette:" title="rosette">&#x1f3f5;&#xfe0f;</span>
+            <span class="em em-activity-1f397" data-c="1f397" data-s=":reminder_ribbon:" title="reminder ribbon">&#x1f397;&#xfe0f;</span>
             <span class="em em-activity-1f3ab" data-c="1f3ab" data-s=":ticket:" title="ticket">&#x1f3ab;</span>
-            <span class="em em-activity-1f39f" data-c="1f39f" data-s=":tickets:" title="admission tickets">&#x1f39f;</span>
+            <span class="em em-activity-1f39f" data-c="1f39f" data-s=":tickets:" title="admission tickets">&#x1f39f;&#xfe0f;</span>
             <span class="em em-activity-1f3aa" data-c="1f3aa" data-s=":circus_tent:" title="circus tent">&#x1f3aa;</span>
             <span class="em em-activity-1f939-200d-2640-fe0f" data-c="1f939-200d-2640-fe0f" data-s=":woman_juggling:" title="woman juggling">&#x1f939;&#x200d;&#x2640;&#xfe0f;</span>
             <span class="em em-activity-1f939-1f3fb-200d-2640-fe0f" data-c="1f939-1f3fb-200d-2640-fe0f" data-s=":woman_juggling_tone1:" title="woman juggling: light skin tone">&#x1f939;&#x1f3fb;&#x200d;&#x2640;&#xfe0f;</span>
@@ -1694,7 +1694,7 @@
             <span class="em em-travel-1f699" data-c="1f699" data-s=":blue_car:" title="sport utility vehicle">&#x1f699;</span>
             <span class="em em-travel-1f68c" data-c="1f68c" data-s=":bus:" title="bus">&#x1f68c;</span>
             <span class="em em-travel-1f68e" data-c="1f68e" data-s=":trolleybus:" title="trolleybus">&#x1f68e;</span>
-            <span class="em em-travel-1f3ce" data-c="1f3ce" data-s=":race_car:" title="racing car">&#x1f3ce;</span>
+            <span class="em em-travel-1f3ce" data-c="1f3ce" data-s=":race_car:" title="racing car">&#x1f3ce;&#xfe0f;</span>
             <span class="em em-travel-1f693" data-c="1f693" data-s=":police_car:" title="police car">&#x1f693;</span>
             <span class="em em-travel-1f691" data-c="1f691" data-s=":ambulance:" title="ambulance">&#x1f691;</span>
             <span class="em em-travel-1f692" data-c="1f692" data-s=":fire_engine:" title="fire engine">&#x1f692;</span>
@@ -1705,7 +1705,7 @@
             <span class="em em-travel-1f6f4" data-c="1f6f4" data-s=":scooter:" title="kick scooter">&#x1f6f4;</span>
             <span class="em em-travel-1f6b2" data-c="1f6b2" data-s=":bike:" title="bicycle">&#x1f6b2;</span>
             <span class="em em-travel-1f6f5" data-c="1f6f5" data-s=":motor_scooter:" title="motor scooter">&#x1f6f5;</span>
-            <span class="em em-travel-1f3cd" data-c="1f3cd" data-s=":motorcycle:" title="motorcycle">&#x1f3cd;</span>
+            <span class="em em-travel-1f3cd" data-c="1f3cd" data-s=":motorcycle:" title="motorcycle">&#x1f3cd;&#xfe0f;</span>
             <span class="em em-travel-1f6a8" data-c="1f6a8" data-s=":rotating_light:" title="police car light">&#x1f6a8;</span>
             <span class="em em-travel-1f694" data-c="1f694" data-s=":oncoming_police_car:" title="oncoming police car">&#x1f694;</span>
             <span class="em em-travel-1f68d" data-c="1f68d" data-s=":oncoming_bus:" title="oncoming bus">&#x1f68d;</span>
@@ -1726,22 +1726,22 @@
             <span class="em em-travel-1f687" data-c="1f687" data-s=":metro:" title="metro">&#x1f687;</span>
             <span class="em em-travel-1f68a" data-c="1f68a" data-s=":tram:" title="tram">&#x1f68a;</span>
             <span class="em em-travel-1f689" data-c="1f689" data-s=":station:" title="station">&#x1f689;</span>
-            <span class="em em-travel-2708" data-c="2708" data-s=":airplane:" title="airplane">&#x2708;</span>
+            <span class="em em-travel-2708" data-c="2708" data-s=":airplane:" title="airplane">&#x2708;&#xfe0f;</span>
             <span class="em em-travel-1f6eb" data-c="1f6eb" data-s=":airplane_departure:" title="airplane departure">&#x1f6eb;</span>
             <span class="em em-travel-1f6ec" data-c="1f6ec" data-s=":airplane_arriving:" title="airplane arrival">&#x1f6ec;</span>
-            <span class="em em-travel-1f6e9" data-c="1f6e9" data-s=":airplane_small:" title="small airplane">&#x1f6e9;</span>
+            <span class="em em-travel-1f6e9" data-c="1f6e9" data-s=":airplane_small:" title="small airplane">&#x1f6e9;&#xfe0f;</span>
             <span class="em em-travel-1f4ba" data-c="1f4ba" data-s=":seat:" title="seat">&#x1f4ba;</span>
             <span class="em em-travel-1f9f3" data-c="1f9f3" data-s=":luggage:" title="luggage">&#x1f9f3;</span>
-            <span class="em em-travel-1f6f0" data-c="1f6f0" data-s=":satellite_orbital:" title="satellite">&#x1f6f0;</span>
+            <span class="em em-travel-1f6f0" data-c="1f6f0" data-s=":satellite_orbital:" title="satellite">&#x1f6f0;&#xfe0f;</span>
             <span class="em em-travel-1f680" data-c="1f680" data-s=":rocket:" title="rocket">&#x1f680;</span>
             <span class="em em-travel-1f6f8" data-c="1f6f8" data-s=":flying_saucer:" title="flying saucer">&#x1f6f8;</span>
             <span class="em em-travel-1f681" data-c="1f681" data-s=":helicopter:" title="helicopter">&#x1f681;</span>
             <span class="em em-travel-1f6f6" data-c="1f6f6" data-s=":canoe:" title="canoe">&#x1f6f6;</span>
             <span class="em em-travel-26f5" data-c="26f5" data-s=":sailboat:" title="sailboat">&#x26f5;</span>
             <span class="em em-travel-1f6a4" data-c="1f6a4" data-s=":speedboat:" title="speedboat">&#x1f6a4;</span>
-            <span class="em em-travel-1f6e5" data-c="1f6e5" data-s=":motorboat:" title="motor boat">&#x1f6e5;</span>
-            <span class="em em-travel-1f6f3" data-c="1f6f3" data-s=":cruise_ship:" title="passenger ship">&#x1f6f3;</span>
-            <span class="em em-travel-26f4" data-c="26f4" data-s=":ferry:" title="ferry">&#x26f4;</span>
+            <span class="em em-travel-1f6e5" data-c="1f6e5" data-s=":motorboat:" title="motor boat">&#x1f6e5;&#xfe0f;</span>
+            <span class="em em-travel-1f6f3" data-c="1f6f3" data-s=":cruise_ship:" title="passenger ship">&#x1f6f3;&#xfe0f;</span>
+            <span class="em em-travel-26f4" data-c="26f4" data-s=":ferry:" title="ferry">&#x26f4;&#xfe0f;</span>
             <span class="em em-travel-1f6a2" data-c="1f6a2" data-s=":ship:" title="ship">&#x1f6a2;</span>
             <span class="em em-travel-2693" data-c="2693" data-s=":anchor:" title="anchor">&#x2693;</span>
             <span class="em em-travel-26fd" data-c="26fd" data-s=":fuelpump:" title="fuel pump">&#x26fd;</span>
@@ -1749,32 +1749,32 @@
             <span class="em em-travel-1f6a6" data-c="1f6a6" data-s=":vertical_traffic_light:" title="vertical traffic light">&#x1f6a6;</span>
             <span class="em em-travel-1f6a5" data-c="1f6a5" data-s=":traffic_light:" title="horizontal traffic light">&#x1f6a5;</span>
             <span class="em em-travel-1f68f" data-c="1f68f" data-s=":busstop:" title="bus stop">&#x1f68f;</span>
-            <span class="em em-travel-1f5fa" data-c="1f5fa" data-s=":map:" title="world map">&#x1f5fa;</span>
+            <span class="em em-travel-1f5fa" data-c="1f5fa" data-s=":map:" title="world map">&#x1f5fa;&#xfe0f;</span>
             <span class="em em-travel-1f5ff" data-c="1f5ff" data-s=":moyai:" title="moai">&#x1f5ff;</span>
             <span class="em em-travel-1f5fd" data-c="1f5fd" data-s=":statue_of_liberty:" title="Statue of Liberty">&#x1f5fd;</span>
             <span class="em em-travel-1f5fc" data-c="1f5fc" data-s=":tokyo_tower:" title="Tokyo tower">&#x1f5fc;</span>
             <span class="em em-travel-1f3f0" data-c="1f3f0" data-s=":european_castle:" title="castle">&#x1f3f0;</span>
             <span class="em em-travel-1f3ef" data-c="1f3ef" data-s=":japanese_castle:" title="Japanese castle">&#x1f3ef;</span>
-            <span class="em em-travel-1f3df" data-c="1f3df" data-s=":stadium:" title="stadium">&#x1f3df;</span>
+            <span class="em em-travel-1f3df" data-c="1f3df" data-s=":stadium:" title="stadium">&#x1f3df;&#xfe0f;</span>
             <span class="em em-travel-1f3a1" data-c="1f3a1" data-s=":ferris_wheel:" title="ferris wheel">&#x1f3a1;</span>
             <span class="em em-travel-1f3a2" data-c="1f3a2" data-s=":roller_coaster:" title="roller coaster">&#x1f3a2;</span>
             <span class="em em-travel-1f3a0" data-c="1f3a0" data-s=":carousel_horse:" title="carousel horse">&#x1f3a0;</span>
             <span class="em em-travel-26f2" data-c="26f2" data-s=":fountain:" title="fountain">&#x26f2;</span>
-            <span class="em em-travel-26f1" data-c="26f1" data-s=":beach_umbrella:" title="umbrella on ground">&#x26f1;</span>
-            <span class="em em-travel-1f3d6" data-c="1f3d6" data-s=":beach:" title="beach with umbrella">&#x1f3d6;</span>
-            <span class="em em-travel-1f3dd" data-c="1f3dd" data-s=":island:" title="desert island">&#x1f3dd;</span>
-            <span class="em em-travel-1f3dc" data-c="1f3dc" data-s=":desert:" title="desert">&#x1f3dc;</span>
+            <span class="em em-travel-26f1" data-c="26f1" data-s=":beach_umbrella:" title="umbrella on ground">&#x26f1;&#xfe0f;</span>
+            <span class="em em-travel-1f3d6" data-c="1f3d6" data-s=":beach:" title="beach with umbrella">&#x1f3d6;&#xfe0f;</span>
+            <span class="em em-travel-1f3dd" data-c="1f3dd" data-s=":island:" title="desert island">&#x1f3dd;&#xfe0f;</span>
+            <span class="em em-travel-1f3dc" data-c="1f3dc" data-s=":desert:" title="desert">&#x1f3dc;&#xfe0f;</span>
             <span class="em em-travel-1f30b" data-c="1f30b" data-s=":volcano:" title="volcano">&#x1f30b;</span>
-            <span class="em em-travel-26f0" data-c="26f0" data-s=":mountain:" title="mountain">&#x26f0;</span>
-            <span class="em em-travel-1f3d4" data-c="1f3d4" data-s=":mountain_snow:" title="snow-capped mountain">&#x1f3d4;</span>
+            <span class="em em-travel-26f0" data-c="26f0" data-s=":mountain:" title="mountain">&#x26f0;&#xfe0f;</span>
+            <span class="em em-travel-1f3d4" data-c="1f3d4" data-s=":mountain_snow:" title="snow-capped mountain">&#x1f3d4;&#xfe0f;</span>
             <span class="em em-travel-1f5fb" data-c="1f5fb" data-s=":mount_fuji:" title="mount fuji">&#x1f5fb;</span>
-            <span class="em em-travel-1f3d5" data-c="1f3d5" data-s=":camping:" title="camping">&#x1f3d5;</span>
+            <span class="em em-travel-1f3d5" data-c="1f3d5" data-s=":camping:" title="camping">&#x1f3d5;&#xfe0f;</span>
             <span class="em em-travel-26fa" data-c="26fa" data-s=":tent:" title="tent">&#x26fa;</span>
             <span class="em em-travel-1f3e0" data-c="1f3e0" data-s=":house:" title="house">&#x1f3e0;</span>
             <span class="em em-travel-1f3e1" data-c="1f3e1" data-s=":house_with_garden:" title="house with garden">&#x1f3e1;</span>
-            <span class="em em-travel-1f3d8" data-c="1f3d8" data-s=":homes:" title="houses">&#x1f3d8;</span>
-            <span class="em em-travel-1f3da" data-c="1f3da" data-s=":house_abandoned:" title="derelict house">&#x1f3da;</span>
-            <span class="em em-travel-1f3d7" data-c="1f3d7" data-s=":construction_site:" title="building construction">&#x1f3d7;</span>
+            <span class="em em-travel-1f3d8" data-c="1f3d8" data-s=":homes:" title="houses">&#x1f3d8;&#xfe0f;</span>
+            <span class="em em-travel-1f3da" data-c="1f3da" data-s=":house_abandoned:" title="derelict house">&#x1f3da;&#xfe0f;</span>
+            <span class="em em-travel-1f3d7" data-c="1f3d7" data-s=":construction_site:" title="building construction">&#x1f3d7;&#xfe0f;</span>
             <span class="em em-travel-1f3ed" data-c="1f3ed" data-s=":factory:" title="factory">&#x1f3ed;</span>
             <span class="em em-travel-1f3e2" data-c="1f3e2" data-s=":office:" title="office building">&#x1f3e2;</span>
             <span class="em em-travel-1f3ec" data-c="1f3ec" data-s=":department_store:" title="department store">&#x1f3ec;</span>
@@ -1787,17 +1787,17 @@
             <span class="em em-travel-1f3eb" data-c="1f3eb" data-s=":school:" title="school">&#x1f3eb;</span>
             <span class="em em-travel-1f3e9" data-c="1f3e9" data-s=":love_hotel:" title="love hotel">&#x1f3e9;</span>
             <span class="em em-travel-1f492" data-c="1f492" data-s=":wedding:" title="wedding">&#x1f492;</span>
-            <span class="em em-travel-1f3db" data-c="1f3db" data-s=":classical_building:" title="classical building">&#x1f3db;</span>
+            <span class="em em-travel-1f3db" data-c="1f3db" data-s=":classical_building:" title="classical building">&#x1f3db;&#xfe0f;</span>
             <span class="em em-travel-26ea" data-c="26ea" data-s=":church:" title="church">&#x26ea;</span>
             <span class="em em-travel-1f54c" data-c="1f54c" data-s=":mosque:" title="mosque">&#x1f54c;</span>
             <span class="em em-travel-1f54d" data-c="1f54d" data-s=":synagogue:" title="synagogue">&#x1f54d;</span>
             <span class="em em-travel-1f54b" data-c="1f54b" data-s=":kaaba:" title="kaaba">&#x1f54b;</span>
-            <span class="em em-travel-26e9" data-c="26e9" data-s=":shinto_shrine:" title="shinto shrine">&#x26e9;</span>
-            <span class="em em-travel-1f6e4" data-c="1f6e4" data-s=":railway_track:" title="railway track">&#x1f6e4;</span>
-            <span class="em em-travel-1f6e3" data-c="1f6e3" data-s=":motorway:" title="motorway">&#x1f6e3;</span>
+            <span class="em em-travel-26e9" data-c="26e9" data-s=":shinto_shrine:" title="shinto shrine">&#x26e9;&#xfe0f;</span>
+            <span class="em em-travel-1f6e4" data-c="1f6e4" data-s=":railway_track:" title="railway track">&#x1f6e4;&#xfe0f;</span>
+            <span class="em em-travel-1f6e3" data-c="1f6e3" data-s=":motorway:" title="motorway">&#x1f6e3;&#xfe0f;</span>
             <span class="em em-travel-1f5fe" data-c="1f5fe" data-s=":japan:" title="map of Japan">&#x1f5fe;</span>
             <span class="em em-travel-1f391" data-c="1f391" data-s=":rice_scene:" title="moon viewing ceremony">&#x1f391;</span>
-            <span class="em em-travel-1f3de" data-c="1f3de" data-s=":park:" title="national park">&#x1f3de;</span>
+            <span class="em em-travel-1f3de" data-c="1f3de" data-s=":park:" title="national park">&#x1f3de;&#xfe0f;</span>
             <span class="em em-travel-1f305" data-c="1f305" data-s=":sunrise:" title="sunrise">&#x1f305;</span>
             <span class="em em-travel-1f304" data-c="1f304" data-s=":sunrise_over_mountains:" title="sunrise over mountains">&#x1f304;</span>
             <span class="em em-travel-1f320" data-c="1f320" data-s=":stars:" title="shooting star">&#x1f320;</span>
@@ -1806,7 +1806,7 @@
             <span class="em em-travel-1f9e8" data-c="1f9e8" data-s=":firecracker:" title="firecracker">&#x1f9e8;</span>
             <span class="em em-travel-1f307" data-c="1f307" data-s=":city_sunset:" title="sunset">&#x1f307;</span>
             <span class="em em-travel-1f306" data-c="1f306" data-s=":city_dusk:" title="cityscape at dusk">&#x1f306;</span>
-            <span class="em em-travel-1f3d9" data-c="1f3d9" data-s=":cityscape:" title="cityscape">&#x1f3d9;</span>
+            <span class="em em-travel-1f3d9" data-c="1f3d9" data-s=":cityscape:" title="cityscape">&#x1f3d9;&#xfe0f;</span>
             <span class="em em-travel-1f303" data-c="1f303" data-s=":night_with_stars:" title="night with stars">&#x1f303;</span>
             <span class="em em-travel-1f30c" data-c="1f30c" data-s=":milky_way:" title="milky way">&#x1f30c;</span>
             <span class="em em-travel-1f309" data-c="1f309" data-s=":bridge_at_night:" title="bridge at night">&#x1f309;</span>
@@ -1823,15 +1823,15 @@
             <span class="em em-objects-1f4f1" data-c="1f4f1" data-s=":iphone:" title="mobile phone">&#x1f4f1;</span>
             <span class="em em-objects-1f4f2" data-c="1f4f2" data-s=":calling:" title="mobile phone with arrow">&#x1f4f2;</span>
             <span class="em em-objects-1f4bb" data-c="1f4bb" data-s=":computer:" title="laptop computer">&#x1f4bb;</span>
-            <span class="em em-objects-2328" data-c="2328" data-s=":keyboard:" title="keyboard">&#x2328;</span>
-            <span class="em em-objects-1f5a5" data-c="1f5a5" data-s=":desktop:" title="desktop computer">&#x1f5a5;</span>
-            <span class="em em-objects-1f5a8" data-c="1f5a8" data-s=":printer:" title="printer">&#x1f5a8;</span>
-            <span class="em em-objects-1f5b1" data-c="1f5b1" data-s=":mouse_three_button:" title="computer mouse">&#x1f5b1;</span>
-            <span class="em em-objects-1f5b2" data-c="1f5b2" data-s=":trackball:" title="trackball">&#x1f5b2;</span>
-            <span class="em em-objects-1f579" data-c="1f579" data-s=":joystick:" title="joystick">&#x1f579;</span>
+            <span class="em em-objects-2328" data-c="2328" data-s=":keyboard:" title="keyboard">&#x2328;&#xfe0f;</span>
+            <span class="em em-objects-1f5a5" data-c="1f5a5" data-s=":desktop:" title="desktop computer">&#x1f5a5;&#xfe0f;</span>
+            <span class="em em-objects-1f5a8" data-c="1f5a8" data-s=":printer:" title="printer">&#x1f5a8;&#xfe0f;</span>
+            <span class="em em-objects-1f5b1" data-c="1f5b1" data-s=":mouse_three_button:" title="computer mouse">&#x1f5b1;&#xfe0f;</span>
+            <span class="em em-objects-1f5b2" data-c="1f5b2" data-s=":trackball:" title="trackball">&#x1f5b2;&#xfe0f;</span>
+            <span class="em em-objects-1f579" data-c="1f579" data-s=":joystick:" title="joystick">&#x1f579;&#xfe0f;</span>
             <span class="em em-objects-265f" data-c="265f" data-s=":chess_pawn:" title="chess pawn">&#x265f;&#xfe0f;</span>
             <span class="em em-objects-1f9e9" data-c="1f9e9" data-s=":jigsaw:" title="jigsaw">&#x1f9e9;</span>
-            <span class="em em-objects-1f5dc" data-c="1f5dc" data-s=":compression:" title="clamp">&#x1f5dc;</span>
+            <span class="em em-objects-1f5dc" data-c="1f5dc" data-s=":compression:" title="clamp">&#x1f5dc;&#xfe0f;</span>
             <span class="em em-objects-1f4bd" data-c="1f4bd" data-s=":minidisc:" title="computer disk">&#x1f4bd;</span>
             <span class="em em-objects-1f4be" data-c="1f4be" data-s=":floppy_disk:" title="floppy disk">&#x1f4be;</span>
             <span class="em em-objects-1f4bf" data-c="1f4bf" data-s=":cd:" title="optical disk">&#x1f4bf;</span>
@@ -1841,21 +1841,21 @@
             <span class="em em-objects-1f4f8" data-c="1f4f8" data-s=":camera_with_flash:" title="camera with flash">&#x1f4f8;</span>
             <span class="em em-objects-1f4f9" data-c="1f4f9" data-s=":video_camera:" title="video camera">&#x1f4f9;</span>
             <span class="em em-objects-1f3a5" data-c="1f3a5" data-s=":movie_camera:" title="movie camera">&#x1f3a5;</span>
-            <span class="em em-objects-1f4fd" data-c="1f4fd" data-s=":projector:" title="film projector">&#x1f4fd;</span>
-            <span class="em em-objects-1f39e" data-c="1f39e" data-s=":film_frames:" title="film frames">&#x1f39e;</span>
+            <span class="em em-objects-1f4fd" data-c="1f4fd" data-s=":projector:" title="film projector">&#x1f4fd;&#xfe0f;</span>
+            <span class="em em-objects-1f39e" data-c="1f39e" data-s=":film_frames:" title="film frames">&#x1f39e;&#xfe0f;</span>
             <span class="em em-objects-1f4de" data-c="1f4de" data-s=":telephone_receiver:" title="telephone receiver">&#x1f4de;</span>
-            <span class="em em-objects-260e" data-c="260e" data-s=":telephone:" title="telephone">&#x260e;</span>
+            <span class="em em-objects-260e" data-c="260e" data-s=":telephone:" title="telephone">&#x260e;&#xfe0f;</span>
             <span class="em em-objects-1f4df" data-c="1f4df" data-s=":pager:" title="pager">&#x1f4df;</span>
             <span class="em em-objects-1f4e0" data-c="1f4e0" data-s=":fax:" title="fax machine">&#x1f4e0;</span>
             <span class="em em-objects-1f4fa" data-c="1f4fa" data-s=":tv:" title="television">&#x1f4fa;</span>
             <span class="em em-objects-1f4fb" data-c="1f4fb" data-s=":radio:" title="radio">&#x1f4fb;</span>
-            <span class="em em-objects-1f399" data-c="1f399" data-s=":microphone2:" title="studio microphone">&#x1f399;</span>
-            <span class="em em-objects-1f39a" data-c="1f39a" data-s=":level_slider:" title="level slider">&#x1f39a;</span>
-            <span class="em em-objects-1f39b" data-c="1f39b" data-s=":control_knobs:" title="control knobs">&#x1f39b;</span>
-            <span class="em em-objects-23f1" data-c="23f1" data-s=":stopwatch:" title="stopwatch">&#x23f1;</span>
-            <span class="em em-objects-23f2" data-c="23f2" data-s=":timer:" title="timer clock">&#x23f2;</span>
+            <span class="em em-objects-1f399" data-c="1f399" data-s=":microphone2:" title="studio microphone">&#x1f399;&#xfe0f;</span>
+            <span class="em em-objects-1f39a" data-c="1f39a" data-s=":level_slider:" title="level slider">&#x1f39a;&#xfe0f;</span>
+            <span class="em em-objects-1f39b" data-c="1f39b" data-s=":control_knobs:" title="control knobs">&#x1f39b;&#xfe0f;</span>
+            <span class="em em-objects-23f1" data-c="23f1" data-s=":stopwatch:" title="stopwatch">&#x23f1;&#xfe0f;</span>
+            <span class="em em-objects-23f2" data-c="23f2" data-s=":timer:" title="timer clock">&#x23f2;&#xfe0f;</span>
             <span class="em em-objects-23f0" data-c="23f0" data-s=":alarm_clock:" title="alarm clock">&#x23f0;</span>
-            <span class="em em-objects-1f570" data-c="1f570" data-s=":clock:" title="mantelpiece clock">&#x1f570;</span>
+            <span class="em em-objects-1f570" data-c="1f570" data-s=":clock:" title="mantelpiece clock">&#x1f570;&#xfe0f;</span>
             <span class="em em-objects-231b" data-c="231b" data-s=":hourglass:" title="hourglass done">&#x231b;</span>
             <span class="em em-objects-23f3" data-c="23f3" data-s=":hourglass_flowing_sand:" title="hourglass not done">&#x23f3;</span>
             <span class="em em-objects-1f4e1" data-c="1f4e1" data-s=":satellite:" title="satellite antenna">&#x1f4e1;</span>
@@ -1865,10 +1865,10 @@
             <span class="em em-objects-1f9f2" data-c="1f9f2" data-s=":magnet:" title="magnet">&#x1f9f2;</span>
             <span class="em em-objects-1f4a1" data-c="1f4a1" data-s=":bulb:" title="light bulb">&#x1f4a1;</span>
             <span class="em em-objects-1f526" data-c="1f526" data-s=":flashlight:" title="flashlight">&#x1f526;</span>
-            <span class="em em-objects-1f56f" data-c="1f56f" data-s=":candle:" title="candle">&#x1f56f;</span>
+            <span class="em em-objects-1f56f" data-c="1f56f" data-s=":candle:" title="candle">&#x1f56f;&#xfe0f;</span>
             <span class="em em-objects-1f9ef" data-c="1f9ef" data-s=":fire_extinguisher:" title="fire extinguisher">&#x1f9ef;</span>
-            <span class="em em-objects-1f5d1" data-c="1f5d1" data-s=":wastebasket:" title="wastebasket">&#x1f5d1;</span>
-            <span class="em em-objects-1f6e2" data-c="1f6e2" data-s=":oil:" title="oil drum">&#x1f6e2;</span>
+            <span class="em em-objects-1f5d1" data-c="1f5d1" data-s=":wastebasket:" title="wastebasket">&#x1f5d1;&#xfe0f;</span>
+            <span class="em em-objects-1f6e2" data-c="1f6e2" data-s=":oil:" title="oil drum">&#x1f6e2;&#xfe0f;</span>
             <span class="em em-objects-1f4b8" data-c="1f4b8" data-s=":money_with_wings:" title="money with wings">&#x1f4b8;</span>
             <span class="em em-objects-1f4b5" data-c="1f4b5" data-s=":dollar:" title="dollar banknote">&#x1f4b5;</span>
             <span class="em em-objects-1f4b4" data-c="1f4b4" data-s=":yen:" title="yen banknote">&#x1f4b4;</span>
@@ -1879,40 +1879,40 @@
             <span class="em em-objects-1f48e" data-c="1f48e" data-s=":gem:" title="gem stone">&#x1f48e;</span>
             <span class="em em-objects-1f9ff" data-c="1f9ff" data-s=":nazar_amulet:" title="nazar amulet">&#x1f9ff;</span>
             <span class="em em-objects-1f9f1" data-c="1f9f1" data-s=":bricks:" title="bricks">&#x1f9f1;</span>
-            <span class="em em-objects-2696" data-c="2696" data-s=":scales:" title="balance scale">&#x2696;</span>
+            <span class="em em-objects-2696" data-c="2696" data-s=":scales:" title="balance scale">&#x2696;&#xfe0f;</span>
             <span class="em em-objects-1f9f0" data-c="1f9f0" data-s=":toolbox:" title="toolbox">&#x1f9f0;</span>
             <span class="em em-objects-1f527" data-c="1f527" data-s=":wrench:" title="wrench">&#x1f527;</span>
             <span class="em em-objects-1f528" data-c="1f528" data-s=":hammer:" title="hammer">&#x1f528;</span>
-            <span class="em em-objects-2692" data-c="2692" data-s=":hammer_pick:" title="hammer and pick">&#x2692;</span>
-            <span class="em em-objects-1f6e0" data-c="1f6e0" data-s=":tools:" title="hammer and wrench">&#x1f6e0;</span>
-            <span class="em em-objects-26cf" data-c="26cf" data-s=":pick:" title="pick">&#x26cf;</span>
+            <span class="em em-objects-2692" data-c="2692" data-s=":hammer_pick:" title="hammer and pick">&#x2692;&#xfe0f;</span>
+            <span class="em em-objects-1f6e0" data-c="1f6e0" data-s=":tools:" title="hammer and wrench">&#x1f6e0;&#xfe0f;</span>
+            <span class="em em-objects-26cf" data-c="26cf" data-s=":pick:" title="pick">&#x26cf;&#xfe0f;</span>
             <span class="em em-objects-1f529" data-c="1f529" data-s=":nut_and_bolt:" title="nut and bolt">&#x1f529;</span>
-            <span class="em em-objects-2699" data-c="2699" data-s=":gear:" title="gear">&#x2699;</span>
-            <span class="em em-objects-26d3" data-c="26d3" data-s=":chains:" title="chains">&#x26d3;</span>
+            <span class="em em-objects-2699" data-c="2699" data-s=":gear:" title="gear">&#x2699;&#xfe0f;</span>
+            <span class="em em-objects-26d3" data-c="26d3" data-s=":chains:" title="chains">&#x26d3;&#xfe0f;</span>
             <span class="em em-objects-1f52b" data-c="1f52b" data-s=":gun:" title="pistol">&#x1f52b;</span>
             <span class="em em-objects-1f4a3" data-c="1f4a3" data-s=":bomb:" title="bomb">&#x1f4a3;</span>
             <span class="em em-objects-1f52a" data-c="1f52a" data-s=":knife:" title="kitchen knife">&#x1f52a;</span>
-            <span class="em em-objects-1f5e1" data-c="1f5e1" data-s=":dagger:" title="dagger">&#x1f5e1;</span>
-            <span class="em em-objects-2694" data-c="2694" data-s=":crossed_swords:" title="crossed swords">&#x2694;</span>
-            <span class="em em-objects-1f6e1" data-c="1f6e1" data-s=":shield:" title="shield">&#x1f6e1;</span>
+            <span class="em em-objects-1f5e1" data-c="1f5e1" data-s=":dagger:" title="dagger">&#x1f5e1;&#xfe0f;</span>
+            <span class="em em-objects-2694" data-c="2694" data-s=":crossed_swords:" title="crossed swords">&#x2694;&#xfe0f;</span>
+            <span class="em em-objects-1f6e1" data-c="1f6e1" data-s=":shield:" title="shield">&#x1f6e1;&#xfe0f;</span>
             <span class="em em-objects-1f6ac" data-c="1f6ac" data-s=":smoking:" title="cigarette">&#x1f6ac;</span>
-            <span class="em em-objects-26b0" data-c="26b0" data-s=":coffin:" title="coffin">&#x26b0;</span>
-            <span class="em em-objects-26b1" data-c="26b1" data-s=":urn:" title="funeral urn">&#x26b1;</span>
+            <span class="em em-objects-26b0" data-c="26b0" data-s=":coffin:" title="coffin">&#x26b0;&#xfe0f;</span>
+            <span class="em em-objects-26b1" data-c="26b1" data-s=":urn:" title="funeral urn">&#x26b1;&#xfe0f;</span>
             <span class="em em-objects-1f3fa" data-c="1f3fa" data-s=":amphora:" title="amphora">&#x1f3fa;</span>
             <span class="em em-objects-1f52e" data-c="1f52e" data-s=":crystal_ball:" title="crystal ball">&#x1f52e;</span>
             <span class="em em-objects-1f4ff" data-c="1f4ff" data-s=":prayer_beads:" title="prayer beads">&#x1f4ff;</span>
             <span class="em em-objects-1f488" data-c="1f488" data-s=":barber:" title="barber pole">&#x1f488;</span>
-            <span class="em em-objects-2697" data-c="2697" data-s=":alembic:" title="alembic">&#x2697;</span>
+            <span class="em em-objects-2697" data-c="2697" data-s=":alembic:" title="alembic">&#x2697;&#xfe0f;</span>
             <span class="em em-objects-1f9ea" data-c="1f9ea" data-s=":test_tube:" title="test tube">&#x1f9ea;</span>
             <span class="em em-objects-1f9eb" data-c="1f9eb" data-s=":petri_dish:" title="petri dish">&#x1f9eb;</span>
             <span class="em em-objects-1f9ec" data-c="1f9ec" data-s=":dna:" title="dna">&#x1f9ec;</span>
             <span class="em em-objects-1f9ee" data-c="1f9ee" data-s=":abacus:" title="abacus">&#x1f9ee;</span>
             <span class="em em-objects-1f52d" data-c="1f52d" data-s=":telescope:" title="telescope">&#x1f52d;</span>
             <span class="em em-objects-1f52c" data-c="1f52c" data-s=":microscope:" title="microscope">&#x1f52c;</span>
-            <span class="em em-objects-1f573" data-c="1f573" data-s=":hole:" title="hole">&#x1f573;</span>
+            <span class="em em-objects-1f573" data-c="1f573" data-s=":hole:" title="hole">&#x1f573;&#xfe0f;</span>
             <span class="em em-objects-1f48a" data-c="1f48a" data-s=":pill:" title="pill">&#x1f48a;</span>
             <span class="em em-objects-1f489" data-c="1f489" data-s=":syringe:" title="syringe">&#x1f489;</span>
-            <span class="em em-objects-1f321" data-c="1f321" data-s=":thermometer:" title="thermometer">&#x1f321;</span>
+            <span class="em em-objects-1f321" data-c="1f321" data-s=":thermometer:" title="thermometer">&#x1f321;&#xfe0f;</span>
             <span class="em em-objects-1f6bd" data-c="1f6bd" data-s=":toilet:" title="toilet">&#x1f6bd;</span>
             <span class="em em-objects-1f6b0" data-c="1f6b0" data-s=":potable_water:" title="potable water">&#x1f6b0;</span>
             <span class="em em-objects-1f6bf" data-c="1f6bf" data-s=":shower:" title="shower">&#x1f6bf;</span>
@@ -1931,12 +1931,12 @@
             <span class="em em-objects-1f9f4" data-c="1f9f4" data-s=":squeeze_bottle:" title="squeeze bottle">&#x1f9f4;</span>
             <span class="em em-objects-1f9f5" data-c="1f9f5" data-s=":thread:" title="thread">&#x1f9f5;</span>
             <span class="em em-objects-1f9f6" data-c="1f9f6" data-s=":yarn:" title="yarn">&#x1f9f6;</span>
-            <span class="em em-objects-1f6ce" data-c="1f6ce" data-s=":bellhop:" title="bellhop bell">&#x1f6ce;</span>
+            <span class="em em-objects-1f6ce" data-c="1f6ce" data-s=":bellhop:" title="bellhop bell">&#x1f6ce;&#xfe0f;</span>
             <span class="em em-objects-1f511" data-c="1f511" data-s=":key:" title="key">&#x1f511;</span>
-            <span class="em em-objects-1f5dd" data-c="1f5dd" data-s=":key2:" title="old key">&#x1f5dd;</span>
+            <span class="em em-objects-1f5dd" data-c="1f5dd" data-s=":key2:" title="old key">&#x1f5dd;&#xfe0f;</span>
             <span class="em em-objects-1f6aa" data-c="1f6aa" data-s=":door:" title="door">&#x1f6aa;</span>
-            <span class="em em-objects-1f6cb" data-c="1f6cb" data-s=":couch:" title="couch and lamp">&#x1f6cb;</span>
-            <span class="em em-objects-1f6cf" data-c="1f6cf" data-s=":bed:" title="bed">&#x1f6cf;</span>
+            <span class="em em-objects-1f6cb" data-c="1f6cb" data-s=":couch:" title="couch and lamp">&#x1f6cb;&#xfe0f;</span>
+            <span class="em em-objects-1f6cf" data-c="1f6cf" data-s=":bed:" title="bed">&#x1f6cf;&#xfe0f;</span>
             <span class="em em-objects-1f6cc" data-c="1f6cc" data-s=":sleeping_accommodation:" title="person in bed">&#x1f6cc;</span>
             <span class="em em-objects-1f6cc-1f3fb" data-c="1f6cc-1f3fb" data-s=":person_in_bed_tone1:" title="person in bed: light skin tone">&#x1f6cc;&#x1f3fb;</span>
             <span class="em em-objects-1f6cc-1f3fc" data-c="1f6cc-1f3fc" data-s=":person_in_bed_tone2:" title="person in bed: medium-light skin tone">&#x1f6cc;&#x1f3fc;</span>
@@ -1944,8 +1944,8 @@
             <span class="em em-objects-1f6cc-1f3fe" data-c="1f6cc-1f3fe" data-s=":person_in_bed_tone4:" title="person in bed: medium-dark skin tone">&#x1f6cc;&#x1f3fe;</span>
             <span class="em em-objects-1f6cc-1f3ff" data-c="1f6cc-1f3ff" data-s=":person_in_bed_tone5:" title="person in bed: dark skin tone">&#x1f6cc;&#x1f3ff;</span>
             <span class="em em-objects-1f9f8" data-c="1f9f8" data-s=":teddy_bear:" title="teddy bear">&#x1f9f8;</span>
-            <span class="em em-objects-1f5bc" data-c="1f5bc" data-s=":frame_photo:" title="framed picture">&#x1f5bc;</span>
-            <span class="em em-objects-1f6cd" data-c="1f6cd" data-s=":shopping_bags:" title="shopping bags">&#x1f6cd;</span>
+            <span class="em em-objects-1f5bc" data-c="1f5bc" data-s=":frame_photo:" title="framed picture">&#x1f5bc;&#xfe0f;</span>
+            <span class="em em-objects-1f6cd" data-c="1f6cd" data-s=":shopping_bags:" title="shopping bags">&#x1f6cd;&#xfe0f;</span>
             <span class="em em-objects-1f6d2" data-c="1f6d2" data-s=":shopping_cart:" title="shopping cart">&#x1f6d2;</span>
             <span class="em em-objects-1f381" data-c="1f381" data-s=":gift:" title="wrapped gift">&#x1f381;</span>
             <span class="em em-objects-1f388" data-c="1f388" data-s=":balloon:" title="balloon">&#x1f388;</span>
@@ -1957,7 +1957,7 @@
             <span class="em em-objects-1f3ee" data-c="1f3ee" data-s=":izakaya_lantern:" title="red paper lantern">&#x1f3ee;</span>
             <span class="em em-objects-1f390" data-c="1f390" data-s=":wind_chime:" title="wind chime">&#x1f390;</span>
             <span class="em em-objects-1f9e7" data-c="1f9e7" data-s=":red_envelope:" title="red envelope">&#x1f9e7;</span>
-            <span class="em em-objects-2709" data-c="2709" data-s=":envelope:" title="envelope">&#x2709;</span>
+            <span class="em em-objects-2709" data-c="2709" data-s=":envelope:" title="envelope">&#x2709;&#xfe0f;</span>
             <span class="em em-objects-1f4e9" data-c="1f4e9" data-s=":envelope_with_arrow:" title="envelope with arrow">&#x1f4e9;</span>
             <span class="em em-objects-1f4e8" data-c="1f4e8" data-s=":incoming_envelope:" title="incoming envelope">&#x1f4e8;</span>
             <span class="em em-objects-1f4e7" data-c="1f4e7" data-s=":e-mail:" title="e-mail">&#x1f4e7;</span>
@@ -1965,7 +1965,7 @@
             <span class="em em-objects-1f4e5" data-c="1f4e5" data-s=":inbox_tray:" title="inbox tray">&#x1f4e5;</span>
             <span class="em em-objects-1f4e4" data-c="1f4e4" data-s=":outbox_tray:" title="outbox tray">&#x1f4e4;</span>
             <span class="em em-objects-1f4e6" data-c="1f4e6" data-s=":package:" title="package">&#x1f4e6;</span>
-            <span class="em em-objects-1f3f7" data-c="1f3f7" data-s=":label:" title="label">&#x1f3f7;</span>
+            <span class="em em-objects-1f3f7" data-c="1f3f7" data-s=":label:" title="label">&#x1f3f7;&#xfe0f;</span>
             <span class="em em-objects-1f4ea" data-c="1f4ea" data-s=":mailbox_closed:" title="closed mailbox with lowered flag">&#x1f4ea;</span>
             <span class="em em-objects-1f4eb" data-c="1f4eb" data-s=":mailbox:" title="closed mailbox with raised flag">&#x1f4eb;</span>
             <span class="em em-objects-1f4ec" data-c="1f4ec" data-s=":mailbox_with_mail:" title="open mailbox with raised flag">&#x1f4ec;</span>
@@ -1980,19 +1980,19 @@
             <span class="em em-objects-1f4ca" data-c="1f4ca" data-s=":bar_chart:" title="bar chart">&#x1f4ca;</span>
             <span class="em em-objects-1f4c8" data-c="1f4c8" data-s=":chart_with_upwards_trend:" title="chart increasing">&#x1f4c8;</span>
             <span class="em em-objects-1f4c9" data-c="1f4c9" data-s=":chart_with_downwards_trend:" title="chart decreasing">&#x1f4c9;</span>
-            <span class="em em-objects-1f5d2" data-c="1f5d2" data-s=":notepad_spiral:" title="spiral notepad">&#x1f5d2;</span>
-            <span class="em em-objects-1f5d3" data-c="1f5d3" data-s=":calendar_spiral:" title="spiral calendar">&#x1f5d3;</span>
+            <span class="em em-objects-1f5d2" data-c="1f5d2" data-s=":notepad_spiral:" title="spiral notepad">&#x1f5d2;&#xfe0f;</span>
+            <span class="em em-objects-1f5d3" data-c="1f5d3" data-s=":calendar_spiral:" title="spiral calendar">&#x1f5d3;&#xfe0f;</span>
             <span class="em em-objects-1f4c6" data-c="1f4c6" data-s=":calendar:" title="tear-off calendar">&#x1f4c6;</span>
             <span class="em em-objects-1f4c5" data-c="1f4c5" data-s=":date:" title="calendar">&#x1f4c5;</span>
             <span class="em em-objects-1f4c7" data-c="1f4c7" data-s=":card_index:" title="card index">&#x1f4c7;</span>
-            <span class="em em-objects-1f5c3" data-c="1f5c3" data-s=":card_box:" title="card file box">&#x1f5c3;</span>
-            <span class="em em-objects-1f5f3" data-c="1f5f3" data-s=":ballot_box:" title="ballot box with ballot">&#x1f5f3;</span>
-            <span class="em em-objects-1f5c4" data-c="1f5c4" data-s=":file_cabinet:" title="file cabinet">&#x1f5c4;</span>
+            <span class="em em-objects-1f5c3" data-c="1f5c3" data-s=":card_box:" title="card file box">&#x1f5c3;&#xfe0f;</span>
+            <span class="em em-objects-1f5f3" data-c="1f5f3" data-s=":ballot_box:" title="ballot box with ballot">&#x1f5f3;&#xfe0f;</span>
+            <span class="em em-objects-1f5c4" data-c="1f5c4" data-s=":file_cabinet:" title="file cabinet">&#x1f5c4;&#xfe0f;</span>
             <span class="em em-objects-1f4cb" data-c="1f4cb" data-s=":clipboard:" title="clipboard">&#x1f4cb;</span>
             <span class="em em-objects-1f4c1" data-c="1f4c1" data-s=":file_folder:" title="file folder">&#x1f4c1;</span>
             <span class="em em-objects-1f4c2" data-c="1f4c2" data-s=":open_file_folder:" title="open file folder">&#x1f4c2;</span>
-            <span class="em em-objects-1f5c2" data-c="1f5c2" data-s=":dividers:" title="card index dividers">&#x1f5c2;</span>
-            <span class="em em-objects-1f5de" data-c="1f5de" data-s=":newspaper2:" title="rolled-up newspaper">&#x1f5de;</span>
+            <span class="em em-objects-1f5c2" data-c="1f5c2" data-s=":dividers:" title="card index dividers">&#x1f5c2;&#xfe0f;</span>
+            <span class="em em-objects-1f5de" data-c="1f5de" data-s=":newspaper2:" title="rolled-up newspaper">&#x1f5de;&#xfe0f;</span>
             <span class="em em-objects-1f4f0" data-c="1f4f0" data-s=":newspaper:" title="newspaper">&#x1f4f0;</span>
             <span class="em em-objects-1f4d3" data-c="1f4d3" data-s=":notebook:" title="notebook">&#x1f4d3;</span>
             <span class="em em-objects-1f4d4" data-c="1f4d4" data-s=":notebook_with_decorative_cover:" title="notebook with decorative cover">&#x1f4d4;</span>
@@ -2006,20 +2006,20 @@
             <span class="em em-objects-1f516" data-c="1f516" data-s=":bookmark:" title="bookmark">&#x1f516;</span>
             <span class="em em-objects-1f517" data-c="1f517" data-s=":link:" title="link">&#x1f517;</span>
             <span class="em em-objects-1f4ce" data-c="1f4ce" data-s=":paperclip:" title="paperclip">&#x1f4ce;</span>
-            <span class="em em-objects-1f587" data-c="1f587" data-s=":paperclips:" title="linked paperclips">&#x1f587;</span>
+            <span class="em em-objects-1f587" data-c="1f587" data-s=":paperclips:" title="linked paperclips">&#x1f587;&#xfe0f;</span>
             <span class="em em-objects-1f4d0" data-c="1f4d0" data-s=":triangular_ruler:" title="triangular ruler">&#x1f4d0;</span>
             <span class="em em-objects-1f4cf" data-c="1f4cf" data-s=":straight_ruler:" title="straight ruler">&#x1f4cf;</span>
             <span class="em em-objects-1f9f7" data-c="1f9f7" data-s=":safety_pin:" title="safety pin">&#x1f9f7;</span>
             <span class="em em-objects-1f4cc" data-c="1f4cc" data-s=":pushpin:" title="pushpin">&#x1f4cc;</span>
             <span class="em em-objects-1f4cd" data-c="1f4cd" data-s=":round_pushpin:" title="round pushpin">&#x1f4cd;</span>
-            <span class="em em-objects-2702" data-c="2702" data-s=":scissors:" title="scissors">&#x2702;</span>
-            <span class="em em-objects-1f58a" data-c="1f58a" data-s=":pen_ballpoint:" title="pen">&#x1f58a;</span>
-            <span class="em em-objects-1f58b" data-c="1f58b" data-s=":pen_fountain:" title="fountain pen">&#x1f58b;</span>
-            <span class="em em-objects-2712" data-c="2712" data-s=":black_nib:" title="black nib">&#x2712;</span>
-            <span class="em em-objects-1f58c" data-c="1f58c" data-s=":paintbrush:" title="paintbrush">&#x1f58c;</span>
-            <span class="em em-objects-1f58d" data-c="1f58d" data-s=":crayon:" title="crayon">&#x1f58d;</span>
+            <span class="em em-objects-2702" data-c="2702" data-s=":scissors:" title="scissors">&#x2702;&#xfe0f;</span>
+            <span class="em em-objects-1f58a" data-c="1f58a" data-s=":pen_ballpoint:" title="pen">&#x1f58a;&#xfe0f;</span>
+            <span class="em em-objects-1f58b" data-c="1f58b" data-s=":pen_fountain:" title="fountain pen">&#x1f58b;&#xfe0f;</span>
+            <span class="em em-objects-2712" data-c="2712" data-s=":black_nib:" title="black nib">&#x2712;&#xfe0f;</span>
+            <span class="em em-objects-1f58c" data-c="1f58c" data-s=":paintbrush:" title="paintbrush">&#x1f58c;&#xfe0f;</span>
+            <span class="em em-objects-1f58d" data-c="1f58d" data-s=":crayon:" title="crayon">&#x1f58d;&#xfe0f;</span>
             <span class="em em-objects-1f4dd" data-c="1f4dd" data-s=":pencil:" title="memo">&#x1f4dd;</span>
-            <span class="em em-objects-270f" data-c="270f" data-s=":pencil2:" title="pencil">&#x270f;</span>
+            <span class="em em-objects-270f" data-c="270f" data-s=":pencil2:" title="pencil">&#x270f;&#xfe0f;</span>
             <span class="em em-objects-1f50d" data-c="1f50d" data-s=":mag:" title="magnifying glass tilted left">&#x1f50d;</span>
             <span class="em em-objects-1f50e" data-c="1f50e" data-s=":mag_right:" title="magnifying glass tilted right">&#x1f50e;</span>
             <span class="em em-objects-1f50f" data-c="1f50f" data-s=":lock_with_ink_pen:" title="locked with pen">&#x1f50f;</span>
@@ -2034,7 +2034,7 @@
             <img src="emoji/symbols.svg" class="em-symbols" height="24" width="24" role="button" tabindex="0"></span>
         </label>
         <div class="content">
-            <span class="em em-symbols-2764" data-c="2764" data-s=":heart:" title="red heart">&#x2764;</span>
+            <span class="em em-symbols-2764" data-c="2764" data-s=":heart:" title="red heart">&#x2764;&#xfe0f;</span>
             <span class="em em-symbols-1f9e1" data-c="1f9e1" data-s=":orange_heart:" title="orange heart">&#x1f9e1;</span>
             <span class="em em-symbols-1f49b" data-c="1f49b" data-s=":yellow_heart:" title="yellow heart">&#x1f49b;</span>
             <span class="em em-symbols-1f49a" data-c="1f49a" data-s=":green_heart:" title="green heart">&#x1f49a;</span>
@@ -2042,7 +2042,7 @@
             <span class="em em-symbols-1f49c" data-c="1f49c" data-s=":purple_heart:" title="purple heart">&#x1f49c;</span>
             <span class="em em-symbols-1f5a4" data-c="1f5a4" data-s=":black_heart:" title="black heart">&#x1f5a4;</span>
             <span class="em em-symbols-1f494" data-c="1f494" data-s=":broken_heart:" title="broken heart">&#x1f494;</span>
-            <span class="em em-symbols-2763" data-c="2763" data-s=":heart_exclamation:" title="heavy heart exclamation">&#x2763;</span>
+            <span class="em em-symbols-2763" data-c="2763" data-s=":heart_exclamation:" title="heavy heart exclamation">&#x2763;&#xfe0f;</span>
             <span class="em em-symbols-1f495" data-c="1f495" data-s=":two_hearts:" title="two hearts">&#x1f495;</span>
             <span class="em em-symbols-1f49e" data-c="1f49e" data-s=":revolving_hearts:" title="revolving hearts">&#x1f49e;</span>
             <span class="em em-symbols-1f493" data-c="1f493" data-s=":heartbeat:" title="beating heart">&#x1f493;</span>
@@ -2051,16 +2051,16 @@
             <span class="em em-symbols-1f498" data-c="1f498" data-s=":cupid:" title="heart with arrow">&#x1f498;</span>
             <span class="em em-symbols-1f49d" data-c="1f49d" data-s=":gift_heart:" title="heart with ribbon">&#x1f49d;</span>
             <span class="em em-symbols-1f49f" data-c="1f49f" data-s=":heart_decoration:" title="heart decoration">&#x1f49f;</span>
-            <span class="em em-symbols-262e" data-c="262e" data-s=":peace:" title="peace symbol">&#x262e;</span>
-            <span class="em em-symbols-271d" data-c="271d" data-s=":cross:" title="latin cross">&#x271d;</span>
-            <span class="em em-symbols-262a" data-c="262a" data-s=":star_and_crescent:" title="star and crescent">&#x262a;</span>
-            <span class="em em-symbols-1f549" data-c="1f549" data-s=":om_symbol:" title="om">&#x1f549;</span>
-            <span class="em em-symbols-2638" data-c="2638" data-s=":wheel_of_dharma:" title="wheel of dharma">&#x2638;</span>
-            <span class="em em-symbols-2721" data-c="2721" data-s=":star_of_david:" title="star of David">&#x2721;</span>
+            <span class="em em-symbols-262e" data-c="262e" data-s=":peace:" title="peace symbol">&#x262e;&#xfe0f;</span>
+            <span class="em em-symbols-271d" data-c="271d" data-s=":cross:" title="latin cross">&#x271d;&#xfe0f;</span>
+            <span class="em em-symbols-262a" data-c="262a" data-s=":star_and_crescent:" title="star and crescent">&#x262a;&#xfe0f;</span>
+            <span class="em em-symbols-1f549" data-c="1f549" data-s=":om_symbol:" title="om">&#x1f549;&#xfe0f;</span>
+            <span class="em em-symbols-2638" data-c="2638" data-s=":wheel_of_dharma:" title="wheel of dharma">&#x2638;&#xfe0f;</span>
+            <span class="em em-symbols-2721" data-c="2721" data-s=":star_of_david:" title="star of David">&#x2721;&#xfe0f;</span>
             <span class="em em-symbols-1f52f" data-c="1f52f" data-s=":six_pointed_star:" title="dotted six-pointed star">&#x1f52f;</span>
             <span class="em em-symbols-1f54e" data-c="1f54e" data-s=":menorah:" title="menorah">&#x1f54e;</span>
-            <span class="em em-symbols-262f" data-c="262f" data-s=":yin_yang:" title="yin yang">&#x262f;</span>
-            <span class="em em-symbols-2626" data-c="2626" data-s=":orthodox_cross:" title="orthodox cross">&#x2626;</span>
+            <span class="em em-symbols-262f" data-c="262f" data-s=":yin_yang:" title="yin yang">&#x262f;&#xfe0f;</span>
+            <span class="em em-symbols-2626" data-c="2626" data-s=":orthodox_cross:" title="orthodox cross">&#x2626;&#xfe0f;</span>
             <span class="em em-symbols-1f6d0" data-c="1f6d0" data-s=":place_of_worship:" title="place of worship">&#x1f6d0;</span>
             <span class="em em-symbols-26ce" data-c="26ce" data-s=":ophiuchus:" title="Ophiuchus">&#x26ce;</span>
             <span class="em em-symbols-2648" data-c="2648" data-s=":aries:" title="Aries">&#x2648;</span>
@@ -2076,33 +2076,33 @@
             <span class="em em-symbols-2652" data-c="2652" data-s=":aquarius:" title="Aquarius">&#x2652;</span>
             <span class="em em-symbols-2653" data-c="2653" data-s=":pisces:" title="Pisces">&#x2653;</span>
             <span class="em em-symbols-1f194" data-c="1f194" data-s=":id:" title="ID button">&#x1f194;</span>
-            <span class="em em-symbols-269b" data-c="269b" data-s=":atom:" title="atom symbol">&#x269b;</span>
+            <span class="em em-symbols-269b" data-c="269b" data-s=":atom:" title="atom symbol">&#x269b;&#xfe0f;</span>
             <span class="em em-symbols-267e" data-c="267e" data-s=":infinity:" title="infinity">&#x267e;&#xfe0f;</span>
             <span class="em em-symbols-1f251" data-c="1f251" data-s=":accept:" title="Japanese “acceptable” button">&#x1f251;</span>
-            <span class="em em-symbols-2622" data-c="2622" data-s=":radioactive:" title="radioactive">&#x2622;</span>
-            <span class="em em-symbols-2623" data-c="2623" data-s=":biohazard:" title="biohazard">&#x2623;</span>
+            <span class="em em-symbols-2622" data-c="2622" data-s=":radioactive:" title="radioactive">&#x2622;&#xfe0f;</span>
+            <span class="em em-symbols-2623" data-c="2623" data-s=":biohazard:" title="biohazard">&#x2623;&#xfe0f;</span>
             <span class="em em-symbols-1f4f4" data-c="1f4f4" data-s=":mobile_phone_off:" title="mobile phone off">&#x1f4f4;</span>
             <span class="em em-symbols-1f4f3" data-c="1f4f3" data-s=":vibration_mode:" title="vibration mode">&#x1f4f3;</span>
             <span class="em em-symbols-1f236" data-c="1f236" data-s=":u6709:" title="Japanese “not free of charge” button">&#x1f236;</span>
             <span class="em em-symbols-1f21a" data-c="1f21a" data-s=":u7121:" title="Japanese “free of charge” button">&#x1f21a;</span>
             <span class="em em-symbols-1f238" data-c="1f238" data-s=":u7533:" title="Japanese “application” button">&#x1f238;</span>
             <span class="em em-symbols-1f23a" data-c="1f23a" data-s=":u55b6:" title="Japanese “open for business” button">&#x1f23a;</span>
-            <span class="em em-symbols-1f237" data-c="1f237" data-s=":u6708:" title="Japanese “monthly amount” button">&#x1f237;</span>
-            <span class="em em-symbols-2734" data-c="2734" data-s=":eight_pointed_black_star:" title="eight-pointed star">&#x2734;</span>
+            <span class="em em-symbols-1f237" data-c="1f237" data-s=":u6708:" title="Japanese “monthly amount” button">&#x1f237;&#xfe0f;</span>
+            <span class="em em-symbols-2734" data-c="2734" data-s=":eight_pointed_black_star:" title="eight-pointed star">&#x2734;&#xfe0f;</span>
             <span class="em em-symbols-1f19a" data-c="1f19a" data-s=":vs:" title="VS button">&#x1f19a;</span>
             <span class="em em-symbols-1f4ae" data-c="1f4ae" data-s=":white_flower:" title="white flower">&#x1f4ae;</span>
             <span class="em em-symbols-1f250" data-c="1f250" data-s=":ideograph_advantage:" title="Japanese “bargain” button">&#x1f250;</span>
-            <span class="em em-symbols-3299" data-c="3299" data-s=":secret:" title="Japanese “secret” button">&#x3299;</span>
-            <span class="em em-symbols-3297" data-c="3297" data-s=":congratulations:" title="Japanese “congratulations” button">&#x3297;</span>
+            <span class="em em-symbols-3299" data-c="3299" data-s=":secret:" title="Japanese “secret” button">&#x3299;&#xfe0f;</span>
+            <span class="em em-symbols-3297" data-c="3297" data-s=":congratulations:" title="Japanese “congratulations” button">&#x3297;&#xfe0f;</span>
             <span class="em em-symbols-1f234" data-c="1f234" data-s=":u5408:" title="Japanese “passing grade” button">&#x1f234;</span>
             <span class="em em-symbols-1f235" data-c="1f235" data-s=":u6e80:" title="Japanese “no vacancy” button">&#x1f235;</span>
             <span class="em em-symbols-1f239" data-c="1f239" data-s=":u5272:" title="Japanese “discount” button">&#x1f239;</span>
             <span class="em em-symbols-1f232" data-c="1f232" data-s=":u7981:" title="Japanese “prohibited” button">&#x1f232;</span>
-            <span class="em em-symbols-1f170" data-c="1f170" data-s=":a:" title="A button (blood type)">&#x1f170;</span>
-            <span class="em em-symbols-1f171" data-c="1f171" data-s=":b:" title="B button (blood type)">&#x1f171;</span>
+            <span class="em em-symbols-1f170" data-c="1f170" data-s=":a:" title="A button (blood type)">&#x1f170;&#xfe0f;</span>
+            <span class="em em-symbols-1f171" data-c="1f171" data-s=":b:" title="B button (blood type)">&#x1f171;&#xfe0f;</span>
             <span class="em em-symbols-1f18e" data-c="1f18e" data-s=":ab:" title="AB button (blood type)">&#x1f18e;</span>
             <span class="em em-symbols-1f191" data-c="1f191" data-s=":cl:" title="CL button">&#x1f191;</span>
-            <span class="em em-symbols-1f17e" data-c="1f17e" data-s=":o2:" title="O button (blood type)">&#x1f17e;</span>
+            <span class="em em-symbols-1f17e" data-c="1f17e" data-s=":o2:" title="O button (blood type)">&#x1f17e;&#xfe0f;</span>
             <span class="em em-symbols-1f198" data-c="1f198" data-s=":sos:" title="SOS button">&#x1f198;</span>
             <span class="em em-symbols-274c" data-c="274c" data-s=":x:" title="cross mark">&#x274c;</span>
             <span class="em em-symbols-2b55" data-c="2b55" data-s=":o:" title="heavy large circle">&#x2b55;</span>
@@ -2112,7 +2112,7 @@
             <span class="em em-symbols-1f6ab" data-c="1f6ab" data-s=":no_entry_sign:" title="prohibited">&#x1f6ab;</span>
             <span class="em em-symbols-1f4af" data-c="1f4af" data-s=":100:" title="hundred points">&#x1f4af;</span>
             <span class="em em-symbols-1f4a2" data-c="1f4a2" data-s=":anger:" title="anger symbol">&#x1f4a2;</span>
-            <span class="em em-symbols-2668" data-c="2668" data-s=":hotsprings:" title="hot springs">&#x2668;</span>
+            <span class="em em-symbols-2668" data-c="2668" data-s=":hotsprings:" title="hot springs">&#x2668;&#xfe0f;</span>
             <span class="em em-symbols-1f6b7" data-c="1f6b7" data-s=":no_pedestrians:" title="no pedestrians">&#x1f6b7;</span>
             <span class="em em-symbols-1f6af" data-c="1f6af" data-s=":do_not_litter:" title="no littering">&#x1f6af;</span>
             <span class="em em-symbols-1f6b3" data-c="1f6b3" data-s=":no_bicycles:" title="no bicycles">&#x1f6b3;</span>
@@ -2124,34 +2124,34 @@
             <span class="em em-symbols-2755" data-c="2755" data-s=":grey_exclamation:" title="white exclamation mark">&#x2755;</span>
             <span class="em em-symbols-2753" data-c="2753" data-s=":question:" title="question mark">&#x2753;</span>
             <span class="em em-symbols-2754" data-c="2754" data-s=":grey_question:" title="white question mark">&#x2754;</span>
-            <span class="em em-symbols-203c" data-c="203c" data-s=":bangbang:" title="double exclamation mark">&#x203c;</span>
-            <span class="em em-symbols-2049" data-c="2049" data-s=":interrobang:" title="exclamation question mark">&#x2049;</span>
+            <span class="em em-symbols-203c" data-c="203c" data-s=":bangbang:" title="double exclamation mark">&#x203c;&#xfe0f;</span>
+            <span class="em em-symbols-2049" data-c="2049" data-s=":interrobang:" title="exclamation question mark">&#x2049;&#xfe0f;</span>
             <span class="em em-symbols-1f505" data-c="1f505" data-s=":low_brightness:" title="dim button">&#x1f505;</span>
             <span class="em em-symbols-1f506" data-c="1f506" data-s=":high_brightness:" title="bright button">&#x1f506;</span>
-            <span class="em em-symbols-303d" data-c="303d" data-s=":part_alternation_mark:" title="part alternation mark">&#x303d;</span>
-            <span class="em em-symbols-26a0" data-c="26a0" data-s=":warning:" title="warning">&#x26a0;</span>
+            <span class="em em-symbols-303d" data-c="303d" data-s=":part_alternation_mark:" title="part alternation mark">&#x303d;&#xfe0f;</span>
+            <span class="em em-symbols-26a0" data-c="26a0" data-s=":warning:" title="warning">&#x26a0;&#xfe0f;</span>
             <span class="em em-symbols-1f6b8" data-c="1f6b8" data-s=":children_crossing:" title="children crossing">&#x1f6b8;</span>
             <span class="em em-symbols-1f531" data-c="1f531" data-s=":trident:" title="trident emblem">&#x1f531;</span>
-            <span class="em em-symbols-269c" data-c="269c" data-s=":fleur-de-lis:" title="fleur-de-lis">&#x269c;</span>
+            <span class="em em-symbols-269c" data-c="269c" data-s=":fleur-de-lis:" title="fleur-de-lis">&#x269c;&#xfe0f;</span>
             <span class="em em-symbols-1f530" data-c="1f530" data-s=":beginner:" title="Japanese symbol for beginner">&#x1f530;</span>
-            <span class="em em-symbols-267b" data-c="267b" data-s=":recycle:" title="recycling symbol">&#x267b;</span>
+            <span class="em em-symbols-267b" data-c="267b" data-s=":recycle:" title="recycling symbol">&#x267b;&#xfe0f;</span>
             <span class="em em-symbols-2705" data-c="2705" data-s=":white_check_mark:" title="white heavy check mark">&#x2705;</span>
             <span class="em em-symbols-1f22f" data-c="1f22f" data-s=":u6307:" title="Japanese “reserved” button">&#x1f22f;</span>
             <span class="em em-symbols-1f4b9" data-c="1f4b9" data-s=":chart:" title="chart increasing with yen">&#x1f4b9;</span>
-            <span class="em em-symbols-2747" data-c="2747" data-s=":sparkle:" title="sparkle">&#x2747;</span>
-            <span class="em em-symbols-2733" data-c="2733" data-s=":eight_spoked_asterisk:" title="eight-spoked asterisk">&#x2733;</span>
+            <span class="em em-symbols-2747" data-c="2747" data-s=":sparkle:" title="sparkle">&#x2747;&#xfe0f;</span>
+            <span class="em em-symbols-2733" data-c="2733" data-s=":eight_spoked_asterisk:" title="eight-spoked asterisk">&#x2733;&#xfe0f;</span>
             <span class="em em-symbols-274e" data-c="274e" data-s=":negative_squared_cross_mark:" title="cross mark button">&#x274e;</span>
             <span class="em em-symbols-1f310" data-c="1f310" data-s=":globe_with_meridians:" title="globe with meridians">&#x1f310;</span>
             <span class="em em-symbols-1f4a0" data-c="1f4a0" data-s=":diamond_shape_with_a_dot_inside:" title="diamond with a dot">&#x1f4a0;</span>
-            <span class="em em-symbols-24c2" data-c="24c2" data-s=":m:" title="circled M">&#x24c2;</span>
+            <span class="em em-symbols-24c2" data-c="24c2" data-s=":m:" title="circled M">&#x24c2;&#xfe0f;</span>
             <span class="em em-symbols-1f300" data-c="1f300" data-s=":cyclone:" title="cyclone">&#x1f300;</span>
             <span class="em em-symbols-1f4a4" data-c="1f4a4" data-s=":zzz:" title="zzz">&#x1f4a4;</span>
             <span class="em em-symbols-1f3e7" data-c="1f3e7" data-s=":atm:" title="ATM sign">&#x1f3e7;</span>
             <span class="em em-symbols-1f6be" data-c="1f6be" data-s=":wc:" title="water closet">&#x1f6be;</span>
             <span class="em em-symbols-267f" data-c="267f" data-s=":wheelchair:" title="wheelchair symbol">&#x267f;</span>
-            <span class="em em-symbols-1f17f" data-c="1f17f" data-s=":parking:" title="P button">&#x1f17f;</span>
+            <span class="em em-symbols-1f17f" data-c="1f17f" data-s=":parking:" title="P button">&#x1f17f;&#xfe0f;</span>
             <span class="em em-symbols-1f233" data-c="1f233" data-s=":u7a7a:" title="Japanese “vacancy” button">&#x1f233;</span>
-            <span class="em em-symbols-1f202" data-c="1f202" data-s=":sa:" title="Japanese “service charge” button">&#x1f202;</span>
+            <span class="em em-symbols-1f202" data-c="1f202" data-s=":sa:" title="Japanese “service charge” button">&#x1f202;&#xfe0f;</span>
             <span class="em em-symbols-1f6c2" data-c="1f6c2" data-s=":passport_control:" title="passport control">&#x1f6c2;</span>
             <span class="em em-symbols-1f6c3" data-c="1f6c3" data-s=":customs:" title="customs">&#x1f6c3;</span>
             <span class="em em-symbols-1f6c4" data-c="1f6c4" data-s=":baggage_claim:" title="baggage claim">&#x1f6c4;</span>
@@ -2165,7 +2165,7 @@
             <span class="em em-symbols-1f4f6" data-c="1f4f6" data-s=":signal_strength:" title="antenna bars">&#x1f4f6;</span>
             <span class="em em-symbols-1f201" data-c="1f201" data-s=":koko:" title="Japanese “here” button">&#x1f201;</span>
             <span class="em em-symbols-1f523" data-c="1f523" data-s=":symbols:" title="input symbols">&#x1f523;</span>
-            <span class="em em-symbols-2139" data-c="2139" data-s=":information_source:" title="information">&#x2139;</span>
+            <span class="em em-symbols-2139" data-c="2139" data-s=":information_source:" title="information">&#x2139;&#xfe0f;</span>
             <span class="em em-symbols-1f524" data-c="1f524" data-s=":abc:" title="input latin letters">&#x1f524;</span>
             <span class="em em-symbols-1f521" data-c="1f521" data-s=":abcd:" title="input latin lowercase">&#x1f521;</span>
             <span class="em em-symbols-1f520" data-c="1f520" data-s=":capital_abcd:" title="input latin uppercase">&#x1f520;</span>
@@ -2189,35 +2189,35 @@
             <span class="em em-symbols-1f522" data-c="1f522" data-s=":1234:" title="input numbers">&#x1f522;</span>
             <span class="em em-symbols-23-20e3" data-c="23-20e3" data-s=":hash:" title="keycap: #">&#x23;&#xfe0f;&#x20e3;</span>
             <span class="em em-symbols-2a-20e3" data-c="2a-20e3" data-s=":asterisk:" title="keycap: *">&#x2a;&#xfe0f;&#x20e3;</span>
-            <span class="em em-symbols-23cf" data-c="23cf" data-s=":eject:" title="eject button">&#x23cf;</span>
-            <span class="em em-symbols-25b6" data-c="25b6" data-s=":arrow_forward:" title="play button">&#x25b6;</span>
-            <span class="em em-symbols-23f8" data-c="23f8" data-s=":pause_button:" title="pause button">&#x23f8;</span>
-            <span class="em em-symbols-23ef" data-c="23ef" data-s=":play_pause:" title="play or pause button">&#x23ef;</span>
-            <span class="em em-symbols-23f9" data-c="23f9" data-s=":stop_button:" title="stop button">&#x23f9;</span>
-            <span class="em em-symbols-23fa" data-c="23fa" data-s=":record_button:" title="record button">&#x23fa;</span>
-            <span class="em em-symbols-23ed" data-c="23ed" data-s=":track_next:" title="next track button">&#x23ed;</span>
-            <span class="em em-symbols-23ee" data-c="23ee" data-s=":track_previous:" title="last track button">&#x23ee;</span>
+            <span class="em em-symbols-23cf" data-c="23cf" data-s=":eject:" title="eject button">&#x23cf;&#xfe0f;</span>
+            <span class="em em-symbols-25b6" data-c="25b6" data-s=":arrow_forward:" title="play button">&#x25b6;&#xfe0f;</span>
+            <span class="em em-symbols-23f8" data-c="23f8" data-s=":pause_button:" title="pause button">&#x23f8;&#xfe0f;</span>
+            <span class="em em-symbols-23ef" data-c="23ef" data-s=":play_pause:" title="play or pause button">&#x23ef;&#xfe0f;</span>
+            <span class="em em-symbols-23f9" data-c="23f9" data-s=":stop_button:" title="stop button">&#x23f9;&#xfe0f;</span>
+            <span class="em em-symbols-23fa" data-c="23fa" data-s=":record_button:" title="record button">&#x23fa;&#xfe0f;</span>
+            <span class="em em-symbols-23ed" data-c="23ed" data-s=":track_next:" title="next track button">&#x23ed;&#xfe0f;</span>
+            <span class="em em-symbols-23ee" data-c="23ee" data-s=":track_previous:" title="last track button">&#x23ee;&#xfe0f;</span>
             <span class="em em-symbols-23e9" data-c="23e9" data-s=":fast_forward:" title="fast-forward button">&#x23e9;</span>
             <span class="em em-symbols-23ea" data-c="23ea" data-s=":rewind:" title="fast reverse button">&#x23ea;</span>
             <span class="em em-symbols-23eb" data-c="23eb" data-s=":arrow_double_up:" title="fast up button">&#x23eb;</span>
             <span class="em em-symbols-23ec" data-c="23ec" data-s=":arrow_double_down:" title="fast down button">&#x23ec;</span>
-            <span class="em em-symbols-25c0" data-c="25c0" data-s=":arrow_backward:" title="reverse button">&#x25c0;</span>
+            <span class="em em-symbols-25c0" data-c="25c0" data-s=":arrow_backward:" title="reverse button">&#x25c0;&#xfe0f;</span>
             <span class="em em-symbols-1f53c" data-c="1f53c" data-s=":arrow_up_small:" title="upwards button">&#x1f53c;</span>
             <span class="em em-symbols-1f53d" data-c="1f53d" data-s=":arrow_down_small:" title="downwards button">&#x1f53d;</span>
-            <span class="em em-symbols-27a1" data-c="27a1" data-s=":arrow_right:" title="right arrow">&#x27a1;</span>
-            <span class="em em-symbols-2b05" data-c="2b05" data-s=":arrow_left:" title="left arrow">&#x2b05;</span>
-            <span class="em em-symbols-2b06" data-c="2b06" data-s=":arrow_up:" title="up arrow">&#x2b06;</span>
-            <span class="em em-symbols-2b07" data-c="2b07" data-s=":arrow_down:" title="down arrow">&#x2b07;</span>
-            <span class="em em-symbols-2197" data-c="2197" data-s=":arrow_upper_right:" title="up-right arrow">&#x2197;</span>
-            <span class="em em-symbols-2198" data-c="2198" data-s=":arrow_lower_right:" title="down-right arrow">&#x2198;</span>
-            <span class="em em-symbols-2199" data-c="2199" data-s=":arrow_lower_left:" title="down-left arrow">&#x2199;</span>
-            <span class="em em-symbols-2196" data-c="2196" data-s=":arrow_upper_left:" title="up-left arrow">&#x2196;</span>
-            <span class="em em-symbols-2195" data-c="2195" data-s=":arrow_up_down:" title="up-down arrow">&#x2195;</span>
-            <span class="em em-symbols-2194" data-c="2194" data-s=":left_right_arrow:" title="left-right arrow">&#x2194;</span>
-            <span class="em em-symbols-21aa" data-c="21aa" data-s=":arrow_right_hook:" title="left arrow curving right">&#x21aa;</span>
-            <span class="em em-symbols-21a9" data-c="21a9" data-s=":leftwards_arrow_with_hook:" title="right arrow curving left">&#x21a9;</span>
-            <span class="em em-symbols-2934" data-c="2934" data-s=":arrow_heading_up:" title="right arrow curving up">&#x2934;</span>
-            <span class="em em-symbols-2935" data-c="2935" data-s=":arrow_heading_down:" title="right arrow curving down">&#x2935;</span>
+            <span class="em em-symbols-27a1" data-c="27a1" data-s=":arrow_right:" title="right arrow">&#x27a1;&#xfe0f;</span>
+            <span class="em em-symbols-2b05" data-c="2b05" data-s=":arrow_left:" title="left arrow">&#x2b05;&#xfe0f;</span>
+            <span class="em em-symbols-2b06" data-c="2b06" data-s=":arrow_up:" title="up arrow">&#x2b06;&#xfe0f;</span>
+            <span class="em em-symbols-2b07" data-c="2b07" data-s=":arrow_down:" title="down arrow">&#x2b07;&#xfe0f;</span>
+            <span class="em em-symbols-2197" data-c="2197" data-s=":arrow_upper_right:" title="up-right arrow">&#x2197;&#xfe0f;</span>
+            <span class="em em-symbols-2198" data-c="2198" data-s=":arrow_lower_right:" title="down-right arrow">&#x2198;&#xfe0f;</span>
+            <span class="em em-symbols-2199" data-c="2199" data-s=":arrow_lower_left:" title="down-left arrow">&#x2199;&#xfe0f;</span>
+            <span class="em em-symbols-2196" data-c="2196" data-s=":arrow_upper_left:" title="up-left arrow">&#x2196;&#xfe0f;</span>
+            <span class="em em-symbols-2195" data-c="2195" data-s=":arrow_up_down:" title="up-down arrow">&#x2195;&#xfe0f;</span>
+            <span class="em em-symbols-2194" data-c="2194" data-s=":left_right_arrow:" title="left-right arrow">&#x2194;&#xfe0f;</span>
+            <span class="em em-symbols-21aa" data-c="21aa" data-s=":arrow_right_hook:" title="left arrow curving right">&#x21aa;&#xfe0f;</span>
+            <span class="em em-symbols-21a9" data-c="21a9" data-s=":leftwards_arrow_with_hook:" title="right arrow curving left">&#x21a9;&#xfe0f;</span>
+            <span class="em em-symbols-2934" data-c="2934" data-s=":arrow_heading_up:" title="right arrow curving up">&#x2934;&#xfe0f;</span>
+            <span class="em em-symbols-2935" data-c="2935" data-s=":arrow_heading_down:" title="right arrow curving down">&#x2935;&#xfe0f;</span>
             <span class="em em-symbols-1f500" data-c="1f500" data-s=":twisted_rightwards_arrows:" title="shuffle tracks button">&#x1f500;</span>
             <span class="em em-symbols-1f501" data-c="1f501" data-s=":repeat:" title="repeat button">&#x1f501;</span>
             <span class="em em-symbols-1f502" data-c="1f502" data-s=":repeat_one:" title="repeat single button">&#x1f502;</span>
@@ -2228,13 +2228,13 @@
             <span class="em em-symbols-2795" data-c="2795" data-s=":heavy_plus_sign:" title="heavy plus sign">&#x2795;</span>
             <span class="em em-symbols-2796" data-c="2796" data-s=":heavy_minus_sign:" title="heavy minus sign">&#x2796;</span>
             <span class="em em-symbols-2797" data-c="2797" data-s=":heavy_division_sign:" title="heavy division sign">&#x2797;</span>
-            <span class="em em-symbols-2716" data-c="2716" data-s=":heavy_multiplication_x:" title="heavy multiplication x">&#x2716;</span>
+            <span class="em em-symbols-2716" data-c="2716" data-s=":heavy_multiplication_x:" title="heavy multiplication x">&#x2716;&#xfe0f;</span>
             <span class="em em-symbols-1f4b2" data-c="1f4b2" data-s=":heavy_dollar_sign:" title="heavy dollar sign">&#x1f4b2;</span>
             <span class="em em-symbols-1f4b1" data-c="1f4b1" data-s=":currency_exchange:" title="currency exchange">&#x1f4b1;</span>
             <span class="em em-symbols-2122" data-c="2122" data-s=":tm:" title="trade mark">&#x2122;&#xfe0f;</span>
             <span class="em em-symbols-a9" data-c="a9" data-s=":copyright:" title="copyright">&#xa9;&#xfe0f;</span>
             <span class="em em-symbols-ae" data-c="ae" data-s=":registered:" title="registered">&#xae;&#xfe0f;</span>
-            <span class="em em-symbols-3030" data-c="3030" data-s=":wavy_dash:" title="wavy dash">&#x3030;</span>
+            <span class="em em-symbols-3030" data-c="3030" data-s=":wavy_dash:" title="wavy dash">&#x3030;&#xfe0f;</span>
             <span class="em em-symbols-27b0" data-c="27b0" data-s=":curly_loop:" title="curly loop">&#x27b0;</span>
             <span class="em em-symbols-27bf" data-c="27bf" data-s=":loop:" title="double curly loop">&#x27bf;</span>
             <span class="em em-symbols-1f51a" data-c="1f51a" data-s=":end:" title="END arrow">&#x1f51a;</span>
@@ -2242,8 +2242,8 @@
             <span class="em em-symbols-1f51b" data-c="1f51b" data-s=":on:" title="ON! arrow">&#x1f51b;</span>
             <span class="em em-symbols-1f51d" data-c="1f51d" data-s=":top:" title="TOP arrow">&#x1f51d;</span>
             <span class="em em-symbols-1f51c" data-c="1f51c" data-s=":soon:" title="SOON arrow">&#x1f51c;</span>
-            <span class="em em-symbols-2714" data-c="2714" data-s=":heavy_check_mark:" title="heavy check mark">&#x2714;</span>
-            <span class="em em-symbols-2611" data-c="2611" data-s=":ballot_box_with_check:" title="ballot box with check">&#x2611;</span>
+            <span class="em em-symbols-2714" data-c="2714" data-s=":heavy_check_mark:" title="heavy check mark">&#x2714;&#xfe0f;</span>
+            <span class="em em-symbols-2611" data-c="2611" data-s=":ballot_box_with_check:" title="ballot box with check">&#x2611;&#xfe0f;</span>
             <span class="em em-symbols-1f518" data-c="1f518" data-s=":radio_button:" title="radio button">&#x1f518;</span>
             <span class="em em-symbols-26aa" data-c="26aa" data-s=":white_circle:" title="white circle">&#x26aa;</span>
             <span class="em em-symbols-26ab" data-c="26ab" data-s=":black_circle:" title="black circle">&#x26ab;</span>
@@ -2257,12 +2257,12 @@
             <span class="em em-symbols-1f537" data-c="1f537" data-s=":large_blue_diamond:" title="large blue diamond">&#x1f537;</span>
             <span class="em em-symbols-1f533" data-c="1f533" data-s=":white_square_button:" title="white square button">&#x1f533;</span>
             <span class="em em-symbols-1f532" data-c="1f532" data-s=":black_square_button:" title="black square button">&#x1f532;</span>
-            <span class="em em-symbols-25aa" data-c="25aa" data-s=":black_small_square:" title="black small square">&#x25aa;</span>
-            <span class="em em-symbols-25ab" data-c="25ab" data-s=":white_small_square:" title="white small square">&#x25ab;</span>
+            <span class="em em-symbols-25aa" data-c="25aa" data-s=":black_small_square:" title="black small square">&#x25aa;&#xfe0f;</span>
+            <span class="em em-symbols-25ab" data-c="25ab" data-s=":white_small_square:" title="white small square">&#x25ab;&#xfe0f;</span>
             <span class="em em-symbols-25fe" data-c="25fe" data-s=":black_medium_small_square:" title="black medium-small square">&#x25fe;</span>
             <span class="em em-symbols-25fd" data-c="25fd" data-s=":white_medium_small_square:" title="white medium-small square">&#x25fd;</span>
-            <span class="em em-symbols-25fc" data-c="25fc" data-s=":black_medium_square:" title="black medium square">&#x25fc;</span>
-            <span class="em em-symbols-25fb" data-c="25fb" data-s=":white_medium_square:" title="white medium square">&#x25fb;</span>
+            <span class="em em-symbols-25fc" data-c="25fc" data-s=":black_medium_square:" title="black medium square">&#x25fc;&#xfe0f;</span>
+            <span class="em em-symbols-25fb" data-c="25fb" data-s=":white_medium_square:" title="white medium square">&#x25fb;&#xfe0f;</span>
             <span class="em em-symbols-2b1b" data-c="2b1b" data-s=":black_large_square:" title="black large square">&#x2b1b;</span>
             <span class="em em-symbols-2b1c" data-c="2b1c" data-s=":white_large_square:" title="white large square">&#x2b1c;</span>
             <span class="em em-symbols-1f508" data-c="1f508" data-s=":speaker:" title="speaker low volume">&#x1f508;</span>
@@ -2273,15 +2273,15 @@
             <span class="em em-symbols-1f515" data-c="1f515" data-s=":no_bell:" title="bell with slash">&#x1f515;</span>
             <span class="em em-symbols-1f4e3" data-c="1f4e3" data-s=":mega:" title="megaphone">&#x1f4e3;</span>
             <span class="em em-symbols-1f4e2" data-c="1f4e2" data-s=":loudspeaker:" title="loudspeaker">&#x1f4e2;</span>
-            <span class="em em-symbols-1f5e8" data-c="1f5e8" data-s=":speech_left:" title="left speech bubble">&#x1f5e8;</span>
+            <span class="em em-symbols-1f5e8" data-c="1f5e8" data-s=":speech_left:" title="left speech bubble">&#x1f5e8;&#xfe0f;</span>
             <span class="em em-symbols-1f441-200d-1f5e8" data-c="1f441-200d-1f5e8" data-s=":eye_in_speech_bubble:" title="eye in speech bubble">&#x1f441;&#xfe0f;&#x200d;&#x1f5e8;&#xfe0f;</span>
             <span class="em em-symbols-1f4ac" data-c="1f4ac" data-s=":speech_balloon:" title="speech balloon">&#x1f4ac;</span>
             <span class="em em-symbols-1f4ad" data-c="1f4ad" data-s=":thought_balloon:" title="thought balloon">&#x1f4ad;</span>
-            <span class="em em-symbols-1f5ef" data-c="1f5ef" data-s=":anger_right:" title="right anger bubble">&#x1f5ef;</span>
-            <span class="em em-symbols-2660" data-c="2660" data-s=":spades:" title="spade suit">&#x2660;</span>
-            <span class="em em-symbols-2663" data-c="2663" data-s=":clubs:" title="club suit">&#x2663;</span>
-            <span class="em em-symbols-2665" data-c="2665" data-s=":hearts:" title="heart suit">&#x2665;</span>
-            <span class="em em-symbols-2666" data-c="2666" data-s=":diamonds:" title="diamond suit">&#x2666;</span>
+            <span class="em em-symbols-1f5ef" data-c="1f5ef" data-s=":anger_right:" title="right anger bubble">&#x1f5ef;&#xfe0f;</span>
+            <span class="em em-symbols-2660" data-c="2660" data-s=":spades:" title="spade suit">&#x2660;&#xfe0f;</span>
+            <span class="em em-symbols-2663" data-c="2663" data-s=":clubs:" title="club suit">&#x2663;&#xfe0f;</span>
+            <span class="em em-symbols-2665" data-c="2665" data-s=":hearts:" title="heart suit">&#x2665;&#xfe0f;</span>
+            <span class="em em-symbols-2666" data-c="2666" data-s=":diamonds:" title="diamond suit">&#x2666;&#xfe0f;</span>
             <span class="em em-symbols-1f0cf" data-c="1f0cf" data-s=":black_joker:" title="joker">&#x1f0cf;</span>
             <span class="em em-symbols-1f3b4" data-c="1f3b4" data-s=":flower_playing_cards:" title="flower playing cards">&#x1f3b4;</span>
             <span class="em em-symbols-1f004" data-c="1f004" data-s=":mahjong:" title="mahjong red dragon">&#x1f004;</span>
@@ -2317,7 +2317,7 @@
             <img src="emoji/flags.svg" class="em-flags" height="24" width="24" role="button" tabindex="0"></span>
         </label>
         <div class="content">
-            <span class="em em-flags-1f3f3" data-c="1f3f3" data-s=":flag_white:" title="white flag">&#x1f3f3;</span>
+            <span class="em em-flags-1f3f3" data-c="1f3f3" data-s=":flag_white:" title="white flag">&#x1f3f3;&#xfe0f;</span>
             <span class="em em-flags-1f3f4" data-c="1f3f4" data-s=":flag_black:" title="black flag">&#x1f3f4;</span>
             <span class="em em-flags-1f3c1" data-c="1f3c1" data-s=":checkered_flag:" title="chequered flag">&#x1f3c1;</span>
             <span class="em em-flags-1f6a9" data-c="1f6a9" data-s=":triangular_flag_on_post:" title="triangular flag">&#x1f6a9;</span>

+ 1 - 27
src/partials/messenger.ts

@@ -735,35 +735,9 @@ class ConversationController {
      * In contrast to startTyping, this method is is always called, not just if
      * the text field is non-empty.
      */
-    public onTyping = (text: string, currentWord: threema.WordResult = null) => {
+    public onTyping = (text: string) => {
         // Update draft
         this.webClientService.setDraft(this.receiver, text);
-
-        /* Make mentions readonly for now
-        if (currentWord && currentWord.substr(0, 1) === '@') {
-            this.currentMentionFilterWord = currentWord.substr(1);
-            const query = this.currentMentionFilterWord.toLowerCase().trim();
-            const selectedMentionObject = this.getSelectedMention();
-            this.currentMentions = this.allMentions.filter((i) => {
-                if (query.length === 0) {
-                    return true;
-                }
-                return i.query.indexOf(query) >= 0;
-            });
-            // If only one mention is filtered, select them
-            if (this.currentMentions.length === 1) {
-                this.selectedMention = 0;
-            } else if (selectedMentionObject !== null) {
-                // Get the new position of the latest selected mention object
-                this.selectedMention = null;
-                this.selectedMention = this.currentMentions.findIndex((m) => {
-                    return m.identity === selectedMentionObject.identity;
-                });
-            }
-        } else {
-            this.currentMentionFilterWord = null;
-        }
-        */
     }
 
     public getSelectedMention = (): threema.Mention => {

+ 0 - 55
src/services/string.ts

@@ -58,59 +58,4 @@ export class StringService {
         });
         return chunks;
     }
-
-    /**
-     * Return the word below the cursor position.
-     *
-     * If the cursor is at the end of a word, the word to the left of it will
-     * be returned.
-     *
-     * If there is whitespace to the left of the cursor, then the returned
-     * `WordResult` object will include the trimmed word to the left of the
-     * cursor. The `realLength` includes the trimmed whitespace though.
-     */
-    public getWord(input: string, pos: number, additionalSeparators: string[] = null): threema.WordResult {
-        const result = {
-            word: null,
-            realLength: 0,
-        };
-        if (input !== null && input.length > 0) {
-            const chars = Array.from(input);
-            let charFound = false;
-            const realPos = Math.min(pos, chars.length) - 1;
-
-            if (realPos > 0) {
-                const wordChars = new Array(realPos);
-                for (let n = realPos; n >= 0; n--) {
-                    const realChar = chars[n].trim();
-                    if (realChar === '') {
-                        // Abort
-                        if (charFound === false) {
-                            result.realLength++;
-                            continue;
-                        } else {
-                            break;
-                        }
-                    } else if (additionalSeparators !== null) {
-                        if (additionalSeparators.indexOf(chars[n]) > -1) {
-                            // append char
-                            result.realLength++;
-                            wordChars[n] = realChar;
-                            if (charFound === false) {
-                                continue;
-                            } else {
-                                break;
-                            }
-                        }
-                    }
-                    result.realLength++;
-                    wordChars[n] = realChar;
-                    charFound = true;
-                }
-                result.word = wordChars.join('');
-            }
-
-        }
-        return result;
-    }
 }

+ 16 - 14
src/threema.d.ts

@@ -27,9 +27,9 @@ declare namespace threema {
     }
 
     interface WireMessageAcknowledgement {
-        id: string,
-        success: boolean,
-        error?: string,
+        id: string;
+        success: boolean;
+        error?: string;
     }
 
     /**
@@ -740,18 +740,11 @@ declare namespace threema {
         isAll: boolean;
     }
 
-    interface WordResult {
-        // The trimmed word
-        word: string;
-        // The length of the untrimmed word
-        realLength: number;
-    }
-
     interface WebClientServiceStopArguments {
-        reason: DisconnectReason,
-        send: boolean,
-        close: boolean | string,
-        connectionBuildupState?: ConnectionBuildupState,
+        reason: DisconnectReason;
+        send: boolean;
+        close: boolean | string;
+        connectionBuildupState?: ConnectionBuildupState;
     }
 
     const enum ChosenTask {
@@ -768,6 +761,15 @@ declare namespace threema {
         SessionError = 'error',
     }
 
+    interface EmojiInfo {
+        // The plain emoji string
+        emojiString: string;
+        // The image path, e.g. emoji/png32/1f9df-200d-2640-fe0f.png
+        imgPath: string;
+        // The codepoint string, e.g. 1f9df-200d-2640-fe0f
+        codepoint: string;
+    }
+
     namespace Container {
         interface ReceiverData {
             contacts: ContactReceiver[];

+ 10 - 0
src/typeguards.ts

@@ -91,3 +91,13 @@ export function isTextNode(node: Node): node is Text {
 export function isElementNode(node: Node): node is HTMLElement {
     return node.nodeType === node.ELEMENT_NODE;
 }
+
+/**
+ * Emoji info type guard.
+ */
+export function isEmojiInfo(val: string | threema.EmojiInfo): val is threema.EmojiInfo {
+    return typeof val === 'object'
+        && val.emojiString !== undefined
+        && val.imgPath !== undefined
+        && val.codepoint !== undefined;
+}

+ 25 - 0
tests/bootstrap.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// tslint:disable:no-console
+
+// A dependency graph that contains any wasm must all be imported asynchronously.
+import('../src/app')
+    .then(() => console.info('Bundle loaded'))
+    .catch((e) => console.error('Could not load bundle', e));

+ 0 - 4
tests/filters.js

@@ -1,7 +1,3 @@
-afterEach(function () {
-    jasmine.clock().uninstall();
-});
-
 describe('Filters', function() {
 
     let $filter;

+ 5 - 0
tests/init.js

@@ -0,0 +1,5 @@
+// Wait for main application to be fully loaded
+beforeAll((done) => setTimeout(done, 1000));
+
+// Uninstall the mock clock after every test
+afterEach(() => jasmine.clock().uninstall());

+ 0 - 87
tests/service/string.js

@@ -16,93 +16,6 @@ describe('StringService', function() {
 
     });
 
-    describe('getWord', function () {
-
-        it('parse null string', () => {
-            expect($service.getWord(null, 1)).toEqual(jasmine.objectContaining({
-                word: null,
-                realLength: 0
-            }));
-        });
-
-        it('parse empty string', () => {
-            expect($service.getWord('', 1)).toEqual(jasmine.objectContaining({
-                word: null,
-                realLength: 0
-            }));
-        });
-
-        it('parse string (spaces)', () => {
-            expect($service.getWord('When the man comes around.', 12)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 3
-            }));
-            expect($service.getWord('When the man comes around.', 13)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 4
-            }));
-            expect($service.getWord('When the man        comes around.', 16)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 7
-            }));
-        });
-
-        it('parse string (newline)', () => {
-            expect($service.getWord("When\nthe\nman\ncomes\naround.", 12)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 3
-            }));
-            expect($service.getWord("When\nthe\nman\ncomes\naround.", 13)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 4
-            }));
-            expect($service.getWord("When\nthe\nman\n\n\n\n\n\n\n\ncomes\naround.", 16)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 7
-            }));
-        });
-
-        it('parse string (newline/spaces)', () => {
-            expect($service.getWord("When the\nman comes around.", 12)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 3
-            }));
-            expect($service.getWord("When the\nman \ncomes around.", 13)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 4
-            }));
-            expect($service.getWord("When the\nman \n \n \n \ncomes around.", 16)).toEqual(jasmine.objectContaining({
-                word: 'man',
-                realLength: 7
-            }));
-        });
-
-        it('parse string (special character)', () => {
-            expect($service.getWord('When the :man: comes around.', 15)).toEqual(jasmine.objectContaining({
-                word: ':man:',
-                realLength: 6
-            }));
-            expect($service.getWord('When the :man: comes around.', 14)).toEqual(jasmine.objectContaining({
-                word: ':man:',
-                realLength: 5
-            }));
-        });
-
-        it('parse string (with emoji (2 chars))', () => {
-            expect($service.getWord('this 😄 is a :smile: face', 19)).toEqual(jasmine.objectContaining({
-                word: ':smile:',
-                realLength: 7
-            }));
-        });
-
-        it('parse string (additional separators)', () => {
-            expect($service.getWord('When the spider:man: comes around.', 20, [':'])).toEqual(jasmine.objectContaining({
-                word: ':man:',
-                realLength: 5
-            }));
-        });
-    });
-
     describe('byteChunkSplit', function() {
         this.testPatterns = (cases, size, offset) => {
             for (let testcase of cases) {

+ 17 - 7
tests/testsuite.html

@@ -7,22 +7,32 @@
 
         <link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
 
+        <!-- Jasmine -->
         <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
         <script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
         <script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
 
+        <!-- Angular core -->
         <script src="../node_modules/angular/angular.js"></script>
-        <script src="../node_modules/angular-mocks/angular-mocks.js"></script>
-        <script src="../node_modules/angular-translate/dist/angular-translate.min.js"></script>
-        <script src="../node_modules/angular-material/angular-material.min.js"></script>
-        <script src="../node_modules/angular-animate/angular-animate.min.js"></script>
         <script src="../node_modules/angular-aria/angular-aria.min.js"></script>
+        <script src="../node_modules/angular-animate/angular-animate.min.js"></script>
+        <script src="../node_modules/angular-sanitize/angular-sanitize.min.js"></script>
+        <script src="../node_modules/angular-route/angular-route.min.js"></script>
+        <script src="../node_modules/angular-material/angular-material.min.js"></script>
+        <script src="../node_modules/angular-translate/dist/angular-translate.min.js"></script>
+
+        <!-- Angular mocking -->
+        <script src="../node_modules/angular-mocks/angular-mocks.js"></script>
 
+        <!-- SaltyRTC -->
         <script src="../node_modules/@saltyrtc/chunked-dc/dist/chunked-dc.es5.js"></script>
 
-        <script src="../dist/app.bundle.js"></script>
-        <script src="../dist/unittests.bundle.js"></script>
+        <!-- App bundles -->
+        <script src="../dist/generated/app_noinit.bundle.js"></script>
+        <script src="../dist/generated/unittest.bundle.js"></script>
 
+        <!-- Tests -->
+        <script src="init.js"></script>
         <script src="filters.js"></script>
         <script src="service/message.js"></script>
         <script src="service/mime.js"></script>
@@ -33,7 +43,7 @@
         <script src="service/browser.js"></script>
         <script src="service/keystore.js"></script>
         <script src="service/notification.js"></script>
-        <script src="helpers.js"></script>
+        <script src="service/receiver.js"></script>
     </head>
     <body>
     <script>

+ 25 - 0
tests/ts/bootstrap.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// tslint:disable:no-console
+
+// A dependency graph that contains any wasm must all be imported asynchronously.
+import('./main')
+    .then(() => console.info('Tests bootstrapped'))
+    .catch((e) => console.error('Could not bootstrap tests', e));

+ 82 - 4
tests/ts/emoji_helpers.ts

@@ -17,7 +17,30 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
-import {emojify, enlargeSingleEmoji, shortnameToUnicode} from '../../src/helpers/emoji';
+import twemoji from 'twemoji';
+import {emojify, emojifyNew, enlargeSingleEmoji, shortnameToUnicode} from '../../src/helpers/emoji';
+
+
+const textVariantSelector = '\ufe0e';
+const emojiVariantSelector = '\ufe0f';
+
+const beer = '\ud83c\udf7b';
+const bird = '\ud83d\udc26';
+
+function makeEmoji(emojiString: string, codepoint?: string, imgCodepoint?: string): threema.EmojiInfo {
+    if (codepoint === undefined) {
+        codepoint = twemoji.convert.toCodePoint(emojiString);
+    }
+    const imgPath = imgCodepoint === undefined
+        ? `emoji/png32/${codepoint}.png`
+        : `emoji/png32/${imgCodepoint}.png`;
+    return {
+        emojiString: emojiString,
+        imgPath: imgPath,
+        codepoint: codepoint,
+    }
+}
+
 
 describe('Emoji Helpers', () => {
     describe('emojify', () => {
@@ -34,11 +57,66 @@ describe('Emoji Helpers', () => {
         });
     });
 
+    describe('emojifyNew', () => {
+        it('returns text unmodified', function() {
+            expect(emojifyNew('hello world')).toEqual(['hello world']);
+        });
+
+        it('emojifies single emoji', function() {
+            expect(emojifyNew(bird))
+                .toEqual([makeEmoji(bird)]);
+        });
+
+        it('emojifies multiple emoji', function() {
+            expect(emojifyNew(`${beer}${bird}`))
+                .toEqual([makeEmoji(beer), makeEmoji(bird)]);
+        });
+
+        it('emojifies mixed content', function() {
+            expect(emojifyNew(`hi ${bird}`))
+                .toEqual(['hi ', makeEmoji(bird)]);
+            expect(emojifyNew(`${bird} bird`))
+                .toEqual([makeEmoji(bird), ' bird']);
+            expect(emojifyNew(`hi ${bird} bird`))
+                .toEqual(['hi ', makeEmoji(bird), ' bird']);
+            expect(emojifyNew(`hi ${bird}${beer}`))
+                .toEqual(['hi ', makeEmoji(bird), makeEmoji(beer)]);
+        });
+
+        it('ignores certain codepoints', function() {
+            expect(emojifyNew('©')).toEqual(['©']);
+            expect(emojifyNew('®')).toEqual(['®']);
+            expect(emojifyNew('™')).toEqual(['™']);
+        });
+
+        it('properly handles variant selectors (text-default)', function() {
+            // Copyright: Text-default
+            const copy = '©';
+            expect(emojifyNew(copy))
+                .toEqual([copy]);
+            expect(emojifyNew(copy + textVariantSelector))
+                .toEqual([copy + textVariantSelector]);
+            expect(emojifyNew(copy + emojiVariantSelector))
+                .toEqual([makeEmoji(copy + emojiVariantSelector, 'a9-fe0f', 'a9')]);
+        });
+
+        it('properly handles variant selectors (emoji-default)', function() {
+            // Exclamation mark: Emoji-default
+            const exclamation = '\u2757';
+            expect(emojifyNew(exclamation))
+                .toEqual([makeEmoji(exclamation, '2757', '2757')]);
+            expect(emojifyNew(exclamation + textVariantSelector))
+                .toEqual([exclamation + textVariantSelector]);
+            expect(emojifyNew(exclamation + emojiVariantSelector))
+                .toEqual([makeEmoji(exclamation + emojiVariantSelector, '2757', '2757')]);
+        });
+    });
+
     describe('shortnameToUnicode', () => {
         it('converts valid shortnames', function() {
-            expect(shortnameToUnicode('+1')).toEqual('\ud83d\udc4d');
-            expect(shortnameToUnicode('thumbup')).toEqual('\ud83d\udc4d');
-            expect(shortnameToUnicode('thumbsup')).toEqual('\ud83d\udc4d');
+            expect(shortnameToUnicode('+1')).toEqual('\ud83d\udc4d\ufe0f');
+            expect(shortnameToUnicode('thumbup')).toEqual('\ud83d\udc4d\ufe0f');
+            expect(shortnameToUnicode('thumbsup')).toEqual('\ud83d\udc4d\ufe0f');
         });
 
         it('returns null for unknown shortcodes', function() {

+ 25 - 0
tests/ui/bootstrap.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2019 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// tslint:disable:no-console
+
+// A dependency graph that contains any wasm must all be imported asynchronously.
+import('./main')
+    .then(() => console.info('Tests bootstrapped'))
+    .catch((e) => console.error('Could not bootstrap tests', e));

+ 13 - 3
tests/ui/compose_area.html

@@ -23,12 +23,22 @@
     <link rel="stylesheet" href="../../public/css/app.css?v=[[VERSION]]">
 
     <!-- Scripts -->
-    <script src="../../dist/uitest.bundle.js"></script>
-    <script>window.uiTests.initComposeArea();</script>
+    <script src="../../dist/generated/uitest.bundle.js"></script>
+    <script>
+        function init() {
+            if (window.uiTests === undefined) {
+                window.setTimeout(init, 100);
+            } else {
+                window.uiTests.initComposeArea();
+            }
+        }
+        init();
+    </script>
 </head>
-<body ng-app="uitest">
+<body>
     <div ng-controller="ComposeAreaController as ctrl">
         <compose-area
+            on-init="ctrl.onInit"
             submit="ctrl.submit"
             initial-data="ctrl.initialData"
             start-typing="ctrl.startTyping"

+ 7 - 0
tests/ui/compose_area.ts

@@ -58,6 +58,8 @@ export function init() {
         }]);
     }]);
 
+    // Bootstrap application
+    angular.bootstrap(document, ['uitest']);
 }
 
 class ComposeAreaController {
@@ -72,6 +74,11 @@ class ComposeAreaController {
         };
     }
 
+    public onInit(composeArea) {
+        // tslint:disable-next-line:no-string-literal
+        window['composeArea'] = composeArea;
+    }
+
     public startTyping() {
         // ignore
     }

+ 28 - 15
tests/ui/run.ts

@@ -14,8 +14,6 @@ import { expect } from 'chai';
 import { Builder, By, Key, until, WebDriver, WebElement } from 'selenium-webdriver';
 import * as TermColor from 'term-color';
 
-import { extractText as extractTextFunc } from '../../src/helpers';
-
 // Script arguments
 const browser = process.argv[2];
 const filterQuery = process.argv[3];
@@ -24,7 +22,7 @@ const filterQuery = process.argv[3];
 type Testfunc = (driver: WebDriver) => void;
 
 // Shared selectors
-const composeArea = By.css('div.compose');
+const composeArea = By.id('composeDiv');
 const emojiKeyboard = By.css('.emoji-keyboard');
 const emojiTrigger = By.css('.emoji-trigger');
 
@@ -32,12 +30,22 @@ const emojiTrigger = By.css('.emoji-trigger');
  * Helper function to extract text.
  */
 async function extractText(driver: WebDriver): Promise<string> {
+    const script = `return window.composeArea.get_text();`;
+    return driver.executeScript<string>(script);
+}
+
+/**
+ * Helper function to send a KeyDown event.
+ */
+async function sendKeyDown(driver: WebDriver, key: string): Promise<void> {
     const script = `
-        ${extractTextFunc.toString()}
+        const e = document.createEvent('HTMLEvents');
+        e.initEvent('keydown', false, true);
+        e.key = '${key}';
         const element = document.querySelector("div.compose");
-        return extractText(element);
+        element.dispatchEvent(e);
     `;
-    return driver.executeScript<string>(script);
+    return driver.executeScript<void>(script);
 }
 
 /**
@@ -57,24 +65,24 @@ async function sendKeyUp(driver: WebDriver, key: string): Promise<void> {
 /**
  * The emoji trigger should toggle the emoji keyboard.
  */
-async function showEmojiSelector(driver: WebDriver) {
+async function buttonTogglesEmojiSelector(driver: WebDriver) {
     // Initially not visible
     expect(
-        await driver.findElement(emojiKeyboard).isDisplayed()
+        await driver.findElement(emojiKeyboard).isDisplayed(),
     ).to.be.false;
 
     // Show
     await driver.findElement(emojiTrigger).click();
 
     expect(
-        await driver.findElement(emojiKeyboard).isDisplayed()
+        await driver.findElement(emojiKeyboard).isDisplayed(),
     ).to.be.true;
 
     // Hide
     await driver.findElement(emojiTrigger).click();
 
     expect(
-        await driver.findElement(emojiKeyboard).isDisplayed()
+        await driver.findElement(emojiKeyboard).isDisplayed(),
     ).to.be.false;
 }
 
@@ -93,7 +101,8 @@ async function insertEmoji(driver: WebDriver) {
 
     // Insert beer
     await driver.findElement(By.className('em-food')).click();
-    await driver.findElement(By.css('.em[data-s=":beers:"]')).click();
+    const elem = await driver.findElement(By.css('.em[data-s=":beers:"]'));
+    await elem.click();
 
     // Validate emoji
     const emoji = await driver.findElement(composeArea).findElements(By.xpath('*'));
@@ -193,16 +202,16 @@ async function regression672(driver: WebDriver) {
 async function insertEmojiWithShortcode(driver: WebDriver) {
     // Insert text
     await driver.findElement(composeArea).click();
-    await driver.findElement(composeArea).sendKeys('hello :+1:');
-    await sendKeyUp(driver, ':');
+    await driver.findElement(composeArea).sendKeys('hello :+1');
+    await sendKeyDown(driver, ':');
 
     const text = await extractText(driver);
-    expect(text).to.equal('hello 👍');
+    expect(text).to.equal('hello 👍');
 }
 
 // Register tests here
 const TESTS: Array<[string, Testfunc]> = [
-    ['Show and hide emoji selector', showEmojiSelector],
+    ['Show and hide emoji selector', buttonTogglesEmojiSelector],
     ['Insert emoji and text', insertEmoji],
     ['Insert three lines of text', insertNewline],
     ['Regression test #574', regression574],
@@ -215,6 +224,7 @@ const TESTS: Array<[string, Testfunc]> = [
 const TEST_URL = 'http://localhost:7777/tests/ui/compose_area.html';
 (async function() {
     const driver: WebDriver = await new Builder().forBrowser(browser).build();
+    driver.manage().setTimeouts({implicit: 1000, pageLoad: 30000, script: 30000});
     let i = 0;
     let success = 0;
     let failed = 0;
@@ -224,6 +234,9 @@ const TEST_URL = 'http://localhost:7777/tests/ui/compose_area.html';
         console.info(`Filter query: "${filterQuery}"\n`);
     }
     try {
+        // Initial pageload to ensure bundles are generated
+        await driver.get(TEST_URL);
+
         for (const [name, testfunc] of TESTS) {
             try {
                 if (filterQuery === undefined || name.toLowerCase().indexOf(filterQuery.toLowerCase()) !== -1) {

+ 5 - 1
tools/twemoji/generate-emoji-picker.py

@@ -42,12 +42,16 @@ for i, category in enumerate(category_order):
     print('        </label>')
     print('        <div class="content">')
     for emoji in groups[category['id']]:
+        if emoji['representation'] == 'emoji-default':
+            hex_codepoint = emoji['codepoint'].lower()
+        else:
+            hex_codepoint = emoji['codepoint_fully_qualified']
         print('            <span class="em em-{3}-{0}" data-c="{0}" data-s="{1}" title="{2}">{4}</span>'.format(
             emoji['codepoint'].lower(),
             emoji['shortname'],
             emoji['name'],
             category['id'],
-            make_hexchar(emoji['codepoint_fully_qualified']),
+            make_hexchar(hex_codepoint),
         ))
     print('        </div>')
     print('   </div>')

+ 7 - 5
tools/twemoji/generate-shortname-mapping.py

@@ -6,12 +6,14 @@ GROUPS_JSON = '../../../twemoji-picker/generated/groups.json'
 with open(GROUPS_JSON, 'r') as f:
     groups = json.loads(f.read())
 
-print('const shortnames = {')
+mappings = []
+
 for emoji_list in groups.values():
     for emoji in emoji_list:
         for shortname in emoji['shortnames']:
-            print("    '{}': '{}',".format(
-                shortname.strip(':'),
-                emoji['codepoint_fully_qualified']
-            ))
+            mappings.append((shortname.strip(':'), emoji['codepoint_fully_qualified']))
+
+print('const shortnames = {')
+for (k, v) in sorted(mappings):
+    print("    '{}': '{}',".format(k, v))
 print('}')

+ 1 - 1
tsconfig.json

@@ -1,7 +1,7 @@
 {
     "compilerOptions": {
         "target": "ES2015",
-        "module": "es2015",
+        "module": "esNext",
         "moduleResolution": "node",
         "removeComments": true
     },

+ 5 - 3
webpack.common.js

@@ -23,12 +23,13 @@ const babelOptions = {
     ['@babel/plugin-transform-runtime', {
       regenerator: true,
     }],
+    ['@babel/plugin-syntax-dynamic-import'],
   ],
 };
 
 module.exports = {
   entry: {
-    app: './src/app.ts',
+    app: './src/bootstrap.ts',
   },
   module: {
     rules: [
@@ -50,10 +51,11 @@ module.exports = {
     ],
   },
   resolve: {
-    extensions: ['.js', '.ts'],
+    extensions: ['.js', '.ts', '.wasm'],
   },
   output: {
-    path: path.resolve(__dirname, 'dist'),
+    path: path.resolve(__dirname, 'dist', 'generated'),
     filename: '[name].bundle.js',
+    chunkFilename: '[name].[chunkhash].bundle.js',
   },
 };

+ 1 - 1
webpack.dev.js

@@ -11,8 +11,8 @@ module.exports = merge(common, {
       path.join(__dirname, 'public'),
       path.join(__dirname, 'src'),
     ],
-    publicPath: '/dist/',
     compress: true,
+    host: '127.0.0.1',
     port: 9966,
   },
 });

+ 1 - 10
webpack.prod.js

@@ -24,16 +24,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
 module.exports = merge(common, {
   mode: 'production',
   devtool: 'source-map',
-  performance: {
-    hints: 'warning'
-  },
-  output: {
-    pathinfo: false
-  },
   plugins: [
-    new webpack.DefinePlugin({"process.env.NODE_ENV": JSON.stringify("production")}),
-    new webpack.optimize.ModuleConcatenationPlugin(),
-    new webpack.NoEmitOnErrorsPlugin(),
-    new webpack.BannerPlugin({banner: banner}),
+    new webpack.BannerPlugin({banner: banner, entryOnly: true}),
   ],
 });

+ 12 - 7
webpack.tests.js

@@ -2,11 +2,16 @@ const dev = require('./webpack.dev.js');
 const merge = require('webpack-merge');
 
 module.exports = merge(dev, {
-    entry: {
-        unittest: './tests/ts/main.ts',
-        uitest: './tests/ui/main.ts',
-    },
-    devServer: {
-        port: 7777,
-    },
+  entry: {
+    unittest: './tests/ts/bootstrap.ts',
+    uitest: './tests/ui/bootstrap.ts',
+    app_noinit: './tests/bootstrap.ts',
+    unittest_karma: './tests/ts/main.ts',
+  },
+  devServer: {
+    port: 7777,
+  },
+  output: {
+    publicPath: '/dist/generated/',
+  },
 });

Некоторые файлы не были показаны из-за большого количества измененных файлов