From 248dafbe0e16d366e75e8f4980a7a500b1d9f9bd Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 16 May 2017 11:19:46 -0400 Subject: [PATCH] RN-97 Changed navigation from NavigationExperimental to ReactNative Navigation (#540) * Add react-navigator dependency * Add react-native-navigation dependency * react-native-navigation, ios and android config * update react-native-navigation * New navigation and code cleanup * Set ScreenBackgroundColor * Fix channel drawer event issue * Applied RNPN non-working popInitial notification * Android navbar and back button * packages and notices * MM-redux update * Fix draft on team switch * Remove custom NavBar --- NOTICE.txt | 215 ++------- android/app/build.gradle | 12 +- android/app/src/main/AndroidManifest.xml | 2 - .../java/com/mattermost/MainActivity.java | 82 ++-- .../java/com/mattermost/MainApplication.java | 64 ++- android/settings.gradle | 4 +- app/actions/navigation/index.js | 298 ------------- app/actions/views/account_notifications.js | 6 +- app/actions/views/channel.js | 13 - app/actions/views/load_team.js | 34 +- app/actions/views/options_modal.js | 30 -- app/actions/views/right_menu_drawer.js | 39 -- app/actions/views/root.js | 25 +- app/actions/views/select_server.js | 23 +- app/actions/views/user_profile.js | 2 - app/components/action_button.js | 125 ------ .../autocomplete/at_mention/at_mention.js | 1 + .../autocomplete/at_mention/index.js | 2 +- .../autocomplete/channel_mention/index.js | 2 +- .../channel_drawer/channel_drawer.js | 190 ++++++++ .../channel_drawer/index.js | 5 +- .../channel_drawer_list.js | 113 ++++- app/components/channel_drawer_list/index.js | 36 +- .../custom_list/channel_list_row.js | 25 +- .../file_attachment_list.js | 24 +- app/components/file_attachment_list/index.js | 2 - .../offline_indicator/offline_indicator.js | 14 +- app/components/post/index.js | 5 - app/components/post/post.js | 76 +++- app/components/post_list/post_list.js | 2 + app/components/post_textbox/index.js | 3 - app/components/post_textbox/post_textbox.js | 27 +- app/components/root/index.js | 35 +- app/components/root/root.js | 133 +----- app/constants/index.js | 2 - app/constants/navigation.js | 15 +- app/constants/storage.js | 15 - app/constants/view.js | 3 +- app/initial_state.js | 26 +- app/mattermost.js | 170 +++++-- app/navigation/navigation_modal.js | 172 ------- app/navigation/router.js | 360 --------------- app/navigation/routes.js | 225 ---------- app/reducers/navigation/index.js | 227 +--------- app/reducers/views/channel.js | 14 +- app/reducers/views/index.js | 2 - app/reducers/views/options_modal.js | 46 -- app/reducers/views/root.js | 16 + .../save_notifications_button.js | 95 ---- app/scenes/channel_drawer/channel_drawer.js | 73 --- .../channel_members/channel_members_title.js | 44 -- .../channel_members/remove_member_button.js | 41 -- app/scenes/create_channel/create_channel.js | 371 ---------------- app/scenes/index.js | 58 --- app/scenes/login_options/index.js | 29 -- app/scenes/more_channels/create_button.js | 54 --- app/scenes/navigationSceneConnect.js | 69 --- app/scenes/options_modal/index.js | 27 -- app/scenes/root/index.js | 37 -- app/scenes/root/root.js | 80 ---- app/scenes/search/search.js | 30 -- app/scenes/thread/thread_title.js | 66 --- app/{scenes => screens}/about/about.js | 0 app/{scenes => screens}/about/index.js | 5 +- .../account_notifications.js | 134 ++++-- .../account_notifications/index.js | 14 +- .../account_notifications/section.js | 0 .../account_notifications/section_item.js | 0 .../account_settings/account_settings.js | 47 +- app/screens/account_settings/index.js | 16 + app/{scenes => screens}/channel/channel.js | 184 +++++--- .../channel/channel_drawer_button.js | 17 +- .../channel_post_list/channel_post_list.js | 42 +- .../channel/channel_post_list/index.js | 7 +- .../channel/channel_title.js | 9 +- app/{scenes => screens}/channel/index.js | 15 +- .../channel_add_members.js | 136 +++--- .../channel_add_members/index.js | 7 +- .../channel_info/channel_info.js | 63 ++- .../channel_info/channel_info_header.js | 0 .../channel_info/channel_info_row.js | 0 app/{scenes => screens}/channel_info/index.js | 8 +- .../channel_members/channel_members.js | 172 +++---- .../channel_members/index.js | 7 +- app/screens/create_channel/create_channel.js | 410 +++++++++++++++++ .../create_channel/index.js | 8 +- .../edit_post/edit_post.js | 185 ++++---- app/{scenes => screens}/edit_post/index.js | 7 +- .../image_preview/image_preview.js | 36 +- .../image_preview/index.js | 9 +- .../image_preview/zoomable_image.js | 0 app/screens/index.js | 72 +++ app/{scenes => screens}/load_team/index.js | 14 +- .../load_team/load_team.js | 49 +- app/{scenes => screens}/login/index.js | 18 +- app/{scenes => screens}/login/login.js | 147 +++--- app/screens/login_options/index.js | 20 + .../login_options/login_options.js | 56 ++- app/{scenes => screens}/mfa/index.js | 7 +- app/{scenes => screens}/mfa/mfa.js | 26 +- .../more_channels/index.js | 26 +- .../more_channels/more_channels.js | 167 ++++--- app/{scenes => screens}/more_dms/index.js | 8 +- app/{scenes => screens}/more_dms/more_dms.js | 59 ++- .../notification}/index.js | 13 +- app/screens/notification/notification.js | 99 +++++ app/screens/options_modal/index.js | 14 + .../options_modal/options_modal.js | 35 +- .../options_modal_list.android.js | 0 .../options_modal/options_modal_list.ios.js | 0 app/{scenes/search => screens/root}/index.js | 14 +- app/screens/root/root.js | 82 ++++ app/{scenes => screens}/saml/index.js | 17 +- app/{scenes => screens}/saml/saml.js | 47 +- .../select_server/index.js | 21 +- .../select_server/select_server.js | 55 ++- app/{scenes => screens}/select_team/index.js | 12 +- .../select_team/select_team.js | 47 +- app/{scenes => screens}/settings/index.js | 18 +- app/{scenes => screens}/settings/settings.js | 118 +++-- .../settings/settings_item.js | 0 app/{scenes => screens}/thread/index.js | 10 +- app/{scenes => screens}/thread/thread.js | 25 +- app/{scenes => screens}/user_profile/index.js | 5 +- .../user_profile/user_profile.js | 11 +- .../user_profile/user_profile_row.js | 0 app/store/index.js | 7 +- index.android.js | 6 +- index.ios.js | 4 +- ios/Mattermost.xcodeproj/project.pbxproj | 65 ++- ios/Mattermost/AppDelegate.m | 30 +- package.json | 5 +- test/app/actions/options_modal.js | 33 -- test/app/reducers/navigation.test.js | 420 ------------------ yarn.lock | 24 +- 135 files changed, 2824 insertions(+), 4656 deletions(-) delete mode 100644 app/actions/navigation/index.js delete mode 100644 app/actions/views/options_modal.js delete mode 100644 app/actions/views/right_menu_drawer.js delete mode 100644 app/components/action_button.js create mode 100644 app/components/channel_drawer/channel_drawer.js rename app/{scenes => components}/channel_drawer/index.js (91%) delete mode 100644 app/constants/storage.js delete mode 100644 app/navigation/navigation_modal.js delete mode 100644 app/navigation/router.js delete mode 100644 app/navigation/routes.js delete mode 100644 app/reducers/views/options_modal.js delete mode 100644 app/scenes/account_notifications/save_notifications_button.js delete mode 100644 app/scenes/channel_drawer/channel_drawer.js delete mode 100644 app/scenes/channel_members/channel_members_title.js delete mode 100644 app/scenes/channel_members/remove_member_button.js delete mode 100644 app/scenes/create_channel/create_channel.js delete mode 100644 app/scenes/index.js delete mode 100644 app/scenes/login_options/index.js delete mode 100644 app/scenes/more_channels/create_button.js delete mode 100644 app/scenes/navigationSceneConnect.js delete mode 100644 app/scenes/options_modal/index.js delete mode 100644 app/scenes/root/index.js delete mode 100644 app/scenes/root/root.js delete mode 100644 app/scenes/search/search.js delete mode 100644 app/scenes/thread/thread_title.js rename app/{scenes => screens}/about/about.js (100%) rename app/{scenes => screens}/about/index.js (73%) rename app/{scenes => screens}/account_notifications/account_notifications.js (88%) rename app/{scenes => screens}/account_notifications/index.js (75%) rename app/{scenes => screens}/account_notifications/section.js (100%) rename app/{scenes => screens}/account_notifications/section_item.js (100%) rename app/{scenes => screens}/account_settings/account_settings.js (81%) create mode 100644 app/screens/account_settings/index.js rename app/{scenes => screens}/channel/channel.js (58%) rename app/{scenes => screens}/channel/channel_drawer_button.js (87%) rename app/{scenes => screens}/channel/channel_post_list/channel_post_list.js (80%) rename app/{scenes => screens}/channel/channel_post_list/index.js (93%) rename app/{scenes => screens}/channel/channel_title.js (89%) rename app/{scenes => screens}/channel/index.js (81%) rename app/{scenes => screens}/channel_add_members/channel_add_members.js (85%) rename app/{scenes => screens}/channel_add_members/index.js (89%) rename app/{scenes => screens}/channel_info/channel_info.js (85%) rename app/{scenes => screens}/channel_info/channel_info_header.js (100%) rename app/{scenes => screens}/channel_info/channel_info_row.js (100%) rename app/{scenes => screens}/channel_info/index.js (89%) rename app/{scenes => screens}/channel_members/channel_members.js (86%) rename app/{scenes => screens}/channel_members/index.js (88%) create mode 100644 app/screens/create_channel/create_channel.js rename app/{scenes => screens}/create_channel/index.js (74%) rename app/{scenes => screens}/edit_post/edit_post.js (70%) rename app/{scenes => screens}/edit_post/index.js (75%) rename app/{scenes => screens}/image_preview/image_preview.js (97%) rename app/{scenes => screens}/image_preview/index.js (77%) rename app/{scenes => screens}/image_preview/zoomable_image.js (100%) create mode 100644 app/screens/index.js rename app/{scenes => screens}/load_team/index.js (76%) rename app/{scenes => screens}/load_team/load_team.js (58%) rename app/{scenes => screens}/login/index.js (69%) rename app/{scenes => screens}/login/login.js (70%) create mode 100644 app/screens/login_options/index.js rename app/{scenes => screens}/login_options/login_options.js (60%) rename app/{scenes => screens}/mfa/index.js (73%) rename app/{scenes => screens}/mfa/mfa.js (88%) rename app/{scenes => screens}/more_channels/index.js (61%) rename app/{scenes => screens}/more_channels/more_channels.js (74%) rename app/{scenes => screens}/more_dms/index.js (83%) rename app/{scenes => screens}/more_dms/more_dms.js (86%) rename app/{scenes/account_settings => screens/notification}/index.js (52%) create mode 100644 app/screens/notification/notification.js create mode 100644 app/screens/options_modal/index.js rename app/{scenes => screens}/options_modal/options_modal.js (71%) rename app/{scenes => screens}/options_modal/options_modal_list.android.js (100%) rename app/{scenes => screens}/options_modal/options_modal_list.ios.js (100%) rename app/{scenes/search => screens/root}/index.js (53%) create mode 100644 app/screens/root/root.js rename app/{scenes => screens}/saml/index.js (60%) rename app/{scenes => screens}/saml/saml.js (52%) rename app/{scenes => screens}/select_server/index.js (68%) rename app/{scenes => screens}/select_server/select_server.js (78%) rename app/{scenes => screens}/select_team/index.js (80%) rename app/{scenes => screens}/select_team/select_team.js (70%) rename app/{scenes => screens}/settings/index.js (69%) rename app/{scenes => screens}/settings/settings.js (68%) rename app/{scenes => screens}/settings/settings_item.js (100%) rename app/{scenes => screens}/thread/index.js (90%) rename app/{scenes => screens}/thread/thread.js (80%) rename app/{scenes => screens}/user_profile/index.js (83%) rename app/{scenes => screens}/user_profile/user_profile.js (95%) rename app/{scenes => screens}/user_profile/user_profile_row.js (100%) delete mode 100644 test/app/actions/options_modal.js delete mode 100644 test/app/reducers/navigation.test.js diff --git a/NOTICE.txt b/NOTICE.txt index bd6ae4de6..805217502 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -8,49 +8,6 @@ This document includes a list of open source components used in Mattermost Mobil -------- -## fbjs - -This product contains a modified portion of 'fbjs', a collection of JavaScript utilities by Facebook, Inc. - -* HOMEPAGE: - * https://github.com/facebook/fbjs - -* LICENSE: - -BSD License - -For fbjs software - -Copyright (c) 2013-present, Facebook, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ---- - ## react-native-search-bar This product contains a modified portion of 'react-native-search-bar', a high-quality iOS native search bar for react native by Zhao Han. @@ -117,49 +74,6 @@ SOFTWARE. --- -## harmony-reflect - -[Note: the software referenced below is made available under two licenses. Mattermost, Inc. has elected to license the software pursuant to the Apache License, Version 2.0.] - -This product contains 'harmony-reflect', a shim for ECMAScript 6 Reflect and Proxy objects. - -* HOMEPAGE: - * https://github.com/tvcutsem/harmony-reflect - -* LICENSE: - -Copyright (C) 2011-2014 Software Languages Lab, Vrije Universiteit Brussel -Copyright (C) 2015 Tom Van Cutsem - -This code is dual-licensed under both the Apache License and the MPL - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -Version: MPL 1.1 - -The contents of this file are subject to the Mozilla Public License Version -1.1 (the "License"); you may not use this file except in compliance with -the License. You may obtain a copy of the License at - - http://www.mozilla.org/MPL/ - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the -License. - ---- - ## intl This product contains 'Intl.js', to bring long overdue localization methods to ECMAScript implementations by Andy Earnshaw. @@ -237,39 +151,6 @@ the property of their respective owners. --- -## isomorphic-fetch - -This product contains 'isomorphic-fetch', to provide the fetch function for node by Matt Andrews. - -* HOMEPAGE: - * https://github.com/matthew-andrews/isomorphic-fetch - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2015 Matt Andrews - -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 This product contains 'react', a declarative, efficient, and @@ -769,39 +650,6 @@ 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. - ---- - ## react-native-device-info This product contains 'react-native-device-info', library to provide a device information to react native on both iOS and Android. @@ -889,37 +737,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --- -## redux-action-buffer - -A middleware for redux that buffers all actions into a queue until a breaker condition is met, at which point the queue is released (i.e. actions are triggered). - -* HOMEPAGE - * https://github.com/rt2zz/redux-action-buffer - -* LICENSE - -The MIT License (MIT) - -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. - ---- - ## redux-persist Persist and rehydrate a redux store. @@ -1024,3 +841,35 @@ 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-navigation + +This product contains 'react-native-navigation', App-wide support for 100% native navigation with an easy cross-platform interface. + +* HOMEPAGE: + * https://github.com/wix/react-native-navigation + +* LICENSE: + +The MIT License (MIT) + +Copyright (c) Wix.com + +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 ab13ea5b1..b1154ee41 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -84,8 +84,8 @@ def enableSeparateBuildPerCPUArchitecture = false def enableProguardInReleaseBuilds = false android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 25 + buildToolsVersion "25.0.1" defaultConfig { applicationId "com.mattermost.react.native" @@ -144,8 +144,11 @@ android { } dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:25.0.1" + compile "com.facebook.react:react-native:+" // From node_modules + compile project(':react-native-navigation') compile project(':react-native-image-picker') - compile project(':react-native-smart-splashscreen') compile project(':react-native-orientation') compile project(':react-native-bottom-sheet') compile project(':react-native-push-notification') @@ -157,9 +160,6 @@ dependencies { compile project(':react-native-linear-gradient') compile project(':react-native-vector-icons') compile project(':react-native-svg') - compile fileTree(dir: "libs", include: ["*.jar"]) - compile "com.android.support:appcompat-v7:23.0.1" - compile "com.facebook.react:react-native:+" // From node_modules } // Run this once to be able to run the application with BUCK diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d14a98ef0..643c3972e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -34,8 +34,6 @@ android:label="@string/app_name" android:windowSoftInputMode="adjustResize" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" - android:launchMode="singleInstance" - android:taskAffinity="" > diff --git a/android/app/src/main/java/com/mattermost/MainActivity.java b/android/app/src/main/java/com/mattermost/MainActivity.java index 8d6aaa92a..2bb98165f 100644 --- a/android/app/src/main/java/com/mattermost/MainActivity.java +++ b/android/app/src/main/java/com/mattermost/MainActivity.java @@ -1,49 +1,67 @@ package com.mattermost; -import com.facebook.react.ReactActivity; import com.github.yamill.orientation.OrientationPackage; import com.psykar.cookiemanager.CookieManagerPackage; import com.BV.LinearGradient.LinearGradientPackage; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import com.reactnativecomponent.splashscreen.RCTSplashScreen; -public class MainActivity extends ReactActivity { +import com.reactnativenavigation.controllers.SplashActivity; + +import java.lang.ref.WeakReference; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.graphics.Color; +import android.widget.TextView; +import android.view.ViewGroup.LayoutParams; +import android.view.Gravity; +import android.util.TypedValue; + +public class MainActivity extends SplashActivity { + + private static ImageView imageView; + private static WeakReference wr_activity; + protected static MainActivity getActivity() { + return wr_activity.get(); + } /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. */ - @Override - protected String getMainComponentName() { - return "Mattermost"; - } + // @Override + // protected String getMainComponentName() { + // return "Mattermost"; + // } @Override - protected void onCreate(Bundle savedInstanceState) { - RCTSplashScreen.openSplashScreen(this); - super.onCreate(savedInstanceState); + public LinearLayout createSplashLayout() { + wr_activity = new WeakReference<>(this); + final int drawableId = getImageId(); + + Context context = getActivity(); + imageView = new ImageView(context); + imageView.setImageResource(drawableId); + + LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + imageView.setLayoutParams(layoutParams); + + imageView.setScaleType(ImageView.ScaleType.CENTER); + + LinearLayout view = new LinearLayout(this); + + view.setBackgroundColor(Color.parseColor("#FFFFFF")); + view.setGravity(Gravity.CENTER); + + view.addView(imageView); + return view; } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Intent intent = new Intent("onConfigurationChanged"); - intent.putExtra("newConfig", newConfig); - this.sendBroadcast(intent); - } - - /** - * When the back button is pressed and the app is closed it will - * be sent to the background instead of exiting fixing the newIntent - * problem where the splash screen wasn't being removed - */ - @Override - public void invokeDefaultOnBackPressed() { - Intent setIntent = new Intent(Intent.ACTION_MAIN); - setIntent.addCategory(Intent.CATEGORY_HOME); - setIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(setIntent); + private static int getImageId() { + int drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getClass().getPackage().getName()); + if (drawableId == 0) { + drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getPackageName()); + } + return drawableId; } } diff --git a/android/app/src/main/java/com/mattermost/MainApplication.java b/android/app/src/main/java/com/mattermost/MainApplication.java index aa8b397f0..ad858e09b 100644 --- a/android/app/src/main/java/com/mattermost/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/MainApplication.java @@ -2,8 +2,15 @@ package com.mattermost; import android.app.Application; import android.util.Log; +import android.support.annotation.NonNull; import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + import com.imagepicker.ImagePickerPackage; import com.gnet.bottomsheet.RNBottomSheetPackage; import com.learnium.RNDeviceInfo.RNDeviceInfo; @@ -11,32 +18,47 @@ import com.psykar.cookiemanager.CookieManagerPackage; import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.horcrux.svg.SvgPackage; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; -import com.facebook.soloader.SoLoader; import com.BV.LinearGradient.LinearGradientPackage; -import com.reactnativecomponent.splashscreen.RCTSplashScreenPackage; import com.github.yamill.orientation.OrientationPackage; +import com.reactnativenavigation.NavigationApplication; import java.util.Arrays; import java.util.List; -public class MainApplication extends Application implements ReactApplication { +// public class MainApplication extends Application implements ReactApplication { +public class MainApplication extends NavigationApplication { - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } + // private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + // @Override + // protected boolean getUseDeveloperSupport() { + // return BuildConfig.DEBUG; + // } + // + // @Override + // protected List getPackages() { + // return Arrays.asList( + // new MainReactPackage() + // ); + // } + // }; + // + // @Override + // public ReactNativeHost getReactNativeHost() { + // return mReactNativeHost; + // } - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), + @Override + public boolean isDebug() { + return BuildConfig.DEBUG; + } + + @NonNull + @Override + public List createAdditionalReactPackages() { + // Add the packages you require here. + // No need to add RnnPackage and MainReactPackage + return Arrays.asList( new ImagePickerPackage(), - new RCTSplashScreenPackage(), new RNBottomSheetPackage(), new RNDeviceInfo(), new CookieManagerPackage(), @@ -45,13 +67,7 @@ public class MainApplication extends Application implements ReactApplication { new SvgPackage(), new LinearGradientPackage(), new OrientationPackage() - ); - } - }; - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; + ); } @Override diff --git a/android/settings.gradle b/android/settings.gradle index 2b8a40c5e..e8f264566 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,8 +1,8 @@ rootProject.name = 'Mattermost' +include ':react-native-navigation' +project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/') include ':react-native-image-picker' project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') -include ':react-native-smart-splashscreen' -project(':react-native-smart-splashscreen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-smart-splash-screen/android') include ':react-native-bottom-sheet' project(':react-native-bottom-sheet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bottom-sheet/android') include ':react-native-device-info' diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js deleted file mode 100644 index 5c03e0818..000000000 --- a/app/actions/navigation/index.js +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {NavigationTypes} from 'app/constants'; -import Routes from 'app/navigation/routes'; -import {General} from 'mattermost-redux/constants'; -import {selectPost} from 'mattermost-redux/actions/posts'; - -export function closeDrawers() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS - }, getState); - }; -} - -export function closeModal() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_CLOSE_MODAL - }, getState); - }; -} - -export function goBack() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_POP - }, getState); - }; -} - -export function goToAbout() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.About - }, getState); - }; -} - -export function goToAccountNotifications() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.AccountNotifications - }, getState); - }; -} - -export function goToAccountSettings() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.AccountSettings - }, getState); - }; -} - -export function goToChannelAddMembers() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.ChannelAddMembers - }, getState); - }; -} - -export function goToChannelInfo() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.ChannelInfo - }, getState); - }; -} - -export function goToChannelMembers() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.ChannelMembers - }, getState); - }; -} - -export function goToCreateChannel(channelType) { - return async (dispatch, getState) => { - closeDrawers()(dispatch, getState); - let type; - let route; - switch (channelType) { - case General.OPEN_CHANNEL: - type = NavigationTypes.NAVIGATION_PUSH; - route = Routes.CreatePublicChannel; - break; - case General.PRIVATE_CHANNEL: - type = NavigationTypes.NAVIGATION_MODAL; - route = Routes.CreatePrivateChannel; - break; - default: - return; - } - - dispatch({ - type, - route: { - ...route, - props: { - channelType - } - } - }, getState); - }; -} - -export function goToImagePreviewModal(post, fileId) { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: { - ...Routes.ImagePreview, - props: { - post, - fileId - } - } - }, getState); - }; -} - -export function goToLoadTeam() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.LoadTeam - }, getState); - }; -} - -export function goToLogin() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.Login - }, getState); - }; -} - -export function goToLoginOptions() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.LoginOptions - }, getState); - }; -} - -export function goToMfa() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.Mfa - }, getState); - }; -} - -export function goToSaml() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.Saml - }, getState); - }; -} - -export function goToSelectTeam() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: Routes.SelectTeam - }, getState); - }; -} - -export function goToThread(channelId, rootId) { - return async (dispatch, getState) => { - selectPost(rootId)(dispatch, getState); - - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: { - ...Routes.Thread, - key: Routes.Thread.key + rootId, - props: { - channelId, - rootId - } - } - }, getState); - }; -} - -export function goToUserProfile(userId) { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_PUSH, - route: { - ...Routes.UserProfile, - props: { - userId - } - } - }, getState); - }; -} - -export function openChannelDrawer() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER, - route: Routes.ChannelDrawer - }, getState); - }; -} - -export function openEditPostModal(post) { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: { - ...Routes.EditPost, - props: { - post - } - } - }, getState); - }; -} - -export function openSettingsModal() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: Routes.Settings - }, getState); - }; -} - -export function requestCloseModal() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_REQUEST_CLOSE_MODAL - }, getState); - }; -} - -export function renderDrawer() { - return async (dispatch, getState) => { - dispatch({type: NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER, data: true}, getState); - }; -} - -export function unrenderDrawer() { - return async (dispatch, getState) => { - dispatch({type: NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER, data: false}, getState); - }; -} - -export function showOptionsModal(options) { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: { - ...Routes.OptionsModal, - props: options - } - }, getState); - }; -} - -export function showMoreChannelsModal() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: Routes.MoreChannels - }, getState); - }; -} - -export function showDirectMessagesModal() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: Routes.MoreDirectMessages - }, getState); - }; -} diff --git a/app/actions/views/account_notifications.js b/app/actions/views/account_notifications.js index 9e15a19a8..6b5fae485 100644 --- a/app/actions/views/account_notifications.js +++ b/app/actions/views/account_notifications.js @@ -26,6 +26,10 @@ export function handleUpdateUserNotifyProps(notifyProps) { } const props = {...otherProps, email}; - updateMe({notify_props: props})(dispatch, getState); + try { + await updateMe({notify_props: props})(dispatch, getState); + } catch (error) { + // do nothing + } }; } diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index c7d1c79b7..f1aa434ab 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -288,19 +288,6 @@ export function closeGMChannel(channel) { }; } -export function closeDirectChannel(channel) { - return async (dispatch, getState) => { - switch (channel.type) { - case General.DM_CHANNEL: - return closeDMChannel(channel)(dispatch, getState); - case General.GM_CHANNEL: - return closeGMChannel(channel)(dispatch, getState); - } - - return null; - }; -} - export function markFavorite(channelId) { return async (dispatch, getState) => { const {currentUserId} = getState().entities.users; diff --git a/app/actions/views/load_team.js b/app/actions/views/load_team.js index 716437e8d..cd22a77ac 100644 --- a/app/actions/views/load_team.js +++ b/app/actions/views/load_team.js @@ -1,38 +1,14 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {NavigationTypes, ViewTypes} from 'app/constants'; -import Routes from 'app/navigation/routes'; +import {ViewTypes} from 'app/constants'; -export function goToChannelView() { +export function initialize() { return async (dispatch, getState) => { - const state = getState(); - if (state.views.login.loginId) { + setTimeout(() => { dispatch({ - type: NavigationTypes.NAVIGATION_REPLACE_AT_ROOT, - route: Routes.ChannelView + type: ViewTypes.APPLICATION_INITIALIZED }, getState); - - dispatch({ - type: NavigationTypes.NAVIGATION_CLOSE_MODAL - }, getState); - - setTimeout(() => { - dispatch({ - type: ViewTypes.APPLICATION_INITIALIZED - }, getState); - }, 400); - } else { - dispatch({ - type: NavigationTypes.NAVIGATION_RESET, - routes: [Routes.ChannelView] - }, getState); - - setTimeout(() => { - dispatch({ - type: ViewTypes.APPLICATION_INITIALIZED - }, getState); - }, 400); - } + }, 400); }; } diff --git a/app/actions/views/options_modal.js b/app/actions/views/options_modal.js deleted file mode 100644 index dc7e91152..000000000 --- a/app/actions/views/options_modal.js +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {ViewTypes} from 'app/constants'; - -export function openModal(title, options) { - return async (dispatch, getState) => { - dispatch({ - type: ViewTypes.OPTIONS_MODAL_CHANGED, - data: { - title, - options, - visible: true - } - }, getState); - }; -} - -export function closeModal() { - return async (dispatch, getState) => { - dispatch({ - type: ViewTypes.OPTIONS_MODAL_CHANGED, - data: { - title: '', - options: [], - visible: false - } - }, getState); - }; -} diff --git a/app/actions/views/right_menu_drawer.js b/app/actions/views/right_menu_drawer.js deleted file mode 100644 index 4ac845f09..000000000 --- a/app/actions/views/right_menu_drawer.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2017-present 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'; - -export function goToRecentMentions() { - return async (dispatch, getState) => { - dispatch(batchActions([{ - type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS - }, { - type: NavigationTypes.NAVIGATION_PUSH, - route: { - ...Routes.Search, - props: { - searchType: 'recent_mentions' - } - } - }]), getState); - }; -} - -export function goToFlaggedPosts() { - return async (dispatch, getState) => { - dispatch(batchActions([{ - type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS - }, { - type: NavigationTypes.NAVIGATION_PUSH, - route: { - ...Routes.Search, - props: { - searchType: 'flagged_posts' - } - } - }]), getState); - }; -} diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 6a67abda0..f27a9821a 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -1,28 +1,17 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {NavigationTypes, ViewTypes} from 'app/constants'; -import Routes from 'app/navigation/routes'; +import {ViewTypes} from 'app/constants'; 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 {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general'; import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels'; -export function goToSelectServer() { - return async (dispatch, getState) => { - dispatch({ - type: NavigationTypes.NAVIGATION_MODAL, - route: Routes.SelectServer - }, getState); - }; -} - export function loadConfigAndLicense(serverVersion) { return async (dispatch, getState) => { getClientConfig()(dispatch, getState); @@ -54,26 +43,24 @@ export function goToNotification(notification) { let teamId = data.team_id || currentTeamId; if (teamId) { - await handleTeamChange(teams[teamId])(dispatch, getState); + handleTeamChange(teams[teamId])(dispatch, getState); await loadChannelsIfNecessary(teamId)(dispatch, getState); } else { await selectFirstAvailableTeam()(dispatch, getState); teamId = currentTeamId; } - viewChannel(teamId, channelId)(dispatch, getState); - await loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState); + viewChannel(channelId)(dispatch, getState); + loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState); // when the notification is tapped go to the channel view before selecting the channel to prevent // weird behavior - goToChannelView()(dispatch, getState); - await handleSelectChannel(channelId)(dispatch, getState); - markChannelAsRead(teamId, channelId)(dispatch, getState); + handleSelectChannel(channelId)(dispatch, getState); + markChannelAsRead(teamId, channelId)(dispatch, getState).then(() => true).catch(() => true); }; } export default { - goToSelectServer, loadConfigAndLicense, queueNotification, clearNotification, diff --git a/app/actions/views/select_server.js b/app/actions/views/select_server.js index 419114a0a..4cee7c4a7 100644 --- a/app/actions/views/select_server.js +++ b/app/actions/views/select_server.js @@ -2,7 +2,6 @@ // See License.txt for license information. import {ViewTypes} from 'app/constants'; -import {goToLogin, goToLoginOptions} from 'app/actions/navigation'; export function handleServerUrlChanged(serverUrl) { return async (dispatch, getState) => { @@ -13,26 +12,6 @@ export function handleServerUrlChanged(serverUrl) { }; } -export function handleLoginOptions() { - return async (dispatch, getState) => { - const {config, license} = getState().entities.general; - - const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; - - let options = 0; - if (samlEnabled) { - options += 1; - } - - if (options) { - await goToLoginOptions()(dispatch, getState); - } else { - await goToLogin()(dispatch, getState); - } - }; -} - export default { - handleServerUrlChanged, - handleLoginOptions + handleServerUrlChanged }; diff --git a/app/actions/views/user_profile.js b/app/actions/views/user_profile.js index f8d4a965c..4ea6a46cd 100644 --- a/app/actions/views/user_profile.js +++ b/app/actions/views/user_profile.js @@ -2,11 +2,9 @@ // See License.txt for license information. import {makeDirectChannel} from 'app/actions/views/more_dms'; -import {NavigationTypes} from 'app/constants'; export function handleSendMessage(otherUserId) { return async (dispatch, getState) => { await makeDirectChannel(otherUserId)(dispatch, getState); - dispatch({type: NavigationTypes.NAVIGATION_POP_TO_INDEX, index: 0}, getState); }; } diff --git a/app/components/action_button.js b/app/components/action_button.js deleted file mode 100644 index 7c743aa8a..000000000 --- a/app/components/action_button.js +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes, PureComponent} from 'react'; -import {connect} from 'react-redux'; -import { - TouchableOpacity, - View -} from 'react-native'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; - -import FormattedText from 'app/components/formatted_text'; -import Loading from 'app/components/loading'; -import {getTheme} from 'app/selectors/preferences'; -import {changeOpacity} from 'app/utils/theme'; - -class ActionButton extends PureComponent { - static propTypes = { - actionEventName: PropTypes.string.isRequired, - emitter: PropTypes.func.isRequired, - enabled: PropTypes.bool, - enableEventName: PropTypes.string, - labelDefaultMessage: PropTypes.string.isRequired, - labelId: PropTypes.string.isRequired, - loadingEventName: PropTypes.string.isRequired, - theme: PropTypes.object - }; - - static defaultProps = { - theme: {}, - enabled: true - }; - - constructor(props) { - super(props); - - this.state = { - enabled: props.enabled, - loading: false - }; - } - - componentWillMount() { - EventEmitter.on(this.props.enableEventName, this.handleEnableEvent); - EventEmitter.on(this.props.loadingEventName, this.handleLoadingEvent); - } - - componentWillUnmount() { - EventEmitter.off(this.props.enableEventName, this.handleEnableEvent); - EventEmitter.off(this.props.loadingEventName, this.handleLoadingEvent); - } - - handleEnableEvent = (enabled) => { - this.setState({enabled}); - }; - - handleLoadingEvent = (loading) => { - this.setState({loading}); - }; - - onPress = () => { - if (this.state.enabled) { - this.props.emitter(this.props.actionEventName); - } - }; - - render() { - const {labelDefaultMessage, labelId, theme} = this.props; - const {enabled, loading} = this.state; - let color = changeOpacity(theme.sidebarHeaderTextColor, 0.4); - let contents; - if (enabled) { - color = theme.sidebarHeaderTextColor; - contents = ( - - - - - - ); - } else { - contents = ( - - - - ); - } - - if (loading) { - return ( - - ); - } - - return contents; - } -} - -function mapStateToProps(state) { - return { - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(ActionButton); diff --git a/app/components/autocomplete/at_mention/at_mention.js b/app/components/autocomplete/at_mention/at_mention.js index bfaa74378..7bf389525 100644 --- a/app/components/autocomplete/at_mention/at_mention.js +++ b/app/components/autocomplete/at_mention/at_mention.js @@ -38,6 +38,7 @@ export default class AtMention extends Component { static defaultProps = { autocompleteUsersInCurrentChannel: {}, + defaultChannel: {}, postDraft: '' }; diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js index ce2d17273..fc275ade9 100644 --- a/app/components/autocomplete/at_mention/index.js +++ b/app/components/autocomplete/at_mention/index.js @@ -17,7 +17,7 @@ function mapStateToProps(state, ownProps) { let postDraft; if (ownProps.rootId.length) { postDraft = state.views.thread.drafts[ownProps.rootId].draft; - } else { + } else if (currentChannelId) { postDraft = state.views.channel.drafts[currentChannelId].draft; } diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js index 09ee79742..e6c7852c8 100644 --- a/app/components/autocomplete/channel_mention/index.js +++ b/app/components/autocomplete/channel_mention/index.js @@ -18,7 +18,7 @@ function mapStateToProps(state, ownProps) { let postDraft; if (ownProps.rootId.length) { postDraft = state.views.thread.drafts[ownProps.rootId].draft; - } else { + } else if (currentChannelId) { postDraft = state.views.channel.drafts[currentChannelId].draft; } diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js new file mode 100644 index 000000000..37ce65104 --- /dev/null +++ b/app/components/channel_drawer/channel_drawer.js @@ -0,0 +1,190 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PropTypes, PureComponent} from 'react'; +import {BackAndroid, InteractionManager, Keyboard} from 'react-native'; + +import Drawer from 'app/components/drawer'; +import ChannelDrawerList from 'app/components/channel_drawer_list'; + +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +export default class ChannelDrawer extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + handleSelectChannel: PropTypes.func.isRequired, + viewChannel: PropTypes.func.isRequired, + markChannelAsRead: PropTypes.func.isRequired, + setChannelLoading: PropTypes.func.isRequired + }).isRequired, + blurPostTextBox: PropTypes.func.isRequired, + navigator: PropTypes.object, + children: PropTypes.node, + currentTeam: PropTypes.object, + currentChannel: PropTypes.object, + channels: PropTypes.object, + channelMembers: PropTypes.object, + theme: PropTypes.object.isRequired + }; + + static defaultProps = { + currentTeam: {}, + currentChannel: {} + }; + + state = { + openDrawer: false + }; + + componentDidMount() { + EventEmitter.on('open_channel_drawer', this.openChannelDrawer); + EventEmitter.on('close_channel_drawer', this.closeChannelDrawer); + BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack); + } + + componentWillUnmount() { + EventEmitter.off('open_channel_drawer', this.openChannelDrawer); + EventEmitter.off('close_channel_drawer', this.closeChannelDrawer); + BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack); + } + + handleAndroidBack = () => { + if (this.state.openDrawer) { + this.setState({openDrawer: false}); + return true; + } + + return false; + }; + + closeChannelDrawer = () => { + this.setState({openDrawer: false}); + }; + + handleDrawerClose = () => { + setTimeout(() => { + InteractionManager.clearInteractionHandle(this.closeLeftHandle); + }); + }; + + handleDrawerCloseStart = () => { + this.closeLeftHandle = InteractionManager.createInteractionHandle(); + }; + + handleDrawerOpen = () => { + this.setState({openDrawer: true}); + Keyboard.dismiss(); + setTimeout(() => { + InteractionManager.clearInteractionHandle(this.openLeftHandle); + }); + }; + + handleDrawerOpenStart = () => { + this.openLeftHandle = InteractionManager.createInteractionHandle(); + }; + + handleDrawerTween = (ratio) => { + const opacity = (ratio / 2); + + EventEmitter.emit('drawer_opacity', opacity); + + return { + mainOverlay: { + backgroundColor: this.props.theme.centerChannelBg, + opacity + }, + drawerOverlay: { + backgroundColor: ratio ? '#000' : '#FFF', + opacity: ratio ? (1 - ratio) / 2 : 1 + } + }; + }; + + openChannelDrawer = () => { + this.props.blurPostTextBox(); + this.setState({openDrawer: true}); + }; + + selectChannel = (id) => { + const { + actions, + currentChannel + } = this.props; + + const { + handleSelectChannel, + markChannelAsRead, + setChannelLoading, + viewChannel + } = actions; + + markChannelAsRead(id, currentChannel.id); + setChannelLoading(); + viewChannel(id, currentChannel.id); + this.closeChannelDrawer(); + InteractionManager.runAfterInteractions(() => { + handleSelectChannel(id); + }); + }; + + render() { + const { + children, + currentChannel, + currentTeam, + channels, + channelMembers, + navigator, + theme + } = this.props; + + return ( + + } + tapToClose={true} + openDrawerOffset={40} + onRequestClose={this.closeChannelDrawer} + panOpenMask={0.2} + panCloseMask={40} + panThreshold={0.25} + acceptPan={true} + negotiatePan={true} + useInteractionManager={false} + tweenHandler={this.handleDrawerTween} + elevation={-5} + styles={{ + main: { + shadowColor: '#000000', + shadowOpacity: 0.4, + shadowRadius: 12, + shadowOffset: { + width: -4, + height: 0 + } + } + }} + > + {children} + + ); + } +} diff --git a/app/scenes/channel_drawer/index.js b/app/components/channel_drawer/index.js similarity index 91% rename from app/scenes/channel_drawer/index.js rename to app/components/channel_drawer/index.js index 493308ab1..7259e6968 100644 --- a/app/scenes/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -4,7 +4,6 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {closeDrawers} from 'app/actions/navigation'; import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel'; import {getChannelsByCategory, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; @@ -15,8 +14,9 @@ import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels' import ChannelDrawer from './channel_drawer.js'; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { return { + ...ownProps, currentTeam: getCurrentTeam(state), currentChannel: getCurrentChannel(state), channels: getChannelsByCategory(state), @@ -28,7 +28,6 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - closeDrawers, handleSelectChannel, viewChannel, markChannelAsRead, diff --git a/app/components/channel_drawer_list/channel_drawer_list.js b/app/components/channel_drawer_list/channel_drawer_list.js index 9038ef0bc..2498c34b8 100644 --- a/app/components/channel_drawer_list/channel_drawer_list.js +++ b/app/components/channel_drawer_list/channel_drawer_list.js @@ -26,24 +26,13 @@ import UnreadIndicator from './unread_indicator'; class ChannelDrawerList extends Component { static propTypes = { - actions: PropTypes.shape({ - closeDirectChannel: PropTypes.func.isRequired, - closeOptionsModal: PropTypes.func.isRequired, - goToCreateChannel: PropTypes.func.isRequired, - leaveChannel: PropTypes.func.isRequired, - markFavorite: PropTypes.func.isRequired, - openSettingsModal: React.PropTypes.func.isRequired, - unmarkFavorite: PropTypes.func.isRequired, - showOptionsModal: PropTypes.func.isRequired, - showDirectMessagesModal: PropTypes.func.isRequired, - showMoreChannelsModal: PropTypes.func.isRequired - }).isRequired, canCreatePrivateChannels: PropTypes.bool.isRequired, channels: PropTypes.object.isRequired, channelMembers: PropTypes.object, currentTeam: PropTypes.object.isRequired, currentChannel: PropTypes.object, intl: intlShape.isRequired, + navigator: PropTypes.object, onSelectChannel: PropTypes.func.isRequired, theme: PropTypes.object.isRequired }; @@ -64,6 +53,10 @@ class ChannelDrawerList extends Component { rowHasChanged: (a, b) => a !== b }).cloneWithRows(this.buildData(props)) }; + MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor). + then((source) => { + this.closeButton = source; + }); } shouldComponentUpdate(nextProps, nextState) { @@ -178,7 +171,25 @@ class ChannelDrawerList extends Component { }; createPrivateChannel = () => { - this.props.actions.goToCreateChannel(General.PRIVATE_CHANNEL); + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'CreateChannel', + animationType: 'slide-up', + title: intl.formatMessage({id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'}), + backButtonTitle: '', + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + passProps: { + channelType: General.PRIVATE_CHANNEL, + closeButton: this.closeButton + } + }); }; buildData = (props) => { @@ -206,7 +217,7 @@ class ChannelDrawerList extends Component { } data.push( - this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.props.actions.showMoreChannelsModal, publicChannels.length > 0), + this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.showMoreChannelsModal, publicChannels.length > 0), ...publicChannels ); @@ -220,7 +231,7 @@ class ChannelDrawerList extends Component { ); data.push( - this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.props.actions.showDirectMessagesModal, directAndGroupChannels.length > 0), + this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.showDirectMessagesModal, directAndGroupChannels.length > 0), ...directAndGroupChannels ); @@ -231,6 +242,76 @@ class ChannelDrawerList extends Component { return data; }; + openSettingsModal = () => { + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'Settings', + title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}), + animationType: 'slide-up', + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + navigatorButtons: { + leftButtons: [{ + id: 'close-settings', + icon: this.closeButton + }] + } + }); + }; + + showDirectMessagesModal = () => { + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'MoreDirectMessages', + title: intl.formatMessage({id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}), + animationType: 'slide-up', + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + navigatorButtons: { + leftButtons: [{ + id: 'close-dms', + icon: this.closeButton + }] + } + }); + }; + + showMoreChannelsModal = () => { + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'MoreChannels', + animationType: 'slide-up', + title: intl.formatMessage({id: 'more_channels.title', defaultMessage: 'More Channels'}), + backButtonTitle: '', + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + passProps: { + channelType: General.PRIVATE_CHANNEL, + closeButton: this.closeButton + } + }); + }; + renderSectionAction = (styles, action) => { const {theme} = this.props; return ( @@ -293,7 +374,7 @@ class ChannelDrawerList extends Component { const settings = ( preventDoubleTap(this.props.actions.openSettingsModal)} + onPress={() => preventDoubleTap(this.openSettingsModal)} underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} > + + {purpose} + + + ); + } + const RowComponent = ( @@ -54,15 +69,7 @@ function ChannelListRow(props) { - - - {purpose} - - + {purposeComponent} ); diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 3ef85a66a..e6684acec 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -21,6 +21,7 @@ export default class FileAttachmentList extends Component { fetchCache: PropTypes.object.isRequired, files: PropTypes.array.isRequired, hideOptionsContext: PropTypes.func.isRequired, + navigator: PropTypes.object, onLongPress: PropTypes.func, onPress: PropTypes.func, post: PropTypes.object.isRequired, @@ -43,14 +44,33 @@ export default class FileAttachmentList extends Component { } } + goToImagePreview = (post, fileId) => { + this.props.navigator.showModal({ + screen: 'ImagePreview', + title: '', + animationType: 'none', + passProps: { + fileId, + post + }, + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + modalPresentationStyle: 'overCurrentContext' + } + }); + }; + handleInfoPress = () => { this.props.hideOptionsContext(); this.props.onPress(); - } + }; handlePreviewPress = (file) => { this.props.hideOptionsContext(); - preventDoubleTap(this.props.actions.goToImagePreviewModal, this, this.props.post, file.id); + preventDoubleTap(this.goToImagePreview, this, this.props.post, file.id); }; render() { diff --git a/app/components/file_attachment_list/index.js b/app/components/file_attachment_list/index.js index b1ffbcd91..9e7a29341 100644 --- a/app/components/file_attachment_list/index.js +++ b/app/components/file_attachment_list/index.js @@ -8,7 +8,6 @@ import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files'; import {loadFilesForPostIfNecessary} from 'app/actions/views/channel'; import {addFileToFetchCache} from 'app/actions/views/file_preview'; import {getTheme} from 'app/selectors/preferences'; -import {goToImagePreviewModal} from 'app/actions/navigation'; import FileAttachmentList from './file_attachment_list'; @@ -29,7 +28,6 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ addFileToFetchCache, - goToImagePreviewModal, loadFilesForPostIfNecessary }, dispatch) }; diff --git a/app/components/offline_indicator/offline_indicator.js b/app/components/offline_indicator/offline_indicator.js index 2ef2b53a2..06c565c5e 100644 --- a/app/components/offline_indicator/offline_indicator.js +++ b/app/components/offline_indicator/offline_indicator.js @@ -17,7 +17,7 @@ import FormattedText from 'app/components/formatted_text'; import {RequestStatus} from 'mattermost-redux/constants'; -const INITIAL_TOP = Platform.OS === 'ios' ? -80 : -60; +const INITIAL_TOP = -46; const OFFLINE = 'offline'; const CONNECTING = 'connecting'; const CONNECTED = 'connected'; @@ -197,18 +197,18 @@ export default class OfflineIndicator extends PureComponent { const styles = StyleSheet.create({ container: { - flex: 1, - zIndex: 10, - position: 'absolute', - elevation: 2 + height: 38, + width: Dimensions.get('window').width, + zIndex: 9, + position: 'relative', + elevation: 1 }, wrapper: { alignItems: 'center', flex: 1, + height: 39, flexDirection: 'row', - height: 38, paddingHorizontal: 12, - width: Dimensions.get('window').width, backgroundColor: 'red' }, message: { diff --git a/app/components/post/index.js b/app/components/post/index.js index f5dfb7ed8..dac212947 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -10,7 +10,6 @@ import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/s import {isPostFlagged} from 'mattermost-redux/utils/post_utils'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {goToUserProfile, openEditPostModal, requestCloseModal, showOptionsModal} from 'app/actions/navigation'; import {getTheme} from 'app/selectors/preferences'; import Post from './post'; @@ -46,11 +45,7 @@ function mapDispatchToProps(dispatch) { createPost, deletePost, flagPost, - goToUserProfile, - openEditPostModal, removePost, - requestCloseModal, - showOptionsModal, unflagPost }, dispatch) }; diff --git a/app/components/post/post.js b/app/components/post/post.js index 235fcb6da..027ff5158 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -14,6 +14,7 @@ import { } from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; import Icon from 'react-native-vector-icons/Ionicons'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FileAttachmentList from 'app/components/file_attachment_list'; import FormattedText from 'app/components/formatted_text'; @@ -23,6 +24,7 @@ import Markdown from 'app/components/markdown'; import OptionsContext from 'app/components/options_context'; import ProfilePicture from 'app/components/profile_picture'; import ReplyIcon from 'app/components/reply_icon'; +import {NavigationTypes} from 'app/constants'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -30,6 +32,7 @@ import webhookIcon from 'assets/images/icons/webhook.jpg'; import {Posts} from 'mattermost-redux/constants'; import DelayedAction from 'mattermost-redux/utils/delayed_action'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {canDeletePost, canEditPost, isSystemMessage} from 'mattermost-redux/utils/post_utils'; import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -52,6 +55,7 @@ class Post extends PureComponent { commentedOnDisplayName: PropTypes.string, commentedOnPost: PropTypes.object, license: PropTypes.object.isRequired, + navigator: PropTypes.object, roles: PropTypes.string, theme: PropTypes.object.isRequired, onPress: PropTypes.func, @@ -59,12 +63,8 @@ class Post extends PureComponent { createPost: PropTypes.func.isRequired, deletePost: PropTypes.func.isRequired, flagPost: PropTypes.func.isRequired, - goToUserProfile: PropTypes.func.isRequired, - openEditPostModal: PropTypes.func.isRequired, removePost: PropTypes.func.isRequired, - unflagPost: PropTypes.func.isRequired, - requestCloseModal: PropTypes.func.isRequired, - showOptionsModal: PropTypes.func.isRequired + unflagPost: PropTypes.func.isRequired }).isRequired }; @@ -91,6 +91,25 @@ class Post extends PureComponent { this.editDisableAction.cancel(); } + goToUserProfile = (userId) => { + const {intl, navigator, theme} = this.props; + navigator.push({ + screen: 'UserProfile', + title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}), + animated: true, + backButtonTitle: '', + passProps: { + userId + }, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + handleEditDisable = () => { this.setState({canEdit: false}); }; @@ -117,8 +136,25 @@ class Post extends PureComponent { }; handlePostEdit = () => { - const {actions, post} = this.props; - actions.openEditPostModal(post); + const {intl, navigator, post, theme} = this.props; + MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor). + then((source) => { + navigator.showModal({ + screen: 'EditPost', + title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + passProps: { + post, + closeButton: source + } + }); + }); }; handleFailedPostPress = () => { @@ -131,7 +167,7 @@ class Post extends PureComponent { action: () => { const {failed, id, ...post} = this.props.post; // eslint-disable-line - this.props.actions.requestCloseModal(); + EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL); this.props.actions.createPost(post); }, text: { @@ -140,7 +176,7 @@ class Post extends PureComponent { } }, { action: () => { - this.props.actions.requestCloseModal(); + EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL); this.onRemovePost(this.props.post); }, text: { @@ -153,7 +189,22 @@ class Post extends PureComponent { }] }; - this.props.actions.showOptionsModal(options); + this.props.navigator.showModal({ + screen: 'OptionsModal', + title: '', + animationType: 'none', + passProps: { + items: options.items, + title: options.title + }, + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + modalPresentationStyle: 'overCurrentContext' + } + }); }; handlePress = () => { @@ -242,7 +293,7 @@ class Post extends PureComponent { }; renderFileAttachments() { - const {post} = this.props; + const {navigator, post} = this.props; const fileIds = post.file_ids || []; let attachments; @@ -254,6 +305,7 @@ class Post extends PureComponent { onPress={this.handlePress} post={post} toggleSelected={this.toggleSelected} + navigator={navigator} /> ); } @@ -389,7 +441,7 @@ class Post extends PureComponent { }; viewUserProfile = () => { - preventDoubleTap(this.props.actions.goToUserProfile, null, this.props.user.id); + preventDoubleTap(this.goToUserProfile, null, this.props.user.id); }; toggleSelected = (selected) => { diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 8ef7a427f..48bd8f11c 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -26,6 +26,7 @@ export default class PostList extends Component { posts: PropTypes.array.isRequired, theme: PropTypes.object.isRequired, loadMore: PropTypes.func, + navigator: PropTypes.object, isLoadingMore: PropTypes.bool, showLoadMore: PropTypes.bool, onPostPress: PropTypes.func, @@ -142,6 +143,7 @@ export default class PostList extends Component { isLastReply={post.isLastReply} commentedOnPost={post.commentedOnPost} onPress={this.props.onPostPress} + navigator={this.props.navigator} /> ); }; diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js index 45385c00e..73b95fbae 100644 --- a/app/components/post_textbox/index.js +++ b/app/components/post_textbox/index.js @@ -6,7 +6,6 @@ import {connect} from 'react-redux'; import {createPost} from 'mattermost-redux/actions/posts'; import {userTyping} from 'mattermost-redux/actions/websocket'; -import {showOptionsModal, requestCloseModal} from 'app/actions/navigation'; import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload'; import {getTheme} from 'app/selectors/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; @@ -29,11 +28,9 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ createPost, - closeModal: requestCloseModal, handleClearFiles, handleRemoveLastFile, handleUploadFiles, - showOptionsModal, userTyping }, dispatch) }; diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index ca05ce66f..39ffa3e98 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -30,12 +30,10 @@ const MAX_MESSAGE_LENGTH = 4000; class PostTextbox extends PureComponent { static propTypes = { actions: PropTypes.shape({ - closeModal: PropTypes.func.isRequired, createPost: PropTypes.func.isRequired, handleClearFiles: PropTypes.func.isRequired, handleRemoveLastFile: PropTypes.func.isRequired, handleUploadFiles: PropTypes.func.isRequired, - showOptionsModal: PropTypes.func.isRequired, userTyping: PropTypes.func.isRequired }).isRequired, channelId: PropTypes.string.isRequired, @@ -43,6 +41,7 @@ class PostTextbox extends PureComponent { currentUserId: PropTypes.string.isRequired, files: PropTypes.array, intl: intlShape.isRequired, + navigator: PropTypes.object, onChangeText: PropTypes.func.isRequired, rootId: PropTypes.string, theme: PropTypes.object.isRequired, @@ -207,7 +206,9 @@ class PostTextbox extends PureComponent { }; attachFileFromCamera = () => { - this.props.actions.closeModal(); + this.props.navigator.dismissModal({ + animationType: 'none' + }); const options = { quality: 0.7, @@ -228,7 +229,9 @@ class PostTextbox extends PureComponent { }; attachFileFromLibrary = () => { - this.props.actions.closeModal(); + this.props.navigator.dismissModal({ + animationType: 'none' + }); const options = { quality: 0.7, @@ -268,7 +271,21 @@ class PostTextbox extends PureComponent { }] }; - this.props.actions.showOptionsModal(options); + this.props.navigator.showModal({ + screen: 'OptionsModal', + title: '', + animationType: 'none', + passProps: { + items: options.items + }, + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: 'transparent', + modalPresentationStyle: 'overCurrentContext' + } + }); }; renderTyping = () => { diff --git a/app/components/root/index.js b/app/components/root/index.js index 7ad5a4915..e6730bfa9 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -1,23 +1,17 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import Config from 'assets/config.json'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; -import {closeDrawers, closeModal, goBack, unrenderDrawer} from 'app/actions/navigation'; -import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root'; -import {setAppState, setDeviceToken} from 'mattermost-redux/actions/general'; -import {logout} from 'mattermost-redux/actions/users'; +import Config from 'assets/config.json'; import Root from './root'; function mapStateToProps(state, ownProps) { const users = state.entities.users; const {currentUserId} = users; - const {currentTeamId} = state.entities.teams; - const {currentChannelId} = state.entities.channels; let locale = Config.DefaultLocale; if (currentUserId && users.profiles[currentUserId]) { @@ -26,28 +20,9 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, - currentTeamId, - currentChannelId, - locale, - navigation: state.navigation + currentChannelId: getCurrentChannelId(state), + locale }; } -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - closeDrawers, - closeModal, - goBack, - loadConfigAndLicense, - logout, - goToNotification, - queueNotification, - setAppState, - setDeviceToken, - unrenderDrawer - }, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Root); +export default connect(mapStateToProps)(Root); diff --git a/app/components/root/root.js b/app/components/root/root.js index 3a41269ed..e7b206458 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -1,128 +1,47 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component, PropTypes} from 'react'; -import { - Alert, - AppState, - BackAndroid, - InteractionManager, - Platform, - StatusBar, - View -} from 'react-native'; +import React, {PropTypes, PureComponent} from 'react'; import {IntlProvider} from 'react-intl'; -import DeviceInfo from 'react-native-device-info'; -import semver from 'semver'; -import PushNotification from 'app/components/push_notification'; -import {getTranslations} from 'app/i18n'; - -import Config from 'assets/config'; - -import {Client4} from 'mattermost-redux/client'; -import {General} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -export default class Root extends Component { +import {ViewTypes} from 'app/constants'; +import {getTranslations} from 'app/i18n'; + +export default class Root extends PureComponent { static propTypes = { children: PropTypes.node, - currentTeamId: PropTypes.string, + navigator: PropTypes.object, currentChannelId: PropTypes.string, - locale: PropTypes.string.isRequired, - navigation: PropTypes.object.isRequired, - actions: PropTypes.shape({ - closeDrawers: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired, - loadConfigAndLicense: PropTypes.func.isRequired, - logout: PropTypes.func.isRequired, - setAppState: PropTypes.func.isRequired, - unrenderDrawer: PropTypes.func.isRequired - }).isRequired + locale: PropTypes.string.isRequired }; - componentDidMount() { - this.props.actions.setAppState(AppState.currentState === 'active'); - AppState.addEventListener('change', this.handleAppStateChange); - EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); - Client4.setUserAgent(DeviceInfo.getUserAgent()); - - if (Platform.OS === 'android') { - BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack); - } + componentWillMount() { + EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); } componentWillUnmount() { - AppState.removeEventListener('change', this.handleAppStateChange); - EventEmitter.off(General.CONFIG_CHANGED, this.handleConfigChanged); - - if (Platform.OS === 'android') { - BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack); - } + EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); } - handleAppStateChange = (appState) => { - this.props.actions.setAppState(appState === 'active'); + handleInAppNotification = (notification) => { + const {data} = notification; + const {currentChannelId, navigator} = this.props; - if (appState === 'inactive') { - // TODO: See if we still need this + if (data.channel_id !== currentChannelId) { + navigator.showInAppNotification({ + screen: 'Notification', + position: 'top', + autoDismissTimerSec: 15, + dismissWithSwipe: true, + passProps: { + notification + } + }); } }; - handleAndroidBack = () => { - const {index, isModal, leftDrawerOpen, modal} = this.props.navigation; - const {closeDrawers, closeModal, goBack} = this.props.actions; - - if (isModal) { - if (modal.index > 0) { - goBack(); - return true; - } - closeModal(); - return true; - } - - if (leftDrawerOpen) { - closeDrawers(); - return true; - } else if (index > 0) { - goBack(); - return true; - } - - return false; - }; - - handleConfigChanged = (serverVersion) => { - const {loadConfigAndLicense} = this.props.actions; - const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; - if (serverVersion) { - if (semver.valid(version) && semver.lt(version, Config.MinServerVersion)) { - Alert.alert( - 'Server upgrade required', - 'A server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.', - [{ - text: 'OK', - onPress: this.handleVersionUpgrade - }] - ); - } else { - loadConfigAndLicense(serverVersion); - } - } - }; - - handleVersionUpgrade = async () => { - const {closeDrawers, logout, unrenderDrawer} = this.props.actions; - - Client4.serverVersion = ''; - - closeDrawers(); - unrenderDrawer(); - InteractionManager.runAfterInteractions(logout); - }; - render() { const locale = this.props.locale; @@ -131,11 +50,7 @@ export default class Root extends Component { locale={locale} messages={getTranslations(locale)} > - - - {this.props.children} - - + {this.props.children} ); } diff --git a/app/constants/index.js b/app/constants/index.js index 4c31ec5b0..91bc7f280 100644 --- a/app/constants/index.js +++ b/app/constants/index.js @@ -2,11 +2,9 @@ // 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/navigation.js b/app/constants/navigation.js index 77862b5c6..66ea09f7f 100644 --- a/app/constants/navigation.js +++ b/app/constants/navigation.js @@ -4,21 +4,8 @@ import keyMirror from 'mattermost-redux/utils/key_mirror'; const NavigationTypes = keyMirror({ - NAVIGATION_PUSH: null, - NAVIGATION_POP: null, - NAVIGATION_POP_TO_INDEX: null, - NAVIGATION_OPEN_LEFT_DRAWER: null, - NAVIGATION_RENDER_LEFT_DRAWER: null, - NAVIGATION_OPEN_RIGHT_DRAWER: null, - NAVIGATION_CLOSE_DRAWERS: null, - NAVIGATION_JUMP: null, - NAVIGATION_JUMP_TO_INDEX: null, - NAVIGATION_REPLACE: null, - NAVIGATION_REPLACE_AT_ROOT: null, NAVIGATION_RESET: null, - NAVIGATION_MODAL: null, - NAVIGATION_CLOSE_MODAL: null, - NAVIGATION_REQUEST_CLOSE_MODAL: null + NAVIGATION_CLOSE_MODAL: null }); export default NavigationTypes; diff --git a/app/constants/storage.js b/app/constants/storage.js deleted file mode 100644 index 3d817f3e8..000000000 --- a/app/constants/storage.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import keyMirror from 'mattermost-redux/utils/key_mirror'; - -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/constants/view.js b/app/constants/view.js index 2efe5953f..95bb5ad5f 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -14,9 +14,8 @@ const ViewTypes = keyMirror({ POST_DRAFT_CHANGED: null, COMMENT_DRAFT_CHANGED: null, - OPTIONS_MODAL_CHANGED: null, - NOTIFICATION_CHANGED: null, + NOTIFICATION_IN_APP: null, CONNECTION_CHANGED: null, diff --git a/app/initial_state.js b/app/initial_state.js index 07e7811cf..32f90af28 100644 --- a/app/initial_state.js +++ b/app/initial_state.js @@ -3,8 +3,6 @@ import Config from 'assets/config.json'; -import Routes from 'app/navigation/routes'; - const state = { entities: { general: { @@ -252,23 +250,7 @@ const state = { } } }, - navigation: { - index: 0, - routes: [ - Routes.Root - ], - modal: { - index: 0, - requestClose: false, - routes: [] - }, - isModal: false, - leftDrawerOpen: false, - leftDrawerRoute: Routes.ChannelDrawer, - rightDrawerOpen: false, - rightDrawerRoute: null, - shouldRenderDrawer: false - }, + navigation: '', views: { channel: { drafts: {}, @@ -284,12 +266,8 @@ const state = { password: '' }, notification: null, - optionsModal: { - title: '', - options: [], - visible: false - }, root: { + appInitializing: false, hydrationComplete: false }, selectServer: { diff --git a/app/mattermost.js b/app/mattermost.js index 7ec56cb1b..a73e24a15 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -1,49 +1,169 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import 'harmony-reflect'; -import React from 'react'; +import 'babel-polyfill'; +import Orientation from 'react-native-orientation'; import {Provider} from 'react-redux'; +import {Navigation} from 'react-native-navigation'; +import DeviceNotification from 'react-native-push-notification'; +import { + Alert, + AppState, + InteractionManager, + Platform +} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; +import semver from 'semver'; -import Root from 'app/components/root'; -import Router from 'app/navigation/router'; +import {setAppState, setDeviceToken} from 'mattermost-redux/actions/general'; +import {logout} from 'mattermost-redux/actions/users'; +import {Client4} from 'mattermost-redux/client'; +import {General} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root'; +import {NavigationTypes, ViewTypes} from 'app/constants'; +import initialState from 'app/initial_state'; +import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; -import initialState from './initial_state'; +import Config from 'assets/config'; const store = configureStore(initialState); +registerScreens(store, Provider); -export default class Mattermost extends React.Component { - state = { - hydrated: false +export default class Mattermost { + constructor() { + this.isConfigured = false; + Orientation.lockToPortrait(); + this.unsubscribeFromStore = store.subscribe(this.listenForHydration); + AppState.addEventListener('change', this.handleAppStateChange); + EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); + EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset); + + this.handleAppStateChange(AppState.currentState); + Client4.setUserAgent(DeviceInfo.getUserAgent()); + } + + handleAppStateChange = (appState) => { + const {dispatch, getState} = store; + setAppState(appState === 'active')(dispatch, getState); }; - componentWillMount() { - this.unsubscribeFromStore = store.subscribe(this.listenForHydration); - } + handleConfigChanged = (serverVersion) => { + const {dispatch, getState} = store; + const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; + if (serverVersion) { + if (semver.valid(version) && semver.lt(version, Config.MinServerVersion)) { + Alert.alert( + 'Server upgrade required', + 'A server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.', + [{ + text: 'OK', + onPress: this.handleVersionUpgrade + }] + ); + } else { + loadConfigAndLicense(serverVersion)(dispatch, getState); + } + } + }; + + handleReset = () => { + Client4.serverVersion = ''; + this.startApp('fade'); + }; + + handleVersionUpgrade = async () => { + const {dispatch, getState} = store; + + // const {closeDrawers, logout, unrenderDrawer} = this.props.actions; + + Client4.serverVersion = ''; + + // closeDrawers(); + // unrenderDrawer(); + if (getState().entities.general.credentials.token) { + InteractionManager.runAfterInteractions(() => { + logout()(dispatch, getState); + }); + } + }; // We need to wait for hydration to occur before load the router. listenForHydration = () => { const state = store.getState(); if (state.views.root.hydrationComplete) { this.unsubscribeFromStore(); - this.setState({ - hydrated: true - }); + this.startApp(); } }; - render() { - if (!this.state.hydrated) { - return null; + configurePushNotifications = () => { + DeviceNotification.configure({ + onRegister: this.onRegisterDevice, + onNotification: this.onPushNotification, + senderID: Config.GooglePlaySenderId, + popInitialNotification: true, + requestPermissions: true + }); + }; + + onRegisterDevice = (data) => { + const prefix = Platform.OS === 'ios' ? General.PUSH_NOTIFY_APPLE_REACT_NATIVE : General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; + const {dispatch, getState} = store; + setDeviceToken(`${prefix}:${data.token}`)(dispatch, getState); + 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 + }; } - return ( - - - - - - ); - } + if (foreground) { + EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); + } else if (userInteraction) { + const {dispatch, getState} = store; + const state = getState(); + + if (!state.views.root.appInitializing) { + // go to notification if the app is initialized + goToNotification(notification)(dispatch, getState); + } else if (state.entities.general.credentials.token) { + // queue notification if app is not initialized but we are logged in + queueNotification(notification)(dispatch, getState); + } + } + }; + + startApp = (animationType = 'none') => { + this.configurePushNotifications(); + + Navigation.startSingleScreenApp({ + screen: { + screen: 'Root', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false + } + }, + animationType + }); + }; } diff --git a/app/navigation/navigation_modal.js b/app/navigation/navigation_modal.js deleted file mode 100644 index e973732a5..000000000 --- a/app/navigation/navigation_modal.js +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes, PureComponent} from 'react'; -import { - Animated, - InteractionManager -} from 'react-native'; - -const {View: AnimatedView} = Animated; - -const ANIMATION_TYPES = { - SlideFromBottom: 'slideFromBottom', - Fade: 'fade' -}; - -export default class NavigationModal extends PureComponent { - static propTypes = { - animationType: PropTypes.oneOf([ - ANIMATION_TYPES.SlideFromBottom, - ANIMATION_TYPES.Fade - ]), - children: PropTypes.node, - deviceHeight: PropTypes.number.isRequired, - deviceWidth: PropTypes.number.isRequired, - duration: PropTypes.number, - show: PropTypes.bool - } - - static defaultProps = { - animationType: ANIMATION_TYPES.SlideFromBottom, - duration: 400, - show: false - } - - constructor(props) { - super(props); - - const top = props.show ? 0 : props.deviceHeight; - this.state = { - top: new Animated.Value(top), - opacity: new Animated.Value(100) - }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.show) { - let currentRouteKeys; - const nextRouteKeys = nextProps.children.props.navigationState.routes.map((r) => r.key).join(''); - if (this.state.children) { - currentRouteKeys = this.state.children.props.navigationState.routes.map((r) => r.key).join(''); - } - - const routesDidChange = currentRouteKeys !== nextRouteKeys; - - // In order for the scene to be shown throughout the - // animated slide down we have to hang on to it by - // storing it in state - if (routesDidChange) { - this.setState({ - children: nextProps.children - }); - } - } - - if (nextProps.show !== this.props.show) { - const animationType = nextProps.show ? nextProps.animationType : this.props.animationType; - - if (animationType === ANIMATION_TYPES.SlideFromBottom) { - InteractionManager.runAfterInteractions(() => { - this.slideFromBottomAnimationRunner(nextProps); - }); - } else if (animationType === ANIMATION_TYPES.Fade) { - InteractionManager.runAfterInteractions(() => { - this.fadeAnimationRunner(nextProps); - }); - } - } - } - - fadeAnimationRunner = (nextProps) => { - if (nextProps.show) { - const setOpacityToZero = Animated.timing(this.state.opacity, { - toValue: 0, - duration: 0 - }); - const setTopToZero = Animated.timing(this.state.top, { - toValue: 0, - duration: 0 - }); - const setOpacityToFull = Animated.timing(this.state.opacity, { - toValue: 1, - duration: nextProps.duration - }); - Animated.sequence([ - setOpacityToZero, - setTopToZero, - setOpacityToFull - ]).start(() => { - // Once the scene has finished sliding down we can release the child scene - // which will unmount the scene correctly. - if (!this.props.show && !nextProps.show) { - this.setState({ - children: null - }); - } - }); - } else { - const setOpacityToZero = Animated.timing(this.state.opacity, { - toValue: 0, - duration: nextProps.duration - }); - const setTopToDeviceHeight = Animated.timing(this.state.top, { - toValue: this.props.deviceHeight, - duration: 0 - }); - const setOpacityToFull = Animated.timing(this.state.opacity, { - toValue: 1, - duration: 0 - }); - Animated.sequence([ - setOpacityToZero, - setTopToDeviceHeight, - setOpacityToFull - ]).start(() => { - // Once the scene has finished sliding down we can release the child scene - // which will unmount the scene correctly. - if (!this.props.show && !nextProps.show) { - this.setState({ - children: null - }); - } - }); - } - } - - slideFromBottomAnimationRunner = (nextProps) => { - const animateValue = nextProps.show ? 0 : this.props.deviceHeight; - - Animated.timing(this.state.top, { - toValue: animateValue, - duration: nextProps.duration - }).start(() => { - // Once the scene has finished sliding down we can release the child scene - // which will unmount the scene correctly. - if (!this.props.show && !nextProps.show) { - this.setState({ - children: null - }); - } - }); - } - - render() { - return ( - - {this.state.children} - - ); - } -} diff --git a/app/navigation/router.js b/app/navigation/router.js deleted file mode 100644 index 5668c791d..000000000 --- a/app/navigation/router.js +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {Component, PropTypes} from 'react'; -import { - Dimensions, - Easing, - InteractionManager, - Keyboard, - NavigationExperimental, - View -} from 'react-native'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; - -import {closeDrawers, goBack, openChannelDrawer} from 'app/actions/navigation'; -import Drawer from 'app/components/drawer'; -import FormattedText from 'app/components/formatted_text'; -import {RouteTransitions} from 'app/navigation/routes'; -import {getTheme} from 'app/selectors/preferences'; - -import EventEmitter from 'mattermost-redux/utils/event_emitter'; - -import NavigationModal from './navigation_modal'; - -const navigationPanResponder = NavigationExperimental.Card.CardStackPanResponder; - -const {width: deviceWidth, height: deviceHeight} = Dimensions.get('window'); - -class Router extends Component { - static propTypes = { - navigation: PropTypes.object, - theme: PropTypes.object, - actions: PropTypes.shape({ - closeDrawers: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired, - openChannelDrawer: PropTypes.func.isRequired - }).isRequired - }; - - headerEventSubscriptions = {}; - - state = { - deviceHeight, - deviceWidth - }; - - emitHeaderEvent = (key) => (event) => { - const subscription = this.headerEventSubscriptions[`${key}-${event}`]; - if (subscription) { - subscription(); - } - }; - - subscribeToHeaderEvent = (key) => (event, callback) => { - this.headerEventSubscriptions[`${key}-${event}`] = callback; - }; - - unsubscribeFromHeaderEvent = (key) => (event) => { - if (this.headerEventSubscriptions[event]) { - Reflect.deleteProperty(this.headerEventSubscriptions, `${key}-${event}`); - } - }; - - wrapHeaderComponent = (fx, emitter) => (props) => { - if (fx && props.scene.isActive) { - return fx(props, emitter, this.props.theme); - } - - return null; - }; - - extractNavigationProps = (route) => { - if (!route) { - return {}; - } - - let component = route.component; - if (!component) { - component = {}; - } - - return Object.assign({}, route.navigationProps, component.navigationProps); - }; - - renderTransition = (transitionProps) => { - const navigationProps = this.extractNavigationProps(transitionProps.scene.route); - let navBar = null; - if (!navigationProps.hideNavBar) { - const emitter = this.emitHeaderEvent(transitionProps.scene.route.key); - const renderLeftComponent = transitionProps.navigationState.index > 0 ? this.wrapHeaderComponent(navigationProps.renderBackButton, emitter) : this.wrapHeaderComponent(navigationProps.renderLeftComponent, emitter); - const renderTitleComponent = navigationProps.renderTitleComponent ? this.wrapHeaderComponent(navigationProps.renderTitleComponent, emitter) : this.renderTitle; - const renderRightComponent = this.wrapHeaderComponent(navigationProps.renderRightComponent, emitter); - - navBar = ( - - ); - } - - const renderedScenes = transitionProps.scenes.map((scene) => { - const cardProps = { - ...transitionProps, - scene - }; - - let style; - if (scene.route.transition === RouteTransitions.Horizontal) { - style = NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({ - ...cardProps - }); - } else { - // have to override shadow props for transparent modals - style = { - backgroundColor: 'transparent', - shadowColor: null, - shadowOffset: null, - shadowOpacity: null, - shadowRadius: null - }; - } - - let panHandlers = null; - if (navigationProps.allowSceneSwipe) { - panHandlers = navigationPanResponder.forHorizontal({ - ...cardProps, - gestureResponseDistance: (this.state.deviceWidth / 4), // sets the distance from the edge for swiping - onNavigateBack: this.props.actions.goBack - }); - } - return ( - - ); - }); - - return ( - - - {renderedScenes} - - {navBar} - - ); - }; - - renderTitle = ({scene}) => { - const navigationProps = this.extractNavigationProps(scene.route); - const title = navigationProps.title; - - if (!title) { - return null; - } - - return ( - - - - ); - }; - - renderScene = ({scene}) => { - return this.renderRoute(scene.route); - }; - - renderRoute = (route) => { - const SceneComponent = route.component; - - const emitterSubscriber = this.subscribeToHeaderEvent(route.key); - const emitterUnsubscriber = this.unsubscribeFromHeaderEvent(route.key); - - return ( - - ); - }; - - configureTransition = () => { - return { - duration: 300, - easing: Easing.inOut(Easing.ease) - }; - }; - - onLayout = (event) => { - if (event.nativeEvent.layout.width !== this.state.deviceWidth) { - this.setState({ - deviceHeight: event.nativeEvent.layout.height, - deviceWidth: event.nativeEvent.layout.width - }); - } - }; - - handleDrawerTween = (ratio) => { - const opacity = (ratio / 2); - - EventEmitter.emit('drawer_opacity', opacity); - - return { - mainOverlay: { - backgroundColor: this.props.theme.centerChannelBg, - opacity - }, - drawerOverlay: { - backgroundColor: ratio ? '#000' : '#FFF', - opacity: ratio ? (1 - ratio) / 2 : 1 - } - }; - }; - - handleLeftDrawerClose = () => { - setTimeout(() => { - InteractionManager.clearInteractionHandle(this.closeLeftHandle); - }); - }; - - handleLeftDrawerCloseStart = () => { - this.closeLeftHandle = InteractionManager.createInteractionHandle(); - }; - - handleLeftDrawerOpen = () => { - Keyboard.dismiss(); - setTimeout(() => { - InteractionManager.clearInteractionHandle(this.openLeftHandle); - }); - }; - - handleLeftDrawerOpenStart = () => { - this.props.actions.openChannelDrawer(); - this.openLeftHandle = InteractionManager.createInteractionHandle(); - }; - - render = () => { - const { - index, - isModal: modalVisible, - leftDrawerOpen, - leftDrawerRoute, - modal, - shouldRenderDrawer, - routes - } = this.props.navigation; - - const navigationProps = this.extractNavigationProps(routes[index]); - const modalNavigationProps = this.extractNavigationProps(modal.routes[0]); - - let leftDrawerContent; - if (leftDrawerRoute && shouldRenderDrawer) { - leftDrawerContent = this.renderRoute(leftDrawerRoute); - } - - return ( - - - - - - - - - - - ); - }; -} - -function mapStateToProps(state) { - return { - navigation: state.navigation, - theme: getTheme(state) - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - closeDrawers, - goBack, - openChannelDrawer - }, dispatch) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Router); diff --git a/app/navigation/routes.js b/app/navigation/routes.js deleted file mode 100644 index 6229ba6db..000000000 --- a/app/navigation/routes.js +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import { - About, - AccountNotifications, - AccountSettings, - ChannelView, - ChannelDrawer, - ChannelInfo, - ChannelMembers, - ChannelAddMembers, - CreateChannel, - EditPost, - ImagePreview, - LoadTeam, - Login, - LoginOptions, - Mfa, - MoreChannels, - MoreDirectMessages, - OptionsModal, - Settings, - Root, - Saml, - Search, - SelectServer, - SelectTeam, - Thread, - UserProfile -} from 'app/scenes'; - -import keyMirror from 'mattermost-redux/utils/key_mirror'; - -export const RouteTransitions = keyMirror({ - Horizontal: null -}); - -export const Routes = { - About: { - key: 'About', - transition: RouteTransitions.Horizontal, - component: About, - navigationProps: { - title: {id: 'about.titles', defaultMessage: 'About Mattermost'} - } - }, - AccountNotifications: { - key: 'About', - transition: RouteTransitions.Horizontal, - component: AccountNotifications, - navigationProps: { - title: {id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'} - } - }, - AccountSettings: { - key: 'AccountSettings', - transition: RouteTransitions.Horizontal, - component: AccountSettings, - navigationProps: { - title: {id: 'user.settings.modal.title', defaultMessage: 'Account Settings'} - } - }, - ChannelInfo: { - key: 'ChannelInfo', - transition: RouteTransitions.Horizontal, - component: ChannelInfo, - navigationProps: { - title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'} - } - }, - ChannelDrawer: { - key: 'ChannelDrawer', - component: ChannelDrawer - }, - ChannelMembers: { - key: 'ChannelMembers', - transition: RouteTransitions.Horizontal, - component: ChannelMembers, - navigationProps: { - title: {id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'} - } - }, - ChannelAddMembers: { - key: 'ChannelAddMembers', - transition: RouteTransitions.Horizontal, - component: ChannelAddMembers, - navigationProps: { - title: {id: 'channel_header.addMembers', defaultMessage: 'Add Members'} - } - }, - ChannelView: { - key: 'ChannelView', - transition: RouteTransitions.Horizontal, - component: ChannelView - }, - CreatePublicChannel: { - key: 'CreatePublicChannel', - component: CreateChannel, - transition: RouteTransitions.Horizontal, - navigationProps: { - title: {id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'} - } - }, - CreatePrivateChannel: { - key: 'CreatePrivateChannel', - component: CreateChannel, - navigationProps: { - title: {id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'} - } - }, - EditPost: { - key: 'EditPost', - component: EditPost, - navigationProps: { - title: {id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'} - } - }, - ImagePreview: { - key: 'ImagePreview', - component: ImagePreview - }, - LoadTeam: { - key: 'LoadTeam', - component: LoadTeam - }, - Login: { - key: 'Login', - transition: RouteTransitions.Horizontal, - component: Login, - navigationProps: { - title: {id: 'mobile.routes.login', defaultMessage: 'Login'} - } - }, - LoginOptions: { - key: 'LoginOptions', - transition: RouteTransitions.Horizontal, - component: LoginOptions, - navigationProps: { - title: {id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'} - } - }, - Mfa: { - key: 'Mfa', - transition: RouteTransitions.Horizontal, - component: Mfa, - navigationProps: { - title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'} - } - }, - MoreChannels: { - key: 'MoreChannels', - component: MoreChannels, - navigationProps: { - title: {id: 'more_channels.title', defaultMessage: 'More Channels'} - } - }, - MoreDirectMessages: { - key: 'MoreDirectMessages', - component: MoreDirectMessages, - navigationProps: { - title: {id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'} - } - }, - OptionsModal: { - key: 'OptionsModal', - component: OptionsModal - }, - Root: { - key: 'Root', - component: Root - }, - Saml: { - key: 'Saml', - transition: RouteTransitions.Horizontal, - component: Saml, - navigationProps: { - title: {id: 'mobile.routes.saml', defaultMessage: 'Single SignOn'} - } - }, - Search: { - key: 'Search', - transition: RouteTransitions.Horizontal, - component: Search - }, - SelectServer: { - key: 'SelectServer', - component: SelectServer, - navigationProps: { - title: {id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'}, - duration: 0, - modalAnimationType: 'fade' - } - }, - SelectTeam: { - key: 'SelectTeam', - component: SelectTeam, - transition: RouteTransitions.Horizontal, - navigationProps: { - title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'} - } - }, - Settings: { - key: 'Settings', - component: Settings, - navigationProps: { - title: {id: 'mobile.routes.settings', defaultMessage: 'Settings'} - } - }, - Thread: { - key: 'Thread', - transition: RouteTransitions.Horizontal, - component: Thread - }, - UserProfile: { - key: 'UserProfile', - transition: RouteTransitions.Horizontal, - component: UserProfile, - navigationProps: { - title: {id: 'mobile.routes.user_profile', defaultMessage: 'Profile'} - } - } -}; - -export default Routes; diff --git a/app/reducers/navigation/index.js b/app/reducers/navigation/index.js index 302ed6b64..47847dfc6 100644 --- a/app/reducers/navigation/index.js +++ b/app/reducers/navigation/index.js @@ -1,229 +1,16 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {NavigationExperimental} from 'react-native'; - -import {UserTypes} from 'mattermost-redux/action_types'; import {NavigationTypes} from 'app/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import Routes from 'app/navigation/routes'; - -const initialState = { - index: 0, - routes: [ - Routes.Root - ], - modal: { - index: 0, - requestClose: false, - routes: [] - }, - isModal: false, - leftDrawerOpen: false, - leftDrawerRoute: null, - rightDrawerOpen: false, - rightDrawerRoute: null, - shouldRenderDrawer: false -}; - -export default function(state = initialState, action) { +export default function(state = '', action) { switch (action.type) { - case NavigationTypes.NAVIGATION_PUSH: { - if (state.isModal) { - const modalState = NavigationExperimental.StateUtils.push(state.modal, {props: action.props, ...action.route}); - return { - ...state, - modal: modalState - }; - } - return NavigationExperimental.StateUtils.push(state, action.route); - } - - case NavigationTypes.NAVIGATION_POP: - if (!state.isModal && (state.leftDrawerOpen || state.rightDrawerOpen)) { - return { - ...state, - leftDrawerOpen: false, - rightDrawerOpen: false - }; - } - - if (state.isModal) { - const nextModalState = state.modal.routes.length > 1 ? - NavigationExperimental.StateUtils.pop(state.modal) : - { - index: 0, - routes: [] - }; - - nextModalState.requestClose = false; - - const isModal = nextModalState.routes.length > 0; - return { - ...state, - modal: nextModalState, - isModal - }; - } - - return NavigationExperimental.StateUtils.pop(state); - - case NavigationTypes.NAVIGATION_POP_TO_INDEX: { - let newState = {...state}; - - if (!newState.isModal && (newState.leftDrawerOpen || newState.rightDrawerOpen)) { - newState = { - ...newState, - leftDrawerOpen: false, - rightDrawerOpen: false - }; - } - - if (newState.isModal) { - newState = { - ...newState, - modal: { - index: 0, - routes: [] - }, - isModal: false - }; - } - - if (action.index === newState.index || action.index > newState.routes.length - 1) { - return newState; - } - - const nextRoutes = newState.routes.slice(0, action.index + 1); - - return { - ...newState, - index: action.index, - routes: nextRoutes - }; - } - - case NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER: - return { - ...state, - leftDrawerOpen: true, - leftDrawerRoute: action.route - }; - - case NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER: - return { - ...state, - rightDrawerOpen: true, - rightDrawerRoute: action.route - }; - - case NavigationTypes.NAVIGATION_CLOSE_DRAWERS: - return { - ...state, - leftDrawerOpen: false, - rightDrawerOpen: false - }; - - case NavigationTypes.NAVIGATION_JUMP: { - if (state.isModal) { - const modalState = NavigationExperimental.StateUtils.jumpTo(state.modal, action.key); - return { - ...state, - modal: modalState - }; - } - - return NavigationExperimental.StateUtils.jumpTo(state, action.key); - } - - case NavigationTypes.NAVIGATION_JUMP_TO_INDEX: { - if (state.isModal) { - const modalState = NavigationExperimental.StateUtils.jumpToIndex(state.modal, action.index); - return { - ...state, - modal: modalState - }; - } - - return NavigationExperimental.StateUtils.jumpToIndex(state, action.index); - } - - case NavigationTypes.NAVIGATION_RESET: - return { - ...state, - ...NavigationExperimental.StateUtils.reset(state, action.routes), - modal: { - index: 0, - requestClose: false, - routes: [] - }, - isModal: false, - leftDrawerOpen: false, - rightDrawerOpen: false - }; - - case NavigationTypes.NAVIGATION_REPLACE: { - if (state.isModal) { - const modalState = NavigationExperimental.StateUtils.replaceAtIndex(state.modal, state.modal.index, action.index); - return { - ...state, - modal: modalState - }; - } - - return NavigationExperimental.StateUtils.replaceAtIndex(state, state.index, action.route); - } - - case NavigationTypes.NAVIGATION_REPLACE_AT_ROOT: - return NavigationExperimental.StateUtils.replaceAtIndex(state, state.index, action.route); - - case NavigationTypes.NAVIGATION_MODAL: { - const modal = { - index: 0, - routes: [ - { - ...action.route - } - ] - }; - return { - ...state, - modal, - isModal: true - }; - } - - case NavigationTypes.NAVIGATION_CLOSE_MODAL: - return { - ...state, - modal: { - index: 0, - requestClose: false, - routes: [] - }, - isModal: false - }; - - case NavigationTypes.NAVIGATION_REQUEST_CLOSE_MODAL: { - const modalState = Object.assign({}, state.modal, {requestClose: true}); - - return { - ...state, - modal: modalState - }; - } - - case NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER: { - return { - ...state, - shouldRenderDrawer: action.data - }; - } - case UserTypes.LOGOUT_SUCCESS: - return NavigationExperimental.StateUtils.reset(state, initialState.routes, initialState.index); - - default: - return state; + EventEmitter.emit(NavigationTypes.NAVIGATION_RESET); + break; } + + return state; } diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index a513b2b7d..51579994b 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -2,7 +2,7 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {ChannelTypes, FileTypes, UserTypes} from 'mattermost-redux/action_types'; +import {ChannelTypes, FileTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; @@ -172,19 +172,7 @@ function loading(state = false, action) { } } -function appInitializing(state = true, action) { - switch (action.type) { - case ViewTypes.APPLICATION_INITIALIZED: - return false; - case UserTypes.RESET_LOGOUT_STATE: - return true; - default: - return state; - } -} - export default combineReducers({ - appInitializing, drafts, loading }); diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js index 78f3a9cc2..866492c5c 100644 --- a/app/reducers/views/index.js +++ b/app/reducers/views/index.js @@ -9,7 +9,6 @@ import fetchCache from './fetch_cache'; import i18n from './i18n'; import login from './login'; import notification from './notification'; -import optionsModal from './options_modal'; import root from './root'; import selectServer from './select_server'; import team from './team'; @@ -22,7 +21,6 @@ export default combineReducers({ i18n, login, notification, - optionsModal, root, selectServer, team, diff --git a/app/reducers/views/options_modal.js b/app/reducers/views/options_modal.js deleted file mode 100644 index dff5152c7..000000000 --- a/app/reducers/views/options_modal.js +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {combineReducers} from 'redux'; - -import {UserTypes} from 'mattermost-redux/action_types'; -import {ViewTypes} from 'app/constants'; - -function title(state = '', action) { - switch (action.type) { - case ViewTypes.OPTIONS_MODAL_CHANGED: - return action.data.title; - case UserTypes.LOGOUT_SUCCESS: - return ''; - default: - return state; - } -} - -function options(state = [], action) { - switch (action.type) { - case ViewTypes.OPTIONS_MODAL_CHANGED: - return action.data.options; - case UserTypes.LOGOUT_SUCCESS: - return []; - default: - return state; - } -} - -function visible(state = false, action) { - switch (action.type) { - case ViewTypes.OPTIONS_MODAL_CHANGED: - return action.data.visible; - case UserTypes.LOGOUT_SUCCESS: - return false; - default: - return state; - } -} - -export default combineReducers({ - title, - options, - visible -}); diff --git a/app/reducers/views/root.js b/app/reducers/views/root.js index 183b7ff13..802575b07 100644 --- a/app/reducers/views/root.js +++ b/app/reducers/views/root.js @@ -2,7 +2,22 @@ // See License.txt for license information. import {combineReducers} from 'redux'; + import {General} from 'mattermost-redux/constants'; +import {UserTypes} from 'mattermost-redux/action_types'; + +import {ViewTypes} from 'app/constants'; + +function appInitializing(state = true, action) { + switch (action.type) { + case ViewTypes.APPLICATION_INITIALIZED: + return false; + case UserTypes.RESET_LOGOUT_STATE: + return true; + default: + return state; + } +} function hydrationComplete(state = false, action) { switch (action.type) { @@ -14,5 +29,6 @@ function hydrationComplete(state = false, action) { } export default combineReducers({ + appInitializing, hydrationComplete }); diff --git a/app/scenes/account_notifications/save_notifications_button.js b/app/scenes/account_notifications/save_notifications_button.js deleted file mode 100644 index e4f7c7831..000000000 --- a/app/scenes/account_notifications/save_notifications_button.js +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes, PureComponent} from 'react'; -import {connect} from 'react-redux'; -import { - TouchableOpacity, - View -} from 'react-native'; - -import FormattedText from 'app/components/formatted_text'; -import Loading from 'app/components/loading'; - -import {getTheme} from 'app/selectors/preferences'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; - -class AccountNotifcationsButton extends PureComponent { - static propTypes = { - emitter: PropTypes.func.isRequired, - theme: PropTypes.object - }; - - static defaultProps = { - theme: {} - }; - - constructor(props) { - super(props); - - this.state = { - loading: false - }; - } - - componentWillMount() { - EventEmitter.on('saving_notify_props', this.onLoading); - } - - componentWillUnmount() { - EventEmitter.off('saving_notify_props', this.onLoading); - } - - onLoading = (loading) => { - this.setState({loading}); - }; - - onPress = () => { - this.props.emitter('save_notify_props'); - }; - - render() { - const {theme} = this.props; - const {loading} = this.state; - const color = theme.sidebarHeaderTextColor; - - if (loading) { - return ( - - ); - } - - return ( - - - - - - ); - } -} - -function mapStateToProps(state) { - return { - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(AccountNotifcationsButton); diff --git a/app/scenes/channel_drawer/channel_drawer.js b/app/scenes/channel_drawer/channel_drawer.js deleted file mode 100644 index e60df765e..000000000 --- a/app/scenes/channel_drawer/channel_drawer.js +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes, PureComponent} from 'react'; -import {InteractionManager} from 'react-native'; - -import ChannelDrawerList from 'app/components/channel_drawer_list'; - -export default class ChannelDrawer extends PureComponent { - static propTypes = { - actions: PropTypes.shape({ - closeDrawers: PropTypes.func.isRequired, - handleSelectChannel: PropTypes.func.isRequired, - viewChannel: PropTypes.func.isRequired, - markChannelAsRead: PropTypes.func.isRequired, - setChannelLoading: PropTypes.func.isRequired - }).isRequired, - currentTeam: PropTypes.object, - currentChannel: PropTypes.object, - channels: PropTypes.object, - channelMembers: PropTypes.object, - theme: PropTypes.object.isRequired - }; - - static defaultProps = { - currentTeam: {}, - currentChannel: {} - }; - - selectChannel = (id) => { - const { - actions, - currentChannel - } = this.props; - - const { - closeDrawers, - handleSelectChannel, - markChannelAsRead, - setChannelLoading, - viewChannel - } = actions; - - markChannelAsRead(id, currentChannel.id); - setChannelLoading(); - viewChannel(id); - closeDrawers(); - InteractionManager.runAfterInteractions(() => { - handleSelectChannel(id); - }); - }; - - render() { - const { - currentChannel, - currentTeam, - channels, - channelMembers, - theme - } = this.props; - - return ( - - ); - } -} diff --git a/app/scenes/channel_members/channel_members_title.js b/app/scenes/channel_members/channel_members_title.js deleted file mode 100644 index 9e7da86e0..000000000 --- a/app/scenes/channel_members/channel_members_title.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes} from 'react'; -import {connect} from 'react-redux'; -import {View} from 'react-native'; - -import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'app/selectors/preferences'; - -import FormattedText from 'app/components/formatted_text'; - -function ChannelMembersTitle(props) { - const {canManageUsers} = props; - return ( - - - - ); -} - -ChannelMembersTitle.propTypes = { - canManageUsers: PropTypes.bool.isRequired, - currentChannel: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired -}; - -ChannelMembersTitle.defaultProps = { - theme: {} -}; - -function mapStateToProps(state) { - return { - canManageUsers: canManageChannelMembers(state), - currentChannel: getCurrentChannel(state), - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(ChannelMembersTitle); diff --git a/app/scenes/channel_members/remove_member_button.js b/app/scenes/channel_members/remove_member_button.js deleted file mode 100644 index 5324b67d6..000000000 --- a/app/scenes/channel_members/remove_member_button.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes} from 'react'; -import {connect} from 'react-redux'; - -import ActionButton from 'app/components/action_button'; - -import {canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; - -function RemoveMemberButton(props) { - const {canManageUsers} = props; - if (!canManageUsers) { - return null; - } - - return ( - - ); -} - -RemoveMemberButton.propTypes = { - emitter: PropTypes.func.isRequired, - canManageUsers: PropTypes.bool.isRequired -}; - -function mapStateToProps(state) { - return { - canManageUsers: canManageChannelMembers(state) - }; -} - -export default connect(mapStateToProps)(RemoveMemberButton); diff --git a/app/scenes/create_channel/create_channel.js b/app/scenes/create_channel/create_channel.js deleted file mode 100644 index 3b7bcf523..000000000 --- a/app/scenes/create_channel/create_channel.js +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. -import React, {PropTypes, PureComponent} from 'react'; -import {injectIntl, intlShape} from 'react-intl'; -import { - Dimensions, - Keyboard, - Platform, - StyleSheet, - TouchableOpacity, - TouchableWithoutFeedback, - View, - findNodeHandle -} from 'react-native'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; - -import ErrorText from 'app/components/error_text'; -import FormattedText from 'app/components/formatted_text'; -import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; - -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -import {General, RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; - -import ActionButton from 'app/components/action_button'; - -class CreateChannel extends PureComponent { - static propTypes = { - intl: intlShape.isRequired, - createChannelRequest: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - channelType: PropTypes.string, - subscribeToHeaderEvent: React.PropTypes.func.isRequired, - unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired, - actions: PropTypes.shape({ - goBack: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired, - handleCreateChannel: PropTypes.func.isRequired - }) - }; - - static defaultProps = { - channelType: General.OPEN_CHANNEL - }; - - static navigationProps = { - renderLeftComponent: (props, emitter, theme) => { - return ( - emitter('close')} - > - - - ); - }, - renderRightComponent: (props, emitter) => { - return ( - - ); - } - }; - - constructor(props) { - super(props); - - this.state = { - displayName: '', - header: '', - purpose: '' - }; - } - - onCreateChannel = async () => { - Keyboard.dismiss(); - const {displayName, purpose, header} = this.state; - await this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType); - }; - - onDisplayNameChangeText = (displayName) => { - this.setState({displayName}); - if (displayName && displayName.length >= 2) { - this.emitCanCreateChannel(true); - } else { - this.emitCanCreateChannel(false); - } - }; - - onPurposeChangeText = (purpose) => { - this.setState({purpose}); - }; - - onHeaderChangeText = (header) => { - this.setState({header}); - }; - - emitCanCreateChannel = (enabled) => { - EventEmitter.emit('can_create_channel', enabled); - }; - - emitCreating = (loading) => { - EventEmitter.emit('creating_channel', loading); - }; - - blur = () => { - this.nameInput.refs.wrappedInstance.blur(); - this.purposeInput.refs.wrappedInstance.blur(); - this.headerInput.refs.wrappedInstance.blur(); - this.scroll.scrollToPosition(0, 0, true); - }; - - channelNameRef = (ref) => { - this.nameInput = ref; - }; - - channelPurposeRef = (ref) => { - this.purposeInput = ref; - }; - - channelHeaderRef = (ref) => { - this.headerInput = ref; - }; - - scrollRef = (ref) => { - this.scroll = ref; - }; - - lastTextRef = (ref) => { - this.lastText = ref; - }; - - scrollToEnd = () => { - this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText)); - }; - - componentWillMount() { - this.props.subscribeToHeaderEvent('close', this.props.actions.goBack); - this.props.subscribeToHeaderEvent('create_channel', this.onCreateChannel); - } - - componentWillReceiveProps(nextProps) { - const {createChannelRequest} = nextProps; - - if (this.props.createChannelRequest !== createChannelRequest) { - switch (createChannelRequest.status) { - case RequestStatus.STARTED: - this.emitCreating(true); - this.setState({error: null}); - break; - case RequestStatus.SUCCESS: - this.emitCreating(false); - this.setState({error: null}); - this.props.actions.closeModal(); - break; - case RequestStatus.FAILURE: - this.emitCreating(false); - this.setState({error: createChannelRequest.error}); - break; - } - } - } - - componentDidMount() { - this.emitCanCreateChannel(false); - } - - componentWillUnmount() { - this.props.unsubscribeFromHeaderEvent('close'); - this.props.unsubscribeFromHeaderEvent('create_channel'); - } - - render() { - const {channelType, theme} = this.props; - const {displayName, header, purpose, error} = this.state; - const {formatMessage} = this.props.intl; - const {height, width} = Dimensions.get('window'); - - const style = getStyleSheet(theme); - - let term; - if (channelType === General.OPEN_CHANNEL) { - term = formatMessage({id: 'channel_modal.channel', defaultMessage: 'Channel'}); - } else if (channelType === General.PRIVATE_CHANNEL) { - term = formatMessage({id: 'channel_modal.group', defaultMessage: 'Group'}); - } - - let displayError; - if (error) { - displayError = ( - - - - - - ); - } - - return ( - - - - {displayError} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } -} - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: theme.centerChannelBg - }, - scrollView: { - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), - paddingTop: 30 - }, - errorContainer: { - position: 'absolute' - }, - errorWrapper: { - justifyContent: 'center', - alignItems: 'center', - marginBottom: 10 - }, - inputContainer: { - marginTop: 10, - backgroundColor: '#fff' - }, - input: { - color: '#333', - fontSize: 14, - height: 40, - paddingHorizontal: 15 - }, - titleContainer30: { - flexDirection: 'row', - marginTop: 30 - }, - titleContainer15: { - flexDirection: 'row', - marginTop: 15 - }, - title: { - fontSize: 14, - color: theme.centerChannelColor, - marginLeft: 15 - }, - optional: { - color: changeOpacity(theme.centerChannelColor, 0.5), - fontSize: 14, - marginLeft: 5 - }, - helpText: { - fontSize: 14, - color: changeOpacity(theme.centerChannelColor, 0.5), - marginTop: 10, - marginHorizontal: 15 - } - }); -}); - -export default injectIntl(CreateChannel); diff --git a/app/scenes/index.js b/app/scenes/index.js deleted file mode 100644 index b1094a32b..000000000 --- a/app/scenes/index.js +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import About from './about'; -import AccountNotifications from './account_notifications'; -import AccountSettings from './account_settings'; -import Channel from './channel'; -import ChannelDrawer from './channel_drawer'; -import ChannelInfo from './channel_info'; -import ChannelMembers from './channel_members'; -import ChannelAddMembers from './channel_add_members'; -import CreateChannel from './create_channel'; -import EditPost from './edit_post'; -import ImagePreview from './image_preview'; -import LoadTeam from './load_team'; -import Login from './login'; -import LoginOptions from './login_options'; -import Mfa from './mfa'; -import MoreChannels from './more_channels'; -import MoreDirectMessages from './more_dms'; -import OptionsModal from './options_modal'; -import Settings from './settings'; -import Root from './root'; -import Search from './search'; -import SelectServer from './select_server'; -import SelectTeam from './select_team'; -import Thread from './thread'; -import UserProfile from './user_profile'; -import Saml from './saml'; - -module.exports = { - About, - AccountNotifications, - AccountSettings, - ChannelView: Channel, // Special case the name for this one to avoid ambiguity - ChannelDrawer, - ChannelInfo, - ChannelMembers, - ChannelAddMembers, - CreateChannel, - EditPost, - ImagePreview, - LoadTeam, - Login, - LoginOptions, - Mfa, - MoreChannels, - MoreDirectMessages, - OptionsModal, - Settings, - Root, - Search, - SelectServer, - SelectTeam, - Thread, - UserProfile, - Saml -}; diff --git a/app/scenes/login_options/index.js b/app/scenes/login_options/index.js deleted file mode 100644 index b16868e6f..000000000 --- a/app/scenes/login_options/index.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {bindActionCreators} from 'redux'; - -import navigationSceneConnect from '../navigationSceneConnect'; - -import {goToLogin, goToSaml} from 'app/actions/navigation'; - -import LoginOptions from './login_options'; - -function mapStateToProps(state) { - const {config, license} = state.entities.general; - return { - config, - license - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - goToLogin, - goToSaml - }, dispatch) - }; -} - -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(LoginOptions); diff --git a/app/scenes/more_channels/create_button.js b/app/scenes/more_channels/create_button.js deleted file mode 100644 index de522de64..000000000 --- a/app/scenes/more_channels/create_button.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes} from 'react'; -import {connect} from 'react-redux'; - -import ActionButton from 'app/components/action_button'; - -import {getTheme} from 'app/selectors/preferences'; - -import {General} from 'mattermost-redux/constants'; -import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; -import {showCreateOption} from 'mattermost-redux/utils/channel_utils'; -import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils'; - -function CreateButton(props) { - if (!props.canCreateChannels) { - return null; - } - - return ( - - ); -} - -CreateButton.propTypes = { - canCreateChannels: PropTypes.bool.isRequired, - emitter: PropTypes.func.isRequired, - theme: PropTypes.object -}; - -CreateButton.defaultProps = { - theme: {} -}; - -function mapStateToProps(state) { - const {config, license} = state.entities.general; - const roles = getCurrentUserRoles(state); - - return { - canCreateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)), - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(CreateButton); diff --git a/app/scenes/navigationSceneConnect.js b/app/scenes/navigationSceneConnect.js deleted file mode 100644 index dbcf90672..000000000 --- a/app/scenes/navigationSceneConnect.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -/* eslint-disable */ - -import {connect} from 'react-redux'; -import React, {PropTypes} from 'react'; -import { - Platform, - TouchableOpacity, - View -} from 'react-native'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -import FormattedText from 'app/components/formatted_text'; - -const defaults = { - renderBackButton: (props, emitter, theme) => { - return ( - - - - ); - }, - renderTitleComponent: (props, emitter, theme) => { - const navProps = props.scene.route.navigationProps || {}; - const title = navProps.title; - if (title) { - return ( - - - - ); - } - }, - headerStyle: { - flexDirection: 'row', - alignItems: 'center' - }, - allowSceneSwipe: true, - allowMenuSwipe: false -}; - -export default (stateProps, dispatchProps) => (WrappedComponent) => { - const componentNavigationProps = WrappedComponent.WrappedComponent ? WrappedComponent.WrappedComponent.navigationProps : WrappedComponent.navigationProps; - - WrappedComponent.navigationProps = Object.assign({}, defaults, componentNavigationProps); - - return connect(stateProps, dispatchProps)(WrappedComponent); -}; diff --git a/app/scenes/options_modal/index.js b/app/scenes/options_modal/index.js deleted file mode 100644 index 850055958..000000000 --- a/app/scenes/options_modal/index.js +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {bindActionCreators} from 'redux'; - -import {closeModal} from 'app/actions/navigation'; - -import navigationSceneConnect from '../navigationSceneConnect'; - -import OptionsModal from './options_modal'; - -function mapStateToProps(state, ownProps) { - return { - ...ownProps, - requestClose: state.navigation.modal.requestClose - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - closeModal - }, dispatch) - }; -} - -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(OptionsModal); diff --git a/app/scenes/root/index.js b/app/scenes/root/index.js deleted file mode 100644 index fcc350feb..000000000 --- a/app/scenes/root/index.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {bindActionCreators} from 'redux'; - -import navigationSceneConnect from '../navigationSceneConnect'; - -import {goToSelectServer} from 'app/actions/views/root'; -import {handleServerUrlChanged} from 'app/actions/views/select_server'; -import {goToLoadTeam} from 'app/actions/navigation'; - -import {loadMe} from 'mattermost-redux/actions/users'; - -import Root from './root'; - -function mapStateToProps(state, ownProps) { - return { - ...ownProps, - credentials: state.entities.general.credentials, - hydrationComplete: state.views.root.hydrationComplete, - logoutRequest: state.requests.users.logout, - loginRequest: state.requests.users.login - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - goToLoadTeam, - goToSelectServer, - handleServerUrlChanged, - loadMe - }, dispatch) - }; -} - -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Root); diff --git a/app/scenes/root/root.js b/app/scenes/root/root.js deleted file mode 100644 index bfb4b0aec..000000000 --- a/app/scenes/root/root.js +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes, PureComponent} from 'react'; -import Orientation from 'react-native-orientation'; -import SplashScreen from 'react-native-smart-splash-screen'; - -import {Client, Client4} from 'mattermost-redux/client'; -import {RequestStatus} from 'mattermost-redux/constants'; - -import Loading from 'app/components/loading'; -import {stripTrailingSlashes} from 'app/utils/url'; - -export default class Root extends PureComponent { - static propTypes = { - credentials: PropTypes.object, - logoutRequest: PropTypes.object, - loginRequest: PropTypes.object, - actions: PropTypes.shape({ - goToLoadTeam: PropTypes.func, - goToSelectServer: PropTypes.func, - handleServerUrlChanged: PropTypes.func.isRequired, - loadMe: PropTypes.func - }).isRequired - }; - - static navigationProps = { - hideNavBar: true - }; - - componentDidMount() { - Orientation.lockToPortrait(); - - this.loadStoreAndScene(this.props.credentials); - setTimeout(() => { - this.handleCloseSplashScreen(); - }, 1000); - } - - componentWillReceiveProps(nextProps) { - if (this.props.logoutRequest.status === RequestStatus.SUCCESS && - nextProps.logoutRequest.status === RequestStatus.NOT_STARTED) { - this.loadStoreAndScene(nextProps.credentials); - } - } - - handleCloseSplashScreen = (options = {}) => { - const opts = { - animationType: SplashScreen.animationType.scale, - duration: 850, - delay: 500 - }; - - SplashScreen.close(Object.assign({}, opts, options)); - }; - - loadStoreAndScene = (credentials = {}) => { - const {actions, loginRequest} = this.props; - const {goToLoadTeam, goToSelectServer, loadMe} = actions; - if (credentials.token && credentials.url) { - // Will probably need to make this optimistic since we - // assume that the stored token is good. - if (loginRequest.status === RequestStatus.NOT_STARTED) { - Client4.setToken(credentials.token); - Client4.setUrl(stripTrailingSlashes(credentials.url)); - Client.setUrl(stripTrailingSlashes(credentials.url)); - - loadMe().then(goToLoadTeam).catch(goToLoadTeam); - } else { - goToLoadTeam(); - } - } else { - goToSelectServer(); - } - }; - - render() { - return ; - } -} diff --git a/app/scenes/search/search.js b/app/scenes/search/search.js deleted file mode 100644 index 9bf671898..000000000 --- a/app/scenes/search/search.js +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import Button from 'app/components/button'; -import {GlobalStyles} from 'app/styles'; -import {Text, View} from 'react-native'; - -export default class Search extends React.Component { - static propTypes = { - actions: React.PropTypes.object.isRequired, - searchType: React.PropTypes.string.isRequired - }; - - render() { - return ( - - - {'This is a search for ' + this.props.searchType} - - - - ); - } -} diff --git a/app/scenes/thread/thread_title.js b/app/scenes/thread/thread_title.js deleted file mode 100644 index 380fc3bdc..000000000 --- a/app/scenes/thread/thread_title.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React, {PropTypes} from 'react'; -import {connect} from 'react-redux'; -import { - View -} from 'react-native'; - -import {General} from 'mattermost-redux/constants'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'app/selectors/preferences'; - -import FormattedText from 'app/components/formatted_text'; - -function ThreadTitle(props) { - const {currentChannel, theme} = props; - let label; - if (currentChannel.type === General.DM_CHANNEL) { - label = ( - - ); - } else { - const channelName = currentChannel.display_name; - label = ( - - ); - } - - return ( - - {label} - - ); -} - -ThreadTitle.propTypes = { - currentChannel: PropTypes.object.isRequired, - theme: PropTypes.object -}; - -ThreadTitle.defaultProps = { - theme: {} -}; - -function mapStateToProps(state) { - return { - currentChannel: getCurrentChannel(state), - theme: getTheme(state) - }; -} - -export default connect(mapStateToProps)(ThreadTitle); diff --git a/app/scenes/about/about.js b/app/screens/about/about.js similarity index 100% rename from app/scenes/about/about.js rename to app/screens/about/about.js diff --git a/app/scenes/about/index.js b/app/screens/about/index.js similarity index 73% rename from app/scenes/about/index.js rename to app/screens/about/index.js index 143031f84..7e71da098 100644 --- a/app/scenes/about/index.js +++ b/app/screens/about/index.js @@ -1,8 +1,9 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import {connect} from 'react-redux'; + import {getTheme} from 'app/selectors/preferences'; -import navigationSceneConnect from 'app/scenes/navigationSceneConnect'; import About from './about'; @@ -16,4 +17,4 @@ function mapStateToProps(state) { }; } -export default navigationSceneConnect(mapStateToProps)(About); +export default connect(mapStateToProps)(About); diff --git a/app/scenes/account_notifications/account_notifications.js b/app/screens/account_notifications/account_notifications.js similarity index 88% rename from app/scenes/account_notifications/account_notifications.js rename to app/screens/account_notifications/account_notifications.js index 5a0dfae3f..3d7fcefa6 100644 --- a/app/scenes/account_notifications/account_notifications.js +++ b/app/screens/account_notifications/account_notifications.js @@ -2,43 +2,43 @@ // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import { Keyboard, Platform, ScrollView, + StatusBar, StyleSheet, View } from 'react-native'; +import {Preferences, RequestStatus} from 'mattermost-redux/constants'; +import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils'; + +import Loading from 'app/components/loading'; import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {Preferences} from 'mattermost-redux/constants'; -import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils'; import Section from './section'; import SectionItem from './section_item'; -import SaveNotificationsButton from './save_notifications_button'; -const SAVE_NOTIFY_PROPS = 'save_notify_props'; - -export default class AccountNotifications extends PureComponent { +class AccountNotifications extends PureComponent { static propTypes = { actions: PropTypes.shape({ - goBack: PropTypes.func.isRequired, handleUpdateUserNotifyProps: PropTypes.func.isRequired }), config: PropTypes.object.isRequired, currentUser: PropTypes.object.isRequired, + intl: intlShape.isRequired, myPreferences: PropTypes.object.isRequired, - subscribeToHeaderEvent: PropTypes.func.isRequired, + navigator: PropTypes.object, theme: PropTypes.object.isRequired, - unsubscribeFromHeaderEvent: PropTypes.func.isRequired + updateMeRequest: PropTypes.object.isRequired }; - static navigationProps = { - renderRightComponent: (props, emitter) => { - return ; - } + saveButton = { + id: 'save-notifications', + showAsAction: 'never' }; constructor(props) { @@ -46,11 +46,16 @@ export default class AccountNotifications extends PureComponent { const {currentUser} = props; const notifyProps = currentUser.notify_props || {}; - this.setStateFromNotifyProps(notifyProps); - } - componentWillMount() { - this.props.subscribeToHeaderEvent(SAVE_NOTIFY_PROPS, this.saveUserNotifyProps); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + props.navigator.setButtons({ + rightButtons: [{ + ...this.saveButton, + title: props.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}) + }] + }); + + this.setStateFromNotifyProps(notifyProps); } componentDidMount() { @@ -60,24 +65,49 @@ export default class AccountNotifications extends PureComponent { } componentWillUnmount() { - this.props.unsubscribeFromHeaderEvent(SAVE_NOTIFY_PROPS); - if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); } } componentWillReceiveProps(nextProps) { - if (nextProps.currentUser !== this.props.currentUser) { - const {notify_props: notifyProps} = nextProps.currentUser; + const {currentUser, updateMeRequest} = nextProps; + if (currentUser !== this.props.currentUser) { + const {notify_props: notifyProps} = currentUser; this.setStateFromNotifyProps(notifyProps); } + + if (this.props.updateMeRequest !== updateMeRequest) { + switch (updateMeRequest.status) { + case RequestStatus.STARTED: + this.savingNotifyProps(true); + this.setState({error: null, saving: true}); + break; + case RequestStatus.SUCCESS: + this.savingNotifyProps(false); + this.setState({error: null, saving: false}); + this.close(); + break; + case RequestStatus.FAILURE: + this.savingNotifyProps(false); + this.setState({error: updateMeRequest.error, saving: false}); + break; + } + } } handleAndroidKeyboard = () => { this.refs.mention_keys.getWrappedInstance().blur(); }; + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'save-notifications') { + this.saveUserNotifyProps(); + } + } + }; + setStateFromNotifyProps = (notifyProps) => { const mentionKeys = (notifyProps.mention_keys || '').split(','); const usernameMentionIndex = mentionKeys.indexOf(this.props.currentUser.username); @@ -108,31 +138,37 @@ export default class AccountNotifications extends PureComponent { } else { this.state = {...newState}; } - } + }; toggleFirstNameMention = () => { this.setState({ first_name: (!(this.state.first_name === 'true')).toString() }); - } + }; toggleUsernameMention = () => { this.setState({ usernameMention: !this.state.usernameMention }); - } + }; toggleChannelMentions = () => { this.setState({ channel: (!(this.state.channel === 'true')).toString() }); - } + }; updateMentionKeys = (text) => { this.setState({ mention_keys: text }); - } + }; + + savingNotifyProps = (loading) => { + this.props.navigator.setButtons({ + rightButtons: [{...this.saveButton, disabled: loading}] + }); + }; setEmailNotifications = (value) => { const {config} = this.props; @@ -149,25 +185,25 @@ export default class AccountNotifications extends PureComponent { email, interval }); - } + }; setReplyNotifications = (value) => { this.setState({ comments: value }); - } + }; setMobilePush = (value) => { this.setState({ push: value }); - } + }; setMobilePushStatus = (value) => { this.setState({ push_status: value }); - } + }; saveUserNotifyProps = () => { let {mention_keys: mentionKeys, usernameMention, ...notifyProps} = this.state; //eslint-disable-line prefer-const @@ -189,9 +225,7 @@ export default class AccountNotifications extends PureComponent { mention_keys: mentionKeys, user_id: this.props.currentUser.id }); - - this.props.actions.goBack(); - } + }; buildMentionSection = () => { const {currentUser, theme} = this.props; @@ -258,7 +292,7 @@ export default class AccountNotifications extends PureComponent { ); - } + }; buildEmailSection = () => { const {config, theme} = this.props; @@ -348,7 +382,7 @@ export default class AccountNotifications extends PureComponent { } ); - } + }; buildReplySection = () => { const {theme} = this.props; @@ -393,7 +427,7 @@ export default class AccountNotifications extends PureComponent { /> ); - } + }; buildMobilePushSection = () => { const {config, theme} = this.props; @@ -443,7 +477,7 @@ export default class AccountNotifications extends PureComponent { /> ); - } + }; buildMobilePushStatusSection = () => { const {config, theme} = this.props; @@ -493,14 +527,28 @@ export default class AccountNotifications extends PureComponent { /> ); - } + }; + + close = () => { + this.props.navigator.pop({animated: true}); + }; render() { const {theme} = this.props; const style = getStyleSheet(theme); + if (this.state.saving) { + return ( + + + + + ); + } + return ( + { return StyleSheet.create({ + wrapper: { + flex: 1, + backgroundColor: theme.centerChannelBg + }, input: { color: theme.centerChannelColor, fontSize: 12, @@ -535,10 +587,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }, scrollViewContent: { paddingBottom: 30 - }, - wrapper: { - flex: 1, - backgroundColor: theme.centerChannelBg } }); }); + +export default injectIntl(AccountNotifications); diff --git a/app/scenes/account_notifications/index.js b/app/screens/account_notifications/index.js similarity index 75% rename from app/scenes/account_notifications/index.js rename to app/screens/account_notifications/index.js index ecb4aaf57..0b6e58fc5 100644 --- a/app/scenes/account_notifications/index.js +++ b/app/screens/account_notifications/index.js @@ -2,21 +2,22 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import {goBack} from 'app/actions/navigation'; -import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications'; -import {getTheme} from 'app/selectors/preferences'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import navigationSceneConnect from '../navigationSceneConnect'; +import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications'; +import {getTheme} from 'app/selectors/preferences'; import AccountNotifications from './account_notifications'; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { return { + ...ownProps, config: state.entities.general.config, myPreferences: state.entities.preferences.myPreferences, currentUser: getCurrentUser(state), + updateMeRequest: state.requests.users.updateMe, theme: getTheme(state) }; } @@ -24,10 +25,9 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - goBack, handleUpdateUserNotifyProps }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(AccountNotifications); +export default connect(mapStateToProps, mapDispatchToProps)(AccountNotifications); diff --git a/app/scenes/account_notifications/section.js b/app/screens/account_notifications/section.js similarity index 100% rename from app/scenes/account_notifications/section.js rename to app/screens/account_notifications/section.js diff --git a/app/scenes/account_notifications/section_item.js b/app/screens/account_notifications/section_item.js similarity index 100% rename from app/scenes/account_notifications/section_item.js rename to app/screens/account_notifications/section_item.js diff --git a/app/scenes/account_settings/account_settings.js b/app/screens/account_settings/account_settings.js similarity index 81% rename from app/scenes/account_settings/account_settings.js rename to app/screens/account_settings/account_settings.js index 65fd84b60..9e2df4175 100644 --- a/app/scenes/account_settings/account_settings.js +++ b/app/screens/account_settings/account_settings.js @@ -2,8 +2,10 @@ // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import { StyleSheet, + StatusBar, TouchableOpacity, View } from 'react-native'; @@ -13,31 +15,13 @@ import FormattedText from 'app/components/formatted_text'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -export default class AccountSettings extends PureComponent { +class AccountSettings extends PureComponent { static propTypes = { - actions: PropTypes.shape({ - goToAccountNotifications: PropTypes.func.isRequired - }), + intl: intlShape.isRequired, + navigator: PropTypes.object, theme: PropTypes.object.isRequired }; - static navigationProps = { - renderLeftComponent: (props, emitter, theme) => { - return ( - - - - ); - } - }; - // The enabled arg can be removed once all the scenes have been implemented. buildItemRow = (icon, id, defaultMessage, action, separator = true, enabled = false) => { if (!enabled) { @@ -79,11 +63,27 @@ export default class AccountSettings extends PureComponent { ); }; + goToAccountNotifications = () => { + const {intl, navigator, theme} = this.props; + navigator.push({ + backButtonTitle: '', + screen: 'AccountNotifications', + title: intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + renderItems = () => { return [ this.buildItemRow('gear', 'user.settings.modal.general', 'General', () => true, true, false), this.buildItemRow('lock', 'user.settings.modal.security', 'Security', () => true, true, false), - this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', this.props.actions.goToAccountNotifications, false, true), + this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', this.goToAccountNotifications, false, true), this.buildItemRow('mobile', 'user.settings.modal.display', 'Display', () => true, true, false), this.buildItemRow('wrench', 'user.settings.modal.advanced', 'Advanced', () => true, false, false) ]; @@ -95,6 +95,7 @@ export default class AccountSettings extends PureComponent { return ( + {this.renderItems()} @@ -154,3 +155,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { } }); }); + +export default injectIntl(AccountSettings); diff --git a/app/screens/account_settings/index.js b/app/screens/account_settings/index.js new file mode 100644 index 000000000..adf8be570 --- /dev/null +++ b/app/screens/account_settings/index.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; + +import {getTheme} from 'app/selectors/preferences'; + +import AccountSettings from './account_settings'; + +function mapStateToProps(state) { + return { + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(AccountSettings); diff --git a/app/scenes/channel/channel.js b/app/screens/channel/channel.js similarity index 58% rename from app/scenes/channel/channel.js rename to app/screens/channel/channel.js index 90e932674..0157c518e 100644 --- a/app/scenes/channel/channel.js +++ b/app/screens/channel/channel.js @@ -2,27 +2,32 @@ // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import { + Dimensions, NetInfo, Platform, StatusBar, + StyleSheet, View } from 'react-native'; +import {RequestStatus} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import ChannelDrawer from 'app/components/channel_drawer'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; import Loading from 'app/components/loading'; import OfflineIndicator from 'app/components/offline_indicator'; import PostTextbox from 'app/components/post_textbox'; import {preventDoubleTap} from 'app/utils/tap'; - -import {RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDrawerButton from './channel_drawer_button'; import ChannelTitle from './channel_title'; -import ChannelPostList from './channel_post_list'; +import ChannelPostList from './channel_post_list/index'; -export default class Channel extends PureComponent { +class Channel extends PureComponent { static propTypes = { actions: PropTypes.shape({ connection: PropTypes.func.isRequired, @@ -30,15 +35,14 @@ export default class Channel extends PureComponent { loadProfilesAndTeamMembersForDMSidebar: PropTypes.func.isRequired, selectFirstAvailableTeam: PropTypes.func.isRequired, selectInitialChannel: PropTypes.func.isRequired, - openChannelDrawer: PropTypes.func.isRequired, handlePostDraftChanged: PropTypes.func.isRequired, - goToChannelInfo: PropTypes.func.isRequired, initWebSocket: PropTypes.func.isRequired, closeWebSocket: PropTypes.func.isRequired, startPeriodicStatusUpdates: PropTypes.func.isRequired, - stopPeriodicStatusUpdates: PropTypes.func.isRequired, - renderDrawer: PropTypes.func.isRequired + stopPeriodicStatusUpdates: PropTypes.func.isRequired }).isRequired, + intl: intlShape.isRequired, + navigator: PropTypes.object, appState: PropTypes.bool, currentTeam: PropTypes.object, currentChannel: PropTypes.object, @@ -49,21 +53,9 @@ export default class Channel extends PureComponent { webSocketRequest: PropTypes.object }; - static navigationProps = { - allowMenuSwipe: true, - renderLeftComponent: (props, emitter) => { - return ; - }, - renderTitleComponent: (props, emitter) => { - return ; - } - }; - componentWillMount() { - this.props.subscribeToHeaderEvent('open_channel_drawer', () => preventDoubleTap(this.openChannelDrawer, this)); - this.props.subscribeToHeaderEvent('show_channel_info', () => preventDoubleTap(this.props.actions.goToChannelInfo)); - NetInfo.isConnected.addEventListener('change', this.handleConnectionChange); EventEmitter.on('leave_team', this.handleLeaveTeam); + NetInfo.isConnected.addEventListener('change', this.handleConnectionChange); NetInfo.isConnected.fetch().then(this.handleConnectionChange); if (this.props.currentTeam) { @@ -73,13 +65,13 @@ export default class Channel extends PureComponent { } componentDidMount() { - const {renderDrawer, startPeriodicStatusUpdates} = this.props.actions; + const {startPeriodicStatusUpdates} = this.props.actions; + try { startPeriodicStatusUpdates(); } catch (error) { // We don't care about the error } - renderDrawer(); } componentWillReceiveProps(nextProps) { @@ -90,13 +82,40 @@ export default class Channel extends PureComponent { } componentWillUnmount() { - this.props.actions.closeWebSocket(); - this.props.actions.stopPeriodicStatusUpdates(); - this.props.unsubscribeFromHeaderEvent('open_channel_drawer'); - NetInfo.isConnected.removeEventListener('change', this.handleConnectionChange); + const {closeWebSocket, stopPeriodicStatusUpdates} = this.props.actions; + EventEmitter.off('leave_team', this.handleLeaveTeam); + NetInfo.isConnected.removeEventListener('change', this.handleConnectionChange); + + closeWebSocket(); + stopPeriodicStatusUpdates(); } + attachPostTextbox = (ref) => { + this.postTextbox = ref; + }; + + blurPostTextBox = () => { + this.postTextbox.getWrappedInstance().getWrappedInstance().blur(); + }; + + goToChannelInfo = () => { + const {intl, navigator, theme} = this.props; + + navigator.push({ + screen: 'ChannelInfo', + title: intl.formatMessage({id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}), + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + handleConnectionChange = (isConnected) => { const {actions, webSocketRequest} = this.props; const {closeWebSocket, connection, initWebSocket} = actions; @@ -115,27 +134,6 @@ export default class Channel extends PureComponent { connection(isConnected); }; - loadChannels = (teamId) => { - const { - loadChannelsIfNecessary, - loadProfilesAndTeamMembersForDMSidebar, - selectInitialChannel - } = this.props.actions; - loadChannelsIfNecessary(teamId).then(() => { - loadProfilesAndTeamMembersForDMSidebar(teamId); - return selectInitialChannel(teamId); - }); - }; - - openChannelDrawer = () => { - this.postTextbox.getWrappedInstance().getWrappedInstance().blur(); - this.props.actions.openChannelDrawer(); - }; - - attachPostTextbox = (ref) => { - this.postTextbox = ref; - }; - handleDraftChanged = (value) => { this.props.actions.handlePostDraftChanged(this.props.currentChannel.id, value); }; @@ -144,10 +142,26 @@ export default class Channel extends PureComponent { this.props.actions.selectFirstAvailableTeam(); }; + loadChannels = (teamId) => { + const { + loadChannelsIfNecessary, + loadProfilesAndTeamMembersForDMSidebar, + selectInitialChannel + } = this.props.actions; + + loadChannelsIfNecessary(teamId).then(() => { + loadProfilesAndTeamMembersForDMSidebar(teamId); + return selectInitialChannel(teamId); + }).catch(() => { + return selectInitialChannel(teamId); + }); + }; + render() { const { currentTeam, currentChannel, + navigator, theme } = this.props; @@ -159,25 +173,67 @@ export default class Channel extends PureComponent { ); } - const channelDraft = this.props.drafts[this.props.currentChannel.id]; + const channelDraft = this.props.drafts[currentChannel.id]; + const style = getStyleFromTheme(theme); return ( - - - - - + + + + + + + + preventDoubleTap(this.goToChannelInfo, this)} + /> + + + + ); } } + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + header: { + backgroundColor: theme.sidebarHeaderBg, + flexDirection: 'row', + justifyContent: 'flex-start', + width: Dimensions.get('window').width, + zIndex: 10, + elevation: 2, + ...Platform.select({ + android: { + height: 46 + }, + ios: { + height: 64, + paddingTop: 20 + } + }) + } + }); +}); + +export default injectIntl(Channel); diff --git a/app/scenes/channel/channel_drawer_button.js b/app/screens/channel/channel_drawer_button.js similarity index 87% rename from app/scenes/channel/channel_drawer_button.js rename to app/screens/channel/channel_drawer_button.js index 9903b564c..9ca8c1e78 100644 --- a/app/scenes/channel/channel_drawer_button.js +++ b/app/screens/channel/channel_drawer_button.js @@ -10,10 +10,12 @@ import { TouchableOpacity, View } from 'react-native'; +import DeviceNotification from 'react-native-push-notification'; import Icon from 'react-native-vector-icons/Ionicons'; import Badge from 'app/components/badge'; import {getTheme} from 'app/selectors/preferences'; +import {preventDoubleTap} from 'app/utils/tap'; import {getUnreads} from 'mattermost-redux/selectors/entities/channels'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; @@ -21,7 +23,6 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter'; class ChannelDrawerButton extends PureComponent { static propTypes = { applicationInitializing: PropTypes.bool.isRequired, - emitter: PropTypes.func.isRequired, theme: PropTypes.object, messageCount: PropTypes.number, mentionCount: PropTypes.number @@ -56,6 +57,12 @@ class ChannelDrawerButton extends PureComponent { EventEmitter.on('drawer_opacity', this.setOpacity); } + componentWillReceiveProps(nextProps) { + if (nextProps.mentionCount !== this.props.mentionCount) { + DeviceNotification.setApplicationIconBadgeNumber(nextProps.mentionCount); + } + } + componentWillUnmount() { EventEmitter.off('drawer_opacity', this.setOpacity); } @@ -65,7 +72,7 @@ class ChannelDrawerButton extends PureComponent { }; handlePress = () => { - this.props.emitter('open_channel_drawer'); + EventEmitter.emit('open_channel_drawer'); }; render() { @@ -88,7 +95,7 @@ class ChannelDrawerButton extends PureComponent { count={badgeCount} minHeight={5} minWidth={5} - onPress={this.handlePress} + onPress={() => preventDoubleTap(this.handlePress, this)} /> ); } @@ -114,7 +121,7 @@ class ChannelDrawerButton extends PureComponent { const style = StyleSheet.create({ container: { - flex: 1 + width: 40 }, wrapper: { alignItems: 'center', @@ -151,7 +158,7 @@ const style = StyleSheet.create({ function mapStateToProps(state) { return { - applicationInitializing: state.views.channel.appInitializing, + applicationInitializing: state.views.root.appInitializing, theme: getTheme(state), ...getUnreads(state) }; diff --git a/app/scenes/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js similarity index 80% rename from app/scenes/channel/channel_post_list/channel_post_list.js rename to app/screens/channel/channel_post_list/channel_post_list.js index 0446141a5..ef9462ba1 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -2,6 +2,7 @@ // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import { Animated, Dimensions, @@ -17,17 +18,19 @@ import PostListRetry from 'app/components/post_list_retry'; const {View: AnimatedView} = Animated; const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window'); -export default class ChannelPostList extends PureComponent { +class ChannelPostList extends PureComponent { static propTypes = { actions: PropTypes.shape({ loadPostsIfNecessary: PropTypes.func.isRequired, getPostsBefore: PropTypes.func.isRequired, - goToThread: PropTypes.func.isRequired + selectPost: PropTypes.func.isRequired }).isRequired, applicationInitializing: PropTypes.bool.isRequired, channel: PropTypes.object.isRequired, channelIsLoading: PropTypes.bool, + intl: intlShape.isRequired, myMember: PropTypes.object.isRequired, + navigator: PropTypes.object, postsRequests: PropTypes.shape({ getPosts: PropTypes.object.isRequired, getPostsBefore: PropTypes.object.isRequired, @@ -105,7 +108,36 @@ export default class ChannelPostList extends PureComponent { }; goToThread = (post) => { - this.props.actions.goToThread(post.channel_id, post.root_id || post.id); + const {actions, channel, intl, navigator, theme} = this.props; + const channelId = post.channel_id; + const rootId = (post.root_id || post.id); + + actions.selectPost(rootId); + + let title; + if (channel.type === General.DM_CHANNEL) { + title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'}); + } else { + const channelName = channel.display_name; + title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName}); + } + + navigator.push({ + screen: 'Thread', + title, + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + passProps: { + channelId, + rootId + } + }); }; render() { @@ -114,6 +146,7 @@ export default class ChannelPostList extends PureComponent { applicationInitializing, channel, channelIsLoading, + navigator, posts, postsRequests, theme, @@ -141,6 +174,7 @@ export default class ChannelPostList extends PureComponent { currentUserId={this.props.myMember.user_id} lastViewedAt={this.state.lastViewedAt} channel={channel} + navigator={navigator} /> ); } else { @@ -167,3 +201,5 @@ export default class ChannelPostList extends PureComponent { ); } } + +export default injectIntl(ChannelPostList); diff --git a/app/scenes/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js similarity index 93% rename from app/scenes/channel/channel_post_list/index.js rename to app/screens/channel/channel_post_list/index.js index ebe2e6dc9..22ecb7792 100644 --- a/app/scenes/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -4,11 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {createSelector} from 'reselect'; -import {getPostsBefore} from 'mattermost-redux/actions/posts'; +import {selectPost, getPostsBefore} from 'mattermost-redux/actions/posts'; import {getAllPosts, getPostsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts'; import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {goToThread} from 'app/actions/navigation'; import {loadPostsIfNecessary} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; @@ -67,7 +66,7 @@ const getPostsInCurrentChannelWithReplyProps = createSelector( function mapStateToProps(state, ownProps) { return { ...ownProps, - applicationInitializing: state.views.channel.appInitializing, + applicationInitializing: state.views.root.appInitializing, channelIsLoading: state.views.channel.loading, myMember: getMyCurrentChannelMembership(state), postsRequests: state.requests.posts, @@ -82,7 +81,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ loadPostsIfNecessary, getPostsBefore, - goToThread + selectPost }, dispatch) }; } diff --git a/app/scenes/channel/channel_title.js b/app/screens/channel/channel_title.js similarity index 89% rename from app/scenes/channel/channel_title.js rename to app/screens/channel/channel_title.js index 9e9b4bb05..b8056a656 100644 --- a/app/scenes/channel/channel_title.js +++ b/app/screens/channel/channel_title.js @@ -11,6 +11,7 @@ import { import Icon from 'react-native-vector-icons/FontAwesome'; import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; + import {getTheme} from 'app/selectors/preferences'; function ChannelTitle(props) { @@ -33,8 +34,8 @@ function ChannelTitle(props) { return ( props.emitter('show_channel_info')} + style={{flexDirection: 'row', flex: 1, marginRight: 40}} + onPress={props.onPress} > { - return ( - - ); - } + addButton = { + disabled: true, + id: 'add-members', + showAsAction: 'never' }; constructor(props) { @@ -76,10 +64,12 @@ class ChannelAddMembers extends PureComponent { selectedMembers: {}, showNoResults: false }; - } + this.addButton.title = props.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}); - componentWillMount() { - this.props.subscribeToHeaderEvent('add_members', this.handleAddMembersPress); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + props.navigator.setButtons({ + rightButtons: [this.addButton] + }); } componentDidMount() { @@ -96,8 +86,6 @@ class ChannelAddMembers extends PureComponent { } componentWillUnmount() { - this.props.unsubscribeFromHeaderEvent('add_members'); - if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); } @@ -128,20 +116,46 @@ class ChannelAddMembers extends PureComponent { switch (addChannelMemberStatus) { case RequestStatus.STARTED: this.emitAdding(true); - this.setState({error: null}); + this.setState({error: null, adding: true, canSelect: false}); break; case RequestStatus.SUCCESS: this.emitAdding(false); - this.setState({error: null}); - this.props.actions.goBack(); + this.setState({error: null, adding: false, canSelect: false}); + this.close(); break; case RequestStatus.FAILURE: this.emitAdding(false); + this.setState({adding: false, canSelect: true}); break; } } } + cancelSearch = () => { + this.setState({ + searching: false, + term: null, + page: 0, + profiles: markSelectedProfiles(this.props.membersNotInChannel, this.state.selectedMembers) + }); + }; + + close = () => { + this.props.navigator.pop({animated: true}); + }; + + emitAdding = (loading) => { + this.props.navigator.setButtons({ + rightButtons: [{...this.addButton, disabled: loading}] + }); + }; + + emitCanAddMembers = (enabled) => { + this.props.navigator.setButtons({ + rightButtons: [{...this.addButton, disabled: !enabled}] + }); + }; + handleAddMembersPress = () => { const {selectedMembers} = this.state; const {actions, currentChannel} = this.props; @@ -159,6 +173,20 @@ class ChannelAddMembers extends PureComponent { this.onSearchButtonPress(); }; + handleRowSelect = (id) => { + const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); + + if (Object.values(selectedMembers).filter((selected) => selected).length) { + this.emitCanAddMembers(true); + } else { + this.emitCanAddMembers(false); + } + this.setState({ + profiles: markSelectedProfiles(this.state.profiles, selectedMembers), + selectedMembers + }); + }; + loadMoreMembers = () => { const {actions, loadMoreRequestStatus, currentChannel, currentTeam} = this.props; const {next, searching} = this.state; @@ -178,37 +206,22 @@ class ChannelAddMembers extends PureComponent { } }; - handleRowSelect = (id) => { - const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); - - if (Object.values(selectedMembers).filter((selected) => selected).length) { - this.emitCanAddMembers(true); - } else { - this.emitCanAddMembers(false); + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'add-members') { + this.handleAddMembersPress(); + } } - this.setState({ - profiles: markSelectedProfiles(this.state.profiles, selectedMembers), - selectedMembers - }); - }; - - emitCanAddMembers = (enabled) => { - EventEmitter.emit('can_add_members', enabled); - }; - - emitAdding = (loading) => { - this.setState({canSelect: false}); - EventEmitter.emit('adding_members', loading); - }; - - searchBarRef = (ref) => { - this.searchBar = ref; }; onSearchButtonPress = () => { this.searchBar.blur(); }; + searchBarRef = (ref) => { + this.searchBar = ref; + }; + searchProfiles = (event) => { const term = event.nativeEvent.text.toLowerCase(); const {actions, currentChannel, currentTeam} = this.props; @@ -225,26 +238,27 @@ class ChannelAddMembers extends PureComponent { } }; - cancelSearch = () => { - this.setState({ - searching: false, - term: null, - page: 0, - profiles: markSelectedProfiles(this.props.membersNotInChannel, this.state.selectedMembers) - }); - }; - render() { const {intl, loadMoreRequestStatus, searchRequestStatus, preferences, theme} = this.props; - const {profiles, searching} = this.state; + const {adding, profiles, searching} = this.state; const {formatMessage} = intl; const isLoading = (loadMoreRequestStatus === RequestStatus.STARTED) || (searchRequestStatus === RequestStatus.STARTED); const style = getStyleFromTheme(theme); const more = searching ? () => true : this.loadMoreMembers; + if (adding) { + return ( + + + + + ); + } + return ( + diff --git a/app/scenes/channel_add_members/index.js b/app/screens/channel_add_members/index.js similarity index 89% rename from app/scenes/channel_add_members/index.js rename to app/screens/channel_add_members/index.js index 4a8669d4c..91c122ae8 100644 --- a/app/scenes/channel_add_members/index.js +++ b/app/screens/channel_add_members/index.js @@ -2,11 +2,9 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; - -import navigationSceneConnect from '../navigationSceneConnect'; +import {connect} from 'react-redux'; import {handleAddChannelMembers} from 'app/actions/views/channel_add_members'; -import {goBack} from 'app/actions/navigation'; import {getTheme} from 'app/selectors/preferences'; import {getCurrentChannel, getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels'; import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences'; @@ -41,11 +39,10 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ getTeamStats, getProfilesNotInChannel, - goBack, handleAddChannelMembers, searchProfiles }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelAddMembers); +export default connect(mapStateToProps, mapDispatchToProps)(ChannelAddMembers); diff --git a/app/scenes/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js similarity index 85% rename from app/scenes/channel_info/channel_info.js rename to app/screens/channel_info/channel_info.js index e111d6f54..19c8f4297 100644 --- a/app/scenes/channel_info/channel_info.js +++ b/app/screens/channel_info/channel_info.js @@ -6,6 +6,7 @@ import {injectIntl, intlShape} from 'react-intl'; import { Alert, ScrollView, + StatusBar, StyleSheet, View } from 'react-native'; @@ -25,6 +26,7 @@ class ChannelInfo extends PureComponent { currentChannel: PropTypes.object.isRequired, currentChannelCreatorName: PropTypes.string, currentChannelMemberCount: PropTypes.number, + navigator: PropTypes.object, status: PropTypes.string, theme: PropTypes.object.isRequired, isCurrent: PropTypes.bool.isRequired, @@ -35,9 +37,6 @@ class ChannelInfo extends PureComponent { closeGMChannel: PropTypes.func.isRequired, deleteChannel: PropTypes.func.isRequired, getChannelStats: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired, - goToChannelAddMembers: PropTypes.func.isRequired, - goToChannelMembers: PropTypes.func.isRequired, leaveChannel: PropTypes.func.isRequired, markFavorite: PropTypes.func.isRequired, unmarkFavorite: PropTypes.func.isRequired @@ -63,6 +62,41 @@ class ChannelInfo extends PureComponent { } } + goToChannelAddMembers = () => { + const {intl, navigator, theme} = this.props; + navigator.push({ + backButtonTitle: '', + screen: 'ChannelAddMembers', + title: intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + + goToChannelMembers = () => { + const {canManageUsers, intl, navigator, theme} = this.props; + const id = canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers'; + const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members'; + + navigator.push({ + backButtonTitle: '', + screen: 'ChannelMembers', + title: intl.formatMessage({id, defaultMessage}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + handleDeleteOrLeave(eventType) { const {formatMessage} = this.props.intl; const channel = this.props.currentChannel; @@ -79,7 +113,9 @@ class ChannelInfo extends PureComponent { defaultMessage: 'Are you sure you want to leave the {term} {name}?' }; onPressAction = () => { - this.props.actions.leaveChannel(channel, true).then(this.props.actions.goBack); + this.props.actions.leaveChannel(channel, true).then(() => { + this.props.navigator.pop({animated: true}); + }); }; } else if (eventType === 'delete') { title = {id: 'mobile.channel_info.alertTitleDeleteChannel', defaultMessage: 'Delete {term}'}; @@ -88,7 +124,9 @@ class ChannelInfo extends PureComponent { defaultMessage: 'Are you sure you want to delete the {term} {name}?' }; onPressAction = () => { - this.props.actions.deleteChannel(channel.id).then(this.props.actions.goBack); + this.props.actions.deleteChannel(channel.id).then(() => { + this.props.navigator.pop({animated: true}); + }); }; } @@ -113,14 +151,18 @@ class ChannelInfo extends PureComponent { handleClose = () => { const {currentChannel, isCurrent, isFavorite} = this.props; const channel = Object.assign({}, currentChannel, {isCurrent}, {isFavorite}); - const {closeDMChannel, closeGMChannel, goBack} = this.props.actions; + const {closeDMChannel, closeGMChannel} = this.props.actions; switch (channel.type) { case General.DM_CHANNEL: - closeDMChannel(channel).then(goBack); + closeDMChannel(channel).then(() => { + this.props.navigator.pop({animated: true}); + }); break; case General.GM_CHANNEL: - closeGMChannel(channel).then(goBack); + closeGMChannel(channel).then(() => { + this.props.navigator.pop({animated: true}); + }); break; } }; @@ -178,6 +220,7 @@ class ChannelInfo extends PureComponent { return ( + @@ -217,7 +260,7 @@ class ChannelInfo extends PureComponent { **/ } preventDoubleTap(this.props.actions.goToChannelMembers)} + action={() => preventDoubleTap(this.goToChannelMembers)} defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'} detail={currentChannelMemberCount} icon='users' @@ -228,7 +271,7 @@ class ChannelInfo extends PureComponent { preventDoubleTap(this.props.actions.goToChannelAddMembers)} + action={() => preventDoubleTap(this.goToChannelAddMembers)} defaultMessage='Add Members' icon='user-plus' textId='channel_header.addMembers' diff --git a/app/scenes/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js similarity index 100% rename from app/scenes/channel_info/channel_info_header.js rename to app/screens/channel_info/channel_info_header.js diff --git a/app/scenes/channel_info/channel_info_row.js b/app/screens/channel_info/channel_info_row.js similarity index 100% rename from app/scenes/channel_info/channel_info_row.js rename to app/screens/channel_info/channel_info_row.js diff --git a/app/scenes/channel_info/index.js b/app/screens/channel_info/index.js similarity index 89% rename from app/scenes/channel_info/index.js rename to app/screens/channel_info/index.js index 997a6ed6b..d2ae03097 100644 --- a/app/scenes/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -2,8 +2,8 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import {goToChannelMembers, goToChannelAddMembers, goBack} from 'app/actions/navigation'; import { closeDMChannel, closeGMChannel, @@ -11,7 +11,6 @@ import { markFavorite, unmarkFavorite } from 'app/actions/views/channel'; -import navigationSceneConnect from 'app/scenes/navigationSceneConnect'; import {getTheme} from 'app/selectors/preferences'; import {getChannelStats, deleteChannel} from 'mattermost-redux/actions/channels'; @@ -67,9 +66,6 @@ function mapDispatchToProps(dispatch) { closeGMChannel, deleteChannel, getChannelStats, - goBack, - goToChannelAddMembers, - goToChannelMembers, leaveChannel, markFavorite, unmarkFavorite @@ -77,4 +73,4 @@ function mapDispatchToProps(dispatch) { }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelInfo); +export default connect(mapStateToProps, mapDispatchToProps)(ChannelInfo); diff --git a/app/scenes/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js similarity index 86% rename from app/scenes/channel_members/channel_members.js rename to app/screens/channel_members/channel_members.js index e6ac81f7a..db4ea5f0f 100644 --- a/app/scenes/channel_members/channel_members.js +++ b/app/screens/channel_members/channel_members.js @@ -7,11 +7,13 @@ import { Keyboard, Platform, InteractionManager, + StatusBar, StyleSheet, View } from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; +import Loading from 'app/components/loading'; import MemberList from 'app/components/custom_list'; import SearchBar from 'app/components/search_bar'; import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list'; @@ -19,12 +21,8 @@ import MemberListRow from 'app/components/custom_list/member_list_row'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {General, RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {displayUsername, filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils'; -import ChannelMembersTitle from './channel_members_title'; -import RemoveMemberButton from './remove_member_button'; - class ChannelMembers extends PureComponent { static propTypes = { intl: intlShape.isRequired, @@ -33,28 +31,23 @@ class ChannelMembers extends PureComponent { currentChannelMembers: PropTypes.array.isRequired, currentChannelMemberCount: PropTypes.number.isRequired, currentUserId: PropTypes.string.isRequired, + navigator: PropTypes.object, preferences: PropTypes.object, requestStatus: PropTypes.string, searchRequestStatus: PropTypes.string, removeMembersStatus: PropTypes.string, canManageUsers: PropTypes.bool.isRequired, - subscribeToHeaderEvent: React.PropTypes.func, - unsubscribeFromHeaderEvent: React.PropTypes.func, actions: PropTypes.shape({ getProfilesInChannel: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired, handleRemoveChannelMembers: PropTypes.func.isRequired, searchProfiles: PropTypes.func.isRequired }) }; - static navigationProps = { - renderTitleComponent: () => { - return ; - }, - renderRightComponent: (props, emitter) => { - return ; - } + removeButton = { + disabled: true, + id: 'remove-members', + showAsAction: 'never' }; constructor(props) { @@ -71,10 +64,14 @@ class ChannelMembers extends PureComponent { selectedMembers: {}, showNoResults: false }; - } + this.removeButton.title = props.intl.formatMessage({id: 'channel_members_modal.remove', defaultMessage: 'Remove'}); - componentWillMount() { - this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + if (props.canManageUsers) { + props.navigator.setButtons({ + rightButtons: [this.removeButton] + }); + } } componentDidMount() { @@ -90,8 +87,6 @@ class ChannelMembers extends PureComponent { } componentWillUnmount() { - this.props.unsubscribeFromHeaderEvent('remove_members'); - if (Platform.OS === 'android') { Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard); } @@ -122,20 +117,51 @@ class ChannelMembers extends PureComponent { switch (removeMembersStatus) { case RequestStatus.STARTED: this.emitRemoving(true); - this.setState({error: null}); + this.setState({error: null, canSelect: false, removing: true}); break; case RequestStatus.SUCCESS: this.emitRemoving(false); - this.setState({error: null}); - this.props.actions.goBack(); + this.setState({error: null, canSelect: false, removing: false}); + this.close(); break; case RequestStatus.FAILURE: this.emitRemoving(false); + this.setState({canSelect: true, removing: false}); break; } } } + cancelSearch = () => { + this.setState({ + searching: false, + term: null, + page: 0, + profiles: markSelectedProfiles(this.props.currentChannelMembers, this.state.selectedMembers) + }); + }; + + close = () => { + this.props.navigator.pop({animated: true}); + }; + + emitCanRemoveMembers = (enabled) => { + this.props.navigator.setButtons({ + rightButtons: [{...this.removeButton, disabled: !enabled}] + }); + }; + + emitRemoving = (loading) => { + this.setState({canSelect: false, removing: loading}); + this.props.navigator.setButtons({ + rightButtons: [{...this.removeButton, disabled: loading}] + }); + }; + + handleAndroidKeyboard = () => { + this.onSearchButtonPress(); + }; + handleRemoveMembersPress = () => { const {selectedMembers} = this.state; const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]); @@ -173,13 +199,17 @@ class ChannelMembers extends PureComponent { ); }; - handleAndroidKeyboard = () => { - this.onSearchButtonPress(); - }; - - removeMembers = (membersToRemove) => { - const {actions, currentChannel} = this.props; - actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove); + handleRowSelect = (id) => { + const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); + if (Object.values(selectedMembers).filter((selected) => selected).length) { + this.emitCanRemoveMembers(true); + } else { + this.emitCanRemoveMembers(false); + } + this.setState({ + profiles: markSelectedProfiles(this.state.profiles, selectedMembers), + selectedMembers + }); }; loadMoreMembers = () => { @@ -201,58 +231,21 @@ class ChannelMembers extends PureComponent { } }; - handleRowSelect = (id) => { - const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]}); - if (Object.values(selectedMembers).filter((selected) => selected).length) { - this.emitCanRemoveMembers(true); - } else { - this.emitCanRemoveMembers(false); + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + if (event.id === 'remove-members') { + this.handleRemoveMembersPress(); + } } - this.setState({ - profiles: markSelectedProfiles(this.state.profiles, selectedMembers), - selectedMembers - }); - }; - - emitCanRemoveMembers = (enabled) => { - EventEmitter.emit('can_remove_members', enabled); - }; - - emitRemoving = (loading) => { - this.setState({canSelect: false}); - EventEmitter.emit('removing_members', loading); - }; - - searchBarRef = (ref) => { - this.searchBar = ref; }; onSearchButtonPress = () => { this.searchBar.blur(); }; - searchProfiles = (event) => { - const term = event.nativeEvent.text.toLowerCase(); - - if (term) { - this.setState({searching: true, term}); - clearTimeout(this.searchTimeoutId); - - this.searchTimeoutId = setTimeout(() => { - this.props.actions.searchProfiles(term, {in_channel_id: this.props.currentChannel.id}); - }, General.SEARCH_TIMEOUT_MILLISECONDS); - } else { - this.cancelSearch(); - } - }; - - cancelSearch = () => { - this.setState({ - searching: false, - term: null, - page: 0, - profiles: markSelectedProfiles(this.props.currentChannelMembers, this.state.selectedMembers) - }); + removeMembers = (membersToRemove) => { + const {actions, currentChannel} = this.props; + actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove); }; renderMemberRow = (user, sectionId, rowId, preferences, theme, selectable, onPress, onSelect) => { @@ -281,17 +274,46 @@ class ChannelMembers extends PureComponent { ); }; + searchBarRef = (ref) => { + this.searchBar = ref; + }; + + searchProfiles = (event) => { + const term = event.nativeEvent.text.toLowerCase(); + + if (term) { + this.setState({searching: true, term}); + clearTimeout(this.searchTimeoutId); + + this.searchTimeoutId = setTimeout(() => { + this.props.actions.searchProfiles(term, {in_channel_id: this.props.currentChannel.id}); + }, General.SEARCH_TIMEOUT_MILLISECONDS); + } else { + this.cancelSearch(); + } + }; + render() { const {canManageUsers, intl, preferences, requestStatus, searchRequestStatus, theme} = this.props; const {formatMessage} = intl; - const {profiles, searching, showNoResults} = this.state; + const {profiles, removing, searching, showNoResults} = this.state; const isLoading = (requestStatus === RequestStatus.STARTED) || (requestStatus.status === RequestStatus.NOT_STARTED) || (searchRequestStatus === RequestStatus.STARTED); const more = searching ? () => true : this.loadMoreMembers; const style = getStyleFromTheme(theme); + if (removing) { + return ( + + + + + ); + } + return ( + diff --git a/app/scenes/channel_members/index.js b/app/screens/channel_members/index.js similarity index 88% rename from app/scenes/channel_members/index.js rename to app/screens/channel_members/index.js index 3852b7477..e16a34032 100644 --- a/app/scenes/channel_members/index.js +++ b/app/screens/channel_members/index.js @@ -2,10 +2,8 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import navigationSceneConnect from '../navigationSceneConnect'; - -import {goBack} from 'app/actions/navigation'; import {handleRemoveChannelMembers} from 'app/actions/views/channel_members'; import {getTheme} from 'app/selectors/preferences'; import {getCurrentChannel, getCurrentChannelStats, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels'; @@ -36,11 +34,10 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ getProfilesInChannel, - goBack, handleRemoveChannelMembers, searchProfiles }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelMembers); +export default connect(mapStateToProps, mapDispatchToProps)(ChannelMembers); diff --git a/app/screens/create_channel/create_channel.js b/app/screens/create_channel/create_channel.js new file mode 100644 index 000000000..4060d39a0 --- /dev/null +++ b/app/screens/create_channel/create_channel.js @@ -0,0 +1,410 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; +import { + Dimensions, + Keyboard, + InteractionManager, + Platform, + StatusBar, + StyleSheet, + TouchableWithoutFeedback, + View, + findNodeHandle +} from 'react-native'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; + +import {General, RequestStatus} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import ErrorText from 'app/components/error_text'; +import FormattedText from 'app/components/formatted_text'; +import Loading from 'app/components/loading'; +import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +class CreateChannel extends PureComponent { + static propTypes = { + intl: intlShape.isRequired, + createChannelRequest: PropTypes.object.isRequired, + navigator: PropTypes.object, + theme: PropTypes.object.isRequired, + channelType: PropTypes.string, + closeButton: PropTypes.object, + actions: PropTypes.shape({ + handleCreateChannel: PropTypes.func.isRequired + }) + }; + + static defaultProps = { + channelType: General.OPEN_CHANNEL + }; + + leftButton = { + id: 'close-new-channel' + }; + + rightButton = { + id: 'create-channel', + disabled: true, + showAsAction: 'never' + }; + + constructor(props) { + super(props); + + this.state = { + displayName: '', + header: '', + purpose: '' + }; + this.rightButton.title = props.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}); + + if (props.channelType === General.PRIVATE_CHANNEL) { + this.left = {...this.leftButton, icon: props.closeButton}; + } + + const buttons = { + rightButtons: [this.rightButton] + }; + + if (this.left) { + buttons.leftButtons = [this.left]; + } + + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + props.navigator.setButtons(buttons); + } + + componentDidMount() { + this.emitCanCreateChannel(false); + } + + componentWillReceiveProps(nextProps) { + const {createChannelRequest} = nextProps; + + if (this.props.createChannelRequest !== createChannelRequest) { + switch (createChannelRequest.status) { + case RequestStatus.STARTED: + this.emitCreating(true); + this.setState({error: null, creating: true}); + break; + case RequestStatus.SUCCESS: + EventEmitter.emit('close_channel_drawer'); + InteractionManager.runAfterInteractions(() => { + this.emitCreating(false); + this.setState({error: null, creating: false}); + this.close(false); + }); + break; + case RequestStatus.FAILURE: + this.emitCreating(false); + this.setState({error: createChannelRequest.error, creating: false}); + break; + } + } + } + + blur = () => { + this.nameInput.refs.wrappedInstance.blur(); + this.purposeInput.refs.wrappedInstance.blur(); + this.headerInput.refs.wrappedInstance.blur(); + this.scroll.scrollToPosition(0, 0, true); + }; + + channelNameRef = (ref) => { + this.nameInput = ref; + }; + + channelPurposeRef = (ref) => { + this.purposeInput = ref; + }; + + channelHeaderRef = (ref) => { + this.headerInput = ref; + }; + + close = (goBack = false) => { + if (goBack) { + this.props.navigator.pop({animated: true}); + } else { + this.props.navigator.dismissAllModals({ + animationType: 'slide-down' + }); + } + }; + + emitCanCreateChannel = (enabled) => { + const buttons = { + rightButtons: [{...this.rightButton, disabled: !enabled}] + }; + + if (this.left) { + buttons.leftButtons = [this.left]; + } + + this.props.navigator.setButtons(buttons); + }; + + emitCreating = (loading) => { + const buttons = { + rightButtons: [{...this.rightButton, disabled: loading}] + }; + + if (this.left) { + buttons.leftButtons = [this.left]; + } + + this.props.navigator.setButtons(buttons); + }; + + lastTextRef = (ref) => { + this.lastText = ref; + }; + + onCreateChannel = () => { + Keyboard.dismiss(); + const {displayName, purpose, header} = this.state; + this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType); + }; + + onDisplayNameChangeText = (displayName) => { + this.setState({displayName}); + if (displayName && displayName.length >= 2) { + this.emitCanCreateChannel(true); + } else { + this.emitCanCreateChannel(false); + } + }; + + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + switch (event.id) { + case 'close-new-channel': + this.close(this.props.channelType === General.OPEN_CHANNEL); + break; + case 'create-channel': + this.onCreateChannel(); + break; + } + } + }; + + onPurposeChangeText = (purpose) => { + this.setState({purpose}); + }; + + onHeaderChangeText = (header) => { + this.setState({header}); + }; + + scrollRef = (ref) => { + this.scroll = ref; + }; + + scrollToEnd = () => { + this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText)); + }; + + render() { + const {theme} = this.props; + const {creating, displayName, header, purpose, error} = this.state; + const {height, width} = Dimensions.get('window'); + + const style = getStyleSheet(theme); + + if (creating) { + return ( + + + + + ); + } + + let displayError; + if (error) { + displayError = ( + + + + + + ); + } + + return ( + + + + + + {displayError} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.centerChannelBg + }, + scrollView: { + flex: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), + paddingTop: 30 + }, + errorContainer: { + position: 'absolute' + }, + errorWrapper: { + justifyContent: 'center', + alignItems: 'center', + marginBottom: 10 + }, + inputContainer: { + marginTop: 10, + backgroundColor: '#fff' + }, + input: { + color: '#333', + fontSize: 14, + height: 40, + paddingHorizontal: 15 + }, + titleContainer30: { + flexDirection: 'row', + marginTop: 30 + }, + titleContainer15: { + flexDirection: 'row', + marginTop: 15 + }, + title: { + fontSize: 14, + color: theme.centerChannelColor, + marginLeft: 15 + }, + optional: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 14, + marginLeft: 5 + }, + helpText: { + fontSize: 14, + color: changeOpacity(theme.centerChannelColor, 0.5), + marginTop: 10, + marginHorizontal: 15 + }, + navTitle: { + ...Platform.select({ + android: { + fontSize: 18 + }, + ios: { + fontSize: 15, + fontWeight: 'bold' + } + }) + } + }); +}); + +export default injectIntl(CreateChannel); diff --git a/app/scenes/create_channel/index.js b/app/screens/create_channel/index.js similarity index 74% rename from app/scenes/create_channel/index.js rename to app/screens/create_channel/index.js index f1d815191..e4ce5311c 100644 --- a/app/scenes/create_channel/index.js +++ b/app/screens/create_channel/index.js @@ -2,10 +2,8 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import navigationSceneConnect from '../navigationSceneConnect'; - -import {goBack, closeModal} from 'app/actions/navigation'; import {handleCreateChannel} from 'app/actions/views/create_channel'; import {getTheme} from 'app/selectors/preferences'; @@ -26,11 +24,9 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - goBack, - closeModal, handleCreateChannel }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(CreateChannel); +export default connect(mapStateToProps, mapDispatchToProps)(CreateChannel); diff --git a/app/scenes/edit_post/edit_post.js b/app/screens/edit_post/edit_post.js similarity index 70% rename from app/scenes/edit_post/edit_post.js rename to app/screens/edit_post/edit_post.js index cc9d228b4..4cb2bd70d 100644 --- a/app/scenes/edit_post/edit_post.js +++ b/app/screens/edit_post/edit_post.js @@ -1,106 +1,60 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import { Dimensions, Platform, + StatusBar, StyleSheet, - TouchableOpacity, View } from 'react-native'; -import ActionButton from 'app/components/action_button'; import ErrorText from 'app/components/error_text'; -import FormattedText from 'app/components/formatted_text'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; +import Loading from 'app/components/loading'; import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {RequestStatus} from 'mattermost-redux/constants'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; -export default class EditPost extends PureComponent { +class EditPost extends PureComponent { static propTypes = { - editPostRequest: PropTypes.object.isRequired, - post: PropTypes.object.isRequired, - theme: PropTypes.object.isRequired, - subscribeToHeaderEvent: React.PropTypes.func.isRequired, - unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired, actions: PropTypes.shape({ - closeModal: PropTypes.func.isRequired, editPost: PropTypes.func.isRequired - }) + }), + closeButton: PropTypes.object, + editPostRequest: PropTypes.object.isRequired, + intl: intlShape.isRequired, + navigator: PropTypes.object, + post: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired }; - static navigationProps = { - renderLeftComponent: (props, emitter, theme) => { - return ( - emitter('close')} - > - - - ); - }, - renderRightComponent: (props, emitter) => { - return ( - - ); - } + leftButton = { + id: 'close-edit-post' + }; + + rightButton = { + id: 'edit-post', + showAsAction: 'always' }; constructor(props) { super(props); + this.state = {message: props.post.message}; + this.rightButton.title = props.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}); + + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + props.navigator.setButtons({ + leftButtons: [{...this.leftButton, icon: props.closeButton}], + rightButtons: [this.rightButton] + }); } - onEditPost = async () => { - const {message} = this.state; - const post = Object.assign({}, this.props.post, {message}); - await this.props.actions.editPost(post); - }; - - onPostChangeText = (message) => { - this.setState({message}); - if (message) { - this.emitCanEditPost(true); - } else { - this.emitCanEditPost(false); - } - }; - - emitCanEditPost = (enabled) => { - EventEmitter.emit('can_edit_post', enabled); - }; - - emitEditing = (loading) => { - EventEmitter.emit('editing_post', loading); - }; - - focus = () => { - this.messageInput.refs.wrappedInstance.focus(); - }; - - messageRef = (ref) => { - this.messageInput = ref; - }; - - componentWillMount() { - this.props.subscribeToHeaderEvent('close', this.props.actions.closeModal); - this.props.subscribeToHeaderEvent('edit_post', this.onEditPost); + componentDidMount() { + this.focus(); } componentWillReceiveProps(nextProps) { @@ -110,29 +64,44 @@ export default class EditPost extends PureComponent { switch (editPostRequest.status) { case RequestStatus.STARTED: this.emitEditing(true); - this.setState({error: null}); + this.setState({error: null, editing: true}); break; case RequestStatus.SUCCESS: this.emitEditing(false); - this.setState({error: null}); - this.props.actions.closeModal(); + this.setState({error: null, editing: false}); + this.close(); break; case RequestStatus.FAILURE: this.emitEditing(false); - this.setState({error: editPostRequest.error}); + this.setState({error: editPostRequest.error, editing: false}); break; } } } - componentDidMount() { - this.focus(); - } + close = () => { + this.props.navigator.dismissModal({ + animationType: 'slide-down' + }); + }; - componentWillUnmount() { - this.props.unsubscribeFromHeaderEvent('close'); - this.props.unsubscribeFromHeaderEvent('edit_post'); - } + emitCanEditPost = (enabled) => { + this.props.navigator.setButtons({ + leftButtons: [this.leftButton], + rightButtons: [{...this.rightButton, disabled: !enabled}] + }); + }; + + emitEditing = (loading) => { + this.props.navigator.setButtons({ + leftButtons: [this.leftButton], + rightButtons: [{...this.rightButton, disabled: loading}] + }); + }; + + focus = () => { + this.messageInput.refs.wrappedInstance.focus(); + }; handleSubmit = () => { // Workaround for android as the multiline is not working @@ -148,13 +117,54 @@ export default class EditPost extends PureComponent { } }; + messageRef = (ref) => { + this.messageInput = ref; + }; + + onEditPost = () => { + const {message} = this.state; + const post = Object.assign({}, this.props.post, {message}); + this.props.actions.editPost(post); + }; + + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + switch (event.id) { + case 'close-edit-post': + this.close(); + break; + case 'edit-post': + this.onEditPost(); + break; + } + } + }; + + onPostChangeText = (message) => { + this.setState({message}); + if (message) { + this.emitCanEditPost(true); + } else { + this.emitCanEditPost(false); + } + }; + render() { const {theme} = this.props; - const {message, error} = this.state; + const {editing, message, error} = this.state; const {height, width} = Dimensions.get('window'); const style = getStyleSheet(theme); + if (editing) { + return ( + + + + + ); + } + let displayError; if (error) { displayError = ( @@ -172,6 +182,7 @@ export default class EditPost extends PureComponent { style={style.container} keyboardVerticalOffset={0} > + {displayError} @@ -231,3 +242,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { } }); }); + +export default injectIntl(EditPost); diff --git a/app/scenes/edit_post/index.js b/app/screens/edit_post/index.js similarity index 75% rename from app/scenes/edit_post/index.js rename to app/screens/edit_post/index.js index 800a78efc..a2c3b5e14 100644 --- a/app/scenes/edit_post/index.js +++ b/app/screens/edit_post/index.js @@ -2,10 +2,8 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import navigationSceneConnect from '../navigationSceneConnect'; - -import {closeModal} from 'app/actions/navigation'; import {getTheme} from 'app/selectors/preferences'; import {editPost} from 'mattermost-redux/actions/posts'; @@ -26,10 +24,9 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - closeModal, editPost }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(EditPost); +export default connect(mapStateToProps, mapDispatchToProps)(EditPost); diff --git a/app/scenes/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js similarity index 97% rename from app/scenes/image_preview/image_preview.js rename to app/screens/image_preview/image_preview.js index a8d442046..4835a7bf8 100644 --- a/app/scenes/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -37,21 +37,15 @@ const STATUSBAR_HEIGHT = Platform.select({ export default class ImagePreview extends PureComponent { static propTypes = { actions: PropTypes.shape({ - addFileToFetchCache: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired + addFileToFetchCache: PropTypes.func.isRequired }), fetchCache: PropTypes.object.isRequired, fileId: PropTypes.string.isRequired, files: PropTypes.array.isRequired, + navigator: PropTypes.object, theme: PropTypes.object.isRequired }; - static navigationProps = { - allowSceneSwipe: false, - hideNavBar: true, - modalAnimationType: 'fade' - }; - constructor(props) { super(props); @@ -111,6 +105,10 @@ export default class ImagePreview extends PureComponent { } } + close = () => { + this.props.navigator.dismissModal({animationType: 'none'}); + }; + mainViewMoveShouldSetPanResponderCapture = (evt, gestureState) => { if (gestureState.numberActiveTouches === 2 || this.state.isZooming) { return false; @@ -124,24 +122,24 @@ export default class ImagePreview extends PureComponent { } return false; - } + }; mainViewPanResponderRelease = (evt, gestureState) => { if (Math.abs(gestureState.dy) > DRAG_VERTICAL_THRESHOLD_END) { - this.props.actions.goBack(); + this.close(); } else { this.setHeaderAndFileInfoVisible(true); Animated.spring(this.state.drag, { toValue: {x: 0, y: 0} }).start(); } - } + }; handleClose = () => { if (this.state.showFileInfo) { - this.props.actions.goBack(); + this.close(); } - } + }; handleImageTap = () => { /*if (!this.lastPress) { @@ -157,7 +155,7 @@ export default class ImagePreview extends PureComponent { }*/ this.setHeaderAndFileInfoVisible(!this.state.showFileInfo); - } + }; setHeaderAndFileInfoVisible = (show) => { this.setState({ @@ -174,7 +172,7 @@ export default class ImagePreview extends PureComponent { toValue: opacity, duration: 300 }).start(); - } + }; handleScroll = (event) => { if (event.nativeEvent.contentOffset.x % this.state.deviceWidth === 0) { @@ -183,11 +181,11 @@ export default class ImagePreview extends PureComponent { pagingEnabled: true }); } - } + }; attachScrollView = (c) => { this.scrollView = c; - } + }; onLayout = (event) => { if (event.nativeEvent.layout.width !== this.state.deviceWidth) { @@ -196,7 +194,7 @@ export default class ImagePreview extends PureComponent { deviceWidth: event.nativeEvent.layout.width }); } - } + }; imageIsZooming = (zooming) => { if (zooming !== this.state.isZooming) { @@ -205,7 +203,7 @@ export default class ImagePreview extends PureComponent { isZooming: zooming }); } - } + }; render() { const maxImageHeight = this.state.deviceHeight - STATUSBAR_HEIGHT; diff --git a/app/scenes/image_preview/index.js b/app/screens/image_preview/index.js similarity index 77% rename from app/scenes/image_preview/index.js rename to app/screens/image_preview/index.js index 69dba2e25..1c6bdbcd7 100644 --- a/app/scenes/image_preview/index.js +++ b/app/screens/image_preview/index.js @@ -2,16 +2,14 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; -import {goBack} from 'app/actions/navigation'; import {addFileToFetchCache} from 'app/actions/views/file_preview'; import {getTheme} from 'app/selectors/preferences'; import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files'; import ImagePreview from './image_preview'; -import navigationSceneConnect from '../navigationSceneConnect'; - function makeMapStateToProps() { const getFilesForPost = makeGetFilesForPost(); return function mapStateToProps(state, ownProps) { @@ -27,10 +25,9 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - addFileToFetchCache, - goBack + addFileToFetchCache }, dispatch) }; } -export default navigationSceneConnect(makeMapStateToProps, mapDispatchToProps)(ImagePreview); +export default connect(makeMapStateToProps, mapDispatchToProps)(ImagePreview); diff --git a/app/scenes/image_preview/zoomable_image.js b/app/screens/image_preview/zoomable_image.js similarity index 100% rename from app/scenes/image_preview/zoomable_image.js rename to app/screens/image_preview/zoomable_image.js diff --git a/app/screens/index.js b/app/screens/index.js new file mode 100644 index 000000000..028f079cc --- /dev/null +++ b/app/screens/index.js @@ -0,0 +1,72 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {Navigation} from 'react-native-navigation'; + +import About from 'app/screens/about'; +import AccountSettings from 'app/screens/account_settings'; +import AccountNotifications from 'app/screens/account_notifications'; +import Channel from 'app/screens/channel'; +import ChannelAddMembers from 'app/screens/channel_add_members'; +import ChannelInfo from 'app/screens/channel_info'; +import ChannelMembers from 'app/screens/channel_members'; +import CreateChannel from 'app/screens/create_channel'; +import EditPost from 'app/screens/edit_post'; +import ImagePreview from 'app/screens/image_preview'; +import LoadTeam from 'app/screens/load_team'; +import Login from 'app/screens/login'; +import LoginOptions from 'app/screens/login_options'; +import Mfa from 'app/screens/mfa'; +import MoreChannels from 'app/screens/more_channels'; +import MoreDirectMessages from 'app/screens/more_dms'; +import Notification from 'app/screens/notification'; +import OptionsModal from 'app/screens/options_modal'; +import Root from 'app/screens/root'; +import Saml from 'app/screens/saml'; +import SelectServer from 'app/screens/select_server'; +import SelectTeam from 'app/screens/select_team'; +import Settings from 'app/screens/settings'; +import Thread from 'app/screens/thread'; +import UserProfile from 'app/screens/user_profile'; + +import IntlWrapper from 'app/components/root'; + +function wrapWithContextProvider(Comp) { + return (props) => { //eslint-disable-line react/display-name + const {navigator} = props; //eslint-disable-line react/prop-types + return ( + + + + ); + }; +} + +export function registerScreens(store, Provider) { + Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider); + Navigation.registerComponent('AccountSettings', () => wrapWithContextProvider(AccountSettings), store, Provider); + Navigation.registerComponent('AccountNotifications', () => wrapWithContextProvider(AccountNotifications), store, Provider); + Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider); + Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider); + Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider); + Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider); + Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider); + Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider); + Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider); + Navigation.registerComponent('LoadTeam', () => LoadTeam, store, Provider); + Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider); + Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(LoginOptions), store, Provider); + Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider); + Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider); + Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider); + Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider); + Navigation.registerComponent('Notification', () => Notification, store, Provider); + Navigation.registerComponent('Root', () => Root, store, Provider); + Navigation.registerComponent('SAML', () => wrapWithContextProvider(Saml), store, Provider); + Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider); + Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider); + Navigation.registerComponent('Settings', () => wrapWithContextProvider(Settings), store, Provider); + Navigation.registerComponent('Thread', () => wrapWithContextProvider(Thread), store, Provider); + Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(UserProfile), store, Provider); +} diff --git a/app/scenes/load_team/index.js b/app/screens/load_team/index.js similarity index 76% rename from app/scenes/load_team/index.js rename to app/screens/load_team/index.js index b1238b9d5..0673e6d06 100644 --- a/app/scenes/load_team/index.js +++ b/app/screens/load_team/index.js @@ -2,20 +2,20 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; -import {goToChannelView} from 'app/actions/views/load_team'; +import {initialize} from 'app/actions/views/load_team'; import {clearNotification, goToNotification} from 'app/actions/views/root'; import {handleTeamChange} from 'app/actions/views/select_team'; import {getTheme} from 'app/selectors/preferences'; -import navigationSceneConnect from '../navigationSceneConnect'; - import LoadTeam from './load_team'; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { return { + ...ownProps, config: state.entities.general.config, theme: getTheme(state), teamsRequest: state.requests.teams.getMyTeams, @@ -30,11 +30,11 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ clearNotification, - goToChannelView, goToNotification, - handleTeamChange + handleTeamChange, + initialize }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(LoadTeam); +export default connect(mapStateToProps, mapDispatchToProps)(LoadTeam); diff --git a/app/scenes/load_team/load_team.js b/app/screens/load_team/load_team.js similarity index 58% rename from app/scenes/load_team/load_team.js rename to app/screens/load_team/load_team.js index 6b1041db6..2e00385cc 100644 --- a/app/scenes/load_team/load_team.js +++ b/app/screens/load_team/load_team.js @@ -2,7 +2,7 @@ // See License.txt for license information. import React, {PropTypes, PureComponent} from 'react'; -import {View} from 'react-native'; +import {StatusBar, View} from 'react-native'; import {RequestStatus} from 'mattermost-redux/constants'; @@ -10,6 +10,7 @@ import ChannelLoader from 'app/components/channel_loader'; export default class LoadTeam extends PureComponent { static propTypes = { + navigator: PropTypes.object, notification: PropTypes.object, teams: PropTypes.object.isRequired, myMembers: PropTypes.object.isRequired, @@ -17,26 +18,25 @@ export default class LoadTeam extends PureComponent { currentTeam: PropTypes.object, actions: PropTypes.shape({ clearNotification: PropTypes.func.isRequired, - goToChannelView: PropTypes.func.isRequired, goToNotification: PropTypes.func.isRequired, - handleTeamChange: PropTypes.func.isRequired + handleTeamChange: PropTypes.func.isRequired, + initialize: PropTypes.func.isRequired }).isRequired, theme: PropTypes.object.isRequired }; - static navigationProps = { - renderBackButton: () => null, - renderTitleComponent: () => null - }; - componentDidMount() { - const {notification, currentTeam, myMembers, teams} = this.props; - const {clearNotification, goToNotification} = this.props.actions; + const {currentTeam, myMembers, teams} = this.props; - if (notification) { - clearNotification(); - goToNotification(notification); - } else if (currentTeam) { + // TODO: GO TO CHANNEL WHEN NOTIFICATION IS TAPPED AND THE APP IS CLOSED + // const {notification, currentTeam, myMembers, teams} = this.props; + // const {clearNotification, goToNotification} = this.props.actions; + + // if (notification) { + // clearNotification(); + // goToNotification(notification); + // this.goToChannelView(); + if (currentTeam) { this.onSelectTeam(currentTeam); } else if (!currentTeam) { this.selectFirstTeam(teams, myMembers); @@ -60,13 +60,30 @@ export default class LoadTeam extends PureComponent { } onSelectTeam(team) { - const {handleTeamChange, goToChannelView} = this.props.actions; - handleTeamChange(team).then(goToChannelView); + const {handleTeamChange} = this.props.actions; + handleTeamChange(team).then(this.goToChannelView); } + goToChannelView = () => { + const {actions, navigator, theme} = this.props; + + actions.initialize(); + navigator.resetTo({ + screen: 'Channel', + animated: false, + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + render() { return ( + ); diff --git a/app/scenes/login/index.js b/app/screens/login/index.js similarity index 69% rename from app/scenes/login/index.js rename to app/screens/login/index.js index 9221ebd16..051b62003 100644 --- a/app/scenes/login/index.js +++ b/app/screens/login/index.js @@ -2,24 +2,26 @@ // See License.txt for license information. import {bindActionCreators} from 'redux'; - -import navigationSceneConnect from '../navigationSceneConnect'; +import {connect} from 'react-redux'; import LoginActions from 'app/actions/views/login'; -import {goToMfa, goToLoadTeam} from 'app/actions/navigation'; +import {getTheme} from 'app/selectors/preferences'; + import {checkMfa, login} from 'mattermost-redux/actions/users'; import Login from './login.js'; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { const {checkMfa: checkMfaRequest, login: loginRequest} = state.requests.users; const {config, license} = state.entities.general; return { + ...ownProps, ...state.views.login, checkMfaRequest, loginRequest, config, - license + license, + theme: getTheme(state) }; } @@ -28,11 +30,9 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators({ ...LoginActions, checkMfa, - login, - goToMfa, - goToLoadTeam + login }, dispatch) }; } -export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Login); +export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/app/scenes/login/login.js b/app/screens/login/login.js similarity index 70% rename from app/scenes/login/login.js rename to app/screens/login/login.js index aa5785837..5b5d85141 100644 --- a/app/scenes/login/login.js +++ b/app/screens/login/login.js @@ -1,14 +1,16 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component, PropTypes} from 'react'; +import React, {PureComponent, PropTypes} from 'react'; import {injectIntl, intlShape} from 'react-intl'; import { ActivityIndicator, Image, + InteractionManager, Keyboard, KeyboardAvoidingView, Platform, + StatusBar, Text, TextInput, TouchableWithoutFeedback, @@ -24,17 +26,17 @@ import logo from 'assets/images/logo.png'; import {RequestStatus} from 'mattermost-redux/constants'; -class Login extends Component { +class Login extends PureComponent { static propTypes = { intl: intlShape.isRequired, + navigator: PropTypes.object, + theme: PropTypes.object, actions: React.PropTypes.shape({ handleLoginIdChanged: React.PropTypes.func.isRequired, handlePasswordChanged: React.PropTypes.func.isRequired, handleSuccessfulLogin: React.PropTypes.func.isRequired, checkMfa: React.PropTypes.func.isRequired, - login: React.PropTypes.func.isRequired, - goToMfa: React.PropTypes.func.isRequired, - goToLoadTeam: React.PropTypes.func.isRequired + login: React.PropTypes.func.isRequired }).isRequired, config: PropTypes.object.isRequired, license: PropTypes.object.isRequired, @@ -60,7 +62,7 @@ class Login extends Component { componentWillReceiveProps(nextProps) { if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) { - this.props.actions.handleSuccessfulLogin().then(this.props.actions.goToLoadTeam); + this.props.actions.handleSuccessfulLogin().then(this.goToLoadTeam); } } @@ -70,6 +72,40 @@ class Login extends Component { } } + goToLoadTeam = () => { + const {navigator, theme} = this.props; + navigator.resetTo({ + screen: 'LoadTeam', + title: '', + animated: false, + backButtonTitle: '', + navigatorStyle: { + statusBarHidden: false, + statusBarHideWithNavBar: false, + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + + goToMfa = () => { + const {intl, navigator, theme} = this.props; + navigator.push({ + screen: 'MFA', + title: intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}), + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + handleAndroidKeyboard = () => { this.blur(); }; @@ -82,56 +118,62 @@ class Login extends Component { preSignIn = () => { this.setState({error: null}); Keyboard.dismiss(); - if (!this.props.loginId) { - // it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements - let msgId = 'login.no'; - if (this.props.config.EnableSignInWithEmail === 'true') { - msgId += 'Email'; - } - if (this.props.config.EnableSignInWithUsername === 'true') { - msgId += 'Username'; - } - if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') { - msgId += 'LdapUsername'; - } + InteractionManager.runAfterInteractions(() => { + if (!this.props.loginId) { + // it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements + let msgId = 'login.no'; + if (this.props.config.EnableSignInWithEmail === 'true') { + msgId += 'Email'; + } + if (this.props.config.EnableSignInWithUsername === 'true') { + msgId += 'Username'; + } + if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') { + msgId += 'LdapUsername'; + } - this.setState({ - error: { - intl: { - id: msgId, - defaultMessage: '', - values: { - ldapUsername: this.props.config.LdapLoginFieldName || - this.props.intl.formatMessage({id: 'login.ldapUsernameLower', defaultMessage: 'AD/LDAP username'}) + this.setState({ + error: { + intl: { + id: msgId, + defaultMessage: '', + values: { + ldapUsername: this.props.config.LdapLoginFieldName || + this.props.intl.formatMessage({ + id: 'login.ldapUsernameLower', + defaultMessage: 'AD/LDAP username' + }) + } } } - } - }); - return; - } + }); + return; + } - if (!this.props.password) { - this.setState({ - error: { - intl: { - id: 'login.noPassword', - defaultMessage: 'Please enter your password' + if (!this.props.password) { + this.setState({ + error: { + intl: { + id: 'login.noPassword', + defaultMessage: 'Please enter your password' + } } - } - }); - return; - } - if (this.props.config.EnableMultifactorAuthentication === 'true') { - this.props.actions.checkMfa(this.props.loginId).then((result) => { - if (result) { - this.props.actions.goToMfa(); - } else { - this.signIn(); - } - }); - } else { - this.signIn(); - } + }); + return; + } + + if (this.props.config.EnableMultifactorAuthentication === 'true') { + this.props.actions.checkMfa(this.props.loginId).then((result) => { + if (result) { + this.goToMfa(); + } else { + this.signIn(); + } + }); + } else { + this.signIn(); + } + }); }; signIn = () => { @@ -256,8 +298,9 @@ class Login extends Component { + { + const {intl, navigator, theme} = this.props; + navigator.push({ + screen: 'Login', + title: intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}), + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); + }; + + goToSaml = () => { + const {intl, navigator, theme} = this.props; + navigator.push({ + screen: 'SAML', + title: intl.formatMessage({id: 'mobile.routes.saml', defaultMessage: 'Single SignOn'}), + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + } + }); }; renderEmailOption = () => { - const config = this.props.config; + const {config} = this.props; if (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') { return (