MM-18241 Fix emoji picker selection and added keyboard tracking on iOS (#3244)

* MM-18241 Fix emoji picker selection and added keyboard tracking on iOS

* Fix flatlist spacing on iOS when searching

* Fix snapshot
This commit is contained in:
Elias Nahum 2019-09-16 00:31:54 -03:00 committed by Miguel Alatzar
parent 6fc8569266
commit d28aaf21a8
7 changed files with 302 additions and 187 deletions

View file

@ -2,12 +2,13 @@
exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
<Connect(SafeAreaIos)
excludeFooter={true}
excludeHeader={true}
>
<KeyboardAvoidingView
behavior="padding"
enabled={true}
keyboardVerticalOffset={107}
enabled={false}
keyboardVerticalOffset={50}
style={
Object {
"flex": 1,
@ -59,11 +60,13 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"flex": 1,
}
Array [
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"flex": 1,
},
]
}
>
<SectionList
@ -74,15 +77,17 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
nativeID="emojiPicker"
onEndReached={[Function]}
onEndReachedThreshold={0}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
pageSize={30}
removeClippedSubviews={true}
removeClippedSubviews={false}
renderItem={[Function]}
renderSectionHeader={[Function]}
scrollEventThrottle={50}
@ -91,10 +96,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
stickySectionHeadersEnabled={true}
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginBottom": 35,
},
Object {},
Object {
"width": 370,
},
@ -103,30 +105,34 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
updateCellsBatchingPeriod={50}
windowSize={21}
/>
<View
style={
Object {
"bottom": 0,
"height": 35,
"left": 0,
"position": "absolute",
"right": 0,
"width": "100%",
}
}
<KeyboardTrackingView
normalList={true}
scrollViewNativeID="emojiPicker"
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"borderTopColor": "rgba(61,60,64,0.3)",
"borderTopWidth": 1,
"backgroundColor": "#ffffff",
"flexDirection": "row",
"justifyContent": "space-between",
"height": 35,
"width": "100%",
}
}
/>
</View>
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"borderTopColor": "rgba(61,60,64,0.3)",
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "space-between",
"width": "100%",
}
}
/>
</View>
</KeyboardTrackingView>
</View>
</KeyboardAvoidingView>
</Connect(SafeAreaIos)>

View file

@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {
FlatList,
SectionList,
View,
} from 'react-native';
import SearchBar from 'app/components/search_bar';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {deviceWidth, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
keyboardShouldPersistTaps='always'
style={styles.flatList}
data={filteredEmojis}
keyExtractor={this.flatListKeyExtractor}
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
removeClippedSubviews={true}
/>
);
} else {
listComponent = (
<SectionList
ref={this.attachSectionList}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
keyboardShouldPersistTaps='always'
getItemLayout={this.sectionListGetItemLayout}
removeClippedSubviews={true}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
onMomentumScrollEnd={this.onMomentumScrollEnd}
pageSize={30}
ListFooterComponent={this.renderFooter}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={1}
/>
);
}
const searchBarInput = {
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13,
marginBottom: -3,
};
return (
<React.Fragment>
<View style={styles.searchBar}>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={styles.container}>
{listComponent}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
}
</View>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {
FlatList,
KeyboardAvoidingView,
SectionList,
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import SafeAreaView from 'app/components/safe_area_view';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
export default class EmojiPicker extends EmojiPickerBase {
render() {
const {formatMessage} = this.context.intl;
const {deviceWidth, isLandscape, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_X && isLandscape ? 6 : 2;
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
data={filteredEmojis}
initialListSize={10}
keyboardShouldPersistTaps='always'
keyExtractor={this.flatListKeyExtractor}
nativeID={SCROLLVIEW_NATIVE_ID}
pageSize={10}
renderItem={this.flatListRenderItem}
style={styles.flatList}
/>
);
} else {
listComponent = (
<SectionList
getItemLayout={this.sectionListGetItemLayout}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
ListFooterComponent={this.renderFooter}
nativeID={SCROLLVIEW_NATIVE_ID}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={0}
onMomentumScrollEnd={this.onMomentumScrollEnd}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
pageSize={30}
ref={this.attachSectionList}
removeClippedSubviews={false}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
sections={emojis}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
/>
);
}
let keyboardOffset = DeviceTypes.IS_IPHONE_X ? 50 : 30;
if (isLandscape) {
keyboardOffset = DeviceTypes.IS_IPHONE_X ? 0 : 10;
}
const searchBarInput = {
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13,
};
return (
<SafeAreaView
excludeHeader={true}
excludeFooter={true}
>
<KeyboardAvoidingView
behavior='padding'
enabled={Boolean(searchTerm)}
keyboardVerticalOffset={keyboardOffset}
style={styles.flex}
>
<View style={[styles.searchBar, padding(isLandscape)]}>
<SearchBar
ref={this.searchBarRef}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={[styles.container]}>
{listComponent}
{!searchTerm &&
<KeyboardTrackingView
ref={this.keyboardTracker}
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
normalList={true}
>
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
</KeyboardTrackingView>
}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}

View file

@ -6,7 +6,8 @@ import React from 'react';
import Preferences from 'mattermost-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper';
import EmojiPicker, {filterEmojiSearchInput} from './emoji_picker.js';
import {filterEmojiSearchInput} from './emoji_picker_base';
import EmojiPicker from './emoji_picker.ios';
describe('components/emoji_picker/EmojiPicker', () => {
const baseProps = {

View file

@ -6,10 +6,7 @@ import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
ActivityIndicator,
FlatList,
KeyboardAvoidingView,
Platform,
SectionList,
Text,
TouchableOpacity,
View,
@ -21,23 +18,20 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import {DeviceTypes} from 'app/constants';
import {emptyFunction} from 'app/utils/general';
import {
makeStyleSheetFromTheme,
changeOpacity,
getKeyboardAppearanceFromTheme,
} from 'app/utils/theme';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import EmojiPickerRow from './emoji_picker_row';
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
const SECTION_HEADER_HEIGHT = 28;
const EMOJIS_PER_PAGE = 200;
const SECTION_HEADER_HEIGHT = 28;
export const SECTION_MARGIN = 15;
export function filterEmojiSearchInput(searchText) {
return searchText.toLowerCase().replace(/^:|:$/g, '');
@ -83,6 +77,7 @@ export default class EmojiPicker extends PureComponent {
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
this.searchBarRef = React.createRef();
this.scrollToSectionTries = 0;
this.state = {
emojis,
@ -99,8 +94,8 @@ export default class EmojiPicker extends PureComponent {
if (this.props.deviceWidth !== nextProps.deviceWidth) {
this.rebuildEmojis = true;
if (this.refs.search_bar) {
this.refs.search_bar.blur();
if (this.searchBarRef?.current) {
this.searchBarRef.current.blur();
}
}
@ -399,118 +394,9 @@ export default class EmojiPicker extends PureComponent {
</View>
);
}
render() {
const {deviceWidth, isLandscape, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const {intl} = this.context;
const {formatMessage} = intl;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_X && isLandscape ? 6 : 2;
let listComponent;
if (searchTerm) {
listComponent = (
<FlatList
keyboardShouldPersistTaps='always'
style={styles.flatList}
data={filteredEmojis}
keyExtractor={this.flatListKeyExtractor}
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
removeClippedSubviews={true}
/>
);
} else {
listComponent = (
<SectionList
ref={this.attachSectionList}
showsVerticalScrollIndicator={false}
style={[styles.listView, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
keyboardShouldPersistTaps='always'
getItemLayout={this.sectionListGetItemLayout}
removeClippedSubviews={true}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
onMomentumScrollEnd={this.onMomentumScrollEnd}
pageSize={30}
ListFooterComponent={this.renderFooter}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
/>
);
}
let keyboardOffset = 64;
if (Platform.OS === 'android') {
keyboardOffset = -200;
} else if (DeviceTypes.IS_IPHONE_X) {
keyboardOffset = isLandscape ? 35 : 107;
} else if (isLandscape) {
keyboardOffset = 52;
}
const searchBarInput = {
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13,
...Platform.select({
android: {
marginBottom: -3,
},
}),
};
return (
<SafeAreaView excludeHeader={true}>
<KeyboardAvoidingView
behavior='padding'
style={styles.flex}
keyboardVerticalOffset={keyboardOffset}
enabled={Platform.OS === 'ios'}
>
<View style={[styles.searchBar, padding(isLandscape)]}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={styles.container}>
{listComponent}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
return {
flex: {
flex: 1,
@ -521,39 +407,29 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
borderTopWidth: 1,
flexDirection: 'row',
justifyContent: 'space-between',
},
bottomContentWrapper: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 35,
width: '100%',
},
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
marginVertical: EMOJI_GUTTER,
justifyContent: 'flex-start',
bottomContentWrapper: {
...Platform.select({
android: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
ios: {
width: '100%',
flexDirection: 'row',
},
}),
backgroundColor: theme.centerChannelBg,
height: 35,
},
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flex: 1,
},
emoji: {
width: EMOJI_SIZE,
height: EMOJI_SIZE,
marginHorizontal: EMOJI_GUTTER,
alignItems: 'center',
justifyContent: 'center',
},
emojiLeft: {
marginLeft: 0,
},
emojiRight: {
marginRight: 0,
},
flatList: {
flex: 1,
backgroundColor: theme.centerChannelBg,
@ -580,16 +456,16 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2),
overflow: 'hidden',
},
listView: {
backgroundColor: theme.centerChannelBg,
marginBottom: 35,
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
paddingVertical: 5,
},
section: {
alignItems: 'center',
sectionList: {
...Platform.select({
android: {
marginBottom: 35,
},
}),
},
sectionIcon: {
color: changeOpacity(theme.centerChannelColor, 0.3),
@ -613,9 +489,6 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
backgroundColor: theme.centerChannelBg,
},
wrapper: {
flex: 1,
},
loading: {
flex: 1,
alignItems: 'center',

4
package-lock.json generated
View file

@ -16580,8 +16580,8 @@
}
},
"react-native-keyboard-tracking-view": {
"version": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2",
"from": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2"
"version": "github:enahum/react-native-keyboard-tracking-view#6ac2c73dee8f86be57bfe6509f59db6e04d36594",
"from": "github:enahum/react-native-keyboard-tracking-view#6ac2c73dee8f86be57bfe6509f59db6e04d36594"
},
"react-native-keychain": {
"version": "3.1.3",

View file

@ -43,7 +43,7 @@
"react-native-image-gallery": "github:mattermost/react-native-image-gallery#c1a9f7118e90cc87d47620bc0584c9cac4b0cf38",
"react-native-image-picker": "0.28.1",
"react-native-keyboard-aware-scroll-view": "0.8.0",
"react-native-keyboard-tracking-view": "github:enahum/react-native-keyboard-tracking-view#66979a750e42fefe06366f0c924b29368bb586f2",
"react-native-keyboard-tracking-view": "github:enahum/react-native-keyboard-tracking-view#6ac2c73dee8f86be57bfe6509f59db6e04d36594",
"react-native-keychain": "3.1.3",
"react-native-linear-gradient": "2.5.4",
"react-native-local-auth": "github:mattermost/react-native-local-auth#cc9ce2f468fbf7b431dfad3191a31aaa9227a6ab",