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

Mobile emoji picker now has search input #18221

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
069d246
handle paddings for EmojiPicker
perunt May 1, 2023
cdf9cca
add search input for native (mobile)
perunt May 1, 2023
dc5b6da
add search input for mobile on WEB
perunt May 1, 2023
94d8eb7
remove KeyboardSpacer
perunt May 2, 2023
c6d1123
clean
perunt May 2, 2023
fd06379
enableKeyboardAvoiding
perunt May 2, 2023
e1bd040
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 9, 2023
f0e8e3c
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 9, 2023
d1c4362
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 10, 2023
17e8924
prettier
perunt May 10, 2023
1ca75c6
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 10, 2023
73068e1
add index to keyExtractor
perunt May 10, 2023
6b4d03e
update getEmojiPickerListHeight style
perunt May 11, 2023
f734e1c
reduce the height of the web emoji picker on mobile devices
perunt May 11, 2023
f3fdc65
Merge branch 'perunt/search-mobile-emoji-picker' of https://github.co…
perunt May 11, 2023
9b84152
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 11, 2023
133f033
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 12, 2023
43028d2
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt May 23, 2023
1b0ae83
make content resizable
perunt Jun 1, 2023
a25f77c
patch-package react-native-modal
perunt Jun 1, 2023
2cf5a4d
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt Jun 1, 2023
38d2295
lint
perunt Jun 1, 2023
f69bb5b
linter
perunt Jun 1, 2023
e7c137f
clean code
perunt Jun 1, 2023
fdfeb03
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt Jun 5, 2023
1d0ef42
Add top padding consistent with the WEB version for uniformity across…
perunt Jun 5, 2023
e485fd3
Merge branch 'main' of https://github.com/Expensify/App into perunt/s…
perunt Jun 5, 2023
26398de
clean styles
perunt Jun 5, 2023
eb687d3
prop-types and the default value for avoidKeyboard
perunt Jun 5, 2023
251ecbe
getOuterModalStyle
perunt Jun 5, 2023
e9fc1d1
update getOuterModalStyle
perunt Jun 5, 2023
6beba0b
add comment
perunt Jun 7, 2023
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
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 @@ -827,7 +827,12 @@ const CONST = {
WIDTH: 320,
HEIGHT: 416,
},
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256,
CATEGORY_SHORTCUT_BAR_HEIGHT: 40,
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 @@ -2443,6 +2448,7 @@ const CONST = {
FLAG_SEVERITY_HARASSMENT: 'harassment',
FLAG_SEVERITY_ASSAULT: 'assault',
},
EMOJI_PICKER_TEXT_INPUT_SIZES: 130,
};

export default CONST;
19 changes: 17 additions & 2 deletions src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ import _ from 'underscore';
import EmojiPickerMenu from './EmojiPickerMenu';
import CONST from '../../CONST';
import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../withViewportOffsetTop';
import compose from '../../libs/compose';
import * as Browser from '../../libs/Browser';

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 +143,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 +171,10 @@ class EmojiPicker extends React.Component {
width: CONST.EMOJI_PICKER_SIZE.WIDTH,
height: CONST.EMOJI_PICKER_SIZE.HEIGHT,
}}
outerStyle={Browser.isMobile() && {maxHeight: this.props.windowHeight, marginTop: this.props.viewportOffsetTop}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only apply this style to mobile browsers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the web, we handle keyboards differently, in this case, this style is used to adjust content dimensions when the keyboard is opened.

perunt marked this conversation as resolved.
Show resolved Hide resolved
anchorAlignment={this.state.emojiPopoverAnchorOrigin}
innerContainerStyle={{paddingTop: 4, maxHeight: '95%'}}
perunt marked this conversation as resolved.
Show resolved Hide resolved
avoidKeyboard
>
<EmojiPickerMenu
onEmojiSelected={this.selectEmoji}
Expand All @@ -172,4 +185,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.pb2, 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.pb1]}>
<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}`;
}
perunt marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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]}>
<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 @@ -137,6 +137,7 @@ class BaseModal extends PureComponent {
animationOutTiming={this.props.animationOutTiming}
statusBarTranslucent={this.props.statusBarTranslucent}
onLayout={this.props.onLayout}
avoidKeyboard={this.props.avoidKeyboard}
perunt marked this conversation as resolved.
Show resolved Hide resolved
>
<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
26 changes: 25 additions & 1 deletion src/styles/StyleUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ function getFontFamilyMonospace({fontStyle, fontWeight}) {
function getEmojiPickerStyle(isSmallScreenWidth) {
if (isSmallScreenWidth) {
return {
width: '100%',
width: CONST.SMALL_EMOJI_PICKER_SIZE.WIDTH,
};
}
return {
Expand Down Expand Up @@ -1126,6 +1126,29 @@ function getGoogleListViewStyle(shouldDisplayBorder) {
};
}

/**
* Gets the correct height for emoji picker list based on screen dimensions
*
* @param {Boolean} hasAdditionalSpace
* @param {Number} windowHeight
* @returns {Object}
*/
function getEmojiPickerListHeight(hasAdditionalSpace, windowHeight) {
const style = {
paddingHorizontal: 16,
perunt marked this conversation as resolved.
Show resolved Hide resolved
height: hasAdditionalSpace ? CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT : CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT,
};

if (windowHeight) {
const dim = hasAdditionalSpace ? CONST.EMOJI_PICKER_TEXT_INPUT_SIZES : CONST.EMOJI_PICKER_TEXT_INPUT_SIZES + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT;
perunt marked this conversation as resolved.
Show resolved Hide resolved
return {
...style,
maxHeight: windowHeight - dim,
};
}
return style;
}

/**
* Returns style object for the user mention component based on whether the mention is ours or not.
* @param {Boolean} isOurMention
Expand Down Expand Up @@ -1210,6 +1233,7 @@ export {
getFontSizeStyle,
getSignInWordmarkWidthStyle,
getGoogleListViewStyle,
getEmojiPickerListHeight,
getMentionStyle,
getMentionTextColor,
getHeightOfMagicCodeInput,
Expand Down
8 changes: 5 additions & 3 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -1610,12 +1610,14 @@ const styles = {
},

emojiPickerList: {
height: 288,
height: CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT,
width: '100%',
...spacing.ph4,
},
emojiPickerListLandscape: {
height: 240,
emojiPickerListWithPadding: {
height: CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT + CONST.CATEGORY_SHORTCUT_BAR_HEIGHT,
width: '100%',
...spacing.ph4,
perunt marked this conversation as resolved.
Show resolved Hide resolved
},

emojiHeaderContainer: {
Expand Down