diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts
index aade4e557a64..3c2849b41e0a 100644
--- a/assets/emojis/index.ts
+++ b/assets/emojis/index.ts
@@ -1,3 +1,5 @@
+import getOperatingSystem from '@libs/getOperatingSystem';
+import CONST from '@src/CONST';
import emojis from './common';
import enEmojis from './en';
import esEmojis from './es';
@@ -31,5 +33,21 @@ const localeEmojis = {
es: esEmojis,
} as const;
-export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis};
-export {skinTones, categoryFrequentlyUsed, default} from './common';
+// On windows, flag emojis are not supported
+const emojisForOperatingSystem =
+ getOperatingSystem() === CONST.OS.WINDOWS
+ ? emojis.slice(
+ 0,
+ emojis.findIndex((emoji) => {
+ if (!('header' in emoji)) {
+ return;
+ }
+
+ return emoji.header && emoji.code === 'flags';
+ }),
+ )
+ : emojis;
+
+export default emojisForOperatingSystem;
+export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis, emojisForOperatingSystem};
+export {skinTones, categoryFrequentlyUsed} from './common';
diff --git a/src/CONST.ts b/src/CONST.ts
index 2e07e88b4314..aca59bd831e6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -946,6 +946,11 @@ const CONST = {
IOS_CAMERAROLL_ACCESS_ERROR: 'Access to photo library was denied',
ADD_PAYMENT_MENU_POSITION_Y: 226,
ADD_PAYMENT_MENU_POSITION_X: 356,
+ EMOJI_PICKER_ITEM_TYPES: {
+ HEADER: 'header',
+ EMOJI: 'emoji',
+ SPACER: 'spacer',
+ },
EMOJI_PICKER_SIZE: {
WIDTH: 320,
HEIGHT: 416,
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
new file mode 100644
index 000000000000..806ab6587917
--- /dev/null
+++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
@@ -0,0 +1,161 @@
+import {FlashList} from '@shopify/flash-list';
+import PropTypes from 'prop-types';
+import React, {useMemo} from 'react';
+import {StyleSheet, Text, View} from 'react-native';
+import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar';
+import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList';
+import refPropTypes from '@components/refPropTypes';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import stylePropTypes from '@styles/stylePropTypes';
+import CONST from '@src/CONST';
+
+const emojiPropTypes = {
+ /** The code of the item */
+ code: PropTypes.string.isRequired,
+
+ /** Whether the item is a header or not */
+ header: PropTypes.bool,
+
+ /** Whether the item is a spacer or not */
+ spacer: PropTypes.bool,
+
+ /** Types of an emoji - e.g. different skin types */
+ types: PropTypes.arrayOf(PropTypes.string),
+};
+
+const propTypes = {
+ /** Indicates if the emoji list is filtered or not */
+ isFiltered: PropTypes.bool.isRequired,
+
+ /** Array of header emojis */
+ headerEmojis: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired,
+
+ /** Function to scroll to a specific header in the emoji list */
+ scrollToHeader: PropTypes.func.isRequired,
+
+ /** Style to be applied to the list wrapper */
+ listWrapperStyle: stylePropTypes,
+
+ /** Reference to the emoji list */
+ forwardedRef: refPropTypes,
+
+ /** The data for the emoji list */
+ data: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired,
+
+ /** Function to render each item in the list */
+ renderItem: PropTypes.func.isRequired,
+
+ /** Extra data to be passed to the list for re-rendering */
+ // eslint-disable-next-line react/forbid-prop-types
+ extraData: PropTypes.any,
+
+ /** Array of indices for the sticky headers */
+ stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
+
+ /** Whether the list should always bounce vertically */
+ alwaysBounceVertical: PropTypes.bool,
+};
+
+const defaultProps = {
+ listWrapperStyle: [],
+ forwardedRef: () => {},
+ extraData: [],
+ stickyHeaderIndices: [],
+ alwaysBounceVertical: false,
+};
+
+/**
+ * Improves FlashList's recycling when there are different types of items
+ * @param {Object} item
+ * @returns {String}
+ */
+const getItemType = (item) => {
+ // item is undefined only when list is empty
+ if (!item) {
+ return;
+ }
+
+ if (item.name) {
+ return CONST.EMOJI_PICKER_ITEM_TYPES.EMOJI;
+ }
+ if (item.header) {
+ return CONST.EMOJI_PICKER_ITEM_TYPES.HEADER;
+ }
+
+ return CONST.EMOJI_PICKER_ITEM_TYPES.SPACER;
+};
+
+/**
+ * Return a unique key for each emoji item
+ *
+ * @param {Object} item
+ * @param {Number} index
+ * @returns {String}
+ */
+const keyExtractor = (item, index) => `emoji_picker_${item.code}_${index}`;
+
+/**
+ * Renders the list empty component
+ * @returns {React.Component}
+ */
+function ListEmptyComponent() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ return {translate('common.noResultsFound')};
+}
+
+function BaseEmojiPickerMenu({headerEmojis, scrollToHeader, isFiltered, listWrapperStyle, forwardedRef, data, renderItem, stickyHeaderIndices, extraData, alwaysBounceVertical}) {
+ const styles = useThemeStyles();
+ const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+
+ const flattenListWrapperStyle = useMemo(() => StyleSheet.flatten(listWrapperStyle), [listWrapperStyle]);
+
+ return (
+ <>
+ {!isFiltered && (
+
+ )}
+
+
+
+
+ >
+ );
+}
+
+BaseEmojiPickerMenu.propTypes = propTypes;
+BaseEmojiPickerMenu.defaultProps = defaultProps;
+BaseEmojiPickerMenu.displayName = 'BaseEmojiPickerMenu';
+
+const BaseEmojiPickerMenuWithRef = React.forwardRef((props, ref) => (
+
+));
+
+BaseEmojiPickerMenuWithRef.displayName = 'BaseEmojiPickerMenuWithRef';
+
+export default BaseEmojiPickerMenuWithRef;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js b/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js
new file mode 100644
index 000000000000..ae345f6fcf56
--- /dev/null
+++ b/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js
@@ -0,0 +1,8 @@
+import PropTypes from 'prop-types';
+
+const emojiPickerMenuPropTypes = {
+ /** Function to add the selected emoji to the main compose text input */
+ onEmojiSelected: PropTypes.func.isRequired,
+};
+
+export default emojiPickerMenuPropTypes;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 36594dabcd30..0791e5113a1d 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -1,121 +1,73 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {FlatList, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {View} from 'react-native';
+import {scrollTo} from 'react-native-reanimated';
import _ from 'underscore';
-import emojiAssets from '@assets/emojis';
-import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
-import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
+import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
-import compose from '@libs/compose';
-import * as EmojiUtils from '@libs/EmojiUtils';
import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
import * as ReportUtils from '@libs/ReportUtils';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
+import BaseEmojiPickerMenu from './BaseEmojiPickerMenu';
+import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes';
+import useEmojiPickerMenu from './useEmojiPickerMenu';
const propTypes = {
- /** Function to add the selected emoji to the main compose text input */
- onEmojiSelected: PropTypes.func.isRequired,
-
/** The ref to the search input (may be null on small screen widths) */
forwardedRef: PropTypes.func,
-
- /** Stores user's preferred skin tone */
- preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- /** Stores user's frequently used emojis */
- // eslint-disable-next-line react/forbid-prop-types
- frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object),
-
- ...withLocalizePropTypes,
+ ...emojiPickerMenuPropTypes,
};
const defaultProps = {
forwardedRef: () => {},
- preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
- frequentlyUsedEmojis: [],
};
const throttleTime = Browser.isMobile() ? 200 : 50;
-function EmojiPickerMenu(props) {
- const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props;
-
+function EmojiPickerMenu({forwardedRef, onEmojiSelected}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
-
- const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
+ const {singleExecution} = useSingleExecution();
+ const {
+ allEmojis,
+ headerEmojis,
+ headerRowIndices,
+ filteredEmojis,
+ headerIndices,
+ setFilteredEmojis,
+ setHeaderIndices,
+ isListFiltered,
+ suggestEmojis,
+ preferredSkinTone,
+ listStyle,
+ emojiListRef,
+ } = useEmojiPickerMenu();
// Ref for the emoji search input
const searchInputRef = useRef(null);
- // Ref for emoji FlatList
- const emojiListRef = useRef(null);
-
// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
// prevent auto focus when open picker for mobile device
const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
- const firstNonHeaderIndex = useRef(0);
-
- /**
- * Calculate the filtered + header emojis and header row indices
- * @returns {Object}
- */
- function getEmojisAndHeaderRowIndices() {
- // If we're on Windows, don't display the flag emojis (the last category),
- // since Windows doesn't support them
- const filteredEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojiAssets);
-
- // Get the header emojis along with the code, index and icon.
- // index is the actual header index starting at the first emoji and counting each one
- const headerEmojis = EmojiUtils.getHeaderEmojis(filteredEmojis);
-
- // 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 to the flatlist
- const headerRowIndices = _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW));
-
- return {filteredEmojis, headerEmojis, headerRowIndices};
- }
-
- const emojis = useRef([]);
- if (emojis.current.length === 0) {
- emojis.current = getEmojisAndHeaderRowIndices().filteredEmojis;
- }
- const headerRowIndices = useRef([]);
- if (headerRowIndices.current.length === 0) {
- headerRowIndices.current = getEmojisAndHeaderRowIndices().headerRowIndices;
- }
- const [headerEmojis, setHeaderEmojis] = useState(() => getEmojisAndHeaderRowIndices().headerEmojis);
-
- const [filteredEmojis, setFilteredEmojis] = useState(emojis.current);
- const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false);
const [selection, setSelection] = useState({start: 0, end: 0});
const [isFocused, setIsFocused] = useState(false);
const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false);
const [highlightFirstEmoji, setHighlightFirstEmoji] = useState(false);
-
- useEffect(() => {
- const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices();
- emojis.current = emojisAndHeaderRowIndices.filteredEmojis;
- headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices;
- setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis);
- setFilteredEmojis(emojis.current);
- setHeaderIndices(headerRowIndices.current);
- }, [frequentlyUsedEmojis]);
+ const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]);
/**
* On text input selection change
@@ -126,14 +78,6 @@ function EmojiPickerMenu(props) {
setSelection(event.nativeEvent.selection);
}, []);
- /**
- * Find and store index of the first emoji item
- * @param {Array} filteredEmojisArr
- */
- function updateFirstNonHeaderIndex(filteredEmojisArr) {
- firstNonHeaderIndex.current = _.findIndex(filteredEmojisArr, (item) => !item.spacer && !item.header);
- }
-
const mouseMoveHandler = useCallback(() => {
if (!arePointerEventsDisabled) {
return;
@@ -141,18 +85,6 @@ function EmojiPickerMenu(props) {
setArePointerEventsDisabled(false);
}, [arePointerEventsDisabled]);
- /**
- * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping
- * the measurement of dynamic content if we know the size (height or width) of items ahead of time.
- * Generate and return an object with properties length(height of each individual row),
- * offset(distance of the current row from the top of the FlatList), index(current row index)
- *
- * @param {*} data FlatList item
- * @param {Number} index row index
- * @returns {Object}
- */
- const getItemLayout = useCallback((data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}), []);
-
/**
* Focuses the search Input and has the text selected
*/
@@ -164,26 +96,23 @@ function EmojiPickerMenu(props) {
}
const filterEmojis = _.throttle((searchTerm) => {
- const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
+ const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm);
+
if (emojiListRef.current) {
- emojiListRef.current.scrollToOffset({offset: 0, animated: false});
+ scrollTo(emojiListRef, 0, 0, false);
}
if (normalizedSearchTerm === '') {
// There are no headers when searching, so we need to re-make them sticky when there is no search term
- setFilteredEmojis(emojis.current);
- setHeaderIndices(headerRowIndices.current);
+ setFilteredEmojis(allEmojis);
+ setHeaderIndices(headerRowIndices);
setHighlightedIndex(-1);
- updateFirstNonHeaderIndex(emojis.current);
setHighlightFirstEmoji(false);
return;
}
- const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length);
-
// Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky
setFilteredEmojis(newFilteredEmojiList);
setHeaderIndices([]);
setHighlightedIndex(0);
- updateFirstNonHeaderIndex(newFilteredEmojiList);
setHighlightFirstEmoji(true);
}, throttleTime);
@@ -223,7 +152,7 @@ function EmojiPickerMenu(props) {
// If nothing is highlighted and an arrow key is pressed
// select the first emoji, apply keyboard movement styles, and disable pointer events
if (highlightedIndex === -1) {
- setHighlightedIndex(firstNonHeaderIndex.current);
+ setHighlightedIndex(firstNonHeaderIndex);
setArePointerEventsDisabled(true);
setIsUsingKeyboardMovement(true);
return;
@@ -253,7 +182,7 @@ function EmojiPickerMenu(props) {
case 'ArrowLeft':
move(
-1,
- () => highlightedIndex - 1 < firstNonHeaderIndex.current,
+ () => highlightedIndex - 1 < firstNonHeaderIndex,
() => {
// Reaching start of the list, arrow left set the focus to searchInput.
focusInputWithTextSelect();
@@ -267,7 +196,7 @@ function EmojiPickerMenu(props) {
case 'ArrowUp':
move(
-CONST.EMOJI_NUM_PER_ROW,
- () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex.current,
+ () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex,
() => {
// Reaching start of the list, arrow up set the focus to searchInput.
focusInputWithTextSelect();
@@ -286,7 +215,7 @@ function EmojiPickerMenu(props) {
setIsUsingKeyboardMovement(true);
}
},
- [filteredEmojis, highlightedIndex, selection.end, selection.start],
+ [filteredEmojis, firstNonHeaderIndex, highlightedIndex, selection.end, selection.start],
);
const keyDownHandler = useCallback(
@@ -376,44 +305,18 @@ function EmojiPickerMenu(props) {
};
}, [forwardedRef, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]);
- useEffect(() => {
- // Find and store index of the first emoji item on mount
- updateFirstNonHeaderIndex(emojis.current);
- }, []);
-
- const scrollToHeader = useCallback((headerIndex) => {
- if (!emojiListRef.current) {
- return;
- }
-
- const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
- emojiListRef.current.flashScrollIndicators();
- emojiListRef.current.scrollToOffset({offset: calculatedOffset, animated: true});
- }, []);
-
- /**
- * @param {Number} skinTone
- */
- const updatePreferredSkinTone = useCallback(
- (skinTone) => {
- if (Number(preferredSkinTone) === Number(skinTone)) {
+ const scrollToHeader = useCallback(
+ (headerIndex) => {
+ if (!emojiListRef.current) {
return;
}
- User.updatePreferredSkinTone(skinTone);
+ const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
+ scrollTo(emojiListRef, 0, calculatedOffset, true);
},
- [preferredSkinTone],
+ [emojiListRef],
);
- /**
- * Return a unique key for each emoji item
- *
- * @param {Object} item
- * @param {Number} index
- * @returns {String}
- */
- const keyExtractor = useCallback((item, index) => `emoji_picker_${item.code}_${index}`, []);
-
/**
* 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
@@ -424,15 +327,15 @@ function EmojiPickerMenu(props) {
* @returns {*}
*/
const renderItem = useCallback(
- ({item, index}) => {
- const {code, header, types} = item;
+ ({item, index, target}) => {
+ const {code, types} = item;
if (item.spacer) {
return null;
}
- if (header) {
+ if (item.header) {
return (
-
+
{translate(`emojiPicker.headers.${code}`)}
);
@@ -445,7 +348,7 @@ function EmojiPickerMenu(props) {
return (
onEmojiSelected(emoji, item)}
+ onPress={singleExecution((emoji) => onEmojiSelected(emoji, item))}
onHoverIn={() => {
setHighlightFirstEmoji(false);
if (!isUsingKeyboardMovement) {
@@ -465,13 +368,20 @@ function EmojiPickerMenu(props) {
/>
);
},
- [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected],
+ [
+ preferredSkinTone,
+ highlightedIndex,
+ isUsingKeyboardMovement,
+ highlightFirstEmoji,
+ singleExecution,
+ styles.emojiHeaderContainer,
+ styles.mh4,
+ styles.textLabelSupporting,
+ translate,
+ onEmojiSelected,
+ ],
);
- const isFiltered = emojis.current.length !== filteredEmojis.length;
- const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight);
- const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight;
- const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8;
return (
0}
/>
- {!isFiltered && (
-
- )}
- overflowLimit ? 'auto' : 'hidden'},
// Set scrollPaddingTop to consider sticky headers while scrolling
- {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT},
+ {scrollPaddingTop: isListFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT},
+ styles.flexShrink1,
]}
- extraData={[filteredEmojis, highlightedIndex, preferredSkinTone]}
+ ref={emojiListRef}
+ data={filteredEmojis}
+ renderItem={renderItem}
+ extraData={[highlightedIndex, preferredSkinTone]}
stickyHeaderIndices={headerIndices}
- getItemLayout={getItemLayout}
- contentContainerStyle={styles.flexGrow1}
- ListEmptyComponent={() => {translate('common.noResultsFound')}}
- />
-
);
}
+EmojiPickerMenu.displayName = 'EmojiPickerMenu';
EmojiPickerMenu.propTypes = propTypes;
EmojiPickerMenu.defaultProps = defaultProps;
@@ -549,14 +445,4 @@ const EmojiPickerMenuWithRef = React.forwardRef((props, ref) => (
EmojiPickerMenuWithRef.displayName = 'EmojiPickerMenuWithRef';
-export default compose(
- withLocalize,
- withOnyx({
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- },
- frequentlyUsedEmojis: {
- key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
- },
- }),
-)(EmojiPickerMenuWithRef);
+export default EmojiPickerMenuWithRef;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index 6ad93538c83b..1463ce736699 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -1,68 +1,42 @@
-import PropTypes from 'prop-types';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import Animated, {runOnUI, scrollTo, useAnimatedRef} from 'react-native-reanimated';
+import {runOnUI, scrollTo} from 'react-native-reanimated';
import _ from 'underscore';
-import emojis from '@assets/emojis';
-import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
-import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
-import * as EmojiUtils from '@libs/EmojiUtils';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
+import BaseEmojiPickerMenu from './BaseEmojiPickerMenu';
+import emojiPickerMenuPropTypes from './emojiPickerMenuPropTypes';
+import useEmojiPickerMenu from './useEmojiPickerMenu';
-const propTypes = {
- /** Function to add the selected emoji to the main compose text input */
- onEmojiSelected: PropTypes.func.isRequired,
+const propTypes = emojiPickerMenuPropTypes;
- /** Stores user's preferred skin tone */
- preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- /** Stores user's frequently used emojis */
- // eslint-disable-next-line react/forbid-prop-types
- frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object),
-
- /** Props related to translation */
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
- frequentlyUsedEmojis: [],
-};
-
-function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) {
+function EmojiPickerMenu({onEmojiSelected}) {
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
- const emojiList = useAnimatedRef();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
- const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
- const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)), [headerEmojis]);
- const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
- const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
- const {windowWidth} = useWindowDimensions();
+ const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
+ const {
+ allEmojis,
+ headerEmojis,
+ headerRowIndices,
+ filteredEmojis,
+ headerIndices,
+ setFilteredEmojis,
+ setHeaderIndices,
+ isListFiltered,
+ suggestEmojis,
+ preferredSkinTone,
+ listStyle,
+ emojiListRef,
+ } = useEmojiPickerMenu();
const {singleExecution} = useSingleExecution();
-
- useEffect(() => {
- setFilteredEmojis(allEmojis);
- }, [allEmojis]);
-
- useEffect(() => {
- setHeaderIndices(headerRowIndices);
- }, [headerRowIndices]);
-
- const getItemLayout = (data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index});
+ const StyleUtils = useStyleUtils();
/**
* Filter the entire list of emojis to only emojis that have the search term in their keywords
@@ -70,10 +44,10 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
* @param {String} searchTerm
*/
const filterEmojis = _.debounce((searchTerm) => {
- const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
+ const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm);
- if (emojiList.current) {
- emojiList.current.scrollToOffset({offset: 0, animated: false});
+ if (emojiListRef.current) {
+ emojiListRef.current.scrollToOffset({offset: 0, animated: false});
}
if (normalizedSearchTerm === '') {
@@ -82,42 +56,20 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
return;
}
- const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);
setFilteredEmojis(newFilteredEmojiList);
- setHeaderIndices(undefined);
+ setHeaderIndices([]);
}, 300);
- /**
- * @param {Number} skinTone
- */
- const updatePreferredSkinTone = (skinTone) => {
- if (preferredSkinTone === skinTone) {
- return;
- }
-
- User.updatePreferredSkinTone(skinTone);
- };
-
const scrollToHeader = (headerIndex) => {
const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
- emojiList.current.flashScrollIndicators();
runOnUI(() => {
'worklet';
- scrollTo(emojiList, 0, calculatedOffset, true);
+ scrollTo(emojiListRef, 0, calculatedOffset, true);
})();
};
- /**
- * Return a unique key for each emoji item
- *
- * @param {Object} item
- * @param {Number} index
- * @returns {String}
- */
- const keyExtractor = (item, index) => `${index}${item.code}`;
-
/**
* 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
@@ -126,34 +78,35 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
* @param {Object} item
* @returns {*}
*/
- const renderItem = ({item}) => {
- const {code, types} = item;
- if (item.spacer) {
- return null;
- }
+ const renderItem = useCallback(
+ ({item, target}) => {
+ const {code, types} = item;
+ if (item.spacer) {
+ return null;
+ }
+
+ if (item.header) {
+ return (
+
+ {translate(`emojiPicker.headers.${code}`)}
+
+ );
+ }
+
+ const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code;
- if (item.header) {
return (
-
- {translate(`emojiPicker.headers.${code}`)}
-
+ onEmojiSelected(emoji, item))}
+ emoji={emojiCode}
+ />
);
- }
-
- const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code;
-
- return (
- onEmojiSelected(emoji, item))}
- emoji={emojiCode}
- />
- );
- };
-
- const isFiltered = allEmojis.length !== filteredEmojis.length;
+ },
+ [styles, windowWidth, preferredSkinTone, singleExecution, onEmojiSelected, translate],
+ );
return (
-
+
0}
/>
- {!isFiltered && (
-
- )}
- {translate('common.noResultsFound')}}
alwaysBounceVertical={filteredEmojis.length !== 0}
/>
-
);
}
EmojiPickerMenu.displayName = 'EmojiPickerMenu';
EmojiPickerMenu.propTypes = propTypes;
-EmojiPickerMenu.defaultProps = defaultProps;
const EmojiPickerMenuWithRef = React.forwardRef((props, ref) => (
(
EmojiPickerMenuWithRef.displayName = 'EmojiPickerMenuWithRef';
-export default compose(
- withLocalize,
- withOnyx({
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- },
- frequentlyUsedEmojis: {
- key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
- },
- }),
-)(EmojiPickerMenuWithRef);
+export default EmojiPickerMenuWithRef;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js
new file mode 100644
index 000000000000..2d895193ec68
--- /dev/null
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js
@@ -0,0 +1,67 @@
+import {useCallback, useEffect, useMemo, useState} from 'react';
+import {useAnimatedRef} from 'react-native-reanimated';
+import _ from 'underscore';
+import emojis from '@assets/emojis';
+import {useFrequentlyUsedEmojis} from '@components/OnyxProvider';
+import useLocalize from '@hooks/useLocalize';
+import usePreferredEmojiSkinTone from '@hooks/usePreferredEmojiSkinTone';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as EmojiUtils from '@libs/EmojiUtils';
+
+const useEmojiPickerMenu = () => {
+ const emojiListRef = useAnimatedRef();
+ const frequentlyUsedEmojis = useFrequentlyUsedEmojis();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
+ const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
+ const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]);
+ const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
+ const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
+ const isListFiltered = allEmojis.length !== filteredEmojis.length;
+ const {preferredLocale} = useLocalize();
+ const [preferredSkinTone] = usePreferredEmojiSkinTone();
+ const {windowHeight} = useWindowDimensions();
+ const StyleUtils = useStyleUtils();
+ const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight);
+
+ useEffect(() => {
+ setFilteredEmojis(allEmojis);
+ }, [allEmojis]);
+
+ useEffect(() => {
+ setHeaderIndices(headerRowIndices);
+ }, [headerRowIndices]);
+
+ /**
+ * Suggest emojis based on the search term
+ * @param {String} searchTerm
+ * @returns {[String, Array]}
+ */
+ const suggestEmojis = useCallback(
+ (searchTerm) => {
+ const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
+ const emojisSuggestions = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);
+
+ return [normalizedSearchTerm, emojisSuggestions];
+ },
+ [allEmojis, preferredLocale],
+ );
+
+ return {
+ allEmojis,
+ headerEmojis,
+ headerRowIndices,
+ filteredEmojis,
+ headerIndices,
+ setFilteredEmojis,
+ setHeaderIndices,
+ isListFiltered,
+ suggestEmojis,
+ preferredSkinTone,
+ listStyle,
+ emojiListRef,
+ };
+};
+
+export default useEmojiPickerMenu;
diff --git a/src/components/EmojiPicker/EmojiSkinToneList.js b/src/components/EmojiPicker/EmojiSkinToneList.js
index 2b574b0b533f..da2559535895 100644
--- a/src/components/EmojiPicker/EmojiSkinToneList.js
+++ b/src/components/EmojiPicker/EmojiSkinToneList.js
@@ -1,60 +1,51 @@
-import PropTypes from 'prop-types';
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import * as Emojis from '@assets/emojis';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
+import usePreferredEmojiSkinTone from '@hooks/usePreferredEmojiSkinTone';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import EmojiPickerMenuItem from './EmojiPickerMenuItem';
import getSkinToneEmojiFromIndex from './getSkinToneEmojiFromIndex';
-const propTypes = {
- /** Stores user's preferred skin tone */
- preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
-
- /** Function to sync the selected skin tone with parent, onyx and nvp */
- updatePreferredSkinTone: PropTypes.func.isRequired,
-
- /** Props related to translation */
- ...withLocalizePropTypes,
-};
-
-function EmojiSkinToneList(props) {
+function EmojiSkinToneList() {
const styles = useThemeStyles();
const [highlightedIndex, setHighlightedIndex] = useState(null);
const [isSkinToneListVisible, setIsSkinToneListVisible] = useState(false);
+ const {translate} = useLocalize();
+ const [preferredSkinTone, setPreferredSkinTone] = usePreferredEmojiSkinTone();
const toggleIsSkinToneListVisible = useCallback(() => {
setIsSkinToneListVisible((prev) => !prev);
}, []);
/**
- * Pass the skinTone to props and hide the picker
+ * Set the preferred skin tone in Onyx and close the skin tone picker
* @param {object} skinToneEmoji
*/
function updateSelectedSkinTone(skinToneEmoji) {
toggleIsSkinToneListVisible();
setHighlightedIndex(skinToneEmoji.skinTone);
- props.updatePreferredSkinTone(skinToneEmoji.skinTone);
+ setPreferredSkinTone(skinToneEmoji.skinTone);
}
- const currentSkinTone = getSkinToneEmojiFromIndex(props.preferredSkinTone);
+ const currentSkinTone = getSkinToneEmojiFromIndex(preferredSkinTone);
return (
{!isSkinToneListVisible && (
{currentSkinTone.code}
- {props.translate('emojiPicker.skinTonePickerLabel')}
+ {translate('emojiPicker.skinTonePickerLabel')}
)}
{isSkinToneListVisible && (
@@ -75,7 +66,6 @@ function EmojiSkinToneList(props) {
);
}
-EmojiSkinToneList.propTypes = propTypes;
EmojiSkinToneList.displayName = 'EmojiSkinToneList';
-export default withLocalize(EmojiSkinToneList);
+export default EmojiSkinToneList;
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index 1009a74ef1f7..124f3558df90 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -13,6 +13,8 @@ const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxConte
const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
+const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
+const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
type OnyxProviderProps = {
/** Rendered child component */
@@ -31,6 +33,8 @@ function OnyxProvider(props: OnyxProviderProps) {
BetasProvider,
ReportCommentDraftsProvider,
PreferredThemeProvider,
+ FrequentlyUsedEmojisProvider,
+ PreferredEmojiSkinToneProvider,
]}
>
{props.children}
@@ -55,4 +59,8 @@ export {
withReportCommentDrafts,
withPreferredTheme,
PreferredThemeContext,
+ withFrequentlyUsedEmojis,
+ useFrequentlyUsedEmojis,
+ withPreferredEmojiSkinTone,
+ PreferredEmojiSkinToneContext,
};
diff --git a/src/hooks/usePreferredEmojiSkinTone.ts b/src/hooks/usePreferredEmojiSkinTone.ts
new file mode 100644
index 000000000000..6eeecdb16617
--- /dev/null
+++ b/src/hooks/usePreferredEmojiSkinTone.ts
@@ -0,0 +1,20 @@
+import {useCallback, useContext} from 'react';
+import {PreferredEmojiSkinToneContext} from '@components/OnyxProvider';
+import * as User from '@userActions/User';
+
+export default function usePreferredEmojiSkinTone() {
+ const preferredSkinTone = useContext(PreferredEmojiSkinToneContext);
+
+ const updatePreferredSkinTone = useCallback(
+ (skinTone: number) => {
+ if (Number(preferredSkinTone) === Number(skinTone)) {
+ return;
+ }
+
+ User.updatePreferredSkinTone(skinTone);
+ },
+ [preferredSkinTone],
+ );
+
+ return [preferredSkinTone, updatePreferredSkinTone];
+}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 71f77689042d..b0178a3cdb46 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1885,7 +1885,6 @@ const styles = (theme: ThemeColors) =>
display: 'flex',
height: CONST.EMOJI_PICKER_HEADER_HEIGHT,
justifyContent: 'center',
- width: '100%',
},
emojiSkinToneTitle: {
@@ -1906,12 +1905,13 @@ const styles = (theme: ThemeColors) =>
},
emojiItem: {
- width: '12.5%',
+ width: '100%',
textAlign: 'center',
borderRadius: 8,
paddingTop: 2,
paddingBottom: 2,
height: CONST.EMOJI_PICKER_ITEM_HEIGHT,
+ flexShrink: 1,
...userSelect.userSelectNone,
},
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 7a5df1657c99..a7bc368983b5 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -18,7 +18,6 @@ import createTooltipStyleUtils from './generators/TooltipStyleUtils';
import getContextMenuItemStyles from './getContextMenuItemStyles';
import {compactContentContainerStyles} from './optionRowStyles';
import positioning from './positioning';
-import spacing from './spacing';
import {
AllStyles,
AvatarSize,
@@ -847,15 +846,14 @@ function displayIfTrue(condition: boolean): ViewStyle {
/**
* Gets the correct height for emoji picker list based on screen dimensions
*/
-function getEmojiPickerListHeight(hasAdditionalSpace: boolean, windowHeight: number): ViewStyle {
+function getEmojiPickerListHeight(isRenderingShortcutRow: boolean, windowHeight: number): ViewStyle {
const style = {
- ...spacing.ph4,
- height: hasAdditionalSpace ? CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT : CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT,
+ height: isRenderingShortcutRow ? CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT : CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT,
};
if (windowHeight) {
// dimensions of content above the emoji picker list
- const dimensions = hasAdditionalSpace ? CONST.EMOJI_PICKER_TEXT_INPUT_SIZES : CONST.EMOJI_PICKER_TEXT_INPUT_SIZES + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT;
+ const dimensions = isRenderingShortcutRow ? CONST.EMOJI_PICKER_TEXT_INPUT_SIZES + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT : CONST.EMOJI_PICKER_TEXT_INPUT_SIZES;
return {
...style,
maxHeight: windowHeight - dimensions,