├── .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 |

62 |
63 |
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 app • Changelog
4 |
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 |
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** | |  | 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 |
557 |
558 | #### Emoticons
559 | The only emoji picker that returns emojis when searching for emoticons.
560 |
561 |
562 |
563 | #### Results intersection
564 | For better results, **Emoji Mart Native** split search into words and only returns results matching both terms.
565 |
566 |
567 |
568 | ### Fully customizable
569 | #### Anchors color, title and default emoji
570 |
571 |
572 | #### Emojis sizes and length
573 |
574 |
575 | #### Default skin color
576 | As the developer, you have control over which skin color is used by default.
577 |
578 |
579 |
580 | It can however be overwritten as per user preference.
581 |
582 |
583 |
584 | #### Multiple sets supported
585 | Apple / Google / Twitter / Facebook
586 |
587 |
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 |
--------------------------------------------------------------------------------