From 943a2d3c7b16bad66d3261db54b2a9a63d544f6d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:19 +0100 Subject: [PATCH 01/28] initial migration to flashlist --- .../EmojiPicker/EmojiPickerMenu/index.js | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 36594dabcd30..3efce1e33121 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -23,6 +23,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import { FlashList } from '@shopify/flash-list'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -141,18 +142,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 */ @@ -507,13 +496,8 @@ function EmojiPickerMenu(props) { onPress={scrollToHeader} /> )} - + {translate('common.noResultsFound')}} + estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} /> + Date: Fri, 15 Dec 2023 10:36:20 +0100 Subject: [PATCH 02/28] display emojis correctly with flashlist --- .../EmojiPicker/EmojiPickerMenu/index.js | 131 ++++++++++-------- src/styles/index.ts | 2 +- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 3efce1e33121..b2ff0497427c 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,7 +1,8 @@ +import {FlashList} from '@shopify/flash-list'; 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 {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import emojiAssets from '@assets/emojis'; @@ -23,7 +24,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import { FlashList } from '@shopify/flash-list'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -82,10 +82,8 @@ function EmojiPickerMenu(props) { // 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)); + // FlashList renders items defined as null, so we have to store each header's row index to set their style properly + const headerRowIndices = _.map(headerEmojis, (headerEmoji) => headerEmoji.index); return {filteredEmojis, headerEmojis, headerRowIndices}; } @@ -462,62 +460,75 @@ function EmojiPickerMenu(props) { const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; return ( - - - { - setHighlightedIndex(-1); - setIsFocused(true); - setIsUsingKeyboardMovement(false); - }} - onBlur={() => setIsFocused(false)} - autoCorrect={false} - blurOnSubmit={filteredEmojis.length > 0} - /> - - {!isFiltered && ( - - )} - Date: Fri, 15 Dec 2023 10:36:21 +0100 Subject: [PATCH 05/28] correctly display changing skin tone --- src/styles/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/index.ts b/src/styles/index.ts index 61cf9afdd433..df374766137a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1878,6 +1878,7 @@ const styles = (theme: ThemeColors) => paddingTop: 2, paddingBottom: 2, height: CONST.EMOJI_PICKER_ITEM_HEIGHT, + flexShrink: 1, ...userSelect.userSelectNone, }, From 6a6e929aa443302e80aac145e04bff183a215efc Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:21 +0100 Subject: [PATCH 06/28] scroll to offset --- .../EmojiPicker/EmojiPickerMenu/index.js | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index c0659efb167d..9b3420667022 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -506,26 +506,26 @@ function EmojiPickerMenu(props) { {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} > - {translate('common.noResultsFound')}} - estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} - getItemType={(item) => { - if (item.header) { - return 'header'; - } - if (item.spacer) { - return 'spacer'; - } - return 'emoji'; - }} - /> + {translate('common.noResultsFound')}} + estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} + getItemType={(item) => { + if (item.header) { + return 'header'; + } + if (item.spacer) { + return 'spacer'; + } + return 'emoji'; + }} + /> Date: Fri, 15 Dec 2023 10:36:22 +0100 Subject: [PATCH 07/28] scroll to offset --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 9b3420667022..20a045a26c2b 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -374,7 +374,6 @@ function EmojiPickerMenu(props) { } 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}); }, []); From 5dc95a5b1f0c5be8f7d13986ddecc7ba6ded1ac6 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:22 +0100 Subject: [PATCH 08/28] display headers on native correctly --- .../EmojiPickerMenu/index.native.js | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 6ad93538c83b..17c0c8fef895 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -1,8 +1,9 @@ +import {FlashList} from '@shopify/flash-list'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} 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, useAnimatedRef} from 'react-native-reanimated'; import _ from 'underscore'; import emojis from '@assets/emojis'; import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; @@ -48,7 +49,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t // 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 headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); const {windowWidth} = useWindowDimensions(); @@ -62,8 +63,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t setHeaderIndices(headerRowIndices); }, [headerRowIndices]); - const getItemLayout = (data, index) => ({length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index}); - /** * Filter the entire list of emojis to only emojis that have the search term in their keywords * @@ -101,7 +100,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t const scrollToHeader = (headerIndex) => { const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT; - emojiList.current.flashScrollIndicators(); runOnUI(() => { 'worklet'; @@ -134,7 +132,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t if (item.header) { return ( - + {translate(`emojiPicker.headers.${code}`)} ); @@ -169,28 +167,41 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t onPress={scrollToHeader} /> )} - {translate('common.noResultsFound')}} - alwaysBounceVertical={filteredEmojis.length !== 0} - /> + > + {translate('common.noResultsFound')}} + alwaysBounceVertical={filteredEmojis.length !== 0} + estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} + extraData={[preferredSkinTone]} + getItemType={(item) => { + if (!item) { + return; + } + if (item.header) { + return 'header'; + } + if (item.spacer) { + return 'spacer'; + } + return 'emoji'; + }} + /> + Date: Fri, 15 Dec 2023 10:36:22 +0100 Subject: [PATCH 09/28] display scrollbar --- src/components/EmojiPicker/EmojiPickerMenu/index.js | 5 +++-- src/components/EmojiPicker/EmojiPickerMenu/index.native.js | 5 +++-- src/styles/utils/index.ts | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 20a045a26c2b..b1630b2545b6 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -410,7 +410,7 @@ function EmojiPickerMenu(props) { * @returns {*} */ const renderItem = useCallback( - ({item, index}) => { + ({item, index, target}) => { const {code, header, types} = item; if (item.spacer) { return null; @@ -418,7 +418,7 @@ function EmojiPickerMenu(props) { if (header) { return ( - + {translate(`emojiPicker.headers.${code}`)} ); @@ -515,6 +515,7 @@ function EmojiPickerMenu(props) { stickyHeaderIndices={headerIndices} ListEmptyComponent={() => {translate('common.noResultsFound')}} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} + contentContainerStyle={styles.ph4} getItemType={(item) => { if (item.header) { return 'header'; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 17c0c8fef895..17ff84c0c1ac 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -124,7 +124,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t * @param {Object} item * @returns {*} */ - const renderItem = ({item}) => { + const renderItem = ({item, target}) => { const {code, types} = item; if (item.spacer) { return null; @@ -132,7 +132,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t if (item.header) { return ( - + {translate(`emojiPicker.headers.${code}`)} ); @@ -187,6 +187,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t ListEmptyComponent={{translate('common.noResultsFound')}} alwaysBounceVertical={filteredEmojis.length !== 0} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} + contentContainerStyle={styles.ph4} extraData={[preferredSkinTone]} getItemType={(item) => { if (!item) { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index c392e61f0814..2cf782bb9609 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -849,7 +849,6 @@ function displayIfTrue(condition: boolean): ViewStyle { */ function getEmojiPickerListHeight(hasAdditionalSpace: 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, }; From 79ac9ba15280d5b12b205696c56cad8fb6ccb9be Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:23 +0100 Subject: [PATCH 10/28] update getItemType --- .../EmojiPicker/EmojiPickerMenu/index.js | 27 ++++++++++------ .../EmojiPickerMenu/index.native.js | 32 +++++++++++-------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index b1630b2545b6..f6d6d70bcebb 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -454,6 +454,21 @@ function EmojiPickerMenu(props) { [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); + /** + * Improves FlashList's recycling when there are different types of items + * @param {Object} item + * @returns {String} + */ + const getItemType = (item) => { + if (item.header) { + return 'header'; + } + if (item.spacer) { + return 'spacer'; + } + return 'emoji'; + }; + 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; @@ -499,7 +514,7 @@ function EmojiPickerMenu(props) { listStyle, // This prevents elastic scrolling when scroll reaches the start or end {overscrollBehaviorY: 'contain'}, - // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList + // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlashList {overflowY: filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, // Set scrollPaddingTop to consider sticky headers while scrolling {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, @@ -516,15 +531,7 @@ function EmojiPickerMenu(props) { ListEmptyComponent={() => {translate('common.noResultsFound')}} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} contentContainerStyle={styles.ph4} - getItemType={(item) => { - if (item.header) { - return 'header'; - } - if (item.spacer) { - return 'spacer'; - } - return 'emoji'; - }} + getItemType={getItemType} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 17ff84c0c1ac..2765b38bb828 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -148,6 +148,24 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t ); }; + /** + * Improves FlashList's recycling when there are different types of items + * @param {Object} item + * @returns {String} + */ + const getItemType = (item) => { + if (!item) { + return; + } + if (item.header) { + return 'header'; + } + if (item.spacer) { + return 'spacer'; + } + return 'emoji'; + }; + const isFiltered = allEmojis.length !== filteredEmojis.length; return ( @@ -183,24 +201,12 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t keyExtractor={keyExtractor} numColumns={CONST.EMOJI_NUM_PER_ROW} stickyHeaderIndices={headerIndices} - showsVerticalScrollIndicator ListEmptyComponent={{translate('common.noResultsFound')}} alwaysBounceVertical={filteredEmojis.length !== 0} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} contentContainerStyle={styles.ph4} extraData={[preferredSkinTone]} - getItemType={(item) => { - if (!item) { - return; - } - if (item.header) { - return 'header'; - } - if (item.spacer) { - return 'spacer'; - } - return 'emoji'; - }} + getItemType={getItemType} /> Date: Fri, 15 Dec 2023 10:36:23 +0100 Subject: [PATCH 11/28] create separate file for getItemType --- .../EmojiPickerMenu/getItemType.js | 16 ++++++++++++++ .../EmojiPicker/EmojiPickerMenu/index.js | 18 ++-------------- .../EmojiPickerMenu/index.native.js | 21 ++----------------- 3 files changed, 20 insertions(+), 35 deletions(-) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/getItemType.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js new file mode 100644 index 000000000000..fca766404807 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js @@ -0,0 +1,16 @@ +/** + * Improves FlashList's recycling when there are different types of items + * @param {Object} item + * @returns {String} + */ +function getItemType(item) { + if (item.header) { + return 'header'; + } + if (item.spacer) { + return 'spacer'; + } + return 'emoji'; +} + +export default getItemType; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index f6d6d70bcebb..4dd61decc66a 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -24,6 +24,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import getItemType from './getItemType'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -418,7 +419,7 @@ function EmojiPickerMenu(props) { if (header) { return ( - + {translate(`emojiPicker.headers.${code}`)} ); @@ -454,21 +455,6 @@ function EmojiPickerMenu(props) { [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); - /** - * Improves FlashList's recycling when there are different types of items - * @param {Object} item - * @returns {String} - */ - const getItemType = (item) => { - if (item.header) { - return 'header'; - } - if (item.spacer) { - return 'spacer'; - } - return 'emoji'; - }; - 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; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 2765b38bb828..ce2536291b8d 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -21,6 +21,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import getItemType from './getItemType'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -148,24 +149,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t ); }; - /** - * Improves FlashList's recycling when there are different types of items - * @param {Object} item - * @returns {String} - */ - const getItemType = (item) => { - if (!item) { - return; - } - if (item.header) { - return 'header'; - } - if (item.spacer) { - return 'spacer'; - } - return 'emoji'; - }; - const isFiltered = allEmojis.length !== filteredEmojis.length; return ( @@ -205,7 +188,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t alwaysBounceVertical={filteredEmojis.length !== 0} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} contentContainerStyle={styles.ph4} - extraData={[preferredSkinTone]} + extraData={[filteredEmojis, preferredSkinTone]} getItemType={getItemType} /> From cfd8066da35285f3dd3e95555f7a374d07a73e83 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:24 +0100 Subject: [PATCH 12/28] update getItemType and list style --- .../EmojiPickerMenu/getItemType.js | 4 + .../EmojiPicker/EmojiPickerMenu/index.js | 111 +++++++++--------- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js index fca766404807..3196a989acc9 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js @@ -4,6 +4,10 @@ * @returns {String} */ function getItemType(item) { + // item is undefined only when list is empty + if (!item) { + return; + } if (item.header) { return 'header'; } diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 4dd61decc66a..f16ddd61e413 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -460,66 +460,65 @@ function EmojiPickerMenu(props) { const height = !listStyle.maxHeight || listStyle.height < listStyle.maxHeight ? listStyle.height : listStyle.maxHeight; const overflowLimit = Math.floor(height / CONST.EMOJI_PICKER_ITEM_HEIGHT) * 8; return ( - + + + { + setHighlightedIndex(-1); + setIsFocused(true); + setIsUsingKeyboardMovement(false); + }} + onBlur={() => setIsFocused(false)} + autoCorrect={false} + blurOnSubmit={filteredEmojis.length > 0} + /> + + {!isFiltered && ( + + )} Date: Fri, 15 Dec 2023 10:36:24 +0100 Subject: [PATCH 13/28] code review updates --- src/CONST.ts | 5 ++ .../EmojiPickerMenu/getItemType.js | 8 ++- .../EmojiPicker/EmojiPickerMenu/index.js | 21 +++---- .../EmojiPickerMenu/index.native.js | 63 ++++++++++--------- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 2733da56e597..eec0202d03d6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -930,6 +930,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/getItemType.js b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js index 3196a989acc9..e0fc786cd089 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js @@ -1,3 +1,5 @@ +import CONST from '@src/CONST'; + /** * Improves FlashList's recycling when there are different types of items * @param {Object} item @@ -9,12 +11,12 @@ function getItemType(item) { return; } if (item.header) { - return 'header'; + return CONST.EMOJI_PICKER_ITEM_TYPES.HEADER; } if (item.spacer) { - return 'spacer'; + return CONST.EMOJI_PICKER_ITEM_TYPES.SPACER; } - return 'emoji'; + return CONST.EMOJI_PICKER_ITEM_TYPES.EMOJI; } export default getItemType; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index f16ddd61e413..937a5ca0278a 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -50,6 +50,15 @@ const defaultProps = { const throttleTime = Browser.isMobile() ? 200 : 50; +/** + * 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}`; + function EmojiPickerMenu(props) { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props; @@ -69,7 +78,6 @@ function EmojiPickerMenu(props) { const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); const firstNonHeaderIndex = useRef(0); - /** * Calculate the filtered + header emojis and header row indices * @returns {Object} @@ -392,15 +400,6 @@ function EmojiPickerMenu(props) { [preferredSkinTone], ); - /** - * 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 @@ -511,7 +510,7 @@ function EmojiPickerMenu(props) { data={filteredEmojis} renderItem={renderItem} keyExtractor={keyExtractor} - extraData={[filteredEmojis, highlightedIndex, preferredSkinTone]} + extraData={[highlightedIndex, preferredSkinTone]} numColumns={CONST.EMOJI_NUM_PER_ROW} stickyHeaderIndices={headerIndices} ListEmptyComponent={() => {translate('common.noResultsFound')}} diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index ce2536291b8d..e389a73c70e7 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -1,6 +1,6 @@ import {FlashList} from '@shopify/flash-list'; import PropTypes from 'prop-types'; -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import {runOnUI, scrollTo, useAnimatedRef} from 'react-native-reanimated'; @@ -43,6 +43,15 @@ const defaultProps = { frequentlyUsedEmojis: [], }; +/** + * Return a unique key for each emoji item + * + * @param {Object} item + * @param {Number} index + * @returns {String} + */ +const keyExtractor = (item, index) => `${index}${item.code}`; + function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -108,15 +117,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t })(); }; - /** - * 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 @@ -125,29 +125,32 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t * @param {Object} item * @returns {*} */ - const renderItem = ({item, target}) => { - 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} - /> - ); - }; + }, + [styles, windowWidth, preferredSkinTone, singleExecution, onEmojiSelected, translate], + ); const isFiltered = allEmojis.length !== filteredEmojis.length; From 18ea4edc804d8ae5a0454a97d682cfe53c88b255 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:24 +0100 Subject: [PATCH 14/28] slice flags for operating system in asset --- assets/emojis/index.ts | 11 ++- .../EmojiPicker/EmojiPickerMenu/index.js | 72 +++++++++---------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index aade4e557a64..5bbd08290130 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,12 @@ const localeEmojis = { es: esEmojis, } as const; -export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; +const flagHeaderIndex = emojis.findIndex((emoji) => { + if ('header' in emoji) { + return emoji.header && emoji.code === 'flags'; + } +}); +const emojisForOperatingSystem = getOperatingSystem() === CONST.OS.WINDOWS ? emojis.slice(0, flagHeaderIndex) : emojis; + +export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis, emojisForOperatingSystem}; export {skinTones, categoryFrequentlyUsed, default} from './common'; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 937a5ca0278a..a945bb961fa3 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import emojiAssets from '@assets/emojis'; +import {emojisForOperatingSystem} from '@assets/emojis'; import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList'; @@ -59,6 +59,22 @@ const throttleTime = Browser.isMobile() ? 200 : 50; */ const keyExtractor = (item, index) => `emoji_picker_${item.code}_${index}`; +/** + * Calculate the filtered + header emojis and header row indices + * @returns {Object} + */ +function getEmojisAndHeaderRowIndices() { + const filteredEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojisForOperatingSystem); + // 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); + + // FlashList renders items defined as null, so we have to store each header's row index to set their style properly + const headerRowIndices = _.map(headerEmojis, (headerEmoji) => headerEmoji.index); + + return {filteredEmojis, headerEmojis, headerRowIndices}; +} + function EmojiPickerMenu(props) { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props; @@ -78,37 +94,13 @@ function EmojiPickerMenu(props) { 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); - // FlashList renders items defined as null, so we have to store each header's row index to set their style properly - const headerRowIndices = _.map(headerEmojis, (headerEmoji) => headerEmoji.index); - - 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 [emojis, setEmojis] = useState([]); + const [initialHeaderRowIndices, setInitialHeaderRowIndices] = useState([]); + const [headerEmojis, setHeaderEmojis] = useState([]); - const [filteredEmojis, setFilteredEmojis] = useState(emojis.current); - const [headerIndices, setHeaderIndices] = useState(headerRowIndices.current); + const [filteredEmojis, setFilteredEmojis] = useState([]); + const [headerIndices, setHeaderIndices] = useState([]); const [highlightedIndex, setHighlightedIndex] = useState(-1); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); const [selection, setSelection] = useState({start: 0, end: 0}); @@ -118,11 +110,11 @@ function EmojiPickerMenu(props) { useEffect(() => { const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); - emojis.current = emojisAndHeaderRowIndices.filteredEmojis; - headerRowIndices.current = emojisAndHeaderRowIndices.headerRowIndices; + setEmojis(emojisAndHeaderRowIndices.filteredEmojis); + setInitialHeaderRowIndices(emojisAndHeaderRowIndices.headerRowIndices); setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis); - setFilteredEmojis(emojis.current); - setHeaderIndices(headerRowIndices.current); + setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); + setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); }, [frequentlyUsedEmojis]); /** @@ -166,14 +158,14 @@ function EmojiPickerMenu(props) { } 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(emojis); + setHeaderIndices(initialHeaderRowIndices); setHighlightedIndex(-1); - updateFirstNonHeaderIndex(emojis.current); + updateFirstNonHeaderIndex(emojis); setHighlightFirstEmoji(false); return; } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.current.length); + const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.length); // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); @@ -374,7 +366,7 @@ function EmojiPickerMenu(props) { useEffect(() => { // Find and store index of the first emoji item on mount - updateFirstNonHeaderIndex(emojis.current); + updateFirstNonHeaderIndex(emojis); }, []); const scrollToHeader = useCallback((headerIndex) => { @@ -454,7 +446,7 @@ function EmojiPickerMenu(props) { [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); - const isFiltered = emojis.current.length !== filteredEmojis.length; + const isFiltered = emojis.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; From 2e69a4fa4fa4f1fd55bfe53ca2d0842a6bc9beae Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:25 +0100 Subject: [PATCH 15/28] extract updatePreferredSkinTone function --- .../EmojiPicker/EmojiPickerMenu/index.js | 24 +++++-------------- .../EmojiPickerMenu/index.native.js | 15 ++---------- .../updatePreferredSkinTone.js | 14 +++++++++++ 3 files changed, 22 insertions(+), 31 deletions(-) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index a945bb961fa3..05b38fae0955 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -21,10 +21,10 @@ 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 getItemType from './getItemType'; +import updatePreferredSkinTone from './updatePreferredSkinTone'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -110,8 +110,10 @@ function EmojiPickerMenu(props) { useEffect(() => { const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); - setEmojis(emojisAndHeaderRowIndices.filteredEmojis); - setInitialHeaderRowIndices(emojisAndHeaderRowIndices.headerRowIndices); + if (emojis.length === 0 || initialHeaderRowIndices.length === 0) { + setEmojis(emojisAndHeaderRowIndices.filteredEmojis); + setInitialHeaderRowIndices(emojisAndHeaderRowIndices.headerRowIndices); + } setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis); setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); @@ -378,20 +380,6 @@ function EmojiPickerMenu(props) { emojiListRef.current.scrollToOffset({offset: calculatedOffset, animated: true}); }, []); - /** - * @param {Number} skinTone - */ - const updatePreferredSkinTone = useCallback( - (skinTone) => { - if (Number(preferredSkinTone) === Number(skinTone)) { - return; - } - - User.updatePreferredSkinTone(skinTone); - }, - [preferredSkinTone], - ); - /** * 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 @@ -512,7 +500,7 @@ function EmojiPickerMenu(props) { /> updatePreferredSkinTone(preferredSkinTone, skinTone)} preferredSkinTone={preferredSkinTone} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index e389a73c70e7..c0224528c7a4 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -18,10 +18,10 @@ 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 getItemType from './getItemType'; +import updatePreferredSkinTone from './updatePreferredSkinTone'; const propTypes = { /** Function to add the selected emoji to the main compose text input */ @@ -97,17 +97,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t setHeaderIndices(undefined); }, 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; runOnUI(() => { @@ -196,7 +185,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t /> updatePreferredSkinTone(preferredSkinTone, skinTone)} preferredSkinTone={preferredSkinTone} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js b/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js new file mode 100644 index 000000000000..2b08434c71b2 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js @@ -0,0 +1,14 @@ +import * as User from '@userActions/User'; + +/** + * @param {Number} skinTone + */ +const updatePreferredSkinTone = (preferredSkinTone, skinTone) => { + if (Number(preferredSkinTone) === Number(skinTone)) { + return; + } + + User.updatePreferredSkinTone(skinTone); +}; + +export default updatePreferredSkinTone; From 3a62259d0e64ea09a4e1d57c5b0fcb4080025a76 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:25 +0100 Subject: [PATCH 16/28] create BaseEmojiPickerMenu --- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 144 ++++++++++++++++++ .../EmojiPicker/EmojiPickerMenu/index.js | 46 +++--- .../EmojiPickerMenu/index.native.js | 48 ++---- 3 files changed, 178 insertions(+), 60 deletions(-) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js new file mode 100644 index 000000000000..f51dc21a5897 --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -0,0 +1,144 @@ +import {FlashList} from '@shopify/flash-list'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; +import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList'; +import refPropTypes from '@components/refPropTypes'; +import stylePropTypes from '@styles/stylePropTypes'; +import CONST from '@src/CONST'; +import getItemType from './getItemType'; + +const emojiPropType = { + code: PropTypes.string.isRequired, + header: PropTypes.bool, + spacer: PropTypes.bool, + 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(emojiPropType)).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(emojiPropType)).isRequired, + + /** Function to render each item in the list */ + renderItem: PropTypes.func.isRequired, + + /** Function to extract a unique key for each item in the list */ + keyExtractor: PropTypes.func.isRequired, + + /** Extra data to be passed to the list for re-rendering */ + extraData: PropTypes.array, + + /** Array of indices for the sticky headers */ + stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), + + /** Component to render when the list is empty */ + ListEmptyComponent: PropTypes.func, + + /** Style to be applied to the content container */ + contentContainerStyle: stylePropTypes, + + /** Current preferred skin tone for emojis */ + preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** Function to update the preferred skin tone */ + onUpdatePreferredSkinTone: PropTypes.func.isRequired, +}; + +const defaultProps = { + isFiltered: false, + headerEmojis: [], + scrollToHeader: () => {}, + listWrapperStyle: [], + forwardedRef: () => {}, + data: [], + renderItem: () => {}, + keyExtractor: () => {}, + extraData: [], + stickyHeaderIndices: [], + ListEmptyComponent: () => {}, + contentContainerStyle: [], + preferredSkinTone: null, + onUpdatePreferredSkinTone: () => {}, +}; + +function BaseEmojiPickerMenu({ + headerEmojis, + scrollToHeader, + isFiltered, + listWrapperStyle, + forwardedRef, + data, + renderItem, + keyExtractor, + stickyHeaderIndices, + ListEmptyComponent, + contentContainerStyle, + onUpdatePreferredSkinTone, + preferredSkinTone, + extraData, + alwaysBounceVertical, +}) { + 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/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 05b38fae0955..a7a3e829f0a8 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,4 +1,3 @@ -import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; @@ -6,9 +5,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import {emojisForOperatingSystem} 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'; @@ -23,6 +20,7 @@ import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposit import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; import getItemType from './getItemType'; import updatePreferredSkinTone from './updatePreferredSkinTone'; @@ -467,14 +465,11 @@ function EmojiPickerMenu(props) { blurOnSubmit={filteredEmojis.length > 0} /> - {!isFiltered && ( - - )} - - {translate('common.noResultsFound')}} - estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} - contentContainerStyle={styles.ph4} - getItemType={getItemType} - /> - - updatePreferredSkinTone(preferredSkinTone, skinTone)} + ref={emojiListRef} + data={filteredEmojis} + renderItem={renderItem} + keyExtractor={keyExtractor} + extraData={[highlightedIndex, preferredSkinTone]} + numColumns={CONST.EMOJI_NUM_PER_ROW} + stickyHeaderIndices={headerIndices} + ListEmptyComponent={() => {translate('common.noResultsFound')}} + estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} + contentContainerStyle={styles.ph4} + getItemType={getItemType} preferredSkinTone={preferredSkinTone} + onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} /> ); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index c0224528c7a4..faff1eab443f 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -1,4 +1,3 @@ -import {FlashList} from '@shopify/flash-list'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -6,9 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import {runOnUI, scrollTo, useAnimatedRef} 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'; @@ -20,7 +17,7 @@ import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import getItemType from './getItemType'; +import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; import updatePreferredSkinTone from './updatePreferredSkinTone'; const propTypes = { @@ -94,7 +91,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length); setFilteredEmojis(newFilteredEmojiList); - setHeaderIndices(undefined); + setHeaderIndices([]); }, 300); const scrollToHeader = (headerIndex) => { @@ -154,39 +151,26 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t blurOnSubmit={filteredEmojis.length > 0} /> - {!isFiltered && ( - - )} - - {translate('common.noResultsFound')}} - alwaysBounceVertical={filteredEmojis.length !== 0} - estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} - contentContainerStyle={styles.ph4} - extraData={[filteredEmojis, preferredSkinTone]} - getItemType={getItemType} - /> - - updatePreferredSkinTone(preferredSkinTone, skinTone)} + ref={emojiList} + data={filteredEmojis} + renderItem={renderItem} + keyExtractor={keyExtractor} + extraData={[filteredEmojis, preferredSkinTone]} + stickyHeaderIndices={headerIndices} + ListEmptyComponent={() => {translate('common.noResultsFound')}} + contentContainerStyle={styles.ph4} preferredSkinTone={preferredSkinTone} + onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} /> ); From 50ebdee96a5cf8c36c4ac4fa4902972b6e9f38bb Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:26 +0100 Subject: [PATCH 17/28] update BaseEmojiPickerMenu --- assets/emojis/index.ts | 7 +++- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 39 ++++++++----------- .../EmojiPicker/EmojiPickerMenu/index.js | 10 +---- .../EmojiPickerMenu/index.native.js | 3 +- .../updatePreferredSkinTone.js | 1 + 5 files changed, 26 insertions(+), 34 deletions(-) diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 5bbd08290130..9f73fcf29008 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -34,10 +34,13 @@ const localeEmojis = { } as const; const flagHeaderIndex = emojis.findIndex((emoji) => { - if ('header' in emoji) { - return emoji.header && emoji.code === 'flags'; + if (!('header' in emoji)) { + return; } + + return emoji.header && emoji.code === 'flags'; }); + const emojisForOperatingSystem = getOperatingSystem() === CONST.OS.WINDOWS ? emojis.slice(0, flagHeaderIndex) : emojis; export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis, emojisForOperatingSystem}; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js index f51dc21a5897..b9d1de949def 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -1,11 +1,13 @@ import {FlashList} from '@shopify/flash-list'; import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; +import {Text, View} from 'react-native'; import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList'; import refPropTypes from '@components/refPropTypes'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import stylePropTypes from '@styles/stylePropTypes'; +import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import getItemType from './getItemType'; @@ -42,39 +44,31 @@ const propTypes = { keyExtractor: PropTypes.func.isRequired, /** Extra data to be passed to the list for re-rendering */ - extraData: PropTypes.array, + // eslint-disable-next-line react/forbid-prop-types + extraData: PropTypes.any, /** Array of indices for the sticky headers */ stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), - /** Component to render when the list is empty */ - ListEmptyComponent: PropTypes.func, - - /** Style to be applied to the content container */ - contentContainerStyle: stylePropTypes, - /** Current preferred skin tone for emojis */ preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** Function to update the preferred skin tone */ onUpdatePreferredSkinTone: PropTypes.func.isRequired, + + /** Whether the list should always bounce vertically */ + alwaysBounceVertical: PropTypes.bool, + + ...withLocalizePropTypes, }; const defaultProps = { - isFiltered: false, - headerEmojis: [], - scrollToHeader: () => {}, listWrapperStyle: [], forwardedRef: () => {}, - data: [], - renderItem: () => {}, - keyExtractor: () => {}, extraData: [], stickyHeaderIndices: [], - ListEmptyComponent: () => {}, - contentContainerStyle: [], preferredSkinTone: null, - onUpdatePreferredSkinTone: () => {}, + alwaysBounceVertical: false, }; function BaseEmojiPickerMenu({ @@ -87,13 +81,14 @@ function BaseEmojiPickerMenu({ renderItem, keyExtractor, stickyHeaderIndices, - ListEmptyComponent, - contentContainerStyle, onUpdatePreferredSkinTone, preferredSkinTone, extraData, alwaysBounceVertical, + translate, }) { + const styles = useThemeStyles(); + return ( <> {!isFiltered && ( @@ -111,10 +106,10 @@ function BaseEmojiPickerMenu({ keyExtractor={keyExtractor} numColumns={CONST.EMOJI_NUM_PER_ROW} stickyHeaderIndices={stickyHeaderIndices} - ListEmptyComponent={ListEmptyComponent} + ListEmptyComponent={() => {translate('common.noResultsFound')}} alwaysBounceVertical={alwaysBounceVertical} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} - contentContainerStyle={contentContainerStyle} + contentContainerStyle={styles.ph4} extraData={extraData} getItemType={getItemType} /> @@ -141,4 +136,4 @@ const BaseEmojiPickerMenuWithRef = React.forwardRef((props, ref) => ( BaseEmojiPickerMenuWithRef.displayName = 'BaseEmojiPickerMenuWithRef'; -export default BaseEmojiPickerMenuWithRef; +export default withLocalize(BaseEmojiPickerMenuWithRef); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index a7a3e829f0a8..5312496d992e 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -21,7 +21,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; -import getItemType from './getItemType'; import updatePreferredSkinTone from './updatePreferredSkinTone'; const propTypes = { @@ -115,7 +114,7 @@ function EmojiPickerMenu(props) { setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis); setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); - }, [frequentlyUsedEmojis]); + }, [emojis.length, frequentlyUsedEmojis, initialHeaderRowIndices.length]); /** * On text input selection change @@ -367,7 +366,7 @@ function EmojiPickerMenu(props) { useEffect(() => { // Find and store index of the first emoji item on mount updateFirstNonHeaderIndex(emojis); - }, []); + }, [emojis]); const scrollToHeader = useCallback((headerIndex) => { if (!emojiListRef.current) { @@ -484,12 +483,7 @@ function EmojiPickerMenu(props) { renderItem={renderItem} keyExtractor={keyExtractor} extraData={[highlightedIndex, preferredSkinTone]} - numColumns={CONST.EMOJI_NUM_PER_ROW} stickyHeaderIndices={headerIndices} - ListEmptyComponent={() => {translate('common.noResultsFound')}} - estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} - contentContainerStyle={styles.ph4} - getItemType={getItemType} preferredSkinTone={preferredSkinTone} onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index faff1eab443f..ed6516fd7fe6 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -167,10 +167,9 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t keyExtractor={keyExtractor} extraData={[filteredEmojis, preferredSkinTone]} stickyHeaderIndices={headerIndices} - ListEmptyComponent={() => {translate('common.noResultsFound')}} - contentContainerStyle={styles.ph4} preferredSkinTone={preferredSkinTone} onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} + alwaysBounceVertical={filteredEmojis.length !== 0} /> ); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js b/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js index 2b08434c71b2..5f352dfcfb60 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/updatePreferredSkinTone.js @@ -1,6 +1,7 @@ import * as User from '@userActions/User'; /** + * @param {Number} preferredSkinTone * @param {Number} skinTone */ const updatePreferredSkinTone = (preferredSkinTone, skinTone) => { From 0054ff6a2bb8a55ae249873fd9ad5d016331a632 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:26 +0100 Subject: [PATCH 18/28] code review updates --- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 53 ++++++++++++++----- .../EmojiPickerMenu/getItemType.js | 22 -------- .../EmojiPicker/EmojiPickerMenu/index.js | 10 ---- .../EmojiPickerMenu/index.native.js | 10 ---- 4 files changed, 40 insertions(+), 55 deletions(-) delete mode 100644 src/components/EmojiPicker/EmojiPickerMenu/getItemType.js diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js index b9d1de949def..c1f5a31e6975 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -5,16 +5,19 @@ import {Text, View} from 'react-native'; import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar'; import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList'; import refPropTypes from '@components/refPropTypes'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import stylePropTypes from '@styles/stylePropTypes'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import getItemType from './getItemType'; -const emojiPropType = { +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), }; @@ -23,7 +26,7 @@ const propTypes = { isFiltered: PropTypes.bool.isRequired, /** Array of header emojis */ - headerEmojis: PropTypes.arrayOf(PropTypes.shape(emojiPropType)).isRequired, + headerEmojis: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired, /** Function to scroll to a specific header in the emoji list */ scrollToHeader: PropTypes.func.isRequired, @@ -35,14 +38,11 @@ const propTypes = { forwardedRef: refPropTypes, /** The data for the emoji list */ - data: PropTypes.arrayOf(PropTypes.shape(emojiPropType)).isRequired, + data: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired, /** Function to render each item in the list */ renderItem: PropTypes.func.isRequired, - /** Function to extract a unique key for each item in the list */ - keyExtractor: 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, @@ -58,8 +58,6 @@ const propTypes = { /** Whether the list should always bounce vertically */ alwaysBounceVertical: PropTypes.bool, - - ...withLocalizePropTypes, }; const defaultProps = { @@ -71,6 +69,36 @@ const defaultProps = { 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}`; + function BaseEmojiPickerMenu({ headerEmojis, scrollToHeader, @@ -79,15 +107,14 @@ function BaseEmojiPickerMenu({ forwardedRef, data, renderItem, - keyExtractor, stickyHeaderIndices, onUpdatePreferredSkinTone, preferredSkinTone, extraData, alwaysBounceVertical, - translate, }) { const styles = useThemeStyles(); + const {translate} = useLocalize(); return ( <> @@ -136,4 +163,4 @@ const BaseEmojiPickerMenuWithRef = React.forwardRef((props, ref) => ( BaseEmojiPickerMenuWithRef.displayName = 'BaseEmojiPickerMenuWithRef'; -export default withLocalize(BaseEmojiPickerMenuWithRef); +export default BaseEmojiPickerMenuWithRef; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js b/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js deleted file mode 100644 index e0fc786cd089..000000000000 --- a/src/components/EmojiPicker/EmojiPickerMenu/getItemType.js +++ /dev/null @@ -1,22 +0,0 @@ -import CONST from '@src/CONST'; - -/** - * Improves FlashList's recycling when there are different types of items - * @param {Object} item - * @returns {String} - */ -function getItemType(item) { - // item is undefined only when list is empty - if (!item) { - return; - } - if (item.header) { - return CONST.EMOJI_PICKER_ITEM_TYPES.HEADER; - } - if (item.spacer) { - return CONST.EMOJI_PICKER_ITEM_TYPES.SPACER; - } - return CONST.EMOJI_PICKER_ITEM_TYPES.EMOJI; -} - -export default getItemType; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 5312496d992e..b117fcd0e166 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -47,15 +47,6 @@ const defaultProps = { const throttleTime = Browser.isMobile() ? 200 : 50; -/** - * 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}`; - /** * Calculate the filtered + header emojis and header row indices * @returns {Object} @@ -481,7 +472,6 @@ function EmojiPickerMenu(props) { ref={emojiListRef} data={filteredEmojis} renderItem={renderItem} - keyExtractor={keyExtractor} extraData={[highlightedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} preferredSkinTone={preferredSkinTone} diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index ed6516fd7fe6..dac4ceaee20e 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -40,15 +40,6 @@ const defaultProps = { frequentlyUsedEmojis: [], }; -/** - * Return a unique key for each emoji item - * - * @param {Object} item - * @param {Number} index - * @returns {String} - */ -const keyExtractor = (item, index) => `${index}${item.code}`; - function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -164,7 +155,6 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t ref={emojiList} data={filteredEmojis} renderItem={renderItem} - keyExtractor={keyExtractor} extraData={[filteredEmojis, preferredSkinTone]} stickyHeaderIndices={headerIndices} preferredSkinTone={preferredSkinTone} From 57b6f881d946f461f8f7e0da694c8de78683c78b Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:26 +0100 Subject: [PATCH 19/28] code review updates --- assets/emojis/index.ts | 28 ++++++++++------- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 3 ++ .../EmojiPicker/EmojiPickerMenu/index.js | 30 ++++++++----------- .../EmojiPickerMenu/index.native.js | 28 +++++++---------- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 9f73fcf29008..3c2849b41e0a 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -33,15 +33,21 @@ const localeEmojis = { es: esEmojis, } as const; -const flagHeaderIndex = emojis.findIndex((emoji) => { - if (!('header' in emoji)) { - return; - } - - return emoji.header && emoji.code === 'flags'; -}); - -const emojisForOperatingSystem = getOperatingSystem() === CONST.OS.WINDOWS ? emojis.slice(0, flagHeaderIndex) : emojis; - +// 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, default} from './common'; +export {skinTones, categoryFrequentlyUsed} from './common'; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js index c1f5a31e6975..a21eb7ff917c 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -13,10 +13,13 @@ 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), }; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index b117fcd0e166..73f0eacc5b40 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -8,13 +8,12 @@ import {emojisForOperatingSystem} from '@assets/emojis'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useLocalize from '@hooks/useLocalize'; 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'; @@ -35,8 +34,6 @@ const propTypes = { /** Stores user's frequently used emojis */ // eslint-disable-next-line react/forbid-prop-types frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.object), - - ...withLocalizePropTypes, }; const defaultProps = { @@ -63,14 +60,14 @@ function getEmojisAndHeaderRowIndices() { return {filteredEmojis, headerEmojis, headerRowIndices}; } -function EmojiPickerMenu(props) { - const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props; - +function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); + const {translate, preferredLocale} = useLocalize(); + // Ref for the emoji search input const searchInputRef = useRef(null); @@ -494,14 +491,11 @@ 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 withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + frequentlyUsedEmojis: { + key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + }, +})(EmojiPickerMenuWithRef); diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index dac4ceaee20e..b292fcca56c5 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -8,12 +8,11 @@ import emojis from '@assets/emojis'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; 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 CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -30,9 +29,6 @@ const propTypes = { /** 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 = { @@ -40,7 +36,7 @@ const defaultProps = { frequentlyUsedEmojis: [], }; -function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) { +function EmojiPickerMenu({onEmojiSelected, preferredSkinTone, frequentlyUsedEmojis}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const emojiList = useAnimatedRef(); @@ -52,6 +48,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t const [headerIndices, setHeaderIndices] = useState(headerRowIndices); const {windowWidth} = useWindowDimensions(); const {singleExecution} = useSingleExecution(); + const {translate, preferredLocale} = useLocalize(); useEffect(() => { setFilteredEmojis(allEmojis); @@ -179,14 +176,11 @@ 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 withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + }, + frequentlyUsedEmojis: { + key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + }, +})(EmojiPickerMenuWithRef); From d5445c009323eabaaa2ebd9cf95ff6146daeac66 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:27 +0100 Subject: [PATCH 20/28] refactor EmojiPickerMenu for web --- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 4 +- .../EmojiPicker/EmojiPickerMenu/index.js | 80 ++++++------------- 2 files changed, 25 insertions(+), 59 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js index a21eb7ff917c..a91c870e14c5 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -19,7 +19,7 @@ const emojiPropTypes = { /** 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), }; @@ -136,7 +136,7 @@ function BaseEmojiPickerMenu({ keyExtractor={keyExtractor} numColumns={CONST.EMOJI_NUM_PER_ROW} stickyHeaderIndices={stickyHeaderIndices} - ListEmptyComponent={() => {translate('common.noResultsFound')}} + ListEmptyComponent={{translate('common.noResultsFound')}} alwaysBounceVertical={alwaysBounceVertical} estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT} contentContainerStyle={styles.ph4} diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 73f0eacc5b40..e1b480815fb7 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -1,10 +1,10 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import {emojisForOperatingSystem} from '@assets/emojis'; +import emojis from '@assets/emojis'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -44,22 +44,6 @@ const defaultProps = { const throttleTime = Browser.isMobile() ? 200 : 50; -/** - * Calculate the filtered + header emojis and header row indices - * @returns {Object} - */ -function getEmojisAndHeaderRowIndices() { - const filteredEmojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojisForOperatingSystem); - // 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); - - // FlashList renders items defined as null, so we have to store each header's row index to set their style properly - const headerRowIndices = _.map(headerEmojis, (headerEmoji) => headerEmoji.index); - - return {filteredEmojis, headerEmojis, headerRowIndices}; -} - function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -78,31 +62,28 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - const firstNonHeaderIndex = useRef(0); - - const [emojis, setEmojis] = useState([]); - const [initialHeaderRowIndices, setInitialHeaderRowIndices] = useState([]); - const [headerEmojis, setHeaderEmojis] = useState([]); - - const [filteredEmojis, setFilteredEmojis] = useState([]); - const [headerIndices, setHeaderIndices] = useState([]); + // 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 [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); + const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]); + useEffect(() => { - const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices(); - if (emojis.length === 0 || initialHeaderRowIndices.length === 0) { - setEmojis(emojisAndHeaderRowIndices.filteredEmojis); - setInitialHeaderRowIndices(emojisAndHeaderRowIndices.headerRowIndices); - } - setHeaderEmojis(emojisAndHeaderRowIndices.headerEmojis); - setFilteredEmojis(emojisAndHeaderRowIndices.filteredEmojis); - setHeaderIndices(emojisAndHeaderRowIndices.headerRowIndices); - }, [emojis.length, frequentlyUsedEmojis, initialHeaderRowIndices.length]); + setFilteredEmojis(allEmojis); + }, [allEmojis]); + + useEffect(() => { + setHeaderIndices(headerRowIndices); + }, [headerRowIndices]); /** * On text input selection change @@ -113,14 +94,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, 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; @@ -145,10 +118,9 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } if (normalizedSearchTerm === '') { // There are no headers when searching, so we need to re-make them sticky when there is no search term - setFilteredEmojis(emojis); - setHeaderIndices(initialHeaderRowIndices); + setFilteredEmojis(allEmojis); + setHeaderIndices(headerRowIndices); setHighlightedIndex(-1); - updateFirstNonHeaderIndex(emojis); setHighlightFirstEmoji(false); return; } @@ -158,7 +130,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); setHighlightedIndex(0); - updateFirstNonHeaderIndex(newFilteredEmojiList); setHighlightFirstEmoji(true); }, throttleTime); @@ -194,11 +165,11 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, return; } } - + // 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; @@ -228,7 +199,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, case 'ArrowLeft': move( -1, - () => highlightedIndex - 1 < firstNonHeaderIndex.current, + () => highlightedIndex - 1 < firstNonHeaderIndex, () => { // Reaching start of the list, arrow left set the focus to searchInput. focusInputWithTextSelect(); @@ -242,7 +213,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, 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(); @@ -351,11 +322,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, }; }, [forwardedRef, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]); - useEffect(() => { - // Find and store index of the first emoji item on mount - updateFirstNonHeaderIndex(emojis); - }, [emojis]); - const scrollToHeader = useCallback((headerIndex) => { if (!emojiListRef.current) { return; @@ -419,7 +385,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); - const isFiltered = emojis.length !== filteredEmojis.length; + const isFiltered = allEmojis.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; From 5bf55b95920917c179bdc1b3b83cc54eb6f7ed90 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:27 +0100 Subject: [PATCH 21/28] implement useArrowKeyFocusManager --- .../EmojiPicker/EmojiPickerMenu/index.js | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index e1b480815fb7..e25664d07698 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -10,6 +10,7 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; @@ -68,14 +69,18 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); - 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); const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]); - + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: headerRowIndices, + maxIndex: filteredEmojis.length - 1, + isActive: false, + }); useEffect(() => { setFilteredEmojis(allEmojis); @@ -120,7 +125,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(allEmojis); setHeaderIndices(headerRowIndices); - setHighlightedIndex(-1); + setFocusedIndex(-1); setHighlightFirstEmoji(false); return; } @@ -129,7 +134,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); - setHighlightedIndex(0); + setFocusedIndex(0); setHighlightFirstEmoji(true); }, throttleTime); @@ -161,21 +166,21 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation - if (highlightedIndex !== -1) { + if (focusedIndex !== -1) { return; } } - + // 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); + if (focusedIndex === -1) { + setFocusedIndex(firstNonHeaderIndex); setArePointerEventsDisabled(true); setIsUsingKeyboardMovement(true); return; } - let newIndex = highlightedIndex; + let newIndex = focusedIndex; const move = (steps, boundsCheck, onBoundReached = () => {}) => { if (boundsCheck()) { onBoundReached(); @@ -194,12 +199,12 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, switch (arrowKey) { case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); + move(CONST.EMOJI_NUM_PER_ROW, () => focusedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); break; case 'ArrowLeft': move( -1, - () => highlightedIndex - 1 < firstNonHeaderIndex, + () => focusedIndex - 1 < firstNonHeaderIndex, () => { // Reaching start of the list, arrow left set the focus to searchInput. focusInputWithTextSelect(); @@ -208,12 +213,12 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, ); break; case 'ArrowRight': - move(1, () => highlightedIndex + 1 > filteredEmojis.length - 1); + move(1, () => focusedIndex + 1 > filteredEmojis.length - 1); break; case 'ArrowUp': move( -CONST.EMOJI_NUM_PER_ROW, - () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex, + () => focusedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex, () => { // Reaching start of the list, arrow up set the focus to searchInput. focusInputWithTextSelect(); @@ -226,13 +231,14 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== highlightedIndex) { - setHighlightedIndex(newIndex); + if (newIndex !== focusedIndex) { + setFocusedIndex(newIndex); setArePointerEventsDisabled(true); setIsUsingKeyboardMovement(true); } }, - [filteredEmojis, highlightedIndex, selection.end, selection.start], + + [filteredEmojis, firstNonHeaderIndex, focusedIndex, selection.end, selection.start, setFocusedIndex], ); const keyDownHandler = useCallback( @@ -248,8 +254,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { - const item = filteredEmojis[highlightedIndex]; + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && focusedIndex !== -1) { + const item = filteredEmojis[focusedIndex]; if (!item) { return; } @@ -275,7 +281,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, searchInputRef.current.focus(); } }, - [filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], + [filteredEmojis, highlightAdjacentEmoji, focusedIndex, isFocused, onEmojiSelected, preferredSkinTone], ); /** @@ -357,8 +363,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; - const shouldEmojiBeHighlighted = index === highlightedIndex && highlightFirstEmoji; + const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement; + const shouldEmojiBeHighlighted = index === focusedIndex && highlightFirstEmoji; return ( setHighlightedIndex(index)} + onFocus={() => setFocusedIndex(index)} onBlur={() => // Only clear the highlighted index if the highlighted index is the same, // meaning that the focus changed to an element that is not an emoji item. - setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState)) + setFocusedIndex((prevState) => (prevState === index ? -1 : prevState)) } isFocused={isEmojiFocused} isHighlighted={shouldEmojiBeHighlighted} /> ); }, - [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], + [ + preferredSkinTone, + focusedIndex, + isUsingKeyboardMovement, + highlightFirstEmoji, + styles.emojiHeaderContainer, + styles.mh4, + styles.textLabelSupporting, + translate, + onEmojiSelected, + setFocusedIndex, + ], ); const isFiltered = allEmojis.length !== filteredEmojis.length; @@ -409,8 +426,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, autoFocus={shouldFocusInputOnScreenFocus} onSelectionChange={onSelectionChange} onFocus={() => { - setHighlightedIndex(-1); setIsFocused(true); + setFocusedIndex(-1); setIsUsingKeyboardMovement(false); }} onBlur={() => setIsFocused(false)} @@ -435,7 +452,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, ref={emojiListRef} data={filteredEmojis} renderItem={renderItem} - extraData={[highlightedIndex, preferredSkinTone]} + extraData={[focusedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} preferredSkinTone={preferredSkinTone} onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} From 0162599b9611ca32b40dfeb4c626f5d0cae1e4d8 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:27 +0100 Subject: [PATCH 22/28] Revert "implement useArrowKeyFocusManager" This reverts commit 646bbb1602a1265f22ffdb8fe23887f2d54fd49b. --- .../EmojiPicker/EmojiPickerMenu/index.js | 66 +++++++------------ .../EmojiPickerMenu/index.native.js | 6 +- 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index e25664d07698..fdae6bbf6a75 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -10,7 +10,6 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; @@ -69,18 +68,13 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); + 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); const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]); - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes: headerRowIndices, - maxIndex: filteredEmojis.length - 1, - isActive: false, - }); useEffect(() => { setFilteredEmojis(allEmojis); @@ -125,7 +119,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // There are no headers when searching, so we need to re-make them sticky when there is no search term setFilteredEmojis(allEmojis); setHeaderIndices(headerRowIndices); - setFocusedIndex(-1); + setHighlightedIndex(-1); setHighlightFirstEmoji(false); return; } @@ -134,7 +128,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); - setFocusedIndex(0); + setHighlightedIndex(0); setHighlightFirstEmoji(true); }, throttleTime); @@ -166,21 +160,21 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation - if (focusedIndex !== -1) { + if (highlightedIndex !== -1) { return; } } // If nothing is highlighted and an arrow key is pressed // select the first emoji, apply keyboard movement styles, and disable pointer events - if (focusedIndex === -1) { - setFocusedIndex(firstNonHeaderIndex); + if (highlightedIndex === -1) { + setHighlightedIndex(firstNonHeaderIndex); setArePointerEventsDisabled(true); setIsUsingKeyboardMovement(true); return; } - let newIndex = focusedIndex; + let newIndex = highlightedIndex; const move = (steps, boundsCheck, onBoundReached = () => {}) => { if (boundsCheck()) { onBoundReached(); @@ -199,12 +193,12 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, switch (arrowKey) { case 'ArrowDown': - move(CONST.EMOJI_NUM_PER_ROW, () => focusedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); + move(CONST.EMOJI_NUM_PER_ROW, () => highlightedIndex + CONST.EMOJI_NUM_PER_ROW > filteredEmojis.length - 1); break; case 'ArrowLeft': move( -1, - () => focusedIndex - 1 < firstNonHeaderIndex, + () => highlightedIndex - 1 < firstNonHeaderIndex, () => { // Reaching start of the list, arrow left set the focus to searchInput. focusInputWithTextSelect(); @@ -213,12 +207,12 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, ); break; case 'ArrowRight': - move(1, () => focusedIndex + 1 > filteredEmojis.length - 1); + move(1, () => highlightedIndex + 1 > filteredEmojis.length - 1); break; case 'ArrowUp': move( -CONST.EMOJI_NUM_PER_ROW, - () => focusedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex, + () => highlightedIndex - CONST.EMOJI_NUM_PER_ROW < firstNonHeaderIndex, () => { // Reaching start of the list, arrow up set the focus to searchInput. focusInputWithTextSelect(); @@ -231,14 +225,13 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events - if (newIndex !== focusedIndex) { - setFocusedIndex(newIndex); + if (newIndex !== highlightedIndex) { + setHighlightedIndex(newIndex); setArePointerEventsDisabled(true); setIsUsingKeyboardMovement(true); } }, - - [filteredEmojis, firstNonHeaderIndex, focusedIndex, selection.end, selection.start, setFocusedIndex], + [filteredEmojis, firstNonHeaderIndex, highlightedIndex, selection.end, selection.start], ); const keyDownHandler = useCallback( @@ -254,8 +247,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } // Select the currently highlighted emoji if enter is pressed - if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && focusedIndex !== -1) { - const item = filteredEmojis[focusedIndex]; + if (!isEnterWhileComposition(keyBoardEvent) && keyBoardEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && highlightedIndex !== -1) { + const item = filteredEmojis[highlightedIndex]; if (!item) { return; } @@ -281,7 +274,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, searchInputRef.current.focus(); } }, - [filteredEmojis, highlightAdjacentEmoji, focusedIndex, isFocused, onEmojiSelected, preferredSkinTone], + [filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone], ); /** @@ -363,8 +356,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code; - const isEmojiFocused = index === focusedIndex && isUsingKeyboardMovement; - const shouldEmojiBeHighlighted = index === focusedIndex && highlightFirstEmoji; + const isEmojiFocused = index === highlightedIndex && isUsingKeyboardMovement; + const shouldEmojiBeHighlighted = index === highlightedIndex && highlightFirstEmoji; return ( setFocusedIndex(index)} + onFocus={() => setHighlightedIndex(index)} onBlur={() => // Only clear the highlighted index if the highlighted index is the same, // meaning that the focus changed to an element that is not an emoji item. - setFocusedIndex((prevState) => (prevState === index ? -1 : prevState)) + setHighlightedIndex((prevState) => (prevState === index ? -1 : prevState)) } isFocused={isEmojiFocused} isHighlighted={shouldEmojiBeHighlighted} /> ); }, - [ - preferredSkinTone, - focusedIndex, - isUsingKeyboardMovement, - highlightFirstEmoji, - styles.emojiHeaderContainer, - styles.mh4, - styles.textLabelSupporting, - translate, - onEmojiSelected, - setFocusedIndex, - ], + [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); const isFiltered = allEmojis.length !== filteredEmojis.length; @@ -426,8 +408,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, autoFocus={shouldFocusInputOnScreenFocus} onSelectionChange={onSelectionChange} onFocus={() => { + setHighlightedIndex(-1); setIsFocused(true); - setFocusedIndex(-1); setIsUsingKeyboardMovement(false); }} onBlur={() => setIsFocused(false)} @@ -452,7 +434,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, ref={emojiListRef} data={filteredEmojis} renderItem={renderItem} - extraData={[focusedIndex, preferredSkinTone]} + extraData={[highlightedIndex, preferredSkinTone]} stickyHeaderIndices={headerIndices} preferredSkinTone={preferredSkinTone} onUpdatePreferredSkinTone={(skinTone) => updatePreferredSkinTone(preferredSkinTone, skinTone)} diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index b292fcca56c5..e93183fbf316 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -39,6 +39,9 @@ const defaultProps = { function EmojiPickerMenu({onEmojiSelected, preferredSkinTone, frequentlyUsedEmojis}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + const {singleExecution} = useSingleExecution(); + const {translate, preferredLocale} = useLocalize(); const emojiList = useAnimatedRef(); // eslint-disable-next-line react-hooks/exhaustive-deps const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]); @@ -46,9 +49,6 @@ function EmojiPickerMenu({onEmojiSelected, preferredSkinTone, frequentlyUsedEmoj const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => headerEmoji.index), [headerEmojis]); const [filteredEmojis, setFilteredEmojis] = useState(allEmojis); const [headerIndices, setHeaderIndices] = useState(headerRowIndices); - const {windowWidth} = useWindowDimensions(); - const {singleExecution} = useSingleExecution(); - const {translate, preferredLocale} = useLocalize(); useEffect(() => { setFilteredEmojis(allEmojis); From 16e12fde8055f8aa8f5699b34075453a4b2780e0 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 15 Dec 2023 10:36:28 +0100 Subject: [PATCH 23/28] simplify implementation across platforms for emoji picker menu --- .../EmojiPickerMenu/BaseEmojiPickerMenu.js | 27 +------- .../emojiPickerMenuPropTypes.js | 21 ++++++ .../EmojiPicker/EmojiPickerMenu/index.js | 66 +++++------------- .../EmojiPickerMenu/index.native.js | 67 ++++--------------- .../EmojiPickerMenu/useEmojiPickerMenu.js | 55 +++++++++++++++ .../EmojiPicker/EmojiSkinToneList.js | 32 +++------ src/components/OnyxProvider.tsx | 8 +++ src/hooks/usePreferredEmojiSkinTone.ts | 17 +++++ 8 files changed, 142 insertions(+), 151 deletions(-) create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js create mode 100644 src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js create mode 100644 src/hooks/usePreferredEmojiSkinTone.ts diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js index a91c870e14c5..3327f5e4a3cb 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js @@ -53,12 +53,6 @@ const propTypes = { /** Array of indices for the sticky headers */ stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), - /** Current preferred skin tone for emojis */ - preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** Function to update the preferred skin tone */ - onUpdatePreferredSkinTone: PropTypes.func.isRequired, - /** Whether the list should always bounce vertically */ alwaysBounceVertical: PropTypes.bool, }; @@ -68,7 +62,6 @@ const defaultProps = { forwardedRef: () => {}, extraData: [], stickyHeaderIndices: [], - preferredSkinTone: null, alwaysBounceVertical: false, }; @@ -102,20 +95,7 @@ const getItemType = (item) => { */ const keyExtractor = (item, index) => `emoji_picker_${item.code}_${index}`; -function BaseEmojiPickerMenu({ - headerEmojis, - scrollToHeader, - isFiltered, - listWrapperStyle, - forwardedRef, - data, - renderItem, - stickyHeaderIndices, - onUpdatePreferredSkinTone, - preferredSkinTone, - extraData, - alwaysBounceVertical, -}) { +function BaseEmojiPickerMenu({headerEmojis, scrollToHeader, isFiltered, listWrapperStyle, forwardedRef, data, renderItem, stickyHeaderIndices, extraData, alwaysBounceVertical}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -144,10 +124,7 @@ function BaseEmojiPickerMenu({ getItemType={getItemType} /> - + ); } diff --git a/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js b/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js new file mode 100644 index 000000000000..a63f099c672e --- /dev/null +++ b/src/components/EmojiPicker/EmojiPickerMenu/emojiPickerMenuPropTypes.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import CONST from '@src/CONST'; + +const emojiPickerMenuPropTypes = { + /** Function to add the selected emoji to the main compose text input */ + onEmojiSelected: PropTypes.func.isRequired, + + /** 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), +}; + +const emojiPickerMenuDefaultProps = { + preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, + frequentlyUsedEmojis: [], +}; + +export {emojiPickerMenuDefaultProps, emojiPickerMenuPropTypes}; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index fdae6bbf6a75..068e46864ed7 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -2,9 +2,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import emojis from '@assets/emojis'; import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -14,43 +12,33 @@ import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import * as EmojiUtils from '@libs/EmojiUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import BaseEmojiPickerMenu from './BaseEmojiPickerMenu'; -import updatePreferredSkinTone from './updatePreferredSkinTone'; +import {emojiPickerMenuDefaultProps, 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), + ...emojiPickerMenuPropTypes, }; const defaultProps = { forwardedRef: () => {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - frequentlyUsedEmojis: [], + ...emojiPickerMenuDefaultProps, }; const throttleTime = Browser.isMobile() ? 200 : 50; -function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected}) { +function EmojiPickerMenu({forwardedRef, onEmojiSelected}) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); - - const {translate, preferredLocale} = useLocalize(); + const {translate} = useLocalize(); + const {allEmojis, headerEmojis, headerRowIndices, filteredEmojis, headerIndices, setFilteredEmojis, setHeaderIndices, isListFiltered, suggestEmojis, preferredSkinTone} = + useEmojiPickerMenu(); // Ref for the emoji search input const searchInputRef = useRef(null); @@ -62,12 +50,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, // prevent auto focus when open picker for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - // 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 [highlightedIndex, setHighlightedIndex] = useState(-1); const [arePointerEventsDisabled, setArePointerEventsDisabled] = useState(false); const [selection, setSelection] = useState({start: 0, end: 0}); @@ -76,14 +58,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, const [highlightFirstEmoji, setHighlightFirstEmoji] = useState(false); const firstNonHeaderIndex = useMemo(() => _.findIndex(filteredEmojis, (item) => !item.spacer && !item.header), [filteredEmojis]); - useEffect(() => { - setFilteredEmojis(allEmojis); - }, [allEmojis]); - - useEffect(() => { - setHeaderIndices(headerRowIndices); - }, [headerRowIndices]); - /** * On text input selection change * @@ -111,7 +85,8 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, } 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}); } @@ -123,8 +98,6 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, setHighlightFirstEmoji(false); return; } - const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, emojis.length); - // Remove sticky header indices. There are no headers while searching and we don't want to make emojis sticky setFilteredEmojis(newFilteredEmojiList); setHeaderIndices([]); @@ -384,8 +357,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, [preferredSkinTone, highlightedIndex, isUsingKeyboardMovement, highlightFirstEmoji, styles, translate, onEmojiSelected], ); - const isFiltered = allEmojis.length !== filteredEmojis.length; - const listStyle = StyleUtils.getEmojiPickerListHeight(isFiltered, windowHeight); + const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, 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 ( @@ -418,7 +390,7 @@ function EmojiPickerMenu({forwardedRef, frequentlyUsedEmojis, preferredSkinTone, />