Skip to content

Commit

Permalink
Merge pull request Expensify#31479 from TMisiukiewicz/feat/emojipicke…
Browse files Browse the repository at this point in the history
…r-flashlist-migration

Migrate EmojiPickerMenu to FlashList
  • Loading branch information
roryabraham authored Dec 22, 2023
2 parents 4f2ba01 + 6b555d0 commit e89e466
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 355 deletions.
22 changes: 20 additions & 2 deletions assets/emojis/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,5 +33,21 @@ const localeEmojis = {
es: esEmojis,
} as const;

export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis};
export {skinTones, categoryFrequentlyUsed, default} from './common';
// On windows, flag emojis are not supported
const emojisForOperatingSystem =
getOperatingSystem() === CONST.OS.WINDOWS
? emojis.slice(
0,
emojis.findIndex((emoji) => {
if (!('header' in emoji)) {
return;
}

return emoji.header && emoji.code === 'flags';
}),
)
: emojis;

export default emojisForOperatingSystem;
export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis, emojisForOperatingSystem};
export {skinTones, categoryFrequentlyUsed} from './common';
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,11 @@ const CONST = {
IOS_CAMERAROLL_ACCESS_ERROR: 'Access to photo library was denied',
ADD_PAYMENT_MENU_POSITION_Y: 226,
ADD_PAYMENT_MENU_POSITION_X: 356,
EMOJI_PICKER_ITEM_TYPES: {
HEADER: 'header',
EMOJI: 'emoji',
SPACER: 'spacer',
},
EMOJI_PICKER_SIZE: {
WIDTH: 320,
HEIGHT: 416,
Expand Down
161 changes: 161 additions & 0 deletions src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {FlashList} from '@shopify/flash-list';
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar';
import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList';
import refPropTypes from '@components/refPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import stylePropTypes from '@styles/stylePropTypes';
import CONST from '@src/CONST';

const emojiPropTypes = {
/** The code of the item */
code: PropTypes.string.isRequired,

/** Whether the item is a header or not */
header: PropTypes.bool,

/** Whether the item is a spacer or not */
spacer: PropTypes.bool,

/** Types of an emoji - e.g. different skin types */
types: PropTypes.arrayOf(PropTypes.string),
};

const propTypes = {
/** Indicates if the emoji list is filtered or not */
isFiltered: PropTypes.bool.isRequired,

/** Array of header emojis */
headerEmojis: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired,

/** Function to scroll to a specific header in the emoji list */
scrollToHeader: PropTypes.func.isRequired,

/** Style to be applied to the list wrapper */
listWrapperStyle: stylePropTypes,

/** Reference to the emoji list */
forwardedRef: refPropTypes,

/** The data for the emoji list */
data: PropTypes.arrayOf(PropTypes.shape(emojiPropTypes)).isRequired,

/** Function to render each item in the list */
renderItem: PropTypes.func.isRequired,

/** Extra data to be passed to the list for re-rendering */
// eslint-disable-next-line react/forbid-prop-types
extraData: PropTypes.any,

/** Array of indices for the sticky headers */
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),

/** Whether the list should always bounce vertically */
alwaysBounceVertical: PropTypes.bool,
};

const defaultProps = {
listWrapperStyle: [],
forwardedRef: () => {},
extraData: [],
stickyHeaderIndices: [],
alwaysBounceVertical: false,
};

/**
* Improves FlashList's recycling when there are different types of items
* @param {Object} item
* @returns {String}
*/
const getItemType = (item) => {
// item is undefined only when list is empty
if (!item) {
return;
}

if (item.name) {
return CONST.EMOJI_PICKER_ITEM_TYPES.EMOJI;
}
if (item.header) {
return CONST.EMOJI_PICKER_ITEM_TYPES.HEADER;
}

return CONST.EMOJI_PICKER_ITEM_TYPES.SPACER;
};

/**
* Return a unique key for each emoji item
*
* @param {Object} item
* @param {Number} index
* @returns {String}
*/
const keyExtractor = (item, index) => `emoji_picker_${item.code}_${index}`;

/**
* Renders the list empty component
* @returns {React.Component}
*/
function ListEmptyComponent() {
const styles = useThemeStyles();
const {translate} = useLocalize();

return <Text style={[styles.textLabel, styles.colorMuted]}>{translate('common.noResultsFound')}</Text>;
}

function BaseEmojiPickerMenu({headerEmojis, scrollToHeader, isFiltered, listWrapperStyle, forwardedRef, data, renderItem, stickyHeaderIndices, extraData, alwaysBounceVertical}) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();

const flattenListWrapperStyle = useMemo(() => StyleSheet.flatten(listWrapperStyle), [listWrapperStyle]);

return (
<>
{!isFiltered && (
<CategoryShortcutBar
headerEmojis={headerEmojis}
onPress={scrollToHeader}
/>
)}
<View style={listWrapperStyle}>
<FlashList
ref={forwardedRef}
keyboardShouldPersistTaps="handled"
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
numColumns={CONST.EMOJI_NUM_PER_ROW}
stickyHeaderIndices={stickyHeaderIndices}
ListEmptyComponent={ListEmptyComponent}
alwaysBounceVertical={alwaysBounceVertical}
estimatedItemSize={CONST.EMOJI_PICKER_ITEM_HEIGHT}
estimatedListSize={{height: flattenListWrapperStyle.height, width: isSmallScreenWidth ? windowWidth : CONST.EMOJI_PICKER_SIZE.WIDTH}}
contentContainerStyle={styles.ph4}
extraData={extraData}
getItemType={getItemType}
/>
</View>
<EmojiSkinToneList />
</>
);
}

BaseEmojiPickerMenu.propTypes = propTypes;
BaseEmojiPickerMenu.defaultProps = defaultProps;
BaseEmojiPickerMenu.displayName = 'BaseEmojiPickerMenu';

const BaseEmojiPickerMenuWithRef = React.forwardRef((props, ref) => (
<BaseEmojiPickerMenu
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));

BaseEmojiPickerMenuWithRef.displayName = 'BaseEmojiPickerMenuWithRef';

export default BaseEmojiPickerMenuWithRef;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import PropTypes from 'prop-types';

const emojiPickerMenuPropTypes = {
/** Function to add the selected emoji to the main compose text input */
onEmojiSelected: PropTypes.func.isRequired,
};

export default emojiPickerMenuPropTypes;
Loading

0 comments on commit e89e466

Please sign in to comment.