diff --git a/.dockerignore b/.dockerignore index 781e9357..a060c90f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,7 +14,7 @@ tests /static/icons.svg /static/robots.txt /static/inline-script.js.map -/static/emoji-mart-all.json +/static/emoji-all-en.json /src/inline-script/checksum.js yarn-error.log /now.json diff --git a/.gitignore b/.gitignore index fc95c0fb..0d7f1dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,9 @@ /static/icons.svg /static/robots.txt /static/inline-script.js.map -/static/emoji-mart-all.json +/static/emoji-all-en.json /src/inline-script/checksum.js yarn-error.log /now.json -.now \ No newline at end of file +.now diff --git a/.nowignore b/.nowignore index 4993a364..93de9e88 100644 --- a/.nowignore +++ b/.nowignore @@ -7,6 +7,6 @@ /static/*.css /static/icons.svg /static/inline-script.js.map -/static/emoji-mart-all.json +/static/emoji-all-en.json /src/inline-script/checksum.js yarn-error.log diff --git a/.travis.yml b/.travis.yml index 9d1cd3a4..1af16e6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: node_js node_js: - "10" -dist: xenial +dist: bionic +group: dev sudo: false services: - redis-server @@ -20,8 +21,7 @@ addons: - gcc - imagemagick - libffi-dev - - libgdbm-dev - - libgdbm-dev + - libgdbm5 - libicu-dev - libidn11-dev - libncurses5-dev @@ -33,13 +33,13 @@ addons: - libxslt1-dev - libyaml-dev - pkg-config - - postgresql-10 - postgresql-client-10 - postgresql-contrib-10 - protobuf-compiler - redis-server - redis-tools - zlib1g-dev + - fonts-noto-color-emoji # required for emoji-picker-element + Chrome on Linux before_install: - psql -d template1 -U postgres -c "CREATE USER pinafore WITH PASSWORD 'pinafore' CREATEDB;" - curl -o- -L https://yarnpkg.com/install.sh | bash -s diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1ce2577..0473597a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,10 +176,10 @@ preprocessor. Highly modular, highly functional, lots of single-function files. Tends to help with tree-shaking and code-splitting, as well as avoiding circular dependencies. -### Preact is loaded dynamically +### emoji-picker-element is loaded as a third-party bundle -This is a Svelte project, but `emoji-mart` is used for the emoji picker, and it's written in React. So we -lazy-load the React-compatible Preact library when we load `emoji-mart`. +`emoji-picker-element` uses Svelte 3, whereas we use Svelte 2. So it's just imported +as a bundled custom element, not as a Svelte component. ### Some third-party code is bundled diff --git a/bin/build-assets.js b/bin/build-assets.js index c3cf8c87..0299de6e 100644 --- a/bin/build-assets.js +++ b/bin/build-assets.js @@ -1,30 +1,22 @@ import path from 'path' import fs from 'fs' import { promisify } from 'util' -import CleanCSS from 'clean-css' +import trimEmojiData from 'emoji-picker-element/trimEmojiData.cjs' -const writeFile = promisify(fs.writeFile) const readFile = promisify(fs.readFile) -const copyFile = promisify(fs.copyFile) - -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') -} - -async function compileThirdPartyJson () { - await copyFile( - path.resolve(__dirname, '../node_modules/emoji-mart/data/all.json'), - path.resolve(__dirname, '../static/emoji-mart-all.json') - ) -} +const writeFile = promisify(fs.writeFile) async function main () { - await Promise.all([ - compileThirdPartyCss(), - compileThirdPartyJson() - ]) + let json = JSON.parse(await readFile( + path.resolve(__dirname, '../node_modules/emojibase-data/en/data.json'), + 'utf8') + ) + json = trimEmojiData(json) + await writeFile( + path.resolve(__dirname, '../static/emoji-all-en.json'), + JSON.stringify(json), + 'utf8' + ) } main().catch(err => { diff --git a/package.json b/package.json index fde32fd9..f1f88462 100644 --- a/package.json +++ b/package.json @@ -49,21 +49,22 @@ "@babel/runtime": "^7.8.4", "@rollup/plugin-replace": "^2.3.0", "@webcomponents/custom-elements": "^1.4.0", - "arrow-key-navigation": "^1.1.0", + "@webcomponents/shadydom": "^1.7.3", + "array-flat-polyfill": "^1.0.1", + "arrow-key-navigation": "^1.2.0", "babel-loader": "^8.0.6", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "blurhash": "^1.1.3", "cheerio": "^1.0.0-rc.3", "child-process-promise": "^2.2.1", "chokidar": "^3.3.1", "circular-dependency-plugin": "^5.2.0", - "clean-css": "^4.2.3", "compression": "^1.7.4", "cross-env": "^7.0.0", "css-dedoupe": "^0.1.1", "css-loader": "^3.4.2", - "emoji-mart": "nolanlawson/emoji-mart#8bb6fb6", + "emoji-picker-element": "^1.0.0", "emoji-regex": "^9.0.0", + "emojibase-data": "^5.0.1", "encoding": "^0.1.12", "escape-html": "^1.0.3", "esm": "^3.2.25", @@ -88,7 +89,6 @@ "page-lifecycle": "^0.1.2", "performance-now": "^2.1.0", "pinch-zoom-element": "^1.1.1", - "preact": "^10.3.3", "promise-worker": "^2.0.1", "prop-types": "^15.7.2", "requestidlecallback": "^0.3.0", @@ -134,6 +134,7 @@ "Element", "Event", "FormData", + "HTMLElement", "IDBKeyRange", "IDBObjectStore", "Image", diff --git a/src/build/template.html b/src/build/template.html index 7a64aea9..b390f763 100644 --- a/src/build/template.html +++ b/src/build/template.html @@ -26,7 +26,7 @@ */ img, svg, video, input[type="checkbox"], input[type="radio"], - .inline-emoji, .theme-preview, .emoji-mart-emoji, .emoji-mart-skin { + .inline-emoji, .theme-preview { filter: grayscale(100%); } diff --git a/src/routes/_actions/autosuggest.js b/src/routes/_actions/autosuggest.js index 0a612d57..d4181e37 100644 --- a/src/routes/_actions/autosuggest.js +++ b/src/routes/_actions/autosuggest.js @@ -1,6 +1,6 @@ import { store } from '../_store/store' -const emojiMapper = emoji => `:${emoji.shortcode}:` +const emojiMapper = emoji => emoji.unicode ? emoji.unicode : `:${emoji.shortcodes[0]}:` const hashtagMapper = hashtag => `#${hashtag.name}` const accountMapper = account => `@${account.acct}` @@ -61,7 +61,7 @@ export function selectAutosuggestItem (item) { const endIndex = composeSelectionStart if (item.acct) { /* no await */ insertUsername(currentComposeRealm, item, startIndex, endIndex) - } else if (item.shortcode) { + } else if (item.shortcodes) { /* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex) } else { // hashtag /* no await */ insertHashtag(currentComposeRealm, item, startIndex, endIndex) diff --git a/src/routes/_actions/autosuggestEmojiSearch.js b/src/routes/_actions/autosuggestEmojiSearch.js index 3b1b16e5..81928932 100644 --- a/src/routes/_actions/autosuggestEmojiSearch.js +++ b/src/routes/_actions/autosuggestEmojiSearch.js @@ -1,24 +1,45 @@ import { store } from '../_store/store' -import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' import { scheduleIdleTask } from '../_utils/scheduleIdleTask' +import * as emojiDatabase from '../_utils/emojiDatabase' +import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' +import { testEmojiSupported } from '../_utils/testEmojiSupported' +import { mark, stop } from '../_utils/marks' -function searchEmoji (searchText) { - searchText = searchText.toLowerCase().substring(1) - const { currentCustomEmoji } = store.get() - const results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText)) - .sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1) - .slice(0, SEARCH_RESULTS_LIMIT) +async function searchEmoji (searchText) { + let emojis = await emojiDatabase.findBySearchQuery(searchText) + + const results = [] + + if (searchText.startsWith(':') && searchText.endsWith(':')) { + // exact shortcode search + const shortcode = searchText.substring(1, searchText.length - 1).toLowerCase() + emojis = emojis.filter(_ => _.shortcodes.includes(shortcode)) + } + + mark('testEmojiSupported') + for (const emoji of emojis) { + if (results.length === SEARCH_RESULTS_LIMIT) { + break + } + if (emoji.url || testEmojiSupported(emoji.unicode)) { // emoji.url is a custom emoji + results.push(emoji) + } + } + stop('testEmojiSupported') return results } export function doEmojiSearch (searchText) { let canceled = false - scheduleIdleTask(() => { + scheduleIdleTask(async () => { + if (canceled) { + return + } + const results = await searchEmoji(searchText) if (canceled) { return } - const results = searchEmoji(searchText) store.setForCurrentAutosuggest({ autosuggestType: 'emoji', autosuggestSelected: 0, diff --git a/src/routes/_actions/emoji.js b/src/routes/_actions/emoji.js index 25031f42..373507e1 100644 --- a/src/routes/_actions/emoji.js +++ b/src/routes/_actions/emoji.js @@ -31,7 +31,7 @@ export async function setupCustomEmojiForInstance (instanceName) { } export function insertEmoji (realm, emoji) { - const emojiText = emoji.custom ? emoji.colons : emoji.native + const emojiText = emoji.unicode || `:${emoji.name}:` const { composeSelectionStart } = store.get() const idx = composeSelectionStart || 0 const oldText = store.getComposeData(realm, 'text') || '' diff --git a/src/routes/_components/compose/ComposeAutosuggestionList.html b/src/routes/_components/compose/ComposeAutosuggestionList.html index f02a4bb6..352501cb 100644 --- a/src/routes/_components/compose/ComposeAutosuggestionList.html +++ b/src/routes/_components/compose/ComposeAutosuggestionList.html @@ -3,7 +3,7 @@ class="compose-autosuggest-list" role="listbox" > - {#each items as item, i (item.shortcode || item.id || item.name)} + {#each items as item, i (item.shortcodes ? `emoji-${item.unicode || item.name}` : item.id ? `account-${item.id}` : `hashtag-${item.name}`)}