Reactions (#214)
* * Tracking emoji * Fixed users/rooms regex tracking * Autocomplete emoji * Toggle reaction * 'User have reacted' style * Show who have reacted onLongPress * Vibration onLongPress
This commit is contained in:
parent
153cfecab5
commit
9ea5c1b765
|
@ -3,6 +3,7 @@
|
|||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:colorEdgeEffect">#aaaaaa</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
# org.gradle.parallel=true
|
||||
|
||||
android.useDeprecatedNdk=true
|
||||
# VERSIONCODE=999999999
|
||||
|
|
|
@ -55,7 +55,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [
|
|||
'TOGGLE_PIN_SUCCESS',
|
||||
'TOGGLE_PIN_FAILURE',
|
||||
'SET_INPUT',
|
||||
'CLEAR_INPUT'
|
||||
'CLEAR_INPUT',
|
||||
'TOGGLE_REACTION_PICKER'
|
||||
]);
|
||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [
|
||||
...defaultTypes,
|
||||
|
|
|
@ -176,3 +176,10 @@ export function clearInput() {
|
|||
type: types.MESSAGES.CLEAR_INPUT
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleReactionPicker(message) {
|
||||
return {
|
||||
type: types.MESSAGES.TOGGLE_REACTION_PICKER,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,19 +6,21 @@ import { connect } from 'react-redux';
|
|||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url
|
||||
}))
|
||||
export default class extends React.PureComponent {
|
||||
export default class extends React.Component {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
emoji: PropTypes.object.isRequired,
|
||||
style: PropTypes.object
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
const { baseUrl, emoji, style } = this.props;
|
||||
return (
|
||||
<CachedImage
|
||||
style={style}
|
||||
source={{ uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content) }.${ emoji.extension }` }}
|
||||
source={{ uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }` }}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View, TouchableOpacity, Platform } from 'react-native';
|
||||
import { emojify } from 'react-emojione';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
import styles from './styles';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
|
||||
|
||||
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9;
|
||||
|
||||
const renderEmoji = (emoji, size) => {
|
||||
if (emoji.isCustom) {
|
||||
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} />;
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
|
||||
{emojify(`:${ emoji }:`, { output: 'unicode' })}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
@responsive
|
||||
export default class EmojiCategory extends React.Component {
|
||||
static propTypes = {
|
||||
emojis: PropTypes.any,
|
||||
window: PropTypes.any,
|
||||
onEmojiSelected: PropTypes.func,
|
||||
emojisPerRow: PropTypes.number,
|
||||
width: PropTypes.number
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { width, height } = this.props.window;
|
||||
|
||||
this.size = Math.min(this.props.width || width, height) / (this.props.emojisPerRow || emojisPerRow);
|
||||
this.emojis = [];
|
||||
}
|
||||
componentWillMount() {
|
||||
this.emojis = this.props.emojis.slice(0, emojisPerRow * 3).map(item => this.renderItem(item, this.size));
|
||||
}
|
||||
async componentDidMount() {
|
||||
const array = this.props.emojis;
|
||||
const temparray = [];
|
||||
let i;
|
||||
let j;
|
||||
const chunk = emojisPerRow * 3;
|
||||
for (i = chunk, j = array.length; i < j; i += chunk) {
|
||||
temparray.push(array.slice(i, i + chunk));
|
||||
}
|
||||
temparray.forEach(async(items) => {
|
||||
await nextFrame();
|
||||
this.emojis = this.emojis.concat(items.map(item => this.renderItem(item, this.size)));
|
||||
this.forceUpdate();
|
||||
await nextFrame();
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
renderItem(emoji, size) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
key={emoji.isCustom ? emoji.content : emoji}
|
||||
onPress={() => this.props.onEmojiSelected(emoji)}
|
||||
>
|
||||
{renderEmoji(emoji, size)}
|
||||
</TouchableOpacity>);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <View style={styles.categoryInner}>{this.emojis}</View>;
|
||||
}
|
||||
}
|
|
@ -7,15 +7,21 @@ export default class extends React.PureComponent {
|
|||
static propTypes = {
|
||||
goToPage: PropTypes.func,
|
||||
activeTab: PropTypes.number,
|
||||
tabs: PropTypes.array
|
||||
tabs: PropTypes.array,
|
||||
tabEmojiStyle: PropTypes.object
|
||||
}
|
||||
|
||||
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>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
key={tab}
|
||||
onPress={() => this.props.goToPage(i)}
|
||||
style={styles.tab}
|
||||
>
|
||||
<Text style={[styles.tabEmoji, this.props.tabEmojiStyle]}>{tab}</Text>
|
||||
{this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
|
@ -1,4 +1,4 @@
|
|||
const list = ['Frequently Used', 'Custom', 'Smileys & People', 'Animals & Nature', 'Food & Drink', 'Activities', 'Travel & Places', 'Objects', 'Symbols', 'Flags'];
|
||||
const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags'];
|
||||
const tabs = [
|
||||
{
|
||||
tabLabel: '🕒',
|
|
@ -1,35 +1,32 @@
|
|||
import 'string.fromcodepoint';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { ScrollView } 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 { emojify } from 'react-emojione';
|
||||
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';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import database from '../../lib/realm';
|
||||
import { emojisByCategory } from '../../emojis';
|
||||
|
||||
const charFromUtf16 = utf16 => String.fromCodePoint(...utf16.split('-').map(u => `0x${ u }`));
|
||||
const charFromEmojiObj = obj => charFromUtf16(obj.unified);
|
||||
const scrollProps = {
|
||||
keyboardShouldPersistTaps: 'always'
|
||||
};
|
||||
|
||||
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 {
|
||||
export default class extends Component {
|
||||
static propTypes = {
|
||||
onEmojiSelected: PropTypes.func
|
||||
onEmojiSelected: PropTypes.func,
|
||||
tabEmojiStyle: PropTypes.object,
|
||||
emojisPerRow: PropTypes.number,
|
||||
width: PropTypes.number
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
categories: categories.list.slice(0, 1),
|
||||
frequentlyUsed: [],
|
||||
customEmojis: []
|
||||
};
|
||||
|
@ -38,16 +35,22 @@ export default class extends PureComponent {
|
|||
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
|
||||
this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
|
||||
}
|
||||
//
|
||||
// shouldComponentUpdate(nextProps) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
componentWillMount() {
|
||||
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
|
||||
this.customEmojis.addListener(this.updateCustomEmojis);
|
||||
this.updateFrequentlyUsed();
|
||||
this.updateCustomEmojis();
|
||||
setTimeout(() => this.setState({ show: true }), 100);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this._timeout);
|
||||
this.frequentlyUsed.removeAllListeners();
|
||||
this.customEmojis.removeAllListeners();
|
||||
}
|
||||
|
||||
onEmojiSelected(emoji) {
|
||||
|
@ -58,10 +61,11 @@ export default class extends PureComponent {
|
|||
});
|
||||
this.props.onEmojiSelected(`:${ emoji.content }:`);
|
||||
} else {
|
||||
const content = emoji.codePointAt(0).toString();
|
||||
const content = emoji;
|
||||
const count = this._getFrequentlyUsedCount(content);
|
||||
this._addFrequentlyUsed({ content, count, isCustom: false });
|
||||
this.props.onEmojiSelected(emoji);
|
||||
const shortname = `:${ emoji }:`;
|
||||
this.props.onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname);
|
||||
}
|
||||
}
|
||||
_addFrequentlyUsed = (emoji) => {
|
||||
|
@ -78,22 +82,17 @@ export default class extends PureComponent {
|
|||
if (item.isCustom) {
|
||||
return item;
|
||||
}
|
||||
return String.fromCodePoint(item.content);
|
||||
return emojify(`${ item.content }`, { output: 'unicode' });
|
||||
});
|
||||
this.setState({ frequentlyUsed });
|
||||
}
|
||||
|
||||
updateCustomEmojis() {
|
||||
const customEmojis = _.map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true }));
|
||||
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) {
|
||||
|
@ -104,31 +103,30 @@ export default class extends PureComponent {
|
|||
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); }}
|
||||
style={styles.categoryContainer}
|
||||
size={this.props.emojisPerRow}
|
||||
width={this.props.width}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const scrollProps = {
|
||||
keyboardShouldPersistTaps: 'always'
|
||||
};
|
||||
if (!this.state.show) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
// <View style={styles.container}>
|
||||
<ScrollableTabView
|
||||
renderTabBar={() => <TabBar />}
|
||||
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />}
|
||||
contentProps={scrollProps}
|
||||
>
|
||||
{
|
||||
_.map(categories.tabs, (tab, i) => (
|
||||
categories.tabs.map((tab, i) => (
|
||||
<ScrollView
|
||||
key={i}
|
||||
key={tab.category}
|
||||
tabLabel={tab.tabLabel}
|
||||
{...scrollPersistTaps}
|
||||
>
|
||||
|
@ -137,7 +135,7 @@ export default class extends PureComponent {
|
|||
))
|
||||
}
|
||||
</ScrollableTabView>
|
||||
</View>
|
||||
// </View>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const EMOJI_SIZE = width / (Platform.OS === 'ios' ? 8 : 9);
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
container: {
|
||||
|
@ -45,18 +42,16 @@ export default StyleSheet.create({
|
|||
categoryInner: {
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
flex: 1
|
||||
},
|
||||
categoryEmoji: {
|
||||
fontSize: EMOJI_SIZE - 14,
|
||||
color: 'black',
|
||||
height: EMOJI_SIZE,
|
||||
width: EMOJI_SIZE,
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'center'
|
||||
},
|
||||
customCategoryEmoji: {
|
||||
height: EMOJI_SIZE - 8,
|
||||
width: EMOJI_SIZE - 8,
|
||||
margin: 4
|
||||
}
|
||||
});
|
|
@ -13,7 +13,8 @@ import {
|
|||
permalinkClear,
|
||||
togglePinRequest,
|
||||
setInput,
|
||||
actionsHide
|
||||
actionsHide,
|
||||
toggleReactionPicker
|
||||
} from '../actions/messages';
|
||||
import { showToast } from '../utils/info';
|
||||
|
||||
|
@ -39,7 +40,8 @@ import { showToast } from '../utils/info';
|
|||
permalinkRequest: message => dispatch(permalinkRequest(message)),
|
||||
permalinkClear: () => dispatch(permalinkClear()),
|
||||
togglePinRequest: message => dispatch(togglePinRequest(message)),
|
||||
setInput: message => dispatch(setInput(message))
|
||||
setInput: message => dispatch(setInput(message)),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
|
||||
})
|
||||
)
|
||||
export default class MessageActions extends React.Component {
|
||||
|
@ -58,6 +60,7 @@ export default class MessageActions extends React.Component {
|
|||
togglePinRequest: PropTypes.func.isRequired,
|
||||
setInput: PropTypes.func.isRequired,
|
||||
permalink: PropTypes.string,
|
||||
toggleReactionPicker: PropTypes.func.isRequired,
|
||||
Message_AllowDeleting: PropTypes.bool,
|
||||
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
|
||||
Message_AllowEditing: PropTypes.bool,
|
||||
|
@ -119,6 +122,11 @@ export default class MessageActions extends React.Component {
|
|||
this.options.push(actionMessage.pinned ? 'Unpin' : 'Pin');
|
||||
this.PIN_INDEX = this.options.length - 1;
|
||||
}
|
||||
// Reaction
|
||||
if (!this.isRoomReadOnly()) {
|
||||
this.options.push('Add Reaction');
|
||||
this.REACTION_INDEX = this.options.length - 1;
|
||||
}
|
||||
// Delete
|
||||
if (this.allowDelete(nextProps)) {
|
||||
this.options.push('Delete');
|
||||
|
@ -275,6 +283,10 @@ export default class MessageActions extends React.Component {
|
|||
this.props.permalinkRequest(this.props.actionMessage);
|
||||
}
|
||||
|
||||
handleReaction() {
|
||||
this.props.toggleReactionPicker(this.props.actionMessage);
|
||||
}
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
switch (actionIndex) {
|
||||
case this.REPLY_INDEX:
|
||||
|
@ -298,6 +310,9 @@ export default class MessageActions extends React.Component {
|
|||
case this.PIN_INDEX:
|
||||
this.handlePin();
|
||||
break;
|
||||
case this.REACTION_INDEX:
|
||||
this.handleReaction();
|
||||
break;
|
||||
case this.DELETE_INDEX:
|
||||
this.handleDelete();
|
||||
break;
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity, Keyboard } from 'react-native';
|
||||
import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity, Keyboard, StyleSheet } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import { connect } from 'react-redux';
|
||||
import { emojify } from 'react-emojione';
|
||||
import { userTyping } from '../../actions/room';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import { editRequest, editCancel, clearInput } from '../../actions/messages';
|
||||
|
@ -11,11 +12,14 @@ import styles from './style';
|
|||
import MyIcon from '../icons';
|
||||
import database from '../../lib/realm';
|
||||
import Avatar from '../Avatar';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import AnimatedContainer from './AnimatedContainer';
|
||||
import EmojiPicker from './EmojiPicker';
|
||||
import EmojiPicker from '../EmojiPicker';
|
||||
import scrollPersistTaps from '../../utils/scrollPersistTaps';
|
||||
import { emojis } from '../../emojis';
|
||||
|
||||
const MENTIONS_TRACKING_TYPE_USERS = '@';
|
||||
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
|
||||
|
||||
const onlyUnique = function onlyUnique(value, index, self) {
|
||||
return self.indexOf(({ _id }) => value._id === _id) === index;
|
||||
|
@ -54,10 +58,13 @@ export default class MessageBox extends React.PureComponent {
|
|||
text: '',
|
||||
mentions: [],
|
||||
showMentionsContainer: false,
|
||||
showEmojiContainer: false
|
||||
showEmojiContainer: false,
|
||||
trackingType: ''
|
||||
};
|
||||
this.users = [];
|
||||
this.rooms = [];
|
||||
this.emojis = [];
|
||||
this.customEmojis = [];
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.message !== nextProps.message && nextProps.message.msg) {
|
||||
|
@ -80,7 +87,7 @@ export default class MessageBox extends React.PureComponent {
|
|||
|
||||
const lastNativeText = this.component._lastNativeText;
|
||||
|
||||
const regexp = /(#|@)([a-z._-]+)$/im;
|
||||
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
|
||||
|
||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||
|
||||
|
@ -176,7 +183,10 @@ export default class MessageBox extends React.PureComponent {
|
|||
this.setState({ text: '' });
|
||||
}
|
||||
async openEmoji() {
|
||||
await this.setState({ showEmojiContainer: !this.state.showEmojiContainer });
|
||||
await this.setState({
|
||||
showEmojiContainer: true,
|
||||
showMentionsContainer: false
|
||||
});
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
closeEmoji() {
|
||||
|
@ -292,25 +302,41 @@ export default class MessageBox extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_getEmojis(keyword) {
|
||||
if (keyword) {
|
||||
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4);
|
||||
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4);
|
||||
const mergedEmojis = [...this.customEmojis, ...this.emojis];
|
||||
this.setState({ mentions: mergedEmojis });
|
||||
}
|
||||
}
|
||||
|
||||
stopTrackingMention() {
|
||||
this.setState({
|
||||
showMentionsContainer: false,
|
||||
mentions: []
|
||||
mentions: [],
|
||||
trackingType: ''
|
||||
});
|
||||
this.users = [];
|
||||
this.rooms = [];
|
||||
this.customEmojis = [];
|
||||
this.emojis = [];
|
||||
}
|
||||
|
||||
identifyMentionKeyword(keyword, type) {
|
||||
this.updateMentions(keyword, type);
|
||||
this.setState({
|
||||
showMentionsContainer: true
|
||||
showMentionsContainer: true,
|
||||
showEmojiContainer: false,
|
||||
trackingType: type
|
||||
});
|
||||
this.updateMentions(keyword, type);
|
||||
}
|
||||
|
||||
updateMentions = (keyword, type) => {
|
||||
if (type === MENTIONS_TRACKING_TYPE_USERS) {
|
||||
this._getUsers(keyword);
|
||||
} else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) {
|
||||
this._getEmojis(keyword);
|
||||
} else {
|
||||
this._getRooms(keyword);
|
||||
}
|
||||
|
@ -323,10 +349,12 @@ export default class MessageBox extends React.PureComponent {
|
|||
|
||||
const cursor = Math.max(start, end);
|
||||
|
||||
const regexp = /([a-z._-]+)$/im;
|
||||
const regexp = /([a-z0-9._-]+)$/im;
|
||||
|
||||
const result = msg.substr(0, cursor).replace(regexp, '');
|
||||
const text = `${ result }${ item.username || item.name } ${ msg.slice(cursor) }`;
|
||||
const mentionName = this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
|
||||
`${ item.name || item }:` : (item.username || item.name);
|
||||
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
|
||||
this.component.setNativeProps({ text });
|
||||
this.setState({ text });
|
||||
this.component.focus();
|
||||
|
@ -357,6 +385,26 @@ export default class MessageBox extends React.PureComponent {
|
|||
<Text>Notify {item.desc} in this room</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
renderMentionEmoji = (item) => {
|
||||
if (item.name) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
key='mention-item-avatar'
|
||||
style={[styles.mentionItemCustomEmoji]}
|
||||
emoji={item}
|
||||
baseUrl={this.props.baseUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
key='mention-item-avatar'
|
||||
style={[StyleSheet.flatten(styles.mentionItemEmoji)]}
|
||||
>
|
||||
{emojify(`:${ item }:`, { output: 'unicode' })}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
renderMentionItem = (item) => {
|
||||
if (item.username === 'all' || item.username === 'here') {
|
||||
return this.renderFixedMentionItem(item);
|
||||
|
@ -366,13 +414,22 @@ export default class MessageBox extends React.PureComponent {
|
|||
style={styles.mentionItem}
|
||||
onPress={() => this._onPressMention(item)}
|
||||
>
|
||||
{this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
|
||||
[
|
||||
this.renderMentionEmoji(item),
|
||||
<Text key='mention-item-name'>:{ item.name || item }:</Text>
|
||||
]
|
||||
: [
|
||||
<Avatar
|
||||
key='mention-item-avatar'
|
||||
style={{ margin: 8 }}
|
||||
text={item.username || item.name}
|
||||
size={30}
|
||||
baseUrl={this.props.baseUrl}
|
||||
/>
|
||||
<Text>{item.username || item.name }</Text>
|
||||
/>,
|
||||
<Text key='mention-item-name'>{ item.username || item.name }</Text>
|
||||
]
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
@ -386,17 +443,17 @@ export default class MessageBox extends React.PureComponent {
|
|||
return <AnimatedContainer visible={showEmojiContainer} subview={emojiContainer} messageboxHeight={messageboxHeight} />;
|
||||
}
|
||||
renderMentions() {
|
||||
const usersList = (
|
||||
const list = (
|
||||
<FlatList
|
||||
style={styles.mentionList}
|
||||
data={this.state.mentions}
|
||||
renderItem={({ item }) => this.renderMentionItem(item)}
|
||||
keyExtractor={item => item._id}
|
||||
keyExtractor={item => item._id || item}
|
||||
{...scrollPersistTaps}
|
||||
/>
|
||||
);
|
||||
const { showMentionsContainer, messageboxHeight } = this.state;
|
||||
return <AnimatedContainer visible={showMentionsContainer} subview={usersList} messageboxHeight={messageboxHeight} />;
|
||||
return <AnimatedContainer visible={showMentionsContainer} subview={list} messageboxHeight={messageboxHeight} />;
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, Platform } from 'react-native';
|
||||
|
||||
const MENTION_HEIGHT = 50;
|
||||
|
||||
|
@ -83,6 +83,17 @@ export default StyleSheet.create({
|
|||
borderTopWidth: 1,
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
mentionItemCustomEmoji: {
|
||||
margin: 8,
|
||||
width: 30,
|
||||
height: 30
|
||||
},
|
||||
mentionItemEmoji: {
|
||||
width: 46,
|
||||
height: 36,
|
||||
fontSize: Platform.OS === 'ios' ? 30 : 25,
|
||||
textAlign: 'center'
|
||||
},
|
||||
fixedMentionAvatar: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { emojify } from 'react-emojione';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
|
||||
export default class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
content: PropTypes.string,
|
||||
standardEmojiStyle: PropTypes.object,
|
||||
customEmojiStyle: PropTypes.object,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
};
|
||||
render() {
|
||||
const {
|
||||
content, standardEmojiStyle, customEmojiStyle, customEmojis
|
||||
} = this.props;
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emojiExtension = customEmojis[parsedContent];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content: parsedContent };
|
||||
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
|
||||
}
|
||||
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line
|
|||
import SimpleMarkdown from 'simple-markdown';
|
||||
import { emojify } from 'react-emojione';
|
||||
import styles from './styles';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
|
||||
const BlockCode = ({ node, state }) => (
|
||||
<Text
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||
import Emoji from './Emoji';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: '#ffffff',
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '600'
|
||||
},
|
||||
reactCount: {
|
||||
color: '#dddddd',
|
||||
fontSize: 10
|
||||
},
|
||||
peopleReacted: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '500'
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemContainer: {
|
||||
height: 50,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: '#ffffff'
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
export default class extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
}
|
||||
renderItem = (item) => {
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
.map(username => (username.value === this.props.user.username ? 'you' : username.value)).join(', ');
|
||||
if (count > 3) {
|
||||
usernames = `${ usernames } and more ${ count - 3 }`;
|
||||
} else {
|
||||
usernames = usernames.replace(/,(?=[^,]*$)/, ' and');
|
||||
}
|
||||
return (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.emojiContainer}>
|
||||
<Emoji
|
||||
content={item.emoji}
|
||||
standardEmojiStyle={standardEmojiStyle}
|
||||
customEmojiStyle={customEmojiStyle}
|
||||
customEmojis={this.props.customEmojis}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
{count === 1 ? '1 person' : `${ count } people`} reacted
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isVisible, onClose, reactions
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
backdropOpacity={0.9}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Icon
|
||||
style={styles.closeButton}
|
||||
name='close'
|
||||
size={20}
|
||||
onPress={onClose}
|
||||
/>
|
||||
<Text style={styles.title}>Reactions</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={styles.listContainer}>
|
||||
<FlatList
|
||||
data={reactions}
|
||||
renderItem={({ item }) => this.renderItem(item)}
|
||||
keyExtractor={item => item.emoji}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -52,10 +52,13 @@ const Url = ({ url }) => {
|
|||
return (
|
||||
<TouchableOpacity onPress={() => onPress(url.url)} style={styles.button}>
|
||||
<QuoteMark />
|
||||
{url.image ?
|
||||
<Image
|
||||
style={styles.image}
|
||||
source={{ uri: encodeURI(url.image) }}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.title}>{url.title}</Text>
|
||||
<Text style={styles.description} numberOfLines={1}>{url.description}</Text>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard } from 'react-native';
|
||||
import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard, StyleSheet, Vibration } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||
import moment from 'moment';
|
||||
import equal from 'deep-equal';
|
||||
|
||||
import { actionsShow, errorActionsShow } from '../../actions/messages';
|
||||
import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages';
|
||||
import Image from './Image';
|
||||
import User from './User';
|
||||
import Avatar from '../Avatar';
|
||||
|
@ -14,6 +15,8 @@ import Video from './Video';
|
|||
import Markdown from './Markdown';
|
||||
import Url from './Url';
|
||||
import Reply from './Reply';
|
||||
import ReactionsModal from './ReactionsModal';
|
||||
import Emoji from './Emoji';
|
||||
import messageStatus from '../../constants/messagesStatus';
|
||||
import styles from './styles';
|
||||
|
||||
|
@ -26,11 +29,13 @@ const flex = { flexDirection: 'row', flex: 1 };
|
|||
customEmojis: state.customEmojis
|
||||
}), dispatch => ({
|
||||
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
|
||||
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage))
|
||||
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
|
||||
}))
|
||||
export default class Message extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
|
@ -39,7 +44,15 @@ export default class Message extends React.Component {
|
|||
actionsShow: PropTypes.func,
|
||||
errorActionsShow: PropTypes.func,
|
||||
animate: PropTypes.bool,
|
||||
customEmojis: PropTypes.object
|
||||
customEmojis: PropTypes.object,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
onReactionPress: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { reactionsModal: false };
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -60,7 +73,13 @@ export default class Message extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!equal(this.props.reactions, nextProps.reactions)) {
|
||||
return true;
|
||||
}
|
||||
if (this.state.reactionsModal !== nextState.reactionsModal) {
|
||||
return true;
|
||||
}
|
||||
return this.props.item._updatedAt.toGMTString() !== nextProps.item._updatedAt.toGMTString() || this.props.item.status !== nextProps.item.status;
|
||||
}
|
||||
|
||||
|
@ -69,13 +88,22 @@ export default class Message extends React.Component {
|
|||
}
|
||||
|
||||
onLongPress() {
|
||||
const { item } = this.props;
|
||||
this.props.actionsShow(JSON.parse(JSON.stringify(item)));
|
||||
this.props.actionsShow(this.parseMessage());
|
||||
}
|
||||
|
||||
onErrorPress() {
|
||||
const { item } = this.props;
|
||||
this.props.errorActionsShow(JSON.parse(JSON.stringify(item)));
|
||||
this.props.errorActionsShow(this.parseMessage());
|
||||
}
|
||||
|
||||
onReactionPress(emoji) {
|
||||
this.props.onReactionPress(emoji, this.props.item._id);
|
||||
}
|
||||
onClose() {
|
||||
this.setState({ reactionsModal: false });
|
||||
}
|
||||
onReactionLongPress() {
|
||||
this.setState({ reactionsModal: true });
|
||||
Vibration.vibrate(50);
|
||||
}
|
||||
|
||||
getInfoMessage() {
|
||||
|
@ -105,11 +133,12 @@ export default class Message extends React.Component {
|
|||
return message;
|
||||
}
|
||||
|
||||
parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
|
||||
|
||||
isInfoMessage() {
|
||||
return ['r', 'au', 'ru', 'ul', 'uj', 'rm', 'user-muted', 'user-unmuted', 'message_pinned'].includes(this.props.item.t);
|
||||
}
|
||||
|
||||
|
||||
isDeleted() {
|
||||
return this.props.item.t === 'rm';
|
||||
}
|
||||
|
@ -165,9 +194,50 @@ export default class Message extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderReaction(reaction) {
|
||||
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1;
|
||||
const reactedContainerStyle = reacted ? { borderColor: '#bde1fe', backgroundColor: '#f3f9ff' } : {};
|
||||
const reactedCount = reacted ? { color: '#4fb0fc' } : {};
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.onReactionPress(reaction.emoji)}
|
||||
onLongPress={() => this.onReactionLongPress()}
|
||||
key={reaction.emoji}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reactedContainerStyle]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
standardEmojiStyle={StyleSheet.flatten(styles.reactionEmoji)}
|
||||
customEmojiStyle={StyleSheet.flatten(styles.reactionCustomEmoji)}
|
||||
customEmojis={this.props.customEmojis}
|
||||
/>
|
||||
<Text style={[styles.reactionCount, reactedCount]}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
renderReactions() {
|
||||
if (this.props.item.reactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{this.props.item.reactions.map(reaction => this.renderReaction(reaction))}
|
||||
<TouchableOpacity
|
||||
onPress={() => this.props.toggleReactionPicker(this.parseMessage())}
|
||||
key='add-reaction'
|
||||
style={styles.reactionContainer}
|
||||
>
|
||||
<Icon name='insert-emoticon' color='#aaaaaa' size={15} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
item, message, editing, baseUrl
|
||||
item, message, editing, baseUrl, customEmojis
|
||||
} = this.props;
|
||||
|
||||
const marginLeft = this._visibility.interpolate({
|
||||
|
@ -181,7 +251,7 @@ export default class Message extends React.Component {
|
|||
const username = item.alias || item.u.username;
|
||||
const isEditing = message._id === item._id && editing;
|
||||
|
||||
const accessibilityLabel = `Message from ${ item.alias || item.u.username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`;
|
||||
const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
|
@ -213,8 +283,19 @@ export default class Message extends React.Component {
|
|||
{this.renderMessageContent()}
|
||||
{this.attachments()}
|
||||
{this.renderUrl()}
|
||||
{this.renderReactions()}
|
||||
</View>
|
||||
</View>
|
||||
{this.state.reactionsModal ?
|
||||
<ReactionsModal
|
||||
isVisible={this.state.reactionsModal}
|
||||
onClose={this.onClose}
|
||||
reactions={item.reactions}
|
||||
user={this.props.user}
|
||||
customEmojis={customEmojis}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Animated.View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
|
|
@ -33,5 +33,35 @@ export default StyleSheet.create({
|
|||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
padding: 5
|
||||
},
|
||||
reactionsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap'
|
||||
},
|
||||
reactionContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: '#cccccc',
|
||||
borderRadius: 4,
|
||||
marginRight: 5,
|
||||
marginBottom: 5,
|
||||
height: 23,
|
||||
width: 35
|
||||
},
|
||||
reactionCount: {
|
||||
fontSize: 12,
|
||||
marginLeft: 2,
|
||||
fontWeight: '600',
|
||||
color: '#aaaaaa'
|
||||
},
|
||||
reactionEmoji: {
|
||||
fontSize: 12
|
||||
},
|
||||
reactionCustomEmoji: {
|
||||
width: 15,
|
||||
height: 15
|
||||
}
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -142,6 +142,21 @@ const url = {
|
|||
}
|
||||
};
|
||||
|
||||
const messagesReactionsUsernamesSchema = {
|
||||
name: 'messagesReactionsUsernames',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const messagesReactionsSchema = {
|
||||
name: 'messagesReactions',
|
||||
primaryKey: 'emoji',
|
||||
properties: {
|
||||
emoji: 'string',
|
||||
usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
|
||||
}
|
||||
};
|
||||
|
||||
const messagesEditedBySchema = {
|
||||
name: 'messagesEditedBy',
|
||||
|
@ -173,7 +188,8 @@ const messagesSchema = {
|
|||
status: { type: 'int', optional: true },
|
||||
pinned: { type: 'bool', optional: true },
|
||||
starred: { type: 'bool', optional: true },
|
||||
editedBy: 'messagesEditedBy'
|
||||
editedBy: 'messagesEditedBy',
|
||||
reactions: { type: 'list', objectType: 'messagesReactions' }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -222,7 +238,9 @@ const schema = [
|
|||
url,
|
||||
frequentlyUsedEmojiSchema,
|
||||
customEmojiAliasesSchema,
|
||||
customEmojisSchema
|
||||
customEmojisSchema,
|
||||
messagesReactionsSchema,
|
||||
messagesReactionsUsernamesSchema
|
||||
];
|
||||
class DB {
|
||||
databases = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Random from 'react-native-meteor/lib/Random';
|
||||
import { AsyncStorage, Platform } from 'react-native';
|
||||
import { hashPassword } from 'react-native-meteor/lib/utils';
|
||||
import _ from 'lodash';
|
||||
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import reduxStore from './createStore';
|
||||
|
@ -264,6 +265,8 @@ const RocketChat = {
|
|||
// loadHistory returns message.starred as object
|
||||
// stream-room-messages returns message.starred as an array
|
||||
message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred);
|
||||
message.reactions = _.map(message.reactions, (value, key) =>
|
||||
({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) }));
|
||||
return message;
|
||||
},
|
||||
loadMessagesForRoom(rid, end, cb) {
|
||||
|
@ -586,6 +589,9 @@ const RocketChat = {
|
|||
},
|
||||
setUserPresenceDefaultStatus(status) {
|
||||
return call('UserPresence:setDefaultStatus', status);
|
||||
},
|
||||
setReaction(emoji, messageId) {
|
||||
return call('setReaction', emoji, messageId);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ const initialState = {
|
|||
editing: false,
|
||||
permalink: '',
|
||||
showActions: false,
|
||||
showErrorActions: false
|
||||
showErrorActions: false,
|
||||
showReactionPicker: false
|
||||
};
|
||||
|
||||
export default function messages(state = initialState, action) {
|
||||
|
@ -96,6 +97,12 @@ export default function messages(state = initialState, action) {
|
|||
...state,
|
||||
message: {}
|
||||
};
|
||||
case types.MESSAGES.TOGGLE_REACTION_PICKER:
|
||||
return {
|
||||
...state,
|
||||
showReactionPicker: !state.showReactionPicker,
|
||||
actionMessage: action.message
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -4,10 +4,16 @@ import cloneReferencedElement from 'react-clone-referenced-element';
|
|||
import { ScrollView, ListView as OldList2 } from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import DateSeparator from './DateSeparator';
|
||||
import UnreadSeparator from './UnreadSeparator';
|
||||
import styles from './styles';
|
||||
import debounce from '../../utils/debounce';
|
||||
import Typing from '../../containers/Typing';
|
||||
import database from '../../lib/realm';
|
||||
|
||||
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50;
|
||||
const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100;
|
||||
|
||||
export class DataSource extends OldList.DataSource {
|
||||
getRowData(sectionIndex: number, rowIndex: number): any {
|
||||
|
@ -20,9 +26,58 @@ export class DataSource extends OldList.DataSource {
|
|||
}
|
||||
}
|
||||
|
||||
const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
|
||||
|
||||
@connect(state => ({
|
||||
lastOpen: state.room.lastOpen
|
||||
}))
|
||||
|
||||
export class List extends React.Component {
|
||||
static propTypes = {
|
||||
onEndReached: PropTypes.func,
|
||||
renderFooter: PropTypes.func,
|
||||
renderRow: PropTypes.func,
|
||||
room: PropTypes.string,
|
||||
end: PropTypes.bool
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.data = database
|
||||
.objects('messages')
|
||||
.filtered('rid = $0', props.room)
|
||||
.sorted('ts', true);
|
||||
this.dataSource = ds.cloneWithRows(this.data);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.data.addListener(this.updateState);
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.end !== nextProps.end;
|
||||
}
|
||||
updateState = debounce(() => {
|
||||
// this.setState({
|
||||
this.dataSource = this.dataSource.cloneWithRows(this.data);
|
||||
this.forceUpdate();
|
||||
// });
|
||||
}, 100);
|
||||
|
||||
render() {
|
||||
return (<ListView
|
||||
enableEmptySections
|
||||
style={styles.list}
|
||||
onEndReachedThreshold={0.5}
|
||||
renderFooter={this.props.renderFooter}
|
||||
renderHeader={() => <Typing />}
|
||||
onEndReached={() => this.props.onEndReached(this.data)}
|
||||
dataSource={this.dataSource}
|
||||
renderRow={item => this.props.renderRow(item)}
|
||||
initialListSize={10}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='none'
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListView extends OldList2 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Platform } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Modal from 'react-native-modal';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
import EmojiPicker from '../../containers/EmojiPicker';
|
||||
import { toggleReactionPicker } from '../../actions/messages';
|
||||
import styles from './styles';
|
||||
|
||||
const margin = Platform.OS === 'android' ? 40 : 20;
|
||||
const tabEmojiStyle = { fontSize: 15 };
|
||||
|
||||
@connect(state => ({
|
||||
showReactionPicker: state.messages.showReactionPicker
|
||||
}), dispatch => ({
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
|
||||
}))
|
||||
@responsive
|
||||
export default class extends React.Component {
|
||||
static propTypes = {
|
||||
window: PropTypes.any,
|
||||
showReactionPicker: PropTypes.bool,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
onEmojiSelected: PropTypes.func
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.showReactionPicker !== this.props.showReactionPicker || this.props.window.width !== nextProps.window.width;
|
||||
}
|
||||
|
||||
onEmojiSelected(emoji, shortname) {
|
||||
// standard emojis: `emoji` is unicode and `shortname` is :joy:
|
||||
// custom emojis: only `emoji` is returned with shortname type (:joy:)
|
||||
// to set reactions, we need shortname type
|
||||
this.props.onEmojiSelected(shortname || emoji);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height } = this.props.window;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={this.props.showReactionPicker}
|
||||
style={{ alignItems: 'center' }}
|
||||
onBackdropPress={() => this.props.toggleReactionPicker()}
|
||||
onBackButtonPress={() => this.props.toggleReactionPicker()}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
>
|
||||
<View style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]}>
|
||||
<EmojiPicker
|
||||
tabEmojiStyle={tabEmojiStyle}
|
||||
width={Math.min(width, height) - margin}
|
||||
onEmojiSelected={(emoji, shortname) => this.onEmojiSelected(emoji, shortname)}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,44 +1,45 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View, Button, SafeAreaView, Platform } from 'react-native';
|
||||
import { Text, View, Button, SafeAreaView, Platform, Keyboard } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import equal from 'deep-equal';
|
||||
import KeyboardSpacer from 'react-native-keyboard-spacer';
|
||||
|
||||
import { ListView } from './ListView';
|
||||
import { List } from './ListView';
|
||||
import * as actions from '../../actions';
|
||||
import { openRoom, setLastOpen } from '../../actions/room';
|
||||
import { editCancel } from '../../actions/messages';
|
||||
import { editCancel, toggleReactionPicker } from '../../actions/messages';
|
||||
import { setKeyboardOpen, setKeyboardClosed } from '../../actions/keyboard';
|
||||
import database from '../../lib/realm';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import Message from '../../containers/message';
|
||||
import MessageActions from '../../containers/MessageActions';
|
||||
import MessageErrorActions from '../../containers/MessageErrorActions';
|
||||
import MessageBox from '../../containers/MessageBox';
|
||||
import Typing from '../../containers/Typing';
|
||||
|
||||
import Header from '../../containers/Header';
|
||||
import RoomsHeader from './Header';
|
||||
import ReactionPicker from './ReactionPicker';
|
||||
import Banner from './banner';
|
||||
import styles from './styles';
|
||||
|
||||
import debounce from '../../utils/debounce';
|
||||
|
||||
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
|
||||
|
||||
const typing = () => <Typing />;
|
||||
@connect(
|
||||
state => ({
|
||||
Site_Url: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
loading: state.messages.isFetching,
|
||||
user: state.login.user
|
||||
user: state.login.user,
|
||||
actionMessage: state.messages.actionMessage
|
||||
}),
|
||||
dispatch => ({
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
openRoom: room => dispatch(openRoom(room)),
|
||||
editCancel: () => dispatch(editCancel()),
|
||||
setLastOpen: date => dispatch(setLastOpen(date))
|
||||
setLastOpen: date => dispatch(setLastOpen(date)),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
|
||||
setKeyboardOpen: () => dispatch(setKeyboardOpen()),
|
||||
setKeyboardClosed: () => dispatch(setKeyboardClosed())
|
||||
})
|
||||
)
|
||||
export default class RoomView extends React.Component {
|
||||
|
@ -51,7 +52,12 @@ export default class RoomView extends React.Component {
|
|||
rid: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
Site_Url: PropTypes.string,
|
||||
Message_TimeFormat: PropTypes.string
|
||||
Message_TimeFormat: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
actionMessage: PropTypes.object,
|
||||
toggleReactionPicker: PropTypes.func.isRequired,
|
||||
setKeyboardOpen: PropTypes.func,
|
||||
setKeyboardClosed: PropTypes.func
|
||||
};
|
||||
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
|
@ -63,22 +69,17 @@ export default class RoomView extends React.Component {
|
|||
this.rid =
|
||||
props.rid ||
|
||||
props.navigation.state.params.room.rid;
|
||||
this.name = this.props.name ||
|
||||
this.props.navigation.state.params.name ||
|
||||
this.props.navigation.state.params.room.name;
|
||||
this.name = props.name ||
|
||||
props.navigation.state.params.name ||
|
||||
props.navigation.state.params.room.name;
|
||||
this.opened = new Date();
|
||||
this.data = database
|
||||
.objects('messages')
|
||||
.filtered('rid = $0', this.rid)
|
||||
.sorted('ts', true);
|
||||
const rowIds = this.data.map((row, index) => index);
|
||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||
this.state = {
|
||||
dataSource: ds.cloneWithRows(this.data, rowIds),
|
||||
loaded: true,
|
||||
joined: typeof props.rid === 'undefined',
|
||||
readOnly: false
|
||||
room: {}
|
||||
};
|
||||
this.onReactionPress = this.onReactionPress.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -86,59 +87,58 @@ export default class RoomView extends React.Component {
|
|||
title: this.name
|
||||
});
|
||||
this.updateRoom();
|
||||
this.props.openRoom({ rid: this.rid, name: this.name, ls: this.room.ls });
|
||||
if (this.room.alert || this.room.unread || this.room.userMentions) {
|
||||
this.props.setLastOpen(this.room.ls);
|
||||
this.props.openRoom({ rid: this.rid, name: this.name, ls: this.state.room.ls });
|
||||
if (this.state.room.alert || this.state.room.unread || this.state.room.userMentions) {
|
||||
this.props.setLastOpen(this.state.room.ls);
|
||||
} else {
|
||||
this.props.setLastOpen(null);
|
||||
}
|
||||
this.data.addListener(this.updateState);
|
||||
|
||||
this.rooms.addListener(this.updateRoom);
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => this.props.setKeyboardOpen());
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => this.props.setKeyboardClosed());
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !(equal(this.props, nextProps) && equal(this.state, nextState));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
this.data.removeAllListeners();
|
||||
this.rooms.removeAllListeners();
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
this.props.editCancel();
|
||||
}
|
||||
|
||||
onEndReached = () => {
|
||||
if (
|
||||
// rowCount &&
|
||||
this.state.loaded &&
|
||||
this.state.loadingMore !== true &&
|
||||
this.state.end !== true
|
||||
) {
|
||||
this.setState({
|
||||
loadingMore: true
|
||||
});
|
||||
onEndReached = (data) => {
|
||||
if (this.props.loading || this.state.end) {
|
||||
return;
|
||||
}
|
||||
if (!this.state.loaded) {
|
||||
alert(2);
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const lastRowData = this.data[this.data.length - 1];
|
||||
const lastRowData = data[data.length - 1];
|
||||
if (!lastRowData) {
|
||||
return;
|
||||
}
|
||||
RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => {
|
||||
this.setState({
|
||||
loadingMore: false,
|
||||
RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => end && this.setState({
|
||||
end
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateState = debounce(() => {
|
||||
const rowIds = this.data.map((row, index) => index);
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.cloneWithRows(this.data, rowIds)
|
||||
});
|
||||
}, 50);
|
||||
onReactionPress = (shortname, messageId) => {
|
||||
if (!messageId) {
|
||||
RocketChat.setReaction(shortname, this.props.actionMessage._id);
|
||||
return this.props.toggleReactionPicker();
|
||||
}
|
||||
RocketChat.setReaction(shortname, messageId);
|
||||
};
|
||||
|
||||
updateRoom = () => {
|
||||
[this.room] = this.rooms;
|
||||
this.setState({ readOnly: this.room.ro });
|
||||
this.setState({ room: this.rooms[0] });
|
||||
}
|
||||
|
||||
sendMessage = message => RocketChat.sendMessage(this.rid, message).then(() => {
|
||||
|
@ -156,10 +156,12 @@ export default class RoomView extends React.Component {
|
|||
<Message
|
||||
key={item._id}
|
||||
item={item}
|
||||
reactions={JSON.parse(JSON.stringify(item.reactions))}
|
||||
animate={this.opened.toISOString() < item.ts.toISOString()}
|
||||
baseUrl={this.props.Site_Url}
|
||||
Message_TimeFormat={this.props.Message_TimeFormat}
|
||||
user={this.props.user}
|
||||
onReactionPress={this.onReactionPress}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -174,7 +176,7 @@ export default class RoomView extends React.Component {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
if (this.state.readOnly) {
|
||||
if (this.state.room.ro) {
|
||||
return (
|
||||
<View style={styles.readOnly}>
|
||||
<Text>This room is read only</Text>
|
||||
|
@ -185,36 +187,28 @@ export default class RoomView extends React.Component {
|
|||
};
|
||||
|
||||
renderHeader = () => {
|
||||
if (this.state.loadingMore) {
|
||||
return <Text style={styles.loadingMore}>Loading more messages...</Text>;
|
||||
}
|
||||
|
||||
if (this.state.end) {
|
||||
return <Text style={styles.loadingMore}>Start of conversation</Text>;
|
||||
}
|
||||
return <Text style={styles.loadingMore}>Loading more messages...</Text>;
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Banner />
|
||||
<SafeAreaView style={styles.safeAreaView}>
|
||||
<ListView
|
||||
enableEmptySections
|
||||
style={styles.list}
|
||||
onEndReachedThreshold={500}
|
||||
<List
|
||||
end={this.state.end}
|
||||
room={this.rid}
|
||||
renderFooter={this.renderHeader}
|
||||
renderHeader={typing}
|
||||
onEndReached={this.onEndReached}
|
||||
dataSource={this.state.dataSource}
|
||||
renderRow={item => this.renderItem(item)}
|
||||
initialListSize={10}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='none'
|
||||
/>
|
||||
</SafeAreaView>
|
||||
{this.renderFooter()}
|
||||
<MessageActions room={this.room} />
|
||||
{this.state.room._id ? <MessageActions room={this.state.room} /> : null}
|
||||
<MessageErrorActions />
|
||||
<ReactionPicker onEmojiSelected={this.onReactionPress} />
|
||||
{Platform.OS === 'ios' ? <KeyboardSpacer /> : null}
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, Platform } from 'react-native';
|
||||
|
||||
export default StyleSheet.create({
|
||||
typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 },
|
||||
|
@ -33,5 +33,13 @@ export default StyleSheet.create({
|
|||
},
|
||||
readOnly: {
|
||||
padding: 10
|
||||
},
|
||||
reactionPickerContainer: {
|
||||
// width: width - 20,
|
||||
// height: width - 20,
|
||||
paddingHorizontal: Platform.OS === 'android' ? 11 : 10,
|
||||
backgroundColor: '#F7F7F7',
|
||||
borderRadius: 4,
|
||||
flexDirection: 'column'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1481,6 +1481,16 @@
|
|||
"babel-helper-is-void-0": "0.2.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-module-resolver": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-2.7.1.tgz",
|
||||
"integrity": "sha1-GL48Qt31n3pFbJ4FEs2ROU9uS+E=",
|
||||
"requires": {
|
||||
"find-babel-config": "1.1.0",
|
||||
"glob": "7.1.2",
|
||||
"resolve": "1.5.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-react-transform": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-3.0.0.tgz",
|
||||
|
@ -2172,6 +2182,18 @@
|
|||
"semver": "5.4.1"
|
||||
}
|
||||
},
|
||||
"babel-preset-expo": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-4.0.0.tgz",
|
||||
"integrity": "sha512-EWFC6WJzZX5t2zZfLNdJXUkNMusUkxP5V+GrXaSk8pKbWGjE3TD2i33ncpF/4aQM9QGDm+SH6pImZJOqIDlRUw==",
|
||||
"requires": {
|
||||
"babel-plugin-module-resolver": "2.7.1",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-exponentiation-operator": "6.24.1",
|
||||
"babel-plugin-transform-export-extensions": "6.22.0",
|
||||
"babel-preset-react-native": "4.0.0"
|
||||
}
|
||||
},
|
||||
"babel-preset-fbjs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz",
|
||||
|
@ -4610,11 +4632,6 @@
|
|||
"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",
|
||||
|
@ -5771,6 +5788,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"find-babel-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.1.0.tgz",
|
||||
"integrity": "sha1-rMAQQ6Z0n+w0Qpvmtk9ULrtdY1U=",
|
||||
"requires": {
|
||||
"json5": "0.5.1",
|
||||
"path-exists": "3.0.0"
|
||||
}
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
|
||||
|
@ -12610,6 +12636,14 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.1.tgz",
|
||||
"integrity": "sha1-DiPbMC0Du0o/KNwHLcryqaEXjtg="
|
||||
},
|
||||
"react-native-responsive-ui": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-responsive-ui/-/react-native-responsive-ui-1.1.1.tgz",
|
||||
"integrity": "sha1-60GDnU85Uf8CVmAYXDapqc4zdZ8=",
|
||||
"requires": {
|
||||
"lodash": "4.17.4"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
@ -14445,11 +14479,6 @@
|
|||
"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",
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-remove-console": "^6.8.5",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-expo": "^4.0.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",
|
||||
|
@ -51,6 +51,7 @@
|
|||
"react-native-modal": "^4.1.1",
|
||||
"react-native-optimized-flatlist": "^1.0.3",
|
||||
"react-native-push-notification": "^3.0.1",
|
||||
"react-native-responsive-ui": "^1.1.1",
|
||||
"react-native-scrollable-tab-view": "^0.8.0",
|
||||
"react-native-slider": "^0.11.0",
|
||||
"react-native-splash-screen": "^3.0.6",
|
||||
|
@ -72,7 +73,6 @@
|
|||
"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