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, },