Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor EmojiPickerMenu native to functional component and using scrollTo method #23854

Merged
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 79 additions & 122 deletions src/components/EmojiPicker/EmojiPickerMenu/index.native.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {Component} from 'react';
import {View, findNodeHandle} from 'react-native';
import React, {useState, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import _ from 'underscore';
import Animated, {runOnUI, _scrollTo} from 'react-native-reanimated';
import Animated, {runOnUI, scrollTo, useAnimatedRef} from 'react-native-reanimated';
import useWindowDimensions from '../../../hooks/useWindowDimensions';
import compose from '../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
import CONST from '../../../CONST';
import ONYXKEYS from '../../../ONYXKEYS';
import styles from '../../../styles/styles';
Expand All @@ -27,9 +27,6 @@ const propTypes = {
/** Stores user's preferred skin tone */
preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

/** Props related to the dimensions of the window */
...windowDimensionsPropTypes,

/** Props related to translation */
...withLocalizePropTypes,
};
Expand All @@ -38,107 +35,70 @@ const defaultProps = {
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
};

class EmojiPickerMenu extends Component {
constructor(props) {
super(props);

// Ref for emoji FlatList
this.emojiList = undefined;

this.emojis = EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis);

// 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
this.headerEmojis = EmojiUtils.getHeaderEmojis(this.emojis);

// This is the indices of each header's Row
// The positions are static, and are calculated as index/numColumns (8 in our case)
// This is because each row of 8 emojis counts as one index to the flatlist
this.headerRowIndices = _.map(this.headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW));

this.renderItem = this.renderItem.bind(this);
this.isMobileLandscape = this.isMobileLandscape.bind(this);
this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300);
this.scrollToHeader = this.scrollToHeader.bind(this);
this.getItemLayout = this.getItemLayout.bind(this);
function EmojiPickerMenu ({preferredLocale, onEmojiSelected, preferredSkinTone, translate}) {

this.state = {
filteredEmojis: this.emojis,
headerIndices: this.headerRowIndices,
};
}
const emojiList = useAnimatedRef();
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), []);
const headerEmojis = useMemo(() => EmojiUtils.getHeaderEmojis(allEmojis), [allEmojis]);
const headerRowIndices = useMemo(() => _.map(headerEmojis, (headerEmoji) => Math.floor(headerEmoji.index / CONST.EMOJI_NUM_PER_ROW)), [headerEmojis]);
const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
const {windowWidth} = useWindowDimensions();

getItemLayout(data, index) {
return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index};
}
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
*
* @param {String} searchTerm
*/
filterEmojis(searchTerm) {
const filterEmojis = _.debounce((searchTerm) => {
const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');

if (this.emojiList) {
this.emojiList.scrollToOffset({offset: 0, animated: false});
if (emojiList.current) {
emojiList.current.scrollToOffset({offset: 0, animated: false});
}

if (normalizedSearchTerm === '') {
this.setState({
filteredEmojis: this.emojis,
headerIndices: this.headerRowIndices,
});
setFilteredEmojis(allEmojis);
setHeaderIndices(headerRowIndices);

return;
}
const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, this.props.preferredLocale, this.emojis.length);
const newFilteredEmojiList = EmojiUtils.suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);

this.setState({
filteredEmojis: newFilteredEmojiList,
headerIndices: undefined,
});
}
setFilteredEmojis(newFilteredEmojiList);
setHeaderIndices(undefined);
}, 300);

/**
* @param {String} emoji
* @param {Object} emojiObject
*/
addToFrequentAndSelectEmoji(emoji, emojiObject) {
const addToFrequentAndSelectEmoji = (emoji, emojiObject) => {
const frequentEmojiList = EmojiUtils.getFrequentlyUsedEmojis(emojiObject);
User.updateFrequentlyUsedEmojis(frequentEmojiList);
this.props.onEmojiSelected(emoji, emojiObject);
}

/**
* Check if its a landscape mode of mobile device
*
* @returns {Boolean}
*/
isMobileLandscape() {
return this.props.windowWidth >= this.props.windowHeight;
onEmojiSelected(emoji, emojiObject);
}

/**
* @param {Number} skinTone
*/
updatePreferredSkinTone(skinTone) {
if (this.props.preferredSkinTone === skinTone) {
const updatePreferredSkinTone = (skinTone) => {
if (preferredSkinTone === skinTone) {
return;
}

User.updatePreferredSkinTone(skinTone);
}

scrollToHeader(headerIndex) {
const scrollToHeader = (headerIndex) => {
const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
this.emojiList.flashScrollIndicators();
const node = findNodeHandle(this.emojiList);
emojiList.current.flashScrollIndicators();
runOnUI(() => {
'worklet';

_scrollTo(node, 0, calculatedOffset, true);
scrollTo(emojiList, 0, calculatedOffset, true);
})();
}

Expand All @@ -149,9 +109,7 @@ class EmojiPickerMenu extends Component {
* @param {Number} index
* @returns {String}
*/
keyExtractor(item, index) {
return `${index}${item.code}`;
}
const keyExtractor = (item, index) => `${index}${item.code}`

/**
* Given an emoji item object, render a component based on its type.
Expand All @@ -161,7 +119,7 @@ class EmojiPickerMenu extends Component {
* @param {Object} item
* @returns {*}
*/
renderItem({item}) {
const renderItem = ({item}) => {
const {code, types} = item;
if (item.spacer) {
return null;
Expand All @@ -170,75 +128,74 @@ class EmojiPickerMenu extends Component {
if (item.header) {
return (
<View style={styles.emojiHeaderContainer}>
<Text style={styles.textLabelSupporting}>{this.props.translate(`emojiPicker.headers.${code}`)}</Text>
<Text style={styles.textLabelSupporting}>{translate(`emojiPicker.headers.${code}`)}</Text>
</View>
);
}

const emojiCode = types && types[this.props.preferredSkinTone] ? types[this.props.preferredSkinTone] : code;
const emojiCode = types && types[preferredSkinTone] ? types[preferredSkinTone] : code;

return (
<EmojiPickerMenuItem
onPress={(emoji) => this.addToFrequentAndSelectEmoji(emoji, item)}
onPress={(emoji) => addToFrequentAndSelectEmoji(emoji, item)}
emoji={emojiCode}
/>
);
}

render() {
const isFiltered = this.emojis.length !== this.state.filteredEmojis.length;
return (
<View style={styles.emojiPickerContainer}>
<View style={[styles.ph4, styles.pb1, styles.pt2]}>
<TextInput
label={this.props.translate('common.search')}
accessibilityLabel={this.props.translate('common.search')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
onChangeText={this.filterEmojis}
/>
</View>
{!isFiltered && (
<CategoryShortcutBar
headerEmojis={this.headerEmojis}
onPress={this.scrollToHeader}
/>
)}
<Animated.FlatList
ref={(el) => (this.emojiList = el)}
keyboardShouldPersistTaps="handled"
data={this.state.filteredEmojis}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[
StyleUtils.getEmojiPickerListHeight(isFiltered),
{
width: this.props.windowWidth,
},
]}
stickyHeaderIndices={this.state.headerIndices}
getItemLayout={this.getItemLayout}
showsVerticalScrollIndicator
// used because of a bug in RN where stickyHeaderIndices can't be updated after the list is rendered https://github.com/facebook/react-native/issues/25157
removeClippedSubviews={false}
contentContainerStyle={styles.flexGrow1}
ListEmptyComponent={<Text style={[styles.disabledText]}>{this.props.translate('common.noResultsFound')}</Text>}
alwaysBounceVertical={this.state.filteredEmojis.length !== 0}
/>
<EmojiSkinToneList
updatePreferredSkinTone={this.updatePreferredSkinTone}
preferredSkinTone={this.props.preferredSkinTone}
const isFiltered = allEmojis.length !== filteredEmojis.length;

return (
<View style={styles.emojiPickerContainer}>
<View style={[styles.ph4, styles.pb1, styles.pt2]}>
<TextInput
label={translate('common.search')}
accessibilityLabel={translate('common.search')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
onChangeText={filterEmojis}
/>
</View>
);
}
{!isFiltered && (
<CategoryShortcutBar
headerEmojis={headerEmojis}
onPress={scrollToHeader}
/>
)}
<Animated.FlatList
ref={emojiList}
keyboardShouldPersistTaps="handled"
data={filteredEmojis}
renderItem={renderItem}
keyExtractor={keyExtractor}
numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[
StyleUtils.getEmojiPickerListHeight(isFiltered),
{
width: windowWidth,
},
]}
stickyHeaderIndices={headerIndices}
getItemLayout={getItemLayout}
showsVerticalScrollIndicator
// used because of a bug in RN where stickyHeaderIndices can't be updated after the list is rendered https://github.com/facebook/react-native/issues/25157
removeClippedSubviews={false}
contentContainerStyle={styles.flexGrow1}
ListEmptyComponent={<Text style={[styles.disabledText]}>{translate('common.noResultsFound')}</Text>}
alwaysBounceVertical={filteredEmojis.length !== 0}
/>
<EmojiSkinToneList
updatePreferredSkinTone={updatePreferredSkinTone}
preferredSkinTone={preferredSkinTone}
/>
</View>
);
}

EmojiPickerMenu.displayName = 'EmojiPickerMenu';
EmojiPickerMenu.propTypes = propTypes;
EmojiPickerMenu.defaultProps = defaultProps;

export default compose(
withWindowDimensions,
withLocalize,
withOnyx({
preferredSkinTone: {
Expand Down
Loading