diff --git a/NOTICE.txt b/NOTICE.txt
index 84257df96..e03f8bebd 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -735,3 +735,69 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
+
+## react-native-push-notification
+
+This product contains 'react-native-push-notification', a library to provide React Native local and remote notifications for iOS and Android.
+
+* HOMEPAGE
+ * https://github.com/zo0r/react-native-push-notification
+
+* LICENSE:
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Dieam
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+## react-native-message-bar
+
+This product contains 'react-native-message-bar', a library for displaying a message bar notification component at the top of the screen for React Native (Android and iOS) projects.
+
+* HOMEPAGE
+ * https://github.com/KBLNY/react-native-message-bar
+
+* LICENSE:
+
+The MIT License (MIT)
+
+Copyright (c) 2016 KBLNY
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3a0c7bb43..62b3a66c8 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -143,6 +143,10 @@ android {
}
dependencies {
+ compile project(':react-native-push-notification')
+ compile ('com.google.android.gms:play-services-gcm:9.0.0') {
+ force = true;
+ }
compile project(':react-native-cookies')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
diff --git a/android/app/google-services.json b/android/app/google-services.json
new file mode 100644
index 000000000..775b64e25
--- /dev/null
+++ b/android/app/google-services.json
@@ -0,0 +1,42 @@
+{
+ "project_info": {
+ "project_number": "184930218130",
+ "firebase_url": "https://api-7231322553409637977-752355.firebaseio.com",
+ "project_id": "api-7231322553409637977-752355",
+ "storage_bucket": "api-7231322553409637977-752355.appspot.com"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:184930218130:android:a0e553d1a1f043b5",
+ "android_client_info": {
+ "package_name": "com.mattermost.react.native"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
+ }
+ ],
+ "services": {
+ "analytics_service": {
+ "status": 1
+ },
+ "appinvite_service": {
+ "status": 1,
+ "other_platform_oauth_client": []
+ },
+ "ads_service": {
+ "status": 2
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 80df7330b..b937917a1 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/mattermost/MainApplication.java b/android/app/src/main/java/com/mattermost/MainApplication.java
index 762667882..f37c04780 100644
--- a/android/app/src/main/java/com/mattermost/MainApplication.java
+++ b/android/app/src/main/java/com/mattermost/MainApplication.java
@@ -5,6 +5,7 @@ import android.util.Log;
import com.facebook.react.ReactApplication;
import com.psykar.cookiemanager.CookieManagerPackage;
+import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.RNSvgPackage;
import com.facebook.react.ReactInstanceManager;
@@ -29,6 +30,7 @@ public class MainApplication extends Application implements ReactApplication {
return Arrays.asList(
new MainReactPackage(),
new CookieManagerPackage(),
+ new ReactNativePushNotificationPackage(),
new VectorIconsPackage(),
new RNSvgPackage()
);
diff --git a/android/build.gradle b/android/build.gradle
index fcba4c587..10fddbb49 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,6 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
+ classpath 'com.google.gms:google-services:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/android/settings.gradle b/android/settings.gradle
index 6e1445e11..c57483643 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1,6 +1,8 @@
rootProject.name = 'Mattermost'
include ':react-native-cookies'
project(':react-native-cookies').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-cookies/android')
+include ':react-native-push-notification'
+project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
diff --git a/app/actions/storage/index.js b/app/actions/storage/index.js
index e4d14eabd..ebd26e67a 100644
--- a/app/actions/storage/index.js
+++ b/app/actions/storage/index.js
@@ -19,11 +19,17 @@ export function loadStorage() {
const actions = [
{type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: credentials},
- {type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion},
- {type: TeamsTypes.SELECT_TEAM, data: (currentTeamId || '')},
- {type: ChannelTypes.SELECT_CHANNEL, data: currentChannelId}
+ {type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion}
];
+ if (currentTeamId) {
+ actions.push({type: TeamsTypes.SELECT_TEAM, data: currentTeamId});
+ }
+
+ if (currentChannelId) {
+ actions.push({type: ChannelTypes.SELECT_CHANNEL, data: currentChannelId});
+ }
+
// Load post drafts if there are any
if (otherStorage.postDrafts) {
Object.keys(otherStorage.postDrafts).forEach((d) => {
diff --git a/app/actions/views/root.js b/app/actions/views/root.js
index bae2bfd92..cf0bad390 100644
--- a/app/actions/views/root.js
+++ b/app/actions/views/root.js
@@ -1,11 +1,19 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import {NavigationTypes} from 'app/constants';
+import {NavigationTypes, ViewTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
+import {
+ handleSelectChannel,
+ loadChannelsIfNecessary,
+ loadProfilesAndTeamMembersForDMSidebar
+} from 'app/actions/views/channel';
+import {goToChannelView} from 'app/actions/views/load_team';
+import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {updateStorage} from 'app/actions/storage';
import Client from 'service/client';
+import {markChannelAsRead, viewChannel} from 'service/actions/channels';
import {getClientConfig, getLicenseConfig, setServerVersion} from 'service/actions/general';
import {loadMe} from 'service/actions/users';
@@ -37,8 +45,45 @@ export function loadConfigAndLicense(serverVersion) {
};
}
+export function queueNotification(notification) {
+ return async (dispatch, getState) => {
+ dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: notification}, getState);
+ };
+}
+
+export function clearNotification() {
+ return async (dispatch, getState) => {
+ dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: null}, getState);
+ };
+}
+
+export function goToNotification(notification) {
+ return async (dispatch, getState) => {
+ const {data} = notification;
+ let teamId = data.team_id;
+ if (teamId) {
+ const {teams} = getState().entities.teams;
+ await handleTeamChange(teams[teamId])(dispatch, getState);
+ loadChannelsIfNecessary(teamId)(dispatch, getState);
+ } else {
+ await selectFirstAvailableTeam()(dispatch, getState);
+ teamId = getState().entities.teams.currentId;
+ }
+
+ const channelId = data.channel_id;
+ viewChannel(teamId, channelId)(dispatch, getState);
+ loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState);
+ await handleSelectChannel(channelId)(dispatch, getState);
+ goToChannelView()(dispatch, getState);
+ markChannelAsRead(teamId, channelId)(dispatch, getState);
+ };
+}
+
export default {
goToSelectServer,
loadConfigAndLicense,
- setStoreFromLocalData
+ setStoreFromLocalData,
+ queueNotification,
+ clearNotification,
+ goToNotification
};
diff --git a/app/components/push_notification/index.js b/app/components/push_notification/index.js
new file mode 100644
index 000000000..6a44f3874
--- /dev/null
+++ b/app/components/push_notification/index.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {goToNotification, queueNotification} from 'app/actions/views/root';
+import {setDeviceToken} from 'service/actions/general';
+import {getUnreads} from 'service/selectors/entities/channels';
+
+import PushNotification from './push_notification';
+
+function mapStateToProps(state, ownProps) {
+ const {currentId: currentTeamId} = state.entities.teams;
+ const {currentId: currentChannelId} = state.entities.channels;
+
+ return {
+ ...ownProps,
+ currentTeamId,
+ currentChannelId,
+ ...getUnreads(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ goToNotification,
+ queueNotification,
+ setDeviceToken
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(PushNotification);
diff --git a/app/components/push_notification/push_notification.js b/app/components/push_notification/push_notification.js
new file mode 100644
index 000000000..48489b1f2
--- /dev/null
+++ b/app/components/push_notification/push_notification.js
@@ -0,0 +1,123 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes, PureComponent} from 'react';
+import {Platform} from 'react-native';
+import DeviceNotification from 'react-native-push-notification';
+import {MessageBar, MessageBarManager} from 'react-native-message-bar';
+
+import {changeOpacity} from 'app/utils/theme';
+import icon from 'assets/images/icon.png';
+import {GooglePlaySenderId} from 'assets/config.json';
+
+import {Constants} from 'service/constants';
+
+export default class PushNotification extends PureComponent {
+ static propTypes = {
+ currentTeamId: PropTypes.string,
+ currentChannelId: PropTypes.string,
+ actions: PropTypes.shape({
+ goToNotification: PropTypes.func.isRequired,
+ queueNotification: PropTypes.func.isRequired,
+ setDeviceToken: PropTypes.func.isRequired
+ }).isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.isConfigured = false;
+ }
+
+ componentDidMount() {
+ MessageBarManager.registerMessageBar(this.refs.notification);
+ this.configurePushNotifications();
+ }
+
+ componentWillUnmount() {
+ MessageBarManager.unregisterMessageBar();
+ DeviceNotification.unregister();
+ }
+
+ componentWillReceiveProps(props) {
+ if (this.isConfigured) {
+ DeviceNotification.setApplicationIconBadgeNumber(props.mentionCount || 0);
+ }
+ }
+
+ configurePushNotifications = () => {
+ DeviceNotification.configure({
+ onRegister: this.onRegisterDevice,
+ onNotification: this.onPushNotification,
+ senderID: GooglePlaySenderId,
+ popInitialNotification: true,
+ requestPermissions: true
+ });
+ };
+
+ onRegisterDevice = (data) => {
+ const prefix = Platform.OS === 'ios' ? Constants.PUSH_NOTIFY_APPLE_REACT_NATIVE : Constants.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
+ this.props.actions.setDeviceToken(`${prefix}:${data.token}`);
+ this.isConfigured = true;
+ };
+
+ onPushNotification = (deviceNotification) => {
+ const {foreground, userInteraction, data, message} = deviceNotification;
+ let notification;
+
+ if (Platform.OS === 'android') {
+ notification = {
+ data: {
+ channel_id: deviceNotification.channel_id,
+ team_id: deviceNotification.team_id
+ },
+ message
+ };
+ } else {
+ notification = {
+ data,
+ message
+ };
+ }
+
+ if (foreground) {
+ this.handleInAppNotification(notification);
+ } else if (userInteraction) {
+ this.onNotificationTapped(notification);
+ }
+ };
+
+ onNotificationTapped = (notification) => {
+ const {currentTeamId, currentChannelId} = this.props;
+
+ if (currentTeamId && currentChannelId) {
+ // this means that the store has the necessary data
+ this.props.actions.goToNotification(notification);
+ } else {
+ this.props.actions.queueNotification(notification);
+ }
+ };
+
+ handleInAppNotification = (notification) => {
+ const {message} = notification;
+
+ MessageBarManager.showAlert({
+ alertType: 'info',
+ avatar: icon,
+ avatarStyle: {borderRadius: 10, width: 20, height: 20},
+ message,
+ stylesheetInfo: {backgroundColor: changeOpacity('#000', 0.9)},
+ messageStyle: {color: 'white', fontSize: 13},
+ viewTopInset: 15,
+ viewBottomInset: 15,
+ duration: 5000,
+ onTapped: () => this.onNotificationTapped(notification)
+ });
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/components/root/root.js b/app/components/root/root.js
index 4f3995f31..ce719c179 100644
--- a/app/components/root/root.js
+++ b/app/components/root/root.js
@@ -1,20 +1,25 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import React from 'react';
-import {AppState} from 'react-native';
+import React, {PropTypes, PureComponent} from 'react';
+import {AppState, View} from 'react-native';
import {IntlProvider} from 'react-intl';
+import PushNotification from 'app/components/push_notification';
+
import {Constants} from 'service/constants';
import {getTranslations} from 'service/i18n';
import EventEmitter from 'service/utils/event_emitter';
-export default class Root extends React.Component {
+export default class Root extends PureComponent {
static propTypes = {
- children: React.PropTypes.node,
- locale: React.PropTypes.string.isRequired,
- actions: React.PropTypes.shape({
- loadConfigAndLicense: React.PropTypes.func.isRequired,
- setAppState: React.PropTypes.func.isRequired
+ children: PropTypes.node,
+ currentTeamId: PropTypes.string,
+ currentChannelId: PropTypes.string,
+ locale: PropTypes.string.isRequired,
+ actions: PropTypes.shape({
+ loadConfigAndLicense: PropTypes.func.isRequired,
+ setAppState: PropTypes.func.isRequired,
+ flushToStorage: PropTypes.func.isRequired
}).isRequired
};
@@ -38,6 +43,10 @@ export default class Root extends React.Component {
handleAppStateChange(appState) {
this.props.actions.setAppState(appState === 'active');
+
+ if (appState === 'inactive') {
+ this.props.actions.flushToStorage();
+ }
}
handleConfigChanged = (serverVersion) => {
@@ -52,7 +61,10 @@ export default class Root extends React.Component {
locale={locale}
messages={getTranslations(locale)}
>
- {this.props.children}
+
+ {this.props.children}
+
+
);
}
diff --git a/app/components/root/root_container.js b/app/components/root/root_container.js
index 3e17153af..e9b08cbdb 100644
--- a/app/components/root/root_container.js
+++ b/app/components/root/root_container.js
@@ -6,14 +6,17 @@ import {connect} from 'react-redux';
import Config from 'assets/config.json';
-import {loadConfigAndLicense} from 'app/actions/views/root';
-import {setAppState} from 'service/actions/general';
+import {flushToStorage} from 'app/actions/storage';
+import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root';
+import {setAppState, setDeviceToken} from 'service/actions/general';
import Root from './root';
function mapStateToProps(state, ownProps) {
const users = state.entities.users;
const currentUserId = users.currentId;
+ const {currentId: currentTeamId} = state.entities.teams;
+ const {currentId: currentChannelId} = state.entities.channels;
let locale = Config.DefaultLocale;
if (currentUserId && users.profiles[currentUserId]) {
@@ -22,6 +25,8 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
+ currentTeamId,
+ currentChannelId,
locale
};
}
@@ -30,7 +35,11 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadConfigAndLicense,
- setAppState
+ goToNotification,
+ queueNotification,
+ setAppState,
+ setDeviceToken,
+ flushToStorage
}, dispatch)
};
}
diff --git a/app/constants/view.js b/app/constants/view.js
index d206ba46c..b302fb62b 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -12,7 +12,9 @@ const ViewTypes = keyMirror({
POST_DRAFT_CHANGED: null,
COMMENT_DRAFT_CHANGED: null,
- OPTIONS_MODAL_CHANGED: null
+ OPTIONS_MODAL_CHANGED: null,
+
+ NOTIFICATION_CHANGED: null
});
export default ViewTypes;
diff --git a/app/initial_state.js b/app/initial_state.js
index bc88f1e9a..8fc99245e 100644
--- a/app/initial_state.js
+++ b/app/initial_state.js
@@ -11,7 +11,8 @@ const state = {
appState: false,
credentials: {},
config: {},
- license: {}
+ license: {},
+ deviceToken: ''
},
users: {
currentId: '',
@@ -281,6 +282,7 @@ const state = {
loginId: '',
password: ''
},
+ notification: null,
optionsModal: {
title: '',
options: [],
diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js
index f1ef9724a..986129e99 100644
--- a/app/reducers/views/index.js
+++ b/app/reducers/views/index.js
@@ -6,6 +6,7 @@ import {combineReducers} from 'redux';
import channel from './channel';
import i18n from './i18n';
import login from './login';
+import notification from './notification';
import optionsModal from './options_modal';
import selectServer from './select_server';
import thread from './thread';
@@ -14,6 +15,7 @@ export default combineReducers({
channel,
i18n,
login,
+ notification,
optionsModal,
selectServer,
thread
diff --git a/app/reducers/views/notification.js b/app/reducers/views/notification.js
new file mode 100644
index 000000000..03d73a563
--- /dev/null
+++ b/app/reducers/views/notification.js
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {ViewTypes} from 'app/constants';
+
+export default function notification(state = null, action) {
+ switch (action.type) {
+ case ViewTypes.NOTIFICATION_CHANGED:
+ return action.data;
+ default:
+ return state;
+ }
+}
diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js
index ab752fc9c..a75f78f07 100644
--- a/app/scenes/channel/channel.js
+++ b/app/scenes/channel/channel.js
@@ -62,9 +62,11 @@ export default class Channel extends React.PureComponent {
this.props.subscribeToHeaderEvent('open_right_menu', this.openRightMenuDrawer);
this.props.subscribeToHeaderEvent('show_channel_info', this.props.actions.goToChannelInfo);
EventEmitter.on('leave_team', this.handleLeaveTeam);
- const teamId = this.props.currentTeam.id;
this.props.actions.initWebSocket(Platform.OS);
- this.loadChannels(teamId);
+ if (this.props.currentTeam) {
+ const teamId = this.props.currentTeam.id;
+ this.loadChannels(teamId);
+ }
}
componentDidMount() {
@@ -72,7 +74,7 @@ export default class Channel extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
- if (nextProps.currentTeam && this.props.currentTeam.id !== nextProps.currentTeam.id) {
+ if (this.props.currentTeam && nextProps.currentTeam && this.props.currentTeam.id !== nextProps.currentTeam.id) {
const teamId = nextProps.currentTeam.id;
this.loadChannels(teamId);
}
diff --git a/app/scenes/channel/channel_drawer_button.js b/app/scenes/channel/channel_drawer_button.js
index 83297d11f..757e794eb 100644
--- a/app/scenes/channel/channel_drawer_button.js
+++ b/app/scenes/channel/channel_drawer_button.js
@@ -16,6 +16,7 @@ import {getTheme} from 'service/selectors/entities/preferences';
function ChannelDrawerButton(props) {
let badge;
let badgeCount = props.mentionCount;
+
if (!badgeCount && props.messageCount) {
badgeCount = -1;
}
diff --git a/app/scenes/load_team/load_team.js b/app/scenes/load_team/load_team.js
index 4ff8fb898..4d97803c4 100644
--- a/app/scenes/load_team/load_team.js
+++ b/app/scenes/load_team/load_team.js
@@ -1,28 +1,35 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import React from 'react';
+import React, {PropTypes, PureComponent} from 'react';
import Loading from 'app/components/loading';
import {RequestStatus} from 'service/constants';
-export default class LoadTeam extends React.Component {
+export default class LoadTeam extends PureComponent {
static propTypes = {
- teams: React.PropTypes.object.isRequired,
- myMembers: React.PropTypes.object.isRequired,
- teamsRequest: React.PropTypes.object.isRequired,
- currentTeam: React.PropTypes.object,
- actions: React.PropTypes.shape({
- goToChannelView: React.PropTypes.func.isRequired,
- handleTeamChange: React.PropTypes.func.isRequired
+ notification: PropTypes.object,
+ teams: PropTypes.object.isRequired,
+ myMembers: PropTypes.object.isRequired,
+ teamsRequest: PropTypes.object.isRequired,
+ currentTeam: PropTypes.object,
+ actions: PropTypes.shape({
+ clearNotification: PropTypes.func.isRequired,
+ goToChannelView: PropTypes.func.isRequired,
+ goToNotification: PropTypes.func.isRequired,
+ handleTeamChange: PropTypes.func.isRequired
}).isRequired
};
componentDidMount() {
- const {currentTeam, myMembers, teams} = this.props;
+ const {notification, currentTeam, myMembers, teams} = this.props;
+ const {clearNotification, goToNotification} = this.props.actions;
- if (currentTeam) {
+ if (notification) {
+ clearNotification();
+ goToNotification(notification);
+ } else if (currentTeam) {
this.onSelectTeam(currentTeam);
} else if (!currentTeam) {
this.selectFirstTeam(teams, myMembers);
diff --git a/app/scenes/load_team/load_team_container.js b/app/scenes/load_team/load_team_container.js
index 9c606b497..583ddd1d8 100644
--- a/app/scenes/load_team/load_team_container.js
+++ b/app/scenes/load_team/load_team_container.js
@@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToChannelView} from 'app/actions/views/load_team';
+import {clearNotification, goToNotification} from 'app/actions/views/root';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getCurrentTeam} from 'service/selectors/entities/teams';
@@ -16,14 +17,17 @@ function mapStateToProps(state) {
teamsRequest: state.requests.teams.allTeams,
teams: state.entities.teams.teams,
currentTeam: getCurrentTeam(state),
- myMembers: state.entities.teams.myMembers
+ myMembers: state.entities.teams.myMembers,
+ notification: state.views.notification
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
+ clearNotification,
goToChannelView,
+ goToNotification,
handleTeamChange
}, dispatch)
};
diff --git a/app/scenes/root/root.js b/app/scenes/root/root.js
index 90284ef67..054d7c546 100644
--- a/app/scenes/root/root.js
+++ b/app/scenes/root/root.js
@@ -2,7 +2,7 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
-import {AppState, AsyncStorage} from 'react-native';
+import {AsyncStorage} from 'react-native';
import Loading from 'app/components/loading';
import {RequestStatus} from 'service/constants';
@@ -10,16 +10,15 @@ import {RequestStatus} from 'service/constants';
export default class Root extends PureComponent {
static propTypes = {
credentials: PropTypes.object,
- logoutRequest: PropTypes.object.isRequired,
- loginRequest: PropTypes.object.isRequired,
+ logoutRequest: PropTypes.object,
+ loginRequest: PropTypes.object,
actions: PropTypes.shape({
- goToLoadTeam: PropTypes.func.isRequired,
- goToSelectServer: PropTypes.func.isRequired,
+ goToLoadTeam: PropTypes.func,
+ goToSelectServer: PropTypes.func,
handleServerUrlChanged: PropTypes.func.isRequired,
- flushToStorage: PropTypes.func.isRequired,
- loadStorage: PropTypes.func.isRequired,
- removeStorage: PropTypes.func.isRequired,
- setStoreFromLocalData: PropTypes.func.isRequired
+ loadStorage: PropTypes.func,
+ removeStorage: PropTypes.func,
+ setStoreFromLocalData: PropTypes.func
}).isRequired
};
@@ -30,7 +29,6 @@ export default class Root extends PureComponent {
componentDidMount() {
// Any initialization logic for navigation, setting up the client, etc should go here
this.init();
- AppState.addEventListener('change', this.handleAppStateChange);
}
componentWillReceiveProps(nextProps) {
@@ -43,13 +41,6 @@ export default class Root extends PureComponent {
}
}
- handleAppStateChange = (event) => {
- // App gets killed, phone call comes in, app is pushed to the background, etc...
- if (event === 'inactive') {
- this.props.actions.flushToStorage();
- }
- };
-
init = () => {
if (this.props.logoutRequest.status === RequestStatus.SUCCESS) {
this.props.actions.removeStorage().then(() => {
diff --git a/app/scenes/root/root_container.js b/app/scenes/root/root_container.js
index 85061dc6a..25cb07ece 100644
--- a/app/scenes/root/root_container.js
+++ b/app/scenes/root/root_container.js
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
-import {flushToStorage, loadStorage, removeStorage} from 'app/actions/storage';
+import {loadStorage, removeStorage} from 'app/actions/storage';
import {goToSelectServer, setStoreFromLocalData} from 'app/actions/views/root';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import {goToLoadTeam} from 'app/actions/navigation';
@@ -27,7 +27,6 @@ function mapDispatchToProps(dispatch) {
goToLoadTeam,
goToSelectServer,
handleServerUrlChanged,
- flushToStorage,
loadStorage,
removeStorage,
setStoreFromLocalData
diff --git a/assets/base/config.json b/assets/base/config.json
index 571a76abe..95169a5b6 100644
--- a/assets/base/config.json
+++ b/assets/base/config.json
@@ -3,5 +3,6 @@
"TestServerUrl": "http://localhost:8065",
"DefaultLocale": "en",
"DefaultTheme": "default",
- "ShowErrorsList": false
+ "ShowErrorsList": false,
+ "GooglePlaySenderId": "184930218130"
}
diff --git a/assets/base/images/icon.png b/assets/base/images/icon.png
new file mode 100644
index 000000000..8cd3500d8
Binary files /dev/null and b/assets/base/images/icon.png differ
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index b4931e97a..8bf828b42 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -29,6 +29,7 @@
420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EDC04CBCF81642219D199CBB /* Octicons.ttf */; };
5E1AF7B72B8D4A4E9E53FF9D /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; };
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
+ 7F63D2841E6C9585001FAE12 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; };
7FBB5E9A1E1F5A4B000DE18A /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */; };
7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
@@ -230,6 +231,20 @@
remoteGlobalIDString = 1BD725DA1CF77A8B005DBD79;
remoteInfo = RNCookieManagerIOS;
};
+ 7F63D2801E6C957C001FAE12 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 134814201AA4EA6300B7C361;
+ remoteInfo = RCTPushNotification;
+ };
+ 7F63D2821E6C957C001FAE12 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 3D05745F1DE6004600184BB4;
+ remoteInfo = "RCTPushNotification-tvOS";
+ };
7FDF28E01E1F4B1F00DBBE56 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */;
@@ -285,6 +300,8 @@
59954D479A89488091EB588F /* RNSearchBar.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSearchBar.xcodeproj; path = "../node_modules/react-native-search-bar/RNSearchBar.xcodeproj"; sourceTree = ""; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; };
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; };
+ 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; };
+ 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; };
AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = ""; };
CE45EB909FB44F3597EF3C60 /* libRNCookieManagerIOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCookieManagerIOS.a; sourceTree = ""; };
@@ -319,6 +336,7 @@
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
+ 7F63D2841E6C9585001FAE12 /* libRCTPushNotification.a in Frameworks */,
B50561A109B4442299F82327 /* libRNSearchBar.a in Frameworks */,
9906AC672C594EEB9D5EE503 /* libRNCookieManagerIOS.a in Frameworks */,
);
@@ -424,6 +442,7 @@
13B07FAE1A68108700A75B9A /* Mattermost */ = {
isa = PBXGroup;
children = (
+ 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
@@ -483,6 +502,15 @@
name = Products;
sourceTree = "";
};
+ 7F63D27C1E6C957C001FAE12 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */,
+ 7F63D2831E6C957C001FAE12 /* libRCTPushNotification-tvOS.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
7FDF28C41E1F4B1F00DBBE56 /* Products */ = {
isa = PBXGroup;
children = (
@@ -502,6 +530,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
+ 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */,
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */,
146833FF1AC3E56700842450 /* React.xcodeproj */,
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */,
@@ -607,6 +636,11 @@
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = UQ8HT4Q2XM;
ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Push = {
+ enabled = 1;
+ };
+ };
};
};
};
@@ -646,6 +680,10 @@
ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */;
ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
},
+ {
+ ProductGroup = 7F63D27C1E6C957C001FAE12 /* Products */;
+ ProjectRef = 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */;
+ },
{
ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */;
ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
@@ -874,6 +912,20 @@
remoteRef = 7F906D961E6A1ADF00B8F49A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
+ 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTPushNotification.a;
+ remoteRef = 7F63D2801E6C957C001FAE12 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 7F63D2831E6C957C001FAE12 /* libRCTPushNotification-tvOS.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = "libRCTPushNotification-tvOS.a";
+ remoteRef = 7F63D2821E6C957C001FAE12 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -988,7 +1040,6 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
- DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
@@ -1011,7 +1062,6 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COPY_PHASE_STRIP = NO;
- DEVELOPMENT_TEAM = UQ8HT4Q2XM;
INFOPLIST_FILE = MattermostTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -1029,10 +1079,11 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 12;
DEAD_CODE_STRIPPING = NO;
- DEVELOPMENT_TEAM = UQ8HT4Q2XM;
+ HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**";
INFOPLIST_FILE = Mattermost/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
@@ -1052,10 +1103,11 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 12;
DEAD_CODE_STRIPPING = NO;
- DEVELOPMENT_TEAM = UQ8HT4Q2XM;
+ HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**";
INFOPLIST_FILE = Mattermost/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
diff --git a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme
index d389d9b75..bc78e5103 100644
--- a/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme
+++ b/ios/Mattermost.xcodeproj/xcshareddata/xcschemes/Mattermost.xcscheme
@@ -80,7 +80,7 @@
#import
@@ -34,4 +34,31 @@
return YES;
}
+// Required to register for notifications
+- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
+{
+ [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
+}
+// Required for the register event.
+- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
+{
+ [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
+}
+// Required for the notification event. You must call the completion handler after handling the remote notification.
+- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
+fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
+{
+ [RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
+}
+// Required for the registrationError event.
+- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
+{
+ [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error];
+}
+// Required for the localNotification event.
+- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
+{
+ [RCTPushNotificationManager didReceiveLocalNotification:notification];
+}
+
@end
diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist
index 33fa1501d..f5c7e6318 100644
--- a/ios/Mattermost/Info.plist
+++ b/ios/Mattermost/Info.plist
@@ -20,10 +20,14 @@
????
CFBundleVersion
12
+ ITSAppUsesNonExemptEncryption
+
LSRequiresIPhoneOS
NSAppTransportSecurity
+ NSAllowsArbitraryLoads
+
NSExceptionDomains
localhost
@@ -72,7 +76,5 @@
UIViewControllerBasedStatusBarAppearance
- ITSAppUsesNonExemptEncryption
-
diff --git a/ios/Mattermost/Mattermost.entitlements b/ios/Mattermost/Mattermost.entitlements
new file mode 100644
index 000000000..903def2af
--- /dev/null
+++ b/ios/Mattermost/Mattermost.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ aps-environment
+ development
+
+
diff --git a/package.json b/package.json
index a3ce02678..08b624a29 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,8 @@
"react-native-drawer": "2.3.0",
"react-native-keyboard-aware-scroll-view": "0.2.7",
"react-native-keyboard-spacer": "0.3.1",
+ "react-native-message-bar": "1.6.0",
+ "react-native-push-notification": "2.2.1",
"react-native-search-bar": "enahum/react-native-search-bar.git",
"react-native-svg": "4.5.0",
"react-native-vector-icons": "4.0.0",
diff --git a/service/actions/general.js b/service/actions/general.js
index f4c1c01ca..0b980e6d4 100644
--- a/service/actions/general.js
+++ b/service/actions/general.js
@@ -94,11 +94,18 @@ export function setServerVersion(serverVersion) {
};
}
+export function setDeviceToken(token) {
+ return async (dispatch, getState) => {
+ dispatch({type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN, data: token}, getState);
+ };
+}
+
export default {
getPing,
getClientConfig,
getLicenseConfig,
logClientError,
setAppState,
- setServerVersion
+ setServerVersion,
+ setDeviceToken
};
diff --git a/service/actions/users.js b/service/actions/users.js
index d192efeda..dea45322b 100644
--- a/service/actions/users.js
+++ b/service/actions/users.js
@@ -29,7 +29,9 @@ export function login(loginId, password, mfaToken = '') {
return async (dispatch, getState) => {
dispatch({type: UsersTypes.LOGIN_REQUEST}, getState);
- return Client.login(loginId, password, mfaToken).
+ const deviceId = getState().entities.general.deviceToken;
+
+ return Client.login(loginId, password, mfaToken, deviceId).
then(async (data) => {
let teamMembers;
let preferences;
@@ -101,6 +103,11 @@ export function loadMe() {
return;
}
+ const deviceId = getState().entities.general.deviceToken;
+ if (deviceId) {
+ Client.attachDevice(deviceId);
+ }
+
let preferences;
dispatch({type: PreferencesTypes.MY_PREFERENCES_REQUEST}, getState);
try {
diff --git a/service/client/client.js b/service/client/client.js
index 7cfdae539..4f3f92129 100644
--- a/service/client/client.js
+++ b/service/client/client.js
@@ -213,11 +213,12 @@ export default class Client {
);
};
- login = async (loginId, password, token = '') => {
+ login = async (loginId, password, token = '', deviceId = '') => {
const body = {
login_id: loginId,
password,
- token
+ token,
+ device_id: deviceId
};
const {headers, data} = await this.doFetchWithResponse(
@@ -244,6 +245,13 @@ export default class Client {
return response;
};
+ attachDevice = async (deviceId) => {
+ return this.doFetch(
+ `${this.getUsersRoute()}/attach_device`,
+ {method: 'post', body: JSON.stringify({device_id: deviceId})}
+ );
+ };
+
updateUser = async (user) => {
return this.doFetch(
`${this.getUsersRoute()}/update`,
diff --git a/service/constants/constants.js b/service/constants/constants.js
index 236953cc5..677611f87 100644
--- a/service/constants/constants.js
+++ b/service/constants/constants.js
@@ -39,7 +39,10 @@ const Constants = {
START_OF_NEW_MESSAGES: 'start-of-new-messages',
POST_HEADER_CHANGE: 'system_header_change',
- POST_PURPOSE_CHANGE: 'system_purpose_change'
+ POST_PURPOSE_CHANGE: 'system_purpose_change',
+
+ PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
+ PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn'
};
const FileConstants = {
diff --git a/service/constants/general.js b/service/constants/general.js
index de40343da..be814a2f2 100644
--- a/service/constants/general.js
+++ b/service/constants/general.js
@@ -7,6 +7,7 @@ const GeneralTypes = keyMirror({
RECEIVED_APP_STATE: null,
RECEIVED_APP_CREDENTIALS: null,
REMOVED_APP_CREDENTIALS: null,
+ RECEIVED_APP_DEVICE_TOKEN: null,
PING_REQUEST: null,
PING_SUCCESS: null,
diff --git a/service/reducers/entities/general.js b/service/reducers/entities/general.js
index aa3967d84..1c34dd62f 100644
--- a/service/reducers/entities/general.js
+++ b/service/reducers/entities/general.js
@@ -59,10 +59,23 @@ function serverVersion(state = '', action) {
}
}
+function deviceToken(state = '', action) {
+ switch (action.type) {
+ case GeneralTypes.RECEIVED_APP_DEVICE_TOKEN:
+ return action.data;
+
+ case UsersTypes.LOGOUT_SUCCESS:
+ return '';
+ default:
+ return state;
+ }
+}
+
export default combineReducers({
appState,
credentials,
config,
license,
- serverVersion
+ serverVersion,
+ deviceToken
});