Add recently used section (#1136)

* Add recently used section and section icons

* Improve section scrolling indication

* Modify Makefile and use onMomentumScrollEnd

* Make sure top section maintains highlight when scrolled to top

* Sort emojis by startsWith then includes when filtering
This commit is contained in:
Chris Duarte 2017-11-21 07:20:09 -08:00 committed by Harrison Healey
parent d1318d4631
commit e345ee1e7b
15 changed files with 563 additions and 144 deletions

View file

@ -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

View file

@ -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
};
}

View file

@ -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);

View file

@ -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 (
<EmojiPickerRow
key={item.key}
emojiGutter={EMOJI_GUTTER}
emojiSize={EMOJI_SIZE}
items={item.items}
onEmojiPress={this.props.onEmojiPress}
/>
);
}
flatListKeyExtractor = (item) => item;
flatListRenderItem = ({item}) => {
const style = getStyleSheetFromTheme(this.props.theme);
return (
<TouchableOpacity
onPress={() => this.props.onEmojiPress(item)}
style={style.flatListRow}
>
<View style={style.flatListEmoji}>
<Emoji
emojiName={item}
size={20}
/>
</View>
<Text style={style.flatListEmojiName}>{`:${item}:`}</Text>
</TouchableOpacity>
);
};
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 (
<View key={section.title}>
<View
style={styles.sectionTitleContainer}
key={section.title}
>
<FormattedText
style={styles.sectionTitle}
id={section.id}
@ -129,75 +273,89 @@ class EmojiPicker extends PureComponent {
/>
</View>
);
};
}
renderEmojis = (emojis, index) => {
renderSectionIcons = () => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
<View
key={index}
style={styles.columnStyle}
>
{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 (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={EMOJI_SIZE}
/>
</TouchableOpacity>
);
})}
</View>
);
};
return (
<TouchableOpacity
key={section.key}
onPress={onPress}
style={styles.sectionIconContainer}
>
<FontAwesomeIcon
name={section.icon}
size={17}
style={[styles.sectionIcon, (index === this.state.currentSectionIndex && styles.sectionIconHighlight)]}
/>
</TouchableOpacity>
);
});
}
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 (
<View style={styles.section}>
{slices.map(this.renderEmojis)}
</View>
);
};
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 = (
<FlatList
keyboardShouldPersistTaps='always'
style={styles.flatList}
data={filteredEmojis}
keyExtractor={this.flatListKeyExtractor}
renderItem={this.flatListRenderItem}
pageSize={10}
initialListSize={10}
/>
);
} else {
listComponent = (
<SectionList
ref={this.attachSectionList}
showsVerticalScrollIndicator={false}
style={[styles.listView, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
keyboardShouldPersistTaps='always'
getItemLayout={this.sectionListGetItemLayout}
removeClippedSubviews={true}
onScroll={this.onScroll}
onMomentumScrollEnd={this.onMomentumScrollEnd}
pageSize={30}
/>
);
}
let keyboardOffset = 64;
if (Platform.OS === 'android') {
keyboardOffset = -200;
} else if (isLandscape) {
keyboardOffset = 51;
}
return (
<View style={styles.wrapper}>
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={keyboardOffset}
>
<View style={styles.searchBar}>
<SearchBar
ref='search_bar'
@ -220,22 +378,37 @@ class EmojiPicker extends PureComponent {
/>
</View>
<View style={styles.container}>
<SectionList
showsVerticalScrollIndicator={false}
style={styles.listView}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
removeClippedSubviews={true}
/>
{listComponent}
{!searchTerm &&
<View style={styles.bottomContentWrapper}>
<View style={styles.bottomContent}>
{this.renderSectionIcons()}
</View>
</View>
}
</View>
</View>
</KeyboardAvoidingView>
);
}
}
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);

View file

@ -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 (
<View
key={index}
style={style}
/>
);
}
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={emojiSize}
/>
</TouchableOpacity>
);
}
render() {
const {emojiGutter, items} = this.props;
return (
<View style={[styles.columnStyle, {marginVertical: emojiGutter}]}>
{items.map(this.renderEmojis)}
</View>
);
}
}
const styles = StyleSheet.create({
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
justifyContent: 'space-between'
},
emoji: {
alignItems: 'center',
justifyContent: 'center'
},
emojiLeft: {
marginLeft: 0
},
emojiRight: {
marginRight: 0
}
});

View file

@ -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)
};
}

View file

@ -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';

View file

@ -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 {

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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
}
};

View file

@ -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",

9
jsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
]
}

View file

@ -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",

View file

@ -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"