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:
parent
d1318d4631
commit
e345ee1e7b
15 changed files with 563 additions and 144 deletions
2
Makefile
2
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
95
app/components/emoji_picker/emoji_picker_row.js
Normal file
95
app/components/emoji_picker/emoji_picker_row.js
Normal 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
|
||||
}
|
||||
});
|
||||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
23
app/reducers/views/recent_emojis.js
Normal file
23
app/reducers/views/recent_emojis.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
9
jsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue