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"