MM-18453 Use native emojis (#3260)

* MM-18453 Use native emojis

* Remove unnecessary else statement

* Add support for builtIn emojis

* Fix Native emojis snapshot tests

* Set emoji text style from StyleSheet

* Remove unneded version check and try/catch

* Add builtIn emojis as custom emojis in emojiPicker

* Simplify emojiPicker lists and fix jumping section headers

* Update snapshots
This commit is contained in:
Elias Nahum 2019-09-25 12:27:47 +03:00 committed by GitHub
parent 218e104010
commit 114a18c16d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 158 additions and 119 deletions

View file

@ -15,6 +15,8 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import Emoji from 'app/components/emoji';
import {BuiltInEmojis} from 'app/utils/emojis';
import {getEmojiByName} from 'app/utils/emoji_utils';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
@ -138,10 +140,16 @@ export default class EmojiSuggestion extends Component {
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft;
let prefix = ':';
if (Platform.OS === 'ios') {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
prefix = '::';
}
const emojiData = getEmojiByName(emoji);
if (emojiData?.filename && !BuiltInEmojis.includes(emojiData.filename)) {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, String.fromCodePoint(parseInt(emojiData.filename, 16)));
} else {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `${prefix}${emoji}: `);
}
if (value.length > cursorPosition) {
@ -150,7 +158,7 @@ export default class EmojiSuggestion extends Component {
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
if (Platform.OS === 'ios' && (!emojiData?.filename || BuiltInEmojis.includes(emojiData?.filename))) {
// This is the second part of the hack were we replace the double : with just one
// after the auto correct vanished
setTimeout(() => {
@ -178,6 +186,7 @@ export default class EmojiSuggestion extends Component {
<View style={style.emoji}>
<Emoji
emojiName={item}
textStyle={style.emojiText}
size={20}
/>
</View>
@ -225,6 +234,10 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 13,
color: theme.centerChannelColor,
},
emojiText: {
color: '#000',
fontWeight: 'bold',
},
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,

View file

@ -38,6 +38,7 @@ export default class Emoji extends React.PureComponent {
literal: PropTypes.string,
size: PropTypes.number,
textStyle: CustomPropTypes.Style,
unicode: PropTypes.string,
};
static defaultProps = {
@ -116,6 +117,19 @@ export default class Emoji extends React.PureComponent {
// force a new image to be rendered when the size changes
const key = Platform.OS === 'android' ? (height + '-' + width) : null;
if (this.props.unicode && !this.props.imageUrl) {
const codeArray = this.props.unicode.split('-');
const code = codeArray.reduce((acc, c) => {
return acc + String.fromCodePoint(parseInt(c, 16));
}, '');
return (
<Text style={[this.props.textStyle, {fontSize: size}]}>
{code}
</Text>
);
}
if (!imageUrl) {
return (
<Image

View file

@ -9,7 +9,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {Client4} from 'mattermost-redux/client';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import Emoji from './emoji';
@ -19,11 +19,15 @@ function mapStateToProps(state, ownProps) {
const customEmojis = getCustomEmojisByName(state);
let imageUrl = '';
let unicode;
let isCustomEmoji = false;
let displayTextOnly = false;
if (EmojiIndicesByAlias.has(emojiName)) {
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
unicode = emoji.filename;
if (BuiltInEmojis.includes(emojiName)) {
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
}
} else if (customEmojis.has(emojiName)) {
const emoji = customEmojis.get(emojiName);
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
@ -33,7 +37,6 @@ function mapStateToProps(state, ownProps) {
config.EnableCustomEmoji !== 'true' ||
config.ExperimentalEnablePostMetadata === 'true' ||
getCurrentUserId(state) === '' ||
!isMinimumServerVersion(Client4.getServerVersion(), 4, 7) ||
isMinimumServerVersion(Client4.getServerVersion(), 5, 12);
}
@ -41,6 +44,7 @@ function mapStateToProps(state, ownProps) {
imageUrl,
isCustomEmoji,
displayTextOnly,
unicode,
};
}

View file

@ -75,7 +75,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
disableVirtualization={false}
getItemLayout={[Function]}
horizontal={false}
initialNumToRender={10}
initialNumToRender={50}
keyExtractor={[Function]}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="always"
@ -86,7 +86,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
pageSize={30}
pageSize={50}
removeClippedSubviews={false}
renderItem={[Function]}
renderSectionHeader={[Function]}

View file

@ -2,61 +2,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {
FlatList,
SectionList,
View,
} from 'react-native';
import {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';
import EmojiPickerBase, {getStyleSheetFromTheme} 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 {theme} = this.props;
const {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,
@ -87,7 +46,7 @@ export default class EmojiPicker extends EmojiPickerBase {
/>
</View>
<View style={styles.container}>
{listComponent}
{this.renderListComponent(2)}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>

View file

@ -3,9 +3,7 @@
import React from 'react';
import {
FlatList,
KeyboardAvoidingView,
SectionList,
View,
} from 'react-native';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
@ -16,58 +14,17 @@ 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';
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
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 {isLandscape, theme} = this.props;
const {searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && 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_WITH_INSETS ? 50 : 30;
if (isLandscape) {
keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10;
@ -111,7 +68,7 @@ export default class EmojiPicker extends EmojiPickerBase {
/>
</View>
<View style={[styles.container]}>
{listComponent}
{this.renderListComponent(shorten)}
{!searchTerm &&
<KeyboardTrackingView
ref={this.keyboardTracker}

View file

@ -6,7 +6,9 @@ import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
ActivityIndicator,
FlatList,
Platform,
SectionList,
Text,
TouchableOpacity,
View,
@ -28,10 +30,11 @@ import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone
import EmojiPickerRow from './emoji_picker_row';
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const EMOJI_GUTTER = 7;
const EMOJIS_PER_PAGE = 200;
const SECTION_HEADER_HEIGHT = 28;
export const SECTION_MARGIN = 15;
const SECTION_MARGIN = 15;
export const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
export function filterEmojiSearchInput(searchText) {
return searchText.toLowerCase().replace(/^:|:$/g, '');
@ -69,7 +72,7 @@ export default class EmojiPicker extends PureComponent {
this.sectionListGetItemLayout = sectionListGetItemLayout({
getItemHeight: () => {
return EMOJI_SIZE + (EMOJI_GUTTER * 2);
return (EMOJI_SIZE + 5) + (EMOJI_GUTTER * 2);
},
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
});
@ -198,10 +201,6 @@ export default class EmojiPicker extends PureComponent {
});
};
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
};
searchEmojis = (searchTerm) => {
const {emojis, fuse} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
@ -232,6 +231,54 @@ export default class EmojiPicker extends PureComponent {
);
};
renderListComponent = (shorten) => {
const {deviceWidth, theme} = this.props;
const {emojis, filteredEmojis, searchTerm} = this.state;
const styles = getStyleSheetFromTheme(theme);
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}
initialNumToRender={50}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
ListFooterComponent={this.renderFooter}
nativeID={SCROLLVIEW_NATIVE_ID}
onEndReached={this.loadMoreCustomEmojis}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
onMomentumScrollEnd={this.onMomentumScrollEnd}
onScroll={this.onScroll}
onScrollToIndexFailed={this.handleScrollToSectionFailed}
pageSize={50}
ref={this.attachSectionList}
removeClippedSubviews={false}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
sections={emojis}
showsVerticalScrollIndicator={false}
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
/>
);
}
return listComponent;
};
flatListKeyExtractor = (item) => item;
flatListRenderItem = ({item}) => {
@ -271,7 +318,7 @@ export default class EmojiPicker extends PureComponent {
}
this.props.actions.incrementEmojiPickerPage();
}
};
onScroll = (e) => {
if (this.state.jumpToSection) {
@ -324,7 +371,7 @@ export default class EmojiPicker extends PureComponent {
this.scrollToSection(index);
}, 200);
}
}
};
renderSectionHeader = ({section}) => {
const {theme} = this.props;
@ -351,7 +398,7 @@ export default class EmojiPicker extends PureComponent {
if (isCustomSection && this.props.customEmojiPage === 0) {
this.loadMoreCustomEmojis();
}
}
};
renderSectionIcons = () => {
const {theme} = this.props;
@ -393,7 +440,7 @@ export default class EmojiPicker extends PureComponent {
<ActivityIndicator/>
</View>
);
}
};
}
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {

View file

@ -27,11 +27,12 @@ export default class EmojiPickerRow extends Component {
renderEmojis = (emoji, index, emojis) => {
const {emojiGutter, emojiSize} = this.props;
const size = emojiSize + 5;
const style = [
styles.emoji,
{
width: emojiSize,
height: emojiSize,
width: size,
height: size,
marginHorizontal: emojiGutter,
},
];
@ -60,6 +61,7 @@ export default class EmojiPickerRow extends Component {
>
<Emoji
emojiName={emoji.name}
textStyle={styles.emojiText}
size={emojiSize}
/>
</TouchableOpacity>
@ -79,7 +81,7 @@ export default class EmojiPickerRow extends Component {
const styles = StyleSheet.create({
columnStyle: {
alignSelf: 'stretch',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
},
@ -88,6 +90,10 @@ const styles = StyleSheet.create({
justifyContent: 'center',
overflow: 'hidden',
},
emojiText: {
color: '#000',
fontWeight: 'bold',
},
emojiLeft: {
marginLeft: 0,
},

View file

@ -12,7 +12,7 @@ import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emoj
import {incrementEmojiPickerPage} from 'app/actions/views/emoji';
import {getDimensions, isLandscape} from 'app/selectors/device';
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import {BuiltInEmojis, CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import {t} from 'app/utils/i18n';
import EmojiPicker from './emoji_picker';
@ -96,6 +96,11 @@ const getEmojisBySection = createSelector(
});
const customEmojiItems = [];
BuiltInEmojis.forEach((emoji) => {
customEmojiItems.push({
name: emoji,
});
});
for (const [key] of customEmojis) {
customEmojiItems.push({

View file

@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
Text,
TouchableOpacity,
} from 'react-native';
@ -46,6 +47,7 @@ export default class Reaction extends PureComponent {
<Emoji
emojiName={emojiName}
size={20}
textStyle={{color: 'black', fontWeight: 'bold'}}
padding={5}
/>
<Text style={styles.count}>{count}</Text>
@ -73,8 +75,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
marginRight: 6,
marginBottom: 5,
marginTop: 10,
paddingVertical: 2,
paddingHorizontal: 6,
...Platform.select({
android: {
paddingBottom: 2,
},
}),
},
};
});

View file

@ -3,7 +3,7 @@
import emojiRegex from 'emoji-regex';
import {EmojiIndicesByAlias} from './emojis';
import {Emojis, EmojiIndicesByAlias} from './emojis';
const RE_NAMED_EMOJI = /(:([a-zA-Z0-9_-]+):)/g;
@ -105,3 +105,11 @@ export function doesMatchNamedEmoji(emojiName) {
return false;
}
export function getEmojiByName(emojiName) {
if (EmojiIndicesByAlias.has(emojiName)) {
return Emojis[EmojiIndicesByAlias.get(emojiName)];
}
return null;
}

File diff suppressed because one or more lines are too long