[WIP] remove meteor lib (#146)

* removed meteor lib

* reconnect saga

* Focused text input touch bug fixed
This commit is contained in:
Guilherme Gazzo 2017-12-20 17:20:06 -02:00 committed by GitHub
parent a15774c4ff
commit 8599d6a7cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1420 additions and 1415 deletions

View File

@ -75,7 +75,7 @@ export const SERVER = createRequestTypes('SERVER', [
'ADD',
'GOTO_ADD'
]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']);
export const LOGOUT = 'LOGOUT'; // logout is always success
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST']);

View File

@ -25,3 +25,8 @@ export function disconnect(err) {
err
};
}
export function disconnect_by_user() {
return {
type: types.METEOR.DISCONNECT_BY_USER
};
}

View File

@ -41,10 +41,9 @@ export default class Routes extends React.Component {
return (<Loading />);
}
if (login.token && !login.failure && !login.isRegistering) {
return (<AuthRoutes ref={nav => this.navigator = nav} />);
}
if (!login.token || login.isRegistering) {
return (<PublicRoutes ref={nav => this.navigator = nav} />);
}
return (<AuthRoutes ref={nav => this.navigator = nav} />);
}
}

View File

@ -6,6 +6,7 @@ import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView';
import CreateChannelView from '../../views/CreateChannelView';
import SelectUsersView from '../../views/SelectUsersView';
import NewServerView from '../../views/NewServerView';
const AuthRoutes = StackNavigator(
{
@ -26,6 +27,12 @@ const AuthRoutes = StackNavigator(
navigationOptions: {
title: 'Select Users'
}
},
AddServer: {
screen: NewServerView,
navigationOptions: {
title: 'New server'
}
}
},
{

126
app/lib/ddp.js Normal file
View File

@ -0,0 +1,126 @@
import EJSON from 'ejson';
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
this.events[event].push(listener);
}
removeListener(event, listener) {
if (typeof this.events[event] === 'object') {
const idx = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
}
}
}
emit(event, ...args) {
if (typeof this.events[event] === 'object') {
this.events[event].forEach((listener) => {
try {
listener.apply(this, args);
} catch (e) {
console.log(e);
}
});
}
}
once(event, listener) {
this.on(event, function g(...args) {
this.removeListener(event, g);
listener.apply(this, args);
});
}
}
export default class Socket extends EventEmitter {
constructor(url) {
super();
this.url = url.replace(/^http/, 'ws');
this.id = 0;
this.subscriptions = {};
this._connect();
this.ddp = new EventEmitter();
this.on('ping', () => this.send({ msg: 'pong' }));
this.on('result', data => this.ddp.emit(data.id, { result: data.result, error: data.error }));
this.on('ready', data => this.ddp.emit(data.subs[0], data));
}
send(obj) {
return new Promise((resolve, reject) => {
this.id += 1;
const id = obj.id || `${ this.id }`;
this.connection.send(EJSON.stringify({ ...obj, id }));
this.ddp.once(id, data => (data.error ? reject(data.error) : resolve(data.result || data.subs)));
});
}
_connect() {
const connection = new WebSocket(`${ this.url }/websocket`);
connection.onopen = () => {
this.emit('open');
this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] });
};
connection.onclose = e => this.emit('disconnected', e);
// connection.onerror = () => {
// // alert(error.type);
// // console.log(error);
// // console.log(`WebSocket Error ${ JSON.stringify({...error}) }`);
// };
connection.onmessage = (e) => {
const data = EJSON.parse(e.data);
this.emit(data.msg, data);
return data.collection && this.emit(data.collection, data);
};
// this.on('disconnected', e => alert(JSON.stringify(e)));
this.connection = connection;
}
logout() {
return this.call('logout').then(() => this.subscriptions = {});
}
disconnect() {
this.emit('disconnected_by_user');
this.connection.close();
}
reconnect() {
this.disconnect();
this.once('connected', () => {
Object.keys(this.subscriptions).forEach((key) => {
const { name, params } = this.subscriptions[key];
this.subscriptions[key].unsubscribe();
this.subscribe(name, params);
});
});
this._connect();
}
call(method, ...params) {
return this.send({
msg: 'method', method, params
});
}
unsubscribe(id) {
if (!this.subscriptions[id]) {
return Promise.reject();
}
delete this.subscriptions[id];
return this.send({
msg: 'unsub',
id
});
}
subscribe(name, ...params) {
return this.send({
msg: 'sub', name, params
}).then((data) => {
this.subscriptions[data.id] = {
name,
params,
unsubscribe: () => this.unsubscribe(data.id)
};
return this.subscriptions[data.id];
});
}
}

View File

@ -1,4 +1,3 @@
import Meteor from 'react-native-meteor';
import Random from 'react-native-meteor/lib/Random';
import { AsyncStorage, Platform } from 'react-native';
import { hashPassword } from 'react-native-meteor/lib/utils';
@ -11,19 +10,13 @@ import realm from './realm';
import * as actions from '../actions';
import { someoneTyping } from '../actions/room';
import { setUser } from '../actions/login';
import { disconnect, connectSuccess } from '../actions/connect';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
import { requestActiveUser } from '../actions/activeUsers';
import Ddp from './ddp';
export { Accounts } from 'react-native-meteor';
const call = (method, ...params) => new Promise((resolve, reject) => {
Meteor.call(method, ...params, (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SERVER_TIMEOUT = 30000;
@ -67,47 +60,57 @@ const RocketChat = {
activeUser[ddpMessage.id] = status;
return reduxStore.dispatch(requestActiveUser(activeUser));
},
connect(_url) {
reconnect() {
if (this.ddp) {
this.ddp.reconnect();
}
},
connect(url) {
if (this.ddp) {
this.ddp.disconnect();
}
this.ddp = new Ddp(url);
return new Promise((resolve) => {
const url = `${ _url }/websocket`;
Meteor.connect(url, { autoConnect: true, autoReconnect: true });
Meteor.ddp.on('disconnected', () => {
this.ddp.on('disconnected_by_user', () => {
reduxStore.dispatch(disconnect_by_user());
});
this.ddp.on('disconnected', () => {
reduxStore.dispatch(disconnect());
});
Meteor.ddp.on('connected', () => {
reduxStore.dispatch(connectSuccess());
resolve();
this.ddp.on('open', async() => resolve(reduxStore.dispatch(connectSuccess())));
this.ddp.on('connected', () => {
RocketChat.getSettings();
RocketChat.getPermissions();
});
Meteor.ddp.on('connected', async() => {
Meteor.ddp.on('added', (ddpMessage) => {
this.ddp.on('error', (err) => {
alert(JSON.stringify(err));
reduxStore.dispatch(connectFailure());
});
this.ddp.on('connected', () => this.ddp.subscribe('activeUsers', null, false));
this.ddp.on('users', (ddpMessage) => {
if (ddpMessage.collection === 'users') {
return RocketChat._setUser(ddpMessage);
}
});
Meteor.ddp.on('removed', (ddpMessage) => {
if (ddpMessage.collection === 'users') {
return RocketChat._setUser(ddpMessage);
}
});
Meteor.ddp.on('changed', (ddpMessage) => {
if (ddpMessage.collection === 'stream-room-messages') {
return realm.write(() => {
this.ddp.on('stream-room-messages', ddpMessage => realm.write(() => {
const message = this._buildMessage(ddpMessage.fields.args[0]);
realm.create('messages', message, true);
});
}
if (ddpMessage.collection === 'stream-notify-room') {
}));
this.ddp.on('stream-notify-room', (ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (ev !== 'typing') {
return;
}
return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
}
if (ddpMessage.collection === 'stream-notify-user') {
});
this.ddp.on('stream-notify-user', (ddpMessage) => {
const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) {
@ -124,36 +127,6 @@ const RocketChat = {
sub.roomUpdatedAt = data._updatedAt;
});
}
}
if (ddpMessage.collection === 'users') {
return RocketChat._setUser(ddpMessage);
}
});
RocketChat.getSettings();
RocketChat.getPermissions();
});
})
.catch(e => console.error(e));
},
login(params, callback) {
return new Promise((resolve, reject) => {
Meteor._startLoggingIn();
return Meteor.call('login', params, (err, result) => {
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);
}
if (typeof callback === 'function') {
callback(err, result);
}
});
});
},
@ -236,10 +209,7 @@ const RocketChat = {
loadSubscriptions(cb) {
const { server } = reduxStore.getState().server;
Meteor.call('subscriptions/get', (err, data) => {
if (err) {
console.error(err);
}
this.ddp.call('subscriptions/get').then((data) => {
if (data.length) {
realm.write(() => {
data.forEach((subscription) => {
@ -305,14 +275,7 @@ const RocketChat = {
return message;
},
loadMessagesForRoom(rid, end, cb) {
return new Promise((resolve, reject) => {
Meteor.call('loadHistory', rid, end, 20, (err, data) => {
if (err) {
if (cb) {
cb({ end: true });
}
return reject(err);
}
return this.ddp.call('loadHistory', rid, end, 20).then((data) => {
if (data && data.messages.length) {
const messages = data.messages.map(message => this._buildMessage(message));
realm.write(() => {
@ -321,16 +284,17 @@ const RocketChat = {
});
});
}
if (cb) {
if (data && data.messages.length < 20) {
cb({ end: data && data.messages.length < 20 });
}
return data.message;
}, (err) => {
if (err) {
if (cb) {
cb({ end: true });
} else {
cb({ end: false });
}
return Promise.reject(err);
}
resolve();
});
});
},
@ -484,14 +448,41 @@ const RocketChat = {
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);
Meteor.subscribe('activeUsers', null, false);
this.ddp.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false);
this.ddp.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false);
return data;
},
disconnect() {
if (!this.ddp) {
return;
}
reduxStore.dispatch(disconnect_by_user());
delete this.ddp;
return this.ddp.disconnect();
},
login(params, callback) {
return this.ddp.call('login', params).then((result) => {
if (typeof callback === 'function') {
callback(null, result);
}
return result;
}, (err) => {
if (/user not found/i.test(err.reason)) {
err.error = 1;
err.reason = 'User or Password incorrect';
err.message = 'User or Password incorrect';
}
if (typeof callback === 'function') {
callback(err, null);
}
return Promise.reject(err);
});
},
logout({ server }) {
Meteor.logout();
Meteor.disconnect();
if (this.ddp) {
this.ddp.logout();
// this.disconnect();
}
AsyncStorage.removeItem(TOKEN_KEY);
AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`);
},
@ -570,7 +561,7 @@ const RocketChat = {
return `${ room._server.id }/${ roomType }/${ room.name }?msg=${ message._id }`;
},
subscribe(...args) {
return Meteor.subscribe(...args);
return this.ddp.subscribe(...args);
},
emitTyping(room, t = true) {
const { login } = reduxStore.getState();

View File

@ -4,6 +4,7 @@ const initialState = {
connecting: false,
connected: false,
errorMessage: '',
disconnected_by_user: false,
failure: false
};
@ -12,7 +13,8 @@ export default function connect(state = initialState, action) {
case METEOR.REQUEST:
return {
...state,
connecting: true
connecting: true,
disconnected_by_user: false
};
case METEOR.SUCCESS:
return {
@ -29,6 +31,11 @@ export default function connect(state = initialState, action) {
failure: true,
errorMessage: action.err
};
case METEOR.DISCONNECT_BY_USER:
return {
...state,
disconnected_by_user: true
};
case METEOR.DISCONNECT:
return initialState;
default:

View File

@ -26,7 +26,7 @@ export default function login(state = initialState, action) {
...state,
isFetching: false,
isAuthenticated: true,
user: action.user,
user: { ...state.user, ...action.user },
token: action.user.token,
failure: false,
error: ''

View File

@ -1,27 +1,44 @@
import { put, call, takeLatest, select } from 'redux-saga/effects';
import { call, takeLatest, select, take, race } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
import { connectSuccess, connectFailure } from '../actions/connect';
const getServer = ({ server }) => server.server;
const connect = url => RocketChat.connect(url);
const test = function* test() {
try {
const server = yield select(getServer);
const response = yield call(connect, server);
yield put(connectSuccess(response));
} catch (err) {
yield put(connectFailure(err.status));
const watchConnect = function* watchConnect() {
const { disconnect } = yield race({
disconnect: take(METEOR.DISCONNECT),
disconnected_by_user: take(METEOR.DISCONNECT_BY_USER)
});
if (disconnect) {
while (true) {
const { connected } = yield race({
connected: take(METEOR.SUCCESS),
timeout: call(delay, 1000)
});
if (connected) {
return;
}
yield RocketChat.reconnect();
}
}
};
// const watchConnect = function* watchConnect() {
// };
const test = function* test() {
// try {
const server = yield select(getServer);
// const response =
yield call(connect, server);
// yield put(connectSuccess(response));
// } catch (err) {
// yield put(connectFailure(err.status));
// }
};
const root = function* root() {
yield takeLatest(METEOR.REQUEST, test);
// yield fork(watchConnect);
// yield fork(auto);
// yield take(METEOR.SUCCESS, watchConnect);
yield takeLatest(METEOR.SUCCESS, watchConnect);
};
export default root;

View File

@ -1,5 +1,5 @@
import { AsyncStorage } from 'react-native';
import { take, put, call, takeLatest, select, all } from 'redux-saga/effects';
import { put, call, takeLatest, select, all } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes';
import {
loginRequest,
@ -8,6 +8,7 @@ import {
registerIncomplete,
loginSuccess,
loginFailure,
logout,
setToken,
registerSuccess,
setUsernameRequest,
@ -40,16 +41,13 @@ const getToken = function* getToken() {
console.log('getTokenerr', e);
}
} else {
yield put(setToken());
return yield put(setToken());
}
};
const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() {
try {
yield take(types.METEOR.SUCCESS);
yield call(getToken);
const user = yield select(getUser);
const user = yield call(getToken);
if (user.token) {
yield put(loginRequest({ resume: user.token }));
}
@ -76,17 +74,19 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) {
// if user has username
if (me.username) {
user.username = me.username;
const userInfo = yield call(userInfoCall, { server, token: user.token, userId: user.id });
user.username = userInfo.user.username;
if (userInfo.user.roles) {
user.roles = userInfo.user.roles;
}
} else {
yield put(registerIncomplete());
}
yield put(loginSuccess(user));
} catch (err) {
if (err.error === 403) {
return yield put(logout());
}
yield put(loginFailure(err));
}
};
@ -149,7 +149,7 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai
};
const root = function* root() {
yield takeLatest(types.SERVER.CHANGED, handleLoginWhenServerChanges);
yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
yield takeLatest(types.LOGIN.SUCCESS, saveToken);
yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit);

View File

@ -67,7 +67,7 @@ const watchRoomOpen = function* watchRoomOpen({ room }) {
const thread = yield fork(usersTyping, { rid: room.rid });
yield take(types.ROOM.OPEN);
cancel(thread);
subscriptions.forEach(sub => sub.stop());
subscriptions.forEach(sub => sub.unsubscribe());
};
const watchuserTyping = function* watchuserTyping({ status }) {

View File

@ -1,10 +1,9 @@
import { put, takeEvery, call, takeLatest, race, take } from 'redux-saga/effects';
import { put, call, takeLatest, race, take } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { AsyncStorage } from 'react-native';
import { SERVER } from '../actions/actionsTypes';
import { connectRequest, disconnect } from '../actions/connect';
import { connectRequest, disconnect, disconnect_by_user } from '../actions/connect';
import { changedServer, serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server';
import { logout } from '../actions/login';
import RocketChat from '../lib/rocketchat';
import realm from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService';
@ -14,13 +13,13 @@ const validate = function* validate(server) {
};
const selectServer = function* selectServer({ server }) {
yield put(disconnect_by_user());
yield put(disconnect());
yield put(changedServer(server));
yield call([AsyncStorage, 'setItem'], 'currentServer', server);
yield put(connectRequest(server));
};
const validateServer = function* validateServer({ server }) {
try {
yield delay(1000);
@ -48,16 +47,14 @@ const addServer = function* addServer({ server }) {
};
const handleGotoAddServer = function* handleGotoAddServer() {
yield put(logout());
yield call(AsyncStorage.removeItem, RocketChat.TOKEN_KEY);
yield delay(1000);
yield call(NavigationService.navigate, 'AddServer');
};
const root = function* root() {
yield takeLatest(SERVER.REQUEST, validateServer);
yield takeEvery(SERVER.SELECT, selectServer);
yield takeEvery(SERVER.ADD, addServer);
yield takeEvery(SERVER.GOTO_ADD, handleGotoAddServer);
yield takeLatest(SERVER.SELECT, selectServer);
yield takeLatest(SERVER.ADD, addServer);
yield takeLatest(SERVER.GOTO_ADD, handleGotoAddServer);
};
export default root;

View File

@ -8,7 +8,6 @@ import { connect } from 'react-redux';
import { setServer } from '../actions/server';
import realm from '../lib/realm';
import Fade from '../animations/fade';
import Banner from '../containers/Banner';
const styles = StyleSheet.create({
view: {
@ -184,7 +183,6 @@ export default class ListServerView extends React.Component {
render() {
return (
<View style={styles.view}>
<Banner />
<SafeAreaView style={styles.view}>
<SectionList
style={styles.list}

View File

@ -1,52 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, TextInput, View, StyleSheet, Dimensions } from 'react-native';
import { Text, TouchableOpacity, ScrollView, TextInput } from 'react-native';
import { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server';
import KeyboardView from '../presentation/KeyboardView';
import styles from './Styles';
const styles = StyleSheet.create({
view: {
flex: 1,
flexDirection: 'column',
alignItems: 'stretch',
backgroundColor: '#fff'
},
input: {
height: 40,
borderColor: '#aaa',
margin: 20,
padding: 5,
borderWidth: 0,
backgroundColor: '#f8f8f8'
},
text: {
textAlign: 'center',
color: '#888'
},
validateText: {
position: 'absolute',
color: 'green',
textAlign: 'center',
paddingLeft: 50,
paddingRight: 50,
width: '100%'
},
validText: {
color: 'green'
},
invalidText: {
color: 'red'
},
validatingText: {
color: '#aaa'
},
spaceView: {
flexGrow: 1
}
});
@connect(state => ({
validInstance: !state.server.failure,
validInstance: !state.server.failure && !state.server.connecting,
validating: state.server.connecting
}), dispatch => ({
validateServer: url => dispatch(serverRequest(url)),
@ -83,7 +44,7 @@ export default class NewServerView extends React.Component {
this.setState({ editable: true });
this.adding = false;
}
if (this.props.validInstance && !this.props.validating) {
if (this.props.validInstance) {
this.props.navigation.goBack();
this.adding = false;
}
@ -127,7 +88,7 @@ export default class NewServerView extends React.Component {
if (this.props.validating) {
return (
<Text style={[styles.validateText, styles.validatingText]}>
Validating {this.state.url} ...
Validating {this.state.text} ...
</Text>
);
}
@ -149,14 +110,17 @@ export default class NewServerView extends React.Component {
render() {
return (
<KeyboardView
scrollEnabled={false}
contentContainerStyle={[styles.view, { height: Dimensions.get('window').height }]}
contentContainerStyle={styles.container}
keyboardVerticalOffset={128}
>
<View style={styles.spaceView} />
<ScrollView
style={styles.loginView}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
>
<TextInput
ref={ref => this.inputElement = ref}
style={styles.input}
style={styles.input_white}
onChangeText={this.onChangeText}
keyboardType='url'
autoCorrect={false}
@ -164,12 +128,19 @@ export default class NewServerView extends React.Component {
autoCapitalize='none'
autoFocus
editable={this.state.editable}
onSubmitEditing={this.submit}
placeholder={this.state.defaultServer}
underlineColorAndroid='transparent'
/>
<View style={styles.spaceView}>
<TouchableOpacity
disabled={!this.props.validInstance}
style={[styles.buttonContainer, this.props.validInstance ? null
: styles.disabledButton]}
onPress={this.submit}
>
<Text style={styles.button} accessibilityTraits='button'>Add</Text>
</TouchableOpacity>
{this.renderValidation()}
</View>
</ScrollView>
</KeyboardView>
);
}

View File

@ -15,6 +15,7 @@ import styles from './styles';
@connect(state => ({
user: state.login.user,
connected: state.meteor.connected,
baseUrl: state.settings.Site_Url
}), dispatch => ({
setSearch: searchText => dispatch(setSearch(searchText))
@ -23,6 +24,7 @@ export default class extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
connected: PropTypes.bool,
baseUrl: PropTypes.string,
setSearch: PropTypes.func
}
@ -57,7 +59,7 @@ export default class extends React.Component {
}
getUserStatus() {
return this.props.user.status || 'offline';
return (this.props.connected && this.props.user.status) || 'offline';
}
getUserStatusLabel() {

View File

@ -156,5 +156,14 @@ export default StyleSheet.create({
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around'
},
validText: {
color: 'green'
},
invalidText: {
color: 'red'
},
validatingText: {
color: '#aaa'
}
});

2189
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,16 +21,17 @@
]
},
"dependencies": {
"@storybook/react-native": "^3.2.15",
"@storybook/addons": "^3.2.18",
"@storybook/react-native": "^3.2.18",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-remove-console": "^6.8.5",
"babel-polyfill": "^6.26.0",
"ejson": "^2.1.2",
"moment": "^2.19.3",
"moment": "^2.20.1",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-emojione": "^5.0.0",
"react-native": "^0.50.4",
"react-native": "^0.51.0",
"react-native-action-button": "^2.8.3",
"react-native-actionsheet": "^2.3.0",
"react-native-animatable": "^1.2.4",
@ -59,16 +60,16 @@
"redux-immutable-state-invariant": "^2.1.0",
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.0",
"regenerator-runtime": "^0.11.0",
"regenerator-runtime": "^0.11.1",
"remote-redux-devtools": "^0.5.12",
"simple-markdown": "^0.3.1",
"snyk": "^1.41.1",
"snyk": "^1.61.1",
"strip-ansi": "^4.0.0"
},
"devDependencies": {
"@storybook/addon-storyshots": "^3.2.15",
"@storybook/addon-storyshots": "^3.2.18",
"babel-eslint": "^8.0.2",
"babel-jest": "21.2.0",
"babel-jest": "^22.0.3",
"babel-plugin-transform-react-remove-prop-types": "^0.4.10",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react-native": "4.0.0",
@ -76,12 +77,12 @@
"eslint": "^4.12.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"eslint-plugin-react-native": "^3.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "21.2.1",
"jest-cli": "^21.2.1",
"jest": "^22.0.3",
"jest-cli": "^22.0.3",
"react-dom": "^16.2.0",
"react-test-renderer": "^16.2.0"
},