Skip to content

Commit

Permalink
Merge pull request #18221 from margelo/perunt/search-mobile-emoji-picker
Browse files Browse the repository at this point in the history
Mobile emoji picker now has search input
  • Loading branch information
stitesExpensify authored Jun 7, 2023
2 parents 5992198 + 6beba0b commit 9ba7cd3
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 53 deletions.
11 changes: 10 additions & 1 deletion patches/react-native-modal+13.0.1.patch
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644
buildPanResponder: () => void;
getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number;
diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js
index 80f4e75..fe028ab 100644
index 80f4e75..a88a2ca 100644
--- a/node_modules/react-native-modal/dist/modal.js
+++ b/node_modules/react-native-modal/dist/modal.js
@@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component {
Expand Down Expand Up @@ -44,3 +44,12 @@ index 80f4e75..fe028ab 100644
if (this.didUpdateDimensionsEmitter) {
this.didUpdateDimensionsEmitter.remove();
}
@@ -525,7 +538,7 @@ export class ReactNativeModal extends React.Component {
}
return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps),
this.makeBackdrop(),
- avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView)));
+ avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: 'padding', pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView)));
}
}
ReactNativeModal.propTypes = {
8 changes: 7 additions & 1 deletion src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,12 @@ const CONST = {
WIDTH: 320,
HEIGHT: 416,
},
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256,
CATEGORY_SHORTCUT_BAR_HEIGHT: 32,
SMALL_EMOJI_PICKER_SIZE: {
WIDTH: '100%',
},
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300,
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200,
EMOJI_PICKER_ITEM_HEIGHT: 32,
EMOJI_PICKER_HEADER_HEIGHT: 32,
RECIPIENT_LOCAL_TIME_HEIGHT: 25,
Expand Down Expand Up @@ -2442,6 +2447,7 @@ const CONST = {
FLAG_SEVERITY_HARASSMENT: 'harassment',
FLAG_SEVERITY_ASSAULT: 'assault',
},
EMOJI_PICKER_TEXT_INPUT_SIZES: 152,
QR: {
DEFAULT_LOGO_SIZE_RATIO: 0.25,
DEFAULT_LOGO_MARGIN_RATIO: 0.02,
Expand Down
20 changes: 18 additions & 2 deletions src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ import {Dimensions, Keyboard} from 'react-native';
import _ from 'underscore';
import EmojiPickerMenu from './EmojiPickerMenu';
import CONST from '../../CONST';
import styles from '../../styles/styles';
import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../withViewportOffsetTop';
import compose from '../../libs/compose';
import * as StyleUtils from '../../styles/StyleUtils';

const DEFAULT_ANCHOR_ORIGIN = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
};

const propTypes = {
...windowDimensionsPropTypes,
...viewportOffsetTopPropTypes,
};

class EmojiPicker extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -134,7 +144,8 @@ class EmojiPicker extends React.Component {
* Focus the search input in the emoji picker.
*/
focusEmojiSearchInput() {
if (!this.emojiSearchInput) {
// we won't focus the input if it's mobile device
if (!this.emojiSearchInput || this.props.isSmallScreenWidth) {
return;
}
this.emojiSearchInput.focus();
Expand All @@ -161,7 +172,10 @@ class EmojiPicker extends React.Component {
width: CONST.EMOJI_PICKER_SIZE.WIDTH,
height: CONST.EMOJI_PICKER_SIZE.HEIGHT,
}}
outerStyle={StyleUtils.getOuterModalStyle(this.props.windowHeight, this.props.viewportOffsetTop)}
anchorAlignment={this.state.emojiPopoverAnchorOrigin}
innerContainerStyle={styles.popoverInnerContainer}
avoidKeyboard
>
<EmojiPickerMenu
onEmojiSelected={this.selectEmoji}
Expand All @@ -172,4 +186,6 @@ class EmojiPicker extends React.Component {
}
}

export default EmojiPicker;
EmojiPicker.propTypes = propTypes;

export default compose(withViewportOffsetTop, withWindowDimensions)(EmojiPicker);
75 changes: 42 additions & 33 deletions src/components/EmojiPicker/EmojiPickerMenu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,16 @@ class EmojiPickerMenu extends Component {
User.updatePreferredSkinTone(skinTone);
}

/**
* Return a unique key for each emoji item
*
* @param {Object} item
* @returns {String}
*/
keyExtractor(item) {
return `emoji_picker_${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
Expand Down Expand Up @@ -482,45 +492,44 @@ class EmojiPickerMenu extends Component {
style={[styles.emojiPickerContainer, StyleUtils.getEmojiPickerStyle(this.props.isSmallScreenWidth)]}
pointerEvents={this.state.arePointerEventsDisabled ? 'none' : 'auto'}
>
{!this.props.isSmallScreenWidth && (
<View style={[styles.ph4, styles.pb3, styles.pt2]}>
<TextInput
label={this.props.translate('common.search')}
onChangeText={this.filterEmojis}
defaultValue=""
ref={(el) => (this.searchInput = el)}
autoFocus
selectTextOnFocus={this.state.selectTextOnFocus}
onSelectionChange={this.onSelectionChange}
onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})}
onBlur={() => this.setState({isFocused: false})}
/>
</View>
)}
<View style={[styles.ph4, styles.pb3, styles.pt2]}>
<TextInput
label={this.props.translate('common.search')}
onChangeText={this.filterEmojis}
defaultValue=""
ref={(el) => (this.searchInput = el)}
autoFocus={!this.props.isSmallScreenWidth}
selectTextOnFocus={this.state.selectTextOnFocus}
onSelectionChange={this.onSelectionChange}
onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})}
onBlur={() => this.setState({isFocused: false})}
autoCorrect={false}
/>
</View>
{!isFiltered && (
<CategoryShortcutBar
headerEmojis={this.headerEmojis}
onPress={this.scrollToHeader}
/>
)}
{this.state.filteredEmojis.length === 0 ? (
<Text style={[styles.disabledText, styles.emojiPickerList, styles.textLabel, styles.colorMuted, this.isMobileLandscape() && styles.emojiPickerListLandscape]}>
{this.props.translate('common.noResultsFound')}
</Text>
) : (
<FlatList
ref={(el) => (this.emojiList = el)}
data={this.state.filteredEmojis}
renderItem={this.renderItem}
keyExtractor={(item) => `emoji_picker_${item.code}`}
numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[styles.emojiPickerList, this.isMobileLandscape() && styles.emojiPickerListLandscape]}
extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]}
stickyHeaderIndices={this.state.headerIndices}
onScroll={(e) => (this.currentScrollOffset = e.nativeEvent.contentOffset.y)}
getItemLayout={this.getItemLayout}
/>
)}
<FlatList
ref={(el) => (this.emojiList = el)}
data={this.state.filteredEmojis}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
numColumns={CONST.EMOJI_NUM_PER_ROW}
style={StyleUtils.getEmojiPickerListHeight(isFiltered, this.props.windowHeight)}
extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]}
stickyHeaderIndices={this.state.headerIndices}
onScroll={(e) => (this.currentScrollOffset = e.nativeEvent.contentOffset.y)}
getItemLayout={this.getItemLayout}
contentContainerStyle={styles.flexGrow1}
ListEmptyComponent={
<View style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.flex1]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{this.props.translate('common.noResultsFound')}</Text>
</View>
}
/>
<EmojiSkinToneList
updatePreferredSkinTone={this.updatePreferredSkinTone}
preferredSkinTone={this.props.preferredSkinTone}
Expand Down
80 changes: 74 additions & 6 deletions src/components/EmojiPicker/EmojiPickerMenu/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import EmojiSkinToneList from '../EmojiSkinToneList';
import * as EmojiUtils from '../../../libs/EmojiUtils';
import * as User from '../../../libs/actions/User';
import TextInput from '../../TextInput';
import CategoryShortcutBar from '../CategoryShortcutBar';
import * as StyleUtils from '../../../styles/StyleUtils';

const propTypes = {
/** Function to add the selected emoji to the main compose text input */
Expand Down Expand Up @@ -57,14 +59,48 @@ class EmojiPickerMenu extends Component {
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);

this.state = {
filteredEmojis: this.emojis,
headerIndices: this.headerRowIndices,
};
}

getItemLayout(data, index) {
return {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 normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');

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

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

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

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

/**
* @param {String} emoji
* @param {Object} emojiObject
Expand Down Expand Up @@ -106,6 +142,17 @@ class EmojiPickerMenu extends Component {
})();
}

/**
* Return a unique key for each emoji item
*
* @param {Object} item
* @param {Number} index
* @returns {String}
*/
keyExtractor(item, index) {
return `${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
Expand Down Expand Up @@ -139,24 +186,45 @@ class EmojiPickerMenu extends Component {
}

render() {
const isFiltered = this.emojis.length !== this.state.filteredEmojis.length;
return (
<View style={styles.emojiPickerContainer}>
<View>
<View style={[styles.ph4, styles.pb1, styles.pt2]}>
<TextInput
label={this.props.translate('common.search')}
onChangeText={this.filterEmojis}
/>
</View>
{!isFiltered && (
<CategoryShortcutBar
headerEmojis={this.headerEmojis}
onPress={this.scrollToHeader}
/>
</View>
)}
<Animated.FlatList
ref={(el) => (this.emojiList = el)}
data={this.emojis}
keyboardShouldPersistTaps="handled"
data={this.state.filteredEmojis}
renderItem={this.renderItem}
keyExtractor={(item) => `emoji_picker_${item.code}`}
keyExtractor={this.keyExtractor}
numColumns={CONST.EMOJI_NUM_PER_ROW}
style={[styles.emojiPickerList, this.isMobileLandscape() && styles.emojiPickerListLandscape]}
stickyHeaderIndices={this.headerRowIndices}
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={
<View style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.flex1]}>
<Text style={[styles.disabledText]}>{this.props.translate('common.noResultsFound')}</Text>
</View>
}
/>
<EmojiSkinToneList
updatePreferredSkinTone={this.updatePreferredSkinTone}
Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/BaseModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class BaseModal extends PureComponent {
animationOutTiming={this.props.animationOutTiming}
statusBarTranslucent={this.props.statusBarTranslucent}
onLayout={this.props.onLayout}
avoidKeyboard={this.props.avoidKeyboard}
>
<SafeAreaInsetsContext.Consumer>
{(insets) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/index.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const Modal = (props) => {
{...props}
onModalHide={hideModal}
onModalShow={showModal}
avoidKeyboard={false}
>
{props.children}
</BaseModal>
Expand Down
4 changes: 4 additions & 0 deletions src/components/Modal/modalPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const propTypes = {
/** Whether the modal should go under the system statusbar */
statusBarTranslucent: PropTypes.bool,

/** Whether the modal should avoid the keyboard */
avoidKeyboard: PropTypes.bool,

...windowDimensionsPropTypes,
};

Expand All @@ -71,6 +74,7 @@ const defaultProps = {
popoverAnchorPosition: {},
innerContainerStyle: {},
statusBarTranslucent: true,
avoidKeyboard: false,
};

export {propTypes, defaultProps};
Loading

0 comments on commit 9ba7cd3

Please sign in to comment.