parent
a6b525b09e
commit
0636fd0266
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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 }` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 };
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -79,5 +79,11 @@ export default StyleSheet.create({
|
|||
borderBottomColor: '#ECECEC',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
height: 200,
|
||||
borderTopColor: '#ECECEC',
|
||||
borderTopWidth: 1,
|
||||
backgroundColor: '#fff'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
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 = {
|
||||
|
||||
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-_.]+/),
|
||||
|
@ -68,7 +65,7 @@ const rules = {
|
|||
})
|
||||
},
|
||||
fence: {
|
||||
order: -5,
|
||||
order: -3,
|
||||
match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/),
|
||||
parse: capture => ({
|
||||
lang: capture[2] || undefined,
|
||||
|
@ -85,7 +82,7 @@ const rules = {
|
|||
})
|
||||
},
|
||||
blockCode: {
|
||||
order: -6,
|
||||
order: -4,
|
||||
match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/),
|
||||
parse: capture => ({ content: capture[2] }),
|
||||
react: (node, output, state) => ({
|
||||
|
@ -97,14 +94,34 @@ const rules = {
|
|||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
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 Markdown = ({ msg }) => {
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
msg = emojify(msg, { output: 'unicode' });
|
||||
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 = {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
keyboardShouldPersistTaps: 'always',
|
||||
keyboardDismissMode: 'interactive'
|
||||
};
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue