Emoji picker (#185)

* Emoji picker working

* Gif support on Android
This commit is contained in:
Diego Mello 2018-01-16 16:48:05 -02:00 committed by Guilherme Gazzo
parent a6b525b09e
commit 0636fd0266
29 changed files with 725 additions and 122 deletions

View File

@ -158,6 +158,8 @@ dependencies {
compile "com.android.support:appcompat-v7:23.0.1"
compile 'com.android.support:customtabs:23.0.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile 'com.facebook.fresco:fresco:1.7.1'
compile 'com.facebook.fresco:animated-gif:1.7.1'
}
// Run this once to be able to run the application with BUCK

View File

@ -81,3 +81,4 @@ export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST'
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const KEYBOARD = createRequestTypes('KEYBOARD', ['OPEN', 'CLOSE']);

View File

@ -39,6 +39,13 @@ export function setAllPermissions(permissions) {
};
}
export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
payload: emojis
};
}
export function login() {
return {
type: 'LOGIN'

13
app/actions/keyboard.js Normal file
View File

@ -0,0 +1,13 @@
import * as types from './actionsTypes';
export function setKeyboardOpen() {
return {
type: types.KEYBOARD.OPEN
};
}
export function setKeyboardClosed() {
return {
type: types.KEYBOARD.CLOSE
};
}

View File

@ -1,4 +1,5 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS';
export const SET_ALL_PERMISSIONS = 'SET_ALL_PERMISSIONS';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ADD_SETTINGS = 'ADD_SETTINGS';

View File

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CachedImage } from 'react-native-img-cache';
import { connect } from 'react-redux';
@connect(state => ({
baseUrl: state.settings.Site_Url
}))
export default class extends React.PureComponent {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
emoji: PropTypes.object.isRequired,
style: PropTypes.object
}
render() {
const { baseUrl, emoji, style } = this.props;
return (
<CachedImage
style={style}
source={{ uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content) }.${ emoji.extension }` }}
/>
);
}
}

View File

@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, TouchableOpacity, StyleSheet } from 'react-native';
import styles from './styles';
import CustomEmoji from '../../CustomEmoji';
export default class extends React.PureComponent {
static propTypes = {
emojis: PropTypes.any,
finishedLoading: PropTypes.func,
onEmojiSelected: PropTypes.func
};
componentDidMount() {
this.props.finishedLoading();
}
renderEmoji = (emoji) => {
if (emoji.isCustom) {
const style = StyleSheet.flatten(styles.customCategoryEmoji);
return <CustomEmoji style={style} emoji={emoji} />;
}
return (
<Text style={styles.categoryEmoji}>
{emoji}
</Text>
);
}
render() {
const { emojis } = this.props;
return (
<View>
<View style={styles.categoryInner}>
{emojis.map(emoji =>
(
<TouchableOpacity
activeOpacity={0.7}
key={emoji.isCustom ? emoji.content : emoji}
onPress={() => this.props.onEmojiSelected(emoji)}
>
{this.renderEmoji(emoji)}
</TouchableOpacity>
))}
</View>
</View>
);
}
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles';
export default class extends React.PureComponent {
static propTypes = {
goToPage: PropTypes.func,
activeTab: PropTypes.number,
tabs: PropTypes.array
}
render() {
return (
<View style={styles.tabsContainer}>
{this.props.tabs.map((tab, i) => (
<TouchableOpacity activeOpacity={0.7} key={tab} onPress={() => this.props.goToPage(i)} style={styles.tab}>
<Text style={styles.tabEmoji}>{tab}</Text>
{this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -0,0 +1,44 @@
const list = ['Frequently Used', 'Custom', 'Smileys & People', 'Animals & Nature', 'Food & Drink', 'Activities', 'Travel & Places', 'Objects', 'Symbols', 'Flags'];
const tabs = [
{
tabLabel: '🕒',
category: list[0]
},
{
tabLabel: '🚀',
category: list[1]
},
{
tabLabel: '😃',
category: list[2]
},
{
tabLabel: '🐶',
category: list[3]
},
{
tabLabel: '🍔',
category: list[4]
},
{
tabLabel: '⚽',
category: list[5]
},
{
tabLabel: '🚌',
category: list[6]
},
{
tabLabel: '💡',
category: list[7]
},
{
tabLabel: '💛',
category: list[8]
},
{
tabLabel: '🏁',
category: list[9]
}
];
export default { list, tabs };

View File

@ -0,0 +1,143 @@
import 'string.fromcodepoint';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { ScrollView, View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import emojiDatasource from 'emoji-datasource/emoji.json';
import _ from 'lodash';
import { groupBy, orderBy } from 'lodash/collection';
import { mapValues } from 'lodash/object';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import styles from './styles';
import categories from './categories';
import scrollPersistTaps from '../../../utils/scrollPersistTaps';
import database from '../../../lib/realm';
const charFromUtf16 = utf16 => String.fromCodePoint(...utf16.split('-').map(u => `0x${ u }`));
const charFromEmojiObj = obj => charFromUtf16(obj.unified);
const filteredEmojis = emojiDatasource.filter(e => parseFloat(e.added_in) < 10.0);
const groupedAndSorted = groupBy(orderBy(filteredEmojis, 'sort_order'), 'category');
const emojisByCategory = mapValues(groupedAndSorted, group => group.map(charFromEmojiObj));
export default class extends PureComponent {
static propTypes = {
onEmojiSelected: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
categories: categories.list.slice(0, 1),
frequentlyUsed: [],
customEmojis: []
};
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
this.customEmojis = database.objects('customEmojis');
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
}
componentWillMount() {
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
this.customEmojis.addListener(this.updateCustomEmojis);
this.updateFrequentlyUsed();
this.updateCustomEmojis();
}
componentWillUnmount() {
clearTimeout(this._timeout);
}
onEmojiSelected(emoji) {
if (emoji.isCustom) {
const count = this._getFrequentlyUsedCount(emoji.content);
this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, count, isCustom: true
});
this.props.onEmojiSelected(`:${ emoji.content }:`);
} else {
const content = emoji.codePointAt(0).toString();
const count = this._getFrequentlyUsedCount(content);
this._addFrequentlyUsed({ content, count, isCustom: false });
this.props.onEmojiSelected(emoji);
}
}
_addFrequentlyUsed = (emoji) => {
database.write(() => {
database.create('frequentlyUsedEmoji', emoji, true);
});
}
_getFrequentlyUsedCount = (content) => {
const emojiRow = this.frequentlyUsed.filtered('content == $0', content);
return emojiRow.length ? emojiRow[0].count + 1 : 1;
}
updateFrequentlyUsed() {
const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) {
return item;
}
return String.fromCodePoint(item.content);
});
this.setState({ frequentlyUsed });
}
updateCustomEmojis() {
const customEmojis = _.map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis });
}
loadNextCategory() {
if (this.state.categories.length < categories.list.length) {
this.setState({ categories: categories.list.slice(0, this.state.categories.length + 1) });
}
}
renderCategory(category, i) {
let emojis = [];
if (i === 0) {
emojis = this.state.frequentlyUsed;
} else if (i === 1) {
emojis = this.state.customEmojis;
} else {
emojis = emojisByCategory[category];
}
return (
<View style={styles.categoryContainer}>
<EmojiCategory
key={category}
emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
finishedLoading={() => { this._timeout = setTimeout(this.loadNextCategory.bind(this), 100); }}
/>
</View>
);
}
render() {
const scrollProps = {
keyboardShouldPersistTaps: 'always'
};
return (
<View style={styles.container}>
<ScrollableTabView
renderTabBar={() => <TabBar />}
contentProps={scrollProps}
>
{
_.map(categories.tabs, (tab, i) => (
<ScrollView
key={i}
tabLabel={tab.tabLabel}
{...scrollPersistTaps}
>
{this.renderCategory(tab.category, i)}
</ScrollView>
))
}
</ScrollableTabView>
</View>
);
}
}

View File

@ -0,0 +1,62 @@
import { StyleSheet, Dimensions, Platform } from 'react-native';
const { width } = Dimensions.get('window');
const EMOJI_SIZE = width / (Platform.OS === 'ios' ? 8 : 9);
export default StyleSheet.create({
container: {
flex: 1
},
tabsContainer: {
height: 45,
flexDirection: 'row',
paddingTop: 5
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 10
},
tabEmoji: {
fontSize: 20,
color: 'black'
},
activeTabLine: {
position: 'absolute',
left: 0,
right: 0,
height: 2,
backgroundColor: '#007aff',
bottom: 0
},
tabLine: {
position: 'absolute',
left: 0,
right: 0,
height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0
},
categoryContainer: {
flex: 1,
alignItems: 'flex-start'
},
categoryInner: {
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'center'
},
categoryEmoji: {
fontSize: EMOJI_SIZE - 14,
color: 'black',
height: EMOJI_SIZE,
width: EMOJI_SIZE,
textAlign: 'center'
},
customCategoryEmoji: {
height: EMOJI_SIZE - 8,
width: EMOJI_SIZE - 8,
margin: 4
}
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TextInput, SafeAreaView, Platform, FlatList, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { View, TextInput, SafeAreaView, Platform, FlatList, Text, TouchableOpacity, Keyboard } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker';
import { connect } from 'react-redux';
import { userTyping } from '../../actions/room';
@ -12,6 +12,8 @@ import MyIcon from '../icons';
import database from '../../lib/realm';
import Avatar from '../Avatar';
import AnimatedContainer from './AnimatedContainer';
import EmojiPicker from './EmojiPicker';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
const MENTIONS_TRACKING_TYPE_USERS = '@';
@ -23,7 +25,8 @@ const onlyUnique = function onlyUnique(value, index, self) {
room: state.room,
message: state.messages.message,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
isKeyboardOpen: state.keyboard.isOpen
}), dispatch => ({
editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)),
@ -40,7 +43,8 @@ export default class MessageBox extends React.PureComponent {
message: PropTypes.object,
editing: PropTypes.bool,
typing: PropTypes.func,
clearInput: PropTypes.func
clearInput: PropTypes.func,
isKeyboardOpen: PropTypes.bool
}
constructor(props) {
@ -50,7 +54,8 @@ export default class MessageBox extends React.PureComponent {
messageboxHeight: 0,
text: '',
mentions: [],
showAnimatedContainer: false
showMentionsContainer: false,
showEmojiContainer: false
};
this.users = [];
this.rooms = [];
@ -61,6 +66,8 @@ export default class MessageBox extends React.PureComponent {
this.component.focus();
} else if (!nextProps.message) {
this.setState({ text: '' });
} else if (this.props.isKeyboardOpen !== nextProps.isKeyboardOpen && nextProps.isKeyboardOpen) {
this.closeEmoji();
}
}
@ -95,24 +102,24 @@ export default class MessageBox extends React.PureComponent {
if (editing) {
return (<Icon
style={styles.actionButtons}
name='ios-close'
name='close'
accessibilityLabel='Cancel editing'
accessibilityTraits='button'
onPress={() => this.editCancel()}
/>);
}
return !this.state.emoji ? (<Icon
return !this.state.showEmojiContainer ? (<Icon
style={styles.actionButtons}
onPress={() => this.openEmoji()}
accessibilityLabel='Open emoji selector'
accessibilityTraits='button'
name='md-happy'
name='mood'
/>) : (<Icon
onPress={() => this.openEmoji()}
onPress={() => this.closeEmoji()}
style={styles.actionButtons}
accessibilityLabel='Close emoji selector'
accessibilityTraits='button'
name='md-sad'
name='keyboard'
/>);
}
get rightButtons() {
@ -176,11 +183,16 @@ export default class MessageBox extends React.PureComponent {
this.props.editCancel();
this.setState({ text: '' });
}
openEmoji() {
this.setState({ emoji: !this.state.emoji });
async openEmoji() {
await this.setState({ showEmojiContainer: !this.state.showEmojiContainer });
Keyboard.dismiss();
}
closeEmoji() {
this.setState({ showEmojiContainer: false });
}
submit(message) {
this.setState({ text: '' });
this.closeEmoji();
this.stopTrackingMention();
requestAnimationFrame(() => {
this.props.typing(false);
@ -279,7 +291,7 @@ export default class MessageBox extends React.PureComponent {
stopTrackingMention() {
this.setState({
showAnimatedContainer: false,
showMentionsContainer: false,
mentions: []
});
this.users = [];
@ -289,7 +301,7 @@ export default class MessageBox extends React.PureComponent {
identifyMentionKeyword(keyword, type) {
this.updateMentions(keyword, type);
this.setState({
showAnimatedContainer: true
showMentionsContainer: true
});
}
@ -317,6 +329,22 @@ export default class MessageBox extends React.PureComponent {
this.component.focus();
requestAnimationFrame(() => this.stopTrackingMention());
}
_onEmojiSelected(emoji) {
const { text } = this.state;
let newText = '';
// if messagebox has an active cursor
if (this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {
// if messagebox doesn't have a cursor, just append selected emoji
newText = `${ text }${ emoji }`;
}
this.component.setNativeProps({ text: newText });
this.setState({ text: newText });
}
renderMentionItem = item => (
<TouchableOpacity
style={styles.mentionItem}
@ -331,6 +359,15 @@ export default class MessageBox extends React.PureComponent {
<Text>{item.username || item.name }</Text>
</TouchableOpacity>
)
renderEmoji() {
const emojiContainer = (
<View style={styles.emojiContainer}>
<EmojiPicker onEmojiSelected={emoji => this._onEmojiSelected(emoji)} />
</View>
);
const { showEmojiContainer, messageboxHeight } = this.state;
return <AnimatedContainer visible={showEmojiContainer} subview={emojiContainer} messageboxHeight={messageboxHeight} />;
}
renderMentions() {
const usersList = (
<FlatList
@ -338,12 +375,11 @@ export default class MessageBox extends React.PureComponent {
data={this.state.mentions}
renderItem={({ item }) => this.renderMentionItem(item)}
keyExtractor={item => item._id}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
{...scrollPersistTaps}
/>
);
const { showAnimatedContainer, messageboxHeight } = this.state;
return <AnimatedContainer visible={showAnimatedContainer} subview={usersList} messageboxHeight={messageboxHeight} />;
const { showMentionsContainer, messageboxHeight } = this.state;
return <AnimatedContainer visible={showMentionsContainer} subview={usersList} messageboxHeight={messageboxHeight} />;
}
render() {
const { height } = this.state;
@ -374,6 +410,7 @@ export default class MessageBox extends React.PureComponent {
</View>
</SafeAreaView>
{this.renderMentions()}
{this.renderEmoji()}
</View>
);
}

View File

@ -79,5 +79,11 @@ export default StyleSheet.create({
borderBottomColor: '#ECECEC',
flexDirection: 'row',
alignItems: 'center'
},
emojiContainer: {
height: 200,
borderTopColor: '#ECECEC',
borderTopWidth: 1,
backgroundColor: '#fff'
}
});

View File

@ -1,110 +1,127 @@
import React from 'react';
import { Text, Platform } from 'react-native';
import { Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line
import SimpleMarkdown from 'simple-markdown';
import { emojify } from 'react-emojione';
const codeStyle = {
...Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
}),
backgroundColor: '#f8f8f8',
borderColor: '#cccccc',
borderWidth: 1,
borderRadius: 5,
padding: 5
};
import styles from './styles';
import CustomEmoji from '../CustomEmoji';
const BlockCode = ({ node, state }) => (
<Text
key={state.key}
style={codeStyle}
style={styles.codeStyle}
>
{node.content}
</Text>
);
const mentionStyle = { color: '#13679a' };
const rules = {
username: {
order: -1,
match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Username')}
>
{node.content}
</Text>
)
}
})
},
heading: {
order: -2,
match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Room')}
>
{node.content}
</Text>
)
}
})
},
fence: {
order: -5,
match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/),
parse: capture => ({
lang: capture[2] || undefined,
content: capture[3]
}),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
blockCode: {
order: -6,
match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/),
parse: capture => ({ content: capture[2] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
}
};
const Markdown = ({ msg }) => {
const Markdown = ({ msg, customEmojis }) => {
if (!msg) {
return null;
}
msg = emojify(msg, { output: 'unicode' });
const rules = {
username: {
order: -1,
match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Username')}
>
{node.content}
</Text>
)
}
})
},
heading: {
order: -2,
match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Room')}
>
{node.content}
</Text>
)
}
})
},
fence: {
order: -3,
match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/),
parse: capture => ({
lang: capture[2] || undefined,
content: capture[3]
}),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
blockCode: {
order: -4,
match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/),
parse: capture => ({ content: capture[2] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
customEmoji: {
order: -5,
match: SimpleMarkdown.inlineRegex(/^:([0-9a-zA-Z-_.]+):/),
parse: capture => ({ content: capture }),
react: (node, output, state) => {
const element = {
type: 'custom',
key: state.key,
props: {
children: <Text key={state.key}>{node.content[0]}</Text>
}
};
const content = node.content[1];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
const style = StyleSheet.flatten(styles.customEmoji);
element.props.children = (
<CustomEmoji key={state.key} style={style} emoji={emoji} />
);
}
return element;
}
}
};
const codeStyle = StyleSheet.flatten(styles.codeStyle);
return (
<EasyMarkdown
style={{ marginBottom: 0 }}
@ -116,7 +133,8 @@ const Markdown = ({ msg }) => {
};
Markdown.propTypes = {
msg: PropTypes.string.isRequired
msg: PropTypes.string.isRequired,
customEmojis: PropTypes.object
};
BlockCode.propTypes = {

View File

@ -23,7 +23,8 @@ const flex = { flexDirection: 'row', flex: 1 };
@connect(state => ({
message: state.messages.message,
editing: state.messages.editing
editing: state.messages.editing,
customEmojis: state.customEmojis
}), dispatch => ({
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage))
@ -38,7 +39,8 @@ export default class Message extends React.Component {
editing: PropTypes.bool,
actionsShow: PropTypes.func,
errorActionsShow: PropTypes.func,
animate: PropTypes.bool
animate: PropTypes.bool,
customEmojis: PropTypes.object
}
componentWillMount() {
@ -135,7 +137,8 @@ export default class Message extends React.Component {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{this.getInfoMessage()}</Text>;
}
return <Markdown msg={this.props.item.msg} />;
const { item, customEmojis, baseUrl } = this.props;
return <Markdown msg={item.msg} customEmojis={customEmojis} baseUrl={baseUrl} />;
}
renderUrl() {

View File

@ -1,4 +1,4 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Platform } from 'react-native';
export default StyleSheet.create({
content: {
@ -18,5 +18,20 @@ export default StyleSheet.create({
},
editing: {
backgroundColor: '#fff5df'
},
customEmoji: {
width: 16,
height: 16
},
codeStyle: {
...Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
}),
backgroundColor: '#f8f8f8',
borderColor: '#cccccc',
borderWidth: 1,
borderRadius: 5,
padding: 5
}
});

View File

@ -175,6 +175,37 @@ const messagesSchema = {
editedBy: 'messagesEditedBy'
}
};
const frequentlyUsedEmojiSchema = {
name: 'frequentlyUsedEmoji',
primaryKey: 'content',
properties: {
content: { type: 'string', optional: true },
extension: { type: 'string', optional: true },
isCustom: 'bool',
count: 'int'
}
};
const customEmojiAliasesSchema = {
name: 'customEmojiAliases',
properties: {
value: 'string'
}
};
const customEmojisSchema = {
name: 'customEmojis',
primaryKey: '_id',
properties: {
_id: 'string',
name: 'string',
aliases: { type: 'list', objectType: 'customEmojiAliases' },
extension: 'string',
_updatedAt: { type: 'date', optional: true }
}
};
const schema = [
settingsSchema,
subscriptionSchema,
@ -187,7 +218,10 @@ const schema = [
messagesEditedBySchema,
permissionsSchema,
permissionsRolesSchema,
url
url,
frequentlyUsedEmojiSchema,
customEmojiAliasesSchema,
customEmojisSchema
];
class DB {
databases = {

View File

@ -83,6 +83,7 @@ const RocketChat = {
this.ddp.on('connected', () => {
RocketChat.getSettings();
RocketChat.getPermissions();
RocketChat.getCustomEmoji();
});
this.ddp.on('error', (err) => {
@ -513,6 +514,29 @@ const RocketChat = {
});
return permissions;
},
async getCustomEmoji() {
const temp = database.objects('customEmojis').sorted('_updatedAt', true)[0];
let emojis = await call('listEmojiCustom');
emojis = emojis.filter(emoji => !temp || emoji._updatedAt > temp._updatedAt);
emojis = RocketChat._prepareEmojis(emojis);
database.write(() => {
emojis.forEach(emoji => database.create('customEmojis', emoji, true));
});
reduxStore.dispatch(actions.setCustomEmojis(RocketChat.parseEmojis(emojis)));
},
parseEmojis: emojis => emojis.reduce((ret, item) => {
ret[item.name] = item.extension;
item.aliases.forEach((alias) => {
ret[alias.value] = item.extension;
});
return ret;
}, {}),
_prepareEmojis(emojis) {
emojis.forEach((emoji) => {
emoji.aliases = emoji.aliases.map(alias => ({ value: alias }));
});
return emojis;
},
deleteMessage(message) {
return call('deleteMessage', { _id: message._id });
},

View File

@ -2,7 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { ViewPropTypes } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { connect } from 'react-redux';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import { setKeyboardOpen, setKeyboardClosed } from '../actions/keyboard';
@connect(null, dispatch => ({
setKeyboardOpen: () => dispatch(setKeyboardOpen()),
setKeyboardClosed: () => dispatch(setKeyboardClosed())
}))
export default class KeyboardView extends React.PureComponent {
static propTypes = {
style: ViewPropTypes.style,
@ -12,20 +19,23 @@ export default class KeyboardView extends React.PureComponent {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
]),
setKeyboardOpen: PropTypes.func,
setKeyboardClosed: PropTypes.func
}
render() {
return (
<KeyboardAwareScrollView
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
{...scrollPersistTaps}
style={this.props.style}
contentContainerStyle={this.props.contentContainerStyle}
scrollEnabled={this.props.scrollEnabled}
alwaysBounceVertical={false}
extraHeight={this.props.keyboardVerticalOffset}
behavior='position'
onKeyboardWillShow={() => this.props.setKeyboardOpen()}
onKeyboardWillHide={() => this.props.setKeyboardClosed()}
>
{this.props.children}
</KeyboardAwareScrollView>

View File

@ -0,0 +1,17 @@
import * as types from '../constants/types';
const initialState = {
customEmojis: {}
};
export default function customEmojis(state = initialState.customEmojis, action) {
if (action.type === types.SET_CUSTOM_EMOJIS) {
return {
...state,
...action.payload
};
}
return state;
}

View File

@ -10,8 +10,23 @@ import navigator from './navigator';
import createChannel from './createChannel';
import app from './app';
import permissions from './permissions';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
import keyboard from './keyboard';
export default combineReducers({
settings, login, meteor, messages, server, navigator, createChannel, app, room, rooms, permissions, activeUsers
settings,
login,
meteor,
messages,
server,
navigator,
createChannel,
app,
room,
rooms,
permissions,
customEmojis,
activeUsers,
keyboard
});

22
app/reducers/keyboard.js Normal file
View File

@ -0,0 +1,22 @@
import * as types from '../actions/actionsTypes';
const initialState = {
isOpen: false
};
export default function messages(state = initialState, action) {
switch (action.type) {
case types.KEYBOARD.OPEN:
return {
...state,
isOpen: true
};
case types.KEYBOARD.CLOSE:
return {
...state,
isOpen: false
};
default:
return state;
}
}

View File

@ -21,6 +21,8 @@ const restore = function* restore() {
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
const permissions = database.objects('permissions');
yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length))));
const emojis = database.objects('customEmojis');
yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
}
yield put(actions.appReady({}));
} catch (e) {

View File

@ -0,0 +1,4 @@
export default {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'interactive'
};

View File

@ -8,6 +8,7 @@ import * as loginActions from '../actions/login';
import KeyboardView from '../presentation/KeyboardView';
import styles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import { showToast } from '../utils/info';
class LoginView extends React.Component {
@ -87,8 +88,7 @@ class LoginView extends React.Component {
>
<ScrollView
style={styles.loginView}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
{...scrollPersistTaps}
>
<SafeAreaView>
<View style={styles.formContainer}>

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server';
import KeyboardView from '../presentation/KeyboardView';
import styles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps';
@connect(state => ({
validInstance: !state.server.failure && !state.server.connecting,
@ -115,8 +116,7 @@ export default class NewServerView extends React.Component {
>
<ScrollView
style={styles.loginView}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
{...scrollPersistTaps}
>
<TextInput
ref={ref => this.inputElement = ref}

View File

@ -23,6 +23,7 @@ import Banner from './banner';
import styles from './styles';
import debounce from '../../utils/debounce';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
@ -187,8 +188,7 @@ export default class RoomView extends React.Component {
dataSource={this.state.dataSource}
renderRow={item => this.renderItem(item)}
initialListSize={10}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
{...scrollPersistTaps}
/>
</SafeAreaView>
{this.renderFooter()}

20
package-lock.json generated
View File

@ -4610,6 +4610,11 @@
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-1.1.1.tgz",
"integrity": "sha512-vkcJJZEb7JXDY883Nx1Lkmb6noM3j1SfSt8L9tVFhZPnPQiFq+Nkd5evc77+tRVS4ChTUSr34voThsglI/ja/A=="
},
"emoji-datasource": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/emoji-datasource/-/emoji-datasource-4.0.3.tgz",
"integrity": "sha1-1gDnDwVoMnyyjPp79B1T88SGeQE="
},
"emoji-regex": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
@ -12600,6 +12605,16 @@
"resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.1.tgz",
"integrity": "sha1-DiPbMC0Du0o/KNwHLcryqaEXjtg="
},
"react-native-scrollable-tab-view": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/react-native-scrollable-tab-view/-/react-native-scrollable-tab-view-0.8.0.tgz",
"integrity": "sha512-8Q7v4f1WyV5cKqvV3QHxnLFRWV8gi24JW2T+Cfx++b3ctHxtJCkGg5Zs15ufYMxaN4W68iDkJrftVVAq0tqb8w==",
"requires": {
"create-react-class": "15.6.2",
"prop-types": "15.6.0",
"react-timer-mixin": "0.13.3"
}
},
"react-native-slider": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
@ -14425,6 +14440,11 @@
"strip-ansi": "4.0.0"
}
},
"string.fromcodepoint": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz",
"integrity": "sha1-jZeDM8C8klOPUPOD5IiPPlYZ1lM="
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",

View File

@ -29,6 +29,8 @@
"babel-polyfill": "^6.26.0",
"deep-equal": "^1.0.1",
"ejson": "^2.1.2",
"emoji-datasource": "^4.0.3",
"lodash": "^4.17.4",
"moment": "^2.20.1",
"prop-types": "^15.6.0",
"react": "^16.2.0",
@ -48,6 +50,7 @@
"react-native-modal": "^4.1.1",
"react-native-optimized-flatlist": "^1.0.3",
"react-native-push-notification": "^3.0.1",
"react-native-scrollable-tab-view": "^0.8.0",
"react-native-slider": "^0.11.0",
"react-native-splash-screen": "^3.0.6",
"react-native-svg": "^6.0.0",
@ -68,6 +71,7 @@
"remote-redux-devtools": "^0.5.12",
"simple-markdown": "^0.3.1",
"snyk": "^1.61.1",
"string.fromcodepoint": "^0.2.1",
"strip-ansi": "^4.0.0"
},
"devDependencies": {