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;