diff --git a/Makefile b/Makefile
index ccaa11ba8..a292ed1ad 100644
--- a/Makefile
+++ b/Makefile
@@ -122,7 +122,7 @@ post-install:
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
- @sed -i'' -e 's^getItemLayout || index <= this._highestMeasuredFrameIndex,^!getItemLayout || index <= this._highestMeasuredFrameIndex,^g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
+ @sed -i'' -e 's^getItemLayout || index <= this._highestMeasuredFrameIndex,^!getItemLayout || index !== -1,^g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
diff --git a/app/actions/views/emoji.js b/app/actions/views/emoji.js
index bc2d75825..c2feff4e1 100644
--- a/app/actions/views/emoji.js
+++ b/app/actions/views/emoji.js
@@ -1,17 +1,34 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import {addReaction} from 'mattermost-redux/actions/posts';
+import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
+import {ViewTypes} from 'app/constants';
+
const getPostIdsForThread = makeGetPostIdsForThread();
+export function addReaction(postId, emoji) {
+ return (dispatch) => {
+ dispatch(serviceAddReaction(postId, emoji));
+ dispatch(addRecentEmoji(emoji));
+ };
+}
+
export function addReactionToLatestPost(emoji, rootId) {
return async (dispatch, getState) => {
const state = getState();
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
const lastPostId = postIds[0];
- dispatch(addReaction(lastPostId, emoji));
+ dispatch(serviceAddReaction(lastPostId, emoji));
+ dispatch(addRecentEmoji(emoji));
+ };
+}
+
+export function addRecentEmoji(emoji) {
+ return {
+ type: ViewTypes.ADD_RECENT_EMOJI,
+ emoji
};
}
diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js
index 5c8b701b9..ccd24e073 100644
--- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js
+++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js
@@ -66,7 +66,17 @@ export default class EmojiSuggestion extends Component {
let data = [];
if (matchTerm.length) {
- data = nextProps.emojis.filter((emoji) => emoji.startsWith(matchTerm.toLowerCase())).sort();
+ const lowerCaseMatchTerm = matchTerm.toLowerCase();
+ const startsWith = [];
+ const includes = [];
+ nextProps.emojis.forEach((emoji) => {
+ if (emoji.startsWith(lowerCaseMatchTerm)) {
+ startsWith.push(emoji);
+ } else {
+ includes.push(emoji);
+ }
+ });
+ data = [...startsWith.sort(), ...includes.sort()];
} else {
const initialEmojis = [...nextProps.emojis];
initialEmojis.splice(0, 300);
diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js
index 27c22dd5e..b3432eb7e 100644
--- a/app/components/emoji_picker/emoji_picker.js
+++ b/app/components/emoji_picker/emoji_picker.js
@@ -3,12 +3,18 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
import {
+ FlatList,
+ KeyboardAvoidingView,
+ Platform,
SectionList,
+ Text,
TouchableOpacity,
View
} from 'react-native';
+import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
+import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
@@ -16,15 +22,19 @@ import SearchBar from 'app/components/search_bar';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
+import EmojiPickerRow from './emoji_picker_row';
+
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
+const SECTION_HEADER_HEIGHT = 28;
-class EmojiPicker extends PureComponent {
+export default class EmojiPicker extends PureComponent {
static propTypes = {
- emojis: PropTypes.array.isRequired,
+ emojisByName: PropTypes.array.isRequired,
+ emojisBySection: PropTypes.array.isRequired,
deviceWidth: PropTypes.number.isRequired,
- intl: intlShape.isRequired,
+ isLandscape: PropTypes.bool.isRequired,
onEmojiPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
@@ -33,95 +43,229 @@ class EmojiPicker extends PureComponent {
onEmojiPress: emptyFunction
};
- leftButton = {
- id: 'close-edit-post'
- };
+ static contextTypes = {
+ intl: intlShape.isRequired
+ }
constructor(props) {
super(props);
+ this.sectionListGetItemLayout = sectionListGetItemLayout({
+ getItemHeight: () => {
+ return EMOJI_SIZE + (EMOJI_GUTTER * 2);
+ },
+ getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT
+ });
+
+ const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
+ const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
+
this.state = {
- emojis: props.emojis,
+ emojis,
+ emojiSectionIndexByOffset,
+ filteredEmojis: [],
searchTerm: '',
- width: props.deviceWidth - (SECTION_MARGIN * 2)
+ currentSectionIndex: 0
};
}
componentWillReceiveProps(nextProps) {
- if (nextProps.deviceWidth !== this.props.deviceWidth) {
+ if (this.props.deviceWidth !== nextProps.deviceWidth) {
this.setState({
- width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
+ emojis: this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth)
});
}
}
+ renderableEmojis = (emojis, deviceWidth) => {
+ const numberOfColumns = this.getNumberOfColumns(deviceWidth);
+
+ const nextEmojis = emojis.map((section) => {
+ const data = [];
+ let row = {
+ key: `${section.key}-0`,
+ items: []
+ };
+
+ section.data.forEach((emoji, index) => {
+ if (index % numberOfColumns === 0 && index !== 0) {
+ data.push(row);
+ row = {
+ key: `${section.key}-${index}`,
+ items: []
+ };
+ }
+
+ row.items.push(emoji);
+ });
+
+ if (row.items.length) {
+ if (row.items.length < numberOfColumns) {
+ // push some empty items to make sure flexbox can justfiy content correctly
+ const emptyEmojis = new Array(numberOfColumns - row.items.length);
+ row.items.push(...emptyEmojis);
+ }
+
+ data.push(row);
+ }
+
+ return {
+ ...section,
+ data
+ };
+ });
+
+ return nextEmojis;
+ }
+
+ measureEmojiSections = (emojiSections) => {
+ let lastOffset = 0;
+ return emojiSections.map((section) => {
+ const start = lastOffset;
+ const nextOffset = (section.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2))) + SECTION_HEADER_HEIGHT;
+ lastOffset += nextOffset;
+
+ return start;
+ });
+ }
+
changeSearchTerm = (text) => {
this.setState({
searchTerm: text
});
clearTimeout(this.searchTermTimeout);
- const timeout = text ? 350 : 0;
+ const timeout = text ? 100 : 0;
this.searchTermTimeout = setTimeout(() => {
- const emojis = this.searchEmojis(text);
+ const filteredEmojis = this.searchEmojis(text);
this.setState({
- emojis
+ filteredEmojis
});
}, timeout);
};
cancelSearch = () => {
this.setState({
- emojis: this.props.emojis,
+ filteredEmojis: [],
searchTerm: ''
});
- };
+ }
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
- };
+ }
searchEmojis = (searchTerm) => {
- const {emojis} = this.props;
+ const {emojisByName} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
- return emojis;
+ return [];
}
- const nextEmojis = [];
- emojis.forEach((section) => {
- const {data, ...otherProps} = section;
- const {key, items} = data[0];
-
- const nextData = {
- key,
- items: items.filter((item) => {
- if (item.aliases) {
- return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
- }
-
- return item.name.includes(searchTermLowerCase);
- })
- };
-
- if (nextData.items.length) {
- nextEmojis.push({
- ...otherProps,
- data: [nextData]
- });
+ const startsWith = [];
+ const includes = [];
+ emojisByName.forEach((emoji) => {
+ if (emoji.startsWith(searchTermLowerCase)) {
+ startsWith.push(emoji);
+ } else if (emoji.includes(searchTermLowerCase)) {
+ includes.push(emoji);
}
});
- return nextEmojis;
+ return [...startsWith.sort(), ...includes.sort()];
+ }
+
+ getNumberOfColumns = (deviceWidth) => {
+ return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2)))));
+ }
+
+ renderItem = ({item}) => {
+ return (
+
+ );
+ }
+
+ flatListKeyExtractor = (item) => item;
+
+ flatListRenderItem = ({item}) => {
+ const style = getStyleSheetFromTheme(this.props.theme);
+
+ return (
+ this.props.onEmojiPress(item)}
+ style={style.flatListRow}
+ >
+
+
+
+ {`:${item}:`}
+
+ );
};
+ onScroll = (e) => {
+ if (this.state.jumpToSection) {
+ return;
+ }
+
+ clearTimeout(this.setIndexTimeout);
+
+ const {contentOffset} = e.nativeEvent;
+ let nextIndex = this.state.emojiSectionIndexByOffset.findIndex((offset) => contentOffset.y <= offset);
+
+ if (nextIndex === -1) {
+ nextIndex = this.state.emojiSectionIndexByOffset.length - 1;
+ } else if (nextIndex !== 0) {
+ nextIndex -= 1;
+ }
+
+ if (nextIndex !== this.state.currentSectionIndex) {
+ this.setState({
+ currentSectionIndex: nextIndex
+ });
+ }
+ }
+
+ onMomentumScrollEnd = () => {
+ if (this.state.jumpToSection) {
+ this.setState({
+ jumpToSection: false
+ });
+ }
+ }
+
+ scrollToSection = (index) => {
+ this.setState({
+ jumpToSection: true,
+ currentSectionIndex: index
+ }, () => {
+ this.sectionList.scrollToLocation({
+ sectionIndex: index,
+ itemIndex: 0,
+ viewOffset: 25
+ });
+ });
+ }
+
renderSectionHeader = ({section}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
-
+
);
- };
+ }
- renderEmojis = (emojis, index) => {
+ renderSectionIcons = () => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
- return (
-
- {emojis.map((emoji, emojiIndex) => {
- const style = [styles.emoji];
- if (emojiIndex === 0) {
- style.push(styles.emojiLeft);
- } else if (emojiIndex === emojis.length - 1) {
- style.push(styles.emojiRight);
- }
+ return this.state.emojis.map((section, index) => {
+ const onPress = () => this.scrollToSection(index);
- return (
- {
- this.props.onEmojiPress(emoji.name);
- }}
- >
-
-
- );
- })}
-
- );
- };
+ return (
+
+
+
+ );
+ });
+ }
- renderItem = ({item}) => {
- const {theme} = this.props;
- const styles = getStyleSheetFromTheme(theme);
+ sectionListKeyExtractor = (item) => item.key
- const numColumns = Number((this.state.width / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
-
- const slices = item.items.reduce((slice, emoji, emojiIndex) => {
- if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
- slice.push([]);
- }
-
- slice[slice.length - 1].push(emoji);
-
- return slice;
- }, [[]]);
-
- return (
-
- {slices.map(this.renderEmojis)}
-
- );
- };
+ attachSectionList = (c) => {
+ this.sectionList = c;
+ }
render() {
- const {intl, theme} = this.props;
- const {emojis, searchTerm} = this.state;
+ const {deviceWidth, isLandscape, theme} = this.props;
+ const {emojis, filteredEmojis, searchTerm} = this.state;
+ const {intl} = this.context;
const {formatMessage} = intl;
const styles = getStyleSheetFromTheme(theme);
+ let listComponent;
+ if (searchTerm) {
+ listComponent = (
+
+ );
+ } else {
+ listComponent = (
+
+ );
+ }
+
+ let keyboardOffset = 64;
+ if (Platform.OS === 'android') {
+ keyboardOffset = -200;
+ } else if (isLandscape) {
+ keyboardOffset = 51;
+ }
+
return (
-
+
-
+ {listComponent}
+ {!searchTerm &&
+
+
+ {this.renderSectionIcons()}
+
+
+ }
-
+
);
}
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
return {
+ bottomContent: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.3),
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between'
+ },
+ bottomContentWrapper: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: 35,
+ backgroundColor: 'white'
+ },
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
@@ -260,8 +433,34 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
emojiRight: {
marginRight: 0
},
+ flatList: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ alignSelf: 'stretch'
+ },
+ flatListEmoji: {
+ marginRight: 5
+ },
+ flatListEmojiName: {
+ fontSize: 13,
+ color: theme.centerChannelColor
+ },
+ flatListRow: {
+ height: 40,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ backgroundColor: theme.centerChannelBg,
+ borderTopWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderLeftWidth: 1,
+ borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderRightWidth: 1,
+ borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
+ },
listView: {
- backgroundColor: theme.centerChannelBg
+ backgroundColor: theme.centerChannelBg,
+ marginBottom: 35
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
@@ -270,16 +469,30 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
section: {
alignItems: 'center'
},
+ sectionIcon: {
+ color: changeOpacity(theme.centerChannelColor, 0.3)
+ },
+ sectionIconContainer: {
+ flex: 1,
+ height: 35,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ sectionIconHighlight: {
+ color: theme.centerChannelColor
+ },
sectionTitle: {
color: changeOpacity(theme.centerChannelColor, 0.2),
fontSize: 15,
- fontWeight: '700',
- paddingVertical: 5
+ fontWeight: '700'
+ },
+ sectionTitleContainer: {
+ height: SECTION_HEADER_HEIGHT,
+ justifyContent: 'center',
+ backgroundColor: theme.centerChannelBg
},
wrapper: {
flex: 1
}
};
});
-
-export default injectIntl(EmojiPicker);
diff --git a/app/components/emoji_picker/emoji_picker_row.js b/app/components/emoji_picker/emoji_picker_row.js
new file mode 100644
index 000000000..6974b55f2
--- /dev/null
+++ b/app/components/emoji_picker/emoji_picker_row.js
@@ -0,0 +1,95 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+import {
+ StyleSheet,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import Emoji from 'app/components/emoji';
+
+export default class EmojiPickerRow extends Component {
+ static propTypes = {
+ emojiGutter: PropTypes.number.isRequired,
+ emojiSize: PropTypes.number.isRequired,
+ items: PropTypes.array.isRequired,
+ onEmojiPress: PropTypes.func.isRequired
+ }
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.items.length !== nextProps.items.length;
+ }
+
+ renderEmojis = (emoji, index, emojis) => {
+ const {emojiGutter, emojiSize} = this.props;
+
+ const style = [
+ styles.emoji,
+ {
+ width: emojiSize,
+ height: emojiSize,
+ marginHorizontal: emojiGutter
+ }
+ ];
+ if (index === 0) {
+ style.push(styles.emojiLeft);
+ } else if (index === emojis.length - 1) {
+ style.push(styles.emojiRight);
+ }
+
+ if (!emoji) {
+ return (
+
+ );
+ }
+
+ return (
+ {
+ this.props.onEmojiPress(emoji.name);
+ }}
+ >
+
+
+ );
+ }
+
+ render() {
+ const {emojiGutter, items} = this.props;
+
+ return (
+
+ {items.map(this.renderEmojis)}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ columnStyle: {
+ alignSelf: 'stretch',
+ flexDirection: 'row',
+ justifyContent: 'space-between'
+ },
+ emoji: {
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ emojiLeft: {
+ marginLeft: 0
+ },
+ emojiRight: {
+ marginRight: 0
+ }
+});
\ No newline at end of file
diff --git a/app/components/emoji_picker/index.js b/app/components/emoji_picker/index.js
index 5dc97406a..11b82f03c 100644
--- a/app/components/emoji_picker/index.js
+++ b/app/components/emoji_picker/index.js
@@ -6,48 +6,62 @@ import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
-import {getDimensions} from 'app/selectors/device';
+import {getDimensions, isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
+import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
import EmojiPicker from './emoji_picker';
const categoryToI18n = {
activity: {
id: 'mobile.emoji_picker.activity',
- defaultMessage: 'ACTIVITY'
+ defaultMessage: 'ACTIVITY',
+ icon: 'futbol-o'
},
custom: {
id: 'mobile.emoji_picker.custom',
- defaultMessage: 'CUSTOM'
+ defaultMessage: 'CUSTOM',
+ icon: 'at'
},
flags: {
id: 'mobile.emoji_picker.flags',
- defaultMessage: 'FLAGS'
+ defaultMessage: 'FLAGS',
+ icon: 'flag-o'
},
foods: {
id: 'mobile.emoji_picker.foods',
- defaultMessage: 'FOODS'
+ defaultMessage: 'FOODS',
+ icon: 'cutlery'
},
nature: {
id: 'mobile.emoji_picker.nature',
- defaultMessage: 'NATURE'
+ defaultMessage: 'NATURE',
+ icon: 'leaf'
},
objects: {
id: 'mobile.emoji_picker.objects',
- defaultMessage: 'OBJECTS'
+ defaultMessage: 'OBJECTS',
+ icon: 'lightbulb-o'
},
people: {
id: 'mobile.emoji_picker.people',
- defaultMessage: 'PEOPLE'
+ defaultMessage: 'PEOPLE',
+ icon: 'smile-o'
},
places: {
id: 'mobile.emoji_picker.places',
- defaultMessage: 'PLACES'
+ defaultMessage: 'PLACES',
+ icon: 'plane'
+ },
+ recent: {
+ id: 'mobile.emoji_picker.recent',
+ defaultMessage: 'RECENTLY USED',
+ icon: 'clock-o'
},
symbols: {
id: 'mobile.emoji_picker.symbols',
- defaultMessage: 'SYMBOLS'
+ defaultMessage: 'SYMBOLS',
+ icon: 'heart-o'
}
};
@@ -61,28 +75,24 @@ function fillEmoji(indice) {
const getEmojisBySection = createSelector(
getCustomEmojisByName,
- (customEmojis) => {
+ (state) => state.views.recentEmojis,
+ (customEmojis, recentEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
+ const items = EmojiIndicesByCategory.get(category).map(fillEmoji);
+
const section = {
...categoryToI18n[category],
key: category,
- data: [{
- key: `${category}-emojis`,
- items: EmojiIndicesByCategory.get(category).map(fillEmoji)
- }]
+ data: items
};
return section;
});
- const customEmojiData = {
- key: 'custom-emojis',
- title: 'CUSTOM',
- items: []
- };
+ const customEmojiItems = [];
for (const [key] of customEmojis) {
- customEmojiData.items.push({
+ customEmojiItems.push({
name: key
});
}
@@ -90,20 +100,45 @@ const getEmojisBySection = createSelector(
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
- data: [customEmojiData]
+ data: customEmojiItems
});
+ if (recentEmojis.length) {
+ const items = recentEmojis.map((emoji) => ({name: emoji}));
+
+ emoticons.unshift({
+ ...categoryToI18n.recent,
+ key: 'recent',
+ data: items
+ });
+ }
+
+ return emoticons;
+ }
+);
+
+const getEmojisByName = createSelector(
+ getCustomEmojisByName,
+ (customEmojis) => {
+ const emoticons = [];
+ for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
+ emoticons.push(key);
+ }
+
return emoticons;
}
);
function mapStateToProps(state) {
- const emojis = getEmojisBySection(state);
+ const emojisBySection = getEmojisBySection(state);
+ const emojisByName = getEmojisByName(state);
const {deviceWidth} = getDimensions(state);
return {
- emojis,
+ emojisByName,
+ emojisBySection,
deviceWidth,
+ isLandscape: isLandscape(state),
theme: getTheme(state)
};
}
diff --git a/app/components/post/index.js b/app/components/post/index.js
index 63939620e..31fbaf4c8 100644
--- a/app/components/post/index.js
+++ b/app/components/post/index.js
@@ -4,13 +4,14 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
-import {addReaction, createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
+import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged} from 'mattermost-redux/utils/post_utils';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
+import {addReaction} from 'app/actions/views/emoji';
import Post from './post';
diff --git a/app/constants/view.js b/app/constants/view.js
index f44fa70b4..d5a5ee13f 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -56,7 +56,9 @@ const ViewTypes = keyMirror({
RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null,
- SET_LAST_UPGRADE_CHECK: null
+ SET_LAST_UPGRADE_CHECK: null,
+
+ ADD_RECENT_EMOJI: null
});
export default {
diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js
index dd158d526..bd0572d0e 100644
--- a/app/reducers/views/index.js
+++ b/app/reducers/views/index.js
@@ -8,6 +8,7 @@ import clientUpgrade from './client_upgrade';
import fetchCache from './fetch_cache';
import i18n from './i18n';
import login from './login';
+import recentEmojis from './recent_emojis';
import root from './root';
import search from './search';
import selectServer from './select_server';
@@ -20,6 +21,7 @@ export default combineReducers({
fetchCache,
i18n,
login,
+ recentEmojis,
root,
search,
selectServer,
diff --git a/app/reducers/views/recent_emojis.js b/app/reducers/views/recent_emojis.js
new file mode 100644
index 000000000..c40108375
--- /dev/null
+++ b/app/reducers/views/recent_emojis.js
@@ -0,0 +1,23 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {ViewTypes} from 'app/constants';
+
+export default function recentEmojis(state = [], action) {
+ switch (action.type) {
+ case ViewTypes.ADD_RECENT_EMOJI: {
+ const nextState = [...state];
+
+ const index = nextState.indexOf(action.emoji);
+ if (index !== -1) {
+ nextState.splice(index, 1);
+ }
+
+ nextState.unshift(action.emoji);
+
+ return nextState;
+ }
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/app/store/middleware.js b/app/store/middleware.js
index f0235f1ef..9f1655024 100644
--- a/app/store/middleware.js
+++ b/app/store/middleware.js
@@ -106,6 +106,11 @@ function resetStateForNewVersion(action) {
selectServer = payload.views.selectServer;
}
+ let recentEmojis = initialState.views.recentEmojis;
+ if (payload.views.recentEmojis) {
+ recentEmojis = payload.views.recentEmojis;
+ }
+
const nextState = {
app: {
build: DeviceInfo.getBuildNumber(),
@@ -131,7 +136,8 @@ function resetStateForNewVersion(action) {
thread: {
drafts: threadDrafts
},
- selectServer
+ selectServer,
+ recentEmojis
}
};
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index a3cae0bdf..f3a51354a 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1963,6 +1963,7 @@
"mobile.emoji_picker.objects": "OBJECTS",
"mobile.emoji_picker.people": "PEOPLE",
"mobile.emoji_picker.places": "PLACES",
+ "mobile.emoji_picker.recent": "RECENTLY USED",
"mobile.emoji_picker.symbols": "SYMBOLS",
"mobile.error_handler.button": "Relaunch",
"mobile.error_handler.description": "\nClick relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 000000000..c98b6e084
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index a1aa4d1a7..39cb9cbcf 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"react-native-notifications": "enahum/react-native-notifications.git",
"react-native-orientation": "enahum/react-native-orientation.git",
"react-native-passcode-status": "1.1.0",
+ "react-native-section-list-get-item-layout": "2.1.0",
"react-native-sentry": "0.15.1",
"react-native-slider": "0.11.0",
"react-native-status-bar-size": "0.3.2",
diff --git a/yarn.lock b/yarn.lock
index f061aecee..b8e1d03e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5140,6 +5140,10 @@ react-native-passcode-status@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/react-native-passcode-status/-/react-native-passcode-status-1.1.0.tgz#e5922d5f4d79bc09849438d620406e5ccd31168a"
+react-native-section-list-get-item-layout@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-native-section-list-get-item-layout/-/react-native-section-list-get-item-layout-2.1.0.tgz#ec5336fa769cfbe0c49ace50086a3cfdb2be2812"
+
react-native-sentry@0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/react-native-sentry/-/react-native-sentry-0.15.1.tgz#fd5e685faa8de8fc4f472703b57e023d45ee53aa"