diff --git a/cli.js b/cli.js
index 4262d2e..f4ccbc9 100755
--- a/cli.js
+++ b/cli.js
@@ -27,12 +27,15 @@ const cli = meow(`
Use the up/down keys during live search to change the skin tone
Use the left/right or 1..9 keys during live search to select the emoji
`, {
- boolean: [
- 'copy'
- ],
- alias: {
- c: 'copy',
- s: 'skinTone'
+ flags: {
+ copy: {
+ type: 'boolean',
+ alias: 'c'
+ },
+ skinTone: {
+ type: 'number',
+ alias: 's'
+ }
}
});
@@ -44,7 +47,7 @@ const config = new Conf({
});
if (cli.flags.skinTone !== undefined) {
- config.set('skinNumber', Math.max(0, Math.min(5, Number(cli.flags.skinTone) || 0)));
+ config.set('skinNumber', Math.max(0, Math.min(5, cli.flags.skinTone || 0)));
}
const skinNumber = config.get('skinNumber');
diff --git a/index.js b/index.js
index d6800d9..2cee2f7 100644
--- a/index.js
+++ b/index.js
@@ -3,6 +3,7 @@ const got = require('got');
const emojilib = require('emojilib');
const getGetdangoEmojis = async input => {
+ // Intentionally using `http` as the `https` endpoint has some stability problems.
const {results} = await got('http://emoji.getdango.com/api/emoji', {
searchParams: {
q: input
@@ -55,8 +56,7 @@ module.exports = async input => {
for (const emoji of await getGetdangoEmojis(input)) {
set.add(emoji);
}
- } catch (_) {
- }
+ } catch {}
return [...set];
};
diff --git a/package.json b/package.json
index 3a806a2..8ce5517 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "emoj",
- "version": "3.0.1",
+ "version": "3.1.0",
"description": "Find relevant emoji from text on the command-line",
"license": "MIT",
"repository": "sindresorhus/emoj",
@@ -37,26 +37,24 @@
"networks"
],
"dependencies": {
- "auto-bind": "^4.0.0",
"clipboardy": "^2.2.0",
- "conf": "^6.2.1",
+ "conf": "^7.1.1",
"emojilib": "^2.4.0",
- "got": "^10.6.0",
- "import-jsx": "^3.1.0",
- "ink": "^2.7.1",
- "ink-text-input": "^3.2.2",
- "lodash.debounce": "^4.0.6",
+ "got": "^11.5.2",
+ "import-jsx": "^4.0.0",
+ "ink": "^3.0.3",
+ "ink-text-input": "^4.0.0",
"mem": "^6.0.1",
- "meow": "^6.0.1",
+ "meow": "^7.0.1",
"react": "^16.5.2",
"skin-tone": "^1.0.0"
},
"devDependencies": {
"ava": "^1.4.0",
"eslint-config-xo-react": "^0.23.0",
- "eslint-plugin-react": "^7.1.0",
- "eslint-plugin-react-hooks": "^2.4.0",
- "xo": "^0.26.1"
+ "eslint-plugin-react": "^7.20.5",
+ "eslint-plugin-react-hooks": "^4.0.8",
+ "xo": "^0.32.1"
},
"xo": {
"extends": [
diff --git a/readme.md b/readme.md
index 423cadf..c533095 100644
--- a/readme.md
+++ b/readme.md
@@ -4,7 +4,7 @@
-Uses the API from this great article on [Emoji & Deep Learning](http://getdango.com/emoji-and-deep-learning.html) and a local emoji database.
+Uses the API from this great article on [Emoji & Deep Learning](https://getdango.com/emoji-and-deep-learning/) and a local emoji database.
## Install
diff --git a/ui.js b/ui.js
index a15672c..d53ff39 100644
--- a/ui.js
+++ b/ui.js
@@ -1,13 +1,29 @@
'use strict';
const React = require('react');
-const {Box, Color, Text, AppContext, StdinContext} = require('ink');
+const {useState, useCallback, useEffect} = require('react');
+const {Box, Text, useApp, useInput} = require('ink');
const TextInput = require('ink-text-input').default;
-const debounce = require('lodash.debounce');
const skinTone = require('skin-tone');
-const autoBindReact = require('auto-bind/react');
const mem = require('mem');
const emoj = require('.');
+// From https://usehooks.com/useDebounce/
+const useDebouncedValue = (value, delay) => {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+};
+
// Limit it to 7 results so not to overwhelm the user
// This also reduces the chance of showing unrelated emojis
const fetch = mem(async string => {
@@ -15,53 +31,36 @@ const fetch = mem(async string => {
return array.slice(0, 7);
});
-const debouncer = debounce(cb => cb(), 200);
-
const STAGE_CHECKING = 0;
const STAGE_SEARCH = 1;
const STAGE_COPIED = 2;
-// TODO: Move these to https://github.com/sindresorhus/ansi-escapes
-const ARROW_UP = '\u001B[A';
-const ARROW_DOWN = '\u001B[B';
-const ARROW_LEFT = '\u001B[D';
-const ARROW_RIGHT = '\u001B[C';
-const ESC = '\u001B';
-const CTRL_C = '\u0003';
-const RETURN = '\r';
-
const QueryInput = ({query, placeholder, onChange}) => (
-
-
- ›
-
-
- {' '}
-
-
+
+ ›{' '}
+
+
);
const CopiedMessage = ({emoji}) => (
-
+
{`${emoji} has been copied to the clipboard`}
-
+
);
const Search = ({query, emojis, skinNumber, selectedIndex, onChangeQuery}) => {
const list = emojis.map((emoji, index) => (
-
-
- {' '}
- {skinTone(emoji, skinNumber)}
- {' '}
-
-
+ {' '}
+ {skinTone(emoji, skinNumber)}
+ {' '}
+
));
return (
@@ -78,80 +77,67 @@ const Search = ({query, emojis, skinNumber, selectedIndex, onChangeQuery}) => {
);
};
-class Emoj extends React.PureComponent {
- constructor(props) {
- super(props);
- autoBindReact(this);
-
- this.state = {
- stage: STAGE_CHECKING,
- query: '',
- emojis: [],
- skinNumber: props.skinNumber,
- selectedIndex: 0,
- selectedEmoji: null
+const Emoj = ({skinNumber: initialSkinNumber, onSelectEmoji}) => {
+ const {exit} = useApp();
+ const [stage, setStage] = useState(STAGE_CHECKING);
+ const [query, setQuery] = useState('');
+ const [emojis, setEmojis] = useState([]);
+ const [skinNumber, setSkinNumber] = useState(initialSkinNumber);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [selectedEmoji, setSelectedEmoji] = useState();
+
+ useEffect(() => {
+ if (selectedEmoji && stage === STAGE_COPIED) {
+ onSelectEmoji(selectedEmoji);
+ }
+ }, [selectedEmoji, stage, onSelectEmoji]);
+
+ const changeQuery = useCallback(query => {
+ setSelectedIndex(0);
+ setEmojis([]);
+ setQuery(query);
+ });
+
+ useEffect(() => {
+ setStage(STAGE_SEARCH);
+ }, []);
+
+ const debouncedQuery = useDebouncedValue(query, 200);
+
+ useEffect(() => {
+ if (debouncedQuery.length <= 1) {
+ return;
+ }
+
+ let isCanceled = false;
+
+ const run = async () => {
+ const emojis = await fetch(debouncedQuery);
+
+ // Don't update state when this effect was canceled to avoid
+ // results that don't match the search query
+ if (!isCanceled) {
+ setEmojis(emojis);
+ }
};
- }
-
- render() {
- const {
- stage,
- query,
- emojis,
- skinNumber,
- selectedIndex,
- selectedEmoji
- } = this.state;
-
- return (
-
- {stage === STAGE_COPIED && }
- {stage === STAGE_SEARCH && (
-
- )}
-
- );
- }
-
- componentDidMount() {
- this.setState({stage: STAGE_SEARCH}, () => {
- this.props.stdin.on('data', this.handleInput);
- });
- }
-
- handleChangeQuery(query) {
- this.setState({
- query,
- emojis: [],
- selectedIndex: 0
- });
-
- this.fetchEmojis(query);
- }
-
- handleInput(input) {
- const {onExit, onSelectEmoji} = this.props;
- let {skinNumber, selectedIndex, emojis, query} = this.state;
-
- if (input === ESC || input === CTRL_C) {
- onExit();
+
+ run();
+
+ return () => {
+ isCanceled = true;
+ };
+ }, [debouncedQuery]);
+
+ useInput((input, key) => {
+ if (key.escape || (key.ctrl && input === 'c')) {
+ exit();
return;
}
- if (input === RETURN) {
+ if (key.return) {
if (emojis.length > 0) {
- this.setState({
- selectedEmoji: skinTone(emojis[selectedIndex], skinNumber),
- stage: STAGE_COPIED
- }, () => {
- onSelectEmoji(this.state.selectedEmoji);
- });
+ setSelectedEmoji(skinTone(emojis[selectedIndex], skinNumber));
+ setStage(STAGE_COPIED);
}
return;
@@ -160,15 +146,11 @@ class Emoj extends React.PureComponent {
// Select emoji by typing a number
// Catch all 10 keys, but handle only the same amount of keys
// as there are currently emojis
- const numKey = Number(input);
- if (numKey >= 0 && numKey <= 9) {
- if (numKey >= 1 && numKey <= emojis.length) {
- this.setState({
- selectedEmoji: skinTone(emojis[numKey - 1], skinNumber),
- stage: STAGE_COPIED
- }, () => {
- onSelectEmoji(this.state.selectedEmoji);
- });
+ const numberKey = Number(input);
+ if (numberKey >= 0 && numberKey <= 9) {
+ if (numberKey >= 1 && numberKey <= emojis.length) {
+ setSelectedEmoji(skinTone(emojis[numberKey - 1], skinNumber));
+ setStage(STAGE_COPIED);
}
return;
@@ -176,66 +158,51 @@ class Emoj extends React.PureComponent {
// Filter out all ansi sequences except the up/down keys which change the skin tone
// and left/right keys which select emoji inside a list
- const isArrowKey = [ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT].includes(input);
+ const isArrowKey = key.upArrow || key.downArrow || key.leftArrow || key.rightArrow;
if (!isArrowKey || query.length <= 1) {
return;
}
- if (input === ARROW_UP) {
- if (skinNumber < 5) {
- skinNumber++;
- }
+ if (key.upArrow && skinNumber < 5) {
+ setSkinNumber(skinNumber + 1);
}
- if (input === ARROW_DOWN) {
- if (skinNumber > 0) {
- skinNumber--;
- }
+ if (key.downArrow && skinNumber > 0) {
+ setSkinNumber(skinNumber - 1);
}
- if (input === ARROW_RIGHT) {
+ if (key.rightArrow) {
if (selectedIndex < emojis.length - 1) {
- selectedIndex++;
+ setSelectedIndex(selectedIndex + 1);
} else {
- selectedIndex = 0;
+ setSelectedIndex(0);
}
}
- if (input === ARROW_LEFT) {
+ if (key.leftArrow) {
if (selectedIndex > 0) {
- selectedIndex--;
+ setSelectedIndex(selectedIndex - 1);
} else {
- selectedIndex = emojis.length - 1;
+ setSelectedIndex(emojis.length - 1);
}
}
+ });
- this.setState({skinNumber, selectedIndex});
- }
-
- fetchEmojis(query) {
- if (query.length <= 1) {
- return;
- }
-
- debouncer(async () => {
- const emojis = await fetch(query);
+ return (
+ <>
+ {stage === STAGE_COPIED && }
+ {stage === STAGE_SEARCH && (
+
+ )}
+ >
+ );
+};
- if (this.state.query.length > 1) {
- this.setState({emojis});
- }
- });
- }
-}
-
-module.exports = props => (
-
- {({exit}) => (
-
- {({stdin, setRawMode}) => (
-
- )}
-
- )}
-
-);
+module.exports = Emoj;