├── .prettierignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── src ├── assets │ ├── arrow-back.png │ ├── clear-icon.png │ ├── emoji-icon.png │ ├── arrow-back@2x.png │ ├── arrow-back@3x.png │ ├── clear-icon@2x.png │ ├── clear-icon@3x.png │ ├── emoji-icon@2x.png │ ├── emoji-icon@3x.png │ ├── arrow-back-dark.png │ ├── clear-icon-dark.png │ ├── emoji-icon-dark.png │ ├── arrow-back-dark@2x.png │ ├── arrow-back-dark@3x.png │ ├── clear-icon-dark@2x.png │ ├── clear-icon-dark@3x.png │ ├── emoji-icon-dark@2x.png │ └── emoji-icon-dark@3x.png ├── utils │ ├── emoji-index │ │ ├── emoji-index.js │ │ ├── __tests__ │ │ │ ├── nimble-emoji-index.test.js │ │ │ └── emoji-index.test.js │ │ └── nimble-emoji-index.js │ ├── skin.js │ ├── __tests__ │ │ └── get-emoji-data-from-native.test.js │ ├── store.js │ ├── frequently.js │ ├── shared-default-props.js │ ├── data.js │ ├── shared-props.js │ └── index.js ├── index.js ├── components │ ├── picker │ │ ├── picker.js │ │ ├── modal-picker.js │ │ ├── __tests__ │ │ │ └── nimble-picker.test.js │ │ └── nimble-picker.js │ ├── emoji-button.js │ ├── emoji │ │ ├── emoji.js │ │ └── nimble-emoji.js │ ├── skins-emoji.js │ ├── not-found.js │ ├── common │ │ └── touchable.js │ ├── skins.js │ ├── anchors.js │ ├── search.js │ └── category.js └── polyfills │ ├── slicedToArray.js │ └── stringFromCodePoint.js ├── jest.config.js ├── .npmignore ├── .travis.yml ├── .prettierrc ├── scripts ├── build-data.js ├── local-images │ ├── build-data.js │ └── build.js └── build.js ├── babel.config.js ├── LICENSE ├── docs └── index.html ├── package.json ├── CODE_OF_CONDUCT.md ├── BACKERS.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pederjohnsen 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | stats.json 5 | report.html 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /src/assets/arrow-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back.png -------------------------------------------------------------------------------- /src/assets/clear-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon.png -------------------------------------------------------------------------------- /src/assets/emoji-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | modulePathIgnorePatterns: ['example'], 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/arrow-back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back@2x.png -------------------------------------------------------------------------------- /src/assets/arrow-back@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back@3x.png -------------------------------------------------------------------------------- /src/assets/clear-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon@2x.png -------------------------------------------------------------------------------- /src/assets/clear-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon@3x.png -------------------------------------------------------------------------------- /src/assets/emoji-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon@2x.png -------------------------------------------------------------------------------- /src/assets/emoji-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon@3x.png -------------------------------------------------------------------------------- /src/assets/arrow-back-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back-dark.png -------------------------------------------------------------------------------- /src/assets/clear-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon-dark.png -------------------------------------------------------------------------------- /src/assets/emoji-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon-dark.png -------------------------------------------------------------------------------- /src/assets/arrow-back-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back-dark@2x.png -------------------------------------------------------------------------------- /src/assets/arrow-back-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/arrow-back-dark@3x.png -------------------------------------------------------------------------------- /src/assets/clear-icon-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon-dark@2x.png -------------------------------------------------------------------------------- /src/assets/clear-icon-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/clear-icon-dark@3x.png -------------------------------------------------------------------------------- /src/assets/emoji-icon-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon-dark@2x.png -------------------------------------------------------------------------------- /src/assets/emoji-icon-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunoltd/emoji-mart-native/HEAD/src/assets/emoji-icon-dark@3x.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/report.html 2 | scripts/ 3 | .* 4 | 5 | src/ 6 | docs/ 7 | example/ 8 | karma.conf.js 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | sudo: false 5 | script: yarn test 6 | before_script: 7 | - yarn prettier:check 8 | branches: 9 | only: 10 | - master 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": false, 4 | "printWidth": 80, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "tabWidth": 2 9 | } -------------------------------------------------------------------------------- /scripts/build-data.js: -------------------------------------------------------------------------------- 1 | const build = require('./build') 2 | const sets = ['apple', 'facebook', 'google', 'twitter'] 3 | 4 | build({output: 'data/all.json'}) 5 | 6 | sets.forEach((set) => { 7 | build({ 8 | output: `data/${set}.json`, 9 | sets: [set], 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /scripts/local-images/build-data.js: -------------------------------------------------------------------------------- 1 | const build = require('./build') 2 | const sets = ['apple', 'facebook', 'google', 'twitter'] 3 | 4 | build({output: 'data/local-images/all.js'}) 5 | 6 | sets.forEach((set) => { 7 | build({ 8 | output: `data/local-images/${set}.js`, 9 | sets: [set], 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/emoji-index/emoji-index.js: -------------------------------------------------------------------------------- 1 | import data from '../../../data/all.json' 2 | import NimbleEmojiIndex from './nimble-emoji-index' 3 | 4 | const emojiIndex = new NimbleEmojiIndex(data) 5 | const {emojis, emoticons} = emojiIndex 6 | 7 | function search() { 8 | return emojiIndex.search(...arguments) 9 | } 10 | 11 | export default {search, emojis, emoticons} 12 | -------------------------------------------------------------------------------- /src/utils/skin.js: -------------------------------------------------------------------------------- 1 | import store from './store' 2 | 3 | let skin, initialized 4 | 5 | function init() { 6 | initialized = true 7 | skin = store.get('skin') 8 | } 9 | 10 | function set(skinTone) { 11 | if (!initialized) init() 12 | 13 | skin = skinTone 14 | store.set('skin', skinTone) 15 | } 16 | 17 | function get() { 18 | if (!initialized) init() 19 | 20 | return skin 21 | } 22 | 23 | export default {set, get} 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Tap on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /src/utils/emoji-index/__tests__/nimble-emoji-index.test.js: -------------------------------------------------------------------------------- 1 | import NimbleEmojiIndex from '../nimble-emoji-index.js' 2 | import store from '../../store' 3 | 4 | import data from '../../../../data/all' 5 | 6 | const nimbleEmojiIndex = new NimbleEmojiIndex(data) 7 | 8 | function getEmojiData(skinTone) { 9 | store.update({skin: skinTone}) 10 | return nimbleEmojiIndex.search('thumbsup')[0] 11 | } 12 | 13 | test('should return emojis with skin tone 1', () => { 14 | const skinTone = 1 15 | const emoji = getEmojiData(skinTone) 16 | expect(emoji.skin).toEqual(skinTone) 17 | }) 18 | 19 | // Re-enable once store works properly 20 | // test('should return emojis with skin tone 6', () => { 21 | // const skinTone = 6 22 | // const emoji = getEmojiData(skinTone) 23 | // expect(emoji.skin).toEqual(skinTone) 24 | // }) 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default as emojiIndex} from './utils/emoji-index/emoji-index' 2 | export { 3 | default as NimbleEmojiIndex, 4 | } from './utils/emoji-index/nimble-emoji-index' 5 | export {default as store} from './utils/store' 6 | export {default as frequently} from './utils/frequently' 7 | export {getEmojiDataFromNative, getEmojiDataFromCustom} from './utils' 8 | 9 | export {default as Picker} from './components/picker/picker' 10 | export {default as ModalPicker} from './components/picker/modal-picker' 11 | export {default as NimblePicker} from './components/picker/nimble-picker' 12 | export {default as Emoji} from './components/emoji/emoji' 13 | export {default as NimbleEmoji} from './components/emoji/nimble-emoji' 14 | export {default as Category} from './components/category' 15 | export {default as EmojiButton} from './components/emoji-button' 16 | -------------------------------------------------------------------------------- /src/components/picker/picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {StyleSheet, View} from 'react-native' 3 | 4 | import data from '../../../data/all.json' 5 | import NimblePicker from './nimble-picker' 6 | 7 | import {PickerPropTypes} from '../../utils/shared-props' 8 | import {PickerDefaultProps} from '../../utils/shared-default-props' 9 | 10 | const styles = StyleSheet.create({ 11 | emojiMartPickerContainer: { 12 | width: '100%', 13 | flexDirection: 'column', 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | }, 17 | }) 18 | 19 | export default class Picker extends React.PureComponent { 20 | static propTypes /* remove-proptypes */ = PickerPropTypes 21 | static defaultProps = {...PickerDefaultProps, data} 22 | 23 | render() { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const {devDependencies} = require('./package.json') 2 | 3 | module.exports = { 4 | presets: ['module:metro-react-native-babel-preset'], 5 | plugins: [ 6 | '@babel/plugin-transform-runtime', 7 | '@babel/plugin-proposal-class-properties', 8 | [ 9 | 'babel-plugin-transform-define', 10 | { 11 | 'process.env.NODE_ENV': 'production', 12 | EMOJI_DATASOURCE_VERSION: devDependencies['emoji-datasource'], 13 | }, 14 | ], 15 | ], 16 | env: { 17 | cjs: { 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | modules: 'cjs', 23 | }, 24 | ], 25 | ], 26 | }, 27 | test: { 28 | presets: [ 29 | [ 30 | '@babel/preset-env', 31 | { 32 | targets: { 33 | node: 'current', 34 | }, 35 | }, 36 | ], 37 | ], 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/__tests__/get-emoji-data-from-native.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {getEmojiDataFromNative} from '../' 3 | 4 | import data from '../../../data/apple' 5 | 6 | test('will find man lifting weights with skin tone 6', () => { 7 | const emojiData = getEmojiDataFromNative('🏋🏿‍♂️', 'apple', data) 8 | expect(emojiData.id).toEqual('man-lifting-weights') 9 | expect(emojiData.skin).toEqual(6) 10 | }) 11 | 12 | test('will find woman swimming with skin tone 4', () => { 13 | const emojiData = getEmojiDataFromNative('🏊🏽‍♀️', 'apple', data) 14 | expect(emojiData.id).toEqual('woman-swimming') 15 | expect(emojiData.skin).toEqual(4) 16 | }) 17 | 18 | test('will find person in lotus positions', () => { 19 | const emojiData = getEmojiDataFromNative('🧘', 'apple', data) 20 | expect(emojiData.id).toEqual('person_in_lotus_position') 21 | expect(emojiData.skin).toEqual(1) 22 | }) 23 | 24 | test('returns null if no match', () => { 25 | const emojiData = getEmojiDataFromNative('', 'apple', data) 26 | expect(emojiData).toEqual(null) 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/emoji-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {TouchableOpacity, Image} from 'react-native' 4 | 5 | const emojiIcon = require('../assets/emoji-icon.png') 6 | 7 | export default class EmojiButton extends React.PureComponent { 8 | static propTypes /* remove-proptypes */ = { 9 | onButtonPress: PropTypes.func, 10 | buttonImage: PropTypes.oneOfType([ 11 | PropTypes.shape({ 12 | uri: PropTypes.string, 13 | }), 14 | PropTypes.number, 15 | ]), 16 | buttonSize: PropTypes.number, 17 | } 18 | 19 | static defaultProps = { 20 | onButtonPress: () => {}, 21 | buttonImage: emojiIcon, 22 | buttonSize: 18, 23 | } 24 | 25 | render() { 26 | const {buttonImage, buttonSize} = this.props 27 | 28 | return ( 29 | 30 | 34 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/emoji/emoji.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import data from '../../../data/all.json' 4 | import NimbleEmoji from './nimble-emoji' 5 | 6 | import {EmojiPropTypes} from '../../utils/shared-props' 7 | import {EmojiDefaultProps} from '../../utils/shared-default-props' 8 | 9 | // TODO: Use functional components? 10 | // const Emoji = (props) => { 11 | // for (let k in Emoji.defaultProps) { 12 | // if (props[k] == undefined && Emoji.defaultProps[k] != undefined) { 13 | // props[k] = Emoji.defaultProps[k] 14 | // } 15 | // } 16 | // 17 | // return NimbleEmoji({ ...props }) 18 | // } 19 | 20 | export default class Emoji extends React.PureComponent { 21 | static propTypes /* remove-proptypes */ = EmojiPropTypes 22 | static defaultProps = {...EmojiDefaultProps, data} 23 | 24 | render() { 25 | for (let k in Emoji.defaultProps) { 26 | if (this.props[k] === undefined && Emoji.defaultProps[k] != undefined) { 27 | this.props[k] = Emoji.defaultProps[k] 28 | } 29 | } 30 | 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/polyfills/slicedToArray.js: -------------------------------------------------------------------------------- 1 | const _Object = Object 2 | 3 | export default (function createClass() { 4 | function sliceIterator(arr, i) { 5 | var _arr = [] 6 | var _n = true 7 | var _d = false 8 | var _e = undefined 9 | 10 | try { 11 | for ( 12 | var _i = _Object.getIterator(arr), _s; 13 | !(_n = (_s = _i.next()).done); 14 | _n = true 15 | ) { 16 | _arr.push(_s.value) 17 | 18 | if (i && _arr.length === i) break 19 | } 20 | } catch (err) { 21 | _d = true 22 | _e = err 23 | } finally { 24 | try { 25 | if (!_n && _i['return']) _i['return']() 26 | } finally { 27 | if (_d) throw _e 28 | } 29 | } 30 | 31 | return _arr 32 | } 33 | 34 | return function(arr, i) { 35 | if (Array.isArray(arr)) { 36 | return arr 37 | } else if (_Object.isIterable(Object(arr))) { 38 | return sliceIterator(arr, i) 39 | } else { 40 | throw new TypeError( 41 | 'Invalid attempt to destructure non-iterable instance', 42 | ) 43 | } 44 | } 45 | })() 46 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | var NAMESPACE = 'emoji-mart-native' 2 | 3 | // TODO: Use a react native alternative to browser local storage 4 | var isLocalStorageSupported = 5 | typeof window !== 'undefined' && 'localStorage' in window 6 | 7 | let getter 8 | let setter 9 | 10 | function setHandlers(handlers) { 11 | handlers || (handlers = {}) 12 | 13 | getter = handlers.getter 14 | setter = handlers.setter 15 | } 16 | 17 | function setNamespace(namespace) { 18 | NAMESPACE = namespace 19 | } 20 | 21 | function update(state) { 22 | for (let key in state) { 23 | let value = state[key] 24 | set(key, value) 25 | } 26 | } 27 | 28 | function set(key, value) { 29 | if (setter) { 30 | setter(key, value) 31 | } else { 32 | if (!isLocalStorageSupported) return 33 | try { 34 | window.localStorage[`${NAMESPACE}.${key}`] = JSON.stringify(value) 35 | } catch (e) {} 36 | } 37 | } 38 | 39 | function get(key) { 40 | if (getter) { 41 | return getter(key) 42 | } else { 43 | if (!isLocalStorageSupported) return 44 | try { 45 | var value = window.localStorage[`${NAMESPACE}.${key}`] 46 | 47 | if (value) { 48 | return JSON.parse(value) 49 | } 50 | } catch (e) { 51 | return 52 | } 53 | } 54 | } 55 | 56 | export default {update, set, get, setNamespace, setHandlers} 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Missive 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /src/polyfills/stringFromCodePoint.js: -------------------------------------------------------------------------------- 1 | const _String = String 2 | 3 | export default _String.fromCodePoint || 4 | function stringFromCodePoint() { 5 | var MAX_SIZE = 0x4000 6 | var codeUnits = [] 7 | var highSurrogate 8 | var lowSurrogate 9 | var index = -1 10 | var length = arguments.length 11 | if (!length) { 12 | return '' 13 | } 14 | var result = '' 15 | while (++index < length) { 16 | var codePoint = Number(arguments[index]) 17 | if ( 18 | !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` 19 | codePoint < 0 || // not a valid Unicode code point 20 | codePoint > 0x10ffff || // not a valid Unicode code point 21 | Math.floor(codePoint) != codePoint // not an integer 22 | ) { 23 | throw RangeError('Invalid code point: ' + codePoint) 24 | } 25 | if (codePoint <= 0xffff) { 26 | // BMP code point 27 | codeUnits.push(codePoint) 28 | } else { 29 | // Astral code point; split in surrogate halves 30 | // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 31 | codePoint -= 0x10000 32 | highSurrogate = (codePoint >> 10) + 0xd800 33 | lowSurrogate = (codePoint % 0x400) + 0xdc00 34 | codeUnits.push(highSurrogate, lowSurrogate) 35 | } 36 | if (index + 1 === length || codeUnits.length > MAX_SIZE) { 37 | result += String.fromCharCode.apply(null, codeUnits) 38 | codeUnits.length = 0 39 | } 40 | } 41 | return result 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/emoji-index/__tests__/emoji-index.test.js: -------------------------------------------------------------------------------- 1 | import emojiIndex from '../emoji-index.js' 2 | 3 | test('should work', () => { 4 | expect(emojiIndex.search('pineapple')).toEqual([ 5 | { 6 | id: 'pineapple', 7 | name: 'Pineapple', 8 | short_names: ['pineapple'], 9 | colons: ':pineapple:', 10 | emoticons: [], 11 | unified: '1f34d', 12 | skin: null, 13 | native: '🍍', 14 | }, 15 | ]) 16 | }) 17 | 18 | test('should filter only emojis we care about, exclude pineapple', () => { 19 | let emojisToShowFilter = (data) => { 20 | data.unified !== '1F34D' 21 | } 22 | expect( 23 | emojiIndex.search('apple', {emojisToShowFilter}).map((obj) => obj.id), 24 | ).not.toContain('pineapple') 25 | }) 26 | 27 | test('can include/exclude categories', () => { 28 | expect(emojiIndex.search('flag', {include: ['people']})).toEqual([]) 29 | }) 30 | 31 | test('can search for thinking_face', () => { 32 | expect(emojiIndex.search('thinking_fac').map((x) => x.id)).toEqual([ 33 | 'thinking_face', 34 | ]) 35 | }) 36 | 37 | test('can search for woman-facepalming', () => { 38 | expect(emojiIndex.search('woman-facep').map((x) => x.id)).toEqual([ 39 | 'woman-facepalming', 40 | ]) 41 | }) 42 | 43 | test('emojiIndex exports emojis', () => { 44 | const emojis = emojiIndex.emojis 45 | expect(emojis['thinking_face'].native).toEqual('\ud83e\udd14') 46 | }) 47 | 48 | test('emojiIndex exports emoticons', () => { 49 | const emoticons = emojiIndex.emoticons 50 | expect(emoticons[':)']).toEqual('slightly_smiling_face') 51 | }) 52 | -------------------------------------------------------------------------------- /src/utils/frequently.js: -------------------------------------------------------------------------------- 1 | import store from './store' 2 | 3 | const DEFAULTS = [ 4 | '+1', 5 | 'grinning', 6 | 'kissing_heart', 7 | 'heart_eyes', 8 | 'laughing', 9 | 'stuck_out_tongue_winking_eye', 10 | 'sweat_smile', 11 | 'joy', 12 | 'scream', 13 | 'disappointed', 14 | 'unamused', 15 | 'weary', 16 | 'sob', 17 | 'sunglasses', 18 | 'heart', 19 | 'poop', 20 | ] 21 | 22 | let frequently, initialized 23 | let defaults = {} 24 | 25 | function init() { 26 | initialized = true 27 | frequently = store.get('frequently') 28 | } 29 | 30 | function add(emoji) { 31 | if (!initialized) init() 32 | var {id} = emoji 33 | 34 | frequently || (frequently = defaults) 35 | frequently[id] || (frequently[id] = 0) 36 | frequently[id] += 1 37 | 38 | store.set('last', id) 39 | store.set('frequently', frequently) 40 | } 41 | 42 | function get(perLine) { 43 | if (!initialized) init() 44 | if (!frequently) { 45 | defaults = {} 46 | 47 | const result = [] 48 | 49 | for (let i = 0; i < perLine; i++) { 50 | defaults[DEFAULTS[i]] = perLine - i 51 | result.push(DEFAULTS[i]) 52 | } 53 | 54 | return result 55 | } 56 | 57 | const quantity = perLine * 3 58 | const frequentlyKeys = [] 59 | 60 | for (let key in frequently) { 61 | if (frequently.hasOwnProperty(key)) { 62 | frequentlyKeys.push(key) 63 | } 64 | } 65 | 66 | const sorted = frequentlyKeys 67 | .sort((a, b) => frequently[a] - frequently[b]) 68 | .reverse() 69 | const sliced = sorted.slice(0, quantity) 70 | 71 | const last = store.get('last') 72 | 73 | if (last && sliced.indexOf(last) == -1) { 74 | sliced.pop() 75 | sliced.push(last) 76 | } 77 | 78 | return sliced 79 | } 80 | 81 | export default {add, get} 82 | -------------------------------------------------------------------------------- /src/components/picker/modal-picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {StyleSheet, Modal, View, TouchableWithoutFeedback} from 'react-native' 4 | 5 | import data from '../../../data/all.json' 6 | import NimblePicker from './nimble-picker' 7 | 8 | import {PickerPropTypes} from '../../utils/shared-props' 9 | import {PickerDefaultProps} from '../../utils/shared-default-props' 10 | 11 | const styles = StyleSheet.create({ 12 | emojiMartBackdrop: { 13 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 14 | ...StyleSheet.absoluteFillObject, 15 | }, 16 | emojiMartPickerContainer: { 17 | flexDirection: 'column', 18 | alignItems: 'center', 19 | justifyContent: 'center', 20 | ...StyleSheet.absoluteFillObject, 21 | }, 22 | }) 23 | 24 | export default class ModalPicker extends React.PureComponent { 25 | static propTypes /* remove-proptypes */ = { 26 | ...PickerPropTypes, 27 | isVisible: PropTypes.bool, 28 | } 29 | static defaultProps = { 30 | ...PickerDefaultProps, 31 | data, 32 | isVisible: false, 33 | } 34 | 35 | render() { 36 | var {onPressClose, isVisible} = this.props 37 | 38 | if (!isVisible) { 39 | return null 40 | } 41 | 42 | return ( 43 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/shared-default-props.js: -------------------------------------------------------------------------------- 1 | const EmojiDefaultProps = { 2 | skin: 1, 3 | set: 'apple', 4 | sheetSize: 64, 5 | sheetColumns: 60, 6 | sheetRows: 60, 7 | native: false, 8 | forceSize: false, 9 | tooltip: false, 10 | spriteSheetFn: (set, sheetSize) => ({ 11 | uri: `https://unpkg.com/emoji-datasource-${set}@${EMOJI_DATASOURCE_VERSION}/img/${set}/sheets-256/${sheetSize}.png`, 12 | }), 13 | emojiImageFn: (image) => image, 14 | onPress: () => {}, 15 | onLongPress: () => {}, 16 | useLocalImages: false, 17 | margin: 14, 18 | noMargin: false, 19 | } 20 | 21 | const PickerDefaultProps = { 22 | onPress: () => {}, 23 | onLongPress: () => {}, 24 | onSelect: () => {}, 25 | onPressClose: () => {}, 26 | onSkinChange: () => {}, 27 | emojiSize: 30, 28 | emojiMargin: EmojiDefaultProps.margin, 29 | anchorSize: 24, 30 | perLine: 7, 31 | rows: 3, 32 | pagesToEagerLoad: 2, 33 | i18n: {}, 34 | style: {}, 35 | color: '#ae65c5', 36 | set: EmojiDefaultProps.set, 37 | theme: 'light', 38 | skin: null, 39 | defaultSkin: EmojiDefaultProps.skin, 40 | native: EmojiDefaultProps.native, 41 | sheetSize: EmojiDefaultProps.sheetSize, 42 | sheetColumns: EmojiDefaultProps.sheetColumns, 43 | sheetRows: EmojiDefaultProps.sheetRows, 44 | spriteSheetFn: EmojiDefaultProps.spriteSheetFn, 45 | emojiImageFn: EmojiDefaultProps.emojiImageFn, 46 | emojisToShowFilter: null, 47 | useLocalImages: EmojiDefaultProps.useLocalImages, 48 | showSkinTones: true, 49 | showAnchors: true, 50 | showCloseButton: false, 51 | emojiTooltip: EmojiDefaultProps.tooltip, 52 | autoFocus: false, 53 | enableFrequentEmojiSort: false, 54 | custom: [], 55 | skinEmoji: '', 56 | skinEmojiSize: 28, 57 | notFound: () => {}, 58 | notFoundEmoji: 'sleuth_or_spy', 59 | categoryEmojis: {}, 60 | fontSize: 15, 61 | } 62 | 63 | export {PickerDefaultProps, EmojiDefaultProps} 64 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Emoji Mart Native 🏬 | One component to pick them all 5 | 53 | 54 | 55 | 56 |
57 |
58 |

Emoji Mart Native 🏬

59 |
60 |
61 | picker 62 |
63 |
64 | Star 69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/components/skins-emoji.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {StyleSheet, View, TouchableWithoutFeedback} from 'react-native' 4 | 5 | import NimbleEmoji from './emoji/nimble-emoji' 6 | 7 | const styles = StyleSheet.create({ 8 | skinSwatches: { 9 | paddingTop: 2, 10 | paddingBottom: 2, 11 | flexDirection: 'row', 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | }, 15 | skinSwatch: { 16 | paddingLeft: 2, 17 | paddingRight: 2, 18 | }, 19 | skin: { 20 | flexDirection: 'row', 21 | alignItems: 'center', 22 | justifyContent: 'center', 23 | }, 24 | }) 25 | 26 | export default class SkinsEmoji extends React.PureComponent { 27 | constructor(props) { 28 | super(props) 29 | 30 | this.state = { 31 | opened: false, 32 | } 33 | } 34 | 35 | handlePress(skin) { 36 | var {onChange} = this.props 37 | 38 | if (!this.state.opened) { 39 | this.setState({opened: true}) 40 | } else { 41 | this.setState({opened: false}) 42 | if (skin != this.props.skin) { 43 | onChange(skin) 44 | } 45 | } 46 | } 47 | 48 | render() { 49 | const {skin, emojiProps, data, skinEmoji, skinEmojiSize} = this.props 50 | const {opened} = this.state 51 | 52 | const skinToneNodes = [] 53 | 54 | for (let skinTone = 1; skinTone <= 6; skinTone++) { 55 | const selected = skinTone === skin 56 | 57 | if (selected || opened) { 58 | skinToneNodes.push( 59 | 60 | 68 | , 69 | ) 70 | } 71 | } 72 | 73 | return {skinToneNodes} 74 | } 75 | } 76 | 77 | SkinsEmoji.propTypes /* remove-proptypes */ = { 78 | onChange: PropTypes.func, 79 | skin: PropTypes.number.isRequired, 80 | emojiProps: PropTypes.object.isRequired, 81 | skinTone: PropTypes.number, 82 | skinEmoji: PropTypes.string.isRequired, 83 | } 84 | 85 | SkinsEmoji.defaultProps = { 86 | onChange: () => {}, 87 | skinTone: null, 88 | } 89 | -------------------------------------------------------------------------------- /src/components/not-found.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {StyleSheet, View, Text} from 'react-native' 4 | import { ViewPropTypes } from 'deprecated-react-native-prop-types' 5 | 6 | import NimbleEmoji from './emoji/nimble-emoji' 7 | 8 | const styles = StyleSheet.create({ 9 | labelText: { 10 | fontWeight: 'bold', 11 | }, 12 | labelTextLight: { 13 | color: '#414141', 14 | }, 15 | labelTextDark: { 16 | color: '#bebebe', 17 | }, 18 | notFound: { 19 | flex: 1, 20 | alignSelf: 'center', 21 | flexDirection: 'column', 22 | alignItems: 'center', 23 | justifyContent: 'center', 24 | }, 25 | }) 26 | 27 | export default class NotFound extends React.PureComponent { 28 | static propTypes /* remove-proptypes */ = { 29 | notFound: PropTypes.func.isRequired, 30 | notFoundEmoji: PropTypes.string.isRequired, 31 | emojiProps: PropTypes.object.isRequired, 32 | style: ViewPropTypes.style, 33 | theme: PropTypes.oneOf(['light', 'dark']), 34 | fontSize: PropTypes.number, 35 | } 36 | 37 | static defaultProps = { 38 | theme: 'light', 39 | fontSize: 15, 40 | } 41 | 42 | render() { 43 | const { 44 | data, 45 | emojiProps, 46 | i18n, 47 | notFound, 48 | notFoundEmoji, 49 | style, 50 | theme, 51 | fontSize, 52 | } = this.props 53 | 54 | const component = ( 55 | 56 | {(notFound && notFound()) || ( 57 | 58 | 59 | 66 | 67 | 68 | 69 | 78 | {i18n.notfound} 79 | 80 | 81 | 82 | )} 83 | 84 | ) 85 | 86 | return component 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/common/touchable.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Platform, 4 | TouchableNativeFeedback, 5 | TouchableWithoutFeedback, 6 | View, 7 | } from 'react-native' 8 | 9 | let TouchableComponent 10 | 11 | if (Platform.OS === 'android') { 12 | TouchableComponent = TouchableNativeFeedback 13 | } else { 14 | TouchableComponent = TouchableWithoutFeedback 15 | } 16 | 17 | if (TouchableComponent !== TouchableNativeFeedback) { 18 | TouchableComponent.SelectableBackground = () => ({}) 19 | TouchableComponent.SelectableBackgroundBorderless = () => ({}) 20 | TouchableComponent.Ripple = () => ({}) 21 | TouchableComponent.canUseNativeForeground = () => false 22 | } 23 | 24 | export default class PlatformTouchable extends React.PureComponent { 25 | static SelectableBackground = TouchableComponent.SelectableBackground 26 | static SelectableBackgroundBorderless = 27 | TouchableComponent.SelectableBackgroundBorderless 28 | static Ripple = TouchableComponent.Ripple 29 | static canUseNativeForeground = TouchableComponent.canUseNativeForeground 30 | 31 | render() { 32 | let { 33 | children, 34 | style, 35 | foreground, 36 | background, 37 | useForeground, 38 | ...props 39 | } = this.props 40 | 41 | children = React.Children.only(children) 42 | 43 | if (TouchableComponent === TouchableNativeFeedback) { 44 | useForeground = 45 | foreground && TouchableNativeFeedback.canUseNativeForeground() 46 | 47 | if (foreground && background) { 48 | console.warn( 49 | 'Specified foreground and background for Touchable, only one can be used at a time. Defaulted to foreground.', 50 | ) 51 | } 52 | 53 | return ( 54 | 59 | {children} 60 | 61 | ) 62 | } else if (TouchableComponent === TouchableWithoutFeedback) { 63 | return ( 64 | 65 | {children} 66 | 67 | ) 68 | } else { 69 | let TouchableFallback = this.props.fallback || TouchableComponent 70 | return ( 71 | 72 | {children} 73 | 74 | ) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 23 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-mart-native", 3 | "version": "0.6.5-beta", 4 | "description": "Customizable Slack-like emoji picker for React Native", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:tunoltd/emoji-mart-native.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "native", 13 | "react native", 14 | "emoji", 15 | "picker" 16 | ], 17 | "author": "Peder Johnsen", 18 | "license": "BSD-3-Clause", 19 | "bugs": { 20 | "url": "https://github.com/tunoltd/emoji-mart-native/issues" 21 | }, 22 | "homepage": "https://tunoltd.github.io/emoji-mart-native/", 23 | "dependencies": { 24 | "@babel/runtime": "^7.16.7", 25 | "deprecated-react-native-prop-types": "^4.0.0", 26 | "prop-types": "^15.6.0" 27 | }, 28 | "peerDependencies": { 29 | "react": "*", 30 | "react-native": "*" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.16.7", 34 | "@babel/core": "^7.16.7", 35 | "@babel/plugin-proposal-class-properties": "^7.8.3", 36 | "@babel/plugin-transform-runtime": "^7.7.6", 37 | "@babel/preset-env": "^7.0.0", 38 | "babel-jest": "^24.9.0", 39 | "babel-loader": "^8.0.0", 40 | "babel-plugin-transform-define": "^2.0.0", 41 | "emoji-datasource": "7.0.2", 42 | "emojilib": "^3.0.0", 43 | "enzyme": "^3.9.0", 44 | "enzyme-adapter-react-16": "^1.11.2", 45 | "inflection": "1.10.0", 46 | "jest": "^24.9.0", 47 | "metro-react-native-babel-preset": "^0.59.0", 48 | "mkdirp": "0.5.1", 49 | "prettier": "^1.16.4", 50 | "react": "^16.11.0", 51 | "react-native": "^0.62.2", 52 | "react-test-renderer": "^16.8.4", 53 | "rimraf": "2.5.2", 54 | "webpack": "^3.6.0" 55 | }, 56 | "scripts": { 57 | "clean": "rm -rf dist/", 58 | "build:data": "node scripts/build-data", 59 | "build:localImagesData": "node scripts/local-images/build-data && BABEL_ENV=cjs babel data/local-images --out-dir data/local-images --copy-files", 60 | "build:dist": "npm run build:cjs", 61 | "build:cjs": "BABEL_ENV=cjs babel src --out-dir dist --copy-files --ignore '**/__tests__/*'", 62 | "build:link": "BABEL_ENV=cjs babel src --copy-files --ignore '**/__tests__/*'", 63 | "build": "npm run clean && npm run build:dist", 64 | "watch": "BABEL_ENV=cjs babel src --watch --out-dir dist --copy-files --ignore '**/__tests__/*'", 65 | "start": "npm run watch", 66 | "test": "npm run clean && jest", 67 | "prepublishOnly": "npm run build", 68 | "prettier": "prettier --write \"{src,scripts}/**/*.js\"", 69 | "prettier:check": "prettier --check \"{src,scripts}/**/*.js\"", 70 | "prepare": "npm run build:dist" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/data.js: -------------------------------------------------------------------------------- 1 | const mapping = { 2 | name: 'a', 3 | unified: 'b', 4 | non_qualified: 'c', 5 | has_img_apple: 'd', 6 | has_img_google: 'e', 7 | has_img_twitter: 'f', 8 | has_img_facebook: 'g', 9 | keywords: 'h', 10 | sheet: 'i', 11 | emoticons: 'j', 12 | text: 'k', 13 | short_names: 'l', 14 | added_in: 'm', 15 | image: 'n', 16 | skin_variations: 'o', 17 | } 18 | 19 | const buildSearch = (emoji) => { 20 | const search = [] 21 | 22 | var addToSearch = (strings, split) => { 23 | if (!strings) { 24 | return 25 | } 26 | 27 | ;(Array.isArray(strings) ? strings : [strings]).forEach((string) => { 28 | ;(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => { 29 | s = s.toLowerCase() 30 | 31 | if (search.indexOf(s) == -1) { 32 | search.push(s) 33 | } 34 | }) 35 | }) 36 | } 37 | 38 | addToSearch(emoji.short_names, true) 39 | addToSearch(emoji.name, true) 40 | addToSearch(emoji.keywords, false) 41 | addToSearch(emoji.emoticons, false) 42 | 43 | return search.join(',') 44 | } 45 | 46 | const compress = (emoji) => { 47 | emoji.short_names = emoji.short_names.filter((short_name) => { 48 | return short_name !== emoji.short_name 49 | }) 50 | delete emoji.short_name 51 | 52 | emoji.sheet = [emoji.sheet_x, emoji.sheet_y] 53 | delete emoji.sheet_x 54 | delete emoji.sheet_y 55 | 56 | emoji.added_in = parseInt(emoji.added_in) 57 | if (emoji.added_in === 6) { 58 | delete emoji.added_in 59 | } 60 | 61 | for (let key in mapping) { 62 | emoji[mapping[key]] = emoji[key] 63 | delete emoji[key] 64 | } 65 | 66 | for (let key in emoji) { 67 | let value = emoji[key] 68 | 69 | if (Array.isArray(value) && !value.length) { 70 | delete emoji[key] 71 | } else if (typeof value === 'string' && !value.length) { 72 | delete emoji[key] 73 | } else if (value === null) { 74 | delete emoji[key] 75 | } 76 | } 77 | } 78 | 79 | const uncompress = (data) => { 80 | data.compressed = false 81 | 82 | for (let id in data.emojis) { 83 | let emoji = data.emojis[id] 84 | 85 | for (let key in mapping) { 86 | emoji[key] = emoji[mapping[key]] 87 | delete emoji[mapping[key]] 88 | } 89 | 90 | if (!emoji.short_names) emoji.short_names = [] 91 | emoji.short_names.unshift(id) 92 | 93 | emoji.sheet_x = emoji.sheet[0] 94 | emoji.sheet_y = emoji.sheet[1] 95 | delete emoji.sheet 96 | 97 | if (!emoji.text) emoji.text = '' 98 | 99 | if (!emoji.added_in) emoji.added_in = 6 100 | emoji.added_in = emoji.added_in.toFixed(1) 101 | 102 | emoji.search = buildSearch(emoji) 103 | } 104 | } 105 | 106 | module.exports = {buildSearch, compress, uncompress} 107 | -------------------------------------------------------------------------------- /src/components/picker/__tests__/nimble-picker.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NimblePicker from '../nimble-picker' 3 | import NimbleEmojiIndex from '../../../utils/emoji-index/nimble-emoji-index' 4 | import renderer from 'react-test-renderer' 5 | 6 | import data from '../../../../data/apple' 7 | 8 | function render(props = {}) { 9 | const defaultProps = {data} 10 | const component = renderer.create( 11 | , 12 | ) 13 | return component.getInstance() 14 | } 15 | 16 | test('shows 10 categories by default', () => { 17 | const subject = render() 18 | expect(subject.categories.length).toEqual(10) 19 | }) 20 | 21 | test('will not show some categories based upon our filter', () => { 22 | const subject = render({emojisToShowFilter: () => false}) 23 | expect(subject.categories.length).toEqual(2) 24 | }) 25 | 26 | test('maintains category ids after it is filtered', () => { 27 | const subject = render({emojisToShowFilter: () => true}) 28 | const categoriesWithIds = subject.categories.filter((category) => category.id) 29 | expect(categoriesWithIds.length).toEqual(10) 30 | }) 31 | 32 | test('with custom emoji', () => { 33 | const custom = [ 34 | { 35 | id: 'custom_name', 36 | name: 'custom_name', 37 | short_names: ['custom_name'], 38 | text: '', 39 | emoticons: [], 40 | keywords: ['custom_name'], 41 | image: {uri: 'https://example.com/emoji'}, 42 | custom: true, 43 | }, 44 | { 45 | id: 'custom_name2', 46 | name: 'custom_name2', 47 | short_names: ['custom_name2'], 48 | text: '', 49 | emoticons: [], 50 | keywords: ['custom_name2'], 51 | image: {uri: 'https://example.com/emoji'}, 52 | custom: true, 53 | }, 54 | ] 55 | const subject = render({ 56 | autoFocus: true, 57 | custom, 58 | }) 59 | expect(subject.categories.length).toEqual(11) 60 | expect(subject.categories[10].name).toEqual('Custom') 61 | subject.handleSearch( 62 | new NimbleEmojiIndex(subject.data).search('custom_', {custom}), 63 | ) 64 | }) 65 | 66 | test('with custom categories', () => { 67 | const custom = [ 68 | { 69 | id: 'custom_name', 70 | name: 'custom_name', 71 | short_names: ['custom_name'], 72 | text: '', 73 | emoticons: [], 74 | keywords: ['custom_name'], 75 | image: {uri: 'https://example.com/emoji'}, 76 | custom: true, 77 | customCategory: 'Category 1', 78 | }, 79 | { 80 | id: 'custom_name2', 81 | name: 'custom_name2', 82 | short_names: ['custom_name2'], 83 | text: '', 84 | emoticons: [], 85 | keywords: ['custom_name2'], 86 | image: {uri: 'https://example.com/emoji'}, 87 | custom: true, 88 | customCategory: 'Category 2', 89 | }, 90 | ] 91 | const subject = render({custom}) 92 | expect(subject.categories.length).toEqual(12) 93 | expect(subject.categories[10].name).toEqual('Category 1') 94 | expect(subject.categories[11].name).toEqual('Category 2') 95 | }) 96 | -------------------------------------------------------------------------------- /src/utils/shared-props.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | const EmojiPropTypes = { 4 | data: PropTypes.object.isRequired, 5 | onPress: PropTypes.func, 6 | onLongPress: PropTypes.func, 7 | fallback: PropTypes.func, 8 | spriteSheetFn: PropTypes.func, 9 | emojiImageFn: PropTypes.func, 10 | native: PropTypes.bool, 11 | forceSize: PropTypes.bool, 12 | tooltip: PropTypes.bool, 13 | skin: PropTypes.oneOf([1, 2, 3, 4, 5, 6]), 14 | sheetSize: PropTypes.oneOf([16, 20, 32, 64]), 15 | sheetColumns: PropTypes.number, 16 | sheetRows: PropTypes.number, 17 | set: PropTypes.oneOf(['apple', 'google', 'twitter', 'facebook']), 18 | size: PropTypes.number.isRequired, 19 | emoji: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, 20 | useLocalImages: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 21 | margin: PropTypes.number, 22 | noMargin: PropTypes.bool, 23 | } 24 | 25 | const PickerPropTypes = { 26 | onPress: PropTypes.func, 27 | onLongPress: PropTypes.func, 28 | onSelect: PropTypes.func, 29 | onPressClose: PropTypes.func, 30 | onSkinChange: PropTypes.func, 31 | perLine: PropTypes.number, 32 | rows: PropTypes.number, 33 | pagesToEagerLoad: PropTypes.number, 34 | emojiSize: PropTypes.number, 35 | emojiMargin: PropTypes.number, 36 | anchorSize: PropTypes.number, 37 | i18n: PropTypes.object, 38 | style: PropTypes.object, 39 | color: PropTypes.string, 40 | set: EmojiPropTypes.set, 41 | skin: EmojiPropTypes.skin, 42 | native: PropTypes.bool, 43 | spriteSheetFn: EmojiPropTypes.spriteSheetFn, 44 | sheetSize: EmojiPropTypes.sheetSize, 45 | sheetColumns: PropTypes.number, 46 | sheetRows: PropTypes.number, 47 | emojiImageFn: EmojiPropTypes.emojiImageFn, 48 | emojisToShowFilter: PropTypes.func, 49 | useLocalImages: EmojiPropTypes.useLocalImages, 50 | showSkinTones: PropTypes.bool, 51 | showAnchors: PropTypes.bool, 52 | showCloseButton: PropTypes.bool, 53 | emojiTooltip: EmojiPropTypes.tooltip, 54 | theme: PropTypes.oneOf(['auto', 'light', 'dark']), 55 | include: PropTypes.arrayOf(PropTypes.string), 56 | exclude: PropTypes.arrayOf(PropTypes.string), 57 | recent: PropTypes.arrayOf(PropTypes.string), 58 | autoFocus: PropTypes.bool, 59 | enableFrequentEmojiSort: PropTypes.bool, 60 | custom: PropTypes.arrayOf( 61 | PropTypes.shape({ 62 | name: PropTypes.string.isRequired, 63 | short_names: PropTypes.arrayOf(PropTypes.string).isRequired, 64 | emoticons: PropTypes.arrayOf(PropTypes.string), 65 | keywords: PropTypes.arrayOf(PropTypes.string), 66 | image: PropTypes.oneOfType([ 67 | PropTypes.shape({ 68 | uri: PropTypes.string, 69 | }), 70 | PropTypes.number, 71 | ]), 72 | spriteSheet: PropTypes.string, 73 | sheet_x: PropTypes.number, 74 | sheet_y: PropTypes.number, 75 | size: PropTypes.number, 76 | sheetColumns: PropTypes.number, 77 | sheetRows: PropTypes.number, 78 | }), 79 | ), 80 | skinEmoji: PropTypes.string, 81 | skinEmojiSize: PropTypes.number, 82 | notFound: PropTypes.func, 83 | notFoundEmoji: PropTypes.string, 84 | categoryEmojis: PropTypes.objectOf(PropTypes.string), 85 | fontSize: PropTypes.number, 86 | } 87 | 88 | export {EmojiPropTypes, PickerPropTypes} 89 | -------------------------------------------------------------------------------- /scripts/local-images/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | emojiLib = require('emojilib'), 3 | inflection = require('inflection'), 4 | mkdirp = require('mkdirp') 5 | 6 | var {compress} = require('../../dist/utils/data') 7 | var {unifiedToNative} = require('../../dist/utils') 8 | 9 | var sets = ['apple', 'facebook', 'google', 'twitter'] 10 | 11 | module.exports = (options) => { 12 | delete require.cache[require.resolve('emoji-datasource')] 13 | var emojiData = require('emoji-datasource') 14 | 15 | var data = {compressed: true, emojis: {}} 16 | 17 | emojiData.sort((a, b) => { 18 | var aTest = a.sort_order || a.short_name, 19 | bTest = b.sort_order || b.short_name 20 | 21 | return aTest - bTest 22 | }) 23 | 24 | emojiData.forEach((datum) => { 25 | var keywords = [] 26 | 27 | var localImageSets = options.sets || sets 28 | 29 | // Local images 30 | datum.localImages = {} 31 | localImageSets.forEach((set) => { 32 | var key = `has_img_${set}` 33 | if (datum[key]) { 34 | datum.localImages[set] = [ 35 | `require('../../../emoji-datasource-${set}/img/${set}/64/${ 36 | datum.image 37 | }')`, 38 | ] 39 | 40 | // Skin variations 41 | if (datum.skin_variations) { 42 | for (let skinKey in datum.skin_variations) { 43 | var skinVariations = datum.skin_variations[skinKey] 44 | if (skinVariations[key]) 45 | datum.localImages[set].push( 46 | `require('../../../emoji-datasource-${set}/img/${set}/64/${ 47 | skinVariations.image 48 | }')`, 49 | ) 50 | } 51 | } 52 | } 53 | }) 54 | 55 | if (options.sets) { 56 | var keepEmoji = false 57 | 58 | options.sets.forEach((set) => { 59 | if (keepEmoji) return 60 | 61 | if (datum[`has_img_${set}`]) { 62 | keepEmoji = true 63 | } 64 | }) 65 | 66 | if (!keepEmoji) { 67 | return 68 | } 69 | 70 | sets.forEach((set) => { 71 | if (options.sets.length == 1 || options.sets.indexOf(set) == -1) { 72 | var key = `has_img_${set}` 73 | delete datum[key] 74 | } 75 | }) 76 | } else { 77 | } 78 | 79 | datum.name || (datum.name = datum.short_name.replace(/\-/g, ' ')) 80 | datum.name = inflection.titleize(datum.name || '') 81 | 82 | if (!datum.name) { 83 | throw new Error('“' + datum.short_name + '” doesn’t have a name') 84 | } 85 | 86 | datum.emoticons = datum.texts || [] 87 | datum.text = datum.text || '' 88 | delete datum.texts 89 | 90 | var nativeEmoji = unifiedToNative(datum.unified) 91 | 92 | if (emojiLib[nativeEmoji]) { 93 | datum.keywords = emojiLib[nativeEmoji] 94 | } 95 | 96 | data.emojis[datum.short_name] = datum 97 | 98 | delete datum.docomo 99 | delete datum.au 100 | delete datum.softbank 101 | delete datum.google 102 | delete datum.category 103 | delete datum.subcategory 104 | delete datum.sort_order 105 | 106 | compress(datum) 107 | }) 108 | 109 | var stingified = JSON.stringify(data) 110 | .replace(/\"([A-Za-z_]+)\":/g, '$1:') 111 | .replace(/(["'])require(?:(?=(\\?))\2.)*?\1/g, (value) => 112 | value.replace(/"/g, ''), 113 | ) 114 | 115 | fs.writeFile(options.output, `export default ${stingified}`, (err) => { 116 | if (err) throw err 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@tuno.tech. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/components/skins.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {StyleSheet, View, TouchableWithoutFeedback} from 'react-native' 4 | 5 | const styles = StyleSheet.create({ 6 | skinSwatches: { 7 | paddingTop: 2, 8 | paddingBottom: 2, 9 | borderWidth: 1, 10 | borderRadius: 14, 11 | flexDirection: 'row', 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | }, 15 | skinSwatchesLight: { 16 | borderColor: '#d9d9d9', 17 | }, 18 | skinSwatchesDark: { 19 | borderColor: '#3f3f3f', 20 | }, 21 | skinSwatch: { 22 | paddingLeft: 2, 23 | paddingRight: 2, 24 | }, 25 | skin: { 26 | flexDirection: 'row', 27 | alignItems: 'center', 28 | justifyContent: 'center', 29 | }, 30 | skinSelected: { 31 | backgroundColor: 'rgba(255, 255, 255, 0.75)', 32 | }, 33 | skinTone1: { 34 | backgroundColor: '#ffc93a', 35 | }, 36 | skinTone2: { 37 | backgroundColor: '#fadcbc', 38 | }, 39 | skinTone3: { 40 | backgroundColor: '#e0bb95', 41 | }, 42 | skinTone4: { 43 | backgroundColor: '#bf8f68', 44 | }, 45 | skinTone5: { 46 | backgroundColor: '#9b643d', 47 | }, 48 | skinTone6: { 49 | backgroundColor: '#594539', 50 | }, 51 | }) 52 | 53 | export default class Skins extends React.PureComponent { 54 | constructor(props) { 55 | super(props) 56 | 57 | this.state = { 58 | opened: false, 59 | } 60 | } 61 | 62 | handlePress(skin) { 63 | var {onChange} = this.props 64 | 65 | if (!this.state.opened) { 66 | this.setState({opened: true}) 67 | } else { 68 | this.setState({opened: false}) 69 | if (skin != this.props.skin) { 70 | onChange(skin) 71 | } 72 | } 73 | } 74 | 75 | render() { 76 | const {skin, theme, iconSize} = this.props 77 | const {opened} = this.state 78 | 79 | const skinToneNodes = [] 80 | 81 | const skinSize = Math.round(iconSize * 0.6666666666666666) 82 | const skinSelectedSize = skinSize / 2 83 | 84 | for (let skinTone = 1; skinTone <= 6; skinTone++) { 85 | const selected = skinTone === skin 86 | 87 | if (selected || opened) { 88 | skinToneNodes.push( 89 | 90 | 94 | 105 | {selected && opened ? ( 106 | 116 | ) : null} 117 | 118 | 119 | , 120 | ) 121 | } 122 | } 123 | 124 | return ( 125 | 133 | {skinToneNodes} 134 | 135 | ) 136 | } 137 | } 138 | 139 | Skins.propTypes /* remove-proptypes */ = { 140 | onChange: PropTypes.func, 141 | skin: PropTypes.number.isRequired, 142 | theme: PropTypes.oneOf(['light', 'dark']), 143 | } 144 | 145 | Skins.defaultProps = { 146 | onChange: () => {}, 147 | theme: 'light', 148 | } 149 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | emojiLib = require('emojilib'), 3 | inflection = require('inflection'), 4 | mkdirp = require('mkdirp') 5 | 6 | var {compress} = require('../dist/utils/data') 7 | var {unifiedToNative} = require('../dist/utils') 8 | 9 | var categories = [ 10 | ['Smileys & Emotion', 'smileys'], 11 | ['People & Body', 'people'], 12 | ['Animals & Nature', 'nature'], 13 | ['Food & Drink', 'foods'], 14 | ['Activities', 'activity'], 15 | ['Travel & Places', 'places'], 16 | ['Objects', 'objects'], 17 | ['Symbols', 'symbols'], 18 | ['Flags', 'flags'], 19 | ] 20 | 21 | var sets = ['apple', 'facebook', 'google', 'twitter'] 22 | 23 | module.exports = (options) => { 24 | delete require.cache[require.resolve('emoji-datasource')] 25 | var emojiData = require('emoji-datasource') 26 | 27 | var data = {compressed: true, categories: [], emojis: {}, aliases: {}}, 28 | categoriesIndex = {} 29 | 30 | categories.forEach((category, i) => { 31 | let [name, id] = category 32 | data.categories[i] = {id: id, name: name, emojis: []} 33 | categoriesIndex[name] = i 34 | }) 35 | 36 | emojiData.sort((a, b) => { 37 | var aTest = a.sort_order || a.short_name, 38 | bTest = b.sort_order || b.short_name 39 | 40 | return aTest - bTest 41 | }) 42 | 43 | emojiData.forEach((datum) => { 44 | var category = datum.category, 45 | keywords = [], 46 | categoryIndex 47 | 48 | if (!datum.category) { 49 | throw new Error('“' + datum.short_name + '” doesn’t have a category') 50 | } 51 | 52 | if (options.sets) { 53 | var keepEmoji = false 54 | 55 | options.sets.forEach((set) => { 56 | if (keepEmoji) return 57 | if (datum[`has_img_${set}`]) { 58 | keepEmoji = true 59 | } 60 | }) 61 | 62 | if (!keepEmoji) { 63 | return 64 | } 65 | 66 | sets.forEach((set) => { 67 | if (options.sets.length == 1 || options.sets.indexOf(set) == -1) { 68 | var key = `has_img_${set}` 69 | delete datum[key] 70 | } 71 | }) 72 | } 73 | 74 | datum.name || (datum.name = datum.short_name.replace(/\-/g, ' ')) 75 | datum.name = inflection.titleize(datum.name || '') 76 | 77 | if (!datum.name) { 78 | throw new Error('“' + datum.short_name + '” doesn’t have a name') 79 | } 80 | 81 | datum.emoticons = datum.texts || [] 82 | datum.text = datum.text || '' 83 | delete datum.texts 84 | 85 | var nativeEmoji = unifiedToNative(datum.unified) 86 | 87 | if (emojiLib[nativeEmoji]) { 88 | datum.keywords = emojiLib[nativeEmoji] 89 | } 90 | 91 | if (datum.category != 'Component') { 92 | categoryIndex = categoriesIndex[category] 93 | data.categories[categoryIndex].emojis.push(datum.short_name) 94 | data.emojis[datum.short_name] = datum 95 | } 96 | 97 | datum.short_names.forEach((short_name, i) => { 98 | if (i == 0) { 99 | return 100 | } 101 | 102 | data.aliases[short_name] = datum.short_name 103 | }) 104 | 105 | delete datum.docomo 106 | delete datum.au 107 | delete datum.softbank 108 | delete datum.google 109 | delete datum.image 110 | delete datum.category 111 | delete datum.subcategory 112 | delete datum.sort_order 113 | 114 | compress(datum) 115 | }) 116 | 117 | var flags = data.categories[categoriesIndex['Flags']] 118 | flags.emojis = flags.emojis 119 | .filter((flag) => { 120 | // Until browsers support Flag UN 121 | if (flag == 'flag-un') return 122 | return true 123 | }) 124 | .sort() 125 | 126 | // Merge “Smileys & Emotion” and “People & Body” into a single category 127 | let smileys = data.categories[0] 128 | let people = data.categories[1] 129 | let smileysAndPeople = {id: 'people', name: 'Smileys & People'} 130 | smileysAndPeople.emojis = [] 131 | .concat(smileys.emojis.slice(0, 114)) 132 | .concat(people.emojis) 133 | .concat(smileys.emojis.slice(114)) 134 | 135 | data.categories.unshift(smileysAndPeople) 136 | data.categories.splice(1, 2) 137 | 138 | fs.writeFile(options.output, JSON.stringify(data), (err) => { 139 | if (err) throw err 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 |

Sponsors & Backers

2 | 3 | The ongoing development of Emoji Mart Native is made possible entirely by the support of these awesome [backers](https://github.com/tunoltd/emoji-mart-native/blob/master/BACKERS.md). If you'd like to join them, please consider becoming a [backer or sponsor on GitHub](https://github.com/sponsors/pederjohnsen). 4 | 5 |

6 | 7 |

Platinum Sponsors

8 | 9 | 10 | 36 | 37 | 38 |

Gold Sponsors

39 | 40 | 41 | 77 | 78 | 79 |

Silver Sponsors

80 | 81 | 82 | 118 | 119 | 120 | 121 |

Bronze Sponsors

122 | 123 | 124 | 160 | 161 | 162 |

Top Supporter

163 | 164 | 165 | 168 | 169 | 170 |

Supporter

171 | 172 | 173 | 176 | 177 | -------------------------------------------------------------------------------- /src/components/anchors.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | StyleSheet, 5 | View, 6 | TouchableWithoutFeedback, 7 | ScrollView, 8 | } from 'react-native' 9 | 10 | import NimbleEmoji from './emoji/nimble-emoji' 11 | 12 | const styles = StyleSheet.create({ 13 | anchors: { 14 | borderTopWidth: 1, 15 | flexDirection: 'row', 16 | justifyContent: 'space-between', 17 | }, 18 | anchorsLight: { 19 | borderTopColor: '#f6f7f8', 20 | backgroundColor: '#e4e7e9', 21 | }, 22 | anchorsDark: { 23 | borderTopColor: '#090807', 24 | backgroundColor: '#1b1816', 25 | }, 26 | anchor: { 27 | flex: 1, 28 | paddingTop: 12.5, 29 | paddingBottom: 12.5, 30 | paddingLeft: 18, 31 | paddingRight: 18, 32 | overflow: 'hidden', 33 | }, 34 | anchorBar: { 35 | position: 'absolute', 36 | bottom: -2, 37 | left: 0, 38 | right: 0, 39 | height: 2, 40 | }, 41 | anchorBarSelected: { 42 | bottom: 0, 43 | }, 44 | }) 45 | 46 | export default class Anchors extends React.PureComponent { 47 | constructor(props) { 48 | super(props) 49 | 50 | let defaultCategory = props.categories.filter( 51 | (category) => category.first, 52 | )[0] 53 | 54 | this.data = props.data 55 | this.state = { 56 | selected: defaultCategory.name, 57 | } 58 | this.setScrollViewRef = this.setScrollViewRef.bind(this) 59 | } 60 | 61 | componentDidMount() { 62 | this.anchorsOffset = {} 63 | this.anchorsWidth = {} 64 | } 65 | 66 | onSelectAnchor(categoryName) { 67 | this.setState({selected: categoryName}, () => { 68 | const {selected} = this.state 69 | let contentOffset = 0 70 | 71 | if (this.clientWidth) { 72 | const anchorOffset = this.anchorsOffset[selected] 73 | const anchorWidth = this.anchorsWidth[selected] 74 | const anchorHalfWidth = anchorWidth / 2 75 | 76 | const clientCenter = this.clientWidth / 2 77 | const scrollStart = clientCenter - anchorHalfWidth 78 | 79 | if (anchorOffset > scrollStart) { 80 | contentOffset = anchorOffset - scrollStart 81 | } 82 | } 83 | this.scrollView.scrollTo({x: contentOffset, animated: true}) 84 | }) 85 | } 86 | 87 | handlePress(index) { 88 | var {categories, onAnchorPress} = this.props 89 | 90 | onAnchorPress(categories[index], index) 91 | } 92 | 93 | setScrollViewRef(c) { 94 | this.scrollView = c 95 | } 96 | 97 | onAnchorsScrollViewLayout = (event) => { 98 | this.clientWidth = event.nativeEvent.layout.width 99 | } 100 | 101 | onAnchorLayout = (index, event) => { 102 | var {categories} = this.props 103 | const {x: left, width} = event.nativeEvent.layout 104 | 105 | const category = categories[index] 106 | 107 | this.anchorsOffset[category.name] = left 108 | this.anchorsWidth[category.name] = width 109 | } 110 | 111 | render() { 112 | var { 113 | categories, 114 | color, 115 | i18n, 116 | emojiProps, 117 | categoryEmojis, 118 | theme, 119 | } = this.props, 120 | {selected} = this.state 121 | 122 | return ( 123 | 130 | 136 | {categories.map((category, i) => { 137 | var {id, name, anchor} = category, 138 | isSelected = name == selected 139 | 140 | if (anchor === false) { 141 | return null 142 | } 143 | 144 | const categoryEmojiId = id.startsWith('custom-') ? 'custom' : id 145 | 146 | return ( 147 | 153 | 159 | 165 | 172 | 173 | 174 | ) 175 | })} 176 | 177 | 178 | ) 179 | } 180 | } 181 | 182 | Anchors.propTypes /* remove-proptypes */ = { 183 | categories: PropTypes.array, 184 | onAnchorPress: PropTypes.func, 185 | emojiProps: PropTypes.object.isRequired, 186 | categoryEmojis: PropTypes.object.isRequired, 187 | theme: PropTypes.oneOf(['light', 'dark']), 188 | } 189 | 190 | Anchors.defaultProps = { 191 | categories: [], 192 | onAnchorPress: () => {}, 193 | theme: 'light', 194 | } 195 | -------------------------------------------------------------------------------- /src/components/emoji/nimble-emoji.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | Platform, 5 | StyleSheet, 6 | View, 7 | Text, 8 | Image, 9 | TouchableWithoutFeedback, 10 | } from 'react-native' 11 | 12 | import {getData, getSanitizedData, unifiedToNative} from '../../utils' 13 | import {uncompress} from '../../utils/data' 14 | import {EmojiPropTypes} from '../../utils/shared-props' 15 | import {EmojiDefaultProps} from '../../utils/shared-default-props' 16 | 17 | const styles = StyleSheet.create({ 18 | emojiWrapper: { 19 | position: 'relative', 20 | overflow: 'hidden', 21 | }, 22 | labelStyle: { 23 | textAlign: 'center', 24 | color: '#ae65c5', 25 | }, 26 | }) 27 | 28 | // TODO: Use functional components? 29 | // const NimbleEmoji = (props) => { 30 | class NimbleEmoji extends React.PureComponent { 31 | static propTypes /* remove-proptypes */ = { 32 | ...EmojiPropTypes, 33 | data: PropTypes.object.isRequired, 34 | } 35 | static defaultProps = EmojiDefaultProps 36 | 37 | _getData = (props) => { 38 | const {emoji, skin, set, data} = props 39 | return getData(emoji, skin, set, data) 40 | } 41 | 42 | _getPosition = (props) => { 43 | const {sheet_x, sheet_y} = this._getData(props) 44 | 45 | return { 46 | x: `-${sheet_x * 100}%`, 47 | y: `-${sheet_y * 100}%`, 48 | } 49 | } 50 | 51 | _getSanitizedData = (props) => { 52 | const {emoji, skin, set, data} = props 53 | return getSanitizedData(emoji, skin, set, data) 54 | } 55 | 56 | _handlePress = (e) => { 57 | const {onPress} = this.props 58 | if (!onPress) { 59 | return 60 | } 61 | const emoji = this._getSanitizedData(this.props) 62 | 63 | onPress(emoji, e) 64 | } 65 | 66 | _handleLongPress = (e) => { 67 | const {onLongPress} = this.props 68 | if (!onLongPress) { 69 | return 70 | } 71 | const emoji = this._getSanitizedData(this.props) 72 | 73 | onLongPress(emoji, e) 74 | } 75 | 76 | render() { 77 | if (this.props.data.compressed) { 78 | uncompress(this.props.data) 79 | } 80 | 81 | for (let k in NimbleEmoji.defaultProps) { 82 | if ( 83 | this.props[k] === undefined && 84 | NimbleEmoji.defaultProps[k] != undefined 85 | ) { 86 | this.props[k] = NimbleEmoji.defaultProps[k] 87 | } 88 | } 89 | 90 | let data = this._getData(this.props) 91 | if (!data) { 92 | if (this.props.fallback) { 93 | return this.props.fallback(null, this.props) 94 | } else { 95 | return null 96 | } 97 | } 98 | 99 | let {unified, custom, short_names, image} = data, 100 | style = {}, 101 | imageStyle = {}, 102 | labelStyle = {}, 103 | children = this.props.children, 104 | emojiImage, 105 | emojiImageSource 106 | 107 | if (!unified && !custom) { 108 | if (this.props.fallback) { 109 | return this.props.fallback(data, this.props) 110 | } else { 111 | return null 112 | } 113 | } 114 | 115 | if (this.props.native && unified) { 116 | const fontSize = this.props.size 117 | labelStyle = {fontSize} 118 | children = unifiedToNative(unified) 119 | style.width = this.props.size + this.props.margin 120 | style.height = this.props.size + this.props.margin 121 | } else if (custom) { 122 | style = { 123 | width: this.props.size, 124 | height: this.props.size, 125 | margin: this.props.noMargin ? 0 : this.props.margin / 2, 126 | } 127 | 128 | if (data.spriteSheet) { 129 | const emojiPosition = this._getPosition(this.props) 130 | 131 | imageStyle = { 132 | position: 'absolute', 133 | top: emojiPosition.y, 134 | left: emojiPosition.x, 135 | width: `${100 * this.props.sheetColumns}%`, 136 | height: `${100 * this.props.sheetRows}%`, 137 | } 138 | 139 | emojiImage = 140 | } else { 141 | imageStyle = { 142 | width: this.props.size, 143 | height: this.props.size, 144 | } 145 | 146 | emojiImage = ( 147 | 148 | ) 149 | } 150 | } else { 151 | const setHasEmoji = 152 | data[`has_img_${this.props.set}`] == undefined || 153 | data[`has_img_${this.props.set}`] 154 | 155 | if (!setHasEmoji) { 156 | if (this.props.fallback) { 157 | return this.props.fallback(data, this.props) 158 | } else { 159 | return null 160 | } 161 | } 162 | 163 | style = { 164 | width: this.props.size, 165 | height: this.props.size, 166 | margin: this.props.noMargin ? 0 : this.props.margin / 2, 167 | } 168 | 169 | const {useLocalImages} = this.props 170 | const emoji = this._getSanitizedData(this.props) 171 | 172 | if (useLocalImages && useLocalImages[emoji.id]) { 173 | imageStyle = { 174 | width: this.props.size, 175 | height: this.props.size, 176 | } 177 | 178 | emojiImageSource = 179 | useLocalImages[emoji.id].localImages[this.props.set][ 180 | (emoji.skin || NimbleEmoji.defaultProps.skin) - 1 181 | ] 182 | } else { 183 | const emojiPosition = this._getPosition(this.props) 184 | 185 | imageStyle = { 186 | position: 'absolute', 187 | top: emojiPosition.y, 188 | left: emojiPosition.x, 189 | width: `${100 * this.props.sheetColumns}%`, 190 | height: `${100 * this.props.sheetRows}%`, 191 | } 192 | 193 | emojiImageSource = this.props.spriteSheetFn( 194 | this.props.set, 195 | this.props.sheetSize, 196 | ) 197 | } 198 | 199 | emojiImage = 200 | } 201 | 202 | const emojiComponent = ( 203 | 204 | {emojiImage || ( 205 | {children} 206 | )} 207 | 208 | ) 209 | 210 | return this.props.onPress || this.props.onLongPress ? ( 211 | 215 | {emojiComponent} 216 | 217 | ) : ( 218 | emojiComponent 219 | ) 220 | } 221 | } 222 | 223 | export default NimbleEmoji 224 | -------------------------------------------------------------------------------- /src/utils/emoji-index/nimble-emoji-index.js: -------------------------------------------------------------------------------- 1 | import {getData, getSanitizedData, intersect} from '../' 2 | import {uncompress} from '../data' 3 | import store from '../store' 4 | 5 | export default class NimbleEmojiIndex { 6 | constructor(data, set) { 7 | if (data.compressed) { 8 | uncompress(data) 9 | } 10 | 11 | this.data = data || {} 12 | this.set = set || null 13 | this.originalPool = {} 14 | this.index = {} 15 | this.emojis = {} 16 | this.emoticons = {} 17 | this.customEmojisList = [] 18 | 19 | this.buildIndex() 20 | } 21 | 22 | buildIndex() { 23 | for (let emoji in this.data.emojis) { 24 | let emojiData = this.data.emojis[emoji], 25 | {short_names, emoticons, skin_variations} = emojiData, 26 | id = short_names[0] 27 | 28 | if (emoticons) { 29 | emoticons.forEach((emoticon) => { 30 | if (this.emoticons[emoticon]) { 31 | return 32 | } 33 | 34 | this.emoticons[emoticon] = id 35 | }) 36 | } 37 | 38 | // If skin variations include them 39 | if (skin_variations) { 40 | this.emojis[id] = {} 41 | for (let skinTone = 1; skinTone <= 6; skinTone++) { 42 | this.emojis[id][skinTone] = getSanitizedData( 43 | {id, skin: skinTone}, 44 | skinTone, 45 | this.set, 46 | this.data, 47 | ) 48 | } 49 | } else { 50 | this.emojis[id] = getSanitizedData(id, null, this.set, this.data) 51 | } 52 | 53 | this.originalPool[id] = emojiData 54 | } 55 | } 56 | 57 | clearCustomEmojis(pool) { 58 | this.customEmojisList.forEach((emoji) => { 59 | let emojiId = emoji.id || emoji.short_names[0] 60 | 61 | delete pool[emojiId] 62 | delete this.emojis[emojiId] 63 | }) 64 | } 65 | 66 | addCustomToPool(custom, pool) { 67 | if (this.customEmojisList.length) this.clearCustomEmojis(pool) 68 | 69 | custom.forEach((emoji) => { 70 | let emojiId = emoji.id || emoji.short_names[0] 71 | 72 | if (emojiId && !pool[emojiId]) { 73 | pool[emojiId] = getData(emoji, null, null, this.data) 74 | this.emojis[emojiId] = getSanitizedData(emoji, null, null, this.data) 75 | } 76 | }) 77 | 78 | this.customEmojisList = custom 79 | this.index = {} 80 | } 81 | 82 | search( 83 | value, 84 | {emojisToShowFilter, maxResults, include, exclude, custom = []} = {}, 85 | ) { 86 | if (this.customEmojisList != custom) 87 | this.addCustomToPool(custom, this.originalPool) 88 | 89 | const skinTone = store.get('skin') || 1 90 | 91 | maxResults || (maxResults = 75) 92 | include || (include = []) 93 | exclude || (exclude = []) 94 | 95 | var results = null, 96 | pool = this.originalPool 97 | 98 | if (value.length) { 99 | if (value == '-' || value == '-1') { 100 | return [this.emojis['-1'][skinTone]] 101 | } 102 | 103 | var values = value.toLowerCase().split(/[\s|,|\-|_]+/), 104 | allResults = [] 105 | 106 | if (values.length > 2) { 107 | values = [values[0], values[1]] 108 | } 109 | 110 | if (include.length || exclude.length) { 111 | pool = {} 112 | 113 | this.data.categories.forEach((category) => { 114 | let isIncluded = 115 | include && include.length ? include.indexOf(category.id) > -1 : true 116 | let isExcluded = 117 | exclude && exclude.length 118 | ? exclude.indexOf(category.id) > -1 119 | : false 120 | if (!isIncluded || isExcluded) { 121 | return 122 | } 123 | 124 | category.emojis.forEach( 125 | (emojiId) => (pool[emojiId] = this.data.emojis[emojiId]), 126 | ) 127 | }) 128 | 129 | if (custom.length) { 130 | let customIsIncluded = 131 | include && include.length ? include.indexOf('custom') > -1 : true 132 | let customIsExcluded = 133 | exclude && exclude.length ? exclude.indexOf('custom') > -1 : false 134 | if (customIsIncluded && !customIsExcluded) { 135 | this.addCustomToPool(custom, pool) 136 | } 137 | } 138 | } 139 | 140 | allResults = values 141 | .map((value) => { 142 | var aPool = pool, 143 | aIndex = this.index, 144 | length = 0 145 | 146 | for (let charIndex = 0; charIndex < value.length; charIndex++) { 147 | const char = value[charIndex] 148 | length++ 149 | 150 | aIndex[char] || (aIndex[char] = {}) 151 | aIndex = aIndex[char] 152 | 153 | if (!aIndex.results) { 154 | let scores = {} 155 | 156 | aIndex.results = [] 157 | aIndex.pool = {} 158 | 159 | for (let id in aPool) { 160 | let emoji = aPool[id], 161 | {search} = emoji, 162 | sub = value.substr(0, length), 163 | subIndex = search.indexOf(sub) 164 | 165 | if (subIndex != -1) { 166 | let score = subIndex + 1 167 | if (sub == id) score = 0 168 | 169 | if (this.emojis[id] && this.emojis[id][skinTone]) { 170 | aIndex.results.push(this.emojis[id][skinTone]) 171 | } else { 172 | aIndex.results.push(this.emojis[id]) 173 | } 174 | aIndex.pool[id] = emoji 175 | 176 | scores[id] = score 177 | } 178 | } 179 | 180 | aIndex.results.sort((a, b) => { 181 | var aScore = scores[a.id], 182 | bScore = scores[b.id] 183 | 184 | if (aScore == bScore) { 185 | return a.id.localeCompare(b.id) 186 | } else { 187 | return aScore - bScore 188 | } 189 | }) 190 | } 191 | 192 | aPool = aIndex.pool 193 | } 194 | 195 | return aIndex.results 196 | }) 197 | .filter((a) => a) 198 | 199 | if (allResults.length > 1) { 200 | results = intersect.apply(null, allResults) 201 | } else if (allResults.length) { 202 | results = allResults[0] 203 | } else { 204 | results = [] 205 | } 206 | } 207 | 208 | if (results) { 209 | if (emojisToShowFilter) { 210 | results = results.filter((result) => 211 | emojisToShowFilter(pool[result.id]), 212 | ) 213 | } 214 | 215 | if (results && results.length > maxResults) { 216 | results = results.slice(0, maxResults) 217 | } 218 | } 219 | 220 | return results 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/components/search.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | Platform, 5 | StyleSheet, 6 | View, 7 | TextInput, 8 | TouchableNativeFeedback, 9 | Image, 10 | } from 'react-native' 11 | 12 | import NimbleEmojiIndex from '../utils/emoji-index/nimble-emoji-index' 13 | 14 | import Skins from './skins' 15 | import SkinsEmoji from './skins-emoji' 16 | import Touchable from './common/touchable' 17 | 18 | const arrowBackIconLight = require('../assets/arrow-back.png') 19 | const arrowBackIconDark = require('../assets/arrow-back-dark.png') 20 | const clearIconLight = require('../assets/clear-icon.png') 21 | const clearIconDark = require('../assets/clear-icon-dark.png') 22 | 23 | const styles = StyleSheet.create({ 24 | searchContainer: { 25 | paddingLeft: 10, 26 | paddingRight: 10, 27 | paddingTop: 2, 28 | paddingBottom: 2, 29 | minHeight: 52, 30 | flexDirection: 'row', 31 | justifyContent: 'center', 32 | alignItems: 'center', 33 | borderBottomWidth: 1, 34 | }, 35 | searchContainerLight: { 36 | backgroundColor: '#eceff1', 37 | borderBottomColor: '#e0e0e0', 38 | }, 39 | searchContainerDark: { 40 | backgroundColor: '#13100e', 41 | borderBottomColor: '#1f1f1f', 42 | }, 43 | searchInput: { 44 | flex: 1, 45 | }, 46 | searchInputLight: { 47 | color: '#414141', 48 | }, 49 | searchInputDark: { 50 | color: '#bebebe', 51 | }, 52 | closeButton: { 53 | borderRadius: 500, 54 | margin: 10, 55 | padding: 2, 56 | }, 57 | closeButtonIcon: { 58 | marginTop: 2, 59 | marginLeft: 2, 60 | height: 24, 61 | width: 24, 62 | }, 63 | }) 64 | 65 | export default class Search extends React.PureComponent { 66 | static propTypes /* remove-proptypes */ = { 67 | onSearch: PropTypes.func, 68 | onPressClose: PropTypes.func, 69 | maxResults: PropTypes.number, 70 | emojisToShowFilter: PropTypes.func, 71 | autoFocus: PropTypes.bool, 72 | showSkinTones: PropTypes.bool, 73 | skinsProps: PropTypes.object.isRequired, 74 | emojiProps: PropTypes.object.isRequired, 75 | theme: PropTypes.oneOf(['light', 'dark']), 76 | fontSize: PropTypes.number, 77 | } 78 | 79 | static defaultProps = { 80 | onSearch: () => {}, 81 | onPressClose: () => {}, 82 | maxResults: 75, 83 | emojisToShowFilter: null, 84 | autoFocus: false, 85 | showSkinTones: true, 86 | theme: 'light', 87 | fontSize: 15, 88 | } 89 | 90 | constructor(props) { 91 | super(props) 92 | this.state = { 93 | searchTerm: '', 94 | } 95 | 96 | this.data = props.data 97 | this.emojiIndex = new NimbleEmojiIndex(this.data) 98 | this.setRef = this.setRef.bind(this) 99 | this.handleChange = this.handleChange.bind(this) 100 | this.pressCancel = this.pressCancel.bind(this) 101 | } 102 | 103 | handleChange(value) { 104 | this.setState({ 105 | searchTerm: value, 106 | }) 107 | 108 | this.props.onSearch( 109 | this.emojiIndex.search(value, { 110 | emojisToShowFilter: this.props.emojisToShowFilter, 111 | maxResults: this.props.maxResults, 112 | include: this.props.include, 113 | exclude: this.props.exclude, 114 | custom: this.props.custom, 115 | }), 116 | ) 117 | } 118 | 119 | pressCancel() { 120 | this.props.onSearch(null) 121 | this.clear() 122 | } 123 | 124 | setRef(c) { 125 | this.input = c 126 | } 127 | 128 | clear() { 129 | this.setState({ 130 | searchTerm: '', 131 | }) 132 | } 133 | 134 | render() { 135 | const { 136 | i18n, 137 | autoFocus, 138 | onPressClose, 139 | skinsProps, 140 | showSkinTones, 141 | showCloseButton, 142 | emojiProps, 143 | theme, 144 | fontSize, 145 | } = this.props 146 | const iconSize = Math.round(fontSize * 1.6) 147 | const {searchTerm} = this.state 148 | 149 | let background 150 | 151 | if (Platform.OS === 'android') { 152 | if (Platform.Version >= 21) { 153 | background = TouchableNativeFeedback.SelectableBackgroundBorderless() 154 | } else { 155 | background = TouchableNativeFeedback.SelectableBackground() 156 | } 157 | } 158 | 159 | const searchContainerWithCloseButtonStyle = { 160 | paddingLeft: 5, 161 | } 162 | 163 | return ( 164 | 173 | {showCloseButton ? ( 174 | 179 | 188 | 189 | ) : null} 190 | 206 | {searchTerm.length > 0 ? ( 207 | 212 | 219 | 220 | ) : null} 221 | {showSkinTones && ( 222 | 223 | {skinsProps.skinEmoji ? ( 224 | 229 | ) : ( 230 | 231 | )} 232 | 233 | )} 234 | 235 | ) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import {buildSearch} from './data' 2 | import stringFromCodePoint from '../polyfills/stringFromCodePoint' 3 | import {uncompress} from './data' 4 | import NimbleEmojiIndex from './emoji-index/nimble-emoji-index' 5 | 6 | const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/ 7 | const SKINS = ['1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'] 8 | 9 | function unifiedToNative(unified) { 10 | var unicodes = unified.split('-'), 11 | codePoints = unicodes.map((u) => `0x${u}`) 12 | 13 | return stringFromCodePoint.apply(null, codePoints) 14 | } 15 | 16 | function sanitize(emoji) { 17 | var { 18 | name, 19 | short_names, 20 | skin_tone, 21 | skin_variations, 22 | emoticons, 23 | unified, 24 | custom, 25 | customCategory, 26 | image, 27 | } = emoji, 28 | id = emoji.id || short_names[0], 29 | colons = `:${id}:` 30 | 31 | if (custom) { 32 | return { 33 | id, 34 | name, 35 | short_names, 36 | colons, 37 | emoticons, 38 | custom, 39 | customCategory, 40 | image, 41 | } 42 | } 43 | 44 | if (skin_tone) { 45 | colons += `:skin-tone-${skin_tone}:` 46 | } 47 | 48 | return { 49 | id, 50 | name, 51 | short_names, 52 | colons, 53 | emoticons, 54 | unified: unified.toLowerCase(), 55 | skin: skin_tone || (skin_variations ? 1 : null), 56 | native: unifiedToNative(unified), 57 | } 58 | } 59 | 60 | function getSanitizedData() { 61 | return sanitize(getData(...arguments)) 62 | } 63 | 64 | function getData(emoji, skin, set, data) { 65 | var emojiData = {} 66 | 67 | if (typeof emoji == 'string') { 68 | let matches = emoji.match(COLONS_REGEX) 69 | 70 | if (matches) { 71 | emoji = matches[1] 72 | 73 | if (matches[2]) { 74 | skin = parseInt(matches[2], 10) 75 | } 76 | } 77 | 78 | if (data.aliases.hasOwnProperty(emoji)) { 79 | emoji = data.aliases[emoji] 80 | } 81 | 82 | if (data.emojis.hasOwnProperty(emoji)) { 83 | emojiData = data.emojis[emoji] 84 | } else { 85 | return null 86 | } 87 | } else if (emoji.id) { 88 | if (data.aliases.hasOwnProperty(emoji.id)) { 89 | emoji.id = data.aliases[emoji.id] 90 | } 91 | 92 | if (data.emojis.hasOwnProperty(emoji.id)) { 93 | emojiData = data.emojis[emoji.id] 94 | skin || (skin = emoji.skin) 95 | } 96 | } 97 | 98 | if (!Object.keys(emojiData).length) { 99 | emojiData = emoji 100 | emojiData.custom = true 101 | 102 | if (!emojiData.search) { 103 | emojiData.search = buildSearch(emoji) 104 | } 105 | } 106 | 107 | emojiData.emoticons || (emojiData.emoticons = []) 108 | emojiData.variations || (emojiData.variations = []) 109 | 110 | if (emojiData.skin_variations && skin > 1) { 111 | emojiData = JSON.parse(JSON.stringify(emojiData)) 112 | 113 | var skinKey = SKINS[skin - 1], 114 | variationData = emojiData.skin_variations[skinKey] 115 | 116 | if (variationData) { 117 | if (!variationData.variations && emojiData.variations) { 118 | delete emojiData.variations 119 | } 120 | 121 | if ( 122 | (set && 123 | (variationData[`has_img_${set}`] == undefined || 124 | variationData[`has_img_${set}`])) || 125 | !set 126 | ) { 127 | emojiData.skin_tone = skin 128 | 129 | for (let k in variationData) { 130 | let v = variationData[k] 131 | emojiData[k] = v 132 | } 133 | } 134 | } 135 | } 136 | 137 | if (emojiData.variations && emojiData.variations.length) { 138 | emojiData = JSON.parse(JSON.stringify(emojiData)) 139 | emojiData.unified = emojiData.variations.shift() 140 | } 141 | 142 | return emojiData 143 | } 144 | 145 | function getEmojiDataFromNative(nativeString, set, data) { 146 | if (data.compressed) { 147 | uncompress(data) 148 | } 149 | 150 | const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'] 151 | const skinCodes = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'] 152 | 153 | let skin 154 | let skinCode 155 | let baseNativeString = nativeString 156 | 157 | skinTones.forEach((skinTone, skinToneIndex) => { 158 | if (nativeString.indexOf(skinTone) > 0) { 159 | skin = skinToneIndex + 2 160 | skinCode = skinCodes[skinToneIndex] 161 | } 162 | }) 163 | 164 | let emojiData 165 | 166 | for (let id in data.emojis) { 167 | let emoji = data.emojis[id] 168 | 169 | let emojiUnified = emoji.unified 170 | 171 | if (emoji.variations && emoji.variations.length) { 172 | emojiUnified = emoji.variations.shift() 173 | } 174 | 175 | if (skin && emoji.skin_variations && emoji.skin_variations[skinCode]) { 176 | emojiUnified = emoji.skin_variations[skinCode].unified 177 | } 178 | 179 | if (unifiedToNative(emojiUnified) === baseNativeString) emojiData = emoji 180 | } 181 | 182 | if (!emojiData) { 183 | return null 184 | } 185 | 186 | emojiData.id = emojiData.short_names[0] 187 | 188 | return getSanitizedData(emojiData, skin, set, data) 189 | } 190 | 191 | function getEmojiDataFromCustom(emoji, custom, data) { 192 | if (data.compressed) { 193 | uncompress(data) 194 | } 195 | 196 | const customEmojis = custom.map((emoji) => { 197 | return { 198 | ...emoji, 199 | id: emoji.short_names[0], 200 | custom: true, 201 | } 202 | }) 203 | 204 | const emojiIndex = new NimbleEmojiIndex(data) 205 | const [customEmoji] = emojiIndex.search(emoji, { 206 | maxResults: 1, 207 | custom: customEmojis, 208 | }) 209 | 210 | return customEmoji 211 | } 212 | 213 | function uniq(arr) { 214 | return arr.reduce((acc, item) => { 215 | if (acc.indexOf(item) === -1) { 216 | acc.push(item) 217 | } 218 | return acc 219 | }, []) 220 | } 221 | 222 | function intersect(a, b) { 223 | const uniqA = uniq(a) 224 | const uniqB = uniq(b) 225 | 226 | return uniqA.filter((item) => uniqB.indexOf(item) >= 0) 227 | } 228 | 229 | function deepMerge(a, b) { 230 | var o = {} 231 | 232 | for (let key in a) { 233 | let originalValue = a[key], 234 | value = originalValue 235 | 236 | if (b.hasOwnProperty(key)) { 237 | value = b[key] 238 | } 239 | 240 | if (typeof value === 'object') { 241 | value = deepMerge(originalValue, value) 242 | } 243 | 244 | o[key] = value 245 | } 246 | 247 | return o 248 | } 249 | 250 | // https://github.com/lodash/lodash/blob/master/slice.js 251 | function slice(array, start, end) { 252 | let length = array == null ? 0 : array.length 253 | if (!length) { 254 | return [] 255 | } 256 | start = start == null ? 0 : start 257 | end = end === undefined ? length : end 258 | 259 | if (start < 0) { 260 | start = -start > length ? 0 : length + start 261 | } 262 | end = end > length ? length : end 263 | if (end < 0) { 264 | end += length 265 | } 266 | length = start > end ? 0 : (end - start) >>> 0 267 | start >>>= 0 268 | 269 | let index = -1 270 | const result = new Array(length) 271 | while (++index < length) { 272 | result[index] = array[index + start] 273 | } 274 | return result 275 | } 276 | 277 | // https://github.com/lodash/lodash/blob/master/chunk.js 278 | function chunk(array, size) { 279 | size = Math.max(size, 0) 280 | const length = array == null ? 0 : array.length 281 | if (!length || size < 1) { 282 | return [] 283 | } 284 | let index = 0 285 | let resIndex = 0 286 | const result = new Array(Math.ceil(length / size)) 287 | 288 | while (index < length) { 289 | result[resIndex++] = slice(array, index, (index += size)) 290 | } 291 | return result 292 | } 293 | 294 | export { 295 | getData, 296 | getEmojiDataFromNative, 297 | getEmojiDataFromCustom, 298 | getSanitizedData, 299 | uniq, 300 | intersect, 301 | deepMerge, 302 | unifiedToNative, 303 | slice, 304 | chunk, 305 | } 306 | -------------------------------------------------------------------------------- /src/components/category.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {StyleSheet, View, Text, ScrollView} from 'react-native' 4 | 5 | import frequently from '../utils/frequently' 6 | import {getData, getSanitizedData, chunk} from '../utils' 7 | 8 | import NimbleEmoji from './emoji/nimble-emoji' 9 | import NotFound from './not-found' 10 | 11 | const styles = StyleSheet.create({ 12 | emojisContainer: { 13 | flexDirection: 'row', 14 | justifyContent: 'flex-start', 15 | alignItems: 'flex-start', 16 | flexWrap: 'wrap', 17 | }, 18 | }) 19 | 20 | export default class Category extends React.Component { 21 | static propTypes /* remove-proptypes */ = { 22 | emojis: PropTypes.array, 23 | hasStickyPosition: PropTypes.bool, 24 | name: PropTypes.string.isRequired, 25 | native: PropTypes.bool.isRequired, 26 | perLine: PropTypes.number.isRequired, 27 | emojiProps: PropTypes.object.isRequired, 28 | recent: PropTypes.arrayOf(PropTypes.string), 29 | notFound: PropTypes.func, 30 | notFoundEmoji: PropTypes.string.isRequired, 31 | theme: PropTypes.oneOf(['light', 'dark']), 32 | fontSize: PropTypes.number, 33 | } 34 | 35 | static defaultProps = { 36 | emojis: [], 37 | hasStickyPosition: true, 38 | theme: 'light', 39 | fontSize: 15, 40 | } 41 | 42 | constructor(props) { 43 | super(props) 44 | 45 | this.data = props.data 46 | 47 | this.active = {} 48 | // Set first 2 pages to active 49 | if (props.name == 'Recent' || props.name == 'Smileys & People') { 50 | this.active['page-0'] = true 51 | } 52 | 53 | if (props.name == 'Search') { 54 | this.active['page-0'] = true 55 | this.active['page-1'] = true 56 | } 57 | 58 | this.state = { 59 | visible: true, 60 | } 61 | } 62 | 63 | componentDidMount() { 64 | this.minMargin = 0 65 | this.pagesOffsetLeft = {} 66 | this.maxMargin = {} 67 | } 68 | 69 | shouldComponentUpdate(nextProps, nextState) { 70 | var { 71 | name, 72 | perLine, 73 | native, 74 | hasStickyPosition, 75 | emojis, 76 | emojiProps, 77 | } = this.props, 78 | {skin, size, set} = emojiProps, 79 | { 80 | perLine: nextPerLine, 81 | native: nextNative, 82 | hasStickyPosition: nextHasStickyPosition, 83 | emojis: nextEmojis, 84 | emojiProps: nextEmojiProps, 85 | } = nextProps, 86 | {skin: nextSkin, size: nextSize, set: nextSet} = nextEmojiProps, 87 | shouldUpdate = false 88 | 89 | if (name == 'Recent' && perLine != nextPerLine) { 90 | shouldUpdate = true 91 | } 92 | 93 | if (name == 'Search') { 94 | shouldUpdate = !(emojis == nextEmojis) 95 | } 96 | 97 | if ( 98 | skin != nextSkin || 99 | size != nextSize || 100 | native != nextNative || 101 | set != nextSet || 102 | hasStickyPosition != nextHasStickyPosition 103 | ) { 104 | shouldUpdate = true 105 | } 106 | 107 | return shouldUpdate 108 | } 109 | 110 | getMaxMarginValue() { 111 | let maxMargin = this.left 112 | 113 | for (let key in this.maxMargin) { 114 | if (this.maxMargin.hasOwnProperty(key)) maxMargin += this.maxMargin[key] 115 | } 116 | 117 | return maxMargin 118 | } 119 | 120 | calculateVisibility(scrollLeft) { 121 | if ( 122 | this.pages && 123 | typeof this.left === 'number' && 124 | scrollLeft % this.width === 0 125 | ) { 126 | let {pagesToEagerLoad} = this.props 127 | for (let index in this.pages) { 128 | const page = parseInt(index) + 1 129 | const pageWidth = this.maxMargin[`page-${index}`] || 0 130 | const pageLeft = 131 | this.pagesOffsetLeft[`page-${index}`] || this.left + index * pageWidth 132 | 133 | const pageEagerLoadWidth = pageWidth * pagesToEagerLoad 134 | 135 | this.active[`page-${index}`] = 136 | scrollLeft >= pageLeft - pageEagerLoadWidth && 137 | scrollLeft <= pageLeft + pageEagerLoadWidth 138 | } 139 | 140 | this.forceUpdate() 141 | } 142 | } 143 | 144 | handleScroll(scrollLeft) { 145 | const maxMargin = this.getMaxMarginValue() 146 | 147 | this.calculateVisibility(scrollLeft) 148 | const bleed = this.width / 2 149 | const thisLeftWithBleed = this.left - bleed 150 | 151 | if (scrollLeft >= thisLeftWithBleed && scrollLeft <= maxMargin) { 152 | return true 153 | } 154 | 155 | return 156 | } 157 | 158 | getEmojis() { 159 | var {name, emojis, recent, perLine, emojiProps} = this.props 160 | 161 | if (name == 'Recent') { 162 | let {custom} = this.props 163 | let frequentlyUsed = recent || frequently.get(perLine) 164 | 165 | if (frequentlyUsed.length) { 166 | emojis = frequentlyUsed 167 | .map((id) => { 168 | const emoji = custom.filter((e) => e.id === id)[0] 169 | if (emoji) { 170 | return emoji 171 | } 172 | 173 | return id 174 | }) 175 | .filter((id) => !!getData(id, null, null, this.data)) 176 | } 177 | 178 | if (emojis.length === 0 && frequentlyUsed.length > 0) { 179 | return null 180 | } 181 | } 182 | 183 | if (emojis) { 184 | emojis = emojis.slice(0) 185 | } 186 | 187 | return emojis 188 | } 189 | 190 | updateDisplay(visible) { 191 | var emojis = this.getEmojis() 192 | 193 | if (!emojis) { 194 | return 195 | } 196 | 197 | this.setState({visible}) 198 | } 199 | 200 | setPagesRef(index, c) { 201 | if (!this.pages) { 202 | this.pages = {} 203 | } 204 | 205 | this.pages[index] = c 206 | } 207 | 208 | onLayout = (index, event) => { 209 | const {x: left, width} = event.nativeEvent.layout 210 | 211 | if (index === 0) { 212 | this.left = left 213 | this.width = width 214 | } 215 | 216 | this.pagesOffsetLeft[`page-${index}`] = left 217 | this.maxMargin[`page-${index}`] = width 218 | } 219 | 220 | _getSanitizedData = (props) => { 221 | const {emoji, skin, set} = props 222 | return getSanitizedData(emoji, skin, set, this.data) 223 | } 224 | 225 | render() { 226 | var { 227 | id, 228 | name, 229 | hasStickyPosition, 230 | emojiProps, 231 | i18n, 232 | perLine, 233 | rows, 234 | pagesToEagerLoad, 235 | notFound, 236 | notFoundEmoji, 237 | theme, 238 | fontSize, 239 | } = this.props, 240 | emojis = this.getEmojis(), 241 | {visible} = this.state 242 | 243 | const {size: emojiSize, margin: emojiMargin} = emojiProps 244 | 245 | const emojiSizing = emojiSize + emojiMargin 246 | const emojisListWidth = perLine * emojiSizing + emojiMargin + 2 247 | const emojisListHeight = rows * emojiSizing + emojiMargin 248 | 249 | const paginatedEmojis = chunk(emojis, perLine * rows) 250 | 251 | return !emojis || !visible 252 | ? null 253 | : [ 254 | emojis.length ? ( 255 | paginatedEmojis.map((emojis, i) => { 256 | const pageVisible = this.active[`page-${i}`] 257 | 258 | return ( 259 | 272 | {emojis.map((item, i) => { 273 | const emoji = this._getSanitizedData({ 274 | emoji: item, 275 | ...emojiProps, 276 | }) 277 | 278 | return pageVisible ? ( 279 | 285 | ) : null 286 | })} 287 | 288 | ) 289 | }) 290 | ) : ( 291 | 306 | ), 307 | ] 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/components/picker/nimble-picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | Appearance, 5 | Platform, 6 | StyleSheet, 7 | View, 8 | ScrollView, 9 | ToastAndroid, 10 | } from 'react-native' 11 | 12 | import skinStore from '../../utils/skin' 13 | import frequently from '../../utils/frequently' 14 | import {deepMerge} from '../../utils' 15 | import {uncompress} from '../../utils/data' 16 | import {PickerPropTypes} from '../../utils/shared-props' 17 | import {PickerDefaultProps} from '../../utils/shared-default-props' 18 | import Anchors from '../anchors' 19 | import Category from '../category' 20 | import Search from '../search' 21 | 22 | const I18N = { 23 | search: 'Search', 24 | notfound: 'No Emoji Found', 25 | categories: { 26 | search: 'Search Results', 27 | recent: 'Frequently Used', 28 | people: 'Smileys & People', 29 | nature: 'Animals & Nature', 30 | foods: 'Food & Drink', 31 | activity: 'Activity', 32 | places: 'Travel & Places', 33 | objects: 'Objects', 34 | symbols: 'Symbols', 35 | flags: 'Flags', 36 | custom: 'Custom', 37 | }, 38 | } 39 | 40 | const categoryEmojis = { 41 | recent: 'clock3', 42 | people: 'smiley', 43 | nature: 'dog', 44 | foods: 'taco', 45 | activity: 'soccer', 46 | places: 'rocket', 47 | objects: 'bulb', 48 | symbols: 'symbols', 49 | flags: 'flag-wales', 50 | custom: 'triangular_ruler', 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | emojiMartPicker: { 55 | flexShrink: 0, 56 | flexDirection: 'column', 57 | }, 58 | emojiMartPickerLight: { 59 | color: '#222427', 60 | backgroundColor: '#eceff1', 61 | }, 62 | emojiMartPickerDark: { 63 | color: '#fff', 64 | backgroundColor: '#222', 65 | }, 66 | emojiMartScroll: { 67 | flexShrink: 0, 68 | }, 69 | emojiMartAnchors: { 70 | flexShrink: 0, 71 | maxHeight: 90, 72 | }, 73 | }) 74 | 75 | export default class NimblePicker extends React.PureComponent { 76 | static propTypes /* remove-proptypes */ = { 77 | ...PickerPropTypes, 78 | data: PropTypes.object.isRequired, 79 | } 80 | static defaultProps = {...PickerDefaultProps} 81 | 82 | constructor(props) { 83 | super(props) 84 | 85 | this.CUSTOM = [] 86 | 87 | this.RECENT_CATEGORY = {id: 'recent', name: 'Recent', emojis: null} 88 | this.SEARCH_CATEGORY = { 89 | id: 'search', 90 | name: 'Search', 91 | emojis: null, 92 | anchor: false, 93 | } 94 | 95 | if (props.data.compressed) { 96 | uncompress(props.data) 97 | } 98 | 99 | this.data = props.data 100 | this.i18n = deepMerge(I18N, props.i18n) 101 | this.categoryEmojis = deepMerge(categoryEmojis, props.categoryEmojis) 102 | this.state = {firstRender: true} 103 | 104 | this.scrollViewScrollLeft = 0 105 | 106 | this.categories = [] 107 | let allCategories = [].concat(this.data.categories) 108 | 109 | if (props.custom.length > 0) { 110 | const customCategories = {} 111 | let customCategoriesCreated = 0 112 | 113 | props.custom.forEach((emoji) => { 114 | if (!customCategories[emoji.customCategory]) { 115 | customCategories[emoji.customCategory] = { 116 | id: emoji.customCategory 117 | ? `custom-${emoji.customCategory}` 118 | : 'custom', 119 | name: emoji.customCategory || 'Custom', 120 | emojis: [], 121 | anchor: customCategoriesCreated === 0, 122 | } 123 | 124 | customCategoriesCreated++ 125 | } 126 | 127 | const category = customCategories[emoji.customCategory] 128 | 129 | const customEmoji = { 130 | ...emoji, 131 | // `` expects emoji to have an `id`. 132 | id: emoji.short_names[0], 133 | custom: true, 134 | } 135 | 136 | category.emojis.push(customEmoji) 137 | this.CUSTOM.push(customEmoji) 138 | }) 139 | 140 | allCategories = allCategories.concat( 141 | Object.keys(customCategories).map((key) => customCategories[key]), 142 | ) 143 | } 144 | 145 | this.hideRecent = true 146 | 147 | if (props.include != undefined) { 148 | allCategories.sort((a, b) => { 149 | if (props.include.indexOf(a.id) > props.include.indexOf(b.id)) { 150 | return 1 151 | } 152 | 153 | return -1 154 | }) 155 | } 156 | 157 | for ( 158 | let categoryIndex = 0; 159 | categoryIndex < allCategories.length; 160 | categoryIndex++ 161 | ) { 162 | const category = allCategories[categoryIndex] 163 | let isIncluded = 164 | props.include && props.include.length 165 | ? props.include.indexOf(category.id) > -1 166 | : true 167 | let isExcluded = 168 | props.exclude && props.exclude.length 169 | ? props.exclude.indexOf(category.id) > -1 170 | : false 171 | if (!isIncluded || isExcluded) { 172 | continue 173 | } 174 | 175 | if (props.emojisToShowFilter) { 176 | let newEmojis = [] 177 | 178 | const {emojis} = category 179 | for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) { 180 | const emoji = emojis[emojiIndex] 181 | if (props.emojisToShowFilter(this.data.emojis[emoji] || emoji)) { 182 | newEmojis.push(emoji) 183 | } 184 | } 185 | 186 | if (newEmojis.length) { 187 | let newCategory = { 188 | emojis: newEmojis, 189 | name: category.name, 190 | id: category.id, 191 | } 192 | 193 | this.categories.push(newCategory) 194 | } 195 | } else { 196 | this.categories.push(category) 197 | } 198 | } 199 | 200 | let includeRecent = 201 | props.include && props.include.length 202 | ? props.include.indexOf(this.RECENT_CATEGORY.id) > -1 203 | : true 204 | let excludeRecent = 205 | props.exclude && props.exclude.length 206 | ? props.exclude.indexOf(this.RECENT_CATEGORY.id) > -1 207 | : false 208 | if (includeRecent && !excludeRecent) { 209 | this.hideRecent = false 210 | this.categories.unshift(this.RECENT_CATEGORY) 211 | } 212 | 213 | if (this.categories[0]) { 214 | this.categories[0].first = true 215 | } 216 | 217 | this.categories.unshift(this.SEARCH_CATEGORY) 218 | 219 | this.setAnchorsRef = this.setAnchorsRef.bind(this) 220 | this.handleAnchorPress = this.handleAnchorPress.bind(this) 221 | this.setSearchRef = this.setSearchRef.bind(this) 222 | this.handleSearch = this.handleSearch.bind(this) 223 | this.setScrollViewRef = this.setScrollViewRef.bind(this) 224 | this.onScroll = this.onScroll.bind(this) 225 | this.handleScroll = this.handleScroll.bind(this) 226 | this.handleScrollPaint = this.handleScrollPaint.bind(this) 227 | this.handleEmojiPress = this.handleEmojiPress.bind(this) 228 | this.handleEmojiSelect = this.handleEmojiSelect.bind(this) 229 | this.handleEmojiLongPress = this.handleEmojiLongPress.bind(this) 230 | this.handleSkinChange = this.handleSkinChange.bind(this) 231 | this.handleAppearanceChange = this.handleAppearanceChange.bind(this) 232 | } 233 | 234 | componentDidMount() { 235 | if (this.state.firstRender) { 236 | this.firstRenderTimeout = setTimeout(() => { 237 | this.setState({firstRender: false}) 238 | }, 60) 239 | } 240 | } 241 | 242 | componentDidUpdate() { 243 | this.handleScroll() 244 | } 245 | 246 | componentWillUnmount() { 247 | this.SEARCH_CATEGORY.emojis = null 248 | 249 | clearTimeout(this.leaveTimeout) 250 | clearTimeout(this.firstRenderTimeout) 251 | 252 | if (this.colorScheme && Appearance) { 253 | Appearance.removeChangeListener(this.handleAppearanceChange) 254 | } 255 | } 256 | 257 | getPreferredTheme() { 258 | if (this.props.theme != 'auto') return this.props.theme 259 | if (this.state.theme) return this.state.theme 260 | 261 | if (!this.colorScheme && Appearance) { 262 | this.colorScheme = Appearance.getColorScheme() 263 | Appearance.addChangeListener(this.handleAppearanceChange) 264 | } 265 | 266 | if (!this.colorScheme) return PickerDefaultProps.theme 267 | return this.colorScheme 268 | } 269 | 270 | handleAppearanceChange = (preferences) => { 271 | const {colorScheme} = preferences 272 | this.setState({ 273 | theme: colorScheme, 274 | }) 275 | } 276 | 277 | handleEmojiPress(emoji, e) { 278 | this.props.onPress(emoji, e) 279 | this.handleEmojiSelect(emoji) 280 | } 281 | 282 | handleEmojiSelect(emoji) { 283 | this.props.onSelect(emoji) 284 | if (!this.hideRecent && !this.props.recent) frequently.add(emoji) 285 | 286 | const component = this.categoryRefs['category-1'] 287 | if (component) { 288 | let maxMargin = component.maxMargin 289 | if (this.props.enableFrequentEmojiSort) { 290 | component.forceUpdate() 291 | } 292 | } 293 | } 294 | 295 | handleEmojiLongPress(emoji, e) { 296 | this.props.onLongPress(emoji, e) 297 | 298 | // TODO: Implement solution for iOS! 299 | if (Platform.OS === 'android') { 300 | ToastAndroid.showWithGravityAndOffset( 301 | emoji.id, 302 | ToastAndroid.SHORT, 303 | ToastAndroid.BOTTOM, 304 | 0, 305 | 190, 306 | ) 307 | } 308 | } 309 | 310 | onScroll(event) { 311 | this.scrollViewScrollLeft = event.nativeEvent.contentOffset.x 312 | this.handleScroll() 313 | } 314 | 315 | handleScroll() { 316 | if (!this.waitingForPaint) { 317 | this.waitingForPaint = true 318 | this.handleScrollPaint() 319 | } 320 | } 321 | 322 | handleScrollPaint() { 323 | this.waitingForPaint = false 324 | 325 | if (!this.scrollView || !this.props.showAnchors) { 326 | return 327 | } 328 | 329 | let activeCategory = null 330 | const scrollLeft = this.scrollViewScrollLeft 331 | 332 | if (this.SEARCH_CATEGORY.emojis) { 333 | activeCategory = this.SEARCH_CATEGORY 334 | const component = this.categoryRefs[`category-0`] 335 | if (component) component.handleScroll(scrollLeft) 336 | } else { 337 | for (let i = 0, l = this.categories.length; i < l; i++) { 338 | let ii = this.categories.length - 1 - i, 339 | category = this.categories[ii], 340 | component = this.categoryRefs[`category-${ii}`] 341 | 342 | if (component) { 343 | let active = component.handleScroll(scrollLeft) 344 | 345 | if (active && !activeCategory) { 346 | activeCategory = category 347 | } 348 | } 349 | } 350 | } 351 | 352 | if (activeCategory) { 353 | let {anchors} = this, 354 | {name: categoryName} = activeCategory 355 | 356 | if (anchors.state.selected != categoryName) { 357 | anchors.onSelectAnchor(categoryName) 358 | } 359 | } 360 | 361 | this.scrollLeft = scrollLeft 362 | } 363 | 364 | handleSearch(emojis) { 365 | this.SEARCH_CATEGORY.emojis = emojis 366 | 367 | for (let i = 0, l = this.categories.length; i < l; i++) { 368 | let component = this.categoryRefs[`category-${i}`] 369 | 370 | if (component && component.props.name != 'Search') { 371 | let display = emojis ? false : true 372 | component.forceUpdate() 373 | component.updateDisplay(display) 374 | } 375 | } 376 | 377 | this.forceUpdate() 378 | if (emojis) this.scrollView.scrollTo({x: 0, animated: false}) 379 | this.handleScroll() 380 | } 381 | 382 | onScrollViewLayout = (event) => { 383 | this.clientWidth = event.nativeEvent.layout.width 384 | } 385 | 386 | onScrollViewContentSizeChange = (contentWidth) => { 387 | this.scrollWidth = contentWidth 388 | } 389 | 390 | handleAnchorPress(category, i) { 391 | const component = this.categoryRefs[`category-${i}`], 392 | {scrollView} = this 393 | 394 | let scrollToComponent = null 395 | 396 | scrollToComponent = () => { 397 | if (component) { 398 | let {left} = component 399 | 400 | if (category.first) { 401 | left = 0 402 | } 403 | 404 | scrollView.scrollTo({x: left, animated: false}) 405 | } 406 | } 407 | 408 | if (this.SEARCH_CATEGORY.emojis) { 409 | this.handleSearch(null) 410 | this.search.clear() 411 | } 412 | 413 | // setTimeout 0 fixes issue where scrollTo happened before component was fully in view 414 | setTimeout(scrollToComponent, 0) 415 | } 416 | 417 | handleSkinChange(skin) { 418 | const newState = {skin}, 419 | {onSkinChange} = this.props 420 | 421 | this.setState(newState) 422 | skinStore.set(skin) 423 | 424 | onSkinChange(skin) 425 | } 426 | 427 | getCategories() { 428 | return this.state.firstRender 429 | ? this.categories.slice(0, 3) 430 | : this.categories 431 | } 432 | 433 | setAnchorsRef(c) { 434 | this.anchors = c 435 | } 436 | 437 | setSearchRef(c) { 438 | this.search = c 439 | } 440 | 441 | setScrollViewRef(c) { 442 | this.scrollView = c 443 | } 444 | 445 | setCategoryRef(name, c) { 446 | if (!this.categoryRefs) { 447 | this.categoryRefs = {} 448 | } 449 | 450 | this.categoryRefs[name] = c 451 | 452 | if (!this.categoryPages) { 453 | this.categoryPages = {} 454 | } 455 | 456 | this.categoryPages[name] = c ? c.pages : {} 457 | } 458 | 459 | render() { 460 | const { 461 | perLine, 462 | rows, 463 | pagesToEagerLoad, 464 | emojiSize, 465 | emojiMargin, 466 | anchorSize, 467 | set, 468 | sheetSize, 469 | sheetColumns, 470 | sheetRows, 471 | style, 472 | color, 473 | native, 474 | spriteSheetFn, 475 | emojiImageFn, 476 | emojisToShowFilter, 477 | showSkinTones, 478 | showAnchors, 479 | showCloseButton, 480 | emojiTooltip, 481 | include, 482 | exclude, 483 | recent, 484 | autoFocus, 485 | useLocalImages, 486 | onPressClose, 487 | notFound, 488 | notFoundEmoji, 489 | skinEmoji, 490 | skinEmojiSize, 491 | fontSize, 492 | } = this.props 493 | 494 | const emojiSizing = emojiSize + emojiMargin 495 | const emojisListWidth = perLine * emojiSizing + emojiMargin + 2 496 | const emojisListHeight = rows * emojiSizing + emojiMargin 497 | 498 | const theme = this.getPreferredTheme() 499 | const skin = 500 | this.props.skin || 501 | this.state.skin || 502 | skinStore.get() || 503 | this.props.defaultSkin 504 | 505 | return ( 506 | 516 | 547 | 548 | 565 | {this.getCategories().map((category, i) => { 566 | return ( 567 | 610 | ) 611 | })} 612 | 613 | 614 | {showAnchors ? ( 615 | 616 | 639 | 640 | ) : null} 641 | 642 | ) 643 | } 644 | } 645 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
Emoji Mart Native is a Slack-like customizable
emoji picker component for React Native ported from [Emoji Mart] 3 |
Example appChangelog 4 |

Build Status 5 |

6 | 7 |

Supporting emoji-mart-native

8 | 9 | The ongoing development of Emoji Mart Native is made possible entirely by the support of these awesome [backers](https://github.com/tunoltd/emoji-mart-native/blob/master/BACKERS.md). If you'd like to join them, please consider becoming a [backer or sponsor on GitHub](https://github.com/sponsors/pederjohnsen). 10 | 11 |

Platinum Sponsors

12 | 13 | 14 | 40 | 41 | 42 |

Gold Sponsors

43 | 44 | 45 | 81 | 82 | 83 | --- 84 | 85 |

86 | picker 87 |

88 | 89 | ## Installation 90 | 91 | `npm install --save emoji-mart-native` 92 | 93 | ## Components 94 | 95 | ### Picker 96 | 97 | Renders _inline-block_ & center aligned if parent is wider than picker. 98 | To render picker in a fullscreen modal use [``](#modalpicker). 99 | 100 | ```jsx 101 | import { Picker } from 'emoji-mart-native' 102 | 103 | 104 | 105 | 106 | 107 | ``` 108 | 109 | | Prop | Required | Default | Description | 110 | | ---- | :------: | ------- | ----------- | 111 | | **autoFocus** | | `false` | Auto focus the search input when mounted | 112 | | **color** | | `#ae65c5` | The top bar anchors select and hover color | 113 | | **include** | | `[]` | Only load included categories. Accepts [I18n categories keys](#i18n). Order will be respected, except for the `recent` category which will always be the first. | 114 | | **exclude** | | `[]` | Don't load excluded categories. Accepts [I18n categories keys](#i18n). | 115 | | **custom** | | `[]` | [Custom emojis](#custom-emojis) | 116 | | **recent** | | | Pass your own frequently used emojis as array of string IDs | 117 | | **enableFrequentEmojiSort** | | `false` | Instantly sort “Frequently Used” category | 118 | | **emojiSize** | | `30` | The emoji width and height | 119 | | **emojiMargin** | | `14` | The emoji margin | 120 | | **anchorSize** | | `24` | The anchor emoji width and height | 121 | | **onClick** | | | Params: `(emoji, event) => {}`. Not called when emoji is selected with `enter` | 122 | | **onSelect** | | | Params: `(emoji) => {}` | 123 | | **onSkinChange** | | | Params: `(skin) => {}` | 124 | | **showCloseButton** | | `false` | Shows the close button which triggers **onPressClose** | 125 | | **onPressClose** | | | Trigger when user press close button | 126 | | **perLine** | | `7` | Number of emojis per line. While there’s no minimum or maximum, this will affect the picker’s width. This will set _Frequently Used_ length as well (`perLine * rows`) | 127 | | **rows** | | `3` | Number of rows. While there's no minimum or maximum, this will affect the picker’s height. This will set _Frequently Used_ length as well (`perLine * rows`) | 128 | | **pagesToEagerLoad** | | `2` | Number of pages to eager load each side of currently active page. | 129 | | **i18n** | | [`{…}`](#i18n) | [An object](#i18n) containing localized strings | 130 | | **native** | | `false` | Renders the native unicode emoji | 131 | | **set** | | `apple` | The emoji set: `'apple', 'google', 'twitter', 'facebook'` | 132 | | **theme** | | `light` | The picker theme: `'auto', 'light', 'dark'` Note: `auto` uses `Appearance` and only works when using `react-native` 0.62.0 or above | 133 | | **sheetSize** | | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | 134 | | **sheetColumns** | | `60` | The emoji sheet columns | 135 | | **sheetRows** | | `60` | The emoji sheet rows | 136 | | **spriteSheetFn** | | `((set, sheetSize) => …)` | [A Fn](#spritesheetfn) that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | 137 | | **useLocalImages** | | false | [Local image requires](#local-image-requires) | 138 | | **emojisToShowFilter** | | `((emoji) => true)` | A Fn to choose whether an emoji should be displayed or not | 139 | | **showPreview** | | `true` | Display preview section | 140 | | **showSkinTones** | | `true` | Display skin tones picker | 141 | | **skin** | | | Forces skin color: `1, 2, 3, 4, 5, 6` | 142 | | **defaultSkin** | | `1` | Default skin color: `1, 2, 3, 4, 5, 6` | 143 | | **skinEmoji** | | | The emoji used to pick a skin tone. Uses an emoji-less skin tone picker by default | 144 | | **skinEmojiSize** | | `28` | The skin emoji width and height | 145 | | **style** | | | Inline styles applied to the root element. Useful for positioning | 146 | | **notFoundEmoji** | | `sleuth_or_spy` | The emoji shown when there are no search results | 147 | | **notFound** | | | [Not Found](#not-found) | 148 | | **categoryEmojis** | | `{}` | [Custom category emojis](#custom-category-emojis) | 149 | | **fontSize** | | 15 | Font size used for all text in the picker | 150 | 151 | #### I18n 152 | 153 | ```js 154 | search: 'Search', 155 | notfound: 'No Emoji Found', 156 | categories: { 157 | search: 'Search Results', 158 | recent: 'Frequently Used', 159 | people: 'Smileys & People', 160 | nature: 'Animals & Nature', 161 | foods: 'Food & Drink', 162 | activity: 'Activity', 163 | places: 'Travel & Places', 164 | objects: 'Objects', 165 | symbols: 'Symbols', 166 | flags: 'Flags', 167 | custom: 'Custom', 168 | } 169 | ``` 170 | 171 | #### SpriteSheetFn 172 | 173 | By default the picker source the emoji sheets online, this may not be the best solution and you may want to bundle the emoji sheets with your app. 174 | For the best results it's recommended to include any emoji sheets you use in the platform specific app package. 175 | 176 | You can either provide your own emoji sheets or use ones available from libraries such as [`iamcal/emoji-data`](https://github.com/iamcal/emoji-data#installation): 177 | 178 | ``` 179 | npm install emoji-datasource-apple 180 | npm install emoji-datasource-google 181 | npm install emoji-datasource-twitter 182 | npm install emoji-datasource-facebook 183 | ``` 184 | 185 | ```jsx 186 | import { Picker } from 'emoji-mart-native' 187 | 188 | const localSpriteSheets = { 189 | ... 190 | twitter: { 191 | ... 192 | '20': {uri: `https://unpkg.com/emoji-datasource@5.0.1/sheet_${set}_${sheetSize}.png`}, // Loads asset from web 193 | '32': require('./node_modules/emoji-datasource-twitter/img/twitter/sheets/32.png'), // Loads static asset 194 | '64': {uri: 'twitter_emoji_64'}, // Loads asset from app package 195 | }, 196 | ... 197 | }; 198 | 199 | 200 | {uri: `https://unpkg.com/emoji-datasource@5.0.1/sheet_${set}_${sheetSize}.png`} 201 | }> 202 | 203 | localSpriteSheets[set][sheetSize] 204 | }> 205 | ``` 206 | 207 | #### Sheet sizes 208 | Sheets are served from [unpkg](https://unpkg.com), a global CDN that serves files published to [npm](https://www.npmjs.com). 209 | 210 | | Set | Size (`sheetSize: 16`) | Size (`sheetSize: 20`) | Size (`sheetSize: 32`) | Size (`sheetSize: 64`) | 211 | | --- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | 212 | | apple | 407 KB | 561 KB | 1.34 MB | 3.60 MB | 213 | | facebook | 416 KB | 579 KB | 1.38 MB | 3.68 MB | 214 | | google | 362 KB | 489 KB | 1.12 MB | 2.78 MB | 215 | | twitter | 361 KB | 485 KB | 1.05 MB | 2.39 MB | 216 | 217 | #### Datasets 218 | While all sets are available by default, you may want to include only a single set data to reduce the size of your bundle. 219 | 220 | | Set | Size (on disk) | 221 | | --- | -------------- | 222 | | all | 611 KB | 223 | | apple | 548 KB | 224 | | facebook | 468 KB | 225 | | google | 518 KB | 226 | | twitter | 517 KB | 227 | 228 | To use these data files (or any other custom data), use the `NimblePicker` component: 229 | 230 | ```js 231 | import data from 'emoji-mart-native/data/apple.json' 232 | import { NimblePicker } from 'emoji-mart-native' 233 | 234 | 235 | ``` 236 | 237 | #### Examples of `emoji` object: 238 | ```js 239 | { 240 | id: 'smiley', 241 | name: 'Smiling Face with Open Mouth', 242 | colons: ':smiley:', 243 | text: ':)', 244 | emoticons: [ 245 | '=)', 246 | '=-)' 247 | ], 248 | skin: null, 249 | native: '😃' 250 | } 251 | 252 | { 253 | id: 'santa', 254 | name: 'Father Christmas', 255 | colons: ':santa::skin-tone-3:', 256 | text: '', 257 | emoticons: [], 258 | skin: 3, 259 | native: '🎅🏼' 260 | } 261 | 262 | { 263 | id: 'octocat', 264 | name: 'Octocat', 265 | colons: ':octocat:', 266 | text: '', 267 | emoticons: [], 268 | custom: true, 269 | image: {uri: 'https://github.githubassets.com/images/icons/emoji/octocat.png'} 270 | } 271 | ``` 272 | 273 | #### Local image requires 274 | By default the picker source the emoji images online, this may not be the best solution and you may want to bundle the emojis with your app. 275 | 276 | | Set | Size (on disk) | 277 | | --- | -------------- | 278 | | all | 1.6 MB | 279 | | apple | 776 KB | 280 | | facebook | 690 KB | 281 | | google | 742 KB | 282 | | twitter | 752 KB | 283 | 284 | To use local image requires you need to install the individual sets you need in your project using the individual sets npm packages from https://github.com/iamcal/emoji-data#installation: 285 | 286 | ``` 287 | npm install emoji-datasource-apple 288 | npm install emoji-datasource-google 289 | npm install emoji-datasource-twitter 290 | npm install emoji-datasource-facebook 291 | ``` 292 | 293 | ```js 294 | import { NimblePicker, NimbleEmoji } from 'emoji-mart-native' 295 | import data from 'emoji-mart-native/data/facebook.json' 296 | import dataRequires from 'emoji-mart-native/data/local-images/facebook' 297 | const {emojis: localEmojis} = dataRequires 298 | 299 | 300 | ``` 301 | 302 | ### ModalPicker 303 | Renders the picker in a fullscreen modal. 304 | 305 | ```jsx 306 | import { ModalPicker } from 'emoji-mart-native' 307 | ; 308 | ``` 309 | 310 | | Prop | Required | Default | Description | 311 | | ---- | :------: | ------- | ----------- | 312 | | **...PickerProps** | | | | 313 | | **isVisible** | | `false` | When true shows the modal with the picker | 314 | 315 | ### EmojiButton 316 | Renders an emoji button that can be used to trigger showing a hidden picker. 317 | 318 | ```jsx 319 | import { EmojiButton } from 'emoji-mart-native' 320 | const emojiImage = require('assets/emoji-image.png') 321 | 322 | 323 | 324 | 325 | ``` 326 | 327 | | Prop | Required | Default | Description | 328 | | ---- | :------: | ------- | ----------- | 329 | | **onButtonPress** | | | Trigger when user press the button | 330 | | **buttonImage** | | ![emoji-icon.png](https://raw.githubusercontent.com/tunoltd/emoji-mart-native/master/dist/assets/emoji-icon.png) | The image used for rendering the button | 331 | | **buttonSize** | | 18 | The button width and height | 332 | 333 | ### Emoji 334 | ```jsx 335 | import { Emoji } from 'emoji-mart-native' 336 | 337 | 338 | 339 | 340 | ``` 341 | 342 | | Prop | Required | Default | Description | 343 | | ---- | :------: | ------- | ----------- | 344 | | **emoji** | ✓ | | Either a string or an `emoji` object | 345 | | **size** | ✓ | | The emoji width and height. | 346 | | **native** | | `false` | Renders the native unicode emoji | 347 | | **onPress** | | | Params: `(emoji, event) => {}` | 348 | | **onLongPress** | | | Params: `(emoji, event) => {}` | 349 | | [**fallback**](#unsupported-emojis-fallback) | | | Params: `(emoji, props) => {}` | 350 | | **set** | | `apple` | The emoji set: `'apple', 'google', 'twitter', 'facebook'` | 351 | | **sheetSize** | | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | 352 | | **sheetColumns** | | `60` | The emoji sheet columns | 353 | | **sheetRows** | | `60` | The emoji sheet rows | 354 | | **spriteSheetFn** | | ``((set, sheetSize) => {uri: `https://unpkg.com/emoji-datasource@5.0.1/sheet_${set}_${sheetSize}.png`})`` | [A Fn](#spritesheetfn) that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | 355 | | **useLocalImages** | | false | [Local image requires](#local-image-requires) | 356 | | **skin** | | `1` | Skin color: `1, 2, 3, 4, 5, 6` | 357 | | [**html**](#using-with-dangerouslysetinnerhtml) | | `false` | Returns an HTML string to use with `dangerouslySetInnerHTML` | 358 | 359 | #### Unsupported emojis fallback 360 | Certain sets don’t support all emojis. By default the Emoji component will not render anything so that the emojis’ don’t take space in the picker when not available. When using the standalone Emoji component, you can however render anything you want by providing the `fallback` props. 361 | 362 | To have the component render `:shrug:` you would need to: 363 | 364 | ```js 365 | { 370 | return emoji ? `:${emoji.short_names[0]}:` : props.emoji 371 | }} 372 | /> 373 | ``` 374 | 375 | ## Custom emojis 376 | You can provide custom emojis which will show up in their own category. You can either use a single image as `image` or use a spritesheet as shown in the third object. 377 | 378 | ```js 379 | import { Picker, NimbleEmoji, getEmojiDataFromCustom } from 'emoji-mart-native' 380 | 381 | const customEmojis = [ 382 | { 383 | name: 'Octocat', 384 | short_names: ['octocat'], 385 | text: '', 386 | emoticons: [], 387 | keywords: ['github'], 388 | image: {uri: 'https://github.githubassets.com/images/icons/emoji/octocat.png'}, 389 | customCategory: 'GitHub' 390 | }, 391 | { 392 | name: 'Trollface', 393 | short_names: ['troll', 'trollface'], 394 | text: '', 395 | emoticons: [], 396 | keywords: ['troll'], 397 | image: require('assets/trollface.png') 398 | }, 399 | { 400 | name: 'Test Flag', 401 | short_names: ['test'], 402 | text: '', 403 | emoticons: [], 404 | keywords: ['test', 'flag'], 405 | spriteSheet: {uri: 'https://unpkg.com/emoji-datasource-twitter@5.0.1/img/twitter/sheets-256/64.png'}, 406 | sheet_x: 1, 407 | sheet_y: 1, 408 | size: 64, 409 | sheetColumns: 60, 410 | sheetRows: 60 411 | }, 412 | { 413 | name: 'Test Flag', 414 | short_names: ['test'], 415 | text: '', 416 | emoticons: [], 417 | keywords: ['test', 'flag'], 418 | spriteSheet: require('assets/twitter/sheets-256/64.png'), 419 | sheet_x: 1, 420 | sheet_y: 1, 421 | size: 64, 422 | sheetColumns: 60, 423 | sheetRows: 60 424 | } 425 | ] 426 | 427 | 428 | 429 | const emoji = getEmojiDataFromCustom('troll', customEmojis, emojiData); 430 | 431 | 439 | ``` 440 | 441 | The `customCategory` string is optional. If you include it, then the custom emoji will be shown in whatever 442 | categories you define. If you don't include it, then there will just be one category called "Custom". 443 | 444 | ## Not Found 445 | You can provide a custom Not Found object which will allow the appearance of the not found search results to change. In this case, we change the default 'sleuth_or_spy' emoji to Octocat when our search finds no results. 446 | 447 | ```js 448 | import { Picker } from 'emoji-mart' 449 | 450 | const notFound = () => 451 | 452 | 453 | ``` 454 | 455 | ## Custom category emojis 456 | You can provide custom emojis for the category anchors. You only need to supply the ones you want changed from the default ones. 457 | 458 | ```js 459 | import { Picker } from 'emoji-mart' 460 | 461 | const categoryEmojis = { 462 | recent: 'fire', 463 | people: 'see_no_evil', 464 | nature: 'beetle', 465 | foods: 'kiwifruit', 466 | activity: 'table_tennis_paddle_and_ball', 467 | places: 'airplane', 468 | objects: 'postal_horn', 469 | symbols: 'copyright', 470 | flags: 'triangular_flag_on_post', 471 | custom: 'hammer_and_wrench', 472 | } 473 | 474 | 475 | ``` 476 | 477 | ## Headless search 478 | The `Picker` doesn’t have to be mounted for you to take advantage of the advanced search results. 479 | 480 | ```js 481 | import { emojiIndex } from 'emoji-mart-native' 482 | 483 | emojiIndex.search('christmas').map((o) => o.native) 484 | // => [🎄, 🎅🏼, 🔔, 🎁, ⛄️, ❄️] 485 | ``` 486 | 487 | ### With custom data 488 | ```js 489 | import data from 'emoji-mart-native/datasets/facebook' 490 | import { NimbleEmojiIndex } from 'emoji-mart-native' 491 | 492 | let emojiIndex = new NimbleEmojiIndex(data) 493 | emojiIndex.search('christmas') 494 | ``` 495 | 496 | ## Get emoji data from Native 497 | You can get emoji data from native emoji unicode using the `getEmojiDataFromNative` util function. 498 | 499 | ```js 500 | import { getEmojiDataFromNative, Emoji } from 'emoji-mart-native' 501 | import data from 'emoji-mart-native/data/all.json' 502 | 503 | const emojiData = getEmojiDataFromNative('🏊🏽‍♀️', 'apple', data) 504 | 505 | 511 | ``` 512 | 513 | #### Example of `emojiData` object: 514 | ```js 515 | emojiData: { 516 | "id": "woman-swimming", 517 | "name": "Woman Swimming", 518 | "colons": ":woman-swimming::skin-tone-4:", 519 | "emoticons": [], 520 | "unified": "1f3ca-1f3fd-200d-2640-fe0f", 521 | "skin": 4, 522 | "native": "🏊🏽‍♀️" 523 | } 524 | ``` 525 | 526 | ## Storage 527 | By default EmojiMartNative will store user chosen skin and frequently used emojis in `localStorage`. That can however be overwritten should you want to store these in your own storage. 528 | 529 | ```js 530 | import { store } from 'emoji-mart-native' 531 | 532 | store.setHandlers({ 533 | getter: (key) => { 534 | // Get from your own storage (sync) 535 | }, 536 | 537 | setter: (key, value) => { 538 | // Persist in your own storage (can be async) 539 | }, 540 | }) 541 | ``` 542 | 543 | Possible keys are: 544 | 545 | | Key | Value | Description | 546 | | --- | ----- | ----------- | 547 | | skin | `1, 2, 3, 4, 5, 6` | | 548 | | frequently | `{ 'astonished': 11, '+1': 22 }` | An object where the key is the emoji name and the value is the usage count | 549 | | last | 'astonished' | (Optional) Used by `frequently` to be sure the latest clicked emoji will always appear in the “Recent” category | 550 | 551 | ## Features 552 | ### Powerful search 553 | #### Short name, name and keywords 554 | Not only does **Emoji Mart Native** return more results than most emoji picker, they’re more accurate and sorted by relevance. 555 | 556 | summer 557 | 558 | #### Emoticons 559 | The only emoji picker that returns emojis when searching for emoticons. 560 | 561 | emoticons 562 | 563 | #### Results intersection 564 | For better results, **Emoji Mart Native** split search into words and only returns results matching both terms. 565 | 566 | hand-raised 567 | 568 | ### Fully customizable 569 | #### Anchors color, title and default emoji 570 | customizable-color 571 | 572 | #### Emojis sizes and length 573 | size-and-length 574 | 575 | #### Default skin color 576 | As the developer, you have control over which skin color is used by default. 577 | 578 | skins 579 | 580 | It can however be overwritten as per user preference. 581 | 582 | customizable-skin 583 | 584 | #### Multiple sets supported 585 | Apple / Google / Twitter / Facebook 586 | 587 | sets 588 | 589 | ## Not opinionated 590 | **Emoji Mart Native** doesn’t automatically insert anything into a text input, nor does it show or hide itself. It simply returns an `emoji` object. It’s up to the developer to mount/unmount (it’s fast!) and position the picker. You can use the returned object as props for the `EmojiMartNative.Emoji` component. You could also use `emoji.colons` to insert text into a textarea or `emoji.native` to use the emoji. 591 | 592 | ### Removing prop-types 593 | 594 | To remove [prop-types](https://github.com/facebook/prop-types) in production, use [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types): 595 | 596 | ```bash 597 | npm install --save-dev babel-plugin-transform-react-remove-prop-types 598 | ``` 599 | 600 | Then add to your `.babelrc`: 601 | 602 | ```json 603 | "plugins": [ 604 | [ 605 | "transform-react-remove-prop-types", 606 | { 607 | "removeImport": true, 608 | "additionalLibraries": [ 609 | "../../utils/shared-props" 610 | ] 611 | } 612 | ] 613 | ] 614 | ``` 615 | 616 | You'll also need to ensure that Babel is transpiling `emoji-mart-native`, e.g. [by not excluding `node_modules` in `babel-loader`](https://github.com/babel/babel-loader#usage). 617 | 618 | ## Development 619 | 620 | ```bash 621 | yarn build 622 | ``` 623 | 624 | ### Testing Changes 625 | 626 | To easier test changes as you make them, you can run `yarn build:link -- --out-dir /$project/node_modules/emoji-mart-native/dist` replacing `$project` with your projects or the example apps location. 627 | 628 | ## 🎩 Hat tips! 629 | Ported from code brought to you by the Missive team
630 | Powered by [iamcal/emoji-data](https://github.com/iamcal/emoji-data) and inspired by [iamcal/js-emoji](https://github.com/iamcal/js-emoji).
631 | 🙌🏼  [Cal Henderson](https://github.com/iamcal). 632 | --------------------------------------------------------------------------------