├── example ├── .watchmanconfig ├── .gitignore ├── babel.config.js ├── app.json ├── package.json └── App.js ├── .npmignore ├── .gitignore ├── assets ├── demo.gif └── cover.png ├── package.json ├── LICENSE.md ├── index.d.ts ├── README.md └── index.js /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | example/* 3 | assets/ 4 | assets/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | .DS_Store 4 | example/module.js -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arronhunt/react-native-emoji-selector/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arronhunt/react-native-emoji-selector/HEAD/assets/cover.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | module.js 9 | package-lock.json -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"] 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ], 11 | "version": "1.0.0", 12 | "orientation": "portrait", 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "dev": "cp ../index.js ./module.js", 6 | "android": "expo start --android", 7 | "ios": "expo start --ios", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^36.0.0", 12 | "react": "16.9.0", 13 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz", 14 | "emoji-datasource": "^6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-preset-expo": "^8.0.0" 18 | }, 19 | "private": true 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-emoji-selector", 3 | "version": "0.2.0", 4 | "description": "A react native emoji selector", 5 | "main": "index.js", 6 | "types": "./index.d.ts", 7 | "scripts": { 8 | "prettify": "prettier --write './**/*.js'" 9 | }, 10 | "keywords": [ 11 | "emoji", 12 | "react" 13 | ], 14 | "author": "Arron Hunt", 15 | "license": "MIT", 16 | "dependencies": { 17 | "emoji-datasource": "^6.0.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/arronhunt/react-native-emoji-selector.git" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2019` `Arron Hunt ` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-native-emoji-selector" { 2 | import * as React from "react"; 3 | 4 | /** 5 | * Categories 6 | * The package itself exports a dictionary of objects, however 7 | * to to enforce usage of the exported dictionary the types 8 | * just simplifies to an enum. Once compiled it runs the 9 | * same because the export is named the same. 10 | */ 11 | export enum Categories { 12 | all = "all", 13 | history = "history", 14 | emotion = "emotion", 15 | people = "people", 16 | nature = "nature", 17 | food = "food", 18 | activities = "activities", 19 | places = "places", 20 | objects = "objects", 21 | symbols = "symbols", 22 | flag = "flag" 23 | } 24 | 25 | export interface EmojiSelectorProps { 26 | onEmojiSelected(emoji: string): void; 27 | theme?: string; 28 | placeholder?: string; 29 | showTabs?: boolean; 30 | showSearchBar?: boolean; 31 | showHistory?: boolean; 32 | showSectionTitles?: boolean; 33 | category?: Categories; 34 | columns?: number; 35 | shouldInclude?: (e: any)=>boolean; 36 | } 37 | 38 | const EmojiSelector: React.ComponentType; 39 | 40 | export default EmojiSelector; 41 | } 42 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, Text, View, SafeAreaView } from "react-native"; 3 | 4 | import EmojiSelector, { Categories } from "./module"; 5 | const THEME = "#007AFF"; 6 | 7 | export default class App extends React.Component { 8 | state = { 9 | emoji: " " 10 | }; 11 | render() { 12 | return ( 13 | 14 | Please select the emoji you would like to use 15 | 16 | 17 | {this.state.emoji} 18 | 19 | 20 | this.setState({ emoji })} 22 | showSearchBar={true} 23 | showTabs={true} 24 | showHistory={true} 25 | showSectionTitles={true} 26 | category={Categories.all} 27 | /> 28 | 29 | ); 30 | } 31 | } 32 | 33 | const styles = StyleSheet.create({ 34 | container: { 35 | flex: 1, 36 | backgroundColor: "#fff", 37 | alignItems: "center", 38 | justifyContent: "center" 39 | }, 40 | display: { 41 | width: 96, 42 | height: 96, 43 | margin: 24, 44 | borderWidth: 2, 45 | borderRadius: 12, 46 | borderColor: THEME, 47 | alignItems: "center", 48 | justifyContent: "center" 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-emoji-selector 2 | 3 | ![Image preview](./assets/cover.png) 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install --save react-native-emoji-selector 9 | ``` 10 | 11 | ``` 12 | import EmojiSelector from 'react-native-emoji-selector' 13 | ``` 14 | 15 | ## Demo 16 | 17 | ![Demo GIF](./assets/demo.gif) 18 | 19 | ## Usage 20 | 21 | ### Basic usage 22 | 23 | ```jsx 24 | console.log(emoji)} /> 25 | ``` 26 | 27 | ### Setting a default category 28 | 29 | If you'd like to define a different default category, you can import the `Categories` class. Setting a default category can also improve performance by loading a single section rather than all sections at once. 30 | 31 | ```jsx 32 | import EmojiSelector, { Categories } from "react-native-emoji-selector"; 33 | 34 | console.log(emoji)} 37 | />; 38 | ``` 39 | 40 | The available categories are `all`, `emotion`, `people`, `nature`, `food`, `activities`, `places`, `objects`, `symbols`, and `flags`. 41 | 42 | ## Props 43 | 44 | | Prop | Type | Default | Description | 45 | | ----------------- | -------- | ------------- | -------------------------------------------------------- | 46 | | onEmojiSelected | _func_ | | Function called when a user selects an Emoji | 47 | | theme | _string_ | `"007AFF"` | Theme color used for loaders and active tab indicator | 48 | | showTabs | _bool_ | `true` | Toggle the tabs on or off | 49 | | showSearchBar | _bool_ | `true` | Toggle the searchbar on or off | 50 | | showHistory | _bool_ | `false` | Toggle the history tab on or off | 51 | | showSectionTitles | _bool_ | `true` | Toggle the section title elements | 52 | | category | _enum_ | `"all"` | Set the default category. Use the `Categories` class | 53 | | columns | _number_ | `6` | Number of columns accross | 54 | | placeholder | _string_ | `"Search..."` | A string placeholder when there is no text in text input | 55 | | shouldInclude | _func_ | | Function called to check for emoji inclusion | 56 | 57 | ## Contributors 58 | 59 | Special thanks to everyone who has contributed to this project! 60 | 61 | [![Victor K Varghese](https://avatars3.githubusercontent.com/u/15869386?s=80&v=4)](https://github.com/victorkvarghese) 62 | [![Kubo](https://avatars3.githubusercontent.com/u/22464192?s=80&v=4)](https://github.com/ma96o) 63 | [![Mateo Silguero](https://avatars3.githubusercontent.com/u/25598400?s=80&v=4)](https://github.com/mateosilguero) 64 | [![Anastasiia Kravchenko](https://avatars3.githubusercontent.com/u/4223266?s=80&v=4)](https://github.com/St1ma) 65 | [![Sindre](https://avatars3.githubusercontent.com/u/4065840?s=80&v=4)](https://github.com/sseppola) 66 | [![Lucas Feijo](https://avatars3.githubusercontent.com/u/4157166?s=80&v=4)](https://github.com/lucasfeijo) 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | View, 4 | Text, 5 | StyleSheet, 6 | TouchableOpacity, 7 | TextInput, 8 | Platform, 9 | ActivityIndicator, 10 | AsyncStorage, 11 | FlatList 12 | } from "react-native"; 13 | import emoji from "emoji-datasource"; 14 | 15 | export const Categories = { 16 | all: { 17 | symbol: null, 18 | name: "All" 19 | }, 20 | history: { 21 | symbol: "🕘", 22 | name: "Recently used" 23 | }, 24 | emotion: { 25 | symbol: "😀", 26 | name: "Smileys & Emotion" 27 | }, 28 | people: { 29 | symbol: "🧑", 30 | name: "People & Body" 31 | }, 32 | nature: { 33 | symbol: "🦄", 34 | name: "Animals & Nature" 35 | }, 36 | food: { 37 | symbol: "🍔", 38 | name: "Food & Drink" 39 | }, 40 | activities: { 41 | symbol: "⚾️", 42 | name: "Activities" 43 | }, 44 | places: { 45 | symbol: "✈️", 46 | name: "Travel & Places" 47 | }, 48 | objects: { 49 | symbol: "💡", 50 | name: "Objects" 51 | }, 52 | symbols: { 53 | symbol: "🔣", 54 | name: "Symbols" 55 | }, 56 | flags: { 57 | symbol: "🏳️‍🌈", 58 | name: "Flags" 59 | } 60 | }; 61 | 62 | const charFromUtf16 = utf16 => 63 | String.fromCodePoint(...utf16.split("-").map(u => "0x" + u)); 64 | export const charFromEmojiObject = obj => charFromUtf16(obj.unified); 65 | const filteredEmojis = emoji.filter(e => !e["obsoleted_by"]); 66 | const emojiByCategory = category => 67 | filteredEmojis.filter(e => e.category === category); 68 | const sortEmoji = list => list.sort((a, b) => a.sort_order - b.sort_order); 69 | const categoryKeys = Object.keys(Categories); 70 | 71 | const TabBar = ({ theme, activeCategory, onPress, width }) => { 72 | const tabSize = width / categoryKeys.length; 73 | 74 | return categoryKeys.map(c => { 75 | const category = Categories[c]; 76 | if (c !== "all") 77 | return ( 78 | onPress(category)} 81 | style={{ 82 | flex: 1, 83 | height: tabSize, 84 | borderColor: category === activeCategory ? theme : "#EEEEEE", 85 | borderBottomWidth: 2, 86 | alignItems: "center", 87 | justifyContent: "center" 88 | }} 89 | > 90 | 97 | {category.symbol} 98 | 99 | 100 | ); 101 | }); 102 | }; 103 | 104 | const EmojiCell = ({ emoji, colSize, ...other }) => ( 105 | 115 | 116 | {charFromEmojiObject(emoji)} 117 | 118 | 119 | ); 120 | 121 | const storage_key = "@react-native-emoji-selector:HISTORY"; 122 | export default class EmojiSelector extends Component { 123 | state = { 124 | searchQuery: "", 125 | category: Categories.people, 126 | isReady: false, 127 | history: [], 128 | emojiList: null, 129 | colSize: 0, 130 | width: 0 131 | }; 132 | 133 | // 134 | // HANDLER METHODS 135 | // 136 | handleTabSelect = category => { 137 | if (this.state.isReady) { 138 | if (this.scrollview) 139 | this.scrollview.scrollToOffset({ x: 0, y: 0, animated: false }); 140 | this.setState({ 141 | searchQuery: "", 142 | category 143 | }); 144 | } 145 | }; 146 | 147 | handleEmojiSelect = emoji => { 148 | if (this.props.showHistory) { 149 | this.addToHistoryAsync(emoji); 150 | } 151 | this.props.onEmojiSelected(charFromEmojiObject(emoji)); 152 | }; 153 | 154 | handleSearch = searchQuery => { 155 | this.setState({ searchQuery }); 156 | }; 157 | 158 | addToHistoryAsync = async emoji => { 159 | let history = await AsyncStorage.getItem(storage_key); 160 | 161 | let value = []; 162 | if (!history) { 163 | // no history 164 | let record = Object.assign({}, emoji, { count: 1 }); 165 | value.push(record); 166 | } else { 167 | let json = JSON.parse(history); 168 | if (json.filter(r => r.unified === emoji.unified).length > 0) { 169 | value = json; 170 | } else { 171 | let record = Object.assign({}, emoji, { count: 1 }); 172 | value = [record, ...json]; 173 | } 174 | } 175 | 176 | AsyncStorage.setItem(storage_key, JSON.stringify(value)); 177 | this.setState({ 178 | history: value 179 | }); 180 | }; 181 | 182 | loadHistoryAsync = async () => { 183 | let result = await AsyncStorage.getItem(storage_key); 184 | if (result) { 185 | let history = JSON.parse(result); 186 | this.setState({ history }); 187 | } 188 | }; 189 | 190 | // 191 | // RENDER METHODS 192 | // 193 | renderEmojiCell = ({ item }) => ( 194 | this.handleEmojiSelect(item.emoji)} 198 | colSize={this.state.colSize} 199 | /> 200 | ); 201 | 202 | returnSectionData() { 203 | const { history, emojiList, searchQuery, category } = this.state; 204 | let emojiData = (function() { 205 | if (category === Categories.all && searchQuery === "") { 206 | //TODO: OPTIMIZE THIS 207 | let largeList = []; 208 | categoryKeys.forEach(c => { 209 | const name = Categories[c].name; 210 | const list = 211 | name === Categories.history.name ? history : emojiList[name]; 212 | if (c !== "all" && c !== "history") largeList = largeList.concat(list); 213 | }); 214 | 215 | return largeList.map(emoji => ({ key: emoji.unified, emoji })); 216 | } else { 217 | let list; 218 | const hasSearchQuery = searchQuery !== ""; 219 | const name = category.name; 220 | if (hasSearchQuery) { 221 | const filtered = emoji.filter(e => { 222 | let display = false; 223 | e.short_names.forEach(name => { 224 | if (name.includes(searchQuery.toLowerCase())) display = true; 225 | }); 226 | return display; 227 | }); 228 | list = sortEmoji(filtered); 229 | } else if (name === Categories.history.name) { 230 | list = history; 231 | } else { 232 | list = emojiList[name]; 233 | } 234 | return list.map(emoji => ({ key: emoji.unified, emoji })); 235 | } 236 | })() 237 | return this.props.shouldInclude ? emojiData.filter(e => this.props.shouldInclude(e.emoji)) : emojiData 238 | } 239 | 240 | prerenderEmojis(callback) { 241 | let emojiList = {}; 242 | categoryKeys.forEach(c => { 243 | let name = Categories[c].name; 244 | emojiList[name] = sortEmoji(emojiByCategory(name)); 245 | }); 246 | 247 | this.setState( 248 | { 249 | emojiList, 250 | colSize: Math.floor(this.state.width / this.props.columns) 251 | }, 252 | callback 253 | ); 254 | } 255 | 256 | handleLayout = ({ nativeEvent: { layout } }) => { 257 | this.setState({ width: layout.width }, () => { 258 | this.prerenderEmojis(() => { 259 | this.setState({ isReady: true }); 260 | }); 261 | }); 262 | }; 263 | 264 | // 265 | // LIFECYCLE METHODS 266 | // 267 | componentDidMount() { 268 | const { category, showHistory } = this.props; 269 | this.setState({ category }); 270 | 271 | if (showHistory) { 272 | this.loadHistoryAsync(); 273 | } 274 | } 275 | 276 | render() { 277 | const { 278 | theme, 279 | columns, 280 | placeholder, 281 | showHistory, 282 | showSearchBar, 283 | showSectionTitles, 284 | showTabs, 285 | ...other 286 | } = this.props; 287 | 288 | const { category, colSize, isReady, searchQuery } = this.state; 289 | 290 | const Searchbar = ( 291 | 292 | 302 | 303 | ); 304 | 305 | const title = searchQuery !== "" ? "Search Results" : category.name; 306 | 307 | return ( 308 | 309 | 310 | {showTabs && ( 311 | 317 | )} 318 | 319 | 320 | {showSearchBar && Searchbar} 321 | {isReady ? ( 322 | 323 | 324 | {showSectionTitles && ( 325 | {title} 326 | )} 327 | (this.scrollview = scrollview)} 336 | removeClippedSubviews 337 | /> 338 | 339 | 340 | ) : ( 341 | 342 | 346 | 347 | )} 348 | 349 | 350 | ); 351 | } 352 | } 353 | 354 | EmojiSelector.defaultProps = { 355 | theme: "#007AFF", 356 | category: Categories.all, 357 | showTabs: true, 358 | showSearchBar: true, 359 | showHistory: false, 360 | showSectionTitles: true, 361 | columns: 6, 362 | placeholder: "Search..." 363 | }; 364 | 365 | const styles = StyleSheet.create({ 366 | frame: { 367 | flex: 1, 368 | width: "100%" 369 | }, 370 | loader: { 371 | flex: 1, 372 | alignItems: "center", 373 | justifyContent: "center" 374 | }, 375 | tabBar: { 376 | flexDirection: "row" 377 | }, 378 | scrollview: { 379 | flex: 1 380 | }, 381 | searchbar_container: { 382 | width: "100%", 383 | zIndex: 1, 384 | backgroundColor: "rgba(255,255,255,0.75)" 385 | }, 386 | search: { 387 | ...Platform.select({ 388 | ios: { 389 | height: 36, 390 | paddingLeft: 8, 391 | borderRadius: 10, 392 | backgroundColor: "#E5E8E9" 393 | } 394 | }), 395 | margin: 8 396 | }, 397 | container: { 398 | flex: 1, 399 | flexWrap: "wrap", 400 | flexDirection: "row", 401 | alignItems: "flex-start" 402 | }, 403 | sectionHeader: { 404 | margin: 8, 405 | fontSize: 17, 406 | width: "100%", 407 | color: "#8F8F8F" 408 | } 409 | }); 410 | --------------------------------------------------------------------------------