From 098a20db49a5c1c0f9486234600c999167e7001f Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 18 Dec 2018 00:43:51 -0800 Subject: [PATCH] feat: add full emoji picker using emoji-mart (#836) * feat: add full emoji picker using emoji-mart Fixes #4 * use a sailboat as the default emoji in the emoji picker * fix tests * fix lint --- bin/build-sass.js | 10 +- package-lock.json | 202 +++++++++++++++--- package.json | 7 +- src/routes/_actions/emoji.js | 3 +- .../dialog/components/EmojiDialog.html | 129 ++++++----- .../dialog/creators/showEmojiDialog.js | 2 +- src/routes/_react/emoji-mart.js | 20 ++ .../_store/observers/resizeObservers.js | 5 +- src/routes/_utils/asyncModules.js | 4 + src/routes/_utils/loadCSS.js | 12 ++ tests/spec/012-compose.js | 34 ++- tests/spec/108-compose-dialog.js | 5 +- tests/utils.js | 1 + webpack/client.config.js | 7 +- webpack/server.config.js | 7 +- webpack/service-worker.config.js | 3 +- webpack/shared.config.js | 13 +- 17 files changed, 355 insertions(+), 109 deletions(-) create mode 100644 src/routes/_react/emoji-mart.js create mode 100644 src/routes/_utils/loadCSS.js diff --git a/bin/build-sass.js b/bin/build-sass.js index 77cbef23..9ba424c2 100755 --- a/bin/build-sass.js +++ b/bin/build-sass.js @@ -4,9 +4,11 @@ import sass from 'node-sass' import path from 'path' import fs from 'fs' import pify from 'pify' +import CleanCSS from 'clean-css' const writeFile = pify(fs.writeFile.bind(fs)) const readdir = pify(fs.readdir.bind(fs)) +const readFile = pify(fs.readFile.bind(fs)) const render = pify(sass.render.bind(sass)) const globalScss = path.join(__dirname, '../scss/global.scss') @@ -39,7 +41,13 @@ async function compileThemesSass () { })) } +async function compileThirdPartyCss () { + let css = await readFile(path.resolve(__dirname, '../node_modules/emoji-mart/css/emoji-mart.css'), 'utf8') + css = `/* compiled from emoji-mart.css */` + new CleanCSS().minify(css).styles + await writeFile(path.resolve(__dirname, '../static/emoji-mart.css'), css, 'utf8') +} + export async function buildSass () { - let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass()]) + let [ result ] = await Promise.all([compileGlobalSass(), compileThemesSass(), compileThirdPartyCss()]) return result } diff --git a/package-lock.json b/package-lock.json index 1f429050..275d00da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1272,6 +1272,23 @@ "babel-types": "^6.24.1" } }, + "babel-polyfill": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", + "integrity": "sha1-g2TKYt+Or7gwSZ9pkXdGbDsDSZ0=", + "requires": { + "babel-runtime": "^6.22.0", + "core-js": "^2.4.0", + "regenerator-runtime": "^0.10.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + } + } + }, "babel-preset-env": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", @@ -1386,7 +1403,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -1953,8 +1969,7 @@ "chardet": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" }, "check-error": { "version": "1.0.2", @@ -2061,14 +2076,12 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2088,8 +2101,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", @@ -2237,7 +2249,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2680,7 +2691,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, "requires": { "restore-cursor": "^2.0.0" } @@ -2688,8 +2698,7 @@ "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, "cliui": { "version": "3.2.0", @@ -2920,8 +2929,7 @@ "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", - "dev": true + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" }, "core-util-is": { "version": "1.0.2", @@ -3513,6 +3521,11 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "emoji-mart": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-2.9.2.tgz", + "integrity": "sha512-5S743OpjFb9nBbbx5F4APWgcp2IOjdT7gLLzu2OBh0k44C3ZoCm+wuIN1llOtj5eosUa3lYqrZWtU/ZiaCULrg==" + }, "emoji-regex": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.1.tgz", @@ -4354,7 +4367,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, "requires": { "chardet": "^0.4.0", "iconv-lite": "^0.4.17", @@ -4365,7 +4377,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } @@ -4476,7 +4487,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, "requires": { "escape-string-regexp": "^1.0.5" } @@ -5380,6 +5390,70 @@ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, + "inferno": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno/-/inferno-7.0.4.tgz", + "integrity": "sha512-R5lk2e7F1Jy1l9+rwz/onTyx7kqb1aikbfnMwJ1GKoKX+DJu5skha1AQvI4ran6HueXFtfoNWpylgSVx0qd9pA==", + "requires": { + "inferno-shared": "7.0.4", + "inferno-vnode-flags": "7.0.4", + "opencollective": "^1.0.3" + } + }, + "inferno-clone-vnode": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-clone-vnode/-/inferno-clone-vnode-7.0.4.tgz", + "integrity": "sha512-Pa2TUZwHT0i28mi+blp9d/z/PAXBv9EWKjbKAcmb8wpVl/fDlk03NP0VIVRgXZvLUo973Rq90g1IQtthJCu0MA==", + "requires": { + "inferno": "7.0.4" + } + }, + "inferno-compat": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-compat/-/inferno-compat-7.0.4.tgz", + "integrity": "sha512-SCE23uS2oryFC4cck1gR9iBN7a8quo9DwtCJ1U8fka61OC92qPOrVjteOBtYJERZr+cJlBw9vK+7uwQ6t1G8Zw==", + "requires": { + "inferno": "7.0.4", + "inferno-clone-vnode": "7.0.4", + "inferno-create-class": "7.0.4", + "inferno-create-element": "7.0.4", + "inferno-extras": "7.0.4" + } + }, + "inferno-create-class": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-create-class/-/inferno-create-class-7.0.4.tgz", + "integrity": "sha512-l4zilxP724wwy3/2M+LlcOw2FMT7UBdAF3NXwuS39+O9xguDY6pw8EuBmBOCLc/C0felG9HOTltRQwvCFF41wA==", + "requires": { + "inferno": "7.0.4" + } + }, + "inferno-create-element": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-create-element/-/inferno-create-element-7.0.4.tgz", + "integrity": "sha512-pT1oZOiMXVV8yZB7TQUojRulr6DW24cb9YfDZBzL1GtIMtfQkzPAYh6/IgCh6V/7acS9OHn6XOF3UK0uXyfoCA==", + "requires": { + "inferno": "7.0.4" + } + }, + "inferno-extras": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-extras/-/inferno-extras-7.0.4.tgz", + "integrity": "sha512-R+AtXDzXTb6hGncajE17c/FBX6lT+1RJu7pEfQw9borZH4NTATHAsvgZwJJsDcfMXe6WbDoq/ioLBFZgM363Og==", + "requires": { + "inferno": "7.0.4" + } + }, + "inferno-shared": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-shared/-/inferno-shared-7.0.4.tgz", + "integrity": "sha512-SlYO5BWsnC2S3osG5goOxxI9Hm4E7OUo3Nq976lNMaU3fWdRNeeJipTvp2s61hQznAq6r+/GY0qwrgQcC97S2Q==" + }, + "inferno-vnode-flags": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inferno-vnode-flags/-/inferno-vnode-flags-7.0.4.tgz", + "integrity": "sha512-zGGBX6a05xqle3w2dcpkyEFGu/axQfsqTsp8icX1myxhW0DFs2LikVnBtskuIegThI9mDLwvBtsf4WubDo1i1A==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5690,8 +5764,7 @@ "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, "is-regex": { "version": "1.0.4", @@ -5710,8 +5783,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-symbol": { "version": "1.0.1", @@ -6212,8 +6284,7 @@ "mimic-fn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", - "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", - "dev": true + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=" }, "minimalistic-assert": { "version": "1.0.1", @@ -6357,8 +6428,7 @@ "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, "nan": { "version": "2.11.1", @@ -6748,16 +6818,78 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, "requires": { "mimic-fn": "^1.0.0" } }, + "opencollective": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz", + "integrity": "sha1-ruY3K8KBRFg2kMPKja7PwSDdDvE=", + "requires": { + "babel-polyfill": "6.23.0", + "chalk": "1.1.3", + "inquirer": "3.0.6", + "minimist": "1.2.0", + "node-fetch": "1.6.3", + "opn": "4.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "inquirer": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.0.6.tgz", + "integrity": "sha1-4EqqnQW3o8ubD0B9BDdfBEcZA0c=", + "requires": { + "ansi-escapes": "^1.1.0", + "chalk": "^1.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.1", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx": "^4.1.0", + "string-width": "^2.0.0", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "node-fetch": { + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", + "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + } + } + }, "opener": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==" }, + "opn": { + "version": "4.0.2", + "resolved": "http://registry.npmjs.org/opn/-/opn-4.0.2.tgz", + "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -7615,8 +7747,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.10.1", @@ -7685,6 +7816,11 @@ "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.7.tgz", "integrity": "sha1-1mdEE6ZWdgB74A3UAJgJh7LDAME=" }, + "remount": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/remount/-/remount-0.9.3.tgz", + "integrity": "sha512-U/a16ZqFadWSbjfr6k8ahEMEjFmJ3EceDFjgsbagE4tzwvcz0NQtgIKeVC4R/hF8mKhRdTyqpeFJIxQzhiP/Gw==" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -7837,7 +7973,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, "requires": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" @@ -7982,7 +8117,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, "requires": { "is-promise": "^2.1.0" } @@ -8001,6 +8135,11 @@ "aproba": "^1.1.1" } }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" + }, "rxjs": { "version": "5.5.12", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", @@ -9522,8 +9661,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.3", diff --git a/package.json b/package.json index 8592b1e2..a20f53a9 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,11 @@ "child-process-promise": "^2.2.1", "chokidar": "^2.0.4", "circular-dependency-plugin": "^5.0.2", + "clean-css": "^4.2.1", "compression": "^1.7.3", "cross-env": "^5.2.0", "css-loader": "^2.0.1", + "emoji-mart": "^2.9.2", "emoji-regex": "^7.0.1", "encoding": "^0.1.12", "escape-html": "^1.0.3", @@ -62,6 +64,7 @@ "helmet": "^3.15.0", "idb-keyval": "^3.1.0", "indexeddb-getall-shim": "^1.3.5", + "inferno-compat": "^7.0.4", "intersection-observer": "^0.5.1", "localstorage-memory": "^1.0.3", "lodash-es": "^4.17.11", @@ -75,6 +78,7 @@ "performance-now": "^2.1.0", "pify": "^4.0.1", "quick-lru": "^2.0.0", + "remount": "^0.9.3", "requestidlecallback": "^0.3.0", "rollup": "^0.68.0", "rollup-plugin-replace": "^2.1.0", @@ -137,7 +141,8 @@ "NotificationEvent", "NodeList", "DOMParser", - "CSS" + "CSS", + "customElements" ], "ignore": [ "dist", diff --git a/src/routes/_actions/emoji.js b/src/routes/_actions/emoji.js index e76aa679..21576864 100644 --- a/src/routes/_actions/emoji.js +++ b/src/routes/_actions/emoji.js @@ -17,11 +17,12 @@ export async function updateCustomEmojiForInstance (instanceName) { } export function insertEmoji (realm, emoji) { + let emojiText = emoji.custom ? emoji.colons : emoji.native let { composeSelectionStart } = store.get() let idx = composeSelectionStart || 0 let oldText = store.getComposeData(realm, 'text') || '' let pre = oldText.substring(0, idx) let post = oldText.substring(idx) - let newText = `${pre}:${emoji.shortcode}: ${post}` + let newText = `${pre}${emojiText} ${post}` store.setComposeData(realm, { text: newText }) } diff --git a/src/routes/_components/dialog/components/EmojiDialog.html b/src/routes/_components/dialog/components/EmojiDialog.html index 9a62e389..4106d13a 100644 --- a/src/routes/_components/dialog/components/EmojiDialog.html +++ b/src/routes/_components/dialog/components/EmojiDialog.html @@ -4,57 +4,39 @@ {title} background="var(--main-bg)" > -
- {#if emojis.length} - +
+ {#if loaded} + + {:elseif error} +
Failed to load emoji picker: {error}
{:else} -
No custom emoji found for this instance.
+
+ +
{/if}
\ No newline at end of file + diff --git a/src/routes/_components/dialog/creators/showEmojiDialog.js b/src/routes/_components/dialog/creators/showEmojiDialog.js index 107f1f99..36520d2e 100644 --- a/src/routes/_components/dialog/creators/showEmojiDialog.js +++ b/src/routes/_components/dialog/creators/showEmojiDialog.js @@ -8,7 +8,7 @@ export default function showEmojiDialog (realm) { data: { id: createDialogId(), label: 'Emoji dialog', - title: 'Custom emoji', + title: 'Emoji', realm } }) diff --git a/src/routes/_react/emoji-mart.js b/src/routes/_react/emoji-mart.js new file mode 100644 index 00000000..51480341 --- /dev/null +++ b/src/routes/_react/emoji-mart.js @@ -0,0 +1,20 @@ +// I wrap the emoji-mart React code itself here, so that we don't need to pass in a huge "data" +// object via a JSON-stringified custom element attribute. Also, AFAICT there is no way when +// using `remount` to pass in functions as attributes, since everything is stringified. So +// I just fire a global event here when an emoji is clicked. + +import data from 'emoji-mart/data/messenger.json' +import NimblePicker from 'emoji-mart/dist-es/components/picker/nimble-picker' +import React from 'react' +import { emit } from '../_utils/eventBus' + +function onEmojiSelected (emoji) { + emit('emoji-selected', emoji) +} + +export default props => React.createElement(NimblePicker, Object.assign({ + set: 'messenger', + data, + native: true, + onSelect: onEmojiSelected +}, props)) diff --git a/src/routes/_store/observers/resizeObservers.js b/src/routes/_store/observers/resizeObservers.js index f94ec93e..ec0bf03a 100644 --- a/src/routes/_store/observers/resizeObservers.js +++ b/src/routes/_store/observers/resizeObservers.js @@ -6,7 +6,10 @@ export function resizeObservers (store) { } const recalculateIsMobileSize = () => { - store.set({ isMobileSize: window.matchMedia('(max-width: 767px)').matches }) + store.set({ + isMobileSize: window.matchMedia('(max-width: 767px)').matches, + isSmallMobileSize: window.matchMedia('(max-width: 479px)').matches + }) } registerResizeListener(recalculateIsMobileSize) diff --git a/src/routes/_utils/asyncModules.js b/src/routes/_utils/asyncModules.js index a826b9a0..1439cdfc 100644 --- a/src/routes/_utils/asyncModules.js +++ b/src/routes/_utils/asyncModules.js @@ -35,3 +35,7 @@ export const importDatabase = () => import( export const importLoggedInObservers = () => import( /* webpackChunkName: 'loggedInObservers.js' */ '../_store/observers/loggedInObservers.js' ).then(getDefault) + +export const importEmojiMart = () => import( + /* webpackChunkName: 'emoji-mart.js' */ '../_react/emoji-mart.js' + ).then(getDefault) diff --git a/src/routes/_utils/loadCSS.js b/src/routes/_utils/loadCSS.js new file mode 100644 index 00000000..367bae7e --- /dev/null +++ b/src/routes/_utils/loadCSS.js @@ -0,0 +1,12 @@ +export function loadCSS (href) { + let existingLink = document.querySelector(`link[href="${href}"]`) + + if (existingLink) { + return + } + let link = document.createElement('link') + link.rel = 'stylesheet' + link.href = href + + document.head.appendChild(link) +} diff --git a/tests/spec/012-compose.js b/tests/spec/012-compose.js index 84eb55aa..140b3aee 100644 --- a/tests/spec/012-compose.js +++ b/tests/spec/012-compose.js @@ -1,6 +1,5 @@ -import { Selector as $ } from 'testcafe' import { - composeButton, composeInput, composeLengthIndicator, emojiButton, getComposeSelectionStart, + composeButton, composeInput, composeLengthIndicator, emojiButton, emojiSearchInput, getComposeSelectionStart, getNthStatusContent, getUrl, homeNavButton, notificationsNavButton, sleep, @@ -63,7 +62,8 @@ test('shows compose limits for custom emoji', async t => { await t .typeText(composeInput, 'hello world ') .click(emojiButton) - .click($('button img[title=":blobnom:"]')) + .typeText(emojiSearchInput, 'blobnom') + .pressKey('enter') .expect(composeInput.value).eql('hello world :blobnom: ') .expect(composeLengthIndicator.innerText).eql('478') }) @@ -75,16 +75,19 @@ test('inserts custom emoji correctly', async t => { .selectText(composeInput, 6, 6) .expect(getComposeSelectionStart()).eql(6) .click(emojiButton) - .click($('button img[title=":blobpats:"]')) + .typeText(emojiSearchInput, 'blobpats') + .pressKey('enter') .expect(composeInput.value).eql('hello :blobpats: world') .selectText(composeInput, 0, 0) .expect(getComposeSelectionStart()).eql(0) .click(emojiButton) - .click($('button img[title=":blobnom:"]')) + .typeText(emojiSearchInput, 'blobnom') + .pressKey('enter') .expect(composeInput.value).eql(':blobnom: hello :blobpats: world') .typeText(composeInput, ' foobar ') .click(emojiButton) - .click($('button img[title=":blobpeek:"]')) + .typeText(emojiSearchInput, 'blobpeek') + .pressKey('enter') .expect(composeInput.value).eql(':blobnom: hello :blobpats: world foobar :blobpeek: ') }) @@ -92,13 +95,28 @@ test('inserts emoji without typing anything', async t => { await loginAsFoobar(t) await t .click(emojiButton) - .click($('button img[title=":blobpats:"]')) + .typeText(emojiSearchInput, 'blobpats') + .pressKey('enter') .expect(composeInput.value).eql(':blobpats: ') .click(emojiButton) - .click($('button img[title=":blobpeek:"]')) + .typeText(emojiSearchInput, 'blobpeek') + .pressKey('enter') .expect(composeInput.value).eql(':blobpeek: :blobpats: ') }) +test('inserts native emoji without typing anything', async t => { + await loginAsFoobar(t) + await t + .click(emojiButton) + .typeText(emojiSearchInput, 'pineapple') + .pressKey('enter') + .expect(composeInput.value).eql('\ud83c\udf4d ') + .click(emojiButton) + .typeText(emojiSearchInput, 'elephant') + .pressKey('enter') + .expect(composeInput.value).eql('\ud83d\udc18 \ud83c\udf4d ') +}) + test('cannot post an empty status', async t => { await loginAsFoobar(t) await t diff --git a/tests/spec/108-compose-dialog.js b/tests/spec/108-compose-dialog.js index df192267..e578d09f 100644 --- a/tests/spec/108-compose-dialog.js +++ b/tests/spec/108-compose-dialog.js @@ -9,7 +9,7 @@ import { getNthStatusSelector, composeModalEmojiButton, composeModalInput, - composeModalComposeButton + composeModalComposeButton, emojiSearchInput } from '../utils' import { loginAsFoobar } from '../roles' import { Selector as $ } from 'testcafe' @@ -42,7 +42,8 @@ test('can use emoji dialog within compose dialog', async t => { await sleep(2000) await t.click(composeButton) .click(composeModalEmojiButton) - .click($('button img[title=":blobpats:"]')) + .typeText(emojiSearchInput, 'blobpats') + .pressKey('enter') .expect(composeModalInput.value).eql(':blobpats: ') .click(composeModalComposeButton) .expect(modalDialog.exists).notOk() diff --git a/tests/utils.js b/tests/utils.js index a8f2719a..d4bdfe1d 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -46,6 +46,7 @@ export const markMediaSensitiveInput = $('#choice-mark-media-sensitive') export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive') export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names') export const dialogOptionsOption = $(`.modal-dialog button`) +export const emojiSearchInput = $('.emoji-mart-search input') export const composeModalInput = $('.modal-dialog .compose-box-input') export const composeModalComposeButton = $('.modal-dialog .compose-box-button') diff --git a/webpack/client.config.js b/webpack/client.config.js index 723b3ec7..52af84ac 100644 --- a/webpack/client.config.js +++ b/webpack/client.config.js @@ -4,15 +4,12 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const LodashModuleReplacementPlugin = require('lodash-webpack-plugin') const terser = require('./terser.config') const CircularDependencyPlugin = require('circular-dependency-plugin') -const { mode, dev } = require('./shared.config') +const { mode, dev, resolve } = require('./shared.config') module.exports = { entry: config.client.entry(), output: Object.assign(config.client.output(), { globalObject: 'this' }), // enables HMR in workers - resolve: { - extensions: ['.js', '.json', '.html'], - mainFields: ['svelte', 'module', 'browser', 'main'] - }, + resolve, mode, module: { rules: [ diff --git a/webpack/server.config.js b/webpack/server.config.js index f40c0941..5707ae0c 100644 --- a/webpack/server.config.js +++ b/webpack/server.config.js @@ -1,15 +1,12 @@ const config = require('sapper/config/webpack.js') const pkg = require('../package.json') -const { mode, dev } = require('./shared.config') +const { mode, dev, resolve } = require('./shared.config') module.exports = { entry: config.server.entry(), output: config.server.output(), target: 'node', - resolve: { - extensions: ['.js', '.json', '.html'], - mainFields: ['svelte', 'module', 'browser', 'main'] - }, + resolve, externals: Object.keys(pkg.dependencies), module: { rules: [ diff --git a/webpack/service-worker.config.js b/webpack/service-worker.config.js index 6d56f571..4d03b104 100644 --- a/webpack/service-worker.config.js +++ b/webpack/service-worker.config.js @@ -1,11 +1,12 @@ const config = require('sapper/config/webpack.js') const terser = require('./terser.config') const webpack = require('webpack') -const { mode, dev } = require('./shared.config') +const { mode, dev, resolve } = require('./shared.config') module.exports = { entry: config.serviceworker.entry(), output: config.serviceworker.output(), + resolve, mode, devtool: dev ? 'inline-source-map' : 'source-map', plugins: [ diff --git a/webpack/shared.config.js b/webpack/shared.config.js index 27dbe592..b64e6d60 100644 --- a/webpack/shared.config.js +++ b/webpack/shared.config.js @@ -1,7 +1,18 @@ const mode = process.env.NODE_ENV const dev = mode === 'development' +const resolve = { + extensions: ['.js', '.json', '.html'], + mainFields: ['svelte', 'module', 'browser', 'main'], + alias: { + 'react': 'inferno-compat', + 'react-dom': 'inferno-compat', + 'inferno': dev ? 'inferno/dist/index.dev.esm.js' : 'inferno' + } +} + module.exports = { mode, - dev + dev, + resolve }