commit
5ad676f7dc
7
.babelrc
7
.babelrc
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"presets": ["react-native"],
|
||||
"plugins": ["transform-decorators-legacy"]
|
||||
"plugins": ["transform-decorators-legacy"],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": ["transform-remove-console"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ const settingsSchema = {
|
|||
_server: 'servers',
|
||||
valueAsString: { type: 'string', optional: true },
|
||||
valueAsBoolean: { type: 'bool', optional: true },
|
||||
valueAsNumber: { type: 'int', optional: true }
|
||||
valueAsNumber: { type: 'int', optional: true },
|
||||
|
||||
_updatedAt: { type: 'date', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -26,9 +26,7 @@ const RocketChat = {
|
|||
TOKEN_KEY,
|
||||
|
||||
createChannel({ name, users, type }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call(type ? 'createChannel' : 'createPrivateGroup', name, users, type, (err, res) => (err ? reject(err) : resolve(res)));
|
||||
});
|
||||
return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type);
|
||||
},
|
||||
|
||||
async getUserToken() {
|
||||
|
@ -52,61 +50,63 @@ const RocketChat = {
|
|||
const url = `${ _url }/websocket`;
|
||||
|
||||
Meteor.connect(url, { autoConnect: true, autoReconnect: true });
|
||||
|
||||
Meteor.ddp.on('disconnected', () => {
|
||||
reduxStore.dispatch(disconnect());
|
||||
});
|
||||
|
||||
Meteor.ddp.on('connected', () => {
|
||||
reduxStore.dispatch(connectSuccess());
|
||||
resolve();
|
||||
});
|
||||
Meteor.ddp.on('connected', () => {
|
||||
Meteor.call('public-settings/get', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const settings = {};
|
||||
realm.write(() => {
|
||||
data.forEach((item) => {
|
||||
const setting = {
|
||||
_id: item._id
|
||||
};
|
||||
setting._server = { id: reduxStore.getState().server.server };
|
||||
if (settingsType[item.type]) {
|
||||
setting[settingsType[item.type]] = item.value;
|
||||
realm.create('settings', setting, true);
|
||||
}
|
||||
|
||||
settings[item._id] = item.value;
|
||||
});
|
||||
});
|
||||
reduxStore.dispatch(actions.setAllSettings(settings));
|
||||
});
|
||||
|
||||
Meteor.ddp.on('connected', async() => {
|
||||
Meteor.ddp.on('changed', (ddbMessage) => {
|
||||
const server = { id: reduxStore.getState().server.server };
|
||||
if (ddbMessage.collection === 'stream-room-messages') {
|
||||
realm.write(() => {
|
||||
const message = ddbMessage.fields.args[0];
|
||||
message.temp = false;
|
||||
message._server = { id: reduxStore.getState().server.server };
|
||||
message._server = server;
|
||||
message.attachments = message.attachments || [];
|
||||
message.starred = !!message.starred;
|
||||
realm.create('messages', message, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (ddbMessage.collection === 'stream-notify-user') {
|
||||
realm.write(() => {
|
||||
const data = ddbMessage.fields.args[1];
|
||||
data._server = { id: reduxStore.getState().server.server };
|
||||
realm.create('subscriptions', data, true);
|
||||
});
|
||||
const [type, data] = ddbMessage.fields.args;
|
||||
const [, ev] = ddbMessage.fields.eventName.split('/');
|
||||
if (/subscriptions/.test(ev)) {
|
||||
switch (type) {
|
||||
case 'inserted':
|
||||
data._server = server;
|
||||
realm.write(() => {
|
||||
realm.create('subscriptions', data, true);
|
||||
});
|
||||
break;
|
||||
case 'updated':
|
||||
delete data._updatedAt;
|
||||
realm.write(() => {
|
||||
realm.create('subscriptions', data, true);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
if (/rooms/.test(ev) && type === 'updated') {
|
||||
const sub = realm.objects('subscriptions').filtered('rid == $0', data._id)[0];
|
||||
realm.write(() => {
|
||||
sub._updatedAt = data._updatedAt;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
RocketChat.getSettings();
|
||||
});
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
},
|
||||
|
||||
login(params, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor._startLoggingIn();
|
||||
|
@ -114,6 +114,11 @@ const RocketChat = {
|
|||
Meteor._endLoggingIn();
|
||||
Meteor._handleLoginCallback(err, result);
|
||||
if (err) {
|
||||
if (/user not found/i.test(err.reason)) {
|
||||
err.error = 1;
|
||||
err.reason = 'User or Password incorrect';
|
||||
err.message = 'User or Password incorrect';
|
||||
}
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
|
@ -137,36 +142,15 @@ const RocketChat = {
|
|||
},
|
||||
|
||||
register({ credentials }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('registerUser', credentials, (err, userId) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(userId);
|
||||
});
|
||||
});
|
||||
return call('registerUser', credentials);
|
||||
},
|
||||
|
||||
setUsername({ credentials }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('setUsername', credentials.username, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
return call('setUsername', credentials.username);
|
||||
},
|
||||
|
||||
forgotPassword(email) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('sendForgotPasswordEmail', email, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
return call('sendForgotPasswordEmail', email);
|
||||
},
|
||||
|
||||
loginWithPassword({ username, password, code }, callback) {
|
||||
|
@ -211,26 +195,6 @@ const RocketChat = {
|
|||
return this.login(params, callback);
|
||||
},
|
||||
|
||||
// loadRooms(cb) {
|
||||
// console.warn('a');
|
||||
// Meteor.call('rooms/get', (err, data) => {
|
||||
// if (err) {
|
||||
// console.error(err);
|
||||
// }
|
||||
// console.warn(`rooms ${ data.length }`);
|
||||
// if (data.length) {
|
||||
// realm.write(() => {
|
||||
// data.forEach((room) => {
|
||||
// room._server = { id: reduxStore.getState().server.server };
|
||||
// realm.create('rooms', room, true);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// return cb && cb();
|
||||
// });
|
||||
// },
|
||||
|
||||
loadSubscriptions(cb) {
|
||||
Meteor.call('subscriptions/get', (err, data) => {
|
||||
if (err) {
|
||||
|
@ -285,6 +249,7 @@ const RocketChat = {
|
|||
data.messages.forEach((message) => {
|
||||
message.temp = false;
|
||||
message._server = { id: reduxStore.getState().server.server };
|
||||
message.attachments = message.attachments || [];
|
||||
// write('messages', message);
|
||||
message.starred = !!message.starred;
|
||||
realm.create('messages', message, true);
|
||||
|
@ -334,38 +299,17 @@ const RocketChat = {
|
|||
},
|
||||
|
||||
spotlight(search, usernames) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('spotlight', search, usernames, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('spotlight', search, usernames);
|
||||
},
|
||||
|
||||
createDirectMessage(username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('createDirectMessage', username, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('createDirectMessage', username);
|
||||
},
|
||||
readMessages(rid) {
|
||||
return call('readMessages', rid);
|
||||
},
|
||||
joinRoom(rid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('joinRoom', rid, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('joinRoom', rid);
|
||||
},
|
||||
|
||||
|
||||
|
@ -379,26 +323,12 @@ const RocketChat = {
|
|||
*/
|
||||
_ufsCreate(fileInfo) {
|
||||
// return call('ufsCreate', fileInfo);
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('ufsCreate', fileInfo, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('ufsCreate', fileInfo);
|
||||
},
|
||||
|
||||
// ["ZTE8CKHJt7LATv7Me","fileSystem","e8E96b2819"
|
||||
_ufsComplete(fileId, store, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('ufsComplete', fileId, store, token, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('ufsComplete', fileId, store, token);
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -412,14 +342,7 @@ const RocketChat = {
|
|||
}
|
||||
*/
|
||||
_sendFileMessage(rid, data, msg = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Meteor.call('sendFileMessage', rid, null, data, msg, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(result);
|
||||
});
|
||||
});
|
||||
return call('sendFileMessage', rid, null, data, msg);
|
||||
},
|
||||
async sendFileMessage(rid, fileInfo, data) {
|
||||
const placeholder = RocketChat.getMessage(rid, 'Sending an image');
|
||||
|
@ -448,23 +371,36 @@ const RocketChat = {
|
|||
});
|
||||
}
|
||||
},
|
||||
getRooms() {
|
||||
async getRooms() {
|
||||
const { server, login } = reduxStore.getState();
|
||||
return Promise.all([call('subscriptions/get'), call('rooms/get')]).then(([subscriptions, rooms]) => {
|
||||
const data = subscriptions.map((subscription) => {
|
||||
subscription._updatedAt = (rooms.find(room => room._id === subscription.rid) || {})._updatedAt;
|
||||
subscription._server = { id: server.server };
|
||||
return subscription;
|
||||
});
|
||||
let lastMessage = realm
|
||||
.objects('subscriptions')
|
||||
.filtered('_server.id = $0', server.server)
|
||||
.sorted('_updatedAt', true)[0];
|
||||
lastMessage = lastMessage && new Date(lastMessage._updatedAt);
|
||||
let [subscriptions, rooms] = await Promise.all([call('subscriptions/get', lastMessage), call('rooms/get', lastMessage)]);
|
||||
|
||||
realm.write(() => {
|
||||
data.forEach((subscription) => {
|
||||
realm.create('subscriptions', subscription, true);
|
||||
});
|
||||
});
|
||||
Meteor.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false);
|
||||
return data;
|
||||
if (lastMessage) {
|
||||
subscriptions = subscriptions.update;
|
||||
rooms = rooms.update;
|
||||
}
|
||||
const data = subscriptions.map((subscription) => {
|
||||
const room = rooms.find(({ _id }) => _id === subscription.rid);
|
||||
delete subscription._updatedAt;
|
||||
if (room) {
|
||||
subscription._updatedAt = room._updatedAt;
|
||||
}
|
||||
subscription._server = { id: server.server };
|
||||
return subscription;
|
||||
});
|
||||
|
||||
realm.write(() => {
|
||||
data.forEach(subscription =>
|
||||
realm.create('subscriptions', subscription, true));
|
||||
});
|
||||
Meteor.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false);
|
||||
Meteor.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false);
|
||||
return data;
|
||||
},
|
||||
logout({ server }) {
|
||||
Meteor.logout();
|
||||
|
@ -472,6 +408,27 @@ const RocketChat = {
|
|||
AsyncStorage.removeItem(TOKEN_KEY);
|
||||
AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`);
|
||||
},
|
||||
async getSettings() {
|
||||
const temp = realm.objects('settings').sorted('_updatedAt', true)[0];
|
||||
const result = await (!temp ? call('public-settings/get') : call('public-settings/get', new Date(temp._updatedAt)));
|
||||
const settings = temp ? result.update : result;
|
||||
const filteredSettings = RocketChat._prepareSettings(RocketChat._filterSettings(settings));
|
||||
realm.write(() => {
|
||||
filteredSettings.forEach(setting => realm.create('settings', setting, true));
|
||||
});
|
||||
reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(filteredSettings)));
|
||||
},
|
||||
parseSettings: settings => settings.reduce((ret, item) => {
|
||||
ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.value;
|
||||
return ret;
|
||||
}, {}),
|
||||
_prepareSettings(settings) {
|
||||
return settings.map((setting) => {
|
||||
setting[settingsType[setting.type]] = setting.value;
|
||||
return setting;
|
||||
});
|
||||
},
|
||||
_filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value),
|
||||
deleteMessage(message) {
|
||||
return call('deleteMessage', { _id: message._id });
|
||||
},
|
||||
|
|
|
@ -87,13 +87,12 @@ export default class RoomItem extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { color } = avatarInitialsAndColor(name);
|
||||
|
||||
if (type === 'd') {
|
||||
return (
|
||||
<Avatar text={name} baseUrl={baseUrl} size={40} />
|
||||
);
|
||||
}
|
||||
const { color } = avatarInitialsAndColor(name);
|
||||
|
||||
return (
|
||||
<View style={[styles.iconContainer, { backgroundColor: color }]}>
|
||||
|
@ -120,24 +119,13 @@ export default class RoomItem extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { unread, name, _updatedAt } = this.props;
|
||||
if (_updatedAt) {
|
||||
return (
|
||||
<TouchableOpacity onPress={this.props.onPress} style={styles.container}>
|
||||
{this.icon}
|
||||
<View style={styles.roomNameView}>
|
||||
<Text style={styles.roomName} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
|
||||
<Text style={styles.update} ellipsizeMode='tail' numberOfLines={1}>{ moment(_updatedAt).format(this.props.dateFormat) }</Text>
|
||||
</View>
|
||||
{this.renderNumber(unread)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={this.props.onPress} style={styles.container}>
|
||||
{this.icon}
|
||||
<View style={styles.roomNameView}>
|
||||
<Text style={styles.roomName} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
|
||||
{_updatedAt ? <Text style={styles.update} ellipsizeMode='tail' numberOfLines={1}>{ moment(_updatedAt).format(this.props.dateFormat) }</Text> : null}
|
||||
</View>
|
||||
{this.renderNumber(unread)}
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -4,6 +4,8 @@ import * as actions from '../actions';
|
|||
import { setServer } from '../actions/server';
|
||||
import { restoreToken } from '../actions/login';
|
||||
import { APP } from '../actions/actionsTypes';
|
||||
import realm from '../lib/realm';
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
|
||||
const restore = function* restore() {
|
||||
try {
|
||||
|
@ -16,6 +18,8 @@ const restore = function* restore() {
|
|||
const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer');
|
||||
if (currentServer) {
|
||||
yield put(setServer(currentServer));
|
||||
const tmp = realm.objects('settings');
|
||||
yield put(actions.setAllSettings(RocketChat.parseSettings(tmp.slice(0, tmp.length))));
|
||||
}
|
||||
yield put(actions.appReady({}));
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AsyncStorage } from 'react-native';
|
||||
import { take, put, call, takeEvery, takeLatest, select, all } from 'redux-saga/effects';
|
||||
import { take, put, call, takeLatest, select, all } from 'redux-saga/effects';
|
||||
import * as types from '../actions/actionsTypes';
|
||||
import {
|
||||
loginRequest,
|
||||
|
@ -9,7 +9,6 @@ import {
|
|||
loginSuccess,
|
||||
loginFailure,
|
||||
setToken,
|
||||
logout,
|
||||
registerSuccess,
|
||||
setUsernameRequest,
|
||||
setUsernameSuccess,
|
||||
|
@ -83,11 +82,7 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) {
|
|||
|
||||
yield put(loginSuccess(user));
|
||||
} catch (err) {
|
||||
if (err.error === 403) {
|
||||
yield put(logout());
|
||||
} else {
|
||||
yield put(loginFailure(err));
|
||||
}
|
||||
yield put(loginFailure(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -147,7 +142,7 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai
|
|||
};
|
||||
|
||||
const root = function* root() {
|
||||
yield takeEvery(types.SERVER.CHANGED, handleLoginWhenServerChanges);
|
||||
yield takeLatest(types.SERVER.CHANGED, handleLoginWhenServerChanges);
|
||||
yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
|
||||
yield takeLatest(types.LOGIN.SUCCESS, saveToken);
|
||||
yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
// import Spinner from 'react-native-loading-spinner-overlay';
|
||||
import Spinner from 'react-native-loading-spinner-overlay';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Keyboard, Text, TextInput, View, TouchableOpacity, SafeAreaView } from 'react-native';
|
||||
|
@ -143,6 +143,7 @@ class LoginView extends React.Component {
|
|||
</TouchableOpacity>
|
||||
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
|
||||
</View>
|
||||
<Spinner visible={this.props.login.isFetching} textContent='Loading...' textStyle={{ color: '#FFF' }} />
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</KeyboardView>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, View, StyleSheet, Button, SafeAreaView } from 'react-native';
|
||||
import { Text, View, StyleSheet, Button, SafeAreaView, Dimensions } from 'react-native';
|
||||
import { ListView } from 'realm/react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
@ -38,7 +38,7 @@ const styles = StyleSheet.create({
|
|||
textAlign: 'center',
|
||||
color: '#a00'
|
||||
},
|
||||
header: {
|
||||
loadingMore: {
|
||||
transform: [{ scaleY: -1 }],
|
||||
textAlign: 'center',
|
||||
padding: 5,
|
||||
|
@ -86,7 +86,7 @@ export default class RoomView extends React.Component {
|
|||
.sorted('ts', true);
|
||||
this.state = {
|
||||
slow: false,
|
||||
dataSource: [],
|
||||
dataSource: ds.cloneWithRows([]),
|
||||
loaded: true,
|
||||
joined: typeof props.rid === 'undefined'
|
||||
};
|
||||
|
@ -102,10 +102,9 @@ export default class RoomView extends React.Component {
|
|||
this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
|
||||
this.props.getMessages(this.rid);
|
||||
this.data.addListener(this.updateState);
|
||||
this.state.dataSource = ds.cloneWithRows(this.data);
|
||||
}
|
||||
componentDidMount() {
|
||||
|
||||
this.updateState();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
return !this.props.loading && clearTimeout(this.timer);
|
||||
|
@ -124,14 +123,12 @@ export default class RoomView extends React.Component {
|
|||
this.state.end !== true
|
||||
) {
|
||||
this.setState({
|
||||
// ...this.state,
|
||||
loadingMore: true
|
||||
});
|
||||
|
||||
const lastRowData = this.data[rowCount - 1];
|
||||
RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => {
|
||||
this.setState({
|
||||
// ...this.state,
|
||||
loadingMore: false,
|
||||
end
|
||||
});
|
||||
|
@ -186,15 +183,16 @@ export default class RoomView extends React.Component {
|
|||
|
||||
renderHeader = () => {
|
||||
if (this.state.loadingMore) {
|
||||
return <Text style={styles.header}>Loading more messages...</Text>;
|
||||
return <Text style={styles.loadingMore}>Loading more messages...</Text>;
|
||||
}
|
||||
|
||||
if (this.state.end) {
|
||||
return <Text style={styles.header}>Start of conversation</Text>;
|
||||
return <Text style={styles.loadingMore}>Start of conversation</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { height } = Dimensions.get('window');
|
||||
return (
|
||||
<KeyboardView contentContainerStyle={styles.container} keyboardVerticalOffset={64}>
|
||||
{this.renderBanner()}
|
||||
|
@ -202,7 +200,7 @@ export default class RoomView extends React.Component {
|
|||
<ListView
|
||||
enableEmptySections
|
||||
style={styles.list}
|
||||
onEndReachedThreshold={10}
|
||||
onEndReachedThreshold={height / 2}
|
||||
renderFooter={this.renderHeader}
|
||||
onEndReached={this.onEndReached}
|
||||
dataSource={this.state.dataSource}
|
||||
|
|
|
@ -231,7 +231,7 @@ export default class RoomsListView extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
navigateToRoom({ sid: id });
|
||||
navigateToRoom({ sid: id, ...item });
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-remove-console": "^6.8.5",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"moment": "^2.19.2",
|
||||
"prop-types": "^15.6.0",
|
||||
|
|
Loading…
Reference in New Issue