diff --git a/assets/emojis.js b/assets/emojis.js
index 6fe6ddf0f06a..358be42555f8 100644
--- a/assets/emojis.js
+++ b/assets/emojis.js
@@ -1728,10 +1728,6 @@ const emojis = [
'sleep',
],
},
- {
- code: 'peopleAndBody',
- header: true,
- },
{
name: 'wave',
code: '👋',
diff --git a/assets/images/emojiCategoryIcons/add-emoji.svg b/assets/images/emojiCategoryIcons/add-emoji.svg
new file mode 100644
index 000000000000..5cec67508e4b
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/add-emoji.svg
@@ -0,0 +1,25 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/calendar.svg b/assets/images/emojiCategoryIcons/calendar.svg
new file mode 100644
index 000000000000..18885029a7c8
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/calendar.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/car.svg b/assets/images/emojiCategoryIcons/car.svg
new file mode 100644
index 000000000000..e5cde58b2615
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/car.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/flag.svg b/assets/images/emojiCategoryIcons/flag.svg
new file mode 100644
index 000000000000..e72787c3665b
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/flag.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/hamburger.svg b/assets/images/emojiCategoryIcons/hamburger.svg
new file mode 100644
index 000000000000..52945988effc
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/hamburger.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/heart.svg b/assets/images/emojiCategoryIcons/heart.svg
new file mode 100644
index 000000000000..95e73f329cfa
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/heart.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/light-bulb.svg b/assets/images/emojiCategoryIcons/light-bulb.svg
new file mode 100644
index 000000000000..0e6a33c041df
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/light-bulb.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/peace-sign.svg b/assets/images/emojiCategoryIcons/peace-sign.svg
new file mode 100644
index 000000000000..ab76642fc48d
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/peace-sign.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/plane.svg b/assets/images/emojiCategoryIcons/plane.svg
new file mode 100644
index 000000000000..17aca931f8a3
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/plane.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/plant.svg b/assets/images/emojiCategoryIcons/plant.svg
new file mode 100644
index 000000000000..a17ed231e1df
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/plant.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/assets/images/emojiCategoryIcons/soccer-ball.svg b/assets/images/emojiCategoryIcons/soccer-ball.svg
new file mode 100644
index 000000000000..40fa05516a11
--- /dev/null
+++ b/assets/images/emojiCategoryIcons/soccer-ball.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/CONST.js b/src/CONST.js
index 5dd6201d61ca..012a85f63829 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -485,6 +485,11 @@ const CONST = {
EMOJI_SPACER: 'SPACER',
+ // This is the number of columns in each row of the picker.
+ // Because of how flatList implements these rows, each row is an index rather than each element
+ // For this reason to make headers work, we need to have the header be the only rendered element in its row
+ // If this number is changed, emojis.js will need to be updated to have the proper number of spacer elements
+ // around each header.
EMOJI_NUM_PER_ROW: 8,
EMOJI_FREQUENT_ROW_COUNT: 3,
@@ -531,9 +536,9 @@ const CONST = {
ADD_PAYMENT_MENU_POSITION_X: 356,
EMOJI_PICKER_SIZE: {
WIDTH: 320,
- HEIGHT: 390,
+ HEIGHT: 392,
},
- NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 288,
+ NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256,
EMOJI_PICKER_ITEM_HEIGHT: 32,
EMOJI_PICKER_HEADER_HEIGHT: 32,
COMPOSER_MAX_HEIGHT: 125,
diff --git a/src/components/EmojiPicker/CategoryShortcutBar.js b/src/components/EmojiPicker/CategoryShortcutBar.js
new file mode 100644
index 000000000000..0114df692850
--- /dev/null
+++ b/src/components/EmojiPicker/CategoryShortcutBar.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import _ from 'underscore';
+import styles from '../../styles/styles';
+import FrequentlyUsed from '../../../assets/images/history.svg';
+import Smiley from '../../../assets/images/emoji.svg';
+import AnimalsAndNature from '../../../assets/images/emojiCategoryIcons/plant.svg';
+import FoodAndDrink from '../../../assets/images/emojiCategoryIcons/hamburger.svg';
+import TravelAndPlaces from '../../../assets/images/emojiCategoryIcons/plane.svg';
+import Activities from '../../../assets/images/emojiCategoryIcons/soccer-ball.svg';
+import Objects from '../../../assets/images/emojiCategoryIcons/light-bulb.svg';
+import Symbols from '../../../assets/images/emojiCategoryIcons/peace-sign.svg';
+import Flags from '../../../assets/images/emojiCategoryIcons/flag.svg';
+import CategoryShortcutButton from './CategoryShortcutButton';
+
+const propTypes = {
+ /** The function to call when an emoji is selected */
+ onPress: PropTypes.func.isRequired,
+
+ /** The indices that the icons should link to */
+ headerIndices: PropTypes.arrayOf(PropTypes.number).isRequired,
+};
+
+const CategoryShortcutBar = (props) => {
+ const icons = [Smiley, AnimalsAndNature, FoodAndDrink, TravelAndPlaces, Activities, Objects, Symbols, Flags];
+
+ // If the user has frequently used emojis, there will be 9 headers, otherwise there will be 8
+ if (props.headerIndices.length === 9) {
+ icons.unshift(FrequentlyUsed);
+ }
+
+ return (
+
+ {_.map(props.headerIndices, (headerIndex, i) => (
+ props.onPress(headerIndex)}
+ key={`categoryShortcut${i}`}
+ />
+ ))}
+
+ );
+};
+CategoryShortcutBar.propTypes = propTypes;
+CategoryShortcutBar.displayName = 'CategoryShortcutBar';
+
+export default CategoryShortcutBar;
diff --git a/src/components/EmojiPicker/CategoryShortcutButton.js b/src/components/EmojiPicker/CategoryShortcutButton.js
new file mode 100644
index 000000000000..3b5d43f9b10d
--- /dev/null
+++ b/src/components/EmojiPicker/CategoryShortcutButton.js
@@ -0,0 +1,53 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {Pressable, View} from 'react-native';
+import Icon from '../Icon';
+import variables from '../../styles/variables';
+import styles from '../../styles/styles';
+import * as StyleUtils from '../../styles/StyleUtils';
+import getButtonState from '../../libs/getButtonState';
+import themeColors from '../../styles/themes/default';
+
+const propTypes = {
+ /** The icon representation of the category that this button links to */
+ icon: PropTypes.func.isRequired,
+
+ /** The function to call when an emoji is selected */
+ onPress: PropTypes.func.isRequired,
+};
+
+class CategoryShortcutButton extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isHighlighted: false,
+ };
+ }
+
+ render() {
+ return (
+ this.setState({isHighlighted: true})}
+ onHoverOut={() => this.setState({isHighlighted: false})}
+ style={({pressed}) => ([
+ StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)),
+ styles.categoryShortcutButton,
+ this.state.isHighlighted && styles.emojiItemHighlighted,
+ ])}
+ >
+
+
+
+
+ );
+ }
+}
+CategoryShortcutButton.propTypes = propTypes;
+
+export default CategoryShortcutButton;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index e2ad3bed1b78..810fc49040d7 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -8,11 +8,9 @@ import CONST from '../../../CONST';
import ONYXKEYS from '../../../ONYXKEYS';
import styles from '../../../styles/styles';
import * as StyleUtils from '../../../styles/StyleUtils';
-import themeColors from '../../../styles/themes/default';
import emojis from '../../../../assets/emojis';
import EmojiPickerMenuItem from '../EmojiPickerMenuItem';
import Text from '../../Text';
-import Composer from '../../Composer';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import compose from '../../../libs/compose';
@@ -20,6 +18,8 @@ import getOperatingSystem from '../../../libs/getOperatingSystem';
import * as User from '../../../libs/actions/User';
import EmojiSkinToneList from '../EmojiSkinToneList';
import * as EmojiUtils from '../../../libs/EmojiUtils';
+import CategoryShortcutBar from '../CategoryShortcutBar';
+import TextInput from '../../TextInput';
const propTypes = {
/** Function to add the selected emoji to the main compose text input */
@@ -57,24 +57,20 @@ class EmojiPickerMenu extends Component {
// Ref for emoji FlatList
this.emojiList = undefined;
- // This is the number of columns in each row of the picker.
- // Because of how flatList implements these rows, each row is an index rather than each element
- // For this reason to make headers work, we need to have the header be the only rendered element in its row
- // If this number is changed, emojis.js will need to be updated to have the proper number of spacer elements
- // around each header.
- this.numColumns = CONST.EMOJI_NUM_PER_ROW;
-
const allEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis, this.props.frequentlyUsedEmojis);
- // This is the indices of each category of emojis
+ // This is the actual header index starting at the first emoji and counting each one
+ this.headerIndices = EmojiUtils.getHeaderIndices(allEmojis);
+
+ // This is the indices of each header's Row
// The positions are static, and are calculated as index/numColumns (8 in our case)
- // This is because each row of 8 emojis counts as one index
- this.unfilteredHeaderIndices = EmojiUtils.getDynamicHeaderIndices(allEmojis);
+ // This is because each row of 8 emojis counts as one index to the flatlist
+ this.headerRowIndices = _.map(this.headerIndices, headerIndex => Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW));
// If we're on Windows, don't display the flag emojis (the last category),
// since Windows doesn't support them (and only displays country codes instead)
this.emojis = getOperatingSystem() === CONST.OS.WINDOWS
- ? allEmojis.slice(0, this.unfilteredHeaderIndices.pop() * this.numColumns)
+ ? allEmojis.slice(0, this.headerRowIndices.pop() * CONST.EMOJI_NUM_PER_ROW)
: allEmojis;
this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300);
@@ -88,13 +84,14 @@ class EmojiPickerMenu extends Component {
this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this);
this.getItemLayout = this.getItemLayout.bind(this);
+ this.scrollToHeader = this.scrollToHeader.bind(this);
this.currentScrollOffset = 0;
this.firstNonHeaderIndex = 0;
this.state = {
filteredEmojis: this.emojis,
- headerIndices: this.unfilteredHeaderIndices,
+ headerIndices: this.headerRowIndices,
highlightedIndex: -1,
arePointerEventsDisabled: false,
selection: {
@@ -301,8 +298,8 @@ class EmojiPickerMenu extends Component {
switch (arrowKey) {
case 'ArrowDown':
move(
- this.numColumns,
- () => this.state.highlightedIndex + this.numColumns > this.state.filteredEmojis.length - 1,
+ CONST.EMOJI_NUM_PER_ROW,
+ () => this.state.highlightedIndex + CONST.EMOJI_NUM_PER_ROW > this.state.filteredEmojis.length - 1,
);
break;
case 'ArrowLeft':
@@ -319,8 +316,8 @@ class EmojiPickerMenu extends Component {
break;
case 'ArrowUp':
move(
- -this.numColumns,
- () => this.state.highlightedIndex - this.numColumns < this.firstNonHeaderIndex,
+ -CONST.EMOJI_NUM_PER_ROW,
+ () => this.state.highlightedIndex - CONST.EMOJI_NUM_PER_ROW < this.firstNonHeaderIndex,
() => {
// Reaching start of the list, arrow up set the focus to searchInput.
this.focusInputWithTextSelect();
@@ -339,25 +336,23 @@ class EmojiPickerMenu extends Component {
}
}
+ scrollToHeader(headerIndex) {
+ const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
+ this.emojiList.flashScrollIndicators();
+ this.emojiList.scrollToOffset({offset: calculatedOffset, animated: true});
+ }
+
/**
* Calculates the required scroll offset (aka distance from top) and scrolls the FlatList to the highlighted emoji
* if any portion of it falls outside of the window.
* Doing this because scrollToIndex doesn't work as expected.
*/
scrollToHighlightedIndex() {
- // If there are headers in the emoji array, so we need to offset by their heights as well
- let numHeaders = 0;
- if (this.state.filteredEmojis.length === this.emojis.length) {
- numHeaders = _.filter(this.unfilteredHeaderIndices, i => this.state.highlightedIndex > i * this.numColumns).length;
- }
-
- // Calculate the scroll offset at the bottom of the currently highlighted emoji
- // (subtract numHeaders because the highlightedIndex includes them, and add 1 to include the current row)
- const numEmojiRows = (Math.floor(this.state.highlightedIndex / this.numColumns) - numHeaders) + 1;
+ // Calculate the number of rows above the current row, then add 1 to include the current row
+ const numRows = Math.floor(this.state.highlightedIndex / CONST.EMOJI_NUM_PER_ROW) + 1;
// The scroll offsets at the top and bottom of the highlighted emoji
- const offsetAtEmojiBottom = ((numHeaders) * CONST.EMOJI_PICKER_HEADER_HEIGHT)
- + (numEmojiRows * CONST.EMOJI_PICKER_ITEM_HEIGHT);
+ const offsetAtEmojiBottom = numRows * CONST.EMOJI_PICKER_HEADER_HEIGHT;
const offsetAtEmojiTop = offsetAtEmojiBottom - CONST.EMOJI_PICKER_ITEM_HEIGHT;
// Scroll to fit the entire highlighted emoji into the window if we need to
@@ -388,7 +383,7 @@ class EmojiPickerMenu extends Component {
// There are no headers when searching, so we need to re-make them sticky when there is no search term
this.setState({
filteredEmojis: this.emojis,
- headerIndices: this.unfilteredHeaderIndices,
+ headerIndices: this.headerRowIndices,
highlightedIndex: -1,
});
this.setFirstNonHeaderIndex(this.emojis);
@@ -439,7 +434,7 @@ class EmojiPickerMenu extends Component {
if (header) {
return (
-
+
{this.props.translate(`emojiPicker.headers.${code}`)}
@@ -473,14 +468,15 @@ class EmojiPickerMenu extends Component {
style={[styles.emojiPickerContainer, StyleUtils.getEmojiPickerStyle(this.props.isSmallScreenWidth)]}
pointerEvents={this.state.arePointerEventsDisabled ? 'none' : 'auto'}
>
+
{!this.props.isSmallScreenWidth && (
-
-
+ this.searchInput = el}
autoFocus
@@ -512,7 +508,7 @@ class EmojiPickerMenu extends Component {
data={this.state.filteredEmojis}
renderItem={this.renderItem}
keyExtractor={item => `emoji_picker_${item.code}`}
- numColumns={this.numColumns}
+ numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[
styles.emojiPickerList,
this.isMobileLandscape() && styles.emojiPickerListLandscape,
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index d70eed98800b..edf383eda1d8 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -1,7 +1,9 @@
import React, {Component} from 'react';
-import {View, FlatList} from 'react-native';
+import {View, findNodeHandle} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
+import _ from 'underscore';
+import Animated, {runOnUI, _scrollTo} from 'react-native-reanimated';
import compose from '../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
import CONST from '../../../CONST';
@@ -14,6 +16,7 @@ import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import EmojiSkinToneList from '../EmojiSkinToneList';
import * as EmojiUtils from '../../../libs/EmojiUtils';
import * as User from '../../../libs/actions/User';
+import CategoryShortcutBar from '../CategoryShortcutBar';
const propTypes = {
/** Function to add the selected emoji to the main compose text input */
@@ -43,23 +46,28 @@ class EmojiPickerMenu extends Component {
constructor(props) {
super(props);
- // This is the number of columns in each row of the picker.
- // Because of how flatList implements these rows, each row is an index rather than each element
- // For this reason to make headers work, we need to have the header be the only rendered element in its row
- // If this number is changed, emojis.js will need to be updated to have the proper number of spacer elements
- // around each header.
- this.numColumns = CONST.EMOJI_NUM_PER_ROW;
+ // Ref for emoji FlatList
+ this.emojiList = undefined;
this.emojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis, this.props.frequentlyUsedEmojis);
- // This is the indices of each category of emojis
+ // This is the actual header index starting at the first emoji and counting each one
+ this.headerIndices = EmojiUtils.getHeaderIndices(this.emojis);
+
+ // This is the indices of each header's Row
// The positions are static, and are calculated as index/numColumns (8 in our case)
- // This is because each row of 8 emojis counts as one index
- this.unfilteredHeaderIndices = EmojiUtils.getDynamicHeaderIndices(this.emojis);
+ // This is because each row of 8 emojis counts as one index to the flatlist
+ this.headerRowIndices = _.map(this.headerIndices, headerIndex => Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW));
this.renderItem = this.renderItem.bind(this);
this.isMobileLandscape = this.isMobileLandscape.bind(this);
this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
+ this.scrollToHeader = this.scrollToHeader.bind(this);
+ this.getItemLayout = this.getItemLayout.bind(this);
+ }
+
+ getItemLayout(data, index) {
+ return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index};
}
/**
@@ -91,6 +99,17 @@ class EmojiPickerMenu extends Component {
User.updatePreferredSkinTone(skinTone);
}
+ scrollToHeader(headerIndex) {
+ const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
+ this.emojiList.flashScrollIndicators();
+ const node = findNodeHandle(this.emojiList);
+ runOnUI(() => {
+ 'worklet';
+
+ _scrollTo(node, 0, calculatedOffset, true);
+ })();
+ }
+
/**
* Given an emoji item object, render a component based on its type.
* Items with the code "SPACER" return nothing and are used to fill rows up to 8
@@ -108,7 +127,7 @@ class EmojiPickerMenu extends Component {
if (item.header) {
return (
-
+
{this.props.translate(`emojiPicker.headers.${item.code}`)}
@@ -130,16 +149,25 @@ class EmojiPickerMenu extends Component {
render() {
return (
-
+
+
+ this.emojiList = el}
data={this.emojis}
renderItem={this.renderItem}
keyExtractor={item => (`emoji_picker_${item.code}`)}
- numColumns={this.numColumns}
+ numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[
styles.emojiPickerList,
this.isMobileLandscape() && styles.emojiPickerListLandscape,
]}
- stickyHeaderIndices={this.unfilteredHeaderIndices}
+ stickyHeaderIndices={this.headerRowIndices}
+ getItemLayout={this.getItemLayout}
+ showsVerticalScrollIndicator
/>
{
if (!emoji.header) {
return;
}
- headerIndices.push(Math.floor(index / CONST.EMOJI_NUM_PER_ROW));
+ headerIndices.push(index);
});
return headerIndices;
}
@@ -246,7 +246,7 @@ function suggestEmojis(text, limit = 5) {
}
export {
- getDynamicHeaderIndices,
+ getHeaderIndices,
mergeEmojisWithFrequentlyUsedEmojis,
addToFrequentlyUsedEmojis,
containsOnlyEmojis,
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 277319de3373..7d0c1b0d9189 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1466,13 +1466,6 @@ const styles = {
width: '100%',
},
- emojiHeaderStyle: {
- fontFamily: fontFamily.EXP_NEUE_BOLD,
- fontWeight: fontWeightBold,
- color: themeColors.heading,
- fontSize: variables.fontSizeSmall,
- },
-
emojiSkinToneTitle: {
backgroundColor: themeColors.componentBG,
width: '100%',
@@ -1514,6 +1507,15 @@ const styles = {
borderRadius: variables.buttonBorderRadius,
},
+ categoryShortcutButton: {
+ flex: 1,
+ borderRadius: 8,
+ paddingTop: 2,
+ paddingBottom: 2,
+ height: CONST.EMOJI_PICKER_ITEM_HEIGHT,
+ justifyContent: 'center',
+ },
+
chatItemEmojiButton: {
alignSelf: 'flex-end',
borderRadius: variables.buttonBorderRadius,
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index 08259eaf77e9..2283bd63d046 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -354,6 +354,10 @@ export default {
paddingTop: 0,
},
+ pt1: {
+ paddingTop: 4,
+ },
+
pt2: {
paddingTop: 8,
},