From 6324f22095a017e90eadf86888efb5755ffff5eb Mon Sep 17 00:00:00 2001 From: enahum Date: Mon, 13 Mar 2017 10:51:21 -0300 Subject: [PATCH] PLT-5630 Implement push notifications and unread badge on app icon (#338) * enable ios push notifications * enable android push notifications * NOTICE for push notification and message bar * include message in noty * re-wording notices * feedback review --- NOTICE.txt | 66 ++++++++++ android/app/build.gradle | 4 + android/app/google-services.json | 42 ++++++ android/app/src/main/AndroidManifest.xml | 31 +++++ .../java/com/mattermost/MainApplication.java | 2 + android/build.gradle | 1 + android/settings.gradle | 2 + app/actions/storage/index.js | 12 +- app/actions/views/root.js | 49 ++++++- app/components/push_notification/index.js | 35 +++++ .../push_notification/push_notification.js | 123 ++++++++++++++++++ app/components/root/root.js | 30 +++-- app/components/root/root_container.js | 15 ++- app/constants/view.js | 4 +- app/initial_state.js | 4 +- app/reducers/views/index.js | 2 + app/reducers/views/notification.js | 13 ++ app/scenes/channel/channel.js | 8 +- app/scenes/channel/channel_drawer_button.js | 1 + app/scenes/load_team/load_team.js | 29 +++-- app/scenes/load_team/load_team_container.js | 6 +- app/scenes/root/root.js | 25 ++-- app/scenes/root/root_container.js | 3 +- assets/base/config.json | 3 +- assets/base/images/icon.png | Bin 0 -> 472719 bytes ios/Mattermost.xcodeproj/project.pbxproj | 60 ++++++++- .../xcschemes/Mattermost.xcscheme | 2 +- ios/Mattermost/AppDelegate.m | 29 ++++- ios/Mattermost/Info.plist | 6 +- ios/Mattermost/Mattermost.entitlements | 8 ++ package.json | 2 + service/actions/general.js | 9 +- service/actions/users.js | 9 +- service/client/client.js | 12 +- service/constants/constants.js | 5 +- service/constants/general.js | 1 + service/reducers/entities/general.js | 15 ++- 37 files changed, 600 insertions(+), 68 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 app/components/push_notification/index.js create mode 100644 app/components/push_notification/push_notification.js create mode 100644 app/reducers/views/notification.js create mode 100644 assets/base/images/icon.png create mode 100644 ios/Mattermost/Mattermost.entitlements 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 0000000000000000000000000000000000000000..8cd3500d8cf30eaf05cd34a45994ed707ea085df GIT binary patch literal 472719 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