diff --git a/app/actions/storage/index.js b/app/actions/storage/index.js
new file mode 100644
index 000000000..012dfd065
--- /dev/null
+++ b/app/actions/storage/index.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {AsyncStorage} from 'react-native';
+import {GeneralTypes, UsersTypes} from 'service/constants';
+import Client from 'service/client';
+
+export function loadStorage() {
+ return async (dispatch, getState) => {
+ try {
+ const data = JSON.parse(await AsyncStorage.getItem('storage'));
+ dispatch({type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data}, getState);
+ } catch (error) {
+ // Error loading data
+ dispatch({type: GeneralTypes.REMOVED_APP_CREDENTIALS, error}, getState);
+ }
+ };
+}
+
+export function saveStorage() {
+ return async (dispatch, getState) => {
+ try {
+ const data = {
+ token: Client.getToken(),
+ url: Client.getUrl()
+ };
+
+ await AsyncStorage.setItem('storage', JSON.stringify(data));
+ dispatch({type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data}, getState);
+ } catch (error) {
+ // Error saving data
+ dispatch({type: GeneralTypes.REMOVED_APP_CREDENTIALS, error}, getState);
+ }
+ };
+}
+
+export function removeStorage() {
+ return async (dispatch, getState) => {
+ try {
+ await AsyncStorage.removeItem('storage');
+ } catch (error) {
+ // Error removing data
+ }
+ dispatch({type: UsersTypes.RESET_LOGOUT_STATE}, getState);
+ };
+}
+
+export default {
+ loadStorage,
+ saveStorage,
+ removeStorage
+};
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index baddc7b99..7e308488b 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -1,9 +1,14 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import {batchActions} from 'redux-batched-actions';
import {NavigationTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
+import Client from 'service/client';
+import {forceLogoutIfNecessary} from 'service/actions/helpers';
+import {PreferencesTypes, TeamsTypes, UsersTypes} from 'service/constants';
+
export function goToSelectServer() {
return async (dispatch, getState) => {
dispatch({
@@ -13,3 +18,85 @@ export function goToSelectServer() {
}, getState);
};
}
+
+export function setStoreFromLocalData(data) {
+ return async (dispatch, getState) => {
+ Client.setToken(data.token);
+ Client.setUrl(data.url);
+
+ let user;
+ dispatch({type: UsersTypes.LOGIN_REQUEST}, getState);
+ try {
+ user = await Client.getMe();
+ } catch (error) {
+ forceLogoutIfNecessary(error, dispatch);
+ dispatch({type: UsersTypes.LOGIN_FAILURE, error}, getState);
+ return;
+ }
+
+ let preferences;
+ dispatch({type: PreferencesTypes.MY_PREFERENCES_REQUEST}, getState);
+ try {
+ preferences = await Client.getMyPreferences();
+ } catch (error) {
+ forceLogoutIfNecessary(error, dispatch);
+ dispatch({type: PreferencesTypes.MY_PREFERENCES_FAILURE, error}, getState);
+ return;
+ }
+
+ let teams;
+ dispatch({type: TeamsTypes.FETCH_TEAMS_REQUEST}, getState);
+ try {
+ teams = await Client.getAllTeams();
+ } catch (error) {
+ forceLogoutIfNecessary(error, dispatch);
+ dispatch({type: TeamsTypes.FETCH_TEAMS_FAILURE, error}, getState);
+ return;
+ }
+
+ // TODO: uncomment and sorround with try/catch when PLT-4167 is merged
+ // const teamMembers = Client.getMyTeamMembers();
+ const teamMembers = Object.keys(teams).map((key) => {
+ return {
+ team_id: key,
+ user_id: user.id
+ };
+ });
+
+ dispatch(batchActions([
+ {
+ type: UsersTypes.RECEIVED_ME,
+ data: user
+ },
+ {
+ type: UsersTypes.LOGIN_SUCCESS
+ },
+ {
+ type: PreferencesTypes.RECEIVED_PREFERENCES,
+ data: preferences
+ },
+ {
+ type: PreferencesTypes.MY_PREFERENCES_SUCCESS
+ },
+ {
+ type: TeamsTypes.RECEIVED_MY_TEAM_MEMBERS,
+ data: teamMembers
+ },
+ {
+ type: TeamsTypes.MY_TEAM_MEMBERS_SUCCESS
+ },
+ {
+ type: TeamsTypes.RECEIVED_ALL_TEAMS,
+ data: teams
+ },
+ {
+ type: TeamsTypes.FETCH_TEAMS_SUCCESS
+ }
+ ]), getState);
+ };
+}
+
+export default {
+ goToSelectServer,
+ setStoreFromLocalData
+};
diff --git a/app/constants/index.js b/app/constants/index.js
index e42c42e3e..76d9737d0 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -2,9 +2,11 @@
// See License.txt for license information.
import NavigationTypes from './navigation';
+import StorageTypes from './storage';
import ViewTypes from './view';
export {
NavigationTypes,
+ StorageTypes,
ViewTypes
};
diff --git a/app/constants/storage.js b/app/constants/storage.js
new file mode 100644
index 000000000..a3937ce92
--- /dev/null
+++ b/app/constants/storage.js
@@ -0,0 +1,15 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import keymirror from 'keymirror';
+
+const StorageTypes = keymirror({
+ SAVE_TO_STORAGE: null,
+ SAVE_TO_STORAGE_ERROR: null,
+ LOAD_FROM_STORAGE: null,
+ LOAD_FROM_STORAGE_ERROR: null,
+ REMOVE_FROM_STORAGE: null,
+ REMOVE_FROM_STORAGE_ERROR: null
+});
+
+export default StorageTypes;
diff --git a/app/layouts/root_layout/root_layout.js b/app/layouts/root_layout/root_layout.js
index bb77931a7..2e520ab52 100644
--- a/app/layouts/root_layout/root_layout.js
+++ b/app/layouts/root_layout/root_layout.js
@@ -2,16 +2,39 @@
// See License.txt for license information.
import React from 'react';
-
+import {AppState} from 'react-native';
import {getTranslations} from 'service/i18n';
import {IntlProvider} from 'react-intl';
export default class RootLayout extends React.Component {
static propTypes = {
children: React.PropTypes.node,
- locale: React.PropTypes.string.isRequired
+ locale: React.PropTypes.string.isRequired,
+ actions: React.PropTypes.shape({
+ setAppState: React.PropTypes.func
+ }).isRequired
};
+ constructor(props) {
+ super(props);
+
+ this.handleAppStateChange = this.handleAppStateChange.bind(this);
+
+ this.props.actions.setAppState(AppState.currentState === 'active');
+ }
+
+ componentDidMount() {
+ AppState.addEventListener('change', this.handleAppStateChange);
+ }
+
+ componentWillUnmount() {
+ AppState.removeEventListener('change', this.handleAppStateChange);
+ }
+
+ handleAppStateChange(appState) {
+ this.props.actions.setAppState(appState === 'active');
+ }
+
render() {
const locale = this.props.locale;
diff --git a/app/layouts/root_layout/root_layout_container.js b/app/layouts/root_layout/root_layout_container.js
index 192b62551..7d1f98567 100644
--- a/app/layouts/root_layout/root_layout_container.js
+++ b/app/layouts/root_layout/root_layout_container.js
@@ -1,10 +1,11 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
+import Config from 'config/index';
-import Config from 'config';
-
+import {setAppState} from 'service/actions/general';
import RootLayout from './root_layout';
function mapStateToProps(state, ownProps) {
@@ -22,4 +23,12 @@ function mapStateToProps(state, ownProps) {
};
}
-export default connect(mapStateToProps)(RootLayout);
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ setAppState
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(RootLayout);
diff --git a/app/navigation/router.js b/app/navigation/router.js
index ed110c8fd..3c692c66e 100644
--- a/app/navigation/router.js
+++ b/app/navigation/router.js
@@ -19,7 +19,7 @@ class Router extends React.Component {
actions: React.PropTypes.shape({
goBack: React.PropTypes.func
}).isRequired
- }
+ };
renderTransition = (transitionProps) => {
let title;
@@ -61,7 +61,7 @@ class Router extends React.Component {
{title}
);
- }
+ };
renderTitle = ({scene}) => {
const title = scene.route.title;
@@ -78,20 +78,20 @@ class Router extends React.Component {
/>
);
- }
+ };
renderScene = ({scene}) => {
const SceneComponent = getComponentForScene(scene.route.key);
return ;
- }
+ };
configureTransition = () => {
return {
duration: 500,
easing: Easing.inOut(Easing.ease)
};
- }
+ };
render = () => {
return (
@@ -102,7 +102,7 @@ class Router extends React.Component {
configureTransition={this.configureTransition}
/>
);
- }
+ };
}
function mapStateToProps(state) {
diff --git a/app/reducers/index.js b/app/reducers/index.js
index bb4b6c92a..c4374e89c 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -1,10 +1,10 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import views from './views';
import navigation from './navigation';
+import views from './views';
export default {
- views,
- navigation
+ navigation,
+ views
};
diff --git a/app/reducers/views/i18n.js b/app/reducers/views/i18n.js
index fe8c1e918..da7628b29 100644
--- a/app/reducers/views/i18n.js
+++ b/app/reducers/views/i18n.js
@@ -3,7 +3,7 @@
import {combineReducers} from 'redux';
-import Config from 'config';
+import Config from 'config/index';
import {UsersTypes} from 'service/constants';
function locale(state = Config.DefaultLocale, action) {
diff --git a/app/reducers/views/select_server.js b/app/reducers/views/select_server.js
index 6c6b6af85..5f5bd0ff5 100644
--- a/app/reducers/views/select_server.js
+++ b/app/reducers/views/select_server.js
@@ -3,7 +3,7 @@
import {combineReducers} from 'redux';
-import Config from 'config';
+import Config from 'config/index';
import {ViewTypes} from 'app/constants';
diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js
index d909f33a7..20143cf70 100644
--- a/app/scenes/channel/channel.js
+++ b/app/scenes/channel/channel.js
@@ -14,7 +14,7 @@ import {StatusBar, Text, TouchableHighlight, View} from 'react-native';
export default class Channel extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired,
- currentTeam: React.PropTypes.object.isRequired,
+ currentTeam: React.PropTypes.object,
currentChannel: React.PropTypes.object,
channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
theme: React.PropTypes.object.isRequired
@@ -36,26 +36,26 @@ export default class Channel extends React.Component {
}
componentWillReceiveProps(nextProps) {
- if (this.props.currentTeam.id !== nextProps.currentTeam.id) {
+ if (this.props.currentTeam && nextProps.currentTeam && this.props.currentTeam.id !== nextProps.currentTeam.id) {
this.props.actions.fetchMyChannelsAndMembers(nextProps.currentTeam.id);
}
}
openLeftSidebar = () => {
this.setState({leftSidebarOpen: true});
- }
+ };
closeLeftSidebar = () => {
this.setState({leftSidebarOpen: false});
- }
+ };
openRightSidebar = () => {
this.setState({rightSidebarOpen: true});
- }
+ };
closeRightSidebar = () => {
this.setState({rightSidebarOpen: false});
- }
+ };
render() {
const {
diff --git a/app/scenes/login/login.js b/app/scenes/login/login.js
index 1abfd55ec..9ea681e22 100644
--- a/app/scenes/login/login.js
+++ b/app/scenes/login/login.js
@@ -35,7 +35,7 @@ class Login extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
- this.props.actions.goToSelectTeam();
+ this.props.actions.saveStorage().then(this.props.actions.goToSelectTeam);
}
}
diff --git a/app/scenes/login/login_container.js b/app/scenes/login/login_container.js
index 68d0c147d..d22434e85 100644
--- a/app/scenes/login/login_container.js
+++ b/app/scenes/login/login_container.js
@@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {getClientConfig, getLicenseConfig} from 'service/actions/general';
import * as LoginActions from 'app/actions/views/login';
+import * as StorageActions from 'app/actions/storage';
import {goToSelectTeam} from 'app/actions/navigation';
import {login} from 'service/actions/users';
@@ -26,6 +27,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...LoginActions,
+ ...StorageActions,
login,
getClientConfig,
getLicenseConfig,
diff --git a/app/scenes/root/root.js b/app/scenes/root/root.js
index afb78760f..3e334cd6d 100644
--- a/app/scenes/root/root.js
+++ b/app/scenes/root/root.js
@@ -2,19 +2,47 @@
// See License.txt for license information.
import React from 'react';
+import Loading from 'app/components/loading';
+
+import {RequestStatus} from 'service/constants';
export default class Root extends React.Component {
static propTypes = {
- actions: React.PropTypes.object.isRequired
+ credentials: React.PropTypes.object,
+ logoutRequest: React.PropTypes.object,
+ actions: React.PropTypes.shape({
+ goToSelectServer: React.PropTypes.func,
+ goToSelectTeam: React.PropTypes.func,
+ loadStorage: React.PropTypes.func,
+ removeStorage: React.PropTypes.func,
+ setStoreFromLocalData: React.PropTypes.func,
+ resetLogout: React.PropTypes.func
+ }).isRequired
};
componentDidMount() {
// Any initialization logic for navigation, setting up the client, etc should go here
- this.props.actions.goToSelectServer();
+ if (this.props.logoutRequest.status === RequestStatus.SUCCESS) {
+ this.props.actions.removeStorage().then(() => {
+ setTimeout(this.loadStoreAndScene, 1000);
+ });
+ } else {
+ this.loadStoreAndScene();
+ }
}
+ loadStoreAndScene = () => {
+ this.props.actions.loadStorage().then(() => {
+ if (this.props.credentials.token && this.props.credentials.url) {
+ this.props.actions.setStoreFromLocalData(this.props.credentials).then(this.props.actions.goToSelectTeam);
+ } else {
+ this.props.actions.goToSelectServer();
+ }
+ });
+ };
+
render() {
- return null;
+ return ;
}
}
diff --git a/app/scenes/root/root_container.js b/app/scenes/root/root_container.js
index 15e78b260..a62997435 100644
--- a/app/scenes/root/root_container.js
+++ b/app/scenes/root/root_container.js
@@ -4,18 +4,30 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {goToSelectServer} from 'app/actions/views/root';
+import {loadStorage, removeStorage} from 'app/actions/storage';
+import {goToSelectServer, setStoreFromLocalData} from 'app/actions/views/root';
+import {goToSelectTeam} from 'app/actions/navigation';
+import {resetLogout} from 'service/actions/users';
import Root from './root';
function mapStateToProps(state, ownProps) {
- return ownProps;
+ return {
+ ...ownProps,
+ credentials: state.entities.general.credentials,
+ logoutRequest: state.requests.users.logout
+ };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
- goToSelectServer
+ goToSelectServer,
+ goToSelectTeam,
+ loadStorage,
+ removeStorage,
+ setStoreFromLocalData,
+ resetLogout
}, dispatch)
};
}
diff --git a/service/actions/general.js b/service/actions/general.js
index da753467f..0b2459c8d 100644
--- a/service/actions/general.js
+++ b/service/actions/general.js
@@ -42,3 +42,17 @@ export function logClientError(message, level = 'ERROR') {
level
);
}
+
+export function setAppState(state) {
+ return async (dispatch, getState) => {
+ dispatch({type: GeneralTypes.RECEIVED_APP_STATE, data: state}, getState);
+ };
+}
+
+export default {
+ getPing,
+ getClientConfig,
+ getLicenseConfig,
+ logClientError,
+ setAppState
+};
diff --git a/service/actions/helpers.js b/service/actions/helpers.js
index d9eca41e7..2888bb3c2 100644
--- a/service/actions/helpers.js
+++ b/service/actions/helpers.js
@@ -7,6 +7,7 @@ const HTTP_UNAUTHORIZED = 401;
export async function forceLogoutIfNecessary(err, dispatch) {
if (err.status_code === HTTP_UNAUTHORIZED && err.url.indexOf('/login') === -1) {
+ dispatch({type: UsersTypes.LOGOUT_REQUEST});
await Client.logout();
dispatch({type: UsersTypes.LOGOUT_SUCCESS});
}
diff --git a/service/client/client.js b/service/client/client.js
index 888abbed8..f4260502e 100644
--- a/service/client/client.js
+++ b/service/client/client.js
@@ -22,10 +22,22 @@ export default class Client {
};
}
+ getUrl() {
+ return this.url;
+ }
+
setUrl(url) {
this.url = url;
}
+ getToken() {
+ return this.token;
+ }
+
+ setToken(token) {
+ this.token = token;
+ }
+
getBaseRoute() {
return `${this.url}${this.urlVersion}`;
}
@@ -192,6 +204,13 @@ export default class Client {
if (response.headers.has(HEADER_TOKEN)) {
this.token = response.headers.get(HEADER_TOKEN);
+ } else {
+ // weird case where fetch does not parse the header correctly
+ const parseHeader = response.headers.get(HEADER_CONTENT_TYPE).split('\n');
+ const filter = parseHeader.filter((h) => h.indexOf('Token:') > -1);
+ if (filter.length) {
+ this.token = filter[0].replace('Token: ', '');
+ }
}
return data;
@@ -242,6 +261,13 @@ export default class Client {
);
};
+ getMe = async () => {
+ return this.doFetch(
+ `${this.getUsersRoute()}/me`,
+ {method: 'get'}
+ );
+ };
+
getProfiles = async (offset, limit) => {
return this.doFetch(
`${this.getUsersRoute()}/${offset}/${limit}`,
diff --git a/service/constants/general.js b/service/constants/general.js
index f25af4346..e5efd3086 100644
--- a/service/constants/general.js
+++ b/service/constants/general.js
@@ -4,6 +4,10 @@
import keymirror from 'keymirror';
const GeneralTypes = keymirror({
+ RECEIVED_APP_STATE: null,
+ RECEIVED_APP_CREDENTIALS: null,
+ REMOVED_APP_CREDENTIALS: null,
+
PING_REQUEST: null,
PING_SUCCESS: null,
PING_FAILURE: null,
diff --git a/service/constants/users.js b/service/constants/users.js
index c46425e15..0de62d572 100644
--- a/service/constants/users.js
+++ b/service/constants/users.js
@@ -54,7 +54,8 @@ const UserTypes = keymirror({
RECEIVED_SESSIONS: null,
RECEIVED_REVOKED_SESSION: null,
RECEIVED_AUDITS: null,
- RECEIVED_STATUSES: null
+ RECEIVED_STATUSES: null,
+ RESET_LOGOUT_STATE: null
});
export default UserTypes;
diff --git a/service/reducers/entities/general.js b/service/reducers/entities/general.js
index 750e23885..b564b47f2 100644
--- a/service/reducers/entities/general.js
+++ b/service/reducers/entities/general.js
@@ -26,7 +26,31 @@ function license(state = {}, action) {
}
}
+function appState(state = false, action) {
+ switch (action.type) {
+ case GeneralTypes.RECEIVED_APP_STATE:
+ return action.data;
+
+ default:
+ return state;
+ }
+}
+
+function credentials(state = {}, action) {
+ switch (action.type) {
+ case GeneralTypes.RECEIVED_APP_CREDENTIALS:
+ return Object.assign({}, state, action.data);
+
+ case UsersTypes.LOGOUT_SUCCESS:
+ return {};
+ default:
+ return state;
+ }
+}
+
export default combineReducers({
+ appState,
+ credentials,
config,
license
});
diff --git a/service/reducers/requests/helpers.js b/service/reducers/requests/helpers.js
index ed0c3629d..e174f3a1e 100644
--- a/service/reducers/requests/helpers.js
+++ b/service/reducers/requests/helpers.js
@@ -23,13 +23,18 @@ export function handleRequest(REQUEST, SUCCESS, FAILURE, state, action) {
status: RequestStatus.SUCCESS,
error: null
};
- case FAILURE:
+ case FAILURE: {
+ let error = action.error;
+ if (error instanceof Error) {
+ error = error.toString();
+ }
+
return {
...state,
status: RequestStatus.FAILURE,
- error: action.error
+ error
};
-
+ }
default:
return state;
}
diff --git a/service/reducers/requests/users.js b/service/reducers/requests/users.js
index 2d68f979d..8b844984d 100644
--- a/service/reducers/requests/users.js
+++ b/service/reducers/requests/users.js
@@ -26,13 +26,22 @@ function login(state = initialRequestState(), action) {
}
function logout(state = initialRequestState(), action) {
- return handleRequest(
- UsersTypes.LOGOUT_REQUEST,
- UsersTypes.LOGOUT_SUCCESS,
- UsersTypes.LOGOUT_FAILURE,
- state,
- action
- );
+ switch (action.type) {
+ case UsersTypes.LOGOUT_REQUEST:
+ return {...state, status: RequestStatus.STARTED};
+
+ case UsersTypes.LOGOUT_SUCCESS:
+ return {...state, status: RequestStatus.SUCCESS, error: null};
+
+ case UsersTypes.LOGOUT_FAILURE:
+ return {...state, status: RequestStatus.FAILURE, error: action.error};
+
+ case UsersTypes.RESET_LOGOUT_STATE:
+ return initialRequestState();
+
+ default:
+ return state;
+ }
}
function getProfiles(state = initialRequestState(), action) {
diff --git a/test/service/actions/channels.test.js b/test/service/actions/channels.test.js
index 67128449a..354dcceff 100644
--- a/test/service/actions/channels.test.js
+++ b/test/service/actions/channels.test.js
@@ -96,7 +96,7 @@ describe('Actions.Channels', () => {
await getProfiles(0)(store.dispatch, store.getState);
Actions.createDirectChannel(TestHelper.basicUser.id, user.id)(store.dispatch, store.getState);
});
- });
+ }).timeout(3000);
it('updateChannel', (done) => {
TestHelper.initBasic(Client).then(() => {
diff --git a/test/service/actions/teams.test.js b/test/service/actions/teams.test.js
index 47ba3c495..b923b477e 100644
--- a/test/service/actions/teams.test.js
+++ b/test/service/actions/teams.test.js
@@ -317,5 +317,5 @@ describe('Actions.Teams', () => {
Actions.addUserToTeam(TestHelper.basicTeam.id, user.id)(store.dispatch, store.getState);
});
- });
+ }).timeout(3000);
});